mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-21 21:04:13 +00:00
Merge branch 'main' into modernize/1.22.0-1
This commit is contained in:
+4
-2
@@ -45,7 +45,9 @@
|
|||||||
# Wee, Sherif - proper name (Wee, Sherif, contributor names should not be flagged as typos)
|
# Wee, Sherif - proper name (Wee, Sherif, contributor names should not be flagged as typos)
|
||||||
|
|
||||||
# queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names
|
# queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names
|
||||||
|
|
||||||
|
# extenions - intentional misspelled key name used in plugin validators to detect invalid manifests
|
||||||
|
|
||||||
# nin - MongoDB $nin operator in security instructions NoSQL injection detection regex
|
# nin - MongoDB $nin operator in security instructions NoSQL injection detection regex
|
||||||
|
|
||||||
# Vertexes - FreeCAD shape sub-elements used as property of obj.Shape
|
# Vertexes - FreeCAD shape sub-elements used as property of obj.Shape
|
||||||
@@ -60,7 +62,7 @@
|
|||||||
|
|
||||||
# Vally/vally - Name of product
|
# Vally/vally - Name of product
|
||||||
|
|
||||||
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally
|
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,extenions,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally
|
||||||
|
|
||||||
# Skip certain files and directories
|
# Skip certain files and directories
|
||||||
# *.tm7 - MTM DataContract exports; embedded base64 icon blobs trigger false positives
|
# *.tm7 - MTM DataContract exports; embedded base64 icon blobs trigger false positives
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"name": "accessibility-kanban",
|
"name": "accessibility-kanban",
|
||||||
"source": "extensions/accessibility-kanban",
|
"source": "extensions/accessibility-kanban",
|
||||||
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "acreadiness-cockpit",
|
"name": "acreadiness-cockpit",
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
"name": "apng-studio",
|
"name": "apng-studio",
|
||||||
"source": "extensions/apng-studio",
|
"source": "extensions/apng-studio",
|
||||||
"description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.",
|
"description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "arcade-canvas",
|
"name": "arcade-canvas",
|
||||||
"source": "extensions/arcade-canvas",
|
"source": "extensions/arcade-canvas",
|
||||||
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
|
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "arch",
|
"name": "arch",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"name": "backlog-swipe-triage",
|
"name": "backlog-swipe-triage",
|
||||||
"source": "extensions/backlog-swipe-triage",
|
"source": "extensions/backlog-swipe-triage",
|
||||||
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "cast-imaging",
|
"name": "cast-imaging",
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
"name": "chromium-control-canvas",
|
"name": "chromium-control-canvas",
|
||||||
"source": "extensions/chromium-control-canvas",
|
"source": "extensions/chromium-control-canvas",
|
||||||
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "clojure-interactive-programming",
|
"name": "clojure-interactive-programming",
|
||||||
@@ -239,13 +239,13 @@
|
|||||||
"name": "color-orb",
|
"name": "color-orb",
|
||||||
"source": "extensions/color-orb",
|
"source": "extensions/color-orb",
|
||||||
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "connector-namespaces",
|
"name": "connector-namespaces",
|
||||||
"source": "extensions/connector-namespaces",
|
"source": "extensions/connector-namespaces",
|
||||||
"description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.",
|
"description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.",
|
||||||
"version": "1.1.0"
|
"version": "1.1.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "context-engineering",
|
"name": "context-engineering",
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
"name": "diagram-viewer",
|
"name": "diagram-viewer",
|
||||||
"source": "extensions/diagram-viewer",
|
"source": "extensions/diagram-viewer",
|
||||||
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dotnet",
|
"name": "dotnet",
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
"name": "feedback-themes",
|
"name": "feedback-themes",
|
||||||
"source": "extensions/feedback-themes",
|
"source": "extensions/feedback-themes",
|
||||||
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "figma",
|
"name": "figma",
|
||||||
@@ -553,7 +553,7 @@
|
|||||||
{
|
{
|
||||||
"name": "foundry-agent-canvas",
|
"name": "foundry-agent-canvas",
|
||||||
"description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.",
|
"description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"url": "https://www.microsoft.com"
|
"url": "https://www.microsoft.com"
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
"source": "github",
|
"source": "github",
|
||||||
"repo": "microsoft/foundry-toolkit",
|
"repo": "microsoft/foundry-toolkit",
|
||||||
"path": "foundry-agent-canvas",
|
"path": "foundry-agent-canvas",
|
||||||
"sha": "0d311e56508ac200ebb86586adbee9c7b8b0601f"
|
"sha": "e16be2b8533ca82c22806388e07581e7497785d7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
"name": "gesture-review",
|
"name": "gesture-review",
|
||||||
"source": "extensions/gesture-review",
|
"source": "extensions/gesture-review",
|
||||||
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gh-skills-builder",
|
"name": "gh-skills-builder",
|
||||||
@@ -701,7 +701,7 @@
|
|||||||
"name": "java-modernization-studio",
|
"name": "java-modernization-studio",
|
||||||
"source": "extensions/java-modernization-studio",
|
"source": "extensions/java-modernization-studio",
|
||||||
"description": "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.",
|
"description": "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.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kotlin-mcp-development",
|
"name": "kotlin-mcp-development",
|
||||||
@@ -770,7 +770,7 @@
|
|||||||
{
|
{
|
||||||
"name": "modernize-dotnet",
|
"name": "modernize-dotnet",
|
||||||
"description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.",
|
"description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.",
|
||||||
"version": "1.0.1157-preview1",
|
"version": "1.0.1161-preview1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"url": "https://www.microsoft.com"
|
"url": "https://www.microsoft.com"
|
||||||
@@ -787,7 +787,8 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"repo": "dotnet/modernize-dotnet",
|
"repo": "dotnet/modernize-dotnet",
|
||||||
"path": "plugins/modernize-dotnet"
|
"path": "plugins/modernize-dotnet",
|
||||||
|
"sha": "ce4ec09678498da38099ca9169c1624c59c2a89a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -979,13 +980,13 @@
|
|||||||
"name": "release-notes-showcase",
|
"name": "release-notes-showcase",
|
||||||
"source": "extensions/release-notes-showcase",
|
"source": "extensions/release-notes-showcase",
|
||||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "repo-actions-hub",
|
"name": "repo-actions-hub",
|
||||||
"source": "extensions/repo-actions-hub",
|
"source": "extensions/repo-actions-hub",
|
||||||
"description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.",
|
"description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "roundup",
|
"name": "roundup",
|
||||||
@@ -1027,7 +1028,7 @@
|
|||||||
"name": "site-studio",
|
"name": "site-studio",
|
||||||
"source": "extensions/site-studio",
|
"source": "extensions/site-studio",
|
||||||
"description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.",
|
"description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "skill-image-gen",
|
"name": "skill-image-gen",
|
||||||
@@ -1117,13 +1118,13 @@
|
|||||||
"name": "tiny-tool-town-submitter",
|
"name": "tiny-tool-town-submitter",
|
||||||
"source": "extensions/tiny-tool-town-submitter",
|
"source": "extensions/tiny-tool-town-submitter",
|
||||||
"description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.",
|
"description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "token-pacman",
|
"name": "token-pacman",
|
||||||
"source": "extensions/token-pacman",
|
"source": "extensions/token-pacman",
|
||||||
"description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.",
|
"description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "typescript-mcp-development",
|
"name": "typescript-mcp-development",
|
||||||
@@ -1246,6 +1247,30 @@
|
|||||||
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
|
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "upgrade-agent",
|
||||||
|
"description": "GitHub Copilot upgrade is an AI-powered agent that helps you upgrade applications to newer versions of languages, frameworks, and runtimes. It assesses your application, creates an upgrade plan, applies code changes, and validates the results through an interactive upgrade workflow.",
|
||||||
|
"version": "1.1.222",
|
||||||
|
"author": {
|
||||||
|
"name": "Microsoft",
|
||||||
|
"url": "https://www.microsoft.com"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/microsoft/upgrade-agent-plugins",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"modernization",
|
||||||
|
"upgrade",
|
||||||
|
"migration",
|
||||||
|
"dotnet",
|
||||||
|
"canvas"
|
||||||
|
],
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "microsoft/upgrade-agent-plugins",
|
||||||
|
"path": "plugins/upgrade-agent",
|
||||||
|
"sha": "379d344e42823b25223f878c002f38fb3a2c1d2b"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "vercel-plugin",
|
"name": "vercel-plugin",
|
||||||
"description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.",
|
"description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.",
|
||||||
@@ -1310,7 +1335,7 @@
|
|||||||
"name": "where-was-i",
|
"name": "where-was-i",
|
||||||
"source": "extensions/where-was-i",
|
"source": "extensions/where-was-i",
|
||||||
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "winappcli",
|
"name": "winappcli",
|
||||||
@@ -1374,7 +1399,7 @@
|
|||||||
"name": "work-hub",
|
"name": "work-hub",
|
||||||
"source": "extensions/work-hub",
|
"source": "extensions/work-hub",
|
||||||
"description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.",
|
"description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.",
|
||||||
"version": "1.0.0"
|
"version": "1.0.2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,6 +753,7 @@ jobs:
|
|||||||
vally_lint_status: 'infra_error',
|
vally_lint_status: 'infra_error',
|
||||||
smoke_status: 'infra_error',
|
smoke_status: 'infra_error',
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.',
|
summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.',
|
||||||
};
|
};
|
||||||
@@ -764,6 +765,7 @@ jobs:
|
|||||||
vally_lint_status: 'infra_error',
|
vally_lint_status: 'infra_error',
|
||||||
smoke_status: 'infra_error',
|
smoke_status: 'infra_error',
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
summary: 'Quality-gate workflow did not return results. Re-run intake to retry.',
|
summary: 'Quality-gate workflow did not return results. Re-run intake to retry.',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ jobs:
|
|||||||
vally_lint_status: 'infra_error',
|
vally_lint_status: 'infra_error',
|
||||||
smoke_status: 'infra_error',
|
smoke_status: 'infra_error',
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.',
|
summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.',
|
||||||
};
|
};
|
||||||
@@ -127,6 +128,7 @@ jobs:
|
|||||||
vally_lint_status: 'infra_error',
|
vally_lint_status: 'infra_error',
|
||||||
smoke_status: 'infra_error',
|
smoke_status: 'infra_error',
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
summary: 'Quality-gate workflow did not return results. Re-run intake to retry.',
|
summary: 'Quality-gate workflow did not return results. Re-run intake to retry.',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ jobs:
|
|||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
checked_plugins: [],
|
checked_plugins: [],
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.',
|
summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.',
|
||||||
};
|
};
|
||||||
} else if (shouldRun) {
|
} else if (shouldRun) {
|
||||||
@@ -167,6 +168,7 @@ jobs:
|
|||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
checked_plugins: [],
|
checked_plugins: [],
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.',
|
summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.',
|
||||||
};
|
};
|
||||||
} else if (process.env.QUALITY_RESULT_JSON) {
|
} else if (process.env.QUALITY_RESULT_JSON) {
|
||||||
@@ -177,6 +179,7 @@ jobs:
|
|||||||
failure_class: 'infra',
|
failure_class: 'infra',
|
||||||
checked_plugins: [],
|
checked_plugins: [],
|
||||||
version_match_status: 'infra_error',
|
version_match_status: 'infra_error',
|
||||||
|
canvas_structure_status: 'infra_error',
|
||||||
summary: 'External plugin PR quality checks did not return a result payload.',
|
summary: 'External plugin PR quality checks did not return a result payload.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -245,15 +248,16 @@ jobs:
|
|||||||
const sourceUrl = String(entry?.source_tree_url || '');
|
const sourceUrl = String(entry?.source_tree_url || '');
|
||||||
const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository');
|
const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository');
|
||||||
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
|
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
|
||||||
return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`;
|
return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.canvas_structure_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`;
|
||||||
})
|
})
|
||||||
: ['| _none_ | not_run | not_run | not_run | not_run | _n/a_ |'];
|
: ['| _none_ | not_run | not_run | not_run | not_run | not_run | _n/a_ |'];
|
||||||
const failureDetails = checkedPlugins.flatMap((entry) => {
|
const failureDetails = checkedPlugins.flatMap((entry) => {
|
||||||
const name = String(entry?.name || 'unknown');
|
const name = String(entry?.name || 'unknown');
|
||||||
const quality = entry?.quality || {};
|
const quality = entry?.quality || {};
|
||||||
const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0;
|
const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0;
|
||||||
const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0;
|
const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0;
|
||||||
const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0;
|
const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0;
|
||||||
|
const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0;
|
||||||
|
|
||||||
const details = [];
|
const details = [];
|
||||||
if (shouldShowVally) {
|
if (shouldShowVally) {
|
||||||
@@ -265,6 +269,9 @@ jobs:
|
|||||||
if (shouldShowVersionMatch) {
|
if (shouldShowVersionMatch) {
|
||||||
details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output));
|
details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output));
|
||||||
}
|
}
|
||||||
|
if (shouldShowCanvasStructure) {
|
||||||
|
details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output));
|
||||||
|
}
|
||||||
return details;
|
return details;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,8 +284,8 @@ jobs:
|
|||||||
'',
|
'',
|
||||||
'### Per-plugin quality summary',
|
'### Per-plugin quality summary',
|
||||||
'',
|
'',
|
||||||
'| Plugin | vally lint | install smoke test | version match | overall | source tree |',
|
'| Plugin | vally lint | install smoke test | version match | canvas structure | overall | source tree |',
|
||||||
'|---|---|---|---|---|---|',
|
'|---|---|---|---|---|---|---|',
|
||||||
...rows,
|
...rows,
|
||||||
'',
|
'',
|
||||||
...(failureDetails.length > 0
|
...(failureDetails.length > 0
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
|
|||||||
- Extension `plugin.json` **must** follow the convention:
|
- Extension `plugin.json` **must** follow the convention:
|
||||||
- `name`, `description`, `version` are required
|
- `name`, `description`, `version` are required
|
||||||
- `logo` **must** be exactly `"assets/preview.png"` (enforced convention)
|
- `logo` **must** be exactly `"assets/preview.png"` (enforced convention)
|
||||||
- `extensions` **must** be exactly `"."` (per [copilot-agent-runtime#9929](https://github.com/github/copilot-agent-runtime/pull/9929))
|
- `extensions` **must** be exactly `"."` in source manifests (materialization rewrites this to `"extensions"` for distribution output)
|
||||||
- Optional: `author`, `keywords` fields
|
- Optional: `author`, `keywords` fields
|
||||||
- **Must not** include `x-awesome-copilot` field (use convention-based `assets/preview.png` only)
|
- **Must not** include `x-awesome-copilot` field (use convention-based `assets/preview.png` only)
|
||||||
- Each extension must have `assets/preview.png` as the primary visual asset
|
- Each extension must have `assets/preview.png` as the primary visual asset
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from "url";
|
|||||||
import { ROOT_FOLDER } from "./constants.mjs";
|
import { ROOT_FOLDER } from "./constants.mjs";
|
||||||
|
|
||||||
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
||||||
|
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
|
||||||
const MATERIALIZED_SPECS = {
|
const MATERIALIZED_SPECS = {
|
||||||
agents: {
|
agents: {
|
||||||
path: "agents",
|
path: "agents",
|
||||||
@@ -27,6 +28,41 @@ const MATERIALIZED_SPECS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function copyDirRecursive(src, dest) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const destPath = path.join(dest, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDirRecursive(srcPath, destPath);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEntry(srcPath, destPath) {
|
||||||
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||||
|
try {
|
||||||
|
fs.renameSync(srcPath, destPath);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (!["EXDEV", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(srcPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
copyDirRecursive(srcPath, destPath);
|
||||||
|
fs.rmSync(srcPath, { recursive: true, force: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
fs.rmSync(srcPath, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
export function restoreManifestFromMaterializedFiles(pluginPath) {
|
export function restoreManifestFromMaterializedFiles(pluginPath) {
|
||||||
const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json");
|
const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json");
|
||||||
if (!fs.existsSync(pluginJsonPath)) {
|
if (!fs.existsSync(pluginJsonPath)) {
|
||||||
@@ -89,6 +125,68 @@ function cleanPlugin(pluginPath) {
|
|||||||
return { removed, manifestUpdated };
|
return { removed, manifestUpdated };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cleanMaterializedExtensionPlugin(extensionPath) {
|
||||||
|
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
|
||||||
|
let manifestUpdated = false;
|
||||||
|
if (fs.existsSync(pluginJsonPath)) {
|
||||||
|
const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
||||||
|
const extensionBundlePrefix = `extensions/${path.basename(extensionPath)}/`;
|
||||||
|
if (plugin.extensions === "extensions") {
|
||||||
|
plugin.extensions = ".";
|
||||||
|
manifestUpdated = true;
|
||||||
|
}
|
||||||
|
if (typeof plugin.logo === "string" && plugin.logo.startsWith(extensionBundlePrefix)) {
|
||||||
|
plugin.logo = plugin.logo.slice(extensionBundlePrefix.length);
|
||||||
|
manifestUpdated = true;
|
||||||
|
}
|
||||||
|
if (manifestUpdated) {
|
||||||
|
fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8");
|
||||||
|
console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = path.join(extensionPath, "extensions");
|
||||||
|
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
||||||
|
return { removed: 0, manifestUpdated };
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundleRoot = path.join(target, path.basename(extensionPath));
|
||||||
|
const count = countFiles(target);
|
||||||
|
if (fs.existsSync(bundleRoot) && fs.statSync(bundleRoot).isDirectory()) {
|
||||||
|
for (const entry of fs.readdirSync(bundleRoot, { withFileTypes: true })) {
|
||||||
|
moveEntry(path.join(bundleRoot, entry.name), path.join(extensionPath, entry.name));
|
||||||
|
}
|
||||||
|
console.log(` Restored ${path.basename(extensionPath)}/ from materialized extensions bundle`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(target, { recursive: true, force: true });
|
||||||
|
console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`);
|
||||||
|
return { removed: count, manifestUpdated };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExtensionPluginDirectory(extensionPath) {
|
||||||
|
if (fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundleEntry = path.join(extensionPath, "extensions", path.basename(extensionPath), "extension.mjs");
|
||||||
|
if (fs.existsSync(bundleEntry)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
|
||||||
|
if (!fs.existsSync(pluginJsonPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
||||||
|
return plugin.extensions === "extensions";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function countFiles(dir) {
|
function countFiles(dir) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
@@ -171,6 +269,25 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(EXTENSIONS_DIR)) {
|
||||||
|
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
for (const dirName of extensionDirs) {
|
||||||
|
const extensionPath = path.join(EXTENSIONS_DIR, dirName);
|
||||||
|
if (!isExtensionPluginDirectory(extensionPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath);
|
||||||
|
total += removed;
|
||||||
|
if (manifestUpdated) {
|
||||||
|
manifestsUpdated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
if (total === 0 && manifestsUpdated === 0) {
|
if (total === 0 && manifestsUpdated === 0) {
|
||||||
console.log("✅ No materialized files found. Plugins are already clean.");
|
console.log("✅ No materialized files found. Plugins are already clean.");
|
||||||
|
|||||||
@@ -459,6 +459,55 @@ async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (manifest.extenions !== undefined) {
|
||||||
|
errors.push(
|
||||||
|
`submission: plugins tagged with "canvas" must use "extensions" (found misspelled key "extenions") in "${manifestPath}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.extensions !== undefined && manifest.extensions !== "extensions") {
|
||||||
|
errors.push(
|
||||||
|
`submission: plugins tagged with "canvas" may omit "extensions", but if provided it must be "extensions" in "${manifestPath}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionContainerPath = joinRepoPath(pluginRoot, "extensions");
|
||||||
|
const extensionContainerResponse = await fetchGitHubFile(repo, extensionContainerPath, releaseLocator, token);
|
||||||
|
if (extensionContainerResponse.kind === "notFound") {
|
||||||
|
errors.push(
|
||||||
|
`submission: plugins tagged with "canvas" must include an "extensions" directory at ${releaseLocatorDescription}`,
|
||||||
|
);
|
||||||
|
} else if (extensionContainerResponse.kind === "apiError") {
|
||||||
|
warnings.push(
|
||||||
|
`submission: could not verify "extensions" directory in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
!(
|
||||||
|
extensionContainerResponse.data?.type === "dir"
|
||||||
|
|| Array.isArray(extensionContainerResponse.data)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`submission: "extensions" must be a directory in ${releaseLocatorDescription}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionEntryPath = joinRepoPath(pluginRoot, "extensions", "extension.mjs");
|
||||||
|
const extensionEntryResponse = await fetchGitHubFile(repo, extensionEntryPath, releaseLocator, token);
|
||||||
|
if (extensionEntryResponse.kind === "notFound") {
|
||||||
|
errors.push(
|
||||||
|
`submission: plugins tagged with "canvas" must include "extensions/extension.mjs" at ${releaseLocatorDescription}`,
|
||||||
|
);
|
||||||
|
} else if (extensionEntryResponse.kind === "apiError") {
|
||||||
|
warnings.push(
|
||||||
|
`submission: could not verify "extensions/extension.mjs" in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||||
|
);
|
||||||
|
} else if (extensionEntryResponse.data?.type !== "file") {
|
||||||
|
errors.push(
|
||||||
|
`submission: "extensions/extension.mjs" must be a file in ${releaseLocatorDescription}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
||||||
const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token);
|
const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token);
|
||||||
if (previewResponse.kind === "notFound") {
|
if (previewResponse.kind === "notFound") {
|
||||||
@@ -580,11 +629,13 @@ function normalizeQualityGateResult(rawResult) {
|
|||||||
vally_lint_status: "not_run",
|
vally_lint_status: "not_run",
|
||||||
smoke_status: "not_run",
|
smoke_status: "not_run",
|
||||||
version_match_status: "not_run",
|
version_match_status: "not_run",
|
||||||
|
canvas_structure_status: "not_run",
|
||||||
failure_class: "none",
|
failure_class: "none",
|
||||||
summary: "",
|
summary: "",
|
||||||
vally_lint_output: "",
|
vally_lint_output: "",
|
||||||
smoke_output: "",
|
smoke_output: "",
|
||||||
version_match_output: "",
|
version_match_output: "",
|
||||||
|
canvas_structure_output: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
|
if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
|
||||||
@@ -601,6 +652,7 @@ function buildQualityGatesCommentSection(qualityResult) {
|
|||||||
const vallyState = qualityResult.vally_lint_status || "not_run";
|
const vallyState = qualityResult.vally_lint_status || "not_run";
|
||||||
const smokeState = qualityResult.smoke_status || "not_run";
|
const smokeState = qualityResult.smoke_status || "not_run";
|
||||||
const versionMatchState = qualityResult.version_match_status || "not_run";
|
const versionMatchState = qualityResult.version_match_status || "not_run";
|
||||||
|
const canvasStructureState = qualityResult.canvas_structure_status || "not_run";
|
||||||
const summaryText = String(qualityResult.summary || "").trim() || "_No quality gate details were provided._";
|
const summaryText = String(qualityResult.summary || "").trim() || "_No quality gate details were provided._";
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
@@ -611,6 +663,7 @@ function buildQualityGatesCommentSection(qualityResult) {
|
|||||||
`| vally lint | ${vallyState} |`,
|
`| vally lint | ${vallyState} |`,
|
||||||
`| install smoke test | ${smokeState} |`,
|
`| install smoke test | ${smokeState} |`,
|
||||||
`| version match | ${versionMatchState} |`,
|
`| version match | ${versionMatchState} |`,
|
||||||
|
`| canvas structure | ${canvasStructureState} |`,
|
||||||
"",
|
"",
|
||||||
summaryText,
|
summaryText,
|
||||||
];
|
];
|
||||||
@@ -660,6 +713,21 @@ function buildQualityGatesCommentSection(qualityResult) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canvasStructureOutput = String(qualityResult.canvas_structure_output || "").trim();
|
||||||
|
if (canvasStructureOutput) {
|
||||||
|
sections.push(
|
||||||
|
"",
|
||||||
|
"<details>",
|
||||||
|
"<summary>Canvas structure output</summary>",
|
||||||
|
"",
|
||||||
|
"```text",
|
||||||
|
canvasStructureOutput,
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"</details>",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return sections.join("\n");
|
return sections.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export async function runExternalPluginPrQualityGates(plugins) {
|
|||||||
? "No changed external plugin entries were detected in plugins/external.json."
|
? "No changed external plugin entries were detected in plugins/external.json."
|
||||||
: checkedPlugins
|
: checkedPlugins
|
||||||
.map((entry) =>
|
.map((entry) =>
|
||||||
`- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, overall=${entry.quality.overall_status}`
|
`- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, canvas-structure=${entry.quality.canvas_structure_status}, overall=${entry.quality.overall_status}`
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { spawnSync } from "child_process";
|
|||||||
import { runLint, LintConsoleReporter } from "@microsoft/vally";
|
import { runLint, LintConsoleReporter } from "@microsoft/vally";
|
||||||
|
|
||||||
const MAX_OUTPUT_LENGTH = 12000;
|
const MAX_OUTPUT_LENGTH = 12000;
|
||||||
|
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
||||||
|
|
||||||
const INFRA_ERROR_PATTERNS = [
|
const INFRA_ERROR_PATTERNS = [
|
||||||
/\b401\b/,
|
/\b401\b/,
|
||||||
@@ -69,6 +70,12 @@ function normalizePluginPath(pluginPath) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCanvasKeyword(plugin) {
|
||||||
|
return (plugin?.keywords ?? []).some(
|
||||||
|
(keyword) => String(keyword).trim().toLowerCase() === EXTERNAL_CANVAS_KEYWORD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveFetchSpec(pluginSource) {
|
function resolveFetchSpec(pluginSource) {
|
||||||
if (pluginSource.sha) {
|
if (pluginSource.sha) {
|
||||||
return pluginSource.sha;
|
return pluginSource.sha;
|
||||||
@@ -467,6 +474,148 @@ function runVersionMatchGate(repoDir, plugin, primaryFetchSpec) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkPathExistsAtLocator(repoDir, locator, repoPath, expectedType) {
|
||||||
|
const result = runCommand("git", ["cat-file", "-e", `${locator}:${repoPath}`], { cwd: repoDir });
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
if (!expectedType) {
|
||||||
|
return { exists: true, output: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeResult = runCommand("git", ["cat-file", "-t", `${locator}:${repoPath}`], { cwd: repoDir });
|
||||||
|
if (typeResult.exitCode !== 0) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
output: `Unable to verify path "${repoPath}" type at "${locator}": ${typeResult.output}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualType = String(typeResult.stdout ?? "").trim();
|
||||||
|
if (actualType !== expectedType) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
output: "",
|
||||||
|
kindMismatch: true,
|
||||||
|
actualType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: true, output: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedOutput = String(result.output ?? "").toLowerCase();
|
||||||
|
if (
|
||||||
|
normalizedOutput.includes("not a valid object name")
|
||||||
|
|| normalizedOutput.includes("path '")
|
||||||
|
|| normalizedOutput.includes("does not exist")
|
||||||
|
) {
|
||||||
|
return { exists: false, output: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
output: `Unable to verify path "${repoPath}" at "${locator}": ${result.output}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) {
|
||||||
|
if (!hasCanvasKeyword(plugin)) {
|
||||||
|
return {
|
||||||
|
status: "not_run",
|
||||||
|
output: "Canvas structure gate skipped because plugin is not tagged with \"canvas\".",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPluginPath = normalizePluginPath(plugin?.source?.path || "/");
|
||||||
|
const locators = [plugin?.source?.ref, plugin?.source?.sha]
|
||||||
|
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter((value, index, values) => values.indexOf(value) === index);
|
||||||
|
|
||||||
|
if (locators.length === 0) {
|
||||||
|
return {
|
||||||
|
status: "not_run",
|
||||||
|
output: "Canvas structure gate skipped because neither source.ref nor source.sha was provided.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionsDir = toPosixPath(normalizedPluginPath, "extensions");
|
||||||
|
const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs");
|
||||||
|
|
||||||
|
let hasFailure = false;
|
||||||
|
let hasInfraError = false;
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
for (const locator of locators) {
|
||||||
|
if (locator !== primaryFetchSpec) {
|
||||||
|
const fetchResult = fetchLocatorIntoRepo(repoDir, locator);
|
||||||
|
if (fetchResult.status === "fail") {
|
||||||
|
hasFailure = true;
|
||||||
|
messages.push(`- ${locator}: ${fetchResult.output}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchResult.status === "infra_error") {
|
||||||
|
hasInfraError = true;
|
||||||
|
messages.push(`- ${locator}: ${fetchResult.output}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionDirCheck = checkPathExistsAtLocator(repoDir, locator, extensionsDir, "tree");
|
||||||
|
if (extensionDirCheck.output) {
|
||||||
|
hasInfraError = true;
|
||||||
|
messages.push(`- ${locator}: ${extensionDirCheck.output}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!extensionDirCheck.exists) {
|
||||||
|
hasFailure = true;
|
||||||
|
if (extensionDirCheck.kindMismatch) {
|
||||||
|
messages.push(`- ${locator}: "${extensionsDir}" must be a directory.`);
|
||||||
|
} else {
|
||||||
|
messages.push(`- ${locator}: missing required canvas extension directory "${extensionsDir}".`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionEntryCheck = checkPathExistsAtLocator(repoDir, locator, extensionEntryPoint, "blob");
|
||||||
|
if (extensionEntryCheck.output) {
|
||||||
|
hasInfraError = true;
|
||||||
|
messages.push(`- ${locator}: ${extensionEntryCheck.output}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!extensionEntryCheck.exists) {
|
||||||
|
hasFailure = true;
|
||||||
|
if (extensionEntryCheck.kindMismatch) {
|
||||||
|
messages.push(`- ${locator}: "${extensionEntryPoint}" must be a file.`);
|
||||||
|
} else {
|
||||||
|
messages.push(`- ${locator}: missing required canvas extension entry point "${extensionEntryPoint}".`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push(`- ${locator}: found "${extensionsDir}" with entry point "${extensionEntryPoint}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInfraError) {
|
||||||
|
return {
|
||||||
|
status: "infra_error",
|
||||||
|
output: messages.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFailure) {
|
||||||
|
return {
|
||||||
|
status: "fail",
|
||||||
|
output: messages.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "pass",
|
||||||
|
output: messages.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toOverallStatus(states) {
|
function toOverallStatus(states) {
|
||||||
if (states.includes("infra_error")) {
|
if (states.includes("infra_error")) {
|
||||||
return "infra_error";
|
return "infra_error";
|
||||||
@@ -497,11 +646,13 @@ export async function runExternalPluginQualityGates(plugin) {
|
|||||||
vally_lint_status: "not_run",
|
vally_lint_status: "not_run",
|
||||||
smoke_status: "not_run",
|
smoke_status: "not_run",
|
||||||
version_match_status: "not_run",
|
version_match_status: "not_run",
|
||||||
|
canvas_structure_status: "not_run",
|
||||||
failure_class: "none",
|
failure_class: "none",
|
||||||
summary: "",
|
summary: "",
|
||||||
vally_lint_output: "",
|
vally_lint_output: "",
|
||||||
smoke_output: "",
|
smoke_output: "",
|
||||||
version_match_output: "",
|
version_match_output: "",
|
||||||
|
canvas_structure_output: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -513,10 +664,14 @@ export async function runExternalPluginQualityGates(plugin) {
|
|||||||
result.vally_lint_status = "fail";
|
result.vally_lint_status = "fail";
|
||||||
result.smoke_status = "fail";
|
result.smoke_status = "fail";
|
||||||
result.version_match_status = "fail";
|
result.version_match_status = "fail";
|
||||||
|
result.canvas_structure_status = hasCanvasKeyword(plugin) ? "fail" : "not_run";
|
||||||
result.overall_status = "fail";
|
result.overall_status = "fail";
|
||||||
result.failure_class = "submitter_fixes";
|
result.failure_class = "submitter_fixes";
|
||||||
result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`;
|
result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`;
|
||||||
result.version_match_output = result.summary;
|
result.version_match_output = result.summary;
|
||||||
|
if (hasCanvasKeyword(plugin)) {
|
||||||
|
result.canvas_structure_output = result.summary;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +679,10 @@ export async function runExternalPluginQualityGates(plugin) {
|
|||||||
result.version_match_status = versionMatchResult.status;
|
result.version_match_status = versionMatchResult.status;
|
||||||
result.version_match_output = versionMatchResult.output;
|
result.version_match_output = versionMatchResult.output;
|
||||||
|
|
||||||
|
const canvasStructureResult = runCanvasStructureGate(repoDir, plugin, fetchSpec);
|
||||||
|
result.canvas_structure_status = canvasStructureResult.status;
|
||||||
|
result.canvas_structure_output = canvasStructureResult.output;
|
||||||
|
|
||||||
const vallyResult = await runVallyLintGate(pluginRoot);
|
const vallyResult = await runVallyLintGate(pluginRoot);
|
||||||
result.vally_lint_status = vallyResult.status;
|
result.vally_lint_status = vallyResult.status;
|
||||||
result.vally_lint_output = vallyResult.output;
|
result.vally_lint_output = vallyResult.output;
|
||||||
@@ -532,12 +691,18 @@ export async function runExternalPluginQualityGates(plugin) {
|
|||||||
result.smoke_status = smokeResult.status;
|
result.smoke_status = smokeResult.status;
|
||||||
result.smoke_output = smokeResult.output;
|
result.smoke_output = smokeResult.output;
|
||||||
|
|
||||||
result.overall_status = toOverallStatus([result.vally_lint_status, result.smoke_status, result.version_match_status]);
|
result.overall_status = toOverallStatus([
|
||||||
|
result.vally_lint_status,
|
||||||
|
result.smoke_status,
|
||||||
|
result.version_match_status,
|
||||||
|
result.canvas_structure_status,
|
||||||
|
]);
|
||||||
result.failure_class = toFailureClass(result.overall_status);
|
result.failure_class = toFailureClass(result.overall_status);
|
||||||
result.summary = [
|
result.summary = [
|
||||||
`- vally lint: ${result.vally_lint_status}`,
|
`- vally lint: ${result.vally_lint_status}`,
|
||||||
`- install smoke test: ${result.smoke_status}`,
|
`- install smoke test: ${result.smoke_status}`,
|
||||||
`- version match: ${result.version_match_status}`,
|
`- version match: ${result.version_match_status}`,
|
||||||
|
`- canvas structure: ${result.canvas_structure_status}`,
|
||||||
`- overall: ${result.overall_status}`,
|
`- overall: ${result.overall_status}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { after, test } from "node:test";
|
||||||
|
import { runCanvasStructureGate } from "./external-plugin-quality-gates.mjs";
|
||||||
|
|
||||||
|
const tempDirs = [];
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
for (const dir of tempDirs) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function runGit(repoDir, ...args) {
|
||||||
|
const result = spawnSync("git", args, { cwd: repoDir, encoding: "utf8" });
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`git ${args.join(" ")} failed: ${result.stdout}\n${result.stderr}`);
|
||||||
|
}
|
||||||
|
return String(result.stdout ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTempRepo() {
|
||||||
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-"));
|
||||||
|
tempDirs.push(repoDir);
|
||||||
|
|
||||||
|
runGit(repoDir, "init", "-q");
|
||||||
|
runGit(repoDir, "config", "user.name", "Copilot Test");
|
||||||
|
runGit(repoDir, "config", "user.email", "copilot@example.com");
|
||||||
|
return repoDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitAll(repoDir, message) {
|
||||||
|
runGit(repoDir, "add", "-A");
|
||||||
|
runGit(repoDir, "commit", "-m", message, "--quiet");
|
||||||
|
return runGit(repoDir, "rev-parse", "HEAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("runCanvasStructureGate passes when extensions/extension.mjs exists", () => {
|
||||||
|
const repoDir = createTempRepo();
|
||||||
|
fs.mkdirSync(path.join(repoDir, "extensions"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs"), "export default {};\n");
|
||||||
|
const sha = commitAll(repoDir, "Add canvas extension container");
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
name: "canvas-plugin",
|
||||||
|
keywords: ["canvas"],
|
||||||
|
source: {
|
||||||
|
source: "github",
|
||||||
|
repo: "owner/repo",
|
||||||
|
sha,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||||
|
assert.equal(result.status, "pass");
|
||||||
|
assert.match(result.output, /found "extensions"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runCanvasStructureGate fails when extension entrypoint is only at repo root", () => {
|
||||||
|
const repoDir = createTempRepo();
|
||||||
|
fs.writeFileSync(path.join(repoDir, "extension.mjs"), "export default {};\n");
|
||||||
|
const sha = commitAll(repoDir, "Add root extension entrypoint");
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
name: "canvas-plugin",
|
||||||
|
keywords: ["canvas"],
|
||||||
|
source: {
|
||||||
|
source: "github",
|
||||||
|
repo: "owner/repo",
|
||||||
|
sha,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||||
|
assert.equal(result.status, "fail");
|
||||||
|
assert.match(result.output, /missing required canvas extension directory "extensions"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runCanvasStructureGate fails when extension entrypoint path is a directory", () => {
|
||||||
|
const repoDir = createTempRepo();
|
||||||
|
fs.mkdirSync(path.join(repoDir, "extensions", "extension.mjs"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs", "placeholder.txt"), "not-a-module\n");
|
||||||
|
const sha = commitAll(repoDir, "Add invalid extension entrypoint directory");
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
name: "canvas-plugin",
|
||||||
|
keywords: ["canvas"],
|
||||||
|
source: {
|
||||||
|
source: "github",
|
||||||
|
repo: "owner/repo",
|
||||||
|
sha,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||||
|
assert.equal(result.status, "fail");
|
||||||
|
assert.match(result.output, /"extensions\/extension\.mjs" must be a file/);
|
||||||
|
});
|
||||||
@@ -62,6 +62,16 @@ function collectLocalPluginsFromRoot(rootDir, sourcePrefix, includeEntry = () =>
|
|||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasExtensionEntryPoint(extensionDir, extensionName) {
|
||||||
|
const candidateEntryPoints = [
|
||||||
|
path.join(extensionDir, "extension.mjs"),
|
||||||
|
path.join(extensionDir, "extensions", "extension.mjs"),
|
||||||
|
path.join(extensionDir, "extensions", extensionName, "extension.mjs"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return candidateEntryPoints.some((entryPointPath) => fs.existsSync(entryPointPath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate marketplace.json from plugin directories
|
* Generate marketplace.json from plugin directories
|
||||||
*/
|
*/
|
||||||
@@ -78,7 +88,7 @@ function generateMarketplace() {
|
|||||||
...collectLocalPluginsFromRoot(
|
...collectLocalPluginsFromRoot(
|
||||||
EXTENSIONS_DIR,
|
EXTENSIONS_DIR,
|
||||||
"extensions",
|
"extensions",
|
||||||
(entryName) => fs.existsSync(path.join(EXTENSIONS_DIR, entryName, "extension.mjs"))
|
(entryName) => hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entryName), entryName)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ const WEBSITE_SOURCE_DATA_DIR = path.join(WEBSITE_DIR, "data");
|
|||||||
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
||||||
const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png";
|
const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png";
|
||||||
|
|
||||||
|
function hasExtensionEntryPoint(extensionDir, extensionName) {
|
||||||
|
const candidateEntryPoints = [
|
||||||
|
path.join(extensionDir, "extension.mjs"),
|
||||||
|
path.join(extensionDir, "extensions", "extension.mjs"),
|
||||||
|
path.join(extensionDir, "extensions", extensionName, "extension.mjs"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return candidateEntryPoints.some((entryPointPath) => fs.existsSync(entryPointPath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the output directory exists
|
* Ensure the output directory exists
|
||||||
*/
|
*/
|
||||||
@@ -544,7 +554,7 @@ function generatePluginsData(gitDates, resourceIndex = {}) {
|
|||||||
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
if (!entry.isDirectory()) return false;
|
if (!entry.isDirectory()) return false;
|
||||||
return fs.existsSync(path.join(EXTENSIONS_DIR, entry.name, "extension.mjs"));
|
return hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entry.name), entry.name);
|
||||||
})
|
})
|
||||||
.map((entry) => entry.name)
|
.map((entry) => entry.name)
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
@@ -1235,12 +1245,7 @@ function generateCanvasManifest(gitDates, commitSha) {
|
|||||||
.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
if (!entry.isDirectory()) return false;
|
if (!entry.isDirectory()) return false;
|
||||||
const extensionEntryPoint = path.join(
|
return hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entry.name), entry.name);
|
||||||
EXTENSIONS_DIR,
|
|
||||||
entry.name,
|
|
||||||
"extension.mjs"
|
|
||||||
);
|
|
||||||
return fs.existsSync(extensionEntryPoint);
|
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
|||||||
+122
-2
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
import { ROOT_FOLDER } from "./constants.mjs";
|
import { ROOT_FOLDER } from "./constants.mjs";
|
||||||
|
|
||||||
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
||||||
|
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively copy a directory.
|
* Recursively copy a directory.
|
||||||
@@ -22,6 +24,36 @@ function copyDirRecursive(src, dest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveEntry(srcPath, destPath) {
|
||||||
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||||
|
try {
|
||||||
|
fs.renameSync(srcPath, destPath);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "EXDEV") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(srcPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
copyDirRecursive(srcPath, destPath);
|
||||||
|
fs.rmSync(srcPath, { recursive: true, force: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
fs.rmSync(srcPath, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRelativeAssetPath(assetPath) {
|
||||||
|
return typeof assetPath === "string" &&
|
||||||
|
assetPath.length > 0 &&
|
||||||
|
!/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(assetPath) &&
|
||||||
|
!assetPath.startsWith("data:") &&
|
||||||
|
!path.isAbsolute(assetPath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a plugin-relative path to the repo-root source file.
|
* Resolve a plugin-relative path to the repo-root source file.
|
||||||
*
|
*
|
||||||
@@ -45,6 +77,59 @@ function resolveSource(relPath) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function materializeExtensionPlugin(extensionPath) {
|
||||||
|
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
|
||||||
|
if (!fs.existsSync(pluginJsonPath)) {
|
||||||
|
return { movedEntries: 0, manifestUpdated: false, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to parse ${pluginJsonPath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionContainerPath = path.join(extensionPath, "extensions");
|
||||||
|
const extensionBundlePath = path.join(extensionContainerPath, path.basename(extensionPath));
|
||||||
|
fs.rmSync(extensionContainerPath, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(extensionBundlePath, { recursive: true });
|
||||||
|
|
||||||
|
let movedEntries = 0;
|
||||||
|
for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) {
|
||||||
|
if (entry.name === ".github" || entry.name === "extensions") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveEntry(
|
||||||
|
path.join(extensionPath, entry.name),
|
||||||
|
path.join(extensionBundlePath, entry.name)
|
||||||
|
);
|
||||||
|
movedEntries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelativeAssetPath(metadata.logo)) {
|
||||||
|
const normalizedLogoPath = metadata.logo.replace(/\\/g, "/").replace(/^\.\//, "");
|
||||||
|
const bundledLogoPath = path.join(extensionBundlePath, normalizedLogoPath);
|
||||||
|
if (fs.existsSync(bundledLogoPath)) {
|
||||||
|
const rootLogoPath = path.join(extensionPath, normalizedLogoPath);
|
||||||
|
fs.mkdirSync(path.dirname(rootLogoPath), { recursive: true });
|
||||||
|
fs.copyFileSync(bundledLogoPath, rootLogoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifestUpdated = false;
|
||||||
|
if (metadata.extensions !== "extensions") {
|
||||||
|
metadata.extensions = "extensions";
|
||||||
|
manifestUpdated = true;
|
||||||
|
}
|
||||||
|
if (manifestUpdated) {
|
||||||
|
fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { movedEntries, manifestUpdated, skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
function materializePlugins() {
|
function materializePlugins() {
|
||||||
console.log("Materializing plugin files...\n");
|
console.log("Materializing plugin files...\n");
|
||||||
|
|
||||||
@@ -61,6 +146,8 @@ function materializePlugins() {
|
|||||||
let totalAgents = 0;
|
let totalAgents = 0;
|
||||||
let totalSkills = 0;
|
let totalSkills = 0;
|
||||||
let totalExtensions = 0;
|
let totalExtensions = 0;
|
||||||
|
let totalExtensionPlugins = 0;
|
||||||
|
let totalExtensionPluginEntries = 0;
|
||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
|
|
||||||
@@ -185,7 +272,36 @@ function materializePlugins() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
|
if (fs.existsSync(EXTENSIONS_DIR)) {
|
||||||
|
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
for (const dirName of extensionDirs) {
|
||||||
|
const extensionPath = path.join(EXTENSIONS_DIR, dirName);
|
||||||
|
if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = materializeExtensionPlugin(extensionPath);
|
||||||
|
if (result.skipped) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalExtensionPlugins++;
|
||||||
|
totalExtensionPluginEntries += result.movedEntries;
|
||||||
|
console.log(`✓ ${dirName}: materialized extension bundle into ./extensions (${result.movedEntries} entries)`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`);
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} plugin extension refs.`);
|
||||||
|
console.log(`Materialized ${totalExtensionPlugins} extension plugins (${totalExtensionPluginEntries} top-level entries).`);
|
||||||
if (warnings > 0) {
|
if (warnings > 0) {
|
||||||
console.log(`${warnings} warning(s).`);
|
console.log(`${warnings} warning(s).`);
|
||||||
}
|
}
|
||||||
@@ -195,4 +311,8 @@ function materializePlugins() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
materializePlugins();
|
export { materializePlugins };
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
materializePlugins();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { after, test } from "node:test";
|
||||||
|
import { materializeExtensionPlugin } from "./materialize-plugins.mjs";
|
||||||
|
import { cleanMaterializedExtensionPlugin } from "./clean-materialized-plugins.mjs";
|
||||||
|
|
||||||
|
const tempDirs = [];
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
for (const dir of tempDirs) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("materializeExtensionPlugin writes extension bundles to ./extensions and preserves root logo assets", () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "materialize-extension-plugin-"));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
const pluginDir = path.join(tempDir, "extension-plugin");
|
||||||
|
fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({
|
||||||
|
name: "test-extension-plugin",
|
||||||
|
description: "test plugin",
|
||||||
|
version: "1.0.0",
|
||||||
|
logo: "assets/preview.png",
|
||||||
|
extensions: ".",
|
||||||
|
}, null, 2));
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n");
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n");
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes");
|
||||||
|
|
||||||
|
const result = materializeExtensionPlugin(pluginDir);
|
||||||
|
const bundleRoot = path.join(pluginDir, "extensions", "extension-plugin");
|
||||||
|
|
||||||
|
assert.equal(result.skipped, false);
|
||||||
|
assert.equal(result.manifestUpdated, true);
|
||||||
|
assert.equal(result.movedEntries, 3);
|
||||||
|
assert.equal(fs.existsSync(path.join(bundleRoot, "extension.mjs")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(bundleRoot, "assets", "preview.png")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(bundleRoot, "README.md")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "extensions", ".github")), false);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "extension.mjs")), false);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "README.md")), false);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "assets", "preview.png")), true);
|
||||||
|
|
||||||
|
const pluginManifest = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8")
|
||||||
|
);
|
||||||
|
assert.equal(pluginManifest.extensions, "extensions");
|
||||||
|
assert.equal(pluginManifest.logo, "assets/preview.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cleanMaterializedExtensionPlugin restores moved extension files to root", () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "clean-materialized-extension-plugin-"));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
const pluginDir = path.join(tempDir, "extension-plugin");
|
||||||
|
fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({
|
||||||
|
name: "test-extension-plugin",
|
||||||
|
description: "test plugin",
|
||||||
|
version: "1.0.0",
|
||||||
|
logo: "assets/preview.png",
|
||||||
|
extensions: ".",
|
||||||
|
}, null, 2));
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n");
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n");
|
||||||
|
fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes");
|
||||||
|
|
||||||
|
materializeExtensionPlugin(pluginDir);
|
||||||
|
const result = cleanMaterializedExtensionPlugin(pluginDir);
|
||||||
|
|
||||||
|
assert.equal(result.removed, 3);
|
||||||
|
assert.equal(result.manifestUpdated, true);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "extension.mjs")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "README.md")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "assets", "preview.png")), true);
|
||||||
|
assert.equal(fs.existsSync(path.join(pluginDir, "extensions")), false);
|
||||||
|
|
||||||
|
const pluginManifest = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8")
|
||||||
|
);
|
||||||
|
assert.equal(pluginManifest.extensions, ".");
|
||||||
|
assert.equal(pluginManifest.logo, "assets/preview.png");
|
||||||
|
});
|
||||||
@@ -305,9 +305,14 @@ function validateExtensionManifest(folderName) {
|
|||||||
errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)");
|
errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension convention: extensions field must be "."
|
if (parsed.extenions !== undefined) {
|
||||||
|
errors.push('use "extensions" field (found misspelled key "extenions")');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension convention: source manifests keep extensions at repository root.
|
||||||
|
// Materialization rewrites this to "extensions" on distribution branches.
|
||||||
if (parsed.extensions !== ".") {
|
if (parsed.extensions !== ".") {
|
||||||
errors.push('extensions field must be exactly "." (extension convention)');
|
errors.push('extensions field must be exactly "." in source manifests (extension convention)');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { errors, plugin: parsedPlugin };
|
return { errors, plugin: parsedPlugin };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "accessibility-kanban",
|
"name": "accessibility-kanban",
|
||||||
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "apng-studio",
|
"name": "apng-studio",
|
||||||
"description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.",
|
"description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Andrea Griffiths",
|
"name": "Andrea Griffiths",
|
||||||
"url": "https://github.com/AndreaGriffiths11"
|
"url": "https://github.com/AndreaGriffiths11"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "arcade-canvas",
|
"name": "arcade-canvas",
|
||||||
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
|
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dan Wahlin",
|
"name": "Dan Wahlin",
|
||||||
"url": "https://github.com/DanWahlin"
|
"url": "https://github.com/DanWahlin"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "backlog-swipe-triage",
|
"name": "backlog-swipe-triage",
|
||||||
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "James Montemagno",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/jamesmontemagno"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chromium-control-canvas",
|
"name": "chromium-control-canvas",
|
||||||
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Andrea Griffiths",
|
"name": "Andrea Griffiths",
|
||||||
"url": "https://github.com/AndreaGriffiths11"
|
"url": "https://github.com/AndreaGriffiths11"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "color-orb",
|
"name": "color-orb",
|
||||||
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "connector-namespaces",
|
"name": "connector-namespaces",
|
||||||
"description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.",
|
"description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.",
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alex Yang",
|
"name": "Alex Yang",
|
||||||
"url": "https://github.com/alexyaang"
|
"url": "https://github.com/alexyaang"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "diagram-viewer",
|
"name": "diagram-viewer",
|
||||||
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "feedback-themes",
|
"name": "feedback-themes",
|
||||||
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gesture-review",
|
"name": "gesture-review",
|
||||||
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "java-modernization-studio",
|
"name": "java-modernization-studio",
|
||||||
"description": "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.",
|
"description": "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.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Ayan Gupta",
|
"name": "Ayan Gupta",
|
||||||
"url": "https://github.com/ayangupt"
|
"url": "https://github.com/ayangupt"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "release-notes-showcase",
|
"name": "release-notes-showcase",
|
||||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Kayla Cinnamon",
|
"name": "Kayla Cinnamon",
|
||||||
"url": "https://github.com/cinnamon-msft"
|
"url": "https://github.com/cinnamon-msft"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "repo-actions-hub",
|
"name": "repo-actions-hub",
|
||||||
"description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.",
|
"description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "James Montemagno",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/jamesmontemagno"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "site-studio",
|
"name": "site-studio",
|
||||||
"description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.",
|
"description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Ayan Gupta",
|
"name": "Ayan Gupta",
|
||||||
"url": "https://github.com/ayangupt"
|
"url": "https://github.com/ayangupt"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tiny-tool-town-submitter",
|
"name": "tiny-tool-town-submitter",
|
||||||
"description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.",
|
"description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "James Montemagno",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/jamesmontemagno"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "token-pacman",
|
"name": "token-pacman",
|
||||||
"description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.",
|
"description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "James Montemagno",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/jamesmontemagno"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "where-was-i",
|
"name": "where-was-i",
|
||||||
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Aaron Powell",
|
"name": "Aaron Powell",
|
||||||
"url": "https://github.com/aaronpowell"
|
"url": "https://github.com/aaronpowell"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "work-hub",
|
"name": "work-hub",
|
||||||
"description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.",
|
"description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "James Montemagno",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/jamesmontemagno"
|
||||||
|
|||||||
+29
-4
@@ -339,7 +339,7 @@
|
|||||||
{
|
{
|
||||||
"name": "foundry-agent-canvas",
|
"name": "foundry-agent-canvas",
|
||||||
"description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.",
|
"description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"url": "https://www.microsoft.com"
|
"url": "https://www.microsoft.com"
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
"source": "github",
|
"source": "github",
|
||||||
"repo": "microsoft/foundry-toolkit",
|
"repo": "microsoft/foundry-toolkit",
|
||||||
"path": "foundry-agent-canvas",
|
"path": "foundry-agent-canvas",
|
||||||
"sha": "0d311e56508ac200ebb86586adbee9c7b8b0601f"
|
"sha": "e16be2b8533ca82c22806388e07581e7497785d7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
{
|
{
|
||||||
"name": "modernize-dotnet",
|
"name": "modernize-dotnet",
|
||||||
"description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.",
|
"description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.",
|
||||||
"version": "1.0.1157-preview1",
|
"version": "1.0.1161-preview1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"url": "https://www.microsoft.com"
|
"url": "https://www.microsoft.com"
|
||||||
@@ -519,7 +519,8 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"repo": "dotnet/modernize-dotnet",
|
"repo": "dotnet/modernize-dotnet",
|
||||||
"path": "plugins/modernize-dotnet"
|
"path": "plugins/modernize-dotnet",
|
||||||
|
"sha": "ce4ec09678498da38099ca9169c1624c59c2a89a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -732,6 +733,30 @@
|
|||||||
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
|
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "upgrade-agent",
|
||||||
|
"description": "GitHub Copilot upgrade is an AI-powered agent that helps you upgrade applications to newer versions of languages, frameworks, and runtimes. It assesses your application, creates an upgrade plan, applies code changes, and validates the results through an interactive upgrade workflow.",
|
||||||
|
"version": "1.1.222",
|
||||||
|
"author": {
|
||||||
|
"name": "Microsoft",
|
||||||
|
"url": "https://www.microsoft.com"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/microsoft/upgrade-agent-plugins",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"modernization",
|
||||||
|
"upgrade",
|
||||||
|
"migration",
|
||||||
|
"dotnet",
|
||||||
|
"canvas"
|
||||||
|
],
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "microsoft/upgrade-agent-plugins",
|
||||||
|
"path": "plugins/upgrade-agent",
|
||||||
|
"sha": "379d344e42823b25223f878c002f38fb3a2c1d2b"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "vercel-plugin",
|
"name": "vercel-plugin",
|
||||||
"description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.",
|
"description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.",
|
||||||
|
|||||||
Reference in New Issue
Block a user