mirror of
https://github.com/github/awesome-copilot.git
synced 2026-09-16 20:01:09 +00:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
228 lines
9.1 KiB
JavaScript
228 lines
9.1 KiB
JavaScript
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);
|
|
}
|