universal-plugin 0.9.0 → 0.11.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 +1106 -492
- package/dist/data/vendors.json +0 -9
- package/package.json +4 -4
- package/plugin.json +1 -1
- package/readme.md +11 -9
- package/{governances → references}/plugin-design.md +1 -5
- package/references/universal-plugin.md +4 -0
- package/skills/build-plugin/README.md +29 -0
- package/skills/build-plugin/SKILL.md +137 -0
- package/skills/build-plugin/scripts/build.mjs +11 -0
- package/skills/doctor-universal-plugin/SKILL.md +1 -1
- package/skills/doctor-universal-plugin/scripts/doctor.mjs +1 -1
- package/skills/init-universal-plugin/SKILL.md +3 -6
- package/skills/init-universal-plugin/references/create.md +1 -1
- package/skills/init-universal-plugin/references/standard.md +2 -2
- package/skills/init-universal-plugin/references/vendors/cursor.md +1 -1
- package/skills/marketplace/README.md +23 -7
- package/skills/marketplace/SKILL.md +54 -165
- package/skills/marketplace/references/add.md +138 -0
- package/skills/marketplace/references/init.md +131 -0
- package/skills/marketplace/references/validate.md +46 -0
- package/skills/marketplace/scripts/add.mjs +11 -0
- package/skills/migrate-plugin/SKILL.md +4 -3
- /package/{governances → references}/slash-invocation.md +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -4,8 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import * as childProcess from "node:child_process";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { join, win32 } from "node:path";
|
|
7
8
|
import * as fs from "node:fs";
|
|
8
9
|
import * as os from "node:os";
|
|
10
|
+
import { homedir } from "node:os";
|
|
9
11
|
//#region \0rolldown/runtime.js
|
|
10
12
|
var __create = Object.create;
|
|
11
13
|
var __defProp = Object.defineProperty;
|
|
@@ -3779,12 +3781,6 @@ function output(data, view) {
|
|
|
3779
3781
|
if (isJsonOutput()) printJson(data);
|
|
3780
3782
|
else console.log(encode(view ?? data));
|
|
3781
3783
|
}
|
|
3782
|
-
/** Print a result whose default rendering is a document body rather than a record.
|
|
3783
|
-
* `--format json` still emits the structured payload. */
|
|
3784
|
-
function outputText(data, text) {
|
|
3785
|
-
if (isJsonOutput()) printJson(data);
|
|
3786
|
-
else text();
|
|
3787
|
-
}
|
|
3788
3784
|
//#endregion
|
|
3789
3785
|
//#region ../../node_modules/.pnpm/semver@7.8.1/node_modules/semver/internal/constants.js
|
|
3790
3786
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -5247,274 +5243,6 @@ function discardedRangeWarning(index, entry, range) {
|
|
|
5247
5243
|
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
5244
|
}
|
|
5249
5245
|
//#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
|
|
5518
5246
|
//#region src/hooks/hooks.ts
|
|
5519
5247
|
/** Per-vendor hook support. Source: `.research/hook-event-survey/conclusion.md`, re-verified against
|
|
5520
5248
|
* vendor documentation August 2026. A vendor fact decays — re-verify before trusting this table. */
|
|
@@ -5681,6 +5409,23 @@ function claudeArtifact(metadata, plugins) {
|
|
|
5681
5409
|
})
|
|
5682
5410
|
};
|
|
5683
5411
|
}
|
|
5412
|
+
/** Whether a catalog entry's `source` names a place inside this repository — a `./`-prefixed path in
|
|
5413
|
+
* the Claude-shaped catalogs, or Codex's `{ source: "local", path }`. Every other form names a
|
|
5414
|
+
* plugin distributed from somewhere else: an npm package, a GitHub repository, a URL. Discovery
|
|
5415
|
+
* walks directories, so it can produce the first kind and can say nothing about the second. */
|
|
5416
|
+
function isLocalCatalogSource(source) {
|
|
5417
|
+
if (typeof source === "string") return true;
|
|
5418
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return false;
|
|
5419
|
+
return source.source === "local";
|
|
5420
|
+
}
|
|
5421
|
+
/** Codex states a source as an object either way: a repository path is tagged `local`, and a source
|
|
5422
|
+
* that already carries its own tag passes through as it stands. */
|
|
5423
|
+
function codexSource(source) {
|
|
5424
|
+
return typeof source === "string" ? {
|
|
5425
|
+
source: "local",
|
|
5426
|
+
path: source
|
|
5427
|
+
} : source;
|
|
5428
|
+
}
|
|
5684
5429
|
function codexArtifact(metadata, plugins) {
|
|
5685
5430
|
return {
|
|
5686
5431
|
path: TARGET_CATALOG_PATHS.codex,
|
|
@@ -5690,10 +5435,7 @@ function codexArtifact(metadata, plugins) {
|
|
|
5690
5435
|
plugins: plugins.map((plugin) => ({
|
|
5691
5436
|
name: plugin.name,
|
|
5692
5437
|
version: plugin.metadata.version,
|
|
5693
|
-
source:
|
|
5694
|
-
source: "local",
|
|
5695
|
-
path: plugin.source
|
|
5696
|
-
},
|
|
5438
|
+
source: codexSource(plugin.source),
|
|
5697
5439
|
policy: {
|
|
5698
5440
|
installation: "AVAILABLE",
|
|
5699
5441
|
authentication: "ON_INSTALL"
|
|
@@ -5742,6 +5484,72 @@ const VENDOR_TARGETS = {
|
|
|
5742
5484
|
codex: "codex",
|
|
5743
5485
|
"copilot-cli": "copilot"
|
|
5744
5486
|
};
|
|
5487
|
+
const GITHUB_URL = /^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:)([^/]+\/[^/]+?)(?:\.git)?\/?$/;
|
|
5488
|
+
/** A git URL read as an origin, recognizing a GitHub one so an entry at the repository root can use
|
|
5489
|
+
* the tidier `github` form. */
|
|
5490
|
+
function originFromUrl(url) {
|
|
5491
|
+
const match = GITHUB_URL.exec(url.trim());
|
|
5492
|
+
return match ? {
|
|
5493
|
+
url,
|
|
5494
|
+
repo: match[1]
|
|
5495
|
+
} : { url };
|
|
5496
|
+
}
|
|
5497
|
+
/** An `owner/repo` slug as an origin. */
|
|
5498
|
+
function originFromRepo(repo) {
|
|
5499
|
+
return {
|
|
5500
|
+
url: `https://github.com/${repo}.git`,
|
|
5501
|
+
repo
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
/** The path part of a local source, `./` stripped, empty at the marketplace root. */
|
|
5505
|
+
function localSourcePath(source) {
|
|
5506
|
+
const raw = typeof source === "string" ? source : source.path;
|
|
5507
|
+
if (typeof raw !== "string") return void 0;
|
|
5508
|
+
return raw.replace(/^\.\/?/, "").replace(/\/+$/, "");
|
|
5509
|
+
}
|
|
5510
|
+
/** Rewrites a source that is local to *another* marketplace into one that resolves from anywhere.
|
|
5511
|
+
*
|
|
5512
|
+
* A copied entry's `./plugins/aced` is relative to the marketplace it came from, so carrying it
|
|
5513
|
+
* across unchanged would point at a directory this repository does not have. The path is not
|
|
5514
|
+
* useless, though — it is a location inside a repository whose URL is known, which is exactly what
|
|
5515
|
+
* `git-subdir` states. An entry at that repository's root needs no subdirectory and takes the
|
|
5516
|
+
* plainer `github` or `url` form. */
|
|
5517
|
+
function absoluteSource(origin, source) {
|
|
5518
|
+
const subdir = localSourcePath(source);
|
|
5519
|
+
if (subdir === void 0) return void 0;
|
|
5520
|
+
if (subdir === "" || subdir === ".") return origin.repo ? {
|
|
5521
|
+
source: "github",
|
|
5522
|
+
repo: origin.repo
|
|
5523
|
+
} : {
|
|
5524
|
+
source: "url",
|
|
5525
|
+
url: origin.url
|
|
5526
|
+
};
|
|
5527
|
+
return {
|
|
5528
|
+
source: "git-subdir",
|
|
5529
|
+
url: origin.url,
|
|
5530
|
+
path: subdir
|
|
5531
|
+
};
|
|
5532
|
+
}
|
|
5533
|
+
/** The source forms each runtime installs from./** The source forms each runtime installs from.
|
|
5534
|
+
*
|
|
5535
|
+
* Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
|
|
5536
|
+
* cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
|
|
5537
|
+
* cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
|
|
5538
|
+
* (<https://json.schemastore.org/claude-code-marketplace.json>). Codex documents npm alongside a
|
|
5539
|
+
* local path. Copilot CLI and Cursor document local paths only, which is why an npm entry reaches
|
|
5540
|
+
* two catalogs rather than four (`.research/local-marketplaces`, and issue #86). */
|
|
5541
|
+
const TARGET_SOURCE_KINDS = {
|
|
5542
|
+
claude: [
|
|
5543
|
+
"path",
|
|
5544
|
+
"npm",
|
|
5545
|
+
"github",
|
|
5546
|
+
"url",
|
|
5547
|
+
"git-subdir"
|
|
5548
|
+
],
|
|
5549
|
+
codex: ["path", "npm"],
|
|
5550
|
+
copilot: ["path"],
|
|
5551
|
+
cursor: ["path"]
|
|
5552
|
+
};
|
|
5745
5553
|
/** Folds one plugin's entry into a catalog that may already exist, and returns the artifact to
|
|
5746
5554
|
* write. An existing catalog keeps its own top-level fields — its name, its owner, a description
|
|
5747
5555
|
* someone wrote — and every entry it lists for other plugins, in place. Only this plugin's entry is
|
|
@@ -5750,7 +5558,7 @@ const VENDOR_TARGETS = {
|
|
|
5750
5558
|
* `version` is derived, never authored (ADR-0010 §3): the entry carries whatever the canonical
|
|
5751
5559
|
* manifest carries, and a version left behind on an entry whose manifest declares none is removed
|
|
5752
5560
|
* rather than kept. */
|
|
5753
|
-
function mergeCatalogEntry(target, metadata, plugin, readExisting) {
|
|
5561
|
+
function mergeCatalogEntry(target, metadata, plugin, readExisting, { keepForeignSource = true } = {}) {
|
|
5754
5562
|
const artifact = serializeTarget(target, metadata, [plugin])[0];
|
|
5755
5563
|
const existing = readExisting(artifact.path);
|
|
5756
5564
|
if (existing === void 0) return artifact;
|
|
@@ -5759,7 +5567,7 @@ function mergeCatalogEntry(target, metadata, plugin, readExisting) {
|
|
|
5759
5567
|
const entry = generated.plugins[0];
|
|
5760
5568
|
const merged = { ...previous };
|
|
5761
5569
|
for (const [key, value] of Object.entries(generated)) if (!(key in merged)) merged[key] = value;
|
|
5762
|
-
merged.plugins = mergeEntries(previous, entry);
|
|
5570
|
+
merged.plugins = mergeEntries(previous, entry, { keepForeignSource });
|
|
5763
5571
|
return {
|
|
5764
5572
|
path: artifact.path,
|
|
5765
5573
|
content: json(merged)
|
|
@@ -5775,18 +5583,78 @@ function parseCatalog(content, path) {
|
|
|
5775
5583
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`error: existing catalog "${path}" is not a JSON object`);
|
|
5776
5584
|
return parsed;
|
|
5777
5585
|
}
|
|
5778
|
-
function mergeEntries(previous, entry) {
|
|
5586
|
+
function mergeEntries(previous, entry, { keepForeignSource = false } = {}) {
|
|
5779
5587
|
const entries = Array.isArray(previous.plugins) ? [...previous.plugins] : [];
|
|
5780
5588
|
const index = entries.findIndex((candidate) => typeof candidate === "object" && candidate !== null && candidate.name === entry.name);
|
|
5781
5589
|
if (index === -1) return [...entries, entry];
|
|
5590
|
+
const existing = entries[index];
|
|
5782
5591
|
const merged = {
|
|
5783
|
-
...
|
|
5592
|
+
...existing,
|
|
5784
5593
|
...entry
|
|
5785
5594
|
};
|
|
5786
5595
|
if (!("version" in entry)) delete merged.version;
|
|
5596
|
+
if (keepForeignSource && !isLocalCatalogSource(existing.source)) merged.source = existing.source;
|
|
5787
5597
|
entries[index] = merged;
|
|
5788
5598
|
return entries;
|
|
5789
5599
|
}
|
|
5600
|
+
/** Re-derives a whole catalog from discovery while keeping the entries discovery cannot see.
|
|
5601
|
+
*
|
|
5602
|
+
* A repository may list plugins that live somewhere else — an npm package, a GitHub repository —
|
|
5603
|
+
* put there by `marketplace add`. Nothing on disk produces those, so a regeneration that trusted
|
|
5604
|
+
* discovery alone would report every one of them as a deletion, and `--force` would carry it out.
|
|
5605
|
+
* Two things survive instead: an entry whose source is not local stays even though discovery never
|
|
5606
|
+
* saw it, and a discovered plugin whose existing entry names a non-local source keeps that source
|
|
5607
|
+
* with only its derived metadata refreshed.
|
|
5608
|
+
*
|
|
5609
|
+
* That second case is the loss in issue #86 — a plugin shipped through npm, whose repository path
|
|
5610
|
+
* holds only gitignored build output, rewritten to that path on every regeneration.
|
|
5611
|
+
*
|
|
5612
|
+
* What discovery owns, it still owns: a local-path entry it no longer finds is dropped, so the
|
|
5613
|
+
* catalog keeps mirroring the repository for the part of the repository it describes. */
|
|
5614
|
+
function mergeDiscoveredCatalog(generated, existing) {
|
|
5615
|
+
if (existing === void 0) return generated;
|
|
5616
|
+
let previous;
|
|
5617
|
+
try {
|
|
5618
|
+
previous = parseCatalog(existing, generated.path);
|
|
5619
|
+
} catch {
|
|
5620
|
+
return generated;
|
|
5621
|
+
}
|
|
5622
|
+
const foreign = /* @__PURE__ */ new Map();
|
|
5623
|
+
for (const candidate of Array.isArray(previous.plugins) ? previous.plugins : []) {
|
|
5624
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
|
|
5625
|
+
const entry = candidate;
|
|
5626
|
+
if (typeof entry.name !== "string" || isLocalCatalogSource(entry.source)) continue;
|
|
5627
|
+
foreign.set(entry.name, entry);
|
|
5628
|
+
}
|
|
5629
|
+
if (foreign.size === 0) return generated;
|
|
5630
|
+
const catalog = JSON.parse(generated.content);
|
|
5631
|
+
const discovered = catalog.plugins ?? [];
|
|
5632
|
+
const merged = discovered.map((entry) => {
|
|
5633
|
+
const kept = typeof entry.name === "string" ? foreign.get(entry.name) : void 0;
|
|
5634
|
+
return kept === void 0 ? entry : {
|
|
5635
|
+
...entry,
|
|
5636
|
+
source: kept.source
|
|
5637
|
+
};
|
|
5638
|
+
});
|
|
5639
|
+
const discoveredNames = new Set(discovered.map((entry) => entry.name));
|
|
5640
|
+
for (const [name, entry] of foreign) if (!discoveredNames.has(name)) merged.push(entry);
|
|
5641
|
+
catalog.plugins = merged;
|
|
5642
|
+
return {
|
|
5643
|
+
path: generated.path,
|
|
5644
|
+
content: json(catalog)
|
|
5645
|
+
};
|
|
5646
|
+
}
|
|
5647
|
+
/** The plugin names a catalog lists, so a result row can report what the file ends up saying rather
|
|
5648
|
+
* than only what discovery contributed to it. */
|
|
5649
|
+
function catalogEntryNames(content) {
|
|
5650
|
+
try {
|
|
5651
|
+
const parsed = JSON.parse(content);
|
|
5652
|
+
if (!Array.isArray(parsed.plugins)) return [];
|
|
5653
|
+
return parsed.plugins.map((entry) => typeof entry === "object" && entry !== null ? entry.name : void 0).filter((name) => typeof name === "string");
|
|
5654
|
+
} catch {
|
|
5655
|
+
return [];
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5790
5658
|
/** The catalog's own top-level identity, read back from the file the repository already carries, so
|
|
5791
5659
|
* a refresh re-derives one entry without proposing a name or an owner of its own. */
|
|
5792
5660
|
function existingMetadata(previous) {
|
|
@@ -5813,7 +5681,7 @@ function refreshCatalogEntry(target, plugin, existing) {
|
|
|
5813
5681
|
path: catalogPath,
|
|
5814
5682
|
content: json({
|
|
5815
5683
|
...previous,
|
|
5816
|
-
plugins: mergeEntries(previous, entry)
|
|
5684
|
+
plugins: mergeEntries(previous, entry, { keepForeignSource: true })
|
|
5817
5685
|
})
|
|
5818
5686
|
};
|
|
5819
5687
|
}
|
|
@@ -5900,6 +5768,79 @@ function gatherCatalogRepo(root) {
|
|
|
5900
5768
|
catalogs
|
|
5901
5769
|
};
|
|
5902
5770
|
}
|
|
5771
|
+
/** Where Claude Code records the marketplaces a user has added, and clones each one.
|
|
5772
|
+
*
|
|
5773
|
+
* This is read, never written. It is the one place on the machine that already knows what
|
|
5774
|
+
* `<plugin>@<marketplace>` refers to, so resolving through it asks the user for nothing and reaches
|
|
5775
|
+
* no network. A marketplace they have not added is simply not found, and `--from` names it instead. */
|
|
5776
|
+
function claudePluginsHome(home = os.homedir()) {
|
|
5777
|
+
return path.join(home, ".claude", "plugins");
|
|
5778
|
+
}
|
|
5779
|
+
/** The origin the runtime recorded for a marketplace, in the vocabulary it records it in: a
|
|
5780
|
+
* `github` repo slug, or a `git`/`url` clone URL. */
|
|
5781
|
+
function registryOrigin(source) {
|
|
5782
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return void 0;
|
|
5783
|
+
const record = source;
|
|
5784
|
+
if (record.source === "github" && typeof record.repo === "string") return originFromRepo(record.repo);
|
|
5785
|
+
if ((record.source === "git" || record.source === "url") && typeof record.url === "string") return originFromUrl(record.url);
|
|
5786
|
+
}
|
|
5787
|
+
/** The URL a checkout was cloned from, which is the origin for a marketplace the registry does not
|
|
5788
|
+
* describe — one reached through `--from`, or added before the runtime recorded a source. */
|
|
5789
|
+
function gitRemoteUrl(dir) {
|
|
5790
|
+
try {
|
|
5791
|
+
const url = execFileSync("git", [
|
|
5792
|
+
"-C",
|
|
5793
|
+
dir,
|
|
5794
|
+
"remote",
|
|
5795
|
+
"get-url",
|
|
5796
|
+
"origin"
|
|
5797
|
+
], {
|
|
5798
|
+
encoding: "utf8",
|
|
5799
|
+
stdio: [
|
|
5800
|
+
"ignore",
|
|
5801
|
+
"pipe",
|
|
5802
|
+
"ignore"
|
|
5803
|
+
]
|
|
5804
|
+
}).trim();
|
|
5805
|
+
return url === "" ? void 0 : url;
|
|
5806
|
+
} catch {
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
/** The marketplace a name refers to, or `undefined` when the user has not added it. */
|
|
5811
|
+
function resolveKnownMarketplace(name, marketplaceFs = realMarketplaceFs, home = os.homedir()) {
|
|
5812
|
+
const pluginsHome = claudePluginsHome(home);
|
|
5813
|
+
const registry = path.join(pluginsHome, "known_marketplaces.json");
|
|
5814
|
+
if (marketplaceFs.exists(registry)) {
|
|
5815
|
+
let parsed;
|
|
5816
|
+
try {
|
|
5817
|
+
parsed = JSON.parse(marketplaceFs.read(registry));
|
|
5818
|
+
} catch {
|
|
5819
|
+
parsed = void 0;
|
|
5820
|
+
}
|
|
5821
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5822
|
+
const entry = parsed[name];
|
|
5823
|
+
if (typeof entry === "object" && entry !== null) {
|
|
5824
|
+
const record = entry;
|
|
5825
|
+
const location = record.installLocation;
|
|
5826
|
+
if (typeof location === "string" && marketplaceFs.exists(location)) return {
|
|
5827
|
+
dir: location,
|
|
5828
|
+
origin: registryOrigin(record.source)
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
const conventional = path.join(pluginsHome, "marketplaces", name);
|
|
5834
|
+
return marketplaceFs.exists(conventional) ? { dir: conventional } : void 0;
|
|
5835
|
+
}
|
|
5836
|
+
/** The catalog a marketplace directory carries, tried in the order the runtimes agree on: the Claude
|
|
5837
|
+
* path first, because three of the four read it. */
|
|
5838
|
+
function readMarketplaceCatalog(dir, marketplaceFs = realMarketplaceFs) {
|
|
5839
|
+
for (const relative of Object.values(TARGET_CATALOG_PATHS)) {
|
|
5840
|
+
const file = path.join(dir, relative);
|
|
5841
|
+
if (marketplaceFs.exists(file)) return marketplaceFs.read(file);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5903
5844
|
//#endregion
|
|
5904
5845
|
//#region src/marketplace/validation.ts
|
|
5905
5846
|
function isObject$1(value) {
|
|
@@ -6483,10 +6424,6 @@ function validateManifest(manifest, targets) {
|
|
|
6483
6424
|
return errors;
|
|
6484
6425
|
}
|
|
6485
6426
|
function buildPlugin(root, opts = {}) {
|
|
6486
|
-
if (opts.check) opts = {
|
|
6487
|
-
...opts,
|
|
6488
|
-
dryRun: true
|
|
6489
|
-
};
|
|
6490
6427
|
const manifestPath = path.join(root, "plugin.json");
|
|
6491
6428
|
if (!fs.existsSync(manifestPath)) throw new Error(`No plugin.json found at ${root}`);
|
|
6492
6429
|
const indent = detectIndent(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -6521,7 +6458,6 @@ function buildPlugin(root, opts = {}) {
|
|
|
6521
6458
|
warnings,
|
|
6522
6459
|
rows,
|
|
6523
6460
|
catalogs: [],
|
|
6524
|
-
governances: [],
|
|
6525
6461
|
summary: summarize(rows)
|
|
6526
6462
|
};
|
|
6527
6463
|
}
|
|
@@ -6532,12 +6468,6 @@ function buildPlugin(root, opts = {}) {
|
|
|
6532
6468
|
const { vendors: _vendors, packagePath: _packagePath, harnesses: _harnesses, dependencies: declaredDependencies, ...componentConfig } = uext;
|
|
6533
6469
|
warnings.push(...validateDependencies(declaredDependencies).warnings);
|
|
6534
6470
|
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));
|
|
6541
6471
|
const canonicalHooks = readCanonicalHooks(root, componentConfig["hooks"], warnings);
|
|
6542
6472
|
const declaredMcp = readCanonicalMcpServers(root, componentConfig["mcpServers"], warnings);
|
|
6543
6473
|
const mcp = declaredMcp ? pinMcpServers(declaredMcp.servers, manifest.version) : null;
|
|
@@ -6620,7 +6550,6 @@ function buildPlugin(root, opts = {}) {
|
|
|
6620
6550
|
rows,
|
|
6621
6551
|
catalogs,
|
|
6622
6552
|
catalogRoot,
|
|
6623
|
-
governances: governances.entries,
|
|
6624
6553
|
summary: summarize(rows)
|
|
6625
6554
|
};
|
|
6626
6555
|
}
|
|
@@ -6973,27 +6902,19 @@ function nextStep$1(result, cwd) {
|
|
|
6973
6902
|
const quoted = /\s/.test(relative) ? JSON.stringify(relative) : relative;
|
|
6974
6903
|
return `→ universal-plugin marketplace validate${relative === "" ? "" : ` --root ${quoted}`}\n`;
|
|
6975
6904
|
}
|
|
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
|
-
}
|
|
6980
6905
|
function buildCommand() {
|
|
6981
6906
|
const cmd = new Command("build").description("Generate vendor manifests from plugin.json");
|
|
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("--
|
|
6907
|
+
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) => {
|
|
6983
6908
|
try {
|
|
6984
6909
|
const result = buildPlugin(resolveRoot(opts.root), {
|
|
6985
6910
|
vendor: opts.vendor,
|
|
6986
6911
|
dryRun: opts.dryRun,
|
|
6987
6912
|
verbose: opts.verbose,
|
|
6988
|
-
clean: opts.clean
|
|
6989
|
-
check: opts.check
|
|
6913
|
+
clean: opts.clean
|
|
6990
6914
|
});
|
|
6991
6915
|
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`);
|
|
6994
6916
|
const { built, skipped, failed, canonical } = result.summary;
|
|
6995
6917
|
const jsonResult = {
|
|
6996
|
-
governances: result.governances,
|
|
6997
6918
|
built: result.rows.filter((r) => r.status === "built"),
|
|
6998
6919
|
skipped: result.rows.filter((r) => r.status === "skipped"),
|
|
6999
6920
|
failed: result.rows.filter((r) => r.status === "failed"),
|
|
@@ -7004,7 +6925,6 @@ function buildCommand() {
|
|
|
7004
6925
|
};
|
|
7005
6926
|
const counts = `built ${built}, skipped ${skipped}, failed ${failed}`;
|
|
7006
6927
|
const catalogSummary = result.catalogs.length > 0 ? `, catalogs ${result.catalogs.length}` : "";
|
|
7007
|
-
const governanceSummary = result.governances.length > 0 ? `, governance copies ${result.governances.length}` : "";
|
|
7008
6928
|
output(jsonResult, {
|
|
7009
6929
|
vendors: result.rows.map((r) => ({
|
|
7010
6930
|
vendor: r.vendor,
|
|
@@ -7015,16 +6935,10 @@ function buildCommand() {
|
|
|
7015
6935
|
path: r.path,
|
|
7016
6936
|
status: r.status
|
|
7017
6937
|
})),
|
|
7018
|
-
|
|
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
|
|
6938
|
+
summary: (canonical > 0 ? `${counts}, served by plugin.json ${canonical}` : counts) + catalogSummary
|
|
7024
6939
|
});
|
|
7025
|
-
process.stderr.write(
|
|
6940
|
+
process.stderr.write(nextStep$1(result, process.cwd()));
|
|
7026
6941
|
if (failed > 0) process.exitCode = 1;
|
|
7027
|
-
if (opts.check && stale.length > 0) process.exitCode = 1;
|
|
7028
6942
|
} catch (err) {
|
|
7029
6943
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
7030
6944
|
process.exit(1);
|
|
@@ -7389,7 +7303,7 @@ const realConfigFs = {
|
|
|
7389
7303
|
function assertNotReserved(key) {
|
|
7390
7304
|
if (isReservedKey(key)) throw new Error(`error: "${key}" is a reserved key (universal-plugin's own config) — not a plugin-registered array; edit .agents/universal-plugin.json directly`);
|
|
7391
7305
|
}
|
|
7392
|
-
function addCommand(fs) {
|
|
7306
|
+
function addCommand$1(fs) {
|
|
7393
7307
|
return new Command("add").description("Register (append or replace-by-name) an entry in the array at a config key").requiredOption("--key <key>", "The config key whose array to write").requiredOption("--entry <json>", "The entry object as JSON (must include a \"name\" field)").option("--format <format>", "Output format: json or toon (default: toon)").addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin config add --key sdd-plugins --entry '{\"name\":\"aces\",\"handles\":[\"agent evaluation\"]}'\n").action((opts) => {
|
|
7394
7308
|
try {
|
|
7395
7309
|
assertNotReserved(opts.key);
|
|
@@ -7448,139 +7362,7 @@ function getCommand(fs) {
|
|
|
7448
7362
|
});
|
|
7449
7363
|
}
|
|
7450
7364
|
function configCommand(fs = realConfigFs) {
|
|
7451
|
-
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand(fs)).addCommand(getCommand(fs));
|
|
7452
|
-
}
|
|
7453
|
-
//#endregion
|
|
7454
|
-
//#region src/governance/governance.ts
|
|
7455
|
-
function getManagedDir() {
|
|
7456
|
-
if (process.platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
|
|
7457
|
-
if (process.platform === "win32") return path.join(process.env["ProgramData"] ?? "C:\\ProgramData", "UniPlugin", "governances");
|
|
7458
|
-
return "/etc/universal-plugin/governances";
|
|
7459
|
-
}
|
|
7460
|
-
function getUserDir() {
|
|
7461
|
-
return path.join(os.homedir(), ".agents", "governances");
|
|
7462
|
-
}
|
|
7463
|
-
function getProjectDir(root) {
|
|
7464
|
-
return path.join(root, "governances");
|
|
7465
|
-
}
|
|
7466
|
-
function getLocalDir(root) {
|
|
7467
|
-
return path.join(root, ".agents", "governances");
|
|
7468
|
-
}
|
|
7469
|
-
function getPackageDir() {
|
|
7470
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
7471
|
-
return path.join(path.dirname(thisFile), "..", "governances");
|
|
7472
|
-
}
|
|
7473
|
-
function getScopedPaths(root) {
|
|
7474
|
-
return [
|
|
7475
|
-
{
|
|
7476
|
-
scope: "managed",
|
|
7477
|
-
dir: getManagedDir()
|
|
7478
|
-
},
|
|
7479
|
-
{
|
|
7480
|
-
scope: "project",
|
|
7481
|
-
dir: getProjectDir(root)
|
|
7482
|
-
},
|
|
7483
|
-
{
|
|
7484
|
-
scope: "local",
|
|
7485
|
-
dir: getLocalDir(root)
|
|
7486
|
-
},
|
|
7487
|
-
{
|
|
7488
|
-
scope: "user",
|
|
7489
|
-
dir: getUserDir()
|
|
7490
|
-
},
|
|
7491
|
-
{
|
|
7492
|
-
scope: "package",
|
|
7493
|
-
dir: getPackageDir()
|
|
7494
|
-
}
|
|
7495
|
-
];
|
|
7496
|
-
}
|
|
7497
|
-
function showGovernance(name, root, govFs, storeOpts) {
|
|
7498
|
-
const slashIdx = name.indexOf("/");
|
|
7499
|
-
if (slashIdx !== -1 && storeOpts) {
|
|
7500
|
-
const pluginName = name.slice(0, slashIdx);
|
|
7501
|
-
const assetName = name.slice(slashIdx + 1);
|
|
7502
|
-
for (const scope of [
|
|
7503
|
-
"managed",
|
|
7504
|
-
"project",
|
|
7505
|
-
"user"
|
|
7506
|
-
]) {
|
|
7507
|
-
let dir;
|
|
7508
|
-
if (scope === "managed") dir = getManagedDir();
|
|
7509
|
-
else if (scope === "project") dir = getProjectDir(root);
|
|
7510
|
-
else dir = getUserDir();
|
|
7511
|
-
const filePath = path.join(dir, pluginName, `${assetName}.md`);
|
|
7512
|
-
if (govFs.exists(filePath)) return {
|
|
7513
|
-
content: govFs.read(filePath),
|
|
7514
|
-
scope
|
|
7515
|
-
};
|
|
7516
|
-
}
|
|
7517
|
-
const entry = storeOpts.state.assets[pluginName];
|
|
7518
|
-
if (!entry) return null;
|
|
7519
|
-
const segment = path.join(entry.source, `${pluginName}@${entry.version}`);
|
|
7520
|
-
const storePath = path.join(storeOpts.globalStorePath, segment, "governances", `${assetName}.md`);
|
|
7521
|
-
if (govFs.exists(storePath)) return {
|
|
7522
|
-
content: govFs.read(storePath),
|
|
7523
|
-
scope: "store"
|
|
7524
|
-
};
|
|
7525
|
-
return null;
|
|
7526
|
-
}
|
|
7527
|
-
for (const { scope, dir } of getScopedPaths(root)) {
|
|
7528
|
-
const filePath = path.join(dir, `${name}.md`);
|
|
7529
|
-
if (govFs.exists(filePath)) return {
|
|
7530
|
-
content: govFs.read(filePath),
|
|
7531
|
-
scope
|
|
7532
|
-
};
|
|
7533
|
-
}
|
|
7534
|
-
return null;
|
|
7535
|
-
}
|
|
7536
|
-
function listGovernances(root, govFs) {
|
|
7537
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7538
|
-
const entries = [];
|
|
7539
|
-
for (const { scope, dir } of getScopedPaths(root)) for (const name of govFs.list(dir)) if (!seen.has(name)) {
|
|
7540
|
-
seen.add(name);
|
|
7541
|
-
entries.push({
|
|
7542
|
-
name,
|
|
7543
|
-
scope
|
|
7544
|
-
});
|
|
7545
|
-
}
|
|
7546
|
-
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7547
|
-
}
|
|
7548
|
-
//#endregion
|
|
7549
|
-
//#region src/governance/cli.ts
|
|
7550
|
-
function readGlobalState() {
|
|
7551
|
-
const p = path.join(os.homedir(), ".agents", "universal-plugin.json");
|
|
7552
|
-
try {
|
|
7553
|
-
return mergeSafeState(JSON.parse(fs.readFileSync(p, "utf8")));
|
|
7554
|
-
} catch {
|
|
7555
|
-
return emptyState();
|
|
7556
|
-
}
|
|
7557
|
-
}
|
|
7558
|
-
function governanceCommand() {
|
|
7559
|
-
const cmd = new Command("governance").description("Manage plugin governances").helpCommand(false);
|
|
7560
|
-
cmd.command("show <name>").description("Show a governance by name").option("--format <format>", "Output format: json or text (default: text)").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((name, opts) => {
|
|
7561
|
-
const result = showGovernance(name, resolveRoot(opts.root), realGovernanceFs, {
|
|
7562
|
-
state: readGlobalState(),
|
|
7563
|
-
globalStorePath: globalStorePath()
|
|
7564
|
-
});
|
|
7565
|
-
if (!result) {
|
|
7566
|
-
process.stderr.write(`Governance "${name}" not found\n`);
|
|
7567
|
-
process.exit(1);
|
|
7568
|
-
}
|
|
7569
|
-
outputText(result, () => {
|
|
7570
|
-
process.stdout.write(result.content);
|
|
7571
|
-
});
|
|
7572
|
-
});
|
|
7573
|
-
cmd.command("list").description("List available governances").option("--format <format>", "Output format: json or text (default: text)").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((opts) => {
|
|
7574
|
-
const entries = listGovernances(resolveRoot(opts.root), realGovernanceFs);
|
|
7575
|
-
output(entries, {
|
|
7576
|
-
governances: entries.map((e) => ({
|
|
7577
|
-
name: e.name,
|
|
7578
|
-
scope: e.scope
|
|
7579
|
-
})),
|
|
7580
|
-
summary: `${entries.length} governances across ${new Set(entries.map((e) => e.scope)).size} scopes`
|
|
7581
|
-
});
|
|
7582
|
-
});
|
|
7583
|
-
return cmd;
|
|
7365
|
+
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand$1(fs)).addCommand(getCommand(fs));
|
|
7584
7366
|
}
|
|
7585
7367
|
//#endregion
|
|
7586
7368
|
//#region src/init/fs.ts
|
|
@@ -7670,7 +7452,7 @@ const UP_NAMESPACE = "org.cyberuni.universal-plugin";
|
|
|
7670
7452
|
const SCAFFOLD_DIRS = [
|
|
7671
7453
|
"skills",
|
|
7672
7454
|
"agents",
|
|
7673
|
-
"
|
|
7455
|
+
"references",
|
|
7674
7456
|
"commands"
|
|
7675
7457
|
];
|
|
7676
7458
|
/** The closed canonical manifest init writes: `$schema` + `name`, plus the extensions namespace
|
|
@@ -7821,7 +7603,7 @@ function collect$1(value, previous) {
|
|
|
7821
7603
|
}
|
|
7822
7604
|
function initCommand$1(deps = { fs: realInitFs }) {
|
|
7823
7605
|
const cmd = new Command("init").description("Scaffold the canonical plugin.json (with --npm, wire an npm package to ship it)");
|
|
7824
|
-
cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/
|
|
7606
|
+
cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/ references/ commands/ directories").option("--force", "Overwrite an existing plugin.json").option("--yes", "Non-interactive (compatibility no-op; init never prompts)").option("--npm", "Wire package.json 'files' to ship the derived vendor manifests").option("--no-marketplace", "Skip the repository's local marketplace catalogs").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 init --name my-plugin --scaffold\n").action((opts) => {
|
|
7825
7607
|
try {
|
|
7826
7608
|
const root = resolveRoot(opts.root);
|
|
7827
7609
|
const plan = planInit(deps.fs.gather(root), {
|
|
@@ -7854,6 +7636,241 @@ function initCommand$1(deps = { fs: realInitFs }) {
|
|
|
7854
7636
|
return cmd;
|
|
7855
7637
|
}
|
|
7856
7638
|
//#endregion
|
|
7639
|
+
//#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.1.0/node_modules/@cyberuni/agent-harness/dist/index.js
|
|
7640
|
+
const harnessIds = [
|
|
7641
|
+
"claude-code",
|
|
7642
|
+
"cursor",
|
|
7643
|
+
"codex",
|
|
7644
|
+
"copilot-cli",
|
|
7645
|
+
"opencode",
|
|
7646
|
+
"kilo",
|
|
7647
|
+
"gemini-cli",
|
|
7648
|
+
"qwen-code",
|
|
7649
|
+
"vscode-copilot",
|
|
7650
|
+
"cline"
|
|
7651
|
+
];
|
|
7652
|
+
function resolveHarnessEnvironment(environment = {}) {
|
|
7653
|
+
return {
|
|
7654
|
+
env: environment.env ?? process.env,
|
|
7655
|
+
homedir: environment.homedir ?? homedir(),
|
|
7656
|
+
cwd: environment.cwd ?? process.cwd(),
|
|
7657
|
+
platform: environment.platform ?? process.platform
|
|
7658
|
+
};
|
|
7659
|
+
}
|
|
7660
|
+
/**
|
|
7661
|
+
* Where a harness keeps plugins at the user level. Paths are computed, not checked: a location may
|
|
7662
|
+
* not exist until the first plugin is installed.
|
|
7663
|
+
*/
|
|
7664
|
+
function pluginStorage(harness, environment = {}) {
|
|
7665
|
+
return storages[harness](resolveHarnessEnvironment(environment));
|
|
7666
|
+
}
|
|
7667
|
+
/** `$XDG_CONFIG_HOME`, else `~/.config`, as the `xdg-basedir` package resolves it. */
|
|
7668
|
+
const xdgConfig = ({ env, homedir }) => env.XDG_CONFIG_HOME || join(homedir, ".config");
|
|
7669
|
+
/** The agent plugin folder of the stable VS Code build, per OS (E-VSC-P3). */
|
|
7670
|
+
function vscodeAgentPlugins({ env, homedir, platform }) {
|
|
7671
|
+
if (platform === "win32") {
|
|
7672
|
+
const configDir = win32.join(env.APPDATA || win32.join(homedir, "AppData", "Roaming"), "Code");
|
|
7673
|
+
return {
|
|
7674
|
+
configDir,
|
|
7675
|
+
path: win32.join(configDir, "agentPlugins")
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7678
|
+
const configDir = platform === "darwin" ? join(homedir, "Library", "Application Support", "Code") : join(homedir, ".config", "Code");
|
|
7679
|
+
return {
|
|
7680
|
+
configDir,
|
|
7681
|
+
path: join(configDir, "agentPlugins")
|
|
7682
|
+
};
|
|
7683
|
+
}
|
|
7684
|
+
const storages = {
|
|
7685
|
+
"claude-code": ({ env, homedir }) => {
|
|
7686
|
+
const configDir = env.CLAUDE_CONFIG_DIR || join(homedir, ".claude");
|
|
7687
|
+
const plugins = join(configDir, "plugins");
|
|
7688
|
+
return {
|
|
7689
|
+
harness: "claude-code",
|
|
7690
|
+
configDir,
|
|
7691
|
+
locations: [
|
|
7692
|
+
{
|
|
7693
|
+
kind: "plugin-cache",
|
|
7694
|
+
path: join(plugins, "cache"),
|
|
7695
|
+
description: "Installed plugins as <marketplace>/<plugin>/<version>/",
|
|
7696
|
+
research: "E-CC-P1"
|
|
7697
|
+
},
|
|
7698
|
+
{
|
|
7699
|
+
kind: "marketplaces",
|
|
7700
|
+
path: join(plugins, "marketplaces"),
|
|
7701
|
+
description: "Marketplace checkouts",
|
|
7702
|
+
research: "E-CC-P1"
|
|
7703
|
+
},
|
|
7704
|
+
{
|
|
7705
|
+
kind: "install-record",
|
|
7706
|
+
path: join(plugins, "installed_plugins.json"),
|
|
7707
|
+
description: "Installs keyed by plugin@marketplace, each with scope, installPath, and version",
|
|
7708
|
+
research: "E-CC-P2"
|
|
7709
|
+
},
|
|
7710
|
+
{
|
|
7711
|
+
kind: "enabled-record",
|
|
7712
|
+
path: join(configDir, "settings.json"),
|
|
7713
|
+
description: "User settings; enabledPlugins maps plugin@marketplace to a boolean",
|
|
7714
|
+
research: "E-CC-P3"
|
|
7715
|
+
}
|
|
7716
|
+
]
|
|
7717
|
+
};
|
|
7718
|
+
},
|
|
7719
|
+
cursor: ({ homedir }) => {
|
|
7720
|
+
const configDir = join(homedir, ".cursor");
|
|
7721
|
+
return {
|
|
7722
|
+
harness: "cursor",
|
|
7723
|
+
configDir,
|
|
7724
|
+
locations: [{
|
|
7725
|
+
kind: "local-plugins",
|
|
7726
|
+
path: join(configDir, "plugins", "local"),
|
|
7727
|
+
description: "Plugins under development, one folder each",
|
|
7728
|
+
research: "E-CUR-P2"
|
|
7729
|
+
}, {
|
|
7730
|
+
kind: "marketplaces",
|
|
7731
|
+
path: join(configDir, "plugins", "marketplaces"),
|
|
7732
|
+
description: "Marketplace checkouts",
|
|
7733
|
+
research: "E-CUR-P3"
|
|
7734
|
+
}]
|
|
7735
|
+
};
|
|
7736
|
+
},
|
|
7737
|
+
codex: ({ env, homedir }) => {
|
|
7738
|
+
const configDir = env.CODEX_HOME || join(homedir, ".codex");
|
|
7739
|
+
return {
|
|
7740
|
+
harness: "codex",
|
|
7741
|
+
configDir,
|
|
7742
|
+
locations: [{
|
|
7743
|
+
kind: "plugin-cache",
|
|
7744
|
+
path: join(configDir, "plugins", "cache"),
|
|
7745
|
+
description: "Installed plugins as <marketplace>/<plugin>/<version>/",
|
|
7746
|
+
research: "E-CODEX-P3"
|
|
7747
|
+
}, {
|
|
7748
|
+
kind: "enabled-record",
|
|
7749
|
+
path: join(configDir, "config.toml"),
|
|
7750
|
+
description: "User config; [plugins.\"<plugin>@<marketplace>\"] tables carry enabled = <bool>",
|
|
7751
|
+
research: "E-CODEX-P2"
|
|
7752
|
+
}]
|
|
7753
|
+
};
|
|
7754
|
+
},
|
|
7755
|
+
"copilot-cli": ({ env, homedir }) => {
|
|
7756
|
+
const configDir = env.COPILOT_HOME || join(homedir, ".copilot");
|
|
7757
|
+
return {
|
|
7758
|
+
harness: "copilot-cli",
|
|
7759
|
+
configDir,
|
|
7760
|
+
locations: [
|
|
7761
|
+
{
|
|
7762
|
+
kind: "installed-plugins",
|
|
7763
|
+
path: join(configDir, "installed-plugins"),
|
|
7764
|
+
description: "Installed plugins as <marketplace>/<plugin>/, or _direct/<source-id>/ for direct installs",
|
|
7765
|
+
research: "E-COPILOT-P9"
|
|
7766
|
+
},
|
|
7767
|
+
{
|
|
7768
|
+
kind: "install-record",
|
|
7769
|
+
path: join(configDir, "config.json"),
|
|
7770
|
+
description: "Application state, including installedPlugins",
|
|
7771
|
+
research: "E-COPILOT-P9"
|
|
7772
|
+
},
|
|
7773
|
+
{
|
|
7774
|
+
kind: "enabled-record",
|
|
7775
|
+
path: join(configDir, "settings.json"),
|
|
7776
|
+
description: "User settings (JSON with comments); enabledPlugins maps plugin specs to a boolean",
|
|
7777
|
+
research: "E-COPILOT-P4"
|
|
7778
|
+
}
|
|
7779
|
+
]
|
|
7780
|
+
};
|
|
7781
|
+
},
|
|
7782
|
+
opencode: (environment) => {
|
|
7783
|
+
const configDir = join(xdgConfig(environment), "opencode");
|
|
7784
|
+
return {
|
|
7785
|
+
harness: "opencode",
|
|
7786
|
+
configDir,
|
|
7787
|
+
locations: [{
|
|
7788
|
+
kind: "local-plugins",
|
|
7789
|
+
path: join(configDir, "plugins"),
|
|
7790
|
+
description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
|
|
7791
|
+
research: "E-OC-P2"
|
|
7792
|
+
}]
|
|
7793
|
+
};
|
|
7794
|
+
},
|
|
7795
|
+
kilo: (environment) => {
|
|
7796
|
+
const configDir = join(xdgConfig(environment), "kilo");
|
|
7797
|
+
return {
|
|
7798
|
+
harness: "kilo",
|
|
7799
|
+
configDir,
|
|
7800
|
+
locations: [{
|
|
7801
|
+
kind: "local-plugins",
|
|
7802
|
+
path: join(configDir, "plugin"),
|
|
7803
|
+
description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
|
|
7804
|
+
research: "E-KILO-P2"
|
|
7805
|
+
}]
|
|
7806
|
+
};
|
|
7807
|
+
},
|
|
7808
|
+
"gemini-cli": ({ env, homedir }) => {
|
|
7809
|
+
const configDir = join(env.GEMINI_CLI_HOME || homedir, ".gemini");
|
|
7810
|
+
const extensions = join(configDir, "extensions");
|
|
7811
|
+
return {
|
|
7812
|
+
harness: "gemini-cli",
|
|
7813
|
+
configDir,
|
|
7814
|
+
locations: [{
|
|
7815
|
+
kind: "installed-plugins",
|
|
7816
|
+
path: extensions,
|
|
7817
|
+
description: "Installed extensions, one folder each with a gemini-extension.json manifest",
|
|
7818
|
+
research: "E-GEM-P1"
|
|
7819
|
+
}, {
|
|
7820
|
+
kind: "enabled-record",
|
|
7821
|
+
path: join(extensions, "extension-enablement.json"),
|
|
7822
|
+
description: "Per-extension path-glob overrides that enable or disable it by folder, not a boolean",
|
|
7823
|
+
research: "E-GEM-P3"
|
|
7824
|
+
}]
|
|
7825
|
+
};
|
|
7826
|
+
},
|
|
7827
|
+
"qwen-code": ({ env, homedir }) => {
|
|
7828
|
+
const configDir = env.QWEN_HOME || join(homedir, ".qwen");
|
|
7829
|
+
const extensions = join(configDir, "extensions");
|
|
7830
|
+
return {
|
|
7831
|
+
harness: "qwen-code",
|
|
7832
|
+
configDir,
|
|
7833
|
+
locations: [{
|
|
7834
|
+
kind: "installed-plugins",
|
|
7835
|
+
path: extensions,
|
|
7836
|
+
description: "Installed extensions, one folder each with a qwen-extension.json manifest",
|
|
7837
|
+
research: "E-QWEN-P1"
|
|
7838
|
+
}, {
|
|
7839
|
+
kind: "enabled-record",
|
|
7840
|
+
path: join(extensions, "extension-enablement.json"),
|
|
7841
|
+
description: "Extension enable/disable state, inherited from Gemini CLI's format",
|
|
7842
|
+
research: "E-QWEN-P2"
|
|
7843
|
+
}]
|
|
7844
|
+
};
|
|
7845
|
+
},
|
|
7846
|
+
"vscode-copilot": (environment) => {
|
|
7847
|
+
const { configDir, path } = vscodeAgentPlugins(environment);
|
|
7848
|
+
return {
|
|
7849
|
+
harness: "vscode-copilot",
|
|
7850
|
+
configDir,
|
|
7851
|
+
locations: [{
|
|
7852
|
+
kind: "installed-plugins",
|
|
7853
|
+
path,
|
|
7854
|
+
description: "Installed agent plugins as <host>/<org>/<repo>/",
|
|
7855
|
+
research: "E-VSC-P3"
|
|
7856
|
+
}]
|
|
7857
|
+
};
|
|
7858
|
+
},
|
|
7859
|
+
cline: ({ homedir }) => {
|
|
7860
|
+
const configDir = join(homedir, ".cline");
|
|
7861
|
+
return {
|
|
7862
|
+
harness: "cline",
|
|
7863
|
+
configDir,
|
|
7864
|
+
locations: [{
|
|
7865
|
+
kind: "installed-plugins",
|
|
7866
|
+
path: join(configDir, "plugins", "_installed"),
|
|
7867
|
+
description: "Installed plugins, grouped by source: npm, git, remote, or local",
|
|
7868
|
+
research: "E-CLINE-P3"
|
|
7869
|
+
}]
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
};
|
|
7873
|
+
//#endregion
|
|
7857
7874
|
//#region src/vendor-registry/vendor-registry.ts
|
|
7858
7875
|
function lookupVendor(registry, vendorId) {
|
|
7859
7876
|
return registry[vendorId] ?? null;
|
|
@@ -7866,6 +7883,15 @@ function mergeRegistries(base, override) {
|
|
|
7866
7883
|
};
|
|
7867
7884
|
return result;
|
|
7868
7885
|
}
|
|
7886
|
+
/** Fills each vendor's `localPluginDir` from `locate`, which returns the harness's `local-plugins`
|
|
7887
|
+
* folder when `@cyberuni/agent-harness` knows one. A vendor it knows nothing about keeps its
|
|
7888
|
+
* registry value, or `null`. */
|
|
7889
|
+
function withLocalPluginDirs(registry, locate) {
|
|
7890
|
+
return Object.fromEntries(Object.entries(registry).map(([id, config]) => [id, {
|
|
7891
|
+
...config,
|
|
7892
|
+
localPluginDir: locate(id) ?? config.localPluginDir ?? null
|
|
7893
|
+
}]));
|
|
7894
|
+
}
|
|
7869
7895
|
//#endregion
|
|
7870
7896
|
//#region src/vendor-registry/fs.ts
|
|
7871
7897
|
function bundledRegistryPath() {
|
|
@@ -7875,8 +7901,15 @@ function bundledRegistryPath() {
|
|
|
7875
7901
|
function userOverridePath() {
|
|
7876
7902
|
return path.join(os.homedir(), ".agents", "universal-plugin-vendors.json");
|
|
7877
7903
|
}
|
|
7904
|
+
/** The harness's `local-plugins` folder, as `@cyberuni/agent-harness` locates it. */
|
|
7905
|
+
function localPluginsDir(vendorId) {
|
|
7906
|
+
if (!harnessIds.includes(vendorId)) return void 0;
|
|
7907
|
+
return pluginStorage(vendorId).locations.find((l) => l.kind === "local-plugins")?.path;
|
|
7908
|
+
}
|
|
7909
|
+
/** The bundled registry, with the per-harness facts `@cyberuni/agent-harness` owns filled in, then
|
|
7910
|
+
* the user's override on top. */
|
|
7878
7911
|
function loadRegistry() {
|
|
7879
|
-
const bundled = JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8"));
|
|
7912
|
+
const bundled = withLocalPluginDirs(JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8")), localPluginsDir);
|
|
7880
7913
|
try {
|
|
7881
7914
|
return mergeRegistries(bundled, JSON.parse(fs.readFileSync(userOverridePath(), "utf8")));
|
|
7882
7915
|
} catch (err) {
|
|
@@ -8269,6 +8302,9 @@ function manifestOwner(manifest) {
|
|
|
8269
8302
|
return owner;
|
|
8270
8303
|
}
|
|
8271
8304
|
}
|
|
8305
|
+
/** The catalog's own identity: its name and its owner, from the root manifest unless overridden.
|
|
8306
|
+
* Exported because `marketplace add` creates a catalog in a repository that has no plugins of its
|
|
8307
|
+
* own to discover, and the two commands must name that catalog the same way. */
|
|
8272
8308
|
function deriveMetadata(root, fs, opts) {
|
|
8273
8309
|
const rootManifest = path.join(root, "plugin.json");
|
|
8274
8310
|
if (fs.exists(rootManifest)) assertContained(root, rootManifest, fs, "root plugin.json");
|
|
@@ -8329,7 +8365,7 @@ function writeArtifacts(fs, artifacts, root) {
|
|
|
8329
8365
|
const changed = artifacts.filter((artifact) => !sameArtifact(fs, path.join(root, artifact.path), artifact.content));
|
|
8330
8366
|
for (const artifact of changed) fs.writeAtomically(path.join(root, artifact.path), artifact.content);
|
|
8331
8367
|
}
|
|
8332
|
-
function selectedTargets(targets) {
|
|
8368
|
+
function selectedTargets$1(targets) {
|
|
8333
8369
|
return targets && targets.length > 0 ? [...new Set(targets)] : [
|
|
8334
8370
|
"claude",
|
|
8335
8371
|
"codex",
|
|
@@ -8337,6 +8373,9 @@ function selectedTargets(targets) {
|
|
|
8337
8373
|
"cursor"
|
|
8338
8374
|
];
|
|
8339
8375
|
}
|
|
8376
|
+
function readCatalog(fs, file) {
|
|
8377
|
+
return fs.exists(file) ? fs.read(file) : void 0;
|
|
8378
|
+
}
|
|
8340
8379
|
function sameArtifact(fs, file, content) {
|
|
8341
8380
|
if (!fs.exists(file)) return false;
|
|
8342
8381
|
const existing = fs.read(file);
|
|
@@ -8358,12 +8397,16 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
8358
8397
|
const root = path.resolve(rootInput);
|
|
8359
8398
|
const metadata = deriveMetadata(root, fs, opts);
|
|
8360
8399
|
const plugins = discoverPlugins(root, fs, opts.scanDirs);
|
|
8361
|
-
const
|
|
8400
|
+
const derived = selectedTargets$1(opts.targets).map((target) => ({
|
|
8362
8401
|
target,
|
|
8363
8402
|
artifacts: serializeTarget(target, metadata, plugins)
|
|
8364
8403
|
}));
|
|
8404
|
+
for (const { artifacts } of derived) for (const artifact of artifacts) assertContained(root, path.join(root, artifact.path), fs, "selected artifact");
|
|
8405
|
+
const planned = derived.map(({ target, artifacts }) => ({
|
|
8406
|
+
target,
|
|
8407
|
+
artifacts: artifacts.map((artifact) => mergeDiscoveredCatalog(artifact, readCatalog(fs, path.join(root, artifact.path))))
|
|
8408
|
+
}));
|
|
8365
8409
|
for (const { target, artifacts } of planned) for (const artifact of artifacts) {
|
|
8366
|
-
assertContained(root, path.join(root, artifact.path), fs, "selected artifact");
|
|
8367
8410
|
const issues = validateCatalogContent(target, artifact.content);
|
|
8368
8411
|
if (issues.length > 0) throw new Error(formatCatalogIssues(artifact.path, issues));
|
|
8369
8412
|
}
|
|
@@ -8386,13 +8429,403 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
8386
8429
|
target,
|
|
8387
8430
|
status: opts.dryRun ? "planned" : unchanged ? "unchanged" : "generated",
|
|
8388
8431
|
paths: artifacts.map((artifact) => artifact.path),
|
|
8389
|
-
plugins:
|
|
8432
|
+
plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))]
|
|
8390
8433
|
};
|
|
8391
8434
|
});
|
|
8392
8435
|
if (!opts.dryRun && plugins.length > 0) writeArtifacts(fs, planned.flatMap((entry) => entry.artifacts), root);
|
|
8393
8436
|
return results;
|
|
8394
8437
|
}
|
|
8395
8438
|
//#endregion
|
|
8439
|
+
//#region src/marketplace/spec.ts
|
|
8440
|
+
/** What a user names on the command line when they ask for a plugin to be listed, reduced to the
|
|
8441
|
+
* source a catalog entry states. Pure: every rule here is a decision about the string itself, so
|
|
8442
|
+
* nothing consults the filesystem, the network, or the runtime's configuration. */
|
|
8443
|
+
const GITHUB_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8444
|
+
const NPM_PACKAGE = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8445
|
+
/** The last meaningful segment of a path or URL, with a `.git` suffix removed. */
|
|
8446
|
+
function basename(value) {
|
|
8447
|
+
const segments = value.replace(/[/\\]+$/, "").split(/[/\\]/);
|
|
8448
|
+
return (segments[segments.length - 1] ?? value).replace(/\.git$/, "");
|
|
8449
|
+
}
|
|
8450
|
+
/** An npm package's entry name is the package name without its scope: `@cyberuni/upx` lists as
|
|
8451
|
+
* `upx`, which is what a user installs and what every other source form yields. */
|
|
8452
|
+
function packageEntryName(pkg) {
|
|
8453
|
+
return pkg.startsWith("@") ? pkg.split("/")[1] ?? pkg : pkg;
|
|
8454
|
+
}
|
|
8455
|
+
function pathSpec(value) {
|
|
8456
|
+
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
8457
|
+
const source = normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
|
|
8458
|
+
return {
|
|
8459
|
+
kind: "path",
|
|
8460
|
+
name: basename(normalized),
|
|
8461
|
+
source
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
function npmSpec(pkg) {
|
|
8465
|
+
return {
|
|
8466
|
+
kind: "npm",
|
|
8467
|
+
name: packageEntryName(pkg),
|
|
8468
|
+
source: {
|
|
8469
|
+
source: "npm",
|
|
8470
|
+
package: pkg
|
|
8471
|
+
}
|
|
8472
|
+
};
|
|
8473
|
+
}
|
|
8474
|
+
function githubSpec(repo) {
|
|
8475
|
+
return {
|
|
8476
|
+
kind: "github",
|
|
8477
|
+
name: basename(repo),
|
|
8478
|
+
source: {
|
|
8479
|
+
source: "github",
|
|
8480
|
+
repo
|
|
8481
|
+
}
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
function urlSpec(url) {
|
|
8485
|
+
return {
|
|
8486
|
+
kind: "url",
|
|
8487
|
+
name: basename(url),
|
|
8488
|
+
source: {
|
|
8489
|
+
source: "url",
|
|
8490
|
+
url
|
|
8491
|
+
}
|
|
8492
|
+
};
|
|
8493
|
+
}
|
|
8494
|
+
/** Splits `<plugin>@<marketplace>` at the separator that is not a scope marker, so a scoped package
|
|
8495
|
+
* name keeps its leading `@`. */
|
|
8496
|
+
function splitMarketplace(value) {
|
|
8497
|
+
const at = value.indexOf("@", value.startsWith("@") ? 1 : 0);
|
|
8498
|
+
if (at <= 0) return void 0;
|
|
8499
|
+
const plugin = value.slice(0, at);
|
|
8500
|
+
const marketplace = value.slice(at + 1);
|
|
8501
|
+
if (plugin === "" || marketplace === "" || marketplace.includes("/")) return void 0;
|
|
8502
|
+
return {
|
|
8503
|
+
plugin,
|
|
8504
|
+
marketplace
|
|
8505
|
+
};
|
|
8506
|
+
}
|
|
8507
|
+
function marketplaceSpec(value) {
|
|
8508
|
+
const split = splitMarketplace(value);
|
|
8509
|
+
if (!split) throw new Error(`error: "${value}" is not <plugin>@<marketplace>`);
|
|
8510
|
+
return {
|
|
8511
|
+
kind: "marketplace",
|
|
8512
|
+
name: packageEntryName(split.plugin),
|
|
8513
|
+
...split
|
|
8514
|
+
};
|
|
8515
|
+
}
|
|
8516
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
8517
|
+
/** A subdirectory as `git-subdir` states it: no leading `./`, no trailing slash. */
|
|
8518
|
+
function normalizeSubdir(subdir) {
|
|
8519
|
+
const path = subdir.trim().replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/+$/, "");
|
|
8520
|
+
if (path === "") throw new Error("error: --subdir must name a directory inside the repository");
|
|
8521
|
+
if (path.startsWith("/")) throw new Error(`error: --subdir "${subdir}" must be relative to the repository root`);
|
|
8522
|
+
return path;
|
|
8523
|
+
}
|
|
8524
|
+
/** The git URL a spec's source points at, for the two kinds that name a whole repository. */
|
|
8525
|
+
function repositoryUrl(spec) {
|
|
8526
|
+
if (typeof spec.source !== "object") return void 0;
|
|
8527
|
+
if (spec.source.source === "github") return originFromRepo(spec.source.repo).url;
|
|
8528
|
+
if (spec.source.source === "url") return spec.source.url;
|
|
8529
|
+
}
|
|
8530
|
+
/** Narrows a whole-repository source to the one directory inside it that is the plugin.
|
|
8531
|
+
*
|
|
8532
|
+
* A monorepo publishes several plugins from one repository, and neither `github` nor `url` can say
|
|
8533
|
+
* which directory. `git-subdir` is the form that can, so `--subdir` turns the source into one —
|
|
8534
|
+
* and the entry takes its name from that directory rather than from the repository. */
|
|
8535
|
+
function withSubdir(spec, subdir) {
|
|
8536
|
+
const url = repositoryUrl(spec);
|
|
8537
|
+
if (url === void 0) throw new Error(`error: --subdir applies to a repository source, not to a ${spec.kind} one`);
|
|
8538
|
+
const path = normalizeSubdir(subdir);
|
|
8539
|
+
return {
|
|
8540
|
+
kind: spec.kind,
|
|
8541
|
+
name: basename(path),
|
|
8542
|
+
source: {
|
|
8543
|
+
source: "git-subdir",
|
|
8544
|
+
url,
|
|
8545
|
+
path
|
|
8546
|
+
}
|
|
8547
|
+
};
|
|
8548
|
+
}
|
|
8549
|
+
/** Pins a source to a branch, tag, or commit. Only the git-backed forms carry one; an npm package is
|
|
8550
|
+
* pinned by version and a path is whatever is on disk. */
|
|
8551
|
+
function withPin(spec, ref, sha) {
|
|
8552
|
+
if (ref === void 0 && sha === void 0) return spec;
|
|
8553
|
+
const source = spec.source;
|
|
8554
|
+
if (typeof source !== "object" || ![
|
|
8555
|
+
"url",
|
|
8556
|
+
"github",
|
|
8557
|
+
"git-subdir"
|
|
8558
|
+
].includes(source.source)) throw new Error(`error: --ref and --sha apply to a git source, not to a ${spec.kind} one`);
|
|
8559
|
+
if (sha !== void 0 && !SHA.test(sha)) throw new Error(`error: --sha "${sha}" must be a full 40-character commit hash`);
|
|
8560
|
+
return {
|
|
8561
|
+
...spec,
|
|
8562
|
+
source: {
|
|
8563
|
+
...source,
|
|
8564
|
+
...ref === void 0 ? {} : { ref },
|
|
8565
|
+
...sha === void 0 ? {} : { sha }
|
|
8566
|
+
}
|
|
8567
|
+
};
|
|
8568
|
+
}
|
|
8569
|
+
function looksLikeUrl(value) {
|
|
8570
|
+
return value.includes("://") || value.startsWith("git@");
|
|
8571
|
+
}
|
|
8572
|
+
function looksLikePath(value) {
|
|
8573
|
+
return value.startsWith("./") || value.startsWith("../") || value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
8574
|
+
}
|
|
8575
|
+
/** Applies the kind the user named, so every guess below has an override. */
|
|
8576
|
+
function explicitSpec(value, kind) {
|
|
8577
|
+
switch (kind) {
|
|
8578
|
+
case "path": return pathSpec(value);
|
|
8579
|
+
case "npm": return npmSpec(value.replace(/^npm:/, ""));
|
|
8580
|
+
case "github": return githubSpec(value);
|
|
8581
|
+
case "url": return urlSpec(value);
|
|
8582
|
+
case "marketplace": return marketplaceSpec(value);
|
|
8583
|
+
}
|
|
8584
|
+
}
|
|
8585
|
+
/** Reads a spec string as the source it names.
|
|
8586
|
+
*
|
|
8587
|
+
* The order below is the disambiguation, and every step of it is overridable with an explicit kind:
|
|
8588
|
+
*
|
|
8589
|
+
* 1. an `npm:` prefix, which is how a bare name that looks like anything else is forced to a package
|
|
8590
|
+
* 2. a URL scheme or an `scp`-style `git@host:path`
|
|
8591
|
+
* 3. a path, which has to say so with `./`, `../`, or a leading slash
|
|
8592
|
+
* 4. `<plugin>@<marketplace>`, splitting after a leading scope `@` so `@scope/pkg` is not one
|
|
8593
|
+
* 5. `owner/repo`
|
|
8594
|
+
* 6. anything else left, which is an npm package name
|
|
8595
|
+
*
|
|
8596
|
+
* Step 3 is why `plugins/alpha` reads as a GitHub repository rather than a directory: both are
|
|
8597
|
+
* `a/b`, and a guess that reached for the filesystem would answer differently depending on where
|
|
8598
|
+
* the command was run. `./plugins/alpha` or `--path` says it plainly. */
|
|
8599
|
+
function parsePluginSpec(value, opts = {}) {
|
|
8600
|
+
const { kind, subdir, ref, sha } = opts;
|
|
8601
|
+
const spec = value.trim();
|
|
8602
|
+
if (spec === "") throw new Error("error: a plugin spec is required");
|
|
8603
|
+
const shape = (base) => withPin(subdir === void 0 ? base : withSubdir(base, subdir), ref, sha);
|
|
8604
|
+
if (kind) return shape(explicitSpec(spec, kind));
|
|
8605
|
+
if (spec.startsWith("npm:")) {
|
|
8606
|
+
const pkg = spec.slice(4);
|
|
8607
|
+
if (!NPM_PACKAGE.test(pkg)) throw new Error(`error: "${pkg}" is not an npm package name`);
|
|
8608
|
+
return shape(npmSpec(pkg));
|
|
8609
|
+
}
|
|
8610
|
+
if (looksLikeUrl(spec)) return shape(urlSpec(spec));
|
|
8611
|
+
if (looksLikePath(spec)) return shape(pathSpec(spec));
|
|
8612
|
+
const split = splitMarketplace(spec);
|
|
8613
|
+
if (split) return shape({
|
|
8614
|
+
kind: "marketplace",
|
|
8615
|
+
name: packageEntryName(split.plugin),
|
|
8616
|
+
...split
|
|
8617
|
+
});
|
|
8618
|
+
if (GITHUB_REPO.test(spec)) return shape(githubSpec(spec));
|
|
8619
|
+
if (NPM_PACKAGE.test(spec)) return shape(npmSpec(spec));
|
|
8620
|
+
throw new Error(`error: cannot tell what "${spec}" names; pass --path, --npm, --github, --url, or --from-marketplace`);
|
|
8621
|
+
}
|
|
8622
|
+
//#endregion
|
|
8623
|
+
//#region src/marketplace/add.ts
|
|
8624
|
+
/** The manifest fields a catalog entry carries. Exactly the set the catalog already derives from a
|
|
8625
|
+
* discovered plugin, so an added entry and a discovered one say the same kinds of things. */
|
|
8626
|
+
const ENTRY_METADATA_FIELDS = [
|
|
8627
|
+
"description",
|
|
8628
|
+
"version",
|
|
8629
|
+
"homepage",
|
|
8630
|
+
"repository",
|
|
8631
|
+
"license",
|
|
8632
|
+
"keywords"
|
|
8633
|
+
];
|
|
8634
|
+
/** The kind name used to decide whether a runtime accepts a source, from the source itself. */
|
|
8635
|
+
function sourceKindOf(source) {
|
|
8636
|
+
if (isLocalCatalogSource(source)) return "path";
|
|
8637
|
+
return typeof source === "string" ? "path" : source.source;
|
|
8638
|
+
}
|
|
8639
|
+
/** A source rendered for the result table — one short cell, not a JSON blob. */
|
|
8640
|
+
function describeSource(source) {
|
|
8641
|
+
if (typeof source === "string") return source;
|
|
8642
|
+
const detail = source.package ?? source.repo ?? source.url ?? source.path;
|
|
8643
|
+
return typeof detail === "string" ? `${source.source}:${detail}` : String(source.source);
|
|
8644
|
+
}
|
|
8645
|
+
function readJsonFile(fs, file) {
|
|
8646
|
+
if (!fs.exists(file)) return void 0;
|
|
8647
|
+
try {
|
|
8648
|
+
const parsed = JSON.parse(fs.read(file));
|
|
8649
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
8650
|
+
} catch {
|
|
8651
|
+
return;
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
/** Metadata already on this machine for the plugin being listed.
|
|
8655
|
+
*
|
|
8656
|
+
* A local path carries its own manifest, and an npm package that happens to be installed carries a
|
|
8657
|
+
* `package.json`. Neither is fetched: what is here is read, what is not here is left to the
|
|
8658
|
+
* metadata flags. An entry missing an optional field still installs. */
|
|
8659
|
+
function localMetadata(root, spec, fs) {
|
|
8660
|
+
if (spec.kind === "path" && typeof spec.source === "string") return readJsonFile(fs, path.join(root, spec.source, "plugin.json")) ?? {};
|
|
8661
|
+
if (spec.kind === "npm" && typeof spec.source === "object") {
|
|
8662
|
+
const pkg = spec.source.package;
|
|
8663
|
+
if (typeof pkg !== "string") return {};
|
|
8664
|
+
return readJsonFile(fs, path.join(root, "node_modules", pkg, "package.json")) ?? {};
|
|
8665
|
+
}
|
|
8666
|
+
return {};
|
|
8667
|
+
}
|
|
8668
|
+
/** The entry another marketplace already publishes for this plugin, copied rather than invented.
|
|
8669
|
+
*
|
|
8670
|
+
* The catalog schema has no "from another marketplace" source, so there is nothing to write until
|
|
8671
|
+
* that marketplace has been read. Its entry already names a source every runtime can resolve — that
|
|
8672
|
+
* is what makes it publishable — so the whole entry comes across. */
|
|
8673
|
+
function resolveFromMarketplace(spec, opts, fs) {
|
|
8674
|
+
const marketplace = spec.marketplace;
|
|
8675
|
+
const known = opts.from === void 0 ? resolveKnownMarketplace(marketplace, fs) : { dir: opts.from };
|
|
8676
|
+
if (known === void 0) throw new Error(`error: marketplace "${marketplace}" is not installed; add it in the runtime first, or pass --from <path>`);
|
|
8677
|
+
const dir = known.dir;
|
|
8678
|
+
const content = readMarketplaceCatalog(dir, fs);
|
|
8679
|
+
if (content === void 0) throw new Error(`error: no marketplace.json found under "${dir}"`);
|
|
8680
|
+
let catalog;
|
|
8681
|
+
try {
|
|
8682
|
+
catalog = JSON.parse(content);
|
|
8683
|
+
} catch {
|
|
8684
|
+
throw new Error(`error: the catalog in "${dir}" is not valid JSON`);
|
|
8685
|
+
}
|
|
8686
|
+
const found = (typeof catalog === "object" && catalog !== null && Array.isArray(catalog.plugins) ? catalog.plugins : []).find((entry) => typeof entry === "object" && entry !== null && entry.name === spec.plugin);
|
|
8687
|
+
if (!found) throw new Error(`error: marketplace "${marketplace}" lists no plugin "${spec.plugin}"`);
|
|
8688
|
+
const source = found.source;
|
|
8689
|
+
if (source === void 0) throw new Error(`error: the "${spec.plugin}" entry in "${marketplace}" names no source`);
|
|
8690
|
+
if (!isLocalCatalogSource(source)) return {
|
|
8691
|
+
source,
|
|
8692
|
+
metadata: found
|
|
8693
|
+
};
|
|
8694
|
+
const remote = gitRemoteUrl(dir);
|
|
8695
|
+
const origin = known.origin ?? (remote === void 0 ? void 0 : originFromUrl(remote));
|
|
8696
|
+
if (origin === void 0) throw new Error(`error: "${spec.plugin}" is local to marketplace "${marketplace}", and that marketplace has no remote to rewrite its source against`);
|
|
8697
|
+
const rewritten = absoluteSource(origin, source);
|
|
8698
|
+
if (rewritten === void 0) throw new Error(`error: the "${spec.plugin}" entry in "${marketplace}" names a source this command cannot read`);
|
|
8699
|
+
return {
|
|
8700
|
+
source: rewritten,
|
|
8701
|
+
metadata: found
|
|
8702
|
+
};
|
|
8703
|
+
}
|
|
8704
|
+
function selectedTargets(targets) {
|
|
8705
|
+
return targets && targets.length > 0 ? [...new Set(targets)] : [
|
|
8706
|
+
"claude",
|
|
8707
|
+
"codex",
|
|
8708
|
+
"copilot",
|
|
8709
|
+
"cursor"
|
|
8710
|
+
];
|
|
8711
|
+
}
|
|
8712
|
+
function entryMetadata(discovered, supplied = {}) {
|
|
8713
|
+
const metadata = {};
|
|
8714
|
+
for (const field of ENTRY_METADATA_FIELDS) {
|
|
8715
|
+
const value = supplied[field] ?? discovered[field];
|
|
8716
|
+
if (value !== void 0) metadata[field] = value;
|
|
8717
|
+
}
|
|
8718
|
+
return metadata;
|
|
8719
|
+
}
|
|
8720
|
+
function catalogEntry(content, name) {
|
|
8721
|
+
try {
|
|
8722
|
+
const parsed = JSON.parse(content);
|
|
8723
|
+
if (!Array.isArray(parsed.plugins)) return void 0;
|
|
8724
|
+
return parsed.plugins.find((entry) => typeof entry === "object" && entry !== null && entry.name === name);
|
|
8725
|
+
} catch {
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
/** Lists a plugin that lives somewhere else in this repository's catalogs.
|
|
8730
|
+
*
|
|
8731
|
+
* Where `marketplace init` derives a catalog from the plugins a repository holds, this adds one it
|
|
8732
|
+
* does not: an npm package, a GitHub repository, an entry another marketplace already publishes.
|
|
8733
|
+
* Together they let one repository be both — a plugin's own home and a curated list.
|
|
8734
|
+
*
|
|
8735
|
+
* Nothing is fetched and nothing is published. The command reads this repository, and at most a
|
|
8736
|
+
* marketplace already installed on this machine, and writes catalog files. */
|
|
8737
|
+
function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
|
|
8738
|
+
const root = path.resolve(rootInput);
|
|
8739
|
+
const parsed = parsePluginSpec(spec, {
|
|
8740
|
+
kind: opts.kind,
|
|
8741
|
+
subdir: opts.subdir,
|
|
8742
|
+
ref: opts.ref,
|
|
8743
|
+
sha: opts.sha
|
|
8744
|
+
});
|
|
8745
|
+
const name = opts.name ?? parsed.name;
|
|
8746
|
+
assertMarketplaceName(name, "plugin name");
|
|
8747
|
+
const resolved = parsed.kind === "marketplace" ? resolveFromMarketplace(parsed, opts, fs) : {
|
|
8748
|
+
source: parsed.source,
|
|
8749
|
+
metadata: localMetadata(root, parsed, fs)
|
|
8750
|
+
};
|
|
8751
|
+
const metadata = deriveMetadata(root, fs, {
|
|
8752
|
+
name: opts.marketplaceName,
|
|
8753
|
+
owner: opts.owner
|
|
8754
|
+
});
|
|
8755
|
+
const plugin = {
|
|
8756
|
+
name,
|
|
8757
|
+
source: resolved.source,
|
|
8758
|
+
metadata: entryMetadata(resolved.metadata, opts.metadata)
|
|
8759
|
+
};
|
|
8760
|
+
const kind = sourceKindOf(resolved.source);
|
|
8761
|
+
const rendered = describeSource(resolved.source);
|
|
8762
|
+
const planned = selectedTargets(opts.targets).map((target) => {
|
|
8763
|
+
const catalogPath = TARGET_CATALOG_PATHS[target];
|
|
8764
|
+
const file = path.join(root, catalogPath);
|
|
8765
|
+
const existing = fs.exists(file) ? fs.read(file) : void 0;
|
|
8766
|
+
if (!TARGET_SOURCE_KINDS[target].includes(kind)) return {
|
|
8767
|
+
target,
|
|
8768
|
+
catalogPath,
|
|
8769
|
+
file,
|
|
8770
|
+
existing,
|
|
8771
|
+
skipped: `${kind} source is not supported by ${target}`,
|
|
8772
|
+
artifact: void 0
|
|
8773
|
+
};
|
|
8774
|
+
return {
|
|
8775
|
+
target,
|
|
8776
|
+
catalogPath,
|
|
8777
|
+
file,
|
|
8778
|
+
existing,
|
|
8779
|
+
skipped: void 0,
|
|
8780
|
+
artifact: mergeCatalogEntry(target, metadata, plugin, () => existing, { keepForeignSource: false })
|
|
8781
|
+
};
|
|
8782
|
+
});
|
|
8783
|
+
for (const entry of planned) {
|
|
8784
|
+
if (!entry.artifact) continue;
|
|
8785
|
+
const issues = validateCatalogContent(entry.target, entry.artifact.content);
|
|
8786
|
+
if (issues.length > 0) throw new Error(formatCatalogIssues(entry.catalogPath, issues));
|
|
8787
|
+
}
|
|
8788
|
+
const conflicts = planned.filter((entry) => {
|
|
8789
|
+
if (!entry.artifact || entry.existing === void 0 || opts.force) return false;
|
|
8790
|
+
if (catalogEntry(entry.existing, name) === void 0) return false;
|
|
8791
|
+
return !sameCatalogContent(entry.existing, entry.artifact.content);
|
|
8792
|
+
}).map((entry) => entry.catalogPath);
|
|
8793
|
+
if (conflicts.length > 0) throw new Error(`error: "${name}" is already listed differently in ${conflicts.join(", ")}; rerun with --force to replace it`);
|
|
8794
|
+
const results = planned.map((entry) => {
|
|
8795
|
+
const row = {
|
|
8796
|
+
target: entry.target,
|
|
8797
|
+
entry: name,
|
|
8798
|
+
source: rendered,
|
|
8799
|
+
path: entry.catalogPath
|
|
8800
|
+
};
|
|
8801
|
+
if (entry.skipped || !entry.artifact) return {
|
|
8802
|
+
...row,
|
|
8803
|
+
status: "skipped",
|
|
8804
|
+
source: rendered,
|
|
8805
|
+
reason: entry.skipped
|
|
8806
|
+
};
|
|
8807
|
+
if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) return {
|
|
8808
|
+
...row,
|
|
8809
|
+
status: "unchanged"
|
|
8810
|
+
};
|
|
8811
|
+
if (opts.dryRun) return {
|
|
8812
|
+
...row,
|
|
8813
|
+
status: "planned"
|
|
8814
|
+
};
|
|
8815
|
+
const existed = entry.existing !== void 0 && catalogEntry(entry.existing, name) !== void 0;
|
|
8816
|
+
return {
|
|
8817
|
+
...row,
|
|
8818
|
+
status: existed ? "updated" : "added"
|
|
8819
|
+
};
|
|
8820
|
+
});
|
|
8821
|
+
if (!opts.dryRun) for (const entry of planned) {
|
|
8822
|
+
if (!entry.artifact) continue;
|
|
8823
|
+
if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) continue;
|
|
8824
|
+
fs.writeAtomically(entry.file, entry.artifact.content);
|
|
8825
|
+
}
|
|
8826
|
+
return results;
|
|
8827
|
+
}
|
|
8828
|
+
//#endregion
|
|
8396
8829
|
//#region src/marketplace/validate.ts
|
|
8397
8830
|
/** A `./`-prefixed source names a directory inside the repository, and Claude Code resolves it
|
|
8398
8831
|
* against the directory holding `.claude-plugin/`. A source pointing nowhere passes every schema
|
|
@@ -8463,6 +8896,65 @@ function targetsFromOptions(opts) {
|
|
|
8463
8896
|
].filter((target) => opts[target]);
|
|
8464
8897
|
return targets.length > 0 ? targets : void 0;
|
|
8465
8898
|
}
|
|
8899
|
+
/** The kind flags, as one map, so a second one is an error rather than a silent precedence rule. */
|
|
8900
|
+
const KIND_FLAGS = {
|
|
8901
|
+
path: "path",
|
|
8902
|
+
npm: "npm",
|
|
8903
|
+
github: "github",
|
|
8904
|
+
url: "url",
|
|
8905
|
+
fromMarketplace: "marketplace"
|
|
8906
|
+
};
|
|
8907
|
+
function kindFromOptions(opts) {
|
|
8908
|
+
const named = Object.keys(KIND_FLAGS).filter((flag) => opts[flag]);
|
|
8909
|
+
if (named.length > 1) throw new Error(`error: pass one source kind, not ${named.length}`);
|
|
8910
|
+
return named.length === 1 ? KIND_FLAGS[named[0]] : void 0;
|
|
8911
|
+
}
|
|
8912
|
+
function metadataFromOptions(opts) {
|
|
8913
|
+
const metadata = {};
|
|
8914
|
+
for (const field of ENTRY_METADATA_FIELDS) {
|
|
8915
|
+
const value = opts[field];
|
|
8916
|
+
if (typeof value !== "string") continue;
|
|
8917
|
+
metadata[field] = field === "keywords" ? value.split(",").map((keyword) => keyword.trim()).filter((keyword) => keyword !== "") : value;
|
|
8918
|
+
}
|
|
8919
|
+
return metadata;
|
|
8920
|
+
}
|
|
8921
|
+
function addCommand() {
|
|
8922
|
+
return new Command("add").description("List a plugin that lives elsewhere in this repository's marketplace catalogs").argument("<spec>", "What to list: ./path, owner/repo, a git URL, an npm package (npm:<pkg>), or <plugin>@<marketplace>").option("--claude", "Write the Claude marketplace catalog").option("--codex", "Write the Codex marketplace catalog").option("--copilot", "Write the Copilot marketplace catalog").option("--cursor", "Write the Cursor marketplace catalog").option("--path", "Read the spec as a repository-relative path").option("--npm", "Read the spec as an npm package name").option("--github", "Read the spec as an owner/repo GitHub repository").option("--url", "Read the spec as a git or https URL").option("--from-marketplace", "Read the spec as <plugin>@<marketplace>").option("--from <dir>", "Directory holding the marketplace to copy an entry from").option("--subdir <path>", "The plugin's directory inside the repository, for a monorepo").option("--ref <ref>", "Branch or tag to pin a git source to").option("--sha <sha>", "Commit to pin a git source to (full 40-character hash)").option("--name <name>", "Entry name, when it differs from the one the spec implies").option("--description <text>", "Entry description").option("--version <version>", "Entry version").option("--homepage <url>", "Entry homepage").option("--repository <url>", "Entry repository URL").option("--license <id>", "Entry license").option("--keywords <list>", "Entry keywords, comma-separated").option("--marketplace-name <name>", "Name for the catalog, when this command has to create one").option("--owner <name>", "Owner for the catalog, when this command has to create one").option("--dry-run", "Preview the entry without writing it").option("--force", "Replace an entry of this name that is already listed differently").option("--format <format>", "Output format: toon or json (default: toon)").addOption(ROOT_OPTION).addHelpText("after", "\nExamples:\n $ universal-plugin marketplace add npm:repobuddy\n $ universal-plugin marketplace add cyberuni/universal-plugin\n $ universal-plugin marketplace add cyberuni/cyber-sdd --subdir plugins/aced\n $ universal-plugin marketplace add repobuddy@cyberplace --dry-run\n").action((spec, opts) => {
|
|
8923
|
+
try {
|
|
8924
|
+
if (opts.format !== void 0 && opts.format !== "toon" && opts.format !== "json") throw new Error("error: --format must be \"toon\" or \"json\"");
|
|
8925
|
+
const results = addToMarketplace(resolveRoot(opts.root), spec, {
|
|
8926
|
+
targets: targetsFromOptions(opts),
|
|
8927
|
+
kind: kindFromOptions(opts),
|
|
8928
|
+
name: opts.name,
|
|
8929
|
+
from: opts.from,
|
|
8930
|
+
subdir: opts.subdir,
|
|
8931
|
+
ref: opts.ref,
|
|
8932
|
+
sha: opts.sha,
|
|
8933
|
+
metadata: metadataFromOptions(opts),
|
|
8934
|
+
marketplaceName: opts.marketplaceName,
|
|
8935
|
+
owner: opts.owner,
|
|
8936
|
+
dryRun: opts.dryRun,
|
|
8937
|
+
force: opts.force
|
|
8938
|
+
});
|
|
8939
|
+
output(results, {
|
|
8940
|
+
targets: results.map((row) => ({
|
|
8941
|
+
target: row.target,
|
|
8942
|
+
status: row.status,
|
|
8943
|
+
entry: row.entry,
|
|
8944
|
+
source: row.source,
|
|
8945
|
+
path: row.path,
|
|
8946
|
+
reason: row.reason ?? "-"
|
|
8947
|
+
})),
|
|
8948
|
+
summary: `${results.filter((row) => row.status === "skipped").length} skipped of ${results.length}`
|
|
8949
|
+
});
|
|
8950
|
+
for (const row of results.filter((row) => row.status === "skipped")) process.stderr.write(`skipped ${row.target}: ${row.reason}\n`);
|
|
8951
|
+
process.stderr.write(`${opts.dryRun ? "Planned" : "Wrote"} repository metadata only; no marketplace publication, registration, installation, authentication, or provisioning occurred.\n`);
|
|
8952
|
+
} catch (err) {
|
|
8953
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
8954
|
+
process.exitCode = 1;
|
|
8955
|
+
}
|
|
8956
|
+
});
|
|
8957
|
+
}
|
|
8466
8958
|
function initCommand() {
|
|
8467
8959
|
return new Command("init").description("Generate local marketplace metadata without publishing or provisioning").option("--claude", "Generate the Claude marketplace catalog").option("--codex", "Generate the Codex marketplace catalog").option("--copilot", "Generate the Copilot marketplace catalog").option("--cursor", "Generate the Cursor marketplace catalog").option("--plugin-scan-dir <dir>", "Directory beneath --root to scan for root-level plugin.json files (repeatable)", (value, previous = []) => [...previous, value]).option("--name <name>", "Override the marketplace name").option("--owner <name>", "Override the marketplace owner").option("--dry-run", "Preview artifacts without writing them").option("--force", "Replace differing artifacts for selected targets").option("--format <format>", "Output format: toon or json (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin marketplace init --root .\n").action((opts) => {
|
|
8468
8960
|
try {
|
|
@@ -8520,13 +9012,70 @@ function validateCommand$1() {
|
|
|
8520
9012
|
});
|
|
8521
9013
|
}
|
|
8522
9014
|
function marketplaceCommand() {
|
|
8523
|
-
return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(initCommand()).addCommand(validateCommand$1());
|
|
9015
|
+
return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(addCommand()).addCommand(initCommand()).addCommand(validateCommand$1());
|
|
8524
9016
|
}
|
|
8525
9017
|
//#endregion
|
|
8526
|
-
//#region src/prepare/
|
|
8527
|
-
|
|
8528
|
-
|
|
9018
|
+
//#region src/prepare/installed-plugins.ts
|
|
9019
|
+
/**
|
|
9020
|
+
* Reads Claude Code's `installed_plugins.json` (version 2): `plugins` maps `<plugin>@<marketplace>`
|
|
9021
|
+
* to one entry per install, each with a `scope`, `installPath`, `version`, and, for project
|
|
9022
|
+
* installs, `projectPath`. Global scope takes the `user` installs; project scope takes the installs
|
|
9023
|
+
* whose `projectPath` is the project root.
|
|
9024
|
+
*/
|
|
9025
|
+
function parseClaudeInstallRecord(raw, { scope, projectRoot }) {
|
|
9026
|
+
const plugins = isRecord(raw) && isRecord(raw["plugins"]) ? raw["plugins"] : {};
|
|
9027
|
+
const result = [];
|
|
9028
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
9029
|
+
if (!Array.isArray(entries)) continue;
|
|
9030
|
+
const entry = entries.filter(isRecord).find((e) => scope === "global" ? e["scope"] === "user" : e["projectPath"] === projectRoot);
|
|
9031
|
+
if (!entry || typeof entry["installPath"] !== "string") continue;
|
|
9032
|
+
result.push({
|
|
9033
|
+
name: pluginName(key),
|
|
9034
|
+
version: typeof entry["version"] === "string" ? entry["version"] : "unknown",
|
|
9035
|
+
root: entry["installPath"]
|
|
9036
|
+
});
|
|
9037
|
+
}
|
|
9038
|
+
return result;
|
|
9039
|
+
}
|
|
9040
|
+
/** Strips the `@<marketplace>` suffix from a `<plugin>@<marketplace>` key. */
|
|
9041
|
+
function pluginName(key) {
|
|
9042
|
+
const at = key.lastIndexOf("@");
|
|
9043
|
+
return at > 0 ? key.slice(0, at) : key;
|
|
9044
|
+
}
|
|
9045
|
+
/**
|
|
9046
|
+
* Picks the newest of a plugin's cached version folders. Dotted numeric versions compare
|
|
9047
|
+
* numerically, part by part; anything else falls back to string order.
|
|
9048
|
+
*/
|
|
9049
|
+
function latestVersion(versions) {
|
|
9050
|
+
return [...versions].sort(compareVersions).at(-1);
|
|
9051
|
+
}
|
|
9052
|
+
function compareVersions(a, b) {
|
|
9053
|
+
const pa = a.split(/[.+-]/);
|
|
9054
|
+
const pb = b.split(/[.+-]/);
|
|
9055
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
9056
|
+
const x = pa[i] ?? "";
|
|
9057
|
+
const y = pb[i] ?? "";
|
|
9058
|
+
if (x === y) continue;
|
|
9059
|
+
const nx = Number(x);
|
|
9060
|
+
const ny = Number(y);
|
|
9061
|
+
if (x !== "" && y !== "" && Number.isInteger(nx) && Number.isInteger(ny)) return nx - ny;
|
|
9062
|
+
return x < y ? -1 : 1;
|
|
9063
|
+
}
|
|
9064
|
+
return 0;
|
|
9065
|
+
}
|
|
9066
|
+
/** Maps installed plugins to the name → version record `prepare` snapshots. */
|
|
9067
|
+
function toVersions(plugins) {
|
|
9068
|
+
return Object.fromEntries(plugins.map((p) => [p.name, p.version]));
|
|
9069
|
+
}
|
|
9070
|
+
/** Maps installed plugins to name → root, with the home directory written as `~`. */
|
|
9071
|
+
function toRoots(plugins, home) {
|
|
9072
|
+
return Object.fromEntries(plugins.map((p) => [p.name, p.root.startsWith(home) ? "~" + p.root.slice(home.length) : p.root]));
|
|
9073
|
+
}
|
|
9074
|
+
function isRecord(value) {
|
|
9075
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8529
9076
|
}
|
|
9077
|
+
//#endregion
|
|
9078
|
+
//#region src/prepare/fs.ts
|
|
8530
9079
|
function globalStatePath$2() {
|
|
8531
9080
|
return path.join(os.homedir(), ".agents", "universal-plugin.json");
|
|
8532
9081
|
}
|
|
@@ -8545,29 +9094,14 @@ function writeStateFile(filePath, state) {
|
|
|
8545
9094
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8546
9095
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
8547
9096
|
}
|
|
8548
|
-
|
|
9097
|
+
/** `environment` locates the harness's plugin folders; it defaults to the current process. */
|
|
9098
|
+
function realPrepareFs(harness, installScope, environment = {}) {
|
|
9099
|
+
const projectRoot = installScope.projectRoot;
|
|
9100
|
+
let installed;
|
|
9101
|
+
const readInstalled = () => installed ??= readInstalledPlugins(harness, installScope, environment);
|
|
8549
9102
|
return {
|
|
8550
|
-
readManifest()
|
|
8551
|
-
|
|
8552
|
-
const manifestPath = expandHome(vendor.globalManifest);
|
|
8553
|
-
try {
|
|
8554
|
-
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
8555
|
-
return Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === "object" && v !== null && "version" in v ? String(v.version) : String(v)]));
|
|
8556
|
-
} catch (err) {
|
|
8557
|
-
if (err.code === "ENOENT") return {};
|
|
8558
|
-
throw err;
|
|
8559
|
-
}
|
|
8560
|
-
},
|
|
8561
|
-
readPluginRoots() {
|
|
8562
|
-
if (!vendor.globalPluginDir) return {};
|
|
8563
|
-
const pluginDir = expandHome(vendor.globalPluginDir);
|
|
8564
|
-
const manifest = this.readManifest();
|
|
8565
|
-
const home = os.homedir();
|
|
8566
|
-
return Object.fromEntries(Object.keys(manifest).map((name) => {
|
|
8567
|
-
const absPath = path.join(pluginDir, name);
|
|
8568
|
-
return [name, absPath.startsWith(home) ? "~" + absPath.slice(home.length) : absPath];
|
|
8569
|
-
}));
|
|
8570
|
-
},
|
|
9103
|
+
readManifest: () => toVersions(readInstalled()),
|
|
9104
|
+
readPluginRoots: () => toRoots(readInstalled(), environment.homedir ?? os.homedir()),
|
|
8571
9105
|
readGlobalState: () => readStateFile(globalStatePath$2()) ?? emptyState(),
|
|
8572
9106
|
readProjectState: () => projectRoot ? readStateFile(projectStatePath(projectRoot)) : null,
|
|
8573
9107
|
writeGlobalState: (s) => writeStateFile(globalStatePath$2(), s),
|
|
@@ -8581,7 +9115,85 @@ function populateStoreFromVendorCache(pluginRoots, versions) {
|
|
|
8581
9115
|
for (const [pluginName, pluginRoot] of Object.entries(pluginRoots)) {
|
|
8582
9116
|
const entryPath = storeEntryPath(storePath, `npm/${pluginName}@${versions[pluginName] ?? "unknown"}`);
|
|
8583
9117
|
if (entryExists(entryPath)) continue;
|
|
8584
|
-
populateEntry(entryPath, pluginRoot);
|
|
9118
|
+
populateEntry(entryPath, expandHome(pluginRoot));
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
function expandHome(p) {
|
|
9122
|
+
return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
|
|
9123
|
+
}
|
|
9124
|
+
/** Lists what a harness has installed, the way that harness records it: Claude Code in its install
|
|
9125
|
+
* record, Copilot CLI and Codex only in the layout of their plugin folders. Cursor keeps no record
|
|
9126
|
+
* of installed plugins. */
|
|
9127
|
+
function readInstalledPlugins(harness, installScope, environment) {
|
|
9128
|
+
const storage = pluginStorage(harness, environment);
|
|
9129
|
+
const location = (kind) => storage.locations.find((l) => l.kind === kind)?.path;
|
|
9130
|
+
switch (harness) {
|
|
9131
|
+
case "claude-code": {
|
|
9132
|
+
const record = location("install-record");
|
|
9133
|
+
const raw = record ? readJson(record) : null;
|
|
9134
|
+
return raw === null ? [] : parseClaudeInstallRecord(raw, installScope);
|
|
9135
|
+
}
|
|
9136
|
+
case "copilot-cli": {
|
|
9137
|
+
const dir = location("installed-plugins");
|
|
9138
|
+
return dir && installScope.scope === "global" ? readCopilotInstalls(dir) : [];
|
|
9139
|
+
}
|
|
9140
|
+
case "codex": {
|
|
9141
|
+
const dir = location("plugin-cache");
|
|
9142
|
+
return dir && installScope.scope === "global" ? readCodexCache(dir) : [];
|
|
9143
|
+
}
|
|
9144
|
+
default: return [];
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
/** `<dir>/<marketplace>/<plugin>/` for marketplace installs, `<dir>/_direct/<source-id>/` for
|
|
9148
|
+
* direct ones; the plugin's own manifest carries its name and version. */
|
|
9149
|
+
function readCopilotInstalls(dir) {
|
|
9150
|
+
return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).map((folder) => {
|
|
9151
|
+
const root = path.join(dir, marketplace, folder);
|
|
9152
|
+
const manifest = readPluginManifest(root);
|
|
9153
|
+
return {
|
|
9154
|
+
name: typeof manifest["name"] === "string" ? manifest["name"] : folder,
|
|
9155
|
+
version: typeof manifest["version"] === "string" ? manifest["version"] : "unknown",
|
|
9156
|
+
root
|
|
9157
|
+
};
|
|
9158
|
+
}));
|
|
9159
|
+
}
|
|
9160
|
+
/** `<dir>/<marketplace>/<plugin>/<version>/`; the newest cached version is the installed one. */
|
|
9161
|
+
function readCodexCache(dir) {
|
|
9162
|
+
return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).flatMap((name) => {
|
|
9163
|
+
const version = latestVersion(subdirs(path.join(dir, marketplace, name)));
|
|
9164
|
+
return version ? [{
|
|
9165
|
+
name,
|
|
9166
|
+
version,
|
|
9167
|
+
root: path.join(dir, marketplace, name, version)
|
|
9168
|
+
}] : [];
|
|
9169
|
+
}));
|
|
9170
|
+
}
|
|
9171
|
+
const PLUGIN_MANIFESTS = [
|
|
9172
|
+
"plugin.json",
|
|
9173
|
+
".github/plugin/plugin.json",
|
|
9174
|
+
".claude-plugin/plugin.json"
|
|
9175
|
+
];
|
|
9176
|
+
function readPluginManifest(root) {
|
|
9177
|
+
for (const file of PLUGIN_MANIFESTS) {
|
|
9178
|
+
const raw = readJson(path.join(root, file));
|
|
9179
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
9180
|
+
}
|
|
9181
|
+
return {};
|
|
9182
|
+
}
|
|
9183
|
+
function readJson(filePath) {
|
|
9184
|
+
try {
|
|
9185
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
9186
|
+
} catch (err) {
|
|
9187
|
+
if (err.code === "ENOENT") return null;
|
|
9188
|
+
throw err;
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
function subdirs(dir) {
|
|
9192
|
+
try {
|
|
9193
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9194
|
+
} catch (err) {
|
|
9195
|
+
if (err.code === "ENOENT") return [];
|
|
9196
|
+
throw err;
|
|
8585
9197
|
}
|
|
8586
9198
|
}
|
|
8587
9199
|
//#endregion
|
|
@@ -8698,8 +9310,7 @@ function runPrepare(opts) {
|
|
|
8698
9310
|
//#region src/prepare/cli.ts
|
|
8699
9311
|
function prepareCommand() {
|
|
8700
9312
|
return new Command("prepare").description("Detect cross-vendor plugin sync actions").argument("<vendor-id>", "Vendor to read manifest from (e.g. claude-code)").addOption(new Option("--scope <scope>", "global or project").default("global").choices(["global", "project"])).option("--root <path>", "Project root for project-scope state file").option("--dry-run", "Print action count without writing state").action((vendorId, opts) => {
|
|
8701
|
-
|
|
8702
|
-
if (!vendor) {
|
|
9313
|
+
if (!lookupVendor(loadRegistry(), vendorId) || !harnessIds.includes(vendorId)) {
|
|
8703
9314
|
process.stderr.write(`Unknown vendor: ${vendorId}\n`);
|
|
8704
9315
|
process.exit(1);
|
|
8705
9316
|
}
|
|
@@ -8708,12 +9319,16 @@ function prepareCommand() {
|
|
|
8708
9319
|
process.exit(1);
|
|
8709
9320
|
}
|
|
8710
9321
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8711
|
-
const
|
|
9322
|
+
const scope = opts.scope;
|
|
9323
|
+
const prepareFs = realPrepareFs(vendorId, {
|
|
9324
|
+
scope,
|
|
9325
|
+
projectRoot: opts.root ? path.resolve(opts.root) : void 0
|
|
9326
|
+
});
|
|
8712
9327
|
const pluginRoots = prepareFs.readPluginRoots();
|
|
8713
9328
|
const manifest = prepareFs.readManifest();
|
|
8714
9329
|
const { newActionCount } = runPrepare({
|
|
8715
9330
|
vendorId,
|
|
8716
|
-
scope
|
|
9331
|
+
scope,
|
|
8717
9332
|
fs: prepareFs,
|
|
8718
9333
|
now,
|
|
8719
9334
|
dryRun: opts.dryRun
|
|
@@ -8775,7 +9390,7 @@ const realSyncVersionFs = realJsonIo;
|
|
|
8775
9390
|
//#endregion
|
|
8776
9391
|
//#region src/publish/sync-version.ts
|
|
8777
9392
|
/** The changesets-driven direction of the version flow: the number is decided by
|
|
8778
|
-
* `changeset version` in `<packagePath>/package.json
|
|
9393
|
+
* `changeset version` in `<packagePath>/package.json` (the plugin root's when none is declared), and this copies it into the canonical
|
|
8779
9394
|
* manifest. `plugin version` is the other direction — the number decided here, flowing out to
|
|
8780
9395
|
* `package.json`. The two differ **only** in where the version comes from, so they share
|
|
8781
9396
|
* `applyVersionPlan` and cannot drift; `package.json` is the source here, never rewritten. */
|
|
@@ -8783,11 +9398,11 @@ function syncVersion(root, syncFs) {
|
|
|
8783
9398
|
const manifestPath = path.join(root, "plugin.json");
|
|
8784
9399
|
if (!syncFs.exists(manifestPath)) throw new Error(`No plugin.json found at ${root}`);
|
|
8785
9400
|
const agentsConfigPath = path.join(root, ".agents", "universal-plugin.json");
|
|
8786
|
-
const
|
|
8787
|
-
|
|
9401
|
+
const declared = getPackagePath(syncFs.exists(agentsConfigPath) ? JSON.parse(syncFs.read(agentsConfigPath)) : {});
|
|
9402
|
+
const packagePath = declared ?? ".";
|
|
8788
9403
|
const manifest = JSON.parse(syncFs.read(manifestPath));
|
|
8789
9404
|
const pkgJsonPath = path.join(root, packagePath, "package.json");
|
|
8790
|
-
if (!syncFs.exists(pkgJsonPath)) throw new Error(`No package.json found at ${packagePath}`);
|
|
9405
|
+
if (!syncFs.exists(pkgJsonPath)) throw new Error(declared === null ? "No package.json to sync from: packagePath is not set in .agents/universal-plugin.json and the plugin root has no package.json" : `No package.json found at ${packagePath}`);
|
|
8791
9406
|
const version = JSON.parse(syncFs.read(pkgJsonPath))["version"];
|
|
8792
9407
|
if (!version || typeof version !== "string") throw new Error(`No version found in ${packagePath}/package.json`);
|
|
8793
9408
|
const current = manifest["version"];
|
|
@@ -8814,7 +9429,7 @@ function syncVersion(root, syncFs) {
|
|
|
8814
9429
|
//#region src/publish/cli.ts
|
|
8815
9430
|
function publishCommand() {
|
|
8816
9431
|
const cmd = new Command("publish").description("Prepare plugin for publishing").helpCommand(false);
|
|
8817
|
-
cmd.command("sync-version").description("Sync version from packagePath/package.json into plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
9432
|
+
cmd.command("sync-version").description("Sync version from packagePath/package.json (default: the plugin root) into plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
8818
9433
|
try {
|
|
8819
9434
|
const result = syncVersion(resolveRoot(opts.root), realSyncVersionFs);
|
|
8820
9435
|
output(result, {
|
|
@@ -9229,7 +9844,6 @@ function pluginCommand() {
|
|
|
9229
9844
|
program.addCommand(pluginCommand());
|
|
9230
9845
|
program.addCommand(cleanCommand());
|
|
9231
9846
|
program.addCommand(configCommand());
|
|
9232
|
-
program.addCommand(governanceCommand());
|
|
9233
9847
|
program.addCommand(marketplaceCommand());
|
|
9234
9848
|
program.addCommand(prepareCommand());
|
|
9235
9849
|
program.addCommand(publishCommand());
|