universal-plugin 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cli.mjs +302 -17
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/{doctor → doctor-universal-plugin}/README.md +1 -1
- package/skills/{doctor → doctor-universal-plugin}/SKILL.md +1 -1
- /package/skills/{doctor → doctor-universal-plugin}/scripts/doctor.mjs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
package/dist/cli.mjs
CHANGED
|
@@ -5247,6 +5247,274 @@ function discardedRangeWarning(index, entry, range) {
|
|
|
5247
5247
|
return `dependencies[${index}] "${entry}" carries a version range the runtime discards — declare ${marketplace ? `{ "name": "${name}", "marketplace": "${marketplace}", "version": "${range}" }` : `{ "name": "${name}", "version": "${range}" }`} for a range that is enforced`;
|
|
5248
5248
|
}
|
|
5249
5249
|
//#endregion
|
|
5250
|
+
//#region src/governance/copy.ts
|
|
5251
|
+
/** The governance copy step: a governance is copied into every skill that uses it at build time and
|
|
5252
|
+
* read from disk at run time, so a skill never pays a registry lookup to read a rule set it was
|
|
5253
|
+
* tested against (repobuddy/buddy-agent-harness#122).
|
|
5254
|
+
*
|
|
5255
|
+
* The files already present in `<skill>/references/governances/` ARE the declaration of which
|
|
5256
|
+
* governances that skill uses — nothing is declared in plugin.json. The build refreshes every file
|
|
5257
|
+
* in that folder from its owning package, adds the governances those files reference, and fails
|
|
5258
|
+
* when a file names no governance or when SKILL.md does not list a copy. */
|
|
5259
|
+
/** Where a skill's copies live, relative to the skill folder. Posix spelling: it is both a path
|
|
5260
|
+
* segment and the text a SKILL.md References section has to carry. */
|
|
5261
|
+
const SKILL_GOVERNANCE_DIR = "references/governances";
|
|
5262
|
+
/** A retrieval pointer inside a governance body: `<runner> <package> governance show <name>`, or the
|
|
5263
|
+
* bare `governance show <name>` in prose. Only the trailing two words carry meaning here — which
|
|
5264
|
+
* package shipped the command is exactly what the copy step removes. */
|
|
5265
|
+
const POINTER = /governance show\s+([A-Za-z0-9][A-Za-z0-9._/-]*)/g;
|
|
5266
|
+
/** A fenced code block opener or closer, with any indentation and info string. */
|
|
5267
|
+
const FENCE = /^\s*(?:```|~~~)/;
|
|
5268
|
+
/** Indentation, plus a list marker when the pointer is a bullet. A pointer listed under References
|
|
5269
|
+
* is usually one bullet among several, and an instruction that loses the bullet leaves the list. */
|
|
5270
|
+
const LINE_PREFIX = /^\s*(?:[-*+]\s+|\d+\.\s+)?/;
|
|
5271
|
+
/** The copy's name for a pointer token: a namespaced `<plugin>/<name>` lookup and a plain `<name>`
|
|
5272
|
+
* resolve to the same document, so both land in the same file. */
|
|
5273
|
+
function governanceName(token) {
|
|
5274
|
+
const slash = token.lastIndexOf("/");
|
|
5275
|
+
const name = slash === -1 ? token : token.slice(slash + 1);
|
|
5276
|
+
return name.endsWith(".md") ? name.slice(0, -3) : name;
|
|
5277
|
+
}
|
|
5278
|
+
/** The governances a governance body points at, in first-appearance order, deduplicated. */
|
|
5279
|
+
function referencedGovernances(content) {
|
|
5280
|
+
const names = [];
|
|
5281
|
+
for (const match of content.matchAll(POINTER)) {
|
|
5282
|
+
const token = match[1];
|
|
5283
|
+
if (!token) continue;
|
|
5284
|
+
const name = governanceName(token);
|
|
5285
|
+
if (!names.includes(name)) names.push(name);
|
|
5286
|
+
}
|
|
5287
|
+
return names;
|
|
5288
|
+
}
|
|
5289
|
+
/** The instruction a pointer becomes. Stated once so the rewriter and its tests cannot drift. */
|
|
5290
|
+
function loadInstruction(name) {
|
|
5291
|
+
return `Load \`${SKILL_GOVERNANCE_DIR}/${name}.md\` if it is not already loaded.`;
|
|
5292
|
+
}
|
|
5293
|
+
/** Rewrites every retrieval pointer in a copy into an instruction to load the sibling copy. The
|
|
5294
|
+
* Agent Skills specification asks that every file an agent needs is one level deep from SKILL.md,
|
|
5295
|
+
* so a copy must never send the agent back to a command to reach the next document.
|
|
5296
|
+
*
|
|
5297
|
+
* A fenced block whose every line was a pointer is unwrapped: prose in a bash fence reads as a
|
|
5298
|
+
* command to run. A block that mixes pointers with real commands keeps its fence. */
|
|
5299
|
+
function rewriteGovernancePointers(content) {
|
|
5300
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
5301
|
+
const entries = content.split(/\r?\n/).map((line) => {
|
|
5302
|
+
if (FENCE.test(line)) return {
|
|
5303
|
+
lines: [line],
|
|
5304
|
+
rewritten: false,
|
|
5305
|
+
fence: true
|
|
5306
|
+
};
|
|
5307
|
+
const names = referencedGovernances(line);
|
|
5308
|
+
if (names.length === 0) return {
|
|
5309
|
+
lines: [line],
|
|
5310
|
+
rewritten: false,
|
|
5311
|
+
fence: false
|
|
5312
|
+
};
|
|
5313
|
+
const prefix = LINE_PREFIX.exec(line)?.[0] ?? "";
|
|
5314
|
+
return {
|
|
5315
|
+
lines: names.map((name) => `${prefix}${loadInstruction(name)}`),
|
|
5316
|
+
rewritten: true,
|
|
5317
|
+
fence: false
|
|
5318
|
+
};
|
|
5319
|
+
});
|
|
5320
|
+
const out = [];
|
|
5321
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5322
|
+
const entry = entries[i];
|
|
5323
|
+
if (!entry.fence) {
|
|
5324
|
+
out.push(...entry.lines);
|
|
5325
|
+
continue;
|
|
5326
|
+
}
|
|
5327
|
+
const close = entries.findIndex((e, j) => j > i && e.fence);
|
|
5328
|
+
const inner = close === -1 ? [] : entries.slice(i + 1, close);
|
|
5329
|
+
if (!(close !== -1 && inner.some((e) => e.rewritten) && inner.every((e) => e.rewritten || e.lines.every((l) => l.trim() === "")))) {
|
|
5330
|
+
out.push(...entry.lines);
|
|
5331
|
+
continue;
|
|
5332
|
+
}
|
|
5333
|
+
for (const e of inner) out.push(...e.lines);
|
|
5334
|
+
i = close;
|
|
5335
|
+
}
|
|
5336
|
+
return out.join(eol);
|
|
5337
|
+
}
|
|
5338
|
+
/** The copies a SKILL.md fails to list under References. The specification asks SKILL.md to
|
|
5339
|
+
* reference every file directly, so a copy no References section names is a file the agent has no
|
|
5340
|
+
* documented way to reach. */
|
|
5341
|
+
function unlistedGovernanceCopies(skillMd, names) {
|
|
5342
|
+
const section = referencesSection(skillMd);
|
|
5343
|
+
return names.filter((name) => !section.includes(`${SKILL_GOVERNANCE_DIR}/${name}.md`));
|
|
5344
|
+
}
|
|
5345
|
+
/** The body of the References section, or the empty string when the document has none. */
|
|
5346
|
+
function referencesSection(skillMd) {
|
|
5347
|
+
const lines = skillMd.split(/\r?\n/);
|
|
5348
|
+
const start = lines.findIndex((line) => /^(#{1,6})\s+references\b/i.test(line));
|
|
5349
|
+
if (start === -1) return "";
|
|
5350
|
+
const level = (/^(#{1,6})/.exec(lines[start])?.[1] ?? "#").length;
|
|
5351
|
+
let end = lines.length;
|
|
5352
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
5353
|
+
const heading = /^(#{1,6})\s/.exec(lines[i]);
|
|
5354
|
+
if (heading && heading[1].length <= level) {
|
|
5355
|
+
end = i;
|
|
5356
|
+
break;
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
return lines.slice(start + 1, end).join("\n");
|
|
5360
|
+
}
|
|
5361
|
+
//#endregion
|
|
5362
|
+
//#region src/governance/copy-sync.ts
|
|
5363
|
+
/** Where a package keeps the governances it owns. A governance ships in its owner's package as a
|
|
5364
|
+
* plain Markdown file — the copy step needs nothing else from the owner. */
|
|
5365
|
+
const OWNER_DIR = "governances";
|
|
5366
|
+
/** Refreshes every skill's governance copies from their owning packages.
|
|
5367
|
+
*
|
|
5368
|
+
* The files already in `<skill>/references/governances/` declare which governances the skill uses.
|
|
5369
|
+
* Each is rewritten from its owner's current copy, every governance a copy references is copied
|
|
5370
|
+
* too, and the retrieval pointers inside each copy are rewritten to name the sibling file. */
|
|
5371
|
+
function syncGovernanceCopies(root, skills, govFs, opts = {}) {
|
|
5372
|
+
const entries = [];
|
|
5373
|
+
const errors = [];
|
|
5374
|
+
const owners = ownerDirs(root, govFs);
|
|
5375
|
+
for (const skill of skills) {
|
|
5376
|
+
const skillDir = path.dirname(skill.path);
|
|
5377
|
+
const copyDir = path.join(skillDir, ...SKILL_GOVERNANCE_DIR.split("/"));
|
|
5378
|
+
if (!govFs.isDirectory(copyDir)) continue;
|
|
5379
|
+
const copied = [];
|
|
5380
|
+
const declared = govFs.list(copyDir);
|
|
5381
|
+
const queue = declared.map((name) => ({ name }));
|
|
5382
|
+
const seen = new Set(declared);
|
|
5383
|
+
while (queue.length > 0) {
|
|
5384
|
+
const wanted = queue.shift();
|
|
5385
|
+
const { name } = wanted;
|
|
5386
|
+
const source = resolveSource(name, owners, govFs);
|
|
5387
|
+
if (!source) {
|
|
5388
|
+
errors.push(unresolvedMessage(skill.name, wanted, owners));
|
|
5389
|
+
continue;
|
|
5390
|
+
}
|
|
5391
|
+
copied.push(name);
|
|
5392
|
+
for (const referenced of referencedGovernances(source.content)) {
|
|
5393
|
+
if (seen.has(referenced)) continue;
|
|
5394
|
+
seen.add(referenced);
|
|
5395
|
+
queue.push({
|
|
5396
|
+
name: referenced,
|
|
5397
|
+
via: name
|
|
5398
|
+
});
|
|
5399
|
+
}
|
|
5400
|
+
const content = rewriteGovernancePointers(source.content);
|
|
5401
|
+
const target = path.join(copyDir, `${name}.md`);
|
|
5402
|
+
if ((govFs.exists(target) ? govFs.read(target) : null) === content) {
|
|
5403
|
+
entries.push({
|
|
5404
|
+
skill: skill.name,
|
|
5405
|
+
name,
|
|
5406
|
+
path: target,
|
|
5407
|
+
owner: source.owner,
|
|
5408
|
+
status: "unchanged"
|
|
5409
|
+
});
|
|
5410
|
+
continue;
|
|
5411
|
+
}
|
|
5412
|
+
if (opts.check || opts.dryRun) {
|
|
5413
|
+
entries.push({
|
|
5414
|
+
skill: skill.name,
|
|
5415
|
+
name,
|
|
5416
|
+
path: target,
|
|
5417
|
+
owner: source.owner,
|
|
5418
|
+
status: "stale"
|
|
5419
|
+
});
|
|
5420
|
+
continue;
|
|
5421
|
+
}
|
|
5422
|
+
govFs.write(target, content);
|
|
5423
|
+
entries.push({
|
|
5424
|
+
skill: skill.name,
|
|
5425
|
+
name,
|
|
5426
|
+
path: target,
|
|
5427
|
+
owner: source.owner,
|
|
5428
|
+
status: "written"
|
|
5429
|
+
});
|
|
5430
|
+
}
|
|
5431
|
+
const unlisted = unlistedGovernanceCopies(govFs.read(skill.path), copied);
|
|
5432
|
+
for (const name of unlisted) errors.push(`skill "${skill.name}": SKILL.md does not list \`${SKILL_GOVERNANCE_DIR}/${name}.md\` under References.`);
|
|
5433
|
+
}
|
|
5434
|
+
return {
|
|
5435
|
+
entries,
|
|
5436
|
+
errors
|
|
5437
|
+
};
|
|
5438
|
+
}
|
|
5439
|
+
function unresolvedMessage(skill, wanted, owners) {
|
|
5440
|
+
const checked = `No installed package owns it — checked ${owners.map((o) => o.name).join(", ")}.`;
|
|
5441
|
+
return wanted.via ? `skill "${skill}": governance "${wanted.via}" references "${wanted.name}", which has no copy to point at. ${checked}` : `skill "${skill}": ${SKILL_GOVERNANCE_DIR}/${wanted.name}.md names no governance. ${checked}`;
|
|
5442
|
+
}
|
|
5443
|
+
function resolveSource(name, owners, govFs) {
|
|
5444
|
+
for (const owner of owners) {
|
|
5445
|
+
const filePath = path.join(owner.dir, `${name}.md`);
|
|
5446
|
+
if (govFs.exists(filePath)) return {
|
|
5447
|
+
owner: owner.name,
|
|
5448
|
+
content: govFs.read(filePath)
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5451
|
+
return null;
|
|
5452
|
+
}
|
|
5453
|
+
/** The packages a governance may come from, in resolution order: the plugin being built, then its
|
|
5454
|
+
* declared dependencies. The plugin wins — a plugin that ships a governance owns that name, and a
|
|
5455
|
+
* build must not copy a stale published copy of a document its own tree holds. */
|
|
5456
|
+
function ownerDirs(root, govFs) {
|
|
5457
|
+
const owners = [{
|
|
5458
|
+
name: ".",
|
|
5459
|
+
dir: path.join(root, OWNER_DIR)
|
|
5460
|
+
}];
|
|
5461
|
+
for (const pkg of declaredDependencies(root, govFs)) {
|
|
5462
|
+
const dir = packageDir(root, pkg, govFs);
|
|
5463
|
+
if (dir) owners.push({
|
|
5464
|
+
name: pkg,
|
|
5465
|
+
dir: path.join(dir, OWNER_DIR)
|
|
5466
|
+
});
|
|
5467
|
+
}
|
|
5468
|
+
return owners;
|
|
5469
|
+
}
|
|
5470
|
+
/** Every package the plugin's own `package.json` declares. A governance owner is installed as a dev
|
|
5471
|
+
* dependency of the repository being built; runtime dependencies are read too, because which of
|
|
5472
|
+
* the two a plugin author picked is not this step's business. */
|
|
5473
|
+
function declaredDependencies(root, govFs) {
|
|
5474
|
+
const manifestPath = path.join(root, "package.json");
|
|
5475
|
+
if (!govFs.exists(manifestPath)) return [];
|
|
5476
|
+
let parsed;
|
|
5477
|
+
try {
|
|
5478
|
+
parsed = JSON.parse(govFs.read(manifestPath));
|
|
5479
|
+
} catch {
|
|
5480
|
+
return [];
|
|
5481
|
+
}
|
|
5482
|
+
const names = /* @__PURE__ */ new Set();
|
|
5483
|
+
for (const field of [parsed.devDependencies, parsed.dependencies]) if (field && typeof field === "object") for (const name of Object.keys(field)) names.add(name);
|
|
5484
|
+
return [...names];
|
|
5485
|
+
}
|
|
5486
|
+
/** Walks up from the plugin root looking for the installed package, the way Node resolves one. A
|
|
5487
|
+
* workspace hoists its dependencies to the repository root, so the first `node_modules` above the
|
|
5488
|
+
* plugin is usually not the one holding them. */
|
|
5489
|
+
function packageDir(root, pkg, govFs) {
|
|
5490
|
+
let dir = path.resolve(root);
|
|
5491
|
+
for (;;) {
|
|
5492
|
+
const candidate = path.join(dir, "node_modules", pkg);
|
|
5493
|
+
if (govFs.exists(path.join(candidate, "package.json"))) return candidate;
|
|
5494
|
+
const parent = path.dirname(dir);
|
|
5495
|
+
if (parent === dir) return null;
|
|
5496
|
+
dir = parent;
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
//#endregion
|
|
5500
|
+
//#region src/governance/fs.ts
|
|
5501
|
+
const realGovernanceFs = {
|
|
5502
|
+
exists: (p) => fs.existsSync(p),
|
|
5503
|
+
read: (p) => fs.readFileSync(p, "utf8"),
|
|
5504
|
+
list: (dir) => {
|
|
5505
|
+
if (!fs.existsSync(dir)) return [];
|
|
5506
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
|
|
5507
|
+
}
|
|
5508
|
+
};
|
|
5509
|
+
const realGovernanceCopyFs = {
|
|
5510
|
+
...realGovernanceFs,
|
|
5511
|
+
isDirectory: (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory(),
|
|
5512
|
+
write: (filePath, content) => {
|
|
5513
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5514
|
+
fs.writeFileSync(filePath, content);
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
//#endregion
|
|
5250
5518
|
//#region src/hooks/hooks.ts
|
|
5251
5519
|
/** Per-vendor hook support. Source: `.research/hook-event-survey/conclusion.md`, re-verified against
|
|
5252
5520
|
* vendor documentation August 2026. A vendor fact decays — re-verify before trusting this table. */
|
|
@@ -6215,6 +6483,10 @@ function validateManifest(manifest, targets) {
|
|
|
6215
6483
|
return errors;
|
|
6216
6484
|
}
|
|
6217
6485
|
function buildPlugin(root, opts = {}) {
|
|
6486
|
+
if (opts.check) opts = {
|
|
6487
|
+
...opts,
|
|
6488
|
+
dryRun: true
|
|
6489
|
+
};
|
|
6218
6490
|
const manifestPath = path.join(root, "plugin.json");
|
|
6219
6491
|
if (!fs.existsSync(manifestPath)) throw new Error(`No plugin.json found at ${root}`);
|
|
6220
6492
|
const indent = detectIndent(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -6241,7 +6513,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6241
6513
|
}
|
|
6242
6514
|
if (vendors.length === 0) {
|
|
6243
6515
|
const signals = legacyLayoutSignals(manifest, fs.existsSync(path.join(root, SHADOWING_MANIFEST)));
|
|
6244
|
-
if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor for the full diagnosis and the skill that owns each repair.`);
|
|
6516
|
+
if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor-universal-plugin for the full diagnosis and the skill that owns each repair.`);
|
|
6245
6517
|
warnings.push("No vendors declared in harnesses — nothing to build");
|
|
6246
6518
|
return {
|
|
6247
6519
|
vendors: [],
|
|
@@ -6249,6 +6521,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6249
6521
|
warnings,
|
|
6250
6522
|
rows,
|
|
6251
6523
|
catalogs: [],
|
|
6524
|
+
governances: [],
|
|
6252
6525
|
summary: summarize(rows)
|
|
6253
6526
|
};
|
|
6254
6527
|
}
|
|
@@ -6259,6 +6532,12 @@ function buildPlugin(root, opts = {}) {
|
|
|
6259
6532
|
const { vendors: _vendors, packagePath: _packagePath, harnesses: _harnesses, dependencies: declaredDependencies, ...componentConfig } = uext;
|
|
6260
6533
|
warnings.push(...validateDependencies(declaredDependencies).warnings);
|
|
6261
6534
|
const skills = readSkills(root, manifest, warnings);
|
|
6535
|
+
const governances = syncGovernanceCopies(root, skills, realGovernanceCopyFs, {
|
|
6536
|
+
check: opts.check,
|
|
6537
|
+
dryRun: opts.dryRun
|
|
6538
|
+
});
|
|
6539
|
+
if (governances.errors.length > 0) throw new Error(`Governance copies are not buildable:\n${governances.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
6540
|
+
written.push(...governances.entries.filter((e) => e.status === "written").map((e) => e.path));
|
|
6262
6541
|
const canonicalHooks = readCanonicalHooks(root, componentConfig["hooks"], warnings);
|
|
6263
6542
|
const declaredMcp = readCanonicalMcpServers(root, componentConfig["mcpServers"], warnings);
|
|
6264
6543
|
const mcp = declaredMcp ? pinMcpServers(declaredMcp.servers, manifest.version) : null;
|
|
@@ -6341,6 +6620,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6341
6620
|
rows,
|
|
6342
6621
|
catalogs,
|
|
6343
6622
|
catalogRoot,
|
|
6623
|
+
governances: governances.entries,
|
|
6344
6624
|
summary: summarize(rows)
|
|
6345
6625
|
};
|
|
6346
6626
|
}
|
|
@@ -6681,8 +6961,8 @@ function formatMcpNote(note) {
|
|
|
6681
6961
|
}
|
|
6682
6962
|
//#endregion
|
|
6683
6963
|
//#region src/build/cli.ts
|
|
6684
|
-
const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor — diagnose why nothing is declared\n";
|
|
6685
|
-
const NEXT_STEP_BUILT = "→ /universal-plugin:doctor — check the built manifests against plugin.json\n";
|
|
6964
|
+
const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor-universal-plugin — diagnose why nothing is declared\n";
|
|
6965
|
+
const NEXT_STEP_BUILT = "→ /universal-plugin:doctor-universal-plugin — check the built manifests against plugin.json\n";
|
|
6686
6966
|
/** Every hint names a command that exists today: an agent that follows a hint into
|
|
6687
6967
|
* `unknown command` is at a dead end (issue #80). A refreshed catalog is checked by
|
|
6688
6968
|
* `marketplace validate`, pointed at the repository the catalog lives in. */
|
|
@@ -6693,19 +6973,27 @@ function nextStep$1(result, cwd) {
|
|
|
6693
6973
|
const quoted = /\s/.test(relative) ? JSON.stringify(relative) : relative;
|
|
6694
6974
|
return `→ universal-plugin marketplace validate${relative === "" ? "" : ` --root ${quoted}`}\n`;
|
|
6695
6975
|
}
|
|
6976
|
+
/** What repairs a stale copy: the same build, without `--check`. */
|
|
6977
|
+
function checkNextStep(root) {
|
|
6978
|
+
return `\u2192 universal-plugin plugin build${root ? ` --root ${root}` : ""} \u2014 refresh the stale governance copies\n`;
|
|
6979
|
+
}
|
|
6696
6980
|
function buildCommand() {
|
|
6697
6981
|
const cmd = new Command("build").description("Generate vendor manifests from plugin.json");
|
|
6698
|
-
cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action((opts) => {
|
|
6982
|
+
cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--check", "Write nothing; fail when a committed governance copy differs from its source").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action((opts) => {
|
|
6699
6983
|
try {
|
|
6700
6984
|
const result = buildPlugin(resolveRoot(opts.root), {
|
|
6701
6985
|
vendor: opts.vendor,
|
|
6702
6986
|
dryRun: opts.dryRun,
|
|
6703
6987
|
verbose: opts.verbose,
|
|
6704
|
-
clean: opts.clean
|
|
6988
|
+
clean: opts.clean,
|
|
6989
|
+
check: opts.check
|
|
6705
6990
|
});
|
|
6706
6991
|
for (const warning of result.warnings) process.stderr.write(`warn: ${warning}\n`);
|
|
6992
|
+
const stale = result.governances.filter((g) => g.status === "stale");
|
|
6993
|
+
if (opts.check) for (const copy of stale) process.stderr.write(`stale: ${path.relative(process.cwd(), copy.path)} (owner ${copy.owner})\n`);
|
|
6707
6994
|
const { built, skipped, failed, canonical } = result.summary;
|
|
6708
6995
|
const jsonResult = {
|
|
6996
|
+
governances: result.governances,
|
|
6709
6997
|
built: result.rows.filter((r) => r.status === "built"),
|
|
6710
6998
|
skipped: result.rows.filter((r) => r.status === "skipped"),
|
|
6711
6999
|
failed: result.rows.filter((r) => r.status === "failed"),
|
|
@@ -6716,6 +7004,7 @@ function buildCommand() {
|
|
|
6716
7004
|
};
|
|
6717
7005
|
const counts = `built ${built}, skipped ${skipped}, failed ${failed}`;
|
|
6718
7006
|
const catalogSummary = result.catalogs.length > 0 ? `, catalogs ${result.catalogs.length}` : "";
|
|
7007
|
+
const governanceSummary = result.governances.length > 0 ? `, governance copies ${result.governances.length}` : "";
|
|
6719
7008
|
output(jsonResult, {
|
|
6720
7009
|
vendors: result.rows.map((r) => ({
|
|
6721
7010
|
vendor: r.vendor,
|
|
@@ -6726,10 +7015,16 @@ function buildCommand() {
|
|
|
6726
7015
|
path: r.path,
|
|
6727
7016
|
status: r.status
|
|
6728
7017
|
})),
|
|
6729
|
-
|
|
7018
|
+
...result.governances.length > 0 ? { governances: result.governances.map((g) => ({
|
|
7019
|
+
skill: g.skill,
|
|
7020
|
+
name: g.name,
|
|
7021
|
+
status: g.status
|
|
7022
|
+
})) } : {},
|
|
7023
|
+
summary: (canonical > 0 ? `${counts}, served by plugin.json ${canonical}` : counts) + catalogSummary + governanceSummary
|
|
6730
7024
|
});
|
|
6731
|
-
process.stderr.write(nextStep$1(result, process.cwd()));
|
|
7025
|
+
process.stderr.write(opts.check && stale.length > 0 ? checkNextStep(opts.root) : nextStep$1(result, process.cwd()));
|
|
6732
7026
|
if (failed > 0) process.exitCode = 1;
|
|
7027
|
+
if (opts.check && stale.length > 0) process.exitCode = 1;
|
|
6733
7028
|
} catch (err) {
|
|
6734
7029
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
6735
7030
|
process.exit(1);
|
|
@@ -7156,16 +7451,6 @@ function configCommand(fs = realConfigFs) {
|
|
|
7156
7451
|
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand(fs)).addCommand(getCommand(fs));
|
|
7157
7452
|
}
|
|
7158
7453
|
//#endregion
|
|
7159
|
-
//#region src/governance/fs.ts
|
|
7160
|
-
const realGovernanceFs = {
|
|
7161
|
-
exists: (p) => fs.existsSync(p),
|
|
7162
|
-
read: (p) => fs.readFileSync(p, "utf8"),
|
|
7163
|
-
list: (dir) => {
|
|
7164
|
-
if (!fs.existsSync(dir)) return [];
|
|
7165
|
-
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
|
|
7166
|
-
}
|
|
7167
|
-
};
|
|
7168
|
-
//#endregion
|
|
7169
7454
|
//#region src/governance/governance.ts
|
|
7170
7455
|
function getManagedDir() {
|
|
7171
7456
|
if (process.platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Universal AI agent plugin build tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-plugin",
|
|
@@ -63,12 +63,13 @@
|
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsdown",
|
|
66
|
+
"check:governances": "tsx src/cli.ts plugin build --check --root .",
|
|
66
67
|
"dev": "tsx src/cli.ts",
|
|
67
68
|
"knip": "knip",
|
|
68
69
|
"lint": "biome check .",
|
|
69
70
|
"test": "pnpm build && vitest run src",
|
|
70
71
|
"test:watch": "vitest",
|
|
71
72
|
"typecheck": "tsc --noEmit",
|
|
72
|
-
"verify": "pnpm typecheck && pnpm lint && pnpm test"
|
|
73
|
+
"verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm check:governances"
|
|
73
74
|
}
|
|
74
75
|
}
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "universal-plugin",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: doctor
|
|
2
|
+
name: doctor-universal-plugin
|
|
3
3
|
description: Use this skill to diagnose a universal agent plugin — when `plugin build` reports "built 0" or "nothing to build", when it warns "No vendors declared in harnesses", when a repository still carries `.plugin/plugin.json` or a top-level `vendorExtensions` block after upgrading universal-plugin across a major, when a released version never reached the vendor manifests, when a runtime loads none of the plugin's skills, when a vendor manifest is missing or looks out of date after a pull, or when checking whether what the canonical plugin.json declares still matches what is on disk for Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "is my plugin set up right", "why isn't my plugin loading", "the build says built 0", "why did nothing get built", "check the plugin", "are the vendor manifests current", or "what does this plugin declare".
|
|
4
4
|
---
|
|
5
5
|
|
|
File without changes
|