Skip to content

Commit 96433d1

Browse files
tmeschterCopilot
andcommitted
Split plugins/external.json into per-plugin files
Split the monolithic plugins/external.json into individual files under plugins/external/ (one per external plugin) to support per-file CODEOWNERS entries. Updated all scripts that scan the plugins directory to exclude the new external/ subdirectory and read individual JSON files instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 092aab6 commit 96433d1

19 files changed

Lines changed: 232 additions & 205 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
152152
6. Verify the plugin appears in `.github/plugin/marketplace.json`
153153

154154
**For External Plugins:**
155-
1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version`
155+
1. Create a new JSON file in `plugins/external/` named after the plugin (e.g., `plugins/external/my-plugin.json`) with `name`, `source`, `description`, and `version`
156156
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))
157157
3. Run `npm run build` to regenerate marketplace.json
158158
4. Verify the external plugin appears in `.github/plugin/marketplace.json`

CODEOWNERS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,13 @@
4747

4848
# Added via #codeowner from PR #1118
4949
/agents/github-actions-node-upgrade.agent.md @joshjohanning
50+
51+
# External plugins
52+
/plugins/external/dataverse.json @suyask-msft
53+
/plugins/external/azure.json @microsoft/ghcp4a
54+
/plugins/external/dotnet.json @ManishJayaswal
55+
/plugins/external/dotnet-diag.json @ManishJayaswal
56+
/plugins/external/skills-for-copilot-studio.json @ericsche
57+
/plugins/external/modernize-dotnet.json @tlmii
58+
/plugins/external/microsoft-docs.json @TianqiZhang
59+
/plugins/external/figma.json @siminapasat

CONTRIBUTING.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -190,22 +190,20 @@ plugins/my-plugin-id/
190190

191191
#### Adding External Plugins
192192

193-
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.
193+
External plugins are plugins hosted outside this repository (e.g., in a GitHub repo, npm package, or git URL). They are listed as individual JSON files in the `plugins/external/` directory and merged into the generated `marketplace.json` during build.
194194

195-
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`:
195+
To add an external plugin, create a new JSON file in `plugins/external/` named after the plugin (e.g., `plugins/external/my-external-plugin.json`) following the [Claude Code plugin marketplace spec](https://code.claude.com/docs/en/plugin-marketplaces#plugin-entries). Each file contains a single JSON object with `name`, `source`, `description`, and `version`:
196196

197197
```json
198-
[
199-
{
200-
"name": "my-external-plugin",
201-
"source": {
202-
"source": "github",
203-
"repo": "owner/plugin-repo"
204-
},
205-
"description": "Description of the external plugin",
206-
"version": "1.0.0"
207-
}
208-
]
198+
{
199+
"name": "my-external-plugin",
200+
"source": {
201+
"source": "github",
202+
"repo": "owner/plugin-repo"
203+
},
204+
"description": "Description of the external plugin",
205+
"version": "1.0.0"
206+
}
209207
```
210208

211209
Supported source types:
@@ -215,7 +213,7 @@ Supported source types:
215213
- **npm**: `{ "source": "npm", "package": "@scope/package", "version": "1.0.0" }`
216214
- **pip**: `{ "source": "pip", "package": "package-name", "version": "1.0.0" }`
217215

218-
After editing `plugins/external.json`, run `npm run build` to regenerate `marketplace.json`.
216+
After creating the file, run `npm run build` to regenerate `marketplace.json`.
219217

220218
### Adding Hooks
221219

eng/clean-materialized-plugins.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function main() {
4242
}
4343

4444
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
45-
.filter(entry => entry.isDirectory())
45+
.filter(entry => entry.isDirectory() && entry.name !== "external")
4646
.map(entry => entry.name)
4747
.sort();
4848

eng/generate-marketplace.mjs

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ 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");
8+
const EXTERNAL_PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins", "external");
99
const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json");
1010

1111
/**
1212
* Validate an external plugin entry has required fields and a non-local source
1313
* @param {object} plugin - External plugin entry
14-
* @param {number} index - Index in the array (for error messages)
14+
* @param {string} filename - Source filename (for error messages)
1515
* @returns {string[]} - Array of validation error messages
1616
*/
17-
function validateExternalPlugin(plugin, index) {
17+
function validateExternalPlugin(plugin, filename) {
1818
const errors = [];
19-
const prefix = `external.json[${index}]`;
19+
const prefix = `external/${filename}`;
2020

2121
if (!plugin.name || typeof plugin.name !== "string") {
2222
errors.push(`${prefix}: "name" is required and must be a string`);
@@ -44,41 +44,56 @@ function validateExternalPlugin(plugin, index) {
4444
}
4545

4646
/**
47-
* Read external plugin entries from external.json
47+
* Read external plugin entries from individual JSON files in plugins/external/
4848
* @returns {Array} - Array of external plugin entries (merged as-is)
4949
*/
5050
function readExternalPlugins() {
51-
if (!fs.existsSync(EXTERNAL_PLUGINS_FILE)) {
51+
if (!fs.existsSync(EXTERNAL_PLUGINS_DIR)) {
5252
return [];
5353
}
5454

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-
}
55+
const files = fs.readdirSync(EXTERNAL_PLUGINS_DIR)
56+
.filter(f => f.endsWith(".json"))
57+
.sort();
58+
59+
if (files.length === 0) {
60+
return [];
61+
}
62+
63+
const plugins = [];
64+
let hasErrors = false;
65+
66+
for (const file of files) {
67+
const filePath = path.join(EXTERNAL_PLUGINS_DIR, file);
68+
try {
69+
const content = fs.readFileSync(filePath, "utf8");
70+
const plugin = JSON.parse(content);
6271

63-
// Validate each entry
64-
let hasErrors = false;
65-
for (let i = 0; i < plugins.length; i++) {
66-
const errors = validateExternalPlugin(plugins[i], i);
72+
if (typeof plugin !== "object" || Array.isArray(plugin)) {
73+
console.error(`Error: external/${file} must contain a single JSON object`);
74+
hasErrors = true;
75+
continue;
76+
}
77+
78+
const errors = validateExternalPlugin(plugin, file);
6779
if (errors.length > 0) {
6880
errors.forEach(e => console.error(`Error: ${e}`));
6981
hasErrors = true;
82+
} else {
83+
plugins.push(plugin);
7084
}
85+
} catch (error) {
86+
console.error(`Error reading external/${file}: ${error.message}`);
87+
hasErrors = true;
7188
}
72-
if (hasErrors) {
73-
console.error("Error: external.json contains invalid entries");
74-
process.exit(1);
75-
}
89+
}
7690

77-
return plugins;
78-
} catch (error) {
79-
console.error(`Error reading external.json: ${error.message}`);
80-
return [];
91+
if (hasErrors) {
92+
console.error("Error: one or more external plugin files contain invalid entries");
93+
process.exit(1);
8194
}
95+
96+
return plugins;
8297
}
8398

8499
/**
@@ -114,9 +129,9 @@ function generateMarketplace() {
114129
process.exit(1);
115130
}
116131

117-
// Read all plugin directories
132+
// Read all plugin directories (excluding the 'external' directory)
118133
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
119-
.filter(entry => entry.isDirectory())
134+
.filter(entry => entry.isDirectory() && entry.name !== "external")
120135
.map(entry => entry.name)
121136
.sort();
122137

eng/generate-website-data.mjs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ function generatePluginsData(gitDates) {
504504

505505
const pluginDirs = fs
506506
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
507-
.filter((d) => d.isDirectory());
507+
.filter((d) => d.isDirectory() && d.name !== "external");
508508

509509
for (const dir of pluginDirs) {
510510
const pluginDir = path.join(PLUGINS_DIR, dir.name);
@@ -544,19 +544,23 @@ function generatePluginsData(gitDates) {
544544
}
545545
}
546546

547-
// Load external plugins from plugins/external.json
548-
const externalJsonPath = path.join(PLUGINS_DIR, "external.json");
549-
if (fs.existsSync(externalJsonPath)) {
547+
// Load external plugins from plugins/external/ directory
548+
const externalDir = path.join(PLUGINS_DIR, "external");
549+
if (fs.existsSync(externalDir)) {
550550
try {
551-
const externalPlugins = JSON.parse(
552-
fs.readFileSync(externalJsonPath, "utf-8")
553-
);
554-
if (Array.isArray(externalPlugins)) {
555-
let addedCount = 0;
556-
for (const ext of externalPlugins) {
551+
const externalFiles = fs.readdirSync(externalDir)
552+
.filter((f) => f.endsWith(".json"))
553+
.sort();
554+
let addedCount = 0;
555+
for (const file of externalFiles) {
556+
try {
557+
const ext = JSON.parse(
558+
fs.readFileSync(path.join(externalDir, file), "utf-8")
559+
);
560+
557561
if (!ext.name || !ext.description) {
558562
console.warn(
559-
`Skipping external plugin with missing name/description`
563+
`Skipping external plugin ${file} with missing name/description`
560564
);
561565
continue;
562566
}
@@ -591,13 +595,15 @@ function generatePluginsData(gitDates) {
591595
)} ${ext.author?.name || ""} ${ext.repository || ""}`.toLowerCase(),
592596
});
593597
addedCount++;
598+
} catch (e) {
599+
console.warn(`Failed to parse external plugin ${file}: ${e.message}`);
594600
}
595-
console.log(
596-
` ✓ Loaded ${addedCount} external plugin(s)`
597-
);
598601
}
602+
console.log(
603+
` ✓ Loaded ${addedCount} external plugin(s)`
604+
);
599605
} catch (e) {
600-
console.warn(`Failed to parse external plugins: ${e.message}`);
606+
console.warn(`Failed to read external plugins directory: ${e.message}`);
601607
}
602608
}
603609

eng/materialize-plugins.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function materializePlugins() {
5050
}
5151

5252
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
53-
.filter(entry => entry.isDirectory())
53+
.filter(entry => entry.isDirectory() && entry.name !== "external")
5454
.map(entry => entry.name)
5555
.sort();
5656

eng/update-plugin-commands-to-skills.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function main() {
9797
// Find all plugin.json files
9898
const pluginDirs = fs
9999
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
100-
.filter((entry) => entry.isDirectory())
100+
.filter((entry) => entry.isDirectory() && entry.name !== "external")
101101
.map((entry) => entry.name);
102102

103103
console.log(`Found ${pluginDirs.length} plugin directory(ies)\n`);

eng/update-readme.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ function generatePluginsSection(pluginsDir) {
731731
// Get all plugin directories
732732
const pluginDirs = fs
733733
.readdirSync(pluginsDir, { withFileTypes: true })
734-
.filter((d) => d.isDirectory())
734+
.filter((d) => d.isDirectory() && d.name !== "external")
735735
.map((d) => d.name);
736736

737737
// Map plugin dirs to objects with name for sorting
@@ -806,7 +806,7 @@ function generateFeaturedPluginsSection(pluginsDir) {
806806
// Get all plugin directories
807807
const pluginDirs = fs
808808
.readdirSync(pluginsDir, { withFileTypes: true })
809-
.filter((d) => d.isDirectory())
809+
.filter((d) => d.isDirectory() && d.name !== "external")
810810
.map((d) => d.name);
811811

812812
// Map plugin dirs to objects, filter for featured

eng/validate-plugins.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function validatePlugins() {
172172

173173
const pluginDirs = fs
174174
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
175-
.filter((d) => d.isDirectory())
175+
.filter((d) => d.isDirectory() && d.name !== "external")
176176
.map((d) => d.name);
177177

178178
if (pluginDirs.length === 0) {

0 commit comments

Comments
 (0)