mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-12 04:05:12 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
1
plugins/external.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
Reference in New Issue
Block a user