Support external plugins in marketplace.json generation (#876)

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>
This commit is contained in:
Tom Meschter
2026-03-04 15:08:54 -08:00
committed by GitHub
parent 833a5c9b5b
commit d4dcc676e4
4 changed files with 129 additions and 1 deletions

View File

@@ -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 5. Run `npm run build` to update README.md and marketplace.json
6. Verify the plugin appears in `.github/plugin/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 ### Testing Instructions
```bash ```bash

View File

@@ -152,6 +152,34 @@ plugins/my-plugin-id/
- **Clear purpose**: The plugin should solve a specific problem or workflow - **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 - **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 ### 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. 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.

View File

@@ -5,8 +5,82 @@ import path from "path";
import { ROOT_FOLDER } from "./constants.mjs"; import { ROOT_FOLDER } from "./constants.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTERNAL_PLUGINS_FILE = path.join(ROOT_FOLDER, "plugins", "external.json");
const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.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 * Read plugin metadata from plugin.json file
* @param {string} pluginDir - Path to plugin directory * @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 // Create marketplace.json structure
const marketplace = { const marketplace = {
name: "awesome-copilot", name: "awesome-copilot",
@@ -91,7 +184,7 @@ function generateMarketplace() {
// Write marketplace.json // Write marketplace.json
fs.writeFileSync(MARKETPLACE_FILE, JSON.stringify(marketplace, null, 2) + "\n"); 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}`); console.log(` Location: ${MARKETPLACE_FILE}`);
} }

1
plugins/external.json Normal file
View File

@@ -0,0 +1 @@
[]