Migrate extension metadata to plugin.json and enforce conventions (#2177)

* Remove pluginRoots property from marketplace.json

The pluginRoots property is not used by install tooling and was only
informational about the extension/plugin source directories. Removing it
simplifies the marketplace.json structure while maintaining all functionality.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Migrate java-modernization-studio to plugin.json and update validation workflow

- Create .github/plugin/plugin.json for java-modernization-studio extension
- Remove legacy canvas.json from java-modernization-studio
- Update validate-canvas-extensions.yml workflow to check for plugin.json instead of canvas.json
- Update workflow to trigger on .schemas/plugin.schema.json changes (instead of canvas.schema.json)
- Remove schema validation logic that relied on canvas.schema.json
- All 12 extensions now use plugin.json for metadata

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add extensions field to all extension plugin.json files

Per https://github.com/github/copilot-agent-runtime/pull/9929, plugins that ship
extensions need to include an extensions field specifying where the extension code
is located. All 12 extensions now have extensions set to '.' to reference the
current directory where extension.mjs is located.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Enforce convention-based extension metadata and remove x-awesome-copilot

- Remove x-awesome-copilot.screenshots from all extension plugin.json files
- Enforce logo=assets/preview.png convention for all extensions
- Enforce extensions=. per copilot-agent-runtime#9929
- Update validate-plugins.mjs to enforce conventions
- Update validate-canvas-extensions.yml workflow with convention checks
- Update AGENTS.md and CONTRIBUTING.md documentation

All 12 extensions validated successfully.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Use standard plugin validation for extensions

Remove the custom extension schema and schema validation helper, and
validate extension plugin.json files through the existing plugin validator
instead. Update workflows to stop depending on the removed schema.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-07-03 12:24:38 +10:00
committed by GitHub
parent da3855ab20
commit e986f49695
27 changed files with 748 additions and 687 deletions
+146 -69
View File
@@ -2,17 +2,22 @@
"name": "awesome-copilot",
"metadata": {
"description": "Community-driven collection of GitHub Copilot plugins, agents, prompts, and skills",
"version": "1.0.0",
"pluginRoot": "./plugins"
"version": "1.0.0"
},
"owner": {
"name": "GitHub",
"email": "copilot@github.com"
},
"plugins": [
{
"name": "accessibility-kanban",
"source": "extensions/accessibility-kanban",
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
"version": "1.0.0"
},
{
"name": "acreadiness-cockpit",
"source": "acreadiness-cockpit",
"source": "plugins/acreadiness-cockpit",
"description": "Drive Microsoft AgentRC from Copilot chat: assess AI readiness, generate Copilot instructions (flat or nested with applyTo globs for monorepos), and manage policies. Produces a self-contained static HTML dashboard at reports/index.html.",
"version": "1.0.0"
},
@@ -72,31 +77,37 @@
},
{
"name": "ai-team-orchestration",
"source": "ai-team-orchestration",
"source": "plugins/ai-team-orchestration",
"description": "Bootstrap and run a multi-agent AI development team with named roles (Producer, Dev Team, QA). Sprint planning, brainstorm prompts with distinct agent voices, cross-chat context survival, and parallel team workflows. Based on a proven template that shipped a 30-game app in 5 days with zero human-written code.",
"version": "1.0.0"
},
{
"name": "arcade-canvas",
"source": "extensions/arcade-canvas",
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
"version": "1.0.0"
},
{
"name": "arize-ax",
"source": "arize-ax",
"source": "plugins/arize-ax",
"description": "Arize AX platform skills for LLM observability, evaluation, and optimization. Includes trace export, instrumentation, datasets, experiments, evaluators, AI provider integrations, annotations, prompt optimization, and deep linking to the Arize UI.",
"version": "1.0.0"
},
{
"name": "automate-this",
"source": "automate-this",
"source": "plugins/automate-this",
"description": "Record your screen doing a manual process, drop the video on your Desktop, and let Copilot CLI analyze it frame-by-frame to build working automation scripts. Supports narrated recordings with audio transcription.",
"version": "1.0.0"
},
{
"name": "awesome-copilot",
"source": "awesome-copilot",
"source": "plugins/awesome-copilot",
"description": "Meta prompts that help you discover and generate curated GitHub Copilot agents, instructions, prompts, and skills.",
"version": "1.1.0"
},
{
"name": "aws-cloud-development",
"source": "aws-cloud-development",
"source": "plugins/aws-cloud-development",
"description": "Comprehensive AWS cloud development tools including Infrastructure as Code, serverless functions, architecture patterns, and cost optimization for building scalable cloud applications.",
"version": "1.0.0"
},
@@ -127,13 +138,19 @@
},
{
"name": "azure-cloud-development",
"source": "azure-cloud-development",
"source": "plugins/azure-cloud-development",
"description": "Comprehensive Azure cloud development tools including Infrastructure as Code, serverless functions, architecture patterns, and cost optimization for building scalable cloud applications.",
"version": "1.0.1"
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "cast-imaging",
"source": "cast-imaging",
"source": "plugins/cast-imaging",
"description": "A comprehensive collection of specialized agents for software analysis, impact assessment, structural quality advisories, and architectural review using CAST Imaging.",
"version": "1.0.0"
},
@@ -162,15 +179,21 @@
"ref": "chrome-devtools-mcp-v1.0.1"
}
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "clojure-interactive-programming",
"source": "clojure-interactive-programming",
"source": "plugins/clojure-interactive-programming",
"description": "Tools for REPL-first Clojure workflows featuring Clojure instructions, the interactive programming chat mode and supporting guidance.",
"version": "1.0.0"
},
{
"name": "cms-development",
"source": "cms-development",
"source": "plugins/cms-development",
"description": "Skills for CMS development across themes, plugins, admin tooling, media workflows, markdown rendering, and static export pipelines.",
"version": "1.1.0"
},
@@ -200,15 +223,21 @@
"sha": "0874b16210768d54b273c1395d9715bc8638b347"
}
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "context-engineering",
"source": "context-engineering",
"source": "plugins/context-engineering",
"description": "Tools and techniques for maximizing GitHub Copilot effectiveness through better context management. Includes guidelines for structuring code, an agent for planning multi-file changes, and prompts for context-aware development.",
"version": "1.0.0"
},
{
"name": "context-matic",
"source": "context-matic",
"source": "plugins/context-matic",
"description": "Coding agents hallucinate APIs. ContextMatic gives them curated, versioned API and SDK docs. Ask your agent to \"integrate the payments API\" and it guesses — falling back on outdated training data and generic patterns that don't match your actual SDK. ContextMatic solves this by giving the agent deterministic, version-aware, SDK-native context at the exact moment it's needed.",
"version": "0.1.0"
},
@@ -239,19 +268,19 @@
},
{
"name": "copilot-sdk",
"source": "copilot-sdk",
"source": "plugins/copilot-sdk",
"description": "Build applications with the GitHub Copilot SDK across multiple programming languages. Includes comprehensive instructions for C#, Go, Node.js/TypeScript, and Python to help you create AI-powered applications.",
"version": "1.0.0"
},
{
"name": "csharp-dotnet-development",
"source": "csharp-dotnet-development",
"source": "plugins/csharp-dotnet-development",
"description": "Essential prompts, instructions, and chat modes for C# and .NET development including testing, documentation, and best practices.",
"version": "1.1.0"
},
{
"name": "database-data-management",
"source": "database-data-management",
"source": "plugins/database-data-management",
"description": "Database administration, SQL optimization, and data management tools for PostgreSQL, SQL Server, and general database development best practices.",
"version": "1.0.0"
},
@@ -306,16 +335,22 @@
},
{
"name": "dataverse-sdk-for-python",
"source": "dataverse-sdk-for-python",
"source": "plugins/dataverse-sdk-for-python",
"description": "Comprehensive collection for building production-ready Python integrations with Microsoft Dataverse. Includes official documentation, best practices, advanced features, file operations, and code generation prompts.",
"version": "1.0.0"
},
{
"name": "devops-oncall",
"source": "devops-oncall",
"source": "plugins/devops-oncall",
"description": "A focused set of prompts, instructions, and a chat mode to help triage incidents and respond quickly with DevOps tools and Azure resources.",
"version": "1.0.0"
},
{
"name": "diagram-viewer",
"source": "extensions/diagram-viewer",
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
"version": "1.0.0"
},
{
"name": "dotnet",
"description": "Common everyday C#/.NET coding skills. Expected to be useful to all .NET developers.",
@@ -403,13 +438,13 @@
},
{
"name": "doublecheck",
"source": "doublecheck",
"source": "plugins/doublecheck",
"description": "Three-layer verification pipeline for AI output. Extracts claims, finds sources, and flags hallucination risks so humans can verify before acting.",
"version": "1.0.0"
},
{
"name": "edge-ai-tasks",
"source": "edge-ai-tasks",
"source": "plugins/edge-ai-tasks",
"description": "Task Researcher and Task Planner for intermediate to expert users and large codebases - Brought to you by microsoft/edge-ai",
"version": "1.0.0"
},
@@ -441,22 +476,28 @@
},
{
"name": "ember",
"source": "ember",
"source": "plugins/ember",
"description": "An AI partner, not a tool. Ember carries fire from person to person — helping humans discover that AI partnership isn't something you learn, it's something you find.",
"version": "1.0.0"
},
{
"name": "eyeball",
"source": "eyeball",
"source": "plugins/eyeball",
"description": "Document analysis with inline source screenshots. When you ask Copilot to analyze a document, Eyeball generates a Word doc where every factual claim includes a highlighted screenshot from the source material so you can verify it with your own eyes.",
"version": "1.0.0"
},
{
"name": "fastah-ip-geo-tools",
"source": "fastah-ip-geo-tools",
"source": "plugins/fastah-ip-geo-tools",
"description": "This plugin is for network operations engineers who wish to tune and publish IP geolocation feeds in RFC 8805 format. It consists of an AI Skill and an associated MCP server that geocodes geolocation place names to real cities for accuracy.",
"version": "0.0.9"
},
{
"name": "feedback-themes",
"source": "extensions/feedback-themes",
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
"version": "1.0.0"
},
{
"name": "figma",
"description": "Plugin that includes the Figma MCP server and Skills for common workflows.",
@@ -481,22 +522,28 @@
},
{
"name": "flowstudio-power-automate",
"source": "flowstudio-power-automate",
"source": "plugins/flowstudio-power-automate",
"description": "Give your AI agent full visibility into Power Automate cloud flows via the FlowStudio MCP server. Connect, debug, build, monitor health, and govern flows at scale — action-level inputs and outputs, not just status codes.",
"version": "2.0.0"
},
{
"name": "frontend-web-dev",
"source": "frontend-web-dev",
"source": "plugins/frontend-web-dev",
"description": "Essential prompts, instructions, and chat modes for modern frontend web development including React, Angular, Vue, TypeScript, and CSS frameworks.",
"version": "1.0.0"
},
{
"name": "gem-team",
"source": "gem-team",
"source": "plugins/gem-team",
"description": "Self-Learning Multi-agent orchestration framework for spec-driven development and automated verification.",
"version": "1.66.0"
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "gh-skills-builder",
"description": "Repo: https://github.com/arilivigni/gh-skills-builder\nThis plugin is for people that want to create a GitHub Skills exercise which are self-paced learning GitHub within GitHub.\nhttps://learn.github.com/skills\nexamples:\nhttps://github.com/skills/agent-orchestration-build-your-ai-dream-team\nhttps://github.com/skills/agentic-workflows-that-read-the-room",
@@ -583,31 +630,37 @@
},
{
"name": "go-mcp-development",
"source": "go-mcp-development",
"source": "plugins/go-mcp-development",
"description": "Complete toolkit for building Model Context Protocol (MCP) servers in Go using the official github.com/modelcontextprotocol/go-sdk. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance.",
"version": "1.0.0"
},
{
"name": "java-development",
"source": "java-development",
"source": "plugins/java-development",
"description": "Comprehensive collection of prompts and instructions for Java development including Spring Boot, Quarkus, testing, documentation, and best practices.",
"version": "1.0.0"
},
{
"name": "java-mcp-development",
"source": "java-mcp-development",
"source": "plugins/java-mcp-development",
"description": "Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.",
"version": "1.0.0"
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "kotlin-mcp-development",
"source": "kotlin-mcp-development",
"source": "plugins/kotlin-mcp-development",
"description": "Complete toolkit for building Model Context Protocol (MCP) servers in Kotlin using the official io.modelcontextprotocol:kotlin-sdk library. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance.",
"version": "1.0.0"
},
{
"name": "mcp-m365-copilot",
"source": "mcp-m365-copilot",
"source": "plugins/mcp-m365-copilot",
"description": "Comprehensive collection for building declarative agents with Model Context Protocol integration for Microsoft 365 Copilot",
"version": "1.0.0"
},
@@ -713,79 +766,79 @@
},
{
"name": "napkin",
"source": "napkin",
"source": "plugins/napkin",
"description": "Visual whiteboard collaboration for Copilot CLI. Opens an interactive whiteboard in your browser where you can draw, sketch, and add sticky notes — then share everything back with Copilot. Copilot sees your drawings and responds with analysis, suggestions, and ideas.",
"version": "1.0.0"
},
{
"name": "noob-mode",
"source": "noob-mode",
"source": "plugins/noob-mode",
"description": "Plain-English translation layer for non-technical Copilot CLI users. Translates every approval prompt, error message, and technical output into clear, jargon-free English with color-coded risk indicators.",
"version": "1.0.0"
},
{
"name": "openapi-to-application-csharp-dotnet",
"source": "openapi-to-application-csharp-dotnet",
"source": "plugins/openapi-to-application-csharp-dotnet",
"description": "Generate production-ready .NET applications from OpenAPI specifications. Includes ASP.NET Core project scaffolding, controller generation, entity framework integration, and C# best practices.",
"version": "1.0.0"
},
{
"name": "openapi-to-application-go",
"source": "openapi-to-application-go",
"source": "plugins/openapi-to-application-go",
"description": "Generate production-ready Go applications from OpenAPI specifications. Includes project scaffolding, handler generation, middleware setup, and Go best practices for REST APIs.",
"version": "1.0.0"
},
{
"name": "openapi-to-application-java-spring-boot",
"source": "openapi-to-application-java-spring-boot",
"source": "plugins/openapi-to-application-java-spring-boot",
"description": "Generate production-ready Spring Boot applications from OpenAPI specifications. Includes project scaffolding, REST controller generation, service layer organization, and Spring Boot best practices.",
"version": "1.0.0"
},
{
"name": "openapi-to-application-nodejs-nestjs",
"source": "openapi-to-application-nodejs-nestjs",
"source": "plugins/openapi-to-application-nodejs-nestjs",
"description": "Generate production-ready NestJS applications from OpenAPI specifications. Includes project scaffolding, controller and service generation, TypeScript best practices, and enterprise patterns.",
"version": "1.0.0"
},
{
"name": "openapi-to-application-python-fastapi",
"source": "openapi-to-application-python-fastapi",
"source": "plugins/openapi-to-application-python-fastapi",
"description": "Generate production-ready FastAPI applications from OpenAPI specifications. Includes project scaffolding, route generation, dependency injection, and Python best practices for async APIs.",
"version": "1.0.0"
},
{
"name": "oracle-to-postgres-migration-expert",
"source": "oracle-to-postgres-migration-expert",
"source": "plugins/oracle-to-postgres-migration-expert",
"description": "Expert agent for Oracle-to-PostgreSQL application migrations in .NET solutions. Performs code edits, runs commands, and invokes extension tools to migrate .NET/Oracle data access patterns to PostgreSQL.",
"version": "1.0.0"
},
{
"name": "ospo-sponsorship",
"source": "ospo-sponsorship",
"source": "plugins/ospo-sponsorship",
"description": "Tools and resources for Open Source Program Offices (OSPOs) to identify, evaluate, and manage sponsorship of open source dependencies through GitHub Sponsors, Open Collective, and other funding platforms.",
"version": "1.0.0"
},
{
"name": "partners",
"source": "partners",
"source": "plugins/partners",
"description": "Custom agents that have been created by GitHub partners",
"version": "1.0.0"
},
{
"name": "pcf-development",
"source": "pcf-development",
"source": "plugins/pcf-development",
"description": "Complete toolkit for developing custom code components using Power Apps Component Framework for model-driven and canvas apps",
"version": "1.0.0"
},
{
"name": "phoenix",
"source": "phoenix",
"source": "plugins/phoenix",
"description": "Phoenix AI observability skills for LLM application debugging, evaluation, and tracing. Includes CLI debugging tools, LLM evaluation workflows, and OpenInference tracing instrumentation.",
"version": "1.0.0"
},
{
"name": "php-mcp-development",
"source": "php-mcp-development",
"source": "plugins/php-mcp-development",
"description": "Comprehensive resources for building Model Context Protocol servers using the official PHP SDK with attribute-based discovery, including best practices, project generation, and expert assistance",
"version": "1.0.0"
},
@@ -819,97 +872,109 @@
},
{
"name": "power-apps-code-apps",
"source": "power-apps-code-apps",
"source": "plugins/power-apps-code-apps",
"description": "Complete toolkit for Power Apps Code Apps development including project scaffolding, development standards, and expert guidance for building code-first applications with Power Platform integration.",
"version": "1.0.0"
},
{
"name": "power-bi-development",
"source": "power-bi-development",
"source": "plugins/power-bi-development",
"description": "Comprehensive Power BI development resources including data modeling, DAX optimization, performance tuning, visualization design, security best practices, and DevOps/ALM guidance for building enterprise-grade Power BI solutions.",
"version": "1.0.0"
},
{
"name": "power-platform-architect",
"source": "power-platform-architect",
"source": "plugins/power-platform-architect",
"description": "Solution Architect for the Microsoft Power Platform, turning business requirements into functioning Power Platform solution architectures.",
"version": "1.0.0"
},
{
"name": "power-platform-mcp-connector-development",
"source": "power-platform-mcp-connector-development",
"source": "plugins/power-platform-mcp-connector-development",
"description": "Complete toolkit for developing Power Platform custom connectors with Model Context Protocol integration for Microsoft Copilot Studio",
"version": "1.0.0"
},
{
"name": "project-documenter",
"source": "project-documenter",
"source": "plugins/project-documenter",
"description": "Generate professional project documentation with draw.io architecture diagrams and Word (.docx) output with embedded images. Automatically discovers any project's technology stack and produces Markdown, diagrams, PNG exports, and a formatted Word document.",
"version": "1.0.0"
},
{
"name": "project-planning",
"source": "project-planning",
"source": "plugins/project-planning",
"description": "Tools and guidance for software project planning, feature breakdown, epic management, implementation planning, and task organization for development teams.",
"version": "1.0.0"
},
{
"name": "python-mcp-development",
"source": "python-mcp-development",
"source": "plugins/python-mcp-development",
"description": "Complete toolkit for building Model Context Protocol (MCP) servers in Python using the official SDK with FastMCP. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance.",
"version": "1.0.0"
},
{
"name": "react18-upgrade",
"source": "react18-upgrade",
"source": "plugins/react18-upgrade",
"description": "Enterprise React 18 migration toolkit with specialized agents and skills for upgrading React 16/17 class-component codebases to React 18.3.1. Includes auditor, dependency surgeon, class component migration specialist, automatic batching fixer, and test guardian.",
"version": "1.0.0"
},
{
"name": "react19-upgrade",
"source": "react19-upgrade",
"source": "plugins/react19-upgrade",
"description": "Enterprise React 19 migration toolkit with specialized agents and skills for upgrading React 18 codebases to React 19. Includes auditor, dependency surgeon, source code migrator, and test guardian. Handles removal of deprecated APIs including ReactDOM.render, forwardRef, defaultProps, legacy context, string refs, and more.",
"version": "1.0.0"
},
{
"name": "release-notes-showcase",
"source": "extensions/release-notes-showcase",
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
"version": "1.0.0"
},
{
"name": "roundup",
"source": "roundup",
"source": "plugins/roundup",
"description": "Self-configuring status briefing generator. Learns your communication style from examples, discovers your data sources, and produces draft updates for any audience on demand.",
"version": "1.0.0"
},
{
"name": "ruby-mcp-development",
"source": "ruby-mcp-development",
"source": "plugins/ruby-mcp-development",
"description": "Complete toolkit for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration support.",
"version": "1.0.0"
},
{
"name": "rug-agentic-workflow",
"source": "rug-agentic-workflow",
"source": "plugins/rug-agentic-workflow",
"description": "Three-agent workflow for orchestrated software delivery with an orchestrator plus implementation and QA subagents.",
"version": "1.0.0"
},
{
"name": "rust-mcp-development",
"source": "rust-mcp-development",
"source": "plugins/rust-mcp-development",
"description": "Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations.",
"version": "1.0.0"
},
{
"name": "salesforce-development",
"source": "salesforce-development",
"source": "plugins/salesforce-development",
"description": "Complete Salesforce agentic development environment covering Apex & Triggers, Flow automation, Lightning Web Components, Aura components, and Visualforce pages.",
"version": "1.1.0"
},
{
"name": "security-best-practices",
"source": "security-best-practices",
"source": "plugins/security-best-practices",
"description": "Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications.",
"version": "1.0.0"
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "skill-image-gen",
"source": "skill-image-gen",
"source": "plugins/skill-image-gen",
"description": "Generate images using AI directly from your coding workflow. Supports OpenAI (gpt-image-2) and Google Gemini. BYO API key — the skill guides you through setup on first use.",
"version": "1.0.0"
},
@@ -939,7 +1004,7 @@
},
{
"name": "software-engineering-team",
"source": "software-engineering-team",
"source": "plugins/software-engineering-team",
"description": "7 specialized agents covering the full software development lifecycle from UX design and architecture to security and DevOps.",
"version": "1.0.0"
},
@@ -969,37 +1034,43 @@
},
{
"name": "structured-autonomy",
"source": "structured-autonomy",
"source": "plugins/structured-autonomy",
"description": "Premium planning, thrifty implementation",
"version": "1.0.0"
},
{
"name": "swift-mcp-development",
"source": "swift-mcp-development",
"source": "plugins/swift-mcp-development",
"description": "Comprehensive collection for building Model Context Protocol servers in Swift using the official MCP Swift SDK with modern concurrency features.",
"version": "1.0.0"
},
{
"name": "technical-spike",
"source": "technical-spike",
"source": "plugins/technical-spike",
"description": "Tools for creation, management and research of technical spikes to reduce unknowns and assumptions before proceeding to specification and implementation of solutions.",
"version": "1.0.0"
},
{
"name": "testing-automation",
"source": "testing-automation",
"source": "plugins/testing-automation",
"description": "Comprehensive collection for writing tests, test automation, and test-driven development including unit tests, integration tests, and end-to-end testing strategies.",
"version": "1.0.0"
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "typescript-mcp-development",
"source": "typescript-mcp-development",
"source": "plugins/typescript-mcp-development",
"description": "Complete toolkit for building Model Context Protocol (MCP) servers in TypeScript/Node.js using the official SDK. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance.",
"version": "1.0.0"
},
{
"name": "typespec-m365-copilot",
"source": "typespec-m365-copilot",
"source": "plugins/typespec-m365-copilot",
"description": "Comprehensive collection of prompts, instructions, and resources for building declarative agents and API plugins using TypeSpec for Microsoft 365 Copilot extensibility.",
"version": "1.0.0"
},
@@ -1116,7 +1187,7 @@
},
{
"name": "visual-pr",
"source": "visual-pr",
"source": "plugins/visual-pr",
"description": "Capture, annotate, and embed screenshots and animated GIF demos in pull request descriptions. Includes Playwright-based UI capture, PIL image annotations, PR embedding workflows for GitHub and Azure DevOps, and screen recording with variable timing.",
"version": "1.0.0"
},
@@ -1145,6 +1216,12 @@
"repo": "microsoft/What-I-Did-Copilot"
}
},
{
"name": "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.",
"version": "1.0.0"
},
{
"name": "winappcli",
"description": "Windows app development, packaging, and distribution. Helps with creating Windows installers (MSIX), code signing, certificates, Windows SDK and Windows App SDK setup, package identity for Windows APIs (push notifications, background tasks, share target), appxmanifest authoring, and Microsoft Store distribution. Supports Electron, .NET, C++, Rust, Flutter, and Tauri apps.",
-1
View File
@@ -12,7 +12,6 @@ on:
- hooks
- workflows
- extensions
- .schemas/canvas.schema.json
permissions:
contents: read
@@ -6,7 +6,6 @@ on:
types: [opened, synchronize, reopened]
paths:
- "extensions/**"
- ".schemas/canvas.schema.json"
permissions:
contents: read
@@ -29,110 +28,7 @@ jobs:
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Validate changed canvas extensions
- name: Validate changed extensions
run: |
set -euo pipefail
# Validate schema structure once, even for schema-only PRs.
if ! node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json; then
echo "❌ .schemas/canvas.schema.json failed schema compilation"
exit 1
fi
# Collect changed extension directories.
# Use null-terminated (-z) output from git diff so filenames containing newlines
# or other special characters are read atomically (matches the pattern in skill-check.yml).
# Each extracted name is then validated against a strict allowlist regex before use,
# rejecting anything containing shell metacharacters ($, (, ), spaces, etc.).
declare -A seen_dirs=()
changed_extensions=()
errors=()
while IFS= read -r -d '' file; do
case "$file" in
extensions/*)
ext_name="${file#extensions/}"
ext_name="${ext_name%%/*}"
if [ "$ext_name" = "external-assets" ]; then
continue
fi
# Allowlist: extension directory names must be lowercase alphanumeric + hyphens only.
# Fail for disallowed names so a PR cannot bypass validation by using a nonconforming folder.
if [[ ! "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
errors+=("\`extensions/$ext_name\`: invalid extension directory name (must match ^[a-z0-9][a-z0-9-]*$).")
continue
fi
if [ -z "${seen_dirs[$ext_name]+x}" ]; then
seen_dirs["$ext_name"]=1
changed_extensions+=("extensions/$ext_name")
fi
;;
esac
done < <(git diff --name-only -z "origin/${{ github.base_ref }}...HEAD")
if [ "${#changed_extensions[@]}" -eq 0 ]; then
if [ "${#errors[@]}" -ne 0 ]; then
echo "❌ Canvas extension validation failed:"
for error in "${errors[@]}"; do
echo "- $error"
done
exit 1
fi
echo "No canvas extension directories changed — skipping validation."
exit 0
fi
echo "Validating ${#changed_extensions[@]} extension(s): ${changed_extensions[*]}"
for ext_dir in "${changed_extensions[@]}"; do
if [ ! -d "$ext_dir" ]; then
echo "$ext_dir no longer exists (deleted?), skipping."
continue
fi
if [ ! -f "$ext_dir/extension.mjs" ]; then
errors+=("\`$ext_dir\`: missing required \`extension.mjs\`.")
fi
if [ ! -f "$ext_dir/canvas.json" ]; then
errors+=("\`$ext_dir\`: missing required \`canvas.json\`.")
continue
fi
if [ ! -f "$ext_dir/assets/preview.png" ]; then
errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.")
fi
if ! schema_output="$(node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json --data "$ext_dir/canvas.json" 2>&1)"; then
condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).")
continue
fi
mapfile -t screenshot_paths < <(
node -e 'const fs = require("fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const paths = [manifest?.screenshots?.icon?.path, manifest?.screenshots?.gallery?.path].filter((value) => typeof value === "string" && value.trim().length > 0); for (const value of [...new Set(paths)]) { console.log(value); }' "$ext_dir/canvas.json"
)
for screenshot_path in "${screenshot_paths[@]}"; do
if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.(png|jpe?g|gif|webp|svg)$ ]]; then
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` is not a valid assets path.")
continue
fi
if [ ! -f "$ext_dir/$screenshot_path" ]; then
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` does not exist in the extension directory.")
fi
done
done
if [ "${#errors[@]}" -ne 0 ]; then
echo "❌ Canvas extension validation failed:"
for error in "${errors[@]}"; do
echo "- $error"
done
exit 1
fi
echo "✅ All changed canvas extensions passed validation."
npm run plugin:validate
-121
View File
@@ -1,121 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Canvas Extension Manifest",
"description": "Schema for extensions/<name>/canvas.json files",
"type": "object",
"required": [
"id",
"name",
"description",
"version",
"keywords",
"screenshots"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the canvas extension",
"pattern": "^[a-z0-9-]+$",
"minLength": 1,
"maxLength": 100
},
"name": {
"type": "string",
"description": "Display name for the extension",
"minLength": 1,
"maxLength": 100
},
"description": {
"type": "string",
"description": "Human-friendly description of what the extension does",
"minLength": 1,
"maxLength": 500
},
"version": {
"type": "string",
"description": "Semantic version of the extension metadata",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
},
"author": {
"type": "object",
"description": "Optional author metadata",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"url": {
"type": "string",
"format": "uri",
"maxLength": 2048
}
}
},
"keywords": {
"type": "array",
"description": "Keywords used for search and filtering",
"items": {
"type": "string",
"pattern": "^[a-z0-9-]+$",
"minLength": 1,
"maxLength": 50
},
"minItems": 1,
"maxItems": 20,
"uniqueItems": true
},
"screenshots": {
"type": "object",
"description": "Screenshot metadata for icon and gallery cards",
"required": [
"icon",
"gallery"
],
"additionalProperties": false,
"properties": {
"icon": {
"$ref": "#/definitions/screenshot"
},
"gallery": {
"$ref": "#/definitions/screenshot"
}
}
}
},
"definitions": {
"screenshot": {
"type": "object",
"required": [
"path",
"type"
],
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "Path relative to the extension root",
"pattern": "^assets/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)*\\.(png|jpg|jpeg|gif|webp|svg)$",
"minLength": 1,
"maxLength": 200
},
"type": {
"type": "string",
"description": "MIME type for the referenced image",
"enum": [
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/svg+xml"
]
}
}
}
}
}
+22
View File
@@ -21,6 +21,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── hooks/ # Automated workflow hooks (folders with README.md + hooks.json)
├── workflows/ # Agentic Workflows (.md files for GitHub Actions automation)
├── plugins/ # Installable plugin packages (folders with plugin.json)
├── extensions/ # Canvas extensions (each with extension.mjs and plugin metadata)
├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts
└── scripts/ # Utility scripts
@@ -81,6 +82,19 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://agentskills.io/specification)
#### Canvas Extensions (extensions/\*)
- Each extension folder must include `extension.mjs`
- Extension metadata must live at `.github/plugin/plugin.json`
- Extension `plugin.json` **must** follow the convention:
- `name`, `description`, `version` are required
- `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))
- Optional: `author`, `keywords` fields
- **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
- Do not add `canvas.json`; website metadata is sourced from `.github/plugin/plugin.json`
#### Hook Folders (hooks/\*/README.md)
- Each hook is a folder containing a `README.md` file with frontmatter
@@ -160,6 +174,14 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
5. Run `npm run build` to update README.md and marketplace.json
6. Verify the plugin appears in `.github/plugin/marketplace.json`
**For Canvas Extensions:**
1. Create/update the extension in `extensions/<extension-id>/` with `extension.mjs`
2. Add `.github/plugin/plugin.json` metadata (required: `name`, `description`, `version`, `logo: "assets/preview.png"`, `extensions: "."`; optional: `author`, `keywords`)
3. Ensure `assets/preview.png` exists as the primary visual asset
4. Run `npm run plugin:validate` to validate plugin and extension metadata
5. Run `npm run build` to regenerate website data and marketplace output
**For External Plugins:**
1. Do not open a direct PR that edits `plugins/external.json` for a public third-party plugin submission
+17
View File
@@ -11,6 +11,7 @@ Thank you for your interest in contributing to the Awesome GitHub Copilot reposi
- [Adding Instructions](#adding-instructions)
- [Adding Agents](#adding-an-agent)
- [Adding Skills](#adding-skills)
- [Adding Canvas Extensions](#adding-canvas-extensions)
- [Adding Plugins](#adding-plugins)
- [Adding Hooks](#adding-hooks)
- [Adding Agentic Workflows](#adding-agentic-workflows)
@@ -137,6 +138,21 @@ Skills are self-contained folders in the `skills/` directory that include a `SKI
3. **Add optional assets**: Keep bundled assets reasonably sized (under 5MB each) and reference them from `SKILL.md`
4. **Validate and update docs**: Run `npm run skill:validate` and then `npm run build` to update the generated README tables
### Adding Canvas Extensions
Canvas extensions live in `extensions/<extension-id>/` and are installable through plugin metadata.
1. **Create/update extension metadata**: Add `.github/plugin/plugin.json` in the extension folder
2. **Use convention-based metadata**: Follow the extension plugin.json structure:
- Required: `name` (matching folder name), `description`, `version`
- Optional: `author`, `keywords`
- `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))
- **Never** include `x-awesome-copilot` field (use convention-based assets only)
3. **Screenshot requirements**: Create `assets/preview.png` as your primary visual
4. **Do not add `canvas.json`**: Extension website metadata is now sourced from `.github/plugin/plugin.json`
5. **Validate before submitting**: Run `npm run plugin:validate` to check compliance with conventions
### Adding Plugins
Plugins group related agents, commands, and skills around specific themes or workflows, making it easy for users to install comprehensive toolkits via GitHub Copilot CLI.
@@ -183,6 +199,7 @@ plugins/my-plugin-id/
- **Declarative content**: Plugin content is specified via `agents`, `commands`, and `skills` arrays in plugin.json — source files live in top-level directories and are materialized into plugins by CI
- **Valid references**: All paths referenced in plugin.json must point to existing source files in the repository
- **Optional extension links**: Curated plugins can reference extensions using `x-awesome-copilot.extensions` with paths like `./extensions/<extension-id>`
- **Instructions excluded**: Instructions are standalone resources and are not part of plugins
- **Clear purpose**: The plugin should solve a specific problem or workflow
- **Validate before submitting**: Run `npm run plugin:validate` to ensure your plugin is valid
+44 -29
View File
@@ -6,6 +6,7 @@ import { ROOT_FOLDER } from "./constants.mjs";
import { readExternalPlugins } from "./external-plugin-validation.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json");
/**
@@ -30,43 +31,58 @@ function readPluginMetadata(pluginDir) {
}
}
function collectLocalPluginsFromRoot(rootDir, sourcePrefix, includeEntry = () => true) {
if (!fs.existsSync(rootDir)) {
return [];
}
const entries = fs.readdirSync(rootDir, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.filter(entry => includeEntry(entry.name))
.map(entry => entry.name)
.sort();
const plugins = [];
for (const dirName of entries) {
const pluginPath = path.join(rootDir, dirName);
const metadata = readPluginMetadata(pluginPath);
if (!metadata) {
continue;
}
plugins.push({
name: metadata.name,
source: `${sourcePrefix}/${dirName}`,
description: metadata.description,
version: metadata.version || "1.0.0"
});
}
return plugins;
}
/**
* Generate marketplace.json from plugin directories
*/
function generateMarketplace() {
console.log("Generating marketplace.json...");
if (!fs.existsSync(PLUGINS_DIR)) {
console.error(`Error: Plugins directory not found at ${PLUGINS_DIR}`);
if (!fs.existsSync(PLUGINS_DIR) && !fs.existsSync(EXTENSIONS_DIR)) {
console.error(`Error: Neither plugins directory (${PLUGINS_DIR}) nor extensions directory (${EXTENSIONS_DIR}) was found`);
process.exit(1);
}
// Read all plugin directories
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort();
const plugins = [
...collectLocalPluginsFromRoot(PLUGINS_DIR, "plugins"),
...collectLocalPluginsFromRoot(
EXTENSIONS_DIR,
"extensions",
(entryName) => fs.existsSync(path.join(EXTENSIONS_DIR, entryName, "extension.mjs"))
)
];
console.log(`Found ${pluginDirs.length} plugin directories`);
// Read metadata for each plugin
const plugins = [];
for (const dirName of pluginDirs) {
const pluginPath = path.join(PLUGINS_DIR, dirName);
const metadata = readPluginMetadata(pluginPath);
if (metadata) {
plugins.push({
name: metadata.name,
source: dirName,
description: metadata.description,
version: metadata.version || "1.0.0"
});
console.log(`✓ Added plugin: ${metadata.name}`);
} else {
console.log(`✗ Skipped: ${dirName} (no valid plugin.json)`);
}
}
console.log(`Found ${plugins.length} local plugin manifests`);
// Read external plugins and merge as-is
const { plugins: externalPlugins, errors: externalErrors, warnings: externalWarnings } = readExternalPlugins({
@@ -96,8 +112,7 @@ function generateMarketplace() {
name: "awesome-copilot",
metadata: {
description: "Community-driven collection of GitHub Copilot plugins, agents, prompts, and skills",
version: "1.0.0",
pluginRoot: "./plugins"
version: "1.0.0"
},
owner: {
name: "GitHub",
+190 -85
View File
@@ -560,6 +560,7 @@ function getAgentFiles(agentDir, pluginRootPath) {
*/
function generatePluginsData(gitDates) {
const plugins = [];
const extensionEntriesByName = new Map();
if (!fs.existsSync(PLUGINS_DIR)) {
return { items: [], filters: { tags: [] } };
@@ -569,6 +570,53 @@ function generatePluginsData(gitDates) {
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory());
if (fs.existsSync(EXTENSIONS_DIR)) {
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((entry) => {
if (!entry.isDirectory()) return false;
return fs.existsSync(path.join(EXTENSIONS_DIR, entry.name, "extension.mjs"));
})
.map((entry) => entry.name)
.sort((a, b) => a.localeCompare(b));
for (const extensionDirName of extensionDirs) {
const extensionDir = path.join(EXTENSIONS_DIR, extensionDirName);
const pluginJsonPath = path.join(extensionDir, ".github", "plugin", "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
continue;
}
try {
const extensionPlugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
const pluginName = normalizeText(extensionPlugin.name, extensionDirName);
const pluginDescription = normalizeText(extensionPlugin.description, "Canvas extension");
const extensionKeywords = Array.isArray(extensionPlugin.keywords)
? [...new Set(extensionPlugin.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: [];
const relPath = `extensions/${extensionDirName}`;
const extensionItem = {
kind: "extension",
path: relPath,
};
extensionEntriesByName.set(pluginName, {
id: pluginName,
name: pluginName,
description: pluginDescription,
path: relPath,
tags: extensionKeywords,
itemCount: 1,
items: [extensionItem],
generatedFromExtension: true,
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
searchText: `${pluginName} ${pluginDescription} ${extensionKeywords.join(" ")} canvas extension`.toLowerCase(),
});
} catch (e) {
console.warn(`Failed to parse extension plugin manifest for ${extensionDirName}: ${e.message}`);
}
}
}
for (const dir of pluginDirs) {
const pluginDir = path.join(PLUGINS_DIR, dir.name);
const jsonPath = path.join(pluginDir, ".github/plugin", "plugin.json");
@@ -578,7 +626,18 @@ function generatePluginsData(gitDates) {
try {
const data = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
const relPath = `plugins/${dir.name}`;
const dates = gitDates[relPath] || gitDates[`${relPath}/`] || {};
const extensionRefs = Array.isArray(data?.["x-awesome-copilot"]?.extensions)
? data["x-awesome-copilot"].extensions
: [];
const extensionItems = extensionRefs
.map((entry) => normalizeText(entry))
.filter(Boolean)
.map((entry) => entry.replace(/^\.\/+/, "").replace(/\/$/, ""))
.filter((entry) => entry.startsWith("extensions/"))
.map((entry) => ({
kind: "extension",
path: entry,
}));
const agentItems = (data.agents || []).flatMap((agent) => {
const agentPath = agent.replace("./", "");
@@ -601,27 +660,34 @@ function generatePluginsData(gitDates) {
...agentItems,
...(data.commands || []).map((p) => ({ kind: "prompt", path: p })),
...(data.skills || []).map((p) => ({ kind: "skill", path: p })),
...extensionItems,
];
const tags = data.keywords || data.tags || [];
const pluginName = data.name || dir.name;
plugins.push({
id: dir.name,
name: data.name || dir.name,
name: pluginName,
description: data.description || "",
path: relPath,
tags: tags,
itemCount: items.length,
items: items,
lastUpdated: dates.lastModified || null,
searchText: `${data.name || dir.name} ${data.description || ""
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
searchText: `${pluginName} ${data.description || ""
} ${tags.join(" ")}`.toLowerCase(),
});
extensionEntriesByName.delete(pluginName);
} catch (e) {
console.warn(`Failed to parse plugin: ${dir.name}`, e.message);
}
}
for (const extensionPlugin of extensionEntriesByName.values()) {
plugins.push(extensionPlugin);
}
// Load external plugins from plugins/external.json
const externalJsonPath = path.join(PLUGINS_DIR, "external.json");
if (fs.existsSync(externalJsonPath)) {
@@ -994,6 +1060,94 @@ function normalizeExternalScreenshotRole(value, ref) {
};
}
function normalizeExtensionScreenshotRole(value, relPath, ref) {
if (!value) return null;
if (typeof value === "string") {
if (/^https?:\/\//i.test(value)) {
return {
path: null,
type: getImageMimeType(value),
imageUrl: value,
};
}
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
const repoPath = normalized.startsWith(`${relPath}/`) ? normalized : `${relPath}/${normalized}`;
return {
path: repoPath,
type: getImageMimeType(repoPath),
imageUrl: buildRepoImageUrl(repoPath, ref),
};
}
const pathValue = normalizeText(value.path);
const urlValue = normalizeText(value.url);
if (!pathValue && !urlValue) return null;
const pathEntry = pathValue
? normalizeExtensionScreenshotRole(pathValue, relPath, ref)
: null;
const urlEntry = urlValue
? normalizeExtensionScreenshotRole(urlValue, relPath, ref)
: null;
return {
path: pathEntry?.path || null,
type: normalizeText(value.type) || pathEntry?.type || urlEntry?.type || null,
imageUrl: urlEntry?.imageUrl || pathEntry?.imageUrl || null,
};
}
function resolveExtensionScreenshots(pluginJson, extensionDir, relPath, ref) {
const inferredAssets = getExtensionAssetInfo(extensionDir, relPath, ref);
const inferredIcon = inferredAssets?.screenshots?.icon
? {
path: inferredAssets.screenshots.icon.path,
type: inferredAssets.screenshots.icon.type,
imageUrl: inferredAssets.screenshots.icon.path
? buildRepoImageUrl(inferredAssets.screenshots.icon.path, ref)
: null,
}
: null;
const inferredGallery = inferredAssets?.screenshots?.gallery
? {
path: inferredAssets.screenshots.gallery.path,
type: inferredAssets.screenshots.gallery.type,
imageUrl: inferredAssets.screenshots.gallery.path
? buildRepoImageUrl(inferredAssets.screenshots.gallery.path, ref)
: null,
}
: null;
const logoEntry = normalizeExtensionScreenshotRole(pluginJson?.logo, relPath, ref);
const screenshotConfig = pluginJson?.["x-awesome-copilot"]?.screenshots || {};
const iconEntry = normalizeExtensionScreenshotRole(screenshotConfig.icon, relPath, ref);
const galleryRaw = screenshotConfig.gallery;
const firstGalleryEntry = Array.isArray(galleryRaw) ? galleryRaw[0] : galleryRaw;
const galleryEntry = normalizeExtensionScreenshotRole(firstGalleryEntry, relPath, ref);
const finalIcon = iconEntry || logoEntry || inferredIcon;
const finalGallery = galleryEntry || logoEntry || inferredGallery || finalIcon;
return {
screenshots: {
icon: finalIcon
? {
path: finalIcon.path,
type: finalIcon.type,
}
: null,
gallery: finalGallery
? {
path: finalGallery.path,
type: finalGallery.type,
}
: null,
},
assetPath: finalIcon?.path || inferredAssets?.assetPath || null,
imageUrl: finalIcon?.imageUrl || inferredAssets?.imageUrl || null,
};
}
function generateCanvasManifest(gitDates, commitSha) {
const items = [];
@@ -1021,17 +1175,28 @@ function generateCanvasManifest(gitDates, commitSha) {
const packageJson = fs.existsSync(packageJsonPath)
? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
: {};
const canvasJsonPath = path.join(extensionDir, "canvas.json");
const canvasJson = fs.existsSync(canvasJsonPath)
? JSON.parse(fs.readFileSync(canvasJsonPath, "utf-8"))
const pluginJsonPath = path.join(extensionDir, ".github", "plugin", "plugin.json");
const pluginJson = fs.existsSync(pluginJsonPath)
? JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"))
: {};
const keywords = Array.isArray(packageJson.keywords)
? [...new Set(packageJson.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: [];
const extensionDescription = normalizeText(packageJson.description, "Canvas extension");
const extensionName = normalizeText(packageJson.name, dir.name);
const extensionVersion = normalizeText(packageJson.version, "1.0.0");
const screenshots = getExtensionAssetInfo(extensionDir, relPath, commitSha);
const keywordsSource = Array.isArray(pluginJson.keywords)
? pluginJson.keywords
: Array.isArray(packageJson.keywords)
? packageJson.keywords
: [];
const keywords = [...new Set(
keywordsSource
.filter((keyword) => typeof keyword === "string")
.map((keyword) => keyword.trim())
.filter(Boolean)
)].sort((a, b) => a.localeCompare(b));
const extensionDescription = normalizeText(
pluginJson.description,
normalizeText(packageJson.description, "Canvas extension")
);
const extensionName = normalizeText(pluginJson.name, normalizeText(packageJson.name, dir.name));
const extensionVersion = normalizeText(pluginJson.version, normalizeText(packageJson.version, "1.0.0"));
const screenshots = resolveExtensionScreenshots(pluginJson, extensionDir, relPath, commitSha);
const canvasFiles = getExtensionCanvasFiles(extensionDir);
const canvases = [];
for (const canvasFile of canvasFiles) {
@@ -1045,6 +1210,7 @@ function generateCanvasManifest(gitDates, commitSha) {
/\\/g,
"/"
)}`;
const installCommand = `copilot plugin install ${extensionName}@awesome-copilot`;
for (const canvas of canvasEntries) {
const canvasId = normalizeText(canvas.id, dir.name);
@@ -1055,6 +1221,7 @@ function generateCanvasManifest(gitDates, commitSha) {
canvasId,
extensionId: dir.name,
extensionName,
pluginName: extensionName,
name: canvasName,
version: extensionVersion,
description: canvasDescription,
@@ -1065,9 +1232,10 @@ function generateCanvasManifest(gitDates, commitSha) {
imageUrl: screenshots?.imageUrl || null,
assetPath: screenshots?.assetPath || null,
installUrl,
installCommand,
sourceUrl: null,
external: false,
author: normalizeAuthor(canvasJson.author),
author: normalizeAuthor(pluginJson.author),
keywords,
});
}
@@ -1128,6 +1296,7 @@ function generateCanvasManifest(gitDates, commitSha) {
canvasId,
extensionId: id,
extensionName: name,
pluginName: null,
name,
version: normalizeText(ext?.version, "1.0.0"),
description: normalizeText(ext?.description, "External canvas extension"),
@@ -1138,6 +1307,7 @@ function generateCanvasManifest(gitDates, commitSha) {
imageUrl,
assetPath,
installUrl,
installCommand: null,
sourceUrl: sourceUrl || null,
external: true,
author: normalizeAuthor(ext?.author),
@@ -1163,12 +1333,12 @@ function generateCanvasManifest(gitDates, commitSha) {
};
}
function generateExtensionsData(canvasManifestData) {
if (!canvasManifestData || !Array.isArray(canvasManifestData.items)) {
function generateExtensionsData(extensionManifestData) {
if (!extensionManifestData || !Array.isArray(extensionManifestData.items)) {
return { items: [], filters: { keywords: [] } };
}
const items = canvasManifestData.items.map((item) => ({
const items = extensionManifestData.items.map((item) => ({
...item,
keywords: Array.isArray(item.keywords) ? item.keywords : [],
screenshots: item.screenshots || { icon: null, gallery: null },
@@ -1182,69 +1352,6 @@ function generateExtensionsData(canvasManifestData) {
return { items, filters };
}
function writePerExtensionCanvasManifests(canvasManifestData) {
const manifests = new Map();
function toExtensionRelativePath(assetPath, extensionId) {
const normalizedPath = normalizeText(assetPath).replace(/\\/g, "/");
if (!normalizedPath) return null;
const prefix = `extensions/${extensionId}/`;
return normalizedPath.startsWith(prefix)
? normalizedPath.slice(prefix.length)
: normalizedPath;
}
function toRelativeScreenshots(screenshots, extensionId) {
if (!screenshots) return { icon: null, gallery: null };
const toRelativeEntry = (entry) =>
entry
? {
...entry,
path: toExtensionRelativePath(entry.path, extensionId),
}
: null;
return {
icon: toRelativeEntry(screenshots.icon),
gallery: toRelativeEntry(screenshots.gallery),
};
}
for (const item of canvasManifestData.items || []) {
if (!item || item.external || !item.extensionId || !item.path) {
continue;
}
// We assume one canvas per extension folder.
if (manifests.has(item.extensionId)) {
continue;
}
manifests.set(item.extensionId, {
id: item.canvasId || item.id,
name: item.name,
description: item.description || "Canvas extension",
version: item.version || "1.0.0",
...(item.author ? { author: item.author } : {}),
keywords: Array.isArray(item.keywords)
? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b))
: [],
screenshots: toRelativeScreenshots(
item.screenshots || { icon: null, gallery: null },
item.extensionId
),
});
}
for (const [extensionId, manifest] of manifests.entries()) {
const canvasManifestPath = path.join(
EXTENSIONS_DIR,
extensionId,
"canvas.json"
);
fs.writeFileSync(canvasManifestPath, JSON.stringify(manifest, null, 2));
}
}
/**
* Generate tools metadata from website/data/tools.yml
*/
@@ -1585,8 +1692,8 @@ async function main() {
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
);
const canvasManifestData = generateCanvasManifest(gitDates, commitSha);
const extensionsData = generateExtensionsData(canvasManifestData);
const extensionManifestData = generateCanvasManifest(gitDates, commitSha);
const extensionsData = generateExtensionsData(extensionManifestData);
const extensions = extensionsData.items;
console.log(
`✓ Generated ${extensions.length} extensions (${extensionsData.filters.keywords.length} keywords)`
@@ -1655,8 +1762,6 @@ async function main() {
JSON.stringify(extensionsData, null, 2)
);
writePerExtensionCanvasManifests(canvasManifestData);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "tools.json"),
JSON.stringify(toolsData, null, 2)
+35 -1
View File
@@ -38,6 +38,10 @@ function resolveSource(relPath) {
const skillName = relPath.replace(/^\.\/skills\//, "").replace(/\/$/, "");
return path.join(ROOT_FOLDER, "skills", skillName);
}
if (relPath.startsWith("./extensions/")) {
const extensionName = relPath.replace(/^\.\/extensions\//, "").replace(/\/$/, "");
return path.join(ROOT_FOLDER, "extensions", extensionName);
}
return null;
}
@@ -56,6 +60,7 @@ function materializePlugins() {
let totalAgents = 0;
let totalSkills = 0;
let totalExtensions = 0;
let warnings = 0;
let errors = 0;
@@ -119,6 +124,27 @@ function materializePlugins() {
}
}
// Process extension references from x-awesome-copilot.extensions
const extensionRefs = Array.isArray(metadata?.["x-awesome-copilot"]?.extensions)
? metadata["x-awesome-copilot"].extensions
: [];
for (const relPath of extensionRefs) {
const src = resolveSource(relPath);
if (!src) {
console.warn(`${pluginName}: Unknown extension path format: ${relPath}`);
warnings++;
continue;
}
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) {
console.warn(`${pluginName}: Extension source directory not found: ${src}`);
warnings++;
continue;
}
const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, ""));
copyDirRecursive(src, dest);
totalExtensions++;
}
// Rewrite plugin.json to use folder paths instead of individual file paths.
// On staged, paths like ./agents/foo.md point to individual source files.
// On main, after materialization, we only need the containing directory.
@@ -139,6 +165,13 @@ function materializePlugins() {
changed = true;
}
if (Array.isArray(rewritten?.["x-awesome-copilot"]?.extensions) &&
rewritten["x-awesome-copilot"].extensions.length > 0) {
rewritten["x-awesome-copilot"].extensions =
rewritten["x-awesome-copilot"].extensions.map((p) => p.replace(/\/$/, ""));
changed = true;
}
if (changed) {
fs.writeFileSync(pluginJsonPath, JSON.stringify(rewritten, null, 2) + "\n", "utf8");
}
@@ -146,12 +179,13 @@ function materializePlugins() {
const counts = [];
if (metadata.agents?.length) counts.push(`${metadata.agents.length} agents`);
if (metadata.skills?.length) counts.push(`${metadata.skills.length} skills`);
if (extensionRefs.length) counts.push(`${extensionRefs.length} extensions`);
if (counts.length) {
console.log(`${pluginName}: ${counts.join(", ")}`);
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills.`);
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
if (warnings > 0) {
console.log(`${warnings} warning(s).`);
}
-75
View File
@@ -1,75 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import Ajv from "ajv";
import addFormats from "ajv-formats";
function parseArgs(argv) {
const args = { schema: null, data: null };
for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--schema") {
args.schema = argv[i + 1] || null;
i += 1;
} else if (arg === "--data") {
args.data = argv[i + 1] || null;
i += 1;
}
}
return args;
}
function readJson(filePath) {
const content = fs.readFileSync(filePath, "utf8");
return JSON.parse(content);
}
const args = parseArgs(process.argv);
if (!args.schema) {
console.error("Missing required argument: --schema <path>");
process.exit(1);
}
const schemaPath = path.resolve(process.cwd(), args.schema);
let schema;
try {
schema = readJson(schemaPath);
} catch (error) {
console.error(`Invalid schema JSON at ${args.schema}: ${error.message}`);
process.exit(1);
}
const ajv = new Ajv({ strict: false, allErrors: true });
addFormats(ajv);
let validate;
try {
validate = ajv.compile(schema);
} catch (error) {
console.error(`Invalid schema at ${args.schema}: ${error.message}`);
process.exit(1);
}
if (!args.data) {
console.log(`Schema is valid: ${args.schema}`);
process.exit(0);
}
const dataPath = path.resolve(process.cwd(), args.data);
let data;
try {
data = readJson(dataPath);
} catch (error) {
console.error(`Invalid data JSON at ${args.data}: ${error.message}`);
process.exit(1);
}
const valid = validate(data);
if (!valid) {
const message = ajv.errorsText(validate.errors, { separator: "; " });
console.error(`Schema validation failed for ${args.data}: ${message}`);
process.exit(1);
}
console.log(`Schema validation passed: ${args.data}`);
+182 -20
View File
@@ -6,6 +6,7 @@ import { ROOT_FOLDER } from "./constants.mjs";
import { readExternalPlugins } from "./external-plugin-validation.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
// Validation functions
function validateName(name, folderName) {
@@ -77,6 +78,29 @@ function sortPluginEntries(entries) {
return [...entries].sort((left, right) => left.localeCompare(right));
}
function parseJsonFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch (err) {
return { parseError: err.message };
}
}
function getExtensionFolderNames() {
if (!fs.existsSync(EXTENSIONS_DIR)) {
return [];
}
return fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((entry) => {
if (!entry.isDirectory()) return false;
const extensionEntryPoint = path.join(EXTENSIONS_DIR, entry.name, "extension.mjs");
return fs.existsSync(extensionEntryPoint);
})
.map((entry) => entry.name)
.sort();
}
function validateSpecPaths(plugin) {
const errors = [];
const specs = {
@@ -131,9 +155,51 @@ function validateSpecPaths(plugin) {
return errors;
}
function validateCuratedPluginExtensionRefs(plugin) {
const errors = [];
const extensionRefs = plugin?.["x-awesome-copilot"]?.extensions;
if (extensionRefs === undefined) {
return errors;
}
if (!Array.isArray(extensionRefs)) {
errors.push('x-awesome-copilot.extensions must be an array');
return errors;
}
if (!arraysEqual(extensionRefs, sortPluginEntries(extensionRefs))) {
errors.push('x-awesome-copilot.extensions must be sorted alphabetically');
}
const knownExtensions = new Set(getExtensionFolderNames());
for (let i = 0; i < extensionRefs.length; i++) {
const ref = extensionRefs[i];
if (typeof ref !== "string") {
errors.push(`x-awesome-copilot.extensions[${i}] must be a string`);
continue;
}
if (!ref.startsWith("./extensions/")) {
errors.push(`x-awesome-copilot.extensions[${i}] must start with "./extensions/"`);
continue;
}
const normalized = ref.replace(/^\.\/extensions\//, "").replace(/\/$/, "");
if (!normalized) {
errors.push(`x-awesome-copilot.extensions[${i}] must include an extension folder name`);
continue;
}
if (!knownExtensions.has(normalized)) {
errors.push(`x-awesome-copilot.extensions[${i}] source not found: extensions/${normalized}`);
}
}
return errors;
}
function validatePlugin(folderName) {
const pluginDir = path.join(PLUGINS_DIR, folderName);
const errors = [];
let parsedPlugin = null;
// Rule 1: Must have .github/plugin/plugin.json
const pluginJsonPath = path.join(pluginDir, ".github/plugin", "plugin.json");
@@ -153,9 +219,10 @@ function validatePlugin(folderName) {
try {
const raw = fs.readFileSync(pluginJsonPath, "utf-8");
plugin = JSON.parse(raw);
parsedPlugin = plugin;
} catch (err) {
errors.push(`failed to parse plugin.json: ${err.message}`);
return errors;
return { errors, plugin: parsedPlugin };
}
// Rule 3 & 4: name, description, version
@@ -176,35 +243,101 @@ function validatePlugin(folderName) {
const specErrors = validateSpecPaths(plugin);
errors.push(...specErrors);
return errors;
const extensionRefErrors = validateCuratedPluginExtensionRefs(plugin);
errors.push(...extensionRefErrors);
return { errors, plugin: parsedPlugin };
}
function validateExtensionScreenshotPath(extensionDir, pathValue, fieldName, errors) {
if (!pathValue || typeof pathValue !== "string") {
errors.push(`${fieldName} must be a string path`);
return;
}
const normalizedPath = pathValue.replace(/^\.\/+/, "");
const absolutePath = path.join(extensionDir, normalizedPath);
if (!fs.existsSync(absolutePath)) {
errors.push(`${fieldName} not found: ${normalizedPath}`);
}
}
function validateExtensionManifest(folderName) {
const extensionDir = path.join(EXTENSIONS_DIR, folderName);
const errors = [];
let parsedPlugin = null;
const pluginJsonPath = path.join(extensionDir, ".github/plugin", "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
errors.push("missing required file: .github/plugin/plugin.json");
return { errors, plugin: parsedPlugin };
}
const parsed = parseJsonFile(pluginJsonPath);
if (parsed.parseError) {
errors.push(`failed to parse plugin.json: ${parsed.parseError}`);
return { errors, plugin: parsedPlugin };
}
parsedPlugin = parsed;
const nameErrors = validateName(parsed.name, folderName);
errors.push(...nameErrors);
const descError = validateDescription(parsed.description);
if (descError) errors.push(descError);
const versionError = validateVersion(parsed.version);
if (versionError) errors.push(versionError);
const keywordsError = validateKeywords(parsed.keywords ?? parsed.tags);
if (keywordsError) errors.push(keywordsError);
// Extension convention: logo must be exactly "assets/preview.png"
if (parsed.logo !== "assets/preview.png") {
errors.push('logo must be exactly "assets/preview.png" (extension convention)');
} else {
validateExtensionScreenshotPath(extensionDir, parsed.logo, "logo", errors);
}
// Extension convention: x-awesome-copilot must not be present
if (parsed["x-awesome-copilot"] !== undefined) {
errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)");
}
// Extension convention: extensions field must be "."
if (parsed.extensions !== ".") {
errors.push('extensions field must be exactly "." (extension convention)');
}
return { errors, plugin: parsedPlugin };
}
// Main validation function
function validatePlugins() {
if (!fs.existsSync(PLUGINS_DIR)) {
console.log("No plugins directory found - validation skipped");
return true;
}
const pluginDirs = fs.existsSync(PLUGINS_DIR)
? fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
: [];
const extensionDirs = getExtensionFolderNames();
const pluginDirs = fs
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
if (pluginDirs.length === 0) {
console.log("No plugin directories found - validation skipped");
if (pluginDirs.length === 0 && extensionDirs.length === 0) {
console.log("No plugins or extension plugin manifests found - validation skipped");
return true;
}
console.log(`Validating ${pluginDirs.length} plugins...\n`);
console.log(`Validating ${extensionDirs.length} extensions as plugin sources...\n`);
let hasErrors = false;
const seenNames = new Set();
const localPluginNames = [];
for (const dir of pluginDirs) {
console.log(`Validating ${dir}...`);
const errors = validatePlugin(dir);
const { errors, plugin } = validatePlugin(dir);
if (errors.length > 0) {
console.error(`${dir}:`);
@@ -214,18 +347,47 @@ function validatePlugins() {
console.log(`${dir} is valid`);
}
// Rule 10: duplicate names
if (seenNames.has(dir)) {
console.error(`❌ Duplicate plugin name "${dir}"`);
if (plugin?.name) {
if (seenNames.has(plugin.name)) {
console.error(`❌ Duplicate plugin name "${plugin.name}"`);
hasErrors = true;
} else {
seenNames.add(plugin.name);
localPluginNames.push(plugin.name);
}
}
}
if (extensionDirs.length > 0) {
console.log("");
}
for (const dir of extensionDirs) {
console.log(`Validating extension ${dir}...`);
const { errors, plugin } = validateExtensionManifest(dir);
if (errors.length > 0) {
console.error(`❌ extension ${dir}:`);
errors.forEach((e) => console.error(` - ${e}`));
hasErrors = true;
} else {
seenNames.add(dir);
console.log(`✅ extension ${dir} is valid`);
}
if (plugin?.name) {
if (seenNames.has(plugin.name)) {
console.error(`❌ Duplicate plugin name "${plugin.name}"`);
hasErrors = true;
} else {
seenNames.add(plugin.name);
localPluginNames.push(plugin.name);
}
}
}
console.log("\nValidating external plugin catalog...");
const { plugins: externalPlugins, errors: externalErrors, warnings: externalWarnings } = readExternalPlugins({
localPluginNames: pluginDirs,
localPluginNames,
policy: "marketplace",
});
@@ -240,7 +402,7 @@ function validatePlugins() {
}
if (!hasErrors) {
console.log(`\n✅ All ${pluginDirs.length} plugins and the external catalog are valid`);
console.log(`\n✅ All ${pluginDirs.length} plugins, ${extensionDirs.length} extensions, and the external catalog are valid`);
}
return !hasErrors;
@@ -1,6 +1,5 @@
{
"id": "accessibility-kanban",
"name": "Accessibility Kanban",
"name": "accessibility-kanban",
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"planning-workflow",
"status-tracking"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "agent-arcade-canvas",
"name": "Agent Arcade",
"name": "arcade-canvas",
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"retro-games",
"session-breaks"
],
"screenshots": {
"icon": {
"path": "assets/icon.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "backlog-swipe-triage",
"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.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"swipe-interface",
"workflow-automation"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "chromium-control-canvas",
"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.",
"version": "1.0.0",
"author": {
@@ -16,14 +15,6 @@
"ui-testing",
"web-navigation"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "color-orb",
"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.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"sse-events",
"visual-feedback"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "diagram",
"name": "Diagram Explorer",
"name": "diagram-viewer",
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"node-drilldown",
"relationship-visualization"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "feedback-themes",
"name": "Feedback Themes",
"name": "feedback-themes",
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"theme-analysis",
"trend-discovery"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "gesture-review",
"name": "Gesture PR Review",
"name": "gesture-review",
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"mediapipe",
"pull-request-review"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "appmod-cockpit",
"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.",
"version": "1.0.0",
"author": {
@@ -16,14 +15,6 @@
"modernization-cockpit",
"validation-gates"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "release-notes-showcase",
"name": "Release Notes Showcase",
"name": "release-notes-showcase",
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"product-updates",
"release-notes"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "site-studio",
"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.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"section-planning",
"site-builder"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "token-pacman",
"name": "Token Pac-Man",
"name": "token-pacman",
"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",
"author": {
@@ -15,14 +14,6 @@
"quota-tracking",
"session-usage"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
@@ -1,6 +1,5 @@
{
"id": "where-was-i",
"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.",
"version": "1.0.0",
"author": {
@@ -15,14 +14,6 @@
"pull-request-context",
"resume-work"
],
"screenshots": {
"icon": {
"path": "assets/preview.png",
"type": "image/png"
},
"gallery": {
"path": "assets/preview.png",
"type": "image/png"
}
}
"logo": "assets/preview.png",
"extensions": "."
}
+2 -2
View File
@@ -18,8 +18,8 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
<div class="page-content">
<div class="container">
<div class="how-to-install">
<p><strong>Installing in the GitHub Copilot app:</strong> Click <strong>Copy URL</strong>, then ask Copilot to install that extension URL.</p>
<p><strong>Example:</strong> <code>Install this extension: https://github.com/github/awesome-copilot/tree/main/extensions/accessibility-kanban</code></p>
<p><strong>Installing in the GitHub Copilot app:</strong> Click <strong>Copy Install</strong>, then run that plugin install command in Copilot.</p>
<p><strong>Example:</strong> <code>copilot plugin install accessibility-kanban@awesome-copilot</code></p>
</div>
<div class="listing-toolbar">
+14 -1
View File
@@ -37,6 +37,8 @@ export interface RenderableExtension {
} | null;
imageUrl?: string | null;
assetPath?: string | null;
pluginName?: string | null;
installCommand?: string | null;
installUrl?: string | null;
sourceUrl?: string | null;
external?: boolean;
@@ -78,6 +80,9 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
"/"
)}`
: "");
const installCommand =
item.installCommand ||
(item.pluginName ? `copilot plugin install ${item.pluginName}@awesome-copilot` : "");
const sourceUrl =
item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "");
@@ -126,8 +131,16 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
const actionsHtml = `
<button
class="btn btn-primary btn-small copy-install-url-btn"
data-install-command="${escapeHtml(installCommand)}"
title="Copy plugin install command"
${installCommand ? "" : "disabled"}
>
Copy Install
</button>
<button
class="btn btn-secondary btn-small copy-install-url-btn"
data-install-url="${escapeHtml(installUrl)}"
title="Copy install URL"
title="Copy fallback URL install target"
${installUrl ? "" : "disabled"}
>
Copy URL
+48 -14
View File
@@ -78,6 +78,13 @@ function getInstallUrl(item: Extension): string {
);
}
function getInstallCommand(item: Extension): string {
return (
item.installCommand ||
(item.pluginName ? `copilot plugin install ${item.pluginName}@awesome-copilot` : "")
);
}
function getSourceUrl(item: Extension): string {
return item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "");
}
@@ -207,6 +214,7 @@ function openDetailsModal(
}
const installUrl = getInstallUrl(item);
const installCommand = getInstallCommand(item);
const sourceUrl = getSourceUrl(item);
const detailsHtml = `
<div class="extension-details-body">
@@ -223,16 +231,18 @@ function openDetailsModal(
""
)}</div>
<div class="resource-actions extension-details-actions">
<button id="extension-details-install" class="btn btn-primary btn-small" type="button" data-install-url="${escapeHtml(
<button id="extension-details-install" class="btn btn-primary btn-small" type="button" data-install-command="${escapeHtml(
installCommand
)}" ${installCommand ? "" : "disabled"}>Copy Install</button>
<button id="extension-details-copy-url" class="btn btn-secondary btn-small" type="button" data-install-url="${escapeHtml(
installUrl
)}" ${installUrl ? "" : "disabled"}>Install</button>
${
sourceUrl
? `<a id="extension-details-source" class="btn btn-secondary btn-small" href="${escapeHtml(
sourceUrl
)}" target="_blank" rel="noopener noreferrer">Source</a>`
: ""
}
)}" ${installUrl ? "" : "disabled"}>Copy URL</button>
${sourceUrl
? `<a id="extension-details-source" class="btn btn-secondary btn-small" href="${escapeHtml(
sourceUrl
)}" target="_blank" rel="noopener noreferrer">Source</a>`
: ""
}
</div>
</div>
</div>
@@ -304,14 +314,20 @@ function setupActionHandlers(list: HTMLElement | null): void {
if (!installButton) return;
event.stopPropagation();
const installCommand = installButton.dataset.installCommand || "";
const installUrl = installButton.dataset.installUrl || "";
if (!installUrl) {
showToast("No install URL available for this extension", "error");
const contentToCopy = installCommand || installUrl;
if (!contentToCopy) {
showToast("No install target available for this extension", "error");
return;
}
const success = await copyToClipboard(installUrl);
const success = await copyToClipboard(contentToCopy);
showToast(
success ? "Extension URL copied!" : "Failed to copy extension URL",
success
? installCommand
? "Install command copied!"
: "Extension URL copied!"
: "Failed to copy install target",
success ? "success" : "error"
);
return;
@@ -371,7 +387,25 @@ function setupActionHandlers(list: HTMLElement | null): void {
"#extension-details-install"
) as HTMLButtonElement | null;
if (detailsInstallButton) {
const installUrl = detailsInstallButton.dataset.installUrl || "";
const installCommand = detailsInstallButton.dataset.installCommand || "";
if (!installCommand) {
showToast("No plugin install command available for this extension", "error");
return;
}
const success = await copyToClipboard(installCommand);
showToast(
success ? "Install command copied!" : "Failed to copy install command",
success ? "success" : "error"
);
return;
}
const detailsCopyUrlButton = target.closest(
"#extension-details-copy-url"
) as HTMLButtonElement | null;
if (detailsCopyUrlButton) {
const installUrl = detailsCopyUrlButton.dataset.installUrl || "";
if (!installUrl) {
showToast("No install URL available for this extension", "error");
return;