From 83543326fe10e7ebe28a9dbe04029d46c8111286 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Wed, 4 Mar 2026 09:25:52 -0800 Subject: [PATCH] Support external plugins in marketplace.json generation The marketplace currently only includes plugins that live as local directories in plugins/. This makes it impossible to list plugins hosted in external GitHub repos, npm packages, or other git URLs. Add plugins/external.json as a hand-curated list of external plugin entries following the Claude Code plugin marketplace spec. The generate-marketplace script now reads this file and merges external entries as-is into the generated marketplace.json, sorted by name. Changes: - Add plugins/external.json (empty array, ready for entries) - Update eng/generate-marketplace.mjs to load, merge, and sort external plugins; warn on duplicate names; log counts - Document the external plugin workflow in CONTRIBUTING.md and AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 6 +++ CONTRIBUTING.md | 28 +++++++++++ eng/generate-marketplace.mjs | 95 +++++++++++++++++++++++++++++++++++- plugins/external.json | 1 + 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 plugins/external.json diff --git a/AGENTS.md b/AGENTS.md index bcb4cea7d..38a2f4005 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,6 +151,12 @@ 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 External Plugins:** +1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version` +2. The `source` field should be an object specifying a GitHub repo, git URL, npm package, or pip package (see [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins)) +3. Run `npm run build` to regenerate marketplace.json +4. Verify the external plugin appears in `.github/plugin/marketplace.json` + ### Testing Instructions ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d7a9f811..c3afca499 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,6 +152,34 @@ plugins/my-plugin-id/ - **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 +#### Adding External Plugins + +External plugins are plugins hosted outside this repository (e.g., in a GitHub repo, npm package, or git URL). They are listed in `plugins/external.json` and merged into the generated `marketplace.json` during build. + +To add an external plugin, append an entry to `plugins/external.json` following the [Claude Code plugin marketplace spec](https://code.claude.com/docs/en/plugin-marketplaces#plugin-entries). Each entry requires `name`, `source`, `description`, and `version`: + +```json +[ + { + "name": "my-external-plugin", + "source": { + "source": "github", + "repo": "owner/plugin-repo" + }, + "description": "Description of the external plugin", + "version": "1.0.0" + } +] +``` + +Supported source types: +- **GitHub**: `{ "source": "github", "repo": "owner/repo", "ref": "v1.0.0" }` +- **Git URL**: `{ "source": "url", "url": "https://gitlab.com/team/plugin.git" }` +- **npm**: `{ "source": "npm", "package": "@scope/package", "version": "1.0.0" }` +- **pip**: `{ "source": "pip", "package": "package-name", "version": "1.0.0" }` + +After editing `plugins/external.json`, run `npm run build` to regenerate `marketplace.json`. + ### Adding Hooks Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions, such as session start, session end, user prompts, and tool usage. diff --git a/eng/generate-marketplace.mjs b/eng/generate-marketplace.mjs index c08c3e783..96cf492f8 100755 --- a/eng/generate-marketplace.mjs +++ b/eng/generate-marketplace.mjs @@ -5,8 +5,82 @@ import path from "path"; import { ROOT_FOLDER } from "./constants.mjs"; const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); +const EXTERNAL_PLUGINS_FILE = path.join(ROOT_FOLDER, "plugins", "external.json"); const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json"); +/** + * Validate an external plugin entry has required fields and a non-local source + * @param {object} plugin - External plugin entry + * @param {number} index - Index in the array (for error messages) + * @returns {string[]} - Array of validation error messages + */ +function validateExternalPlugin(plugin, index) { + const errors = []; + const prefix = `external.json[${index}]`; + + if (!plugin.name || typeof plugin.name !== "string") { + errors.push(`${prefix}: "name" is required and must be a string`); + } + if (!plugin.description || typeof plugin.description !== "string") { + errors.push(`${prefix}: "description" is required and must be a string`); + } + if (!plugin.version || typeof plugin.version !== "string") { + errors.push(`${prefix}: "version" is required and must be a string`); + } + + if (!plugin.source) { + errors.push(`${prefix}: "source" is required`); + } else if (typeof plugin.source === "string") { + errors.push(`${prefix}: "source" must be an object (local file paths are not allowed for external plugins)`); + } else if (typeof plugin.source === "object") { + if (!plugin.source.source) { + errors.push(`${prefix}: "source.source" is required (e.g. "github", "url", "npm", "pip")`); + } + } else { + errors.push(`${prefix}: "source" must be an object`); + } + + return errors; +} + +/** + * Read external plugin entries from external.json + * @returns {Array} - Array of external plugin entries (merged as-is) + */ +function readExternalPlugins() { + if (!fs.existsSync(EXTERNAL_PLUGINS_FILE)) { + return []; + } + + try { + const content = fs.readFileSync(EXTERNAL_PLUGINS_FILE, "utf8"); + const plugins = JSON.parse(content); + if (!Array.isArray(plugins)) { + console.warn("Warning: external.json must contain an array"); + return []; + } + + // Validate each entry + let hasErrors = false; + for (let i = 0; i < plugins.length; i++) { + const errors = validateExternalPlugin(plugins[i], i); + if (errors.length > 0) { + errors.forEach(e => console.error(`Error: ${e}`)); + hasErrors = true; + } + } + if (hasErrors) { + console.error("Error: external.json contains invalid entries"); + process.exit(1); + } + + return plugins; + } catch (error) { + console.error(`Error reading external.json: ${error.message}`); + return []; + } +} + /** * Read plugin metadata from plugin.json file * @param {string} pluginDir - Path to plugin directory @@ -67,6 +141,25 @@ function generateMarketplace() { } } + // Read external plugins and merge as-is + const externalPlugins = readExternalPlugins(); + if (externalPlugins.length > 0) { + console.log(`\nFound ${externalPlugins.length} external plugins`); + + // Warn on duplicate names + const localNames = new Set(plugins.map(p => p.name)); + for (const ext of externalPlugins) { + if (localNames.has(ext.name)) { + console.warn(`Warning: external plugin "${ext.name}" has the same name as a local plugin`); + } + plugins.push(ext); + console.log(`āœ“ Added external plugin: ${ext.name}`); + } + } + + // Sort all plugins by name (case-insensitive) + plugins.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })); + // Create marketplace.json structure const marketplace = { name: "awesome-copilot", @@ -91,7 +184,7 @@ function generateMarketplace() { // Write marketplace.json fs.writeFileSync(MARKETPLACE_FILE, JSON.stringify(marketplace, null, 2) + "\n"); - console.log(`\nāœ“ Successfully generated marketplace.json with ${plugins.length} plugins`); + console.log(`\nāœ“ Successfully generated marketplace.json with ${plugins.length} plugins (${plugins.length - externalPlugins.length} local, ${externalPlugins.length} external)`); console.log(` Location: ${MARKETPLACE_FILE}`); } diff --git a/plugins/external.json b/plugins/external.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/plugins/external.json @@ -0,0 +1 @@ +[]