Files

168 lines
7.2 KiB
JavaScript

import { basename } from "node:path";
import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension";
import { createInstanceServer, pushState } from "./lib/server.mjs";
import {
configureWorkflow,
getWorkflowState,
refreshWorkflow,
resolveWorkspace,
setStepStatus,
} from "./lib/state.mjs";
const instances = new Map();
let sessionRef;
let workingDirectory = process.cwd();
function instance(ctx) {
const value = instances.get(ctx.instanceId);
if (!value) throw new CanvasError("not_open", "Open the Modernization Workflow canvas first.");
return value;
}
async function updateInstance(rec, operation) {
const state = await operation(rec.workspace, rec.stateId);
await pushState(rec, state);
return state;
}
const session = await joinSession({
canvases: [
createCanvas({
id: "modernization-workflow",
displayName: "Modernization Workflow",
description: "Track and guide a Modernize CLI Assess, Plan, and Execute workflow from real repository artifacts.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {
workspace: { type: "string", description: "Absolute local application repository path." },
stateId: {
type: "string",
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
description: "Stable workflow identifier used for persisted state.",
},
language: { type: "string", enum: ["auto", "dotnet", "java", "cpp"] },
source: { type: "string" },
goal: { type: "string" },
upgradeTarget: { type: "string" },
planName: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
},
},
actions: [
{
name: "get_workflow_state",
description: "Return the current workflow configuration, generated commands, artifacts, and step progress.",
handler: async (ctx) => {
const rec = instance(ctx);
return getWorkflowState(rec.workspace, rec.stateId);
},
},
{
name: "configure_workflow",
description: "Configure the source, language, modernization goal, and plan name.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {
language: { type: "string", enum: ["auto", "dotnet", "java", "cpp"] },
source: { type: "string", minLength: 1 },
goal: { type: "string", minLength: 1 },
upgradeTarget: { type: "string" },
planName: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
delegate: { type: "string", enum: ["local", "cloud"] },
},
},
handler: async (ctx) => {
const rec = instance(ctx);
return updateInstance(rec, (workspace, stateId) =>
configureWorkflow(workspace, stateId, ctx.input ?? {}));
},
},
{
name: "set_step_status",
description: "Set an Assess, Plan, or Execute step to pending, active, complete, or blocked.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["step", "status"],
properties: {
step: { type: "string", enum: ["assess", "plan", "execute"] },
status: { type: "string", enum: ["pending", "active", "complete", "blocked"] },
},
},
handler: async (ctx) => {
const rec = instance(ctx);
return updateInstance(rec, (workspace, stateId) =>
setStepStatus(workspace, stateId, ctx.input.step, ctx.input.status));
},
},
{
name: "refresh_from_artifacts",
description: "Re-scan the repository and update progress from Modernize CLI output artifacts.",
handler: async (ctx) => {
const rec = instance(ctx);
return updateInstance(rec, refreshWorkflow);
},
},
],
open: async (ctx) => {
const workspace = resolveWorkspace(ctx.input?.workspace, workingDirectory);
const stateId = ctx.input?.stateId ?? "default";
let rec = instances.get(ctx.instanceId);
if (rec && (rec.workspace !== workspace || rec.stateId !== stateId)) {
for (const client of rec.clients) client.end();
await new Promise((resolve) => rec.server.close(resolve));
instances.delete(ctx.instanceId);
rec = null;
}
if (!rec) {
rec = await createInstanceServer({
workspace,
stateId,
initialConfig: ctx.input ?? {},
onConfigure: (input) => configureWorkflow(workspace, stateId, input),
onSetStep: (step, status) => setStepStatus(workspace, stateId, step, status),
onRefresh: () => refreshWorkflow(workspace, stateId),
});
instances.set(ctx.instanceId, rec);
}
return {
title: "Modernization Workflow",
status: basename(workspace),
url: rec.url,
};
},
onClose: async (ctx) => {
const rec = instances.get(ctx.instanceId);
if (!rec) return;
instances.delete(ctx.instanceId);
for (const client of rec.clients) client.end();
await new Promise((resolve) => rec.server.close(resolve));
},
}),
],
hooks: {
onSessionStart: async (input) => {
if (input.workingDirectory) workingDirectory = input.workingDirectory;
return {
additionalContext:
"When a user is modernizing an application, use the Modernization Workflow canvas to make Assess, Plan, and Execute progress visible. Refresh it after Modernize CLI commands change repository artifacts.",
};
},
},
});
sessionRef = session;
session.on("session.idle", async () => {
for (const rec of instances.values()) {
const state = await refreshWorkflow(rec.workspace, rec.stateId);
await pushState(rec, state);
}
});
await session.log("Modernization Workflow canvas ready", { ephemeral: true });