Skip to content

Commit d4dcc67

Browse files
tmeschterCopilot
andauthored
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>
1 parent 833a5c9 commit d4dcc67

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
151151
5. Run `npm run build` to update README.md and marketplace.json
152152
6. Verify the plugin appears in `.github/plugin/marketplace.json`
153153

154+
**For External Plugins:**
155+
1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version`
156+
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))
157+
3. Run `npm run build` to regenerate marketplace.json
158+
4. Verify the external plugin appears in `.github/plugin/marketplace.json`
159+
154160
### Testing Instructions
155161

156162
```bash

CONTRIBUTING.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,34 @@ plugins/my-plugin-id/
152152
- **Clear purpose**: The plugin should solve a specific problem or workflow
153153
- **Validate before submitting**: Run `npm run plugin:validate` to ensure your plugin is valid
154154

155+
#### Adding External Plugins
156+
157+
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.
158+
159+
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`:
160+
161+
```json
162+
[
163+
{
164+
"name": "my-external-plugin",
165+
"source": {
166+
"source": "github",
167+
"repo": "owner/plugin-repo"
168+
},
169+
"description": "Description of the external plugin",
170+
"version": "1.0.0"
171+
}
172+
]
173+
```
174+
175+
Supported source types:
176+
- **GitHub**: `{ "source": "github", "repo": "owner/repo", "ref": "v1.0.0" }`
177+
- **Git URL**: `{ "source": "url", "url": "https://gitlab.com/team/plugin.git" }`
178+
- **npm**: `{ "source": "npm", "package": "@scope/package", "version": "1.0.0" }`
179+
- **pip**: `{ "source": "pip", "package": "package-name", "version": "1.0.0" }`
180+
181+
After editing `plugins/external.json`, run `npm run build` to regenerate `marketplace.json`.
182+
155183
### Adding Hooks
156184

157185
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.

eng/generate-marketplace.mjs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,82 @@ import path from "path";
55
import { ROOT_FOLDER } from "./constants.mjs";
66

77
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
8+
const EXTERNAL_PLUGINS_FILE = path.join(ROOT_FOLDER, "plugins", "external.json");
89
const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json");
910

11+
/**
12+
* Validate an external plugin entry has required fields and a non-local source
13+
* @param {object} plugin - External plugin entry
14+
* @param {number} index - Index in the array (for error messages)
15+
* @returns {string[]} - Array of validation error messages
16+
*/
17+
function validateExternalPlugin(plugin, index) {
18+
const errors = [];
19+
const prefix = `external.json[${index}]`;
20+
21+
if (!plugin.name || typeof plugin.name !== "string") {
22+
errors.push(`${prefix}: "name" is required and must be a string`);
23+
}
24+
if (!plugin.description || typeof plugin.description !== "string") {
25+
errors.push(`${prefix}: "description" is required and must be a string`);
26+
}
27+
if (!plugin.version || typeof plugin.version !== "string") {
28+
errors.push(`${prefix}: "version" is required and must be a string`);
29+
}
30+
31+
if (!plugin.source) {
32+
errors.push(`${prefix}: "source" is required`);
33+
} else if (typeof plugin.source === "string") {
34+
errors.push(`${prefix}: "source" must be an object (local file paths are not allowed for external plugins)`);
35+
} else if (typeof plugin.source === "object") {
36+
if (!plugin.source.source) {
37+
errors.push(`${prefix}: "source.source" is required (e.g. "github", "url", "npm", "pip")`);
38+
}
39+
} else {
40+
errors.push(`${prefix}: "source" must be an object`);
41+
}
42+
43+
return errors;
44+
}
45+
46+
/**
47+
* Read external plugin entries from external.json
48+
* @returns {Array} - Array of external plugin entries (merged as-is)
49+
*/
50+
function readExternalPlugins() {
51+
if (!fs.existsSync(EXTERNAL_PLUGINS_FILE)) {
52+
return [];
53+
}
54+
55+
try {
56+
const content = fs.readFileSync(EXTERNAL_PLUGINS_FILE, "utf8");
57+
const plugins = JSON.parse(content);
58+
if (!Array.isArray(plugins)) {
59+
console.warn("Warning: external.json must contain an array");
60+
return [];
61+
}
62+
63+
// Validate each entry
64+
let hasErrors = false;
65+
for (let i = 0; i < plugins.length; i++) {
66+
const errors = validateExternalPlugin(plugins[i], i);
67+
if (errors.length > 0) {
68+
errors.forEach(e => console.error(`Error: ${e}`));
69+
hasErrors = true;
70+
}
71+
}
72+
if (hasErrors) {
73+
console.error("Error: external.json contains invalid entries");
74+
process.exit(1);
75+
}
76+
77+
return plugins;
78+
} catch (error) {
79+
console.error(`Error reading external.json: ${error.message}`);
80+
return [];
81+
}
82+
}
83+
1084
/**
1185
* Read plugin metadata from plugin.json file
1286
* @param {string} pluginDir - Path to plugin directory
@@ -67,6 +141,25 @@ function generateMarketplace() {
67141
}
68142
}
69143

144+
// Read external plugins and merge as-is
145+
const externalPlugins = readExternalPlugins();
146+
if (externalPlugins.length > 0) {
147+
console.log(`\nFound ${externalPlugins.length} external plugins`);
148+
149+
// Warn on duplicate names
150+
const localNames = new Set(plugins.map(p => p.name));
151+
for (const ext of externalPlugins) {
152+
if (localNames.has(ext.name)) {
153+
console.warn(`Warning: external plugin "${ext.name}" has the same name as a local plugin`);
154+
}
155+
plugins.push(ext);
156+
console.log(`✓ Added external plugin: ${ext.name}`);
157+
}
158+
}
159+
160+
// Sort all plugins by name (case-insensitive)
161+
plugins.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
162+
70163
// Create marketplace.json structure
71164
const marketplace = {
72165
name: "awesome-copilot",
@@ -91,7 +184,7 @@ function generateMarketplace() {
91184
// Write marketplace.json
92185
fs.writeFileSync(MARKETPLACE_FILE, JSON.stringify(marketplace, null, 2) + "\n");
93186

94-
console.log(`\n✓ Successfully generated marketplace.json with ${plugins.length} plugins`);
187+
console.log(`\n✓ Successfully generated marketplace.json with ${plugins.length} plugins (${plugins.length - externalPlugins.length} local, ${externalPlugins.length} external)`);
95188
console.log(` Location: ${MARKETPLACE_FILE}`);
96189
}
97190

plugins/external.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)