From f0ec6399c192c2234732f5747dd85a71c4e99107 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 11:43:10 +1000 Subject: [PATCH 1/6] 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> --- .github/plugin/marketplace.json | 203 ++++++++----- AGENTS.md | 17 ++ CONTRIBUTING.md | 12 + eng/generate-marketplace.mjs | 71 +++-- eng/generate-website-data.mjs | 275 ++++++++++++------ eng/materialize-plugins.mjs | 36 ++- eng/validate-plugins.mjs | 219 ++++++++++++-- eng/yaml-parser.mjs | 2 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 26 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 26 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 26 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 26 +- .../plugin/plugin.json} | 24 +- .../plugin/plugin.json} | 26 +- website/src/pages/extensions.astro | 4 +- .../src/scripts/pages/extensions-render.ts | 15 +- website/src/scripts/pages/extensions.ts | 62 +++- 22 files changed, 854 insertions(+), 336 deletions(-) rename extensions/accessibility-kanban/{canvas.json => .github/plugin/plugin.json} (55%) rename extensions/arcade-canvas/{canvas.json => .github/plugin/plugin.json} (53%) rename extensions/backlog-swipe-triage/{canvas.json => .github/plugin/plugin.json} (57%) rename extensions/chromium-control-canvas/{canvas.json => .github/plugin/plugin.json} (58%) rename extensions/color-orb/{canvas.json => .github/plugin/plugin.json} (56%) rename extensions/diagram-viewer/{canvas.json => .github/plugin/plugin.json} (58%) rename extensions/feedback-themes/{canvas.json => .github/plugin/plugin.json} (56%) rename extensions/gesture-review/{canvas.json => .github/plugin/plugin.json} (55%) rename extensions/release-notes-showcase/{canvas.json => .github/plugin/plugin.json} (55%) rename extensions/site-studio/{canvas.json => .github/plugin/plugin.json} (55%) rename extensions/where-was-i/{canvas.json => .github/plugin/plugin.json} (57%) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 17a91a230..75d92f746 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -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,27 +179,39 @@ "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" }, + { + "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" }, @@ -213,19 +242,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" }, @@ -280,16 +309,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.", @@ -375,13 +410,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" }, @@ -413,22 +448,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.", @@ -453,22 +494,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", @@ -555,31 +602,31 @@ }, { "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": "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" }, @@ -685,79 +732,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" }, @@ -791,97 +838,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" }, @@ -911,7 +970,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" }, @@ -941,37 +1000,37 @@ }, { "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": "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" }, @@ -1088,7 +1147,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" }, @@ -1117,6 +1176,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.", diff --git a/AGENTS.md b/AGENTS.md index b6db0a139..308f8576e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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,14 @@ 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` should include `name`, `description`, and `version` (plus optional `author`, `keywords`, and `logo`) +- Prefer screenshot conventions from `assets/` (`icon.*`, `preview.*`) and use `x-awesome-copilot.screenshots` only for overrides +- 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 +169,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//` with `extension.mjs` +2. Add `.github/plugin/plugin.json` metadata (name/description/version and optional logo/keywords/author) +3. Add screenshots to `assets/` using convention-based names, or use `x-awesome-copilot.screenshots` for explicit overrides +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d7a6a512..fec7f6d8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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,16 @@ 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//` and are installable through plugin metadata. + +1. **Create/update extension metadata**: Add `.github/plugin/plugin.json` in the extension folder +2. **Use plugin-style metadata**: Include `name`, `description`, `version`, and optional `author`, `keywords`, `logo` +3. **Screenshots**: Prefer conventions in `assets/` (`icon.*`, `preview.*`); use `x-awesome-copilot.screenshots` only for overrides +4. **Keep names stable**: The manifest `name` should match the extension folder name for install consistency +5. **Do not add `canvas.json`**: Extension website metadata is now sourced from `.github/plugin/plugin.json` + ### 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 +194,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/` - **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 diff --git a/eng/generate-marketplace.mjs b/eng/generate-marketplace.mjs index b69a9bd69..45a54b21c 100755 --- a/eng/generate-marketplace.mjs +++ b/eng/generate-marketplace.mjs @@ -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,44 +31,59 @@ function readPluginMetadata(pluginDir) { } } -/** - * 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}`); - process.exit(1); +function collectLocalPluginsFromRoot(rootDir, sourcePrefix, includeEntry = () => true) { + if (!fs.existsSync(rootDir)) { + return []; } - // Read all plugin directories - const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }) + const entries = fs.readdirSync(rootDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) + .filter(entry => includeEntry(entry.name)) .map(entry => entry.name) .sort(); - 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); + for (const dirName of entries) { + const pluginPath = path.join(rootDir, 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)`); + 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) && !fs.existsSync(EXTENSIONS_DIR)) { + console.error(`Error: Neither plugins directory (${PLUGINS_DIR}) nor extensions directory (${EXTENSIONS_DIR}) was found`); + process.exit(1); } + const plugins = [ + ...collectLocalPluginsFromRoot(PLUGINS_DIR, "plugins"), + ...collectLocalPluginsFromRoot( + EXTENSIONS_DIR, + "extensions", + (entryName) => fs.existsSync(path.join(EXTENSIONS_DIR, entryName, "extension.mjs")) + ) + ]; + + console.log(`Found ${plugins.length} local plugin manifests`); + // Read external plugins and merge as-is const { plugins: externalPlugins, errors: externalErrors, warnings: externalWarnings } = readExternalPlugins({ localPluginNames: plugins.map((plugin) => plugin.name), @@ -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", diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs index 875eb8030..f10eafcf6 100755 --- a/eng/generate-website-data.mjs +++ b/eng/generate-website-data.mjs @@ -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) diff --git a/eng/materialize-plugins.mjs b/eng/materialize-plugins.mjs index 6bed0902d..978c250fe 100644 --- a/eng/materialize-plugins.mjs +++ b/eng/materialize-plugins.mjs @@ -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).`); } diff --git a/eng/validate-plugins.mjs b/eng/validate-plugins.mjs index f9af74fa2..4eace879f 100755 --- a/eng/validate-plugins.mjs +++ b/eng/validate-plugins.mjs @@ -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,118 @@ function validatePlugin(folderName) { const specErrors = validateSpecPaths(plugin); errors.push(...specErrors); - return errors; + const extensionRefErrors = validateCuratedPluginExtensionRefs(plugin); + errors.push(...extensionRefErrors); + + return { errors, plugin: parsedPlugin }; } -// Main validation function -function validatePlugins() { - if (!fs.existsSync(PLUGINS_DIR)) { - console.log("No plugins directory found - validation skipped"); - return true; +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); + + if (parsed.logo !== undefined && typeof parsed.logo !== "string") { + errors.push("logo must be a string"); + } else if (typeof parsed.logo === "string") { + validateExtensionScreenshotPath(extensionDir, parsed.logo, "logo", errors); + } + + const screenshots = parsed?.["x-awesome-copilot"]?.screenshots; + if (screenshots !== undefined) { + if (typeof screenshots !== "object" || screenshots === null) { + errors.push("x-awesome-copilot.screenshots must be an object"); + } else { + if (screenshots.icon !== undefined) { + const iconPath = typeof screenshots.icon === "string" ? screenshots.icon : screenshots.icon?.path; + validateExtensionScreenshotPath(extensionDir, iconPath, "x-awesome-copilot.screenshots.icon", errors); + } + if (screenshots.gallery !== undefined) { + const galleryEntries = Array.isArray(screenshots.gallery) ? screenshots.gallery : [screenshots.gallery]; + galleryEntries.forEach((entry, index) => { + const galleryPath = typeof entry === "string" ? entry : entry?.path; + validateExtensionScreenshotPath( + extensionDir, + galleryPath, + `x-awesome-copilot.screenshots.gallery[${index}]`, + errors + ); + }); + } + } + } + + if (parsed.logo === undefined && screenshots?.icon === undefined && screenshots?.gallery === undefined) { + errors.push("at least one visual must be defined via logo or x-awesome-copilot.screenshots"); } - const pluginDirs = fs - .readdirSync(PLUGINS_DIR, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name); + return { errors, plugin: parsedPlugin }; +} - if (pluginDirs.length === 0) { - console.log("No plugin directories found - validation skipped"); +// Main validation function +function validatePlugins() { + const pluginDirs = fs.existsSync(PLUGINS_DIR) + ? fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + : []; + const extensionDirs = getExtensionFolderNames(); + + 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 +364,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 +419,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; diff --git a/eng/yaml-parser.mjs b/eng/yaml-parser.mjs index 19bb3f73c..3e202668a 100644 --- a/eng/yaml-parser.mjs +++ b/eng/yaml-parser.mjs @@ -1,6 +1,6 @@ // YAML parser for frontmatter parsing using vfile-matter import fs from "fs"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import path from "path"; import { VFile } from "vfile"; import { matter } from "vfile-matter"; diff --git a/extensions/accessibility-kanban/canvas.json b/extensions/accessibility-kanban/.github/plugin/plugin.json similarity index 55% rename from extensions/accessibility-kanban/canvas.json rename to extensions/accessibility-kanban/.github/plugin/plugin.json index c9aecb272..ce5959333 100644 --- a/extensions/accessibility-kanban/canvas.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/arcade-canvas/canvas.json b/extensions/arcade-canvas/.github/plugin/plugin.json similarity index 53% rename from extensions/arcade-canvas/canvas.json rename to extensions/arcade-canvas/.github/plugin/plugin.json index daf543c63..fad2483c2 100644 --- a/extensions/arcade-canvas/canvas.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -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,19 @@ "retro-games", "session-breaks" ], - "screenshots": { - "icon": { - "path": "assets/icon.png", - "type": "image/png" - }, - "gallery": { - "path": "assets/preview.png", - "type": "image/png" + "logo": "assets/icon.png", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/icon.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } -} \ No newline at end of file +} diff --git a/extensions/backlog-swipe-triage/canvas.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json similarity index 57% rename from extensions/backlog-swipe-triage/canvas.json rename to extensions/backlog-swipe-triage/.github/plugin/plugin.json index 08dd7eadf..a0257cf56 100644 --- a/extensions/backlog-swipe-triage/canvas.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/chromium-control-canvas/canvas.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json similarity index 58% rename from extensions/chromium-control-canvas/canvas.json rename to extensions/chromium-control-canvas/.github/plugin/plugin.json index 3325ca631..d1d909f1d 100644 --- a/extensions/chromium-control-canvas/canvas.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } -} \ No newline at end of file +} diff --git a/extensions/color-orb/canvas.json b/extensions/color-orb/.github/plugin/plugin.json similarity index 56% rename from extensions/color-orb/canvas.json rename to extensions/color-orb/.github/plugin/plugin.json index a68404edf..8cb278c37 100644 --- a/extensions/color-orb/canvas.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/diagram-viewer/canvas.json b/extensions/diagram-viewer/.github/plugin/plugin.json similarity index 58% rename from extensions/diagram-viewer/canvas.json rename to extensions/diagram-viewer/.github/plugin/plugin.json index ddc3b4d2a..7cd27d999 100644 --- a/extensions/diagram-viewer/canvas.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/feedback-themes/canvas.json b/extensions/feedback-themes/.github/plugin/plugin.json similarity index 56% rename from extensions/feedback-themes/canvas.json rename to extensions/feedback-themes/.github/plugin/plugin.json index 3bc88213d..08231fe31 100644 --- a/extensions/feedback-themes/canvas.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } -} \ No newline at end of file +} diff --git a/extensions/gesture-review/canvas.json b/extensions/gesture-review/.github/plugin/plugin.json similarity index 55% rename from extensions/gesture-review/canvas.json rename to extensions/gesture-review/.github/plugin/plugin.json index 61f91cdc1..e015e70b9 100644 --- a/extensions/gesture-review/canvas.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/release-notes-showcase/canvas.json b/extensions/release-notes-showcase/.github/plugin/plugin.json similarity index 55% rename from extensions/release-notes-showcase/canvas.json rename to extensions/release-notes-showcase/.github/plugin/plugin.json index 4b1b18350..f424b9067 100644 --- a/extensions/release-notes-showcase/canvas.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } -} \ No newline at end of file +} diff --git a/extensions/site-studio/canvas.json b/extensions/site-studio/.github/plugin/plugin.json similarity index 55% rename from extensions/site-studio/canvas.json rename to extensions/site-studio/.github/plugin/plugin.json index 05f5c010c..a50e0bfa1 100644 --- a/extensions/site-studio/canvas.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -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", "keywords": [ @@ -11,14 +10,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } } diff --git a/extensions/where-was-i/canvas.json b/extensions/where-was-i/.github/plugin/plugin.json similarity index 57% rename from extensions/where-was-i/canvas.json rename to extensions/where-was-i/.github/plugin/plugin.json index bf5a69e36..f8783f304 100644 --- a/extensions/where-was-i/canvas.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -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,19 @@ "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", + "x-awesome-copilot": { + "screenshots": { + "icon": { + "path": "assets/preview.png", + "type": "image/png" + }, + "gallery": [ + { + "path": "assets/preview.png", + "type": "image/png" + } + ] } } -} \ No newline at end of file +} diff --git a/website/src/pages/extensions.astro b/website/src/pages/extensions.astro index e19888898..9ebb8d13a 100644 --- a/website/src/pages/extensions.astro +++ b/website/src/pages/extensions.astro @@ -18,8 +18,8 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
-

Installing in the GitHub Copilot app: Click Copy URL, then ask Copilot to install that extension URL.

-

Example: Install this extension: https://github.com/github/awesome-copilot/tree/main/extensions/accessibility-kanban

+

Installing in the GitHub Copilot app: Click Copy Install, then run that plugin install command in Copilot.

+

Example: copilot plugin install accessibility-kanban@awesome-copilot

diff --git a/website/src/scripts/pages/extensions-render.ts b/website/src/scripts/pages/extensions-render.ts index 8172cef56..bb0a7c538 100644 --- a/website/src/scripts/pages/extensions-render.ts +++ b/website/src/scripts/pages/extensions-render.ts @@ -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 = ` + + - ${ - sourceUrl - ? `Source` - : "" - } + )}" ${installUrl ? "" : "disabled"}>Copy URL + ${sourceUrl + ? `Source` + : "" + }
@@ -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; From 5657606d0ec7364da47bf924713a445b8193e275 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 14:59:35 +1000 Subject: [PATCH 2/6] 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> --- .../workflows/validate-canvas-extensions.yml | 40 ++++++------------- .../plugin/plugin.json} | 16 ++------ 2 files changed, 15 insertions(+), 41 deletions(-) rename extensions/java-modernization-studio/{canvas.json => .github/plugin/plugin.json} (69%) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 558a5766c..7486349fe 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened] paths: - "extensions/**" - - ".schemas/canvas.schema.json" + - ".schemas/plugin.schema.json" permissions: contents: read @@ -33,10 +33,11 @@ jobs: 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 + # Note: We're still using canvas.schema.json for now to validate the structure, + # but in future we may migrate to using plugin.schema.json. + # For now, plugin.json will be validated against plugin specs separately. + if ! node ./eng/validate-json-schema.mjs --schema .schemas/plugin.schema.json 2>/dev/null || ! [ -f .schemas/plugin.schema.json ]; then + echo "Note: plugin.schema.json not yet in use for extension validation. Using plugin validation only." fi # Collect changed extension directories. @@ -97,8 +98,8 @@ jobs: errors+=("\`$ext_dir\`: missing required \`extension.mjs\`.") fi - if [ ! -f "$ext_dir/canvas.json" ]; then - errors+=("\`$ext_dir\`: missing required \`canvas.json\`.") + if [ ! -f "$ext_dir/.github/plugin/plugin.json" ]; then + errors+=("\`$ext_dir\`: missing required \`.github/plugin/plugin.json\`.") continue fi @@ -106,33 +107,16 @@ jobs: 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 + # Extension manifest is validated by the build system (npm run build). + # This workflow focuses on structural requirements (presence of required files). done if [ "${#errors[@]}" -ne 0 ]; then - echo "❌ Canvas extension validation failed:" + echo "❌ Extension validation failed:" for error in "${errors[@]}"; do echo "- $error" done exit 1 fi - echo "✅ All changed canvas extensions passed validation." \ No newline at end of file + echo "✅ All changed extensions passed validation." \ No newline at end of file diff --git a/extensions/java-modernization-studio/canvas.json b/extensions/java-modernization-studio/.github/plugin/plugin.json similarity index 69% rename from extensions/java-modernization-studio/canvas.json rename to extensions/java-modernization-studio/.github/plugin/plugin.json index ec31d1c71..329d53b28 100644 --- a/extensions/java-modernization-studio/canvas.json +++ b/extensions/java-modernization-studio/.github/plugin/plugin.json @@ -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,5 @@ "modernization-cockpit", "validation-gates" ], - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": { - "path": "assets/preview.png", - "type": "image/png" - } - } -} \ No newline at end of file + "logo": "assets/preview.png" +} From 069ffe8237d2bfb126c8a21800bf8054a56a6d30 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 15:20:42 +1000 Subject: [PATCH 3/6] 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> --- extensions/accessibility-kanban/.github/plugin/plugin.json | 3 ++- extensions/arcade-canvas/.github/plugin/plugin.json | 3 ++- extensions/backlog-swipe-triage/.github/plugin/plugin.json | 3 ++- extensions/chromium-control-canvas/.github/plugin/plugin.json | 3 ++- extensions/color-orb/.github/plugin/plugin.json | 3 ++- extensions/diagram-viewer/.github/plugin/plugin.json | 3 ++- extensions/feedback-themes/.github/plugin/plugin.json | 3 ++- extensions/gesture-review/.github/plugin/plugin.json | 3 ++- .../java-modernization-studio/.github/plugin/plugin.json | 3 ++- extensions/release-notes-showcase/.github/plugin/plugin.json | 3 ++- extensions/site-studio/.github/plugin/plugin.json | 3 ++- extensions/where-was-i/.github/plugin/plugin.json | 3 ++- 12 files changed, 24 insertions(+), 12 deletions(-) diff --git a/extensions/accessibility-kanban/.github/plugin/plugin.json b/extensions/accessibility-kanban/.github/plugin/plugin.json index ce5959333..0aa2dfd4c 100644 --- a/extensions/accessibility-kanban/.github/plugin/plugin.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/arcade-canvas/.github/plugin/plugin.json b/extensions/arcade-canvas/.github/plugin/plugin.json index fad2483c2..e17c97ef6 100644 --- a/extensions/arcade-canvas/.github/plugin/plugin.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/backlog-swipe-triage/.github/plugin/plugin.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json index a0257cf56..6ec7a4451 100644 --- a/extensions/backlog-swipe-triage/.github/plugin/plugin.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/chromium-control-canvas/.github/plugin/plugin.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json index d1d909f1d..70b1dcdf8 100644 --- a/extensions/chromium-control-canvas/.github/plugin/plugin.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -29,5 +29,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/color-orb/.github/plugin/plugin.json b/extensions/color-orb/.github/plugin/plugin.json index 8cb278c37..223fa47f8 100644 --- a/extensions/color-orb/.github/plugin/plugin.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/diagram-viewer/.github/plugin/plugin.json b/extensions/diagram-viewer/.github/plugin/plugin.json index 7cd27d999..90e112bef 100644 --- a/extensions/diagram-viewer/.github/plugin/plugin.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/feedback-themes/.github/plugin/plugin.json b/extensions/feedback-themes/.github/plugin/plugin.json index 08231fe31..83a9f4f5d 100644 --- a/extensions/feedback-themes/.github/plugin/plugin.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/gesture-review/.github/plugin/plugin.json b/extensions/gesture-review/.github/plugin/plugin.json index e015e70b9..263437a76 100644 --- a/extensions/gesture-review/.github/plugin/plugin.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/java-modernization-studio/.github/plugin/plugin.json b/extensions/java-modernization-studio/.github/plugin/plugin.json index 329d53b28..1f71b7641 100644 --- a/extensions/java-modernization-studio/.github/plugin/plugin.json +++ b/extensions/java-modernization-studio/.github/plugin/plugin.json @@ -15,5 +15,6 @@ "modernization-cockpit", "validation-gates" ], - "logo": "assets/preview.png" + "logo": "assets/preview.png", + "extensions": "." } diff --git a/extensions/release-notes-showcase/.github/plugin/plugin.json b/extensions/release-notes-showcase/.github/plugin/plugin.json index f424b9067..e1f185e16 100644 --- a/extensions/release-notes-showcase/.github/plugin/plugin.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/site-studio/.github/plugin/plugin.json b/extensions/site-studio/.github/plugin/plugin.json index 193eb6691..7ce098d43 100644 --- a/extensions/site-studio/.github/plugin/plugin.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } diff --git a/extensions/where-was-i/.github/plugin/plugin.json b/extensions/where-was-i/.github/plugin/plugin.json index f8783f304..4542f4915 100644 --- a/extensions/where-was-i/.github/plugin/plugin.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -28,5 +28,6 @@ } ] } - } + }, + "extensions": "." } From 8c9f539b3c42a2834174c418c4c73cd1687d6246 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 15:49:35 +1000 Subject: [PATCH 4/6] 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> --- .../workflows/validate-canvas-extensions.yml | 20 +++++++++- AGENTS.md | 9 ++++- CONTRIBUTING.md | 13 +++++-- eng/validate-plugins.mjs | 37 +++++-------------- .../.github/plugin/plugin.json | 14 ------- .../arcade-canvas/.github/plugin/plugin.json | 16 +------- .../.github/plugin/plugin.json | 14 ------- .../.github/plugin/plugin.json | 14 ------- .../color-orb/.github/plugin/plugin.json | 14 ------- .../diagram-viewer/.github/plugin/plugin.json | 14 ------- .../.github/plugin/plugin.json | 14 ------- .../gesture-review/.github/plugin/plugin.json | 14 ------- .../.github/plugin/plugin.json | 14 ------- .../site-studio/.github/plugin/plugin.json | 14 ------- .../where-was-i/.github/plugin/plugin.json | 14 ------- 15 files changed, 45 insertions(+), 190 deletions(-) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 7486349fe..82e20f00c 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -107,8 +107,24 @@ jobs: errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.") fi - # Extension manifest is validated by the build system (npm run build). - # This workflow focuses on structural requirements (presence of required files). + # Validate convention-based plugin.json fields + logo=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m.logo || '');" "$ext_dir/.github/plugin/plugin.json") + if [ "$logo" != "assets/preview.png" ]; then + errors+=("\`$ext_dir/.github/plugin/plugin.json\`: logo must be exactly \`assets/preview.png\` (convention), got \`$logo\`.") + fi + + extensions=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m.extensions || '');" "$ext_dir/.github/plugin/plugin.json") + if [ "$extensions" != "." ]; then + errors+=("\`$ext_dir/.github/plugin/plugin.json\`: extensions field must be exactly \`.\" (extension convention), got \`$extensions\`.") + fi + + has_x_awesome=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m['x-awesome-copilot'] ? 'yes' : 'no');" "$ext_dir/.github/plugin/plugin.json") + if [ "$has_x_awesome" = "yes" ]; then + errors+=("\`$ext_dir/.github/plugin/plugin.json\`: x-awesome-copilot field must not be present (use convention-based logo instead).") + fi + + # Extension manifest detailed validation is done by npm run plugin:validate + # This workflow focuses on structural requirements (presence of required files) done if [ "${#errors[@]}" -ne 0 ]; then diff --git a/AGENTS.md b/AGENTS.md index 308f8576e..164bea7b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,8 +86,13 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must - Each extension folder must include `extension.mjs` - Extension metadata must live at `.github/plugin/plugin.json` -- Extension `plugin.json` should include `name`, `description`, and `version` (plus optional `author`, `keywords`, and `logo`) -- Prefer screenshot conventions from `assets/` (`icon.*`, `preview.*`) and use `x-awesome-copilot.screenshots` only for overrides +- 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fec7f6d8e..075101c2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,10 +143,15 @@ Skills are self-contained folders in the `skills/` directory that include a `SKI Canvas extensions live in `extensions//` and are installable through plugin metadata. 1. **Create/update extension metadata**: Add `.github/plugin/plugin.json` in the extension folder -2. **Use plugin-style metadata**: Include `name`, `description`, `version`, and optional `author`, `keywords`, `logo` -3. **Screenshots**: Prefer conventions in `assets/` (`icon.*`, `preview.*`); use `x-awesome-copilot.screenshots` only for overrides -4. **Keep names stable**: The manifest `name` should match the extension folder name for install consistency -5. **Do not add `canvas.json`**: Extension website metadata is now sourced from `.github/plugin/plugin.json` +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 diff --git a/eng/validate-plugins.mjs b/eng/validate-plugins.mjs index 4eace879f..fc8003b34 100755 --- a/eng/validate-plugins.mjs +++ b/eng/validate-plugins.mjs @@ -293,38 +293,21 @@ function validateExtensionManifest(folderName) { const keywordsError = validateKeywords(parsed.keywords ?? parsed.tags); if (keywordsError) errors.push(keywordsError); - if (parsed.logo !== undefined && typeof parsed.logo !== "string") { - errors.push("logo must be a string"); - } else if (typeof parsed.logo === "string") { + // 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); } - const screenshots = parsed?.["x-awesome-copilot"]?.screenshots; - if (screenshots !== undefined) { - if (typeof screenshots !== "object" || screenshots === null) { - errors.push("x-awesome-copilot.screenshots must be an object"); - } else { - if (screenshots.icon !== undefined) { - const iconPath = typeof screenshots.icon === "string" ? screenshots.icon : screenshots.icon?.path; - validateExtensionScreenshotPath(extensionDir, iconPath, "x-awesome-copilot.screenshots.icon", errors); - } - if (screenshots.gallery !== undefined) { - const galleryEntries = Array.isArray(screenshots.gallery) ? screenshots.gallery : [screenshots.gallery]; - galleryEntries.forEach((entry, index) => { - const galleryPath = typeof entry === "string" ? entry : entry?.path; - validateExtensionScreenshotPath( - extensionDir, - galleryPath, - `x-awesome-copilot.screenshots.gallery[${index}]`, - 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)"); } - if (parsed.logo === undefined && screenshots?.icon === undefined && screenshots?.gallery === undefined) { - errors.push("at least one visual must be defined via logo or x-awesome-copilot.screenshots"); + // Extension convention: extensions field must be "." + if (parsed.extensions !== ".") { + errors.push('extensions field must be exactly "." (extension convention)'); } return { errors, plugin: parsedPlugin }; diff --git a/extensions/accessibility-kanban/.github/plugin/plugin.json b/extensions/accessibility-kanban/.github/plugin/plugin.json index 0aa2dfd4c..824a71c5b 100644 --- a/extensions/accessibility-kanban/.github/plugin/plugin.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "status-tracking" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/arcade-canvas/.github/plugin/plugin.json b/extensions/arcade-canvas/.github/plugin/plugin.json index e17c97ef6..8bf1d4cb5 100644 --- a/extensions/arcade-canvas/.github/plugin/plugin.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -14,20 +14,6 @@ "retro-games", "session-breaks" ], - "logo": "assets/icon.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/icon.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, + "logo": "assets/preview.png", "extensions": "." } diff --git a/extensions/backlog-swipe-triage/.github/plugin/plugin.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json index 6ec7a4451..d45e7e39f 100644 --- a/extensions/backlog-swipe-triage/.github/plugin/plugin.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "workflow-automation" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/chromium-control-canvas/.github/plugin/plugin.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json index 70b1dcdf8..c8a2fa721 100644 --- a/extensions/chromium-control-canvas/.github/plugin/plugin.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -16,19 +16,5 @@ "web-navigation" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/color-orb/.github/plugin/plugin.json b/extensions/color-orb/.github/plugin/plugin.json index 223fa47f8..c162c294a 100644 --- a/extensions/color-orb/.github/plugin/plugin.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "visual-feedback" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/diagram-viewer/.github/plugin/plugin.json b/extensions/diagram-viewer/.github/plugin/plugin.json index 90e112bef..0ad6af5de 100644 --- a/extensions/diagram-viewer/.github/plugin/plugin.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "relationship-visualization" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/feedback-themes/.github/plugin/plugin.json b/extensions/feedback-themes/.github/plugin/plugin.json index 83a9f4f5d..3bbdbc967 100644 --- a/extensions/feedback-themes/.github/plugin/plugin.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "trend-discovery" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/gesture-review/.github/plugin/plugin.json b/extensions/gesture-review/.github/plugin/plugin.json index 263437a76..648d069b7 100644 --- a/extensions/gesture-review/.github/plugin/plugin.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "pull-request-review" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/release-notes-showcase/.github/plugin/plugin.json b/extensions/release-notes-showcase/.github/plugin/plugin.json index e1f185e16..e4088e3e0 100644 --- a/extensions/release-notes-showcase/.github/plugin/plugin.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "release-notes" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/site-studio/.github/plugin/plugin.json b/extensions/site-studio/.github/plugin/plugin.json index 7ce098d43..f2c294afd 100644 --- a/extensions/site-studio/.github/plugin/plugin.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "site-builder" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } diff --git a/extensions/where-was-i/.github/plugin/plugin.json b/extensions/where-was-i/.github/plugin/plugin.json index 4542f4915..ca60cd3e5 100644 --- a/extensions/where-was-i/.github/plugin/plugin.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -15,19 +15,5 @@ "resume-work" ], "logo": "assets/preview.png", - "x-awesome-copilot": { - "screenshots": { - "icon": { - "path": "assets/preview.png", - "type": "image/png" - }, - "gallery": [ - { - "path": "assets/preview.png", - "type": "image/png" - } - ] - } - }, "extensions": "." } From 6d0427c5d88306ea061beafdbef865f7ce68303d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 2 Jul 2026 14:33:14 +1000 Subject: [PATCH 5/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 164bea7b6..7eedca71a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,8 +177,8 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin: **For Canvas Extensions:** 1. Create/update the extension in `extensions//` with `extension.mjs` -2. Add `.github/plugin/plugin.json` metadata (name/description/version and optional logo/keywords/author) -3. Add screenshots to `assets/` using convention-based names, or use `x-awesome-copilot.screenshots` for explicit overrides +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 From ba8540e66b2a88ae0755640a72a0248882eb39d3 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 3 Jul 2026 12:15:07 +1000 Subject: [PATCH 6/6] 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> --- .github/workflows/build-website.yml | 1 - .../workflows/validate-canvas-extensions.yml | 108 +--------------- .schemas/canvas.schema.json | 121 ------------------ eng/validate-json-schema.mjs | 75 ----------- 4 files changed, 2 insertions(+), 303 deletions(-) delete mode 100644 .schemas/canvas.schema.json delete mode 100644 eng/validate-json-schema.mjs diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml index efcd2bfcd..77080a3e3 100644 --- a/.github/workflows/build-website.yml +++ b/.github/workflows/build-website.yml @@ -12,7 +12,6 @@ on: - hooks - workflows - extensions - - .schemas/canvas.schema.json permissions: contents: read diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 82e20f00c..b8026a512 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -6,7 +6,6 @@ on: types: [opened, synchronize, reopened] paths: - "extensions/**" - - ".schemas/plugin.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 - - # Note: We're still using canvas.schema.json for now to validate the structure, - # but in future we may migrate to using plugin.schema.json. - # For now, plugin.json will be validated against plugin specs separately. - if ! node ./eng/validate-json-schema.mjs --schema .schemas/plugin.schema.json 2>/dev/null || ! [ -f .schemas/plugin.schema.json ]; then - echo "Note: plugin.schema.json not yet in use for extension validation. Using plugin validation only." - 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/.github/plugin/plugin.json" ]; then - errors+=("\`$ext_dir\`: missing required \`.github/plugin/plugin.json\`.") - continue - fi - - if [ ! -f "$ext_dir/assets/preview.png" ]; then - errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.") - fi - - # Validate convention-based plugin.json fields - logo=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m.logo || '');" "$ext_dir/.github/plugin/plugin.json") - if [ "$logo" != "assets/preview.png" ]; then - errors+=("\`$ext_dir/.github/plugin/plugin.json\`: logo must be exactly \`assets/preview.png\` (convention), got \`$logo\`.") - fi - - extensions=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m.extensions || '');" "$ext_dir/.github/plugin/plugin.json") - if [ "$extensions" != "." ]; then - errors+=("\`$ext_dir/.github/plugin/plugin.json\`: extensions field must be exactly \`.\" (extension convention), got \`$extensions\`.") - fi - - has_x_awesome=$(node -e "const fs = require('fs'); const m = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(m['x-awesome-copilot'] ? 'yes' : 'no');" "$ext_dir/.github/plugin/plugin.json") - if [ "$has_x_awesome" = "yes" ]; then - errors+=("\`$ext_dir/.github/plugin/plugin.json\`: x-awesome-copilot field must not be present (use convention-based logo instead).") - fi - - # Extension manifest detailed validation is done by npm run plugin:validate - # This workflow focuses on structural requirements (presence of required files) - done - - if [ "${#errors[@]}" -ne 0 ]; then - echo "❌ Extension validation failed:" - for error in "${errors[@]}"; do - echo "- $error" - done - exit 1 - fi - - echo "✅ All changed extensions passed validation." \ No newline at end of file + npm run plugin:validate \ No newline at end of file diff --git a/.schemas/canvas.schema.json b/.schemas/canvas.schema.json deleted file mode 100644 index d1dc26569..000000000 --- a/.schemas/canvas.schema.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Canvas Extension Manifest", - "description": "Schema for extensions//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" - ] - } - } - } - } -} diff --git a/eng/validate-json-schema.mjs b/eng/validate-json-schema.mjs deleted file mode 100644 index e1eba3bf7..000000000 --- a/eng/validate-json-schema.mjs +++ /dev/null @@ -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 "); - 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}`); \ No newline at end of file