diff --git a/package.json b/package.json index 6bada5c..e60e90c 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "check:package-bin": "node scripts/check-package-bin.mjs", "check:silent-column-drop": "node scripts/check-silent-column-drop.mjs", "check:typed-error-lint": "node scripts/check-typed-error-lint.mjs", - "check:plugin-parity": "node scripts/check-plugin-command-parity.mjs" + "check:plugin-parity": "node scripts/check-plugin-command-parity.mjs", + "check:plugin-root-registration": "node scripts/check-plugin-root-registration.mjs" }, "keywords": [ "cli", diff --git a/scripts/check-plugin-root-registration.mjs b/scripts/check-plugin-root-registration.mjs new file mode 100644 index 0000000..273993a --- /dev/null +++ b/scripts/check-plugin-root-registration.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +/** + * check-plugin-root-registration.mjs — every plugins// must be listed + * in the root webcmd-plugin.json plugins map, and vice versa. + * + * The promotion flow (webcmd-adapter-author skill / adapter-template.md) has + * an easy-to-skip step: after `webcmd plugin create` scaffolds plugins//, + * the author must also add to the root webcmd-plugin.json. Nothing in + * `webcmd plugin create`, `webcmd plugin install file://...`, or + * `webcmd validate ` errors or warns when that step is skipped — the + * plugin works locally either way, so the omission is silent until someone + * tries `webcmd plugin install github:agentrhq/webcmd/` (which + * resolves sub-plugins by looking up in the root manifest) and it + * fails to resolve. This check catches the gap at CI time instead. See #222. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const pluginsDir = path.join(root, 'plugins'); +const rootManifest = readJson(path.join(root, 'webcmd-plugin.json')); +const registered = new Set(Object.keys(rootManifest.plugins ?? {})); + +const onDisk = fs + .readdirSync(pluginsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((name) => fs.existsSync(path.join(pluginsDir, name, 'webcmd-plugin.json'))) + .sort(); + +const issues = []; + +for (const site of onDisk) { + if (!registered.has(site)) { + issues.push(`plugins/${site} exists but is not registered in root webcmd-plugin.json`); + } +} + +const onDiskSet = new Set(onDisk); +for (const site of registered) { + if (!onDiskSet.has(site)) { + issues.push(`root webcmd-plugin.json registers "${site}" but plugins/${site} does not exist`); + } +} + +if (issues.length) { + console.error(`Plugin root-registration check failed (${issues.length} issue(s)):`); + for (const issue of issues) console.error(` - ${issue}`); + console.error('\nRegister every plugins// in the root webcmd-plugin.json "plugins" map (see references/adapter-template.md).'); + process.exit(1); +} + +console.log(`OK - ${onDisk.length} plugin(s) on disk match root webcmd-plugin.json registration.`); + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} diff --git a/skills/webcmd-adapter-author/references/adapter-template.md b/skills/webcmd-adapter-author/references/adapter-template.md index 20871da..b13710a 100644 --- a/skills/webcmd-adapter-author/references/adapter-template.md +++ b/skills/webcmd-adapter-author/references/adapter-template.md @@ -34,6 +34,12 @@ Then add `` to the root `webcmd-plugin.json` `plugins` map: } ``` +**Verify the registration landed** — nothing else errors or warns if this step is silently skipped: + +```bash +node scripts/check-plugin-root-registration.mjs +``` + Before handing off, remove the private shadow and prove the plugin path works: ```bash