Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
95 changes: 94 additions & 1 deletion eng/generate-marketplace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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}`);
}

Expand Down
1 change: 1 addition & 0 deletions plugins/external.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]