diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 3a4502efc..725f0a85c 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -992,6 +992,12 @@ "ref": "v0.0.179" } }, + { + "name": "modernization-workflow", + "source": "plugins/modernization-workflow", + "description": "Visualize and guide the GitHub Copilot Modernize CLI Assess, Plan, and Execute workflow for .NET, Java, and C++ application repositories.", + "version": "1.0.0" + }, { "name": "modernize-dotnet", "description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.", diff --git a/docs/README.plugins.md b/docs/README.plugins.md index ba76c8438..3c611385d 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -72,6 +72,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [java-modernization-studio](../plugins/java-modernization-studio/README.md) | Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts. | 1 items | app-modernization, assessment-dashboard, azure-migration, java-modernization, legacy-java, modernization-cockpit, validation-gates | | [kotlin-mcp-development](../plugins/kotlin-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Kotlin using the official io.modelcontextprotocol:kotlin-sdk library. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | kotlin, mcp, model-context-protocol, kotlin-multiplatform, server-development, ktor | | [mcp-m365-copilot](../plugins/mcp-m365-copilot/README.md) | Comprehensive collection for building declarative agents with Model Context Protocol integration for Microsoft 365 Copilot | 4 items | mcp, m365-copilot, declarative-agents, api-plugins, model-context-protocol, adaptive-cards | +| [modernization-workflow](../plugins/modernization-workflow/README.md) | Visualize and guide the GitHub Copilot Modernize CLI Assess, Plan, and Execute workflow for .NET, Java, and C++ application repositories. | 1 items | app-modernization, assessment-dashboard, azure-migration, cpp-modernization, dotnet-modernization, java-modernization, modernization-canvas, modernize-cli | | [napkin](../plugins/napkin/README.md) | Visual whiteboard collaboration for Copilot CLI. Opens an interactive whiteboard in your browser where you can draw, sketch, and add sticky notes — then share everything back with Copilot. Copilot sees your drawings and responds with analysis, suggestions, and ideas. | 1 items | whiteboard, visual, collaboration, brainstorming, non-technical, drawing, sticky-notes, accessibility, copilot-cli, ux | | [noob-mode](../plugins/noob-mode/README.md) | Plain-English translation layer for non-technical Copilot CLI users. Translates every approval prompt, error message, and technical output into clear, jargon-free English with color-coded risk indicators. | 1 items | accessibility, plain-english, non-technical, beginner, translation, copilot-cli, ux | | [openapi-to-application-csharp-dotnet](../plugins/openapi-to-application-csharp-dotnet/README.md) | Generate production-ready .NET applications from OpenAPI specifications. Includes ASP.NET Core project scaffolding, controller generation, entity framework integration, and C# best practices. | 2 items | openapi, code-generation, api, csharp, dotnet, aspnet | diff --git a/extensions/modernization-workflow/LICENSE b/extensions/modernization-workflow/LICENSE new file mode 100644 index 000000000..ed9f9a40c --- /dev/null +++ b/extensions/modernization-workflow/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 EMEA App GBB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/modernization-workflow/README.md b/extensions/modernization-workflow/README.md new file mode 100644 index 000000000..d457a6dc6 --- /dev/null +++ b/extensions/modernization-workflow/README.md @@ -0,0 +1,26 @@ +# Modernization Workflow canvas + +An interactive GitHub Copilot CLI canvas that visualizes the documented +Modernize CLI lifecycle: + +1. **Assess** the application and generate evidence. +2. **Plan** a reviewable modernization strategy and task list. +3. **Execute** transformations with build, security, and result validation. + +The canvas detects .NET, Java, and C++ repository signals, generates commands +from the current workflow configuration, and refreshes progress from artifacts +under `.github/modernize/`. + +## Canvas ID + +`modernization-workflow` + +## Agent actions + +- `get_workflow_state` +- `configure_workflow` +- `set_step_status` +- `refresh_from_artifacts` + +Workflow state is stored in `.github/modernize/canvas-.json` in the +application repository so progress survives canvas and CLI restarts. diff --git a/extensions/modernization-workflow/assets/preview.png b/extensions/modernization-workflow/assets/preview.png new file mode 100644 index 000000000..27ac2e9ee Binary files /dev/null and b/extensions/modernization-workflow/assets/preview.png differ diff --git a/extensions/modernization-workflow/copilot-extension.json b/extensions/modernization-workflow/copilot-extension.json new file mode 100644 index 000000000..7feff9342 --- /dev/null +++ b/extensions/modernization-workflow/copilot-extension.json @@ -0,0 +1,4 @@ +{ + "name": "modernization-workflow", + "version": 1 +} diff --git a/extensions/modernization-workflow/extension.mjs b/extensions/modernization-workflow/extension.mjs new file mode 100644 index 000000000..d9351ec84 --- /dev/null +++ b/extensions/modernization-workflow/extension.mjs @@ -0,0 +1,167 @@ +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 }); diff --git a/extensions/modernization-workflow/lib/renderer.mjs b/extensions/modernization-workflow/lib/renderer.mjs new file mode 100644 index 000000000..a6de32a58 --- /dev/null +++ b/extensions/modernization-workflow/lib/renderer.mjs @@ -0,0 +1,190 @@ +function serialize(value) { + return JSON.stringify(value).replaceAll("<", "\\u003c"); +} + +export function renderHtml(initialState, token) { + return ` + + + + +Modernization Workflow + + + +
+
+
+
+
GitHub Copilot modernization
+

Modernization Workflow

+

Move from evidence to an approved plan and validated code changes with the Modernize CLI.

+
+
0%
+
+
+
+
+
+
+

Assess → Plan → Execute

+
+
+
+

Upgrade fast path

Run the documented end-to-end plan-and-execute workflow when a separate plan review is not required.

+
+
+
+
+
+
+

Workflow setup

+
+
+
+ + + + + + +
+
+
+
+
+
+

Repository signal

+
+
+
+

Detected artifacts

+
    +
    +
    +

    Reference

    + +
    +
    +
    +
    +
    +
    + + +`; +} diff --git a/extensions/modernization-workflow/lib/server.mjs b/extensions/modernization-workflow/lib/server.mjs new file mode 100644 index 000000000..ad3314036 --- /dev/null +++ b/extensions/modernization-workflow/lib/server.mjs @@ -0,0 +1,104 @@ +import { randomBytes } from "node:crypto"; +import { createServer } from "node:http"; +import { getWorkflowState } from "./state.mjs"; +import { renderHtml } from "./renderer.mjs"; + +function sendJson(res, status, value) { + res.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + }); + res.end(JSON.stringify(value)); +} + +async function readJson(req) { + const chunks = []; + let size = 0; + for await (const chunk of req) { + size += chunk.length; + if (size > 64 * 1024) throw new Error("Request body is too large."); + chunks.push(chunk); + } + return chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {}; +} + +export async function pushState(rec, state) { + const payload = `event: state\ndata: ${JSON.stringify(state)}\n\n`; + for (const client of rec.clients) client.write(payload); +} + +export async function createInstanceServer(options) { + const token = randomBytes(24).toString("hex"); + const clients = new Set(); + let rec; + + const server = createServer(async (req, res) => { + const url = new URL(req.url, "http://127.0.0.1"); + try { + if (req.method === "GET" && url.pathname === "/") { + const state = await getWorkflowState(options.workspace, options.stateId); + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Content-Security-Policy": "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + }); + res.end(renderHtml(state, token)); + return; + } + + if (req.method === "GET" && url.pathname === "/events") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + clients.add(res); + req.on("close", () => clients.delete(res)); + return; + } + + if (req.method !== "POST" || req.headers["x-canvas-token"] !== token) { + sendJson(res, 403, { error: "Request rejected." }); + return; + } + + const input = await readJson(req); + let state; + if (url.pathname === "/api/configure") { + state = await options.onConfigure(input); + } else if (url.pathname === "/api/step") { + state = await options.onSetStep(input.step, input.status); + } else if (url.pathname === "/api/refresh") { + state = await options.onRefresh(); + } else { + sendJson(res, 404, { error: "Not found." }); + return; + } + + await pushState(rec, state); + sendJson(res, 200, state); + } catch (error) { + sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) }); + } + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + rec = { + ...options, + server, + clients, + url: `http://127.0.0.1:${address.port}/`, + }; + + if (Object.keys(options.initialConfig).some((key) => + ["language", "source", "goal", "planName"].includes(key))) { + await options.onConfigure(options.initialConfig); + } + return rec; +} diff --git a/extensions/modernization-workflow/lib/state.mjs b/extensions/modernization-workflow/lib/state.mjs new file mode 100644 index 000000000..c89ef2b77 --- /dev/null +++ b/extensions/modernization-workflow/lib/state.mjs @@ -0,0 +1,227 @@ +import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +const DEFAULT_STATE = { + version: 1, + language: "auto", + source: ".", + goal: "modernize this application for Azure", + upgradeTarget: "", + planName: "modernization-plan", + delegate: "local", + steps: { + assess: { status: "pending", updatedAt: null }, + plan: { status: "pending", updatedAt: null }, + execute: { status: "pending", updatedAt: null }, + }, +}; + +function statePath(workspace, stateId) { + return join(workspace, ".github", "modernize", `canvas-${stateId}.json`); +} + +function cleanStateId(value) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value)) { + throw new Error("Invalid workflow state identifier."); + } + return value; +} + +export function resolveWorkspace(input, fallback) { + const candidate = input || fallback || process.cwd(); + return resolve(candidate); +} + +async function exists(path) { + try { + await access(path); + return true; + } catch { + return false; + } +} + +async function loadState(workspace, stateId) { + const path = statePath(workspace, cleanStateId(stateId)); + try { + const saved = JSON.parse(await readFile(path, "utf8")); + return { + ...DEFAULT_STATE, + ...saved, + steps: { + ...DEFAULT_STATE.steps, + ...(saved.steps ?? {}), + }, + }; + } catch (error) { + if (error.code === "ENOENT") return structuredClone(DEFAULT_STATE); + throw error; + } +} + +async function saveState(workspace, stateId, state) { + const path = statePath(workspace, cleanStateId(stateId)); + await mkdir(join(workspace, ".github", "modernize"), { recursive: true }); + const next = { ...state, updatedAt: new Date().toISOString() }; + await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8"); + return next; +} + +async function walkNames(root, maxDepth = 3, depth = 0) { + if (depth > maxDepth || !(await exists(root))) return []; + const output = []; + for (const entry of await readdir(root, { withFileTypes: true })) { + if ([".git", "node_modules", "bin", "obj", "target", "build"].includes(entry.name)) continue; + const path = join(root, entry.name); + output.push(path); + if (entry.isDirectory()) output.push(...await walkNames(path, maxDepth, depth + 1)); + } + return output; +} + +function detectLanguage(paths) { + const names = paths.map((path) => path.toLowerCase()); + if (names.some((path) => /\.(sln|csproj|fsproj|vbproj)$/.test(path))) return "dotnet"; + if (names.some((path) => /\/(pom\.xml|build\.gradle|build\.gradle\.kts)$/.test(path.replaceAll("\\", "/")))) return "java"; + if (names.some((path) => /\/(cmakelists\.txt|makefile)$/.test(path.replaceAll("\\", "/")) || /\.(cpp|cc|cxx|vcxproj)$/.test(path))) return "cpp"; + return "unknown"; +} + +async function scanArtifacts(workspace, planName) { + const modernizeRoot = join(workspace, ".github", "modernize"); + const assessmentRoot = join(modernizeRoot, "assessment"); + const planRoot = join(modernizeRoot, planName); + const assessmentFiles = (await walkNames(assessmentRoot, 3)) + .filter((path) => /\.(json|md|html|ya?ml)$/i.test(path)); + const planFiles = (await walkNames(planRoot, 2)) + .filter((path) => /\.(json|md|html)$/i.test(path)); + const assessmentOutputs = assessmentFiles.filter((path) => + !/config|canvas-/i.test(path) && /\.(json|md|html)$/i.test(path)); + const summaryFiles = planFiles.filter((path) => /summary|progress|result/i.test(path)); + + return { + assessment: assessmentFiles.map((path) => path.slice(workspace.length + 1)), + plan: planFiles.map((path) => path.slice(workspace.length + 1)), + execution: summaryFiles.map((path) => path.slice(workspace.length + 1)), + hasAssessment: assessmentOutputs.length > 0, + hasPlan: planFiles.some((path) => /plan\.md$/i.test(path)) + && planFiles.some((path) => /tasks\.json$/i.test(path)), + hasExecution: summaryFiles.length > 0, + }; +} + +function quote(value) { + const text = String(value).trim(); + if (/^[A-Za-z0-9_./:\\-]+$/.test(text)) return text; + return `"${text.replaceAll('"', '\\"')}"`; +} + +function languageFlag(language) { + if (language === "java" || language === "dotnet") return ` --language ${language}`; + return ""; +} + +function delegateFlag(delegate) { + return delegate === "cloud" ? " --delegate cloud" : ""; +} + +function commands(state, artifacts) { + const source = quote(state.source); + const name = quote(state.planName); + const goal = quote(state.goal); + const upgradeTarget = state.upgradeTarget ? ` ${quote(state.upgradeTarget)}` : ""; + const language = state.language === "auto" ? state.detectedLanguage : state.language; + const assessment = artifacts.assessment.find((path) => /assessment.*\.json$/i.test(path)) + ?? artifacts.assessment.find((path) => /\.json$/i.test(path)); + const assessmentFlag = assessment ? ` --assess-file-path ${quote(assessment)}` : ""; + + return { + assess: `modernize assess --source ${source}${delegateFlag(state.delegate)}${state.delegate === "cloud" ? " --wait" : ""}`, + plan: `modernize plan create ${goal} --source ${source} --plan-name ${name}${languageFlag(language)}${assessmentFlag}`, + execute: `modernize plan execute --source ${source} --plan-name ${name}${languageFlag(language)}${delegateFlag(state.delegate)}`, + upgrade: `modernize upgrade${upgradeTarget} --source ${source}${delegateFlag(state.delegate)}`, + }; +} + +function deriveSteps(state, artifacts) { + const steps = structuredClone(state.steps); + if (artifacts.hasAssessment) steps.assess.status = "complete"; + if (artifacts.hasPlan) { + steps.assess.status = "complete"; + steps.plan.status = "complete"; + } + if (artifacts.hasExecution) { + steps.assess.status = "complete"; + steps.plan.status = "complete"; + steps.execute.status = "complete"; + } + return steps; +} + +export async function getWorkflowState(workspace, stateId = "default") { + const saved = await loadState(workspace, stateId); + const paths = await walkNames(workspace, 3); + const detectedLanguage = detectLanguage(paths); + const artifacts = await scanArtifacts(workspace, saved.planName); + const state = { + ...saved, + stateId, + workspace, + detectedLanguage, + effectiveLanguage: saved.language === "auto" ? detectedLanguage : saved.language, + artifacts, + steps: deriveSteps(saved, artifacts), + }; + const notices = []; + if (state.effectiveLanguage === "cpp") { + notices.push("The current CLI reference documents explicit --language values for Java and .NET only. C++ commands use CLI auto-detection; verify support with your installed Modernize CLI version."); + } + if (state.delegate === "cloud" && !/^https:\/\/github\.com\//i.test(state.source)) { + notices.push("Cloud delegation requires a github.com repository URL as the source. Local paths and other Git providers are not supported."); + } + return { + ...state, + commands: commands(state, artifacts), + compatibilityNotice: notices.length ? notices.join(" ") : null, + }; +} + +export async function configureWorkflow(workspace, stateId, input) { + const current = await loadState(workspace, stateId); + const allowed = ["language", "source", "goal", "upgradeTarget", "planName", "delegate"]; + const next = { ...current }; + for (const key of allowed) { + if (input[key] !== undefined) next[key] = String(input[key]).trim(); + } + if (!["auto", "dotnet", "java", "cpp"].includes(next.language)) { + throw new Error("Language must be auto, dotnet, java, or cpp."); + } + if (!["local", "cloud"].includes(next.delegate)) { + throw new Error("Execution must be local or cloud."); + } + if (!next.source || !next.goal) { + throw new Error("Source and modernization goal are required."); + } + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(next.planName)) { + throw new Error("Plan name must contain only letters, numbers, dots, underscores, and hyphens."); + } + await saveState(workspace, stateId, next); + return getWorkflowState(workspace, stateId); +} + +export async function setStepStatus(workspace, stateId, step, status) { + if (!["assess", "plan", "execute"].includes(step)) throw new Error("Unknown workflow step."); + if (!["pending", "active", "complete", "blocked"].includes(status)) throw new Error("Unknown step status."); + const current = await loadState(workspace, stateId); + current.steps[step] = { status, updatedAt: new Date().toISOString() }; + await saveState(workspace, stateId, current); + return getWorkflowState(workspace, stateId); +} + +export async function refreshWorkflow(workspace, stateId = "default") { + const current = await loadState(workspace, stateId); + const refreshed = await getWorkflowState(workspace, stateId); + current.steps = refreshed.steps; + await saveState(workspace, stateId, current); + return getWorkflowState(workspace, stateId); +} diff --git a/plugins/modernization-workflow/README.md b/plugins/modernization-workflow/README.md new file mode 100644 index 000000000..f7f67f8d1 --- /dev/null +++ b/plugins/modernization-workflow/README.md @@ -0,0 +1,20 @@ +# Modernization Workflow Plugin + +Interactive canvas for visualizing and guiding the GitHub Copilot Modernize CLI +Assess, Plan, and Execute workflow for .NET, Java, and C++ application +repositories. + +## Installation + +```bash +copilot plugin install modernization-workflow@awesome-copilot +``` + +## Source + +This plugin is part of +[Awesome Copilot](https://github.com/github/awesome-copilot). + +## License + +MIT diff --git a/plugins/modernization-workflow/plugin.json b/plugins/modernization-workflow/plugin.json new file mode 100644 index 000000000..dea6a8f37 --- /dev/null +++ b/plugins/modernization-workflow/plugin.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "modernization-workflow", + "description": "Visualize and guide the GitHub Copilot Modernize CLI Assess, Plan, and Execute workflow for .NET, Java, and C++ application repositories.", + "version": "1.0.0", + "author": { + "name": "EMEA App GBB", + "url": "https://github.com/EmeaAppGbb" + }, + "homepage": "https://github.com/EmeaAppGbb/modernization-canvas", + "repository": "https://github.com/EmeaAppGbb/modernization-canvas", + "license": "MIT", + "keywords": [ + "app-modernization", + "assessment-dashboard", + "azure-migration", + "cpp-modernization", + "dotnet-modernization", + "java-modernization", + "modernization-canvas", + "modernize-cli" + ], + "extensions": { + "com.github.copilot": { + "logo": "assets/preview.png" + }, + "com.github.awesome-copilot": { + "extensions": [ + "./extensions/modernization-workflow" + ] + } + } +}