Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 59 additions & 0 deletions scripts/check-plugin-root-registration.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env node
/**
* check-plugin-root-registration.mjs — every plugins/<site>/ 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/<site>/,
* the author must also add <site> to the root webcmd-plugin.json. Nothing in
* `webcmd plugin create`, `webcmd plugin install file://...`, or
* `webcmd validate <site>` 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/<site>` (which
* resolves sub-plugins by looking up <site> 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/<site>/ 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'));
}
6 changes: 6 additions & 0 deletions skills/webcmd-adapter-author/references/adapter-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ Then add `<site>` 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
Expand Down