universal-plugin 0.8.0 → 0.10.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 +717 -164
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/readme.md +12 -8
- 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 → doctor-universal-plugin}/README.md +1 -1
- package/skills/{doctor → doctor-universal-plugin}/SKILL.md +2 -2
- package/skills/{doctor → 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 +1 -1
- 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
|
@@ -3779,12 +3779,6 @@ function output(data, view) {
|
|
|
3779
3779
|
if (isJsonOutput()) printJson(data);
|
|
3780
3780
|
else console.log(encode(view ?? data));
|
|
3781
3781
|
}
|
|
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
3782
|
//#endregion
|
|
3789
3783
|
//#region ../../node_modules/.pnpm/semver@7.8.1/node_modules/semver/internal/constants.js
|
|
3790
3784
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -5413,6 +5407,23 @@ function claudeArtifact(metadata, plugins) {
|
|
|
5413
5407
|
})
|
|
5414
5408
|
};
|
|
5415
5409
|
}
|
|
5410
|
+
/** Whether a catalog entry's `source` names a place inside this repository — a `./`-prefixed path in
|
|
5411
|
+
* the Claude-shaped catalogs, or Codex's `{ source: "local", path }`. Every other form names a
|
|
5412
|
+
* plugin distributed from somewhere else: an npm package, a GitHub repository, a URL. Discovery
|
|
5413
|
+
* walks directories, so it can produce the first kind and can say nothing about the second. */
|
|
5414
|
+
function isLocalCatalogSource(source) {
|
|
5415
|
+
if (typeof source === "string") return true;
|
|
5416
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return false;
|
|
5417
|
+
return source.source === "local";
|
|
5418
|
+
}
|
|
5419
|
+
/** Codex states a source as an object either way: a repository path is tagged `local`, and a source
|
|
5420
|
+
* that already carries its own tag passes through as it stands. */
|
|
5421
|
+
function codexSource(source) {
|
|
5422
|
+
return typeof source === "string" ? {
|
|
5423
|
+
source: "local",
|
|
5424
|
+
path: source
|
|
5425
|
+
} : source;
|
|
5426
|
+
}
|
|
5416
5427
|
function codexArtifact(metadata, plugins) {
|
|
5417
5428
|
return {
|
|
5418
5429
|
path: TARGET_CATALOG_PATHS.codex,
|
|
@@ -5422,10 +5433,7 @@ function codexArtifact(metadata, plugins) {
|
|
|
5422
5433
|
plugins: plugins.map((plugin) => ({
|
|
5423
5434
|
name: plugin.name,
|
|
5424
5435
|
version: plugin.metadata.version,
|
|
5425
|
-
source:
|
|
5426
|
-
source: "local",
|
|
5427
|
-
path: plugin.source
|
|
5428
|
-
},
|
|
5436
|
+
source: codexSource(plugin.source),
|
|
5429
5437
|
policy: {
|
|
5430
5438
|
installation: "AVAILABLE",
|
|
5431
5439
|
authentication: "ON_INSTALL"
|
|
@@ -5474,6 +5482,72 @@ const VENDOR_TARGETS = {
|
|
|
5474
5482
|
codex: "codex",
|
|
5475
5483
|
"copilot-cli": "copilot"
|
|
5476
5484
|
};
|
|
5485
|
+
const GITHUB_URL = /^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:)([^/]+\/[^/]+?)(?:\.git)?\/?$/;
|
|
5486
|
+
/** A git URL read as an origin, recognizing a GitHub one so an entry at the repository root can use
|
|
5487
|
+
* the tidier `github` form. */
|
|
5488
|
+
function originFromUrl(url) {
|
|
5489
|
+
const match = GITHUB_URL.exec(url.trim());
|
|
5490
|
+
return match ? {
|
|
5491
|
+
url,
|
|
5492
|
+
repo: match[1]
|
|
5493
|
+
} : { url };
|
|
5494
|
+
}
|
|
5495
|
+
/** An `owner/repo` slug as an origin. */
|
|
5496
|
+
function originFromRepo(repo) {
|
|
5497
|
+
return {
|
|
5498
|
+
url: `https://github.com/${repo}.git`,
|
|
5499
|
+
repo
|
|
5500
|
+
};
|
|
5501
|
+
}
|
|
5502
|
+
/** The path part of a local source, `./` stripped, empty at the marketplace root. */
|
|
5503
|
+
function localSourcePath(source) {
|
|
5504
|
+
const raw = typeof source === "string" ? source : source.path;
|
|
5505
|
+
if (typeof raw !== "string") return void 0;
|
|
5506
|
+
return raw.replace(/^\.\/?/, "").replace(/\/+$/, "");
|
|
5507
|
+
}
|
|
5508
|
+
/** Rewrites a source that is local to *another* marketplace into one that resolves from anywhere.
|
|
5509
|
+
*
|
|
5510
|
+
* A copied entry's `./plugins/aced` is relative to the marketplace it came from, so carrying it
|
|
5511
|
+
* across unchanged would point at a directory this repository does not have. The path is not
|
|
5512
|
+
* useless, though — it is a location inside a repository whose URL is known, which is exactly what
|
|
5513
|
+
* `git-subdir` states. An entry at that repository's root needs no subdirectory and takes the
|
|
5514
|
+
* plainer `github` or `url` form. */
|
|
5515
|
+
function absoluteSource(origin, source) {
|
|
5516
|
+
const subdir = localSourcePath(source);
|
|
5517
|
+
if (subdir === void 0) return void 0;
|
|
5518
|
+
if (subdir === "" || subdir === ".") return origin.repo ? {
|
|
5519
|
+
source: "github",
|
|
5520
|
+
repo: origin.repo
|
|
5521
|
+
} : {
|
|
5522
|
+
source: "url",
|
|
5523
|
+
url: origin.url
|
|
5524
|
+
};
|
|
5525
|
+
return {
|
|
5526
|
+
source: "git-subdir",
|
|
5527
|
+
url: origin.url,
|
|
5528
|
+
path: subdir
|
|
5529
|
+
};
|
|
5530
|
+
}
|
|
5531
|
+
/** The source forms each runtime installs from./** The source forms each runtime installs from.
|
|
5532
|
+
*
|
|
5533
|
+
* Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
|
|
5534
|
+
* cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
|
|
5535
|
+
* cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
|
|
5536
|
+
* (<https://json.schemastore.org/claude-code-marketplace.json>). Codex documents npm alongside a
|
|
5537
|
+
* local path. Copilot CLI and Cursor document local paths only, which is why an npm entry reaches
|
|
5538
|
+
* two catalogs rather than four (`.research/local-marketplaces`, and issue #86). */
|
|
5539
|
+
const TARGET_SOURCE_KINDS = {
|
|
5540
|
+
claude: [
|
|
5541
|
+
"path",
|
|
5542
|
+
"npm",
|
|
5543
|
+
"github",
|
|
5544
|
+
"url",
|
|
5545
|
+
"git-subdir"
|
|
5546
|
+
],
|
|
5547
|
+
codex: ["path", "npm"],
|
|
5548
|
+
copilot: ["path"],
|
|
5549
|
+
cursor: ["path"]
|
|
5550
|
+
};
|
|
5477
5551
|
/** Folds one plugin's entry into a catalog that may already exist, and returns the artifact to
|
|
5478
5552
|
* write. An existing catalog keeps its own top-level fields — its name, its owner, a description
|
|
5479
5553
|
* someone wrote — and every entry it lists for other plugins, in place. Only this plugin's entry is
|
|
@@ -5482,7 +5556,7 @@ const VENDOR_TARGETS = {
|
|
|
5482
5556
|
* `version` is derived, never authored (ADR-0010 §3): the entry carries whatever the canonical
|
|
5483
5557
|
* manifest carries, and a version left behind on an entry whose manifest declares none is removed
|
|
5484
5558
|
* rather than kept. */
|
|
5485
|
-
function mergeCatalogEntry(target, metadata, plugin, readExisting) {
|
|
5559
|
+
function mergeCatalogEntry(target, metadata, plugin, readExisting, { keepForeignSource = true } = {}) {
|
|
5486
5560
|
const artifact = serializeTarget(target, metadata, [plugin])[0];
|
|
5487
5561
|
const existing = readExisting(artifact.path);
|
|
5488
5562
|
if (existing === void 0) return artifact;
|
|
@@ -5491,7 +5565,7 @@ function mergeCatalogEntry(target, metadata, plugin, readExisting) {
|
|
|
5491
5565
|
const entry = generated.plugins[0];
|
|
5492
5566
|
const merged = { ...previous };
|
|
5493
5567
|
for (const [key, value] of Object.entries(generated)) if (!(key in merged)) merged[key] = value;
|
|
5494
|
-
merged.plugins = mergeEntries(previous, entry);
|
|
5568
|
+
merged.plugins = mergeEntries(previous, entry, { keepForeignSource });
|
|
5495
5569
|
return {
|
|
5496
5570
|
path: artifact.path,
|
|
5497
5571
|
content: json(merged)
|
|
@@ -5507,18 +5581,78 @@ function parseCatalog(content, path) {
|
|
|
5507
5581
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`error: existing catalog "${path}" is not a JSON object`);
|
|
5508
5582
|
return parsed;
|
|
5509
5583
|
}
|
|
5510
|
-
function mergeEntries(previous, entry) {
|
|
5584
|
+
function mergeEntries(previous, entry, { keepForeignSource = false } = {}) {
|
|
5511
5585
|
const entries = Array.isArray(previous.plugins) ? [...previous.plugins] : [];
|
|
5512
5586
|
const index = entries.findIndex((candidate) => typeof candidate === "object" && candidate !== null && candidate.name === entry.name);
|
|
5513
5587
|
if (index === -1) return [...entries, entry];
|
|
5588
|
+
const existing = entries[index];
|
|
5514
5589
|
const merged = {
|
|
5515
|
-
...
|
|
5590
|
+
...existing,
|
|
5516
5591
|
...entry
|
|
5517
5592
|
};
|
|
5518
5593
|
if (!("version" in entry)) delete merged.version;
|
|
5594
|
+
if (keepForeignSource && !isLocalCatalogSource(existing.source)) merged.source = existing.source;
|
|
5519
5595
|
entries[index] = merged;
|
|
5520
5596
|
return entries;
|
|
5521
5597
|
}
|
|
5598
|
+
/** Re-derives a whole catalog from discovery while keeping the entries discovery cannot see.
|
|
5599
|
+
*
|
|
5600
|
+
* A repository may list plugins that live somewhere else — an npm package, a GitHub repository —
|
|
5601
|
+
* put there by `marketplace add`. Nothing on disk produces those, so a regeneration that trusted
|
|
5602
|
+
* discovery alone would report every one of them as a deletion, and `--force` would carry it out.
|
|
5603
|
+
* Two things survive instead: an entry whose source is not local stays even though discovery never
|
|
5604
|
+
* saw it, and a discovered plugin whose existing entry names a non-local source keeps that source
|
|
5605
|
+
* with only its derived metadata refreshed.
|
|
5606
|
+
*
|
|
5607
|
+
* That second case is the loss in issue #86 — a plugin shipped through npm, whose repository path
|
|
5608
|
+
* holds only gitignored build output, rewritten to that path on every regeneration.
|
|
5609
|
+
*
|
|
5610
|
+
* What discovery owns, it still owns: a local-path entry it no longer finds is dropped, so the
|
|
5611
|
+
* catalog keeps mirroring the repository for the part of the repository it describes. */
|
|
5612
|
+
function mergeDiscoveredCatalog(generated, existing) {
|
|
5613
|
+
if (existing === void 0) return generated;
|
|
5614
|
+
let previous;
|
|
5615
|
+
try {
|
|
5616
|
+
previous = parseCatalog(existing, generated.path);
|
|
5617
|
+
} catch {
|
|
5618
|
+
return generated;
|
|
5619
|
+
}
|
|
5620
|
+
const foreign = /* @__PURE__ */ new Map();
|
|
5621
|
+
for (const candidate of Array.isArray(previous.plugins) ? previous.plugins : []) {
|
|
5622
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
|
|
5623
|
+
const entry = candidate;
|
|
5624
|
+
if (typeof entry.name !== "string" || isLocalCatalogSource(entry.source)) continue;
|
|
5625
|
+
foreign.set(entry.name, entry);
|
|
5626
|
+
}
|
|
5627
|
+
if (foreign.size === 0) return generated;
|
|
5628
|
+
const catalog = JSON.parse(generated.content);
|
|
5629
|
+
const discovered = catalog.plugins ?? [];
|
|
5630
|
+
const merged = discovered.map((entry) => {
|
|
5631
|
+
const kept = typeof entry.name === "string" ? foreign.get(entry.name) : void 0;
|
|
5632
|
+
return kept === void 0 ? entry : {
|
|
5633
|
+
...entry,
|
|
5634
|
+
source: kept.source
|
|
5635
|
+
};
|
|
5636
|
+
});
|
|
5637
|
+
const discoveredNames = new Set(discovered.map((entry) => entry.name));
|
|
5638
|
+
for (const [name, entry] of foreign) if (!discoveredNames.has(name)) merged.push(entry);
|
|
5639
|
+
catalog.plugins = merged;
|
|
5640
|
+
return {
|
|
5641
|
+
path: generated.path,
|
|
5642
|
+
content: json(catalog)
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
/** The plugin names a catalog lists, so a result row can report what the file ends up saying rather
|
|
5646
|
+
* than only what discovery contributed to it. */
|
|
5647
|
+
function catalogEntryNames(content) {
|
|
5648
|
+
try {
|
|
5649
|
+
const parsed = JSON.parse(content);
|
|
5650
|
+
if (!Array.isArray(parsed.plugins)) return [];
|
|
5651
|
+
return parsed.plugins.map((entry) => typeof entry === "object" && entry !== null ? entry.name : void 0).filter((name) => typeof name === "string");
|
|
5652
|
+
} catch {
|
|
5653
|
+
return [];
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5522
5656
|
/** The catalog's own top-level identity, read back from the file the repository already carries, so
|
|
5523
5657
|
* a refresh re-derives one entry without proposing a name or an owner of its own. */
|
|
5524
5658
|
function existingMetadata(previous) {
|
|
@@ -5545,7 +5679,7 @@ function refreshCatalogEntry(target, plugin, existing) {
|
|
|
5545
5679
|
path: catalogPath,
|
|
5546
5680
|
content: json({
|
|
5547
5681
|
...previous,
|
|
5548
|
-
plugins: mergeEntries(previous, entry)
|
|
5682
|
+
plugins: mergeEntries(previous, entry, { keepForeignSource: true })
|
|
5549
5683
|
})
|
|
5550
5684
|
};
|
|
5551
5685
|
}
|
|
@@ -5632,6 +5766,79 @@ function gatherCatalogRepo(root) {
|
|
|
5632
5766
|
catalogs
|
|
5633
5767
|
};
|
|
5634
5768
|
}
|
|
5769
|
+
/** Where Claude Code records the marketplaces a user has added, and clones each one.
|
|
5770
|
+
*
|
|
5771
|
+
* This is read, never written. It is the one place on the machine that already knows what
|
|
5772
|
+
* `<plugin>@<marketplace>` refers to, so resolving through it asks the user for nothing and reaches
|
|
5773
|
+
* no network. A marketplace they have not added is simply not found, and `--from` names it instead. */
|
|
5774
|
+
function claudePluginsHome(home = os.homedir()) {
|
|
5775
|
+
return path.join(home, ".claude", "plugins");
|
|
5776
|
+
}
|
|
5777
|
+
/** The origin the runtime recorded for a marketplace, in the vocabulary it records it in: a
|
|
5778
|
+
* `github` repo slug, or a `git`/`url` clone URL. */
|
|
5779
|
+
function registryOrigin(source) {
|
|
5780
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return void 0;
|
|
5781
|
+
const record = source;
|
|
5782
|
+
if (record.source === "github" && typeof record.repo === "string") return originFromRepo(record.repo);
|
|
5783
|
+
if ((record.source === "git" || record.source === "url") && typeof record.url === "string") return originFromUrl(record.url);
|
|
5784
|
+
}
|
|
5785
|
+
/** The URL a checkout was cloned from, which is the origin for a marketplace the registry does not
|
|
5786
|
+
* describe — one reached through `--from`, or added before the runtime recorded a source. */
|
|
5787
|
+
function gitRemoteUrl(dir) {
|
|
5788
|
+
try {
|
|
5789
|
+
const url = execFileSync("git", [
|
|
5790
|
+
"-C",
|
|
5791
|
+
dir,
|
|
5792
|
+
"remote",
|
|
5793
|
+
"get-url",
|
|
5794
|
+
"origin"
|
|
5795
|
+
], {
|
|
5796
|
+
encoding: "utf8",
|
|
5797
|
+
stdio: [
|
|
5798
|
+
"ignore",
|
|
5799
|
+
"pipe",
|
|
5800
|
+
"ignore"
|
|
5801
|
+
]
|
|
5802
|
+
}).trim();
|
|
5803
|
+
return url === "" ? void 0 : url;
|
|
5804
|
+
} catch {
|
|
5805
|
+
return;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
/** The marketplace a name refers to, or `undefined` when the user has not added it. */
|
|
5809
|
+
function resolveKnownMarketplace(name, marketplaceFs = realMarketplaceFs, home = os.homedir()) {
|
|
5810
|
+
const pluginsHome = claudePluginsHome(home);
|
|
5811
|
+
const registry = path.join(pluginsHome, "known_marketplaces.json");
|
|
5812
|
+
if (marketplaceFs.exists(registry)) {
|
|
5813
|
+
let parsed;
|
|
5814
|
+
try {
|
|
5815
|
+
parsed = JSON.parse(marketplaceFs.read(registry));
|
|
5816
|
+
} catch {
|
|
5817
|
+
parsed = void 0;
|
|
5818
|
+
}
|
|
5819
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5820
|
+
const entry = parsed[name];
|
|
5821
|
+
if (typeof entry === "object" && entry !== null) {
|
|
5822
|
+
const record = entry;
|
|
5823
|
+
const location = record.installLocation;
|
|
5824
|
+
if (typeof location === "string" && marketplaceFs.exists(location)) return {
|
|
5825
|
+
dir: location,
|
|
5826
|
+
origin: registryOrigin(record.source)
|
|
5827
|
+
};
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
const conventional = path.join(pluginsHome, "marketplaces", name);
|
|
5832
|
+
return marketplaceFs.exists(conventional) ? { dir: conventional } : void 0;
|
|
5833
|
+
}
|
|
5834
|
+
/** The catalog a marketplace directory carries, tried in the order the runtimes agree on: the Claude
|
|
5835
|
+
* path first, because three of the four read it. */
|
|
5836
|
+
function readMarketplaceCatalog(dir, marketplaceFs = realMarketplaceFs) {
|
|
5837
|
+
for (const relative of Object.values(TARGET_CATALOG_PATHS)) {
|
|
5838
|
+
const file = path.join(dir, relative);
|
|
5839
|
+
if (marketplaceFs.exists(file)) return marketplaceFs.read(file);
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5635
5842
|
//#endregion
|
|
5636
5843
|
//#region src/marketplace/validation.ts
|
|
5637
5844
|
function isObject$1(value) {
|
|
@@ -6241,7 +6448,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6241
6448
|
}
|
|
6242
6449
|
if (vendors.length === 0) {
|
|
6243
6450
|
const signals = legacyLayoutSignals(manifest, fs.existsSync(path.join(root, SHADOWING_MANIFEST)));
|
|
6244
|
-
if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor for the full diagnosis and the skill that owns each repair.`);
|
|
6451
|
+
if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor-universal-plugin for the full diagnosis and the skill that owns each repair.`);
|
|
6245
6452
|
warnings.push("No vendors declared in harnesses — nothing to build");
|
|
6246
6453
|
return {
|
|
6247
6454
|
vendors: [],
|
|
@@ -6681,8 +6888,8 @@ function formatMcpNote(note) {
|
|
|
6681
6888
|
}
|
|
6682
6889
|
//#endregion
|
|
6683
6890
|
//#region src/build/cli.ts
|
|
6684
|
-
const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor — diagnose why nothing is declared\n";
|
|
6685
|
-
const NEXT_STEP_BUILT = "→ /universal-plugin:doctor — check the built manifests against plugin.json\n";
|
|
6891
|
+
const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor-universal-plugin — diagnose why nothing is declared\n";
|
|
6892
|
+
const NEXT_STEP_BUILT = "→ /universal-plugin:doctor-universal-plugin — check the built manifests against plugin.json\n";
|
|
6686
6893
|
/** Every hint names a command that exists today: an agent that follows a hint into
|
|
6687
6894
|
* `unknown command` is at a dead end (issue #80). A refreshed catalog is checked by
|
|
6688
6895
|
* `marketplace validate`, pointed at the repository the catalog lives in. */
|
|
@@ -7094,7 +7301,7 @@ const realConfigFs = {
|
|
|
7094
7301
|
function assertNotReserved(key) {
|
|
7095
7302
|
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`);
|
|
7096
7303
|
}
|
|
7097
|
-
function addCommand(fs) {
|
|
7304
|
+
function addCommand$1(fs) {
|
|
7098
7305
|
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) => {
|
|
7099
7306
|
try {
|
|
7100
7307
|
assertNotReserved(opts.key);
|
|
@@ -7153,148 +7360,35 @@ function getCommand(fs) {
|
|
|
7153
7360
|
});
|
|
7154
7361
|
}
|
|
7155
7362
|
function configCommand(fs = realConfigFs) {
|
|
7156
|
-
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand(fs)).addCommand(getCommand(fs));
|
|
7363
|
+
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand$1(fs)).addCommand(getCommand(fs));
|
|
7157
7364
|
}
|
|
7158
7365
|
//#endregion
|
|
7159
|
-
//#region src/governance/
|
|
7160
|
-
const
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
|
|
7166
|
-
}
|
|
7167
|
-
};
|
|
7168
|
-
//#endregion
|
|
7169
|
-
//#region src/governance/governance.ts
|
|
7170
|
-
function getManagedDir() {
|
|
7171
|
-
if (process.platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
|
|
7172
|
-
if (process.platform === "win32") return path.join(process.env["ProgramData"] ?? "C:\\ProgramData", "UniPlugin", "governances");
|
|
7173
|
-
return "/etc/universal-plugin/governances";
|
|
7174
|
-
}
|
|
7175
|
-
function getUserDir() {
|
|
7176
|
-
return path.join(os.homedir(), ".agents", "governances");
|
|
7177
|
-
}
|
|
7178
|
-
function getProjectDir(root) {
|
|
7179
|
-
return path.join(root, "governances");
|
|
7180
|
-
}
|
|
7181
|
-
function getLocalDir(root) {
|
|
7182
|
-
return path.join(root, ".agents", "governances");
|
|
7183
|
-
}
|
|
7184
|
-
function getPackageDir() {
|
|
7185
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
7186
|
-
return path.join(path.dirname(thisFile), "..", "governances");
|
|
7187
|
-
}
|
|
7188
|
-
function getScopedPaths(root) {
|
|
7189
|
-
return [
|
|
7190
|
-
{
|
|
7191
|
-
scope: "managed",
|
|
7192
|
-
dir: getManagedDir()
|
|
7193
|
-
},
|
|
7194
|
-
{
|
|
7195
|
-
scope: "project",
|
|
7196
|
-
dir: getProjectDir(root)
|
|
7197
|
-
},
|
|
7198
|
-
{
|
|
7199
|
-
scope: "local",
|
|
7200
|
-
dir: getLocalDir(root)
|
|
7201
|
-
},
|
|
7202
|
-
{
|
|
7203
|
-
scope: "user",
|
|
7204
|
-
dir: getUserDir()
|
|
7205
|
-
},
|
|
7206
|
-
{
|
|
7207
|
-
scope: "package",
|
|
7208
|
-
dir: getPackageDir()
|
|
7209
|
-
}
|
|
7210
|
-
];
|
|
7366
|
+
//#region src/governance/cli.ts
|
|
7367
|
+
const RETIRED = "universal-plugin governance is retired; buddy-agent-harness reference reads the same documents.\n";
|
|
7368
|
+
const IN_A_SKILL = "In a skill, load a document with the load-reference skill in the buddy-agent-harness plugin.\n";
|
|
7369
|
+
function retire(replacement) {
|
|
7370
|
+
process.stderr.write(`${RETIRED}${IN_A_SKILL}→ ${replacement}\n`);
|
|
7371
|
+
process.exit(1);
|
|
7211
7372
|
}
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
const
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
"project",
|
|
7220
|
-
"user"
|
|
7221
|
-
]) {
|
|
7222
|
-
let dir;
|
|
7223
|
-
if (scope === "managed") dir = getManagedDir();
|
|
7224
|
-
else if (scope === "project") dir = getProjectDir(root);
|
|
7225
|
-
else dir = getUserDir();
|
|
7226
|
-
const filePath = path.join(dir, pluginName, `${assetName}.md`);
|
|
7227
|
-
if (govFs.exists(filePath)) return {
|
|
7228
|
-
content: govFs.read(filePath),
|
|
7229
|
-
scope
|
|
7230
|
-
};
|
|
7231
|
-
}
|
|
7232
|
-
const entry = storeOpts.state.assets[pluginName];
|
|
7233
|
-
if (!entry) return null;
|
|
7234
|
-
const segment = path.join(entry.source, `${pluginName}@${entry.version}`);
|
|
7235
|
-
const storePath = path.join(storeOpts.globalStorePath, segment, "governances", `${assetName}.md`);
|
|
7236
|
-
if (govFs.exists(storePath)) return {
|
|
7237
|
-
content: govFs.read(storePath),
|
|
7238
|
-
scope: "store"
|
|
7239
|
-
};
|
|
7240
|
-
return null;
|
|
7241
|
-
}
|
|
7242
|
-
for (const { scope, dir } of getScopedPaths(root)) {
|
|
7243
|
-
const filePath = path.join(dir, `${name}.md`);
|
|
7244
|
-
if (govFs.exists(filePath)) return {
|
|
7245
|
-
content: govFs.read(filePath),
|
|
7246
|
-
scope
|
|
7247
|
-
};
|
|
7373
|
+
/** The flags `governance show` took that carried a value, so the value is not read as the name. */
|
|
7374
|
+
const VALUE_FLAGS = new Set(["--root", "--format"]);
|
|
7375
|
+
function documentName(args) {
|
|
7376
|
+
for (let i = 0; i < args.length; i++) {
|
|
7377
|
+
const arg = args[i];
|
|
7378
|
+
if (VALUE_FLAGS.has(arg)) i++;
|
|
7379
|
+
else if (!arg.startsWith("-")) return arg;
|
|
7248
7380
|
}
|
|
7249
|
-
return
|
|
7381
|
+
return "<name>";
|
|
7250
7382
|
}
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
seen.add(name);
|
|
7256
|
-
entries.push({
|
|
7257
|
-
name,
|
|
7258
|
-
scope
|
|
7259
|
-
});
|
|
7260
|
-
}
|
|
7261
|
-
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7262
|
-
}
|
|
7263
|
-
//#endregion
|
|
7264
|
-
//#region src/governance/cli.ts
|
|
7265
|
-
function readGlobalState() {
|
|
7266
|
-
const p = path.join(os.homedir(), ".agents", "universal-plugin.json");
|
|
7267
|
-
try {
|
|
7268
|
-
return mergeSafeState(JSON.parse(fs.readFileSync(p, "utf8")));
|
|
7269
|
-
} catch {
|
|
7270
|
-
return emptyState();
|
|
7271
|
-
}
|
|
7383
|
+
/** Takes any argument or flag, so a caller still passing `--root` or `--format json` is told where
|
|
7384
|
+
* to go instead of what it typed wrong. */
|
|
7385
|
+
function retiredCommand(name) {
|
|
7386
|
+
return new Command(name).allowUnknownOption().allowExcessArguments().helpOption(false);
|
|
7272
7387
|
}
|
|
7273
7388
|
function governanceCommand() {
|
|
7274
|
-
const cmd =
|
|
7275
|
-
cmd.
|
|
7276
|
-
|
|
7277
|
-
state: readGlobalState(),
|
|
7278
|
-
globalStorePath: globalStorePath()
|
|
7279
|
-
});
|
|
7280
|
-
if (!result) {
|
|
7281
|
-
process.stderr.write(`Governance "${name}" not found\n`);
|
|
7282
|
-
process.exit(1);
|
|
7283
|
-
}
|
|
7284
|
-
outputText(result, () => {
|
|
7285
|
-
process.stdout.write(result.content);
|
|
7286
|
-
});
|
|
7287
|
-
});
|
|
7288
|
-
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) => {
|
|
7289
|
-
const entries = listGovernances(resolveRoot(opts.root), realGovernanceFs);
|
|
7290
|
-
output(entries, {
|
|
7291
|
-
governances: entries.map((e) => ({
|
|
7292
|
-
name: e.name,
|
|
7293
|
-
scope: e.scope
|
|
7294
|
-
})),
|
|
7295
|
-
summary: `${entries.length} governances across ${new Set(entries.map((e) => e.scope)).size} scopes`
|
|
7296
|
-
});
|
|
7297
|
-
});
|
|
7389
|
+
const cmd = retiredCommand("governance").description("Retired: use buddy-agent-harness reference").helpOption("-h, --help", "Show help").helpCommand(false).addHelpText("after", "\nThis command is retired. buddy-agent-harness reference show|list|search reads the same documents.\nExample:\n $ buddy-agent-harness reference show plugin-design\n").action(() => retire("buddy-agent-harness reference list"));
|
|
7390
|
+
cmd.addCommand(retiredCommand("show").action((_opts, command) => retire(`buddy-agent-harness reference show ${documentName(command.args)}`)));
|
|
7391
|
+
cmd.addCommand(retiredCommand("list").action(() => retire("buddy-agent-harness reference list")));
|
|
7298
7392
|
return cmd;
|
|
7299
7393
|
}
|
|
7300
7394
|
//#endregion
|
|
@@ -7984,6 +8078,9 @@ function manifestOwner(manifest) {
|
|
|
7984
8078
|
return owner;
|
|
7985
8079
|
}
|
|
7986
8080
|
}
|
|
8081
|
+
/** The catalog's own identity: its name and its owner, from the root manifest unless overridden.
|
|
8082
|
+
* Exported because `marketplace add` creates a catalog in a repository that has no plugins of its
|
|
8083
|
+
* own to discover, and the two commands must name that catalog the same way. */
|
|
7987
8084
|
function deriveMetadata(root, fs, opts) {
|
|
7988
8085
|
const rootManifest = path.join(root, "plugin.json");
|
|
7989
8086
|
if (fs.exists(rootManifest)) assertContained(root, rootManifest, fs, "root plugin.json");
|
|
@@ -8044,7 +8141,7 @@ function writeArtifacts(fs, artifacts, root) {
|
|
|
8044
8141
|
const changed = artifacts.filter((artifact) => !sameArtifact(fs, path.join(root, artifact.path), artifact.content));
|
|
8045
8142
|
for (const artifact of changed) fs.writeAtomically(path.join(root, artifact.path), artifact.content);
|
|
8046
8143
|
}
|
|
8047
|
-
function selectedTargets(targets) {
|
|
8144
|
+
function selectedTargets$1(targets) {
|
|
8048
8145
|
return targets && targets.length > 0 ? [...new Set(targets)] : [
|
|
8049
8146
|
"claude",
|
|
8050
8147
|
"codex",
|
|
@@ -8052,6 +8149,9 @@ function selectedTargets(targets) {
|
|
|
8052
8149
|
"cursor"
|
|
8053
8150
|
];
|
|
8054
8151
|
}
|
|
8152
|
+
function readCatalog(fs, file) {
|
|
8153
|
+
return fs.exists(file) ? fs.read(file) : void 0;
|
|
8154
|
+
}
|
|
8055
8155
|
function sameArtifact(fs, file, content) {
|
|
8056
8156
|
if (!fs.exists(file)) return false;
|
|
8057
8157
|
const existing = fs.read(file);
|
|
@@ -8073,12 +8173,16 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
8073
8173
|
const root = path.resolve(rootInput);
|
|
8074
8174
|
const metadata = deriveMetadata(root, fs, opts);
|
|
8075
8175
|
const plugins = discoverPlugins(root, fs, opts.scanDirs);
|
|
8076
|
-
const
|
|
8176
|
+
const derived = selectedTargets$1(opts.targets).map((target) => ({
|
|
8077
8177
|
target,
|
|
8078
8178
|
artifacts: serializeTarget(target, metadata, plugins)
|
|
8079
8179
|
}));
|
|
8180
|
+
for (const { artifacts } of derived) for (const artifact of artifacts) assertContained(root, path.join(root, artifact.path), fs, "selected artifact");
|
|
8181
|
+
const planned = derived.map(({ target, artifacts }) => ({
|
|
8182
|
+
target,
|
|
8183
|
+
artifacts: artifacts.map((artifact) => mergeDiscoveredCatalog(artifact, readCatalog(fs, path.join(root, artifact.path))))
|
|
8184
|
+
}));
|
|
8080
8185
|
for (const { target, artifacts } of planned) for (const artifact of artifacts) {
|
|
8081
|
-
assertContained(root, path.join(root, artifact.path), fs, "selected artifact");
|
|
8082
8186
|
const issues = validateCatalogContent(target, artifact.content);
|
|
8083
8187
|
if (issues.length > 0) throw new Error(formatCatalogIssues(artifact.path, issues));
|
|
8084
8188
|
}
|
|
@@ -8101,13 +8205,403 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
8101
8205
|
target,
|
|
8102
8206
|
status: opts.dryRun ? "planned" : unchanged ? "unchanged" : "generated",
|
|
8103
8207
|
paths: artifacts.map((artifact) => artifact.path),
|
|
8104
|
-
plugins:
|
|
8208
|
+
plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))]
|
|
8105
8209
|
};
|
|
8106
8210
|
});
|
|
8107
8211
|
if (!opts.dryRun && plugins.length > 0) writeArtifacts(fs, planned.flatMap((entry) => entry.artifacts), root);
|
|
8108
8212
|
return results;
|
|
8109
8213
|
}
|
|
8110
8214
|
//#endregion
|
|
8215
|
+
//#region src/marketplace/spec.ts
|
|
8216
|
+
/** What a user names on the command line when they ask for a plugin to be listed, reduced to the
|
|
8217
|
+
* source a catalog entry states. Pure: every rule here is a decision about the string itself, so
|
|
8218
|
+
* nothing consults the filesystem, the network, or the runtime's configuration. */
|
|
8219
|
+
const GITHUB_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8220
|
+
const NPM_PACKAGE = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8221
|
+
/** The last meaningful segment of a path or URL, with a `.git` suffix removed. */
|
|
8222
|
+
function basename(value) {
|
|
8223
|
+
const segments = value.replace(/[/\\]+$/, "").split(/[/\\]/);
|
|
8224
|
+
return (segments[segments.length - 1] ?? value).replace(/\.git$/, "");
|
|
8225
|
+
}
|
|
8226
|
+
/** An npm package's entry name is the package name without its scope: `@cyberuni/upx` lists as
|
|
8227
|
+
* `upx`, which is what a user installs and what every other source form yields. */
|
|
8228
|
+
function packageEntryName(pkg) {
|
|
8229
|
+
return pkg.startsWith("@") ? pkg.split("/")[1] ?? pkg : pkg;
|
|
8230
|
+
}
|
|
8231
|
+
function pathSpec(value) {
|
|
8232
|
+
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
8233
|
+
const source = normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
|
|
8234
|
+
return {
|
|
8235
|
+
kind: "path",
|
|
8236
|
+
name: basename(normalized),
|
|
8237
|
+
source
|
|
8238
|
+
};
|
|
8239
|
+
}
|
|
8240
|
+
function npmSpec(pkg) {
|
|
8241
|
+
return {
|
|
8242
|
+
kind: "npm",
|
|
8243
|
+
name: packageEntryName(pkg),
|
|
8244
|
+
source: {
|
|
8245
|
+
source: "npm",
|
|
8246
|
+
package: pkg
|
|
8247
|
+
}
|
|
8248
|
+
};
|
|
8249
|
+
}
|
|
8250
|
+
function githubSpec(repo) {
|
|
8251
|
+
return {
|
|
8252
|
+
kind: "github",
|
|
8253
|
+
name: basename(repo),
|
|
8254
|
+
source: {
|
|
8255
|
+
source: "github",
|
|
8256
|
+
repo
|
|
8257
|
+
}
|
|
8258
|
+
};
|
|
8259
|
+
}
|
|
8260
|
+
function urlSpec(url) {
|
|
8261
|
+
return {
|
|
8262
|
+
kind: "url",
|
|
8263
|
+
name: basename(url),
|
|
8264
|
+
source: {
|
|
8265
|
+
source: "url",
|
|
8266
|
+
url
|
|
8267
|
+
}
|
|
8268
|
+
};
|
|
8269
|
+
}
|
|
8270
|
+
/** Splits `<plugin>@<marketplace>` at the separator that is not a scope marker, so a scoped package
|
|
8271
|
+
* name keeps its leading `@`. */
|
|
8272
|
+
function splitMarketplace(value) {
|
|
8273
|
+
const at = value.indexOf("@", value.startsWith("@") ? 1 : 0);
|
|
8274
|
+
if (at <= 0) return void 0;
|
|
8275
|
+
const plugin = value.slice(0, at);
|
|
8276
|
+
const marketplace = value.slice(at + 1);
|
|
8277
|
+
if (plugin === "" || marketplace === "" || marketplace.includes("/")) return void 0;
|
|
8278
|
+
return {
|
|
8279
|
+
plugin,
|
|
8280
|
+
marketplace
|
|
8281
|
+
};
|
|
8282
|
+
}
|
|
8283
|
+
function marketplaceSpec(value) {
|
|
8284
|
+
const split = splitMarketplace(value);
|
|
8285
|
+
if (!split) throw new Error(`error: "${value}" is not <plugin>@<marketplace>`);
|
|
8286
|
+
return {
|
|
8287
|
+
kind: "marketplace",
|
|
8288
|
+
name: packageEntryName(split.plugin),
|
|
8289
|
+
...split
|
|
8290
|
+
};
|
|
8291
|
+
}
|
|
8292
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
8293
|
+
/** A subdirectory as `git-subdir` states it: no leading `./`, no trailing slash. */
|
|
8294
|
+
function normalizeSubdir(subdir) {
|
|
8295
|
+
const path = subdir.trim().replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/+$/, "");
|
|
8296
|
+
if (path === "") throw new Error("error: --subdir must name a directory inside the repository");
|
|
8297
|
+
if (path.startsWith("/")) throw new Error(`error: --subdir "${subdir}" must be relative to the repository root`);
|
|
8298
|
+
return path;
|
|
8299
|
+
}
|
|
8300
|
+
/** The git URL a spec's source points at, for the two kinds that name a whole repository. */
|
|
8301
|
+
function repositoryUrl(spec) {
|
|
8302
|
+
if (typeof spec.source !== "object") return void 0;
|
|
8303
|
+
if (spec.source.source === "github") return originFromRepo(spec.source.repo).url;
|
|
8304
|
+
if (spec.source.source === "url") return spec.source.url;
|
|
8305
|
+
}
|
|
8306
|
+
/** Narrows a whole-repository source to the one directory inside it that is the plugin.
|
|
8307
|
+
*
|
|
8308
|
+
* A monorepo publishes several plugins from one repository, and neither `github` nor `url` can say
|
|
8309
|
+
* which directory. `git-subdir` is the form that can, so `--subdir` turns the source into one —
|
|
8310
|
+
* and the entry takes its name from that directory rather than from the repository. */
|
|
8311
|
+
function withSubdir(spec, subdir) {
|
|
8312
|
+
const url = repositoryUrl(spec);
|
|
8313
|
+
if (url === void 0) throw new Error(`error: --subdir applies to a repository source, not to a ${spec.kind} one`);
|
|
8314
|
+
const path = normalizeSubdir(subdir);
|
|
8315
|
+
return {
|
|
8316
|
+
kind: spec.kind,
|
|
8317
|
+
name: basename(path),
|
|
8318
|
+
source: {
|
|
8319
|
+
source: "git-subdir",
|
|
8320
|
+
url,
|
|
8321
|
+
path
|
|
8322
|
+
}
|
|
8323
|
+
};
|
|
8324
|
+
}
|
|
8325
|
+
/** Pins a source to a branch, tag, or commit. Only the git-backed forms carry one; an npm package is
|
|
8326
|
+
* pinned by version and a path is whatever is on disk. */
|
|
8327
|
+
function withPin(spec, ref, sha) {
|
|
8328
|
+
if (ref === void 0 && sha === void 0) return spec;
|
|
8329
|
+
const source = spec.source;
|
|
8330
|
+
if (typeof source !== "object" || ![
|
|
8331
|
+
"url",
|
|
8332
|
+
"github",
|
|
8333
|
+
"git-subdir"
|
|
8334
|
+
].includes(source.source)) throw new Error(`error: --ref and --sha apply to a git source, not to a ${spec.kind} one`);
|
|
8335
|
+
if (sha !== void 0 && !SHA.test(sha)) throw new Error(`error: --sha "${sha}" must be a full 40-character commit hash`);
|
|
8336
|
+
return {
|
|
8337
|
+
...spec,
|
|
8338
|
+
source: {
|
|
8339
|
+
...source,
|
|
8340
|
+
...ref === void 0 ? {} : { ref },
|
|
8341
|
+
...sha === void 0 ? {} : { sha }
|
|
8342
|
+
}
|
|
8343
|
+
};
|
|
8344
|
+
}
|
|
8345
|
+
function looksLikeUrl(value) {
|
|
8346
|
+
return value.includes("://") || value.startsWith("git@");
|
|
8347
|
+
}
|
|
8348
|
+
function looksLikePath(value) {
|
|
8349
|
+
return value.startsWith("./") || value.startsWith("../") || value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
8350
|
+
}
|
|
8351
|
+
/** Applies the kind the user named, so every guess below has an override. */
|
|
8352
|
+
function explicitSpec(value, kind) {
|
|
8353
|
+
switch (kind) {
|
|
8354
|
+
case "path": return pathSpec(value);
|
|
8355
|
+
case "npm": return npmSpec(value.replace(/^npm:/, ""));
|
|
8356
|
+
case "github": return githubSpec(value);
|
|
8357
|
+
case "url": return urlSpec(value);
|
|
8358
|
+
case "marketplace": return marketplaceSpec(value);
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
/** Reads a spec string as the source it names.
|
|
8362
|
+
*
|
|
8363
|
+
* The order below is the disambiguation, and every step of it is overridable with an explicit kind:
|
|
8364
|
+
*
|
|
8365
|
+
* 1. an `npm:` prefix, which is how a bare name that looks like anything else is forced to a package
|
|
8366
|
+
* 2. a URL scheme or an `scp`-style `git@host:path`
|
|
8367
|
+
* 3. a path, which has to say so with `./`, `../`, or a leading slash
|
|
8368
|
+
* 4. `<plugin>@<marketplace>`, splitting after a leading scope `@` so `@scope/pkg` is not one
|
|
8369
|
+
* 5. `owner/repo`
|
|
8370
|
+
* 6. anything else left, which is an npm package name
|
|
8371
|
+
*
|
|
8372
|
+
* Step 3 is why `plugins/alpha` reads as a GitHub repository rather than a directory: both are
|
|
8373
|
+
* `a/b`, and a guess that reached for the filesystem would answer differently depending on where
|
|
8374
|
+
* the command was run. `./plugins/alpha` or `--path` says it plainly. */
|
|
8375
|
+
function parsePluginSpec(value, opts = {}) {
|
|
8376
|
+
const { kind, subdir, ref, sha } = opts;
|
|
8377
|
+
const spec = value.trim();
|
|
8378
|
+
if (spec === "") throw new Error("error: a plugin spec is required");
|
|
8379
|
+
const shape = (base) => withPin(subdir === void 0 ? base : withSubdir(base, subdir), ref, sha);
|
|
8380
|
+
if (kind) return shape(explicitSpec(spec, kind));
|
|
8381
|
+
if (spec.startsWith("npm:")) {
|
|
8382
|
+
const pkg = spec.slice(4);
|
|
8383
|
+
if (!NPM_PACKAGE.test(pkg)) throw new Error(`error: "${pkg}" is not an npm package name`);
|
|
8384
|
+
return shape(npmSpec(pkg));
|
|
8385
|
+
}
|
|
8386
|
+
if (looksLikeUrl(spec)) return shape(urlSpec(spec));
|
|
8387
|
+
if (looksLikePath(spec)) return shape(pathSpec(spec));
|
|
8388
|
+
const split = splitMarketplace(spec);
|
|
8389
|
+
if (split) return shape({
|
|
8390
|
+
kind: "marketplace",
|
|
8391
|
+
name: packageEntryName(split.plugin),
|
|
8392
|
+
...split
|
|
8393
|
+
});
|
|
8394
|
+
if (GITHUB_REPO.test(spec)) return shape(githubSpec(spec));
|
|
8395
|
+
if (NPM_PACKAGE.test(spec)) return shape(npmSpec(spec));
|
|
8396
|
+
throw new Error(`error: cannot tell what "${spec}" names; pass --path, --npm, --github, --url, or --from-marketplace`);
|
|
8397
|
+
}
|
|
8398
|
+
//#endregion
|
|
8399
|
+
//#region src/marketplace/add.ts
|
|
8400
|
+
/** The manifest fields a catalog entry carries. Exactly the set the catalog already derives from a
|
|
8401
|
+
* discovered plugin, so an added entry and a discovered one say the same kinds of things. */
|
|
8402
|
+
const ENTRY_METADATA_FIELDS = [
|
|
8403
|
+
"description",
|
|
8404
|
+
"version",
|
|
8405
|
+
"homepage",
|
|
8406
|
+
"repository",
|
|
8407
|
+
"license",
|
|
8408
|
+
"keywords"
|
|
8409
|
+
];
|
|
8410
|
+
/** The kind name used to decide whether a runtime accepts a source, from the source itself. */
|
|
8411
|
+
function sourceKindOf(source) {
|
|
8412
|
+
if (isLocalCatalogSource(source)) return "path";
|
|
8413
|
+
return typeof source === "string" ? "path" : source.source;
|
|
8414
|
+
}
|
|
8415
|
+
/** A source rendered for the result table — one short cell, not a JSON blob. */
|
|
8416
|
+
function describeSource(source) {
|
|
8417
|
+
if (typeof source === "string") return source;
|
|
8418
|
+
const detail = source.package ?? source.repo ?? source.url ?? source.path;
|
|
8419
|
+
return typeof detail === "string" ? `${source.source}:${detail}` : String(source.source);
|
|
8420
|
+
}
|
|
8421
|
+
function readJsonFile(fs, file) {
|
|
8422
|
+
if (!fs.exists(file)) return void 0;
|
|
8423
|
+
try {
|
|
8424
|
+
const parsed = JSON.parse(fs.read(file));
|
|
8425
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
8426
|
+
} catch {
|
|
8427
|
+
return;
|
|
8428
|
+
}
|
|
8429
|
+
}
|
|
8430
|
+
/** Metadata already on this machine for the plugin being listed.
|
|
8431
|
+
*
|
|
8432
|
+
* A local path carries its own manifest, and an npm package that happens to be installed carries a
|
|
8433
|
+
* `package.json`. Neither is fetched: what is here is read, what is not here is left to the
|
|
8434
|
+
* metadata flags. An entry missing an optional field still installs. */
|
|
8435
|
+
function localMetadata(root, spec, fs) {
|
|
8436
|
+
if (spec.kind === "path" && typeof spec.source === "string") return readJsonFile(fs, path.join(root, spec.source, "plugin.json")) ?? {};
|
|
8437
|
+
if (spec.kind === "npm" && typeof spec.source === "object") {
|
|
8438
|
+
const pkg = spec.source.package;
|
|
8439
|
+
if (typeof pkg !== "string") return {};
|
|
8440
|
+
return readJsonFile(fs, path.join(root, "node_modules", pkg, "package.json")) ?? {};
|
|
8441
|
+
}
|
|
8442
|
+
return {};
|
|
8443
|
+
}
|
|
8444
|
+
/** The entry another marketplace already publishes for this plugin, copied rather than invented.
|
|
8445
|
+
*
|
|
8446
|
+
* The catalog schema has no "from another marketplace" source, so there is nothing to write until
|
|
8447
|
+
* that marketplace has been read. Its entry already names a source every runtime can resolve — that
|
|
8448
|
+
* is what makes it publishable — so the whole entry comes across. */
|
|
8449
|
+
function resolveFromMarketplace(spec, opts, fs) {
|
|
8450
|
+
const marketplace = spec.marketplace;
|
|
8451
|
+
const known = opts.from === void 0 ? resolveKnownMarketplace(marketplace, fs) : { dir: opts.from };
|
|
8452
|
+
if (known === void 0) throw new Error(`error: marketplace "${marketplace}" is not installed; add it in the runtime first, or pass --from <path>`);
|
|
8453
|
+
const dir = known.dir;
|
|
8454
|
+
const content = readMarketplaceCatalog(dir, fs);
|
|
8455
|
+
if (content === void 0) throw new Error(`error: no marketplace.json found under "${dir}"`);
|
|
8456
|
+
let catalog;
|
|
8457
|
+
try {
|
|
8458
|
+
catalog = JSON.parse(content);
|
|
8459
|
+
} catch {
|
|
8460
|
+
throw new Error(`error: the catalog in "${dir}" is not valid JSON`);
|
|
8461
|
+
}
|
|
8462
|
+
const found = (typeof catalog === "object" && catalog !== null && Array.isArray(catalog.plugins) ? catalog.plugins : []).find((entry) => typeof entry === "object" && entry !== null && entry.name === spec.plugin);
|
|
8463
|
+
if (!found) throw new Error(`error: marketplace "${marketplace}" lists no plugin "${spec.plugin}"`);
|
|
8464
|
+
const source = found.source;
|
|
8465
|
+
if (source === void 0) throw new Error(`error: the "${spec.plugin}" entry in "${marketplace}" names no source`);
|
|
8466
|
+
if (!isLocalCatalogSource(source)) return {
|
|
8467
|
+
source,
|
|
8468
|
+
metadata: found
|
|
8469
|
+
};
|
|
8470
|
+
const remote = gitRemoteUrl(dir);
|
|
8471
|
+
const origin = known.origin ?? (remote === void 0 ? void 0 : originFromUrl(remote));
|
|
8472
|
+
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`);
|
|
8473
|
+
const rewritten = absoluteSource(origin, source);
|
|
8474
|
+
if (rewritten === void 0) throw new Error(`error: the "${spec.plugin}" entry in "${marketplace}" names a source this command cannot read`);
|
|
8475
|
+
return {
|
|
8476
|
+
source: rewritten,
|
|
8477
|
+
metadata: found
|
|
8478
|
+
};
|
|
8479
|
+
}
|
|
8480
|
+
function selectedTargets(targets) {
|
|
8481
|
+
return targets && targets.length > 0 ? [...new Set(targets)] : [
|
|
8482
|
+
"claude",
|
|
8483
|
+
"codex",
|
|
8484
|
+
"copilot",
|
|
8485
|
+
"cursor"
|
|
8486
|
+
];
|
|
8487
|
+
}
|
|
8488
|
+
function entryMetadata(discovered, supplied = {}) {
|
|
8489
|
+
const metadata = {};
|
|
8490
|
+
for (const field of ENTRY_METADATA_FIELDS) {
|
|
8491
|
+
const value = supplied[field] ?? discovered[field];
|
|
8492
|
+
if (value !== void 0) metadata[field] = value;
|
|
8493
|
+
}
|
|
8494
|
+
return metadata;
|
|
8495
|
+
}
|
|
8496
|
+
function catalogEntry(content, name) {
|
|
8497
|
+
try {
|
|
8498
|
+
const parsed = JSON.parse(content);
|
|
8499
|
+
if (!Array.isArray(parsed.plugins)) return void 0;
|
|
8500
|
+
return parsed.plugins.find((entry) => typeof entry === "object" && entry !== null && entry.name === name);
|
|
8501
|
+
} catch {
|
|
8502
|
+
return;
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
/** Lists a plugin that lives somewhere else in this repository's catalogs.
|
|
8506
|
+
*
|
|
8507
|
+
* Where `marketplace init` derives a catalog from the plugins a repository holds, this adds one it
|
|
8508
|
+
* does not: an npm package, a GitHub repository, an entry another marketplace already publishes.
|
|
8509
|
+
* Together they let one repository be both — a plugin's own home and a curated list.
|
|
8510
|
+
*
|
|
8511
|
+
* Nothing is fetched and nothing is published. The command reads this repository, and at most a
|
|
8512
|
+
* marketplace already installed on this machine, and writes catalog files. */
|
|
8513
|
+
function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
|
|
8514
|
+
const root = path.resolve(rootInput);
|
|
8515
|
+
const parsed = parsePluginSpec(spec, {
|
|
8516
|
+
kind: opts.kind,
|
|
8517
|
+
subdir: opts.subdir,
|
|
8518
|
+
ref: opts.ref,
|
|
8519
|
+
sha: opts.sha
|
|
8520
|
+
});
|
|
8521
|
+
const name = opts.name ?? parsed.name;
|
|
8522
|
+
assertMarketplaceName(name, "plugin name");
|
|
8523
|
+
const resolved = parsed.kind === "marketplace" ? resolveFromMarketplace(parsed, opts, fs) : {
|
|
8524
|
+
source: parsed.source,
|
|
8525
|
+
metadata: localMetadata(root, parsed, fs)
|
|
8526
|
+
};
|
|
8527
|
+
const metadata = deriveMetadata(root, fs, {
|
|
8528
|
+
name: opts.marketplaceName,
|
|
8529
|
+
owner: opts.owner
|
|
8530
|
+
});
|
|
8531
|
+
const plugin = {
|
|
8532
|
+
name,
|
|
8533
|
+
source: resolved.source,
|
|
8534
|
+
metadata: entryMetadata(resolved.metadata, opts.metadata)
|
|
8535
|
+
};
|
|
8536
|
+
const kind = sourceKindOf(resolved.source);
|
|
8537
|
+
const rendered = describeSource(resolved.source);
|
|
8538
|
+
const planned = selectedTargets(opts.targets).map((target) => {
|
|
8539
|
+
const catalogPath = TARGET_CATALOG_PATHS[target];
|
|
8540
|
+
const file = path.join(root, catalogPath);
|
|
8541
|
+
const existing = fs.exists(file) ? fs.read(file) : void 0;
|
|
8542
|
+
if (!TARGET_SOURCE_KINDS[target].includes(kind)) return {
|
|
8543
|
+
target,
|
|
8544
|
+
catalogPath,
|
|
8545
|
+
file,
|
|
8546
|
+
existing,
|
|
8547
|
+
skipped: `${kind} source is not supported by ${target}`,
|
|
8548
|
+
artifact: void 0
|
|
8549
|
+
};
|
|
8550
|
+
return {
|
|
8551
|
+
target,
|
|
8552
|
+
catalogPath,
|
|
8553
|
+
file,
|
|
8554
|
+
existing,
|
|
8555
|
+
skipped: void 0,
|
|
8556
|
+
artifact: mergeCatalogEntry(target, metadata, plugin, () => existing, { keepForeignSource: false })
|
|
8557
|
+
};
|
|
8558
|
+
});
|
|
8559
|
+
for (const entry of planned) {
|
|
8560
|
+
if (!entry.artifact) continue;
|
|
8561
|
+
const issues = validateCatalogContent(entry.target, entry.artifact.content);
|
|
8562
|
+
if (issues.length > 0) throw new Error(formatCatalogIssues(entry.catalogPath, issues));
|
|
8563
|
+
}
|
|
8564
|
+
const conflicts = planned.filter((entry) => {
|
|
8565
|
+
if (!entry.artifact || entry.existing === void 0 || opts.force) return false;
|
|
8566
|
+
if (catalogEntry(entry.existing, name) === void 0) return false;
|
|
8567
|
+
return !sameCatalogContent(entry.existing, entry.artifact.content);
|
|
8568
|
+
}).map((entry) => entry.catalogPath);
|
|
8569
|
+
if (conflicts.length > 0) throw new Error(`error: "${name}" is already listed differently in ${conflicts.join(", ")}; rerun with --force to replace it`);
|
|
8570
|
+
const results = planned.map((entry) => {
|
|
8571
|
+
const row = {
|
|
8572
|
+
target: entry.target,
|
|
8573
|
+
entry: name,
|
|
8574
|
+
source: rendered,
|
|
8575
|
+
path: entry.catalogPath
|
|
8576
|
+
};
|
|
8577
|
+
if (entry.skipped || !entry.artifact) return {
|
|
8578
|
+
...row,
|
|
8579
|
+
status: "skipped",
|
|
8580
|
+
source: rendered,
|
|
8581
|
+
reason: entry.skipped
|
|
8582
|
+
};
|
|
8583
|
+
if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) return {
|
|
8584
|
+
...row,
|
|
8585
|
+
status: "unchanged"
|
|
8586
|
+
};
|
|
8587
|
+
if (opts.dryRun) return {
|
|
8588
|
+
...row,
|
|
8589
|
+
status: "planned"
|
|
8590
|
+
};
|
|
8591
|
+
const existed = entry.existing !== void 0 && catalogEntry(entry.existing, name) !== void 0;
|
|
8592
|
+
return {
|
|
8593
|
+
...row,
|
|
8594
|
+
status: existed ? "updated" : "added"
|
|
8595
|
+
};
|
|
8596
|
+
});
|
|
8597
|
+
if (!opts.dryRun) for (const entry of planned) {
|
|
8598
|
+
if (!entry.artifact) continue;
|
|
8599
|
+
if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) continue;
|
|
8600
|
+
fs.writeAtomically(entry.file, entry.artifact.content);
|
|
8601
|
+
}
|
|
8602
|
+
return results;
|
|
8603
|
+
}
|
|
8604
|
+
//#endregion
|
|
8111
8605
|
//#region src/marketplace/validate.ts
|
|
8112
8606
|
/** A `./`-prefixed source names a directory inside the repository, and Claude Code resolves it
|
|
8113
8607
|
* against the directory holding `.claude-plugin/`. A source pointing nowhere passes every schema
|
|
@@ -8178,6 +8672,65 @@ function targetsFromOptions(opts) {
|
|
|
8178
8672
|
].filter((target) => opts[target]);
|
|
8179
8673
|
return targets.length > 0 ? targets : void 0;
|
|
8180
8674
|
}
|
|
8675
|
+
/** The kind flags, as one map, so a second one is an error rather than a silent precedence rule. */
|
|
8676
|
+
const KIND_FLAGS = {
|
|
8677
|
+
path: "path",
|
|
8678
|
+
npm: "npm",
|
|
8679
|
+
github: "github",
|
|
8680
|
+
url: "url",
|
|
8681
|
+
fromMarketplace: "marketplace"
|
|
8682
|
+
};
|
|
8683
|
+
function kindFromOptions(opts) {
|
|
8684
|
+
const named = Object.keys(KIND_FLAGS).filter((flag) => opts[flag]);
|
|
8685
|
+
if (named.length > 1) throw new Error(`error: pass one source kind, not ${named.length}`);
|
|
8686
|
+
return named.length === 1 ? KIND_FLAGS[named[0]] : void 0;
|
|
8687
|
+
}
|
|
8688
|
+
function metadataFromOptions(opts) {
|
|
8689
|
+
const metadata = {};
|
|
8690
|
+
for (const field of ENTRY_METADATA_FIELDS) {
|
|
8691
|
+
const value = opts[field];
|
|
8692
|
+
if (typeof value !== "string") continue;
|
|
8693
|
+
metadata[field] = field === "keywords" ? value.split(",").map((keyword) => keyword.trim()).filter((keyword) => keyword !== "") : value;
|
|
8694
|
+
}
|
|
8695
|
+
return metadata;
|
|
8696
|
+
}
|
|
8697
|
+
function addCommand() {
|
|
8698
|
+
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) => {
|
|
8699
|
+
try {
|
|
8700
|
+
if (opts.format !== void 0 && opts.format !== "toon" && opts.format !== "json") throw new Error("error: --format must be \"toon\" or \"json\"");
|
|
8701
|
+
const results = addToMarketplace(resolveRoot(opts.root), spec, {
|
|
8702
|
+
targets: targetsFromOptions(opts),
|
|
8703
|
+
kind: kindFromOptions(opts),
|
|
8704
|
+
name: opts.name,
|
|
8705
|
+
from: opts.from,
|
|
8706
|
+
subdir: opts.subdir,
|
|
8707
|
+
ref: opts.ref,
|
|
8708
|
+
sha: opts.sha,
|
|
8709
|
+
metadata: metadataFromOptions(opts),
|
|
8710
|
+
marketplaceName: opts.marketplaceName,
|
|
8711
|
+
owner: opts.owner,
|
|
8712
|
+
dryRun: opts.dryRun,
|
|
8713
|
+
force: opts.force
|
|
8714
|
+
});
|
|
8715
|
+
output(results, {
|
|
8716
|
+
targets: results.map((row) => ({
|
|
8717
|
+
target: row.target,
|
|
8718
|
+
status: row.status,
|
|
8719
|
+
entry: row.entry,
|
|
8720
|
+
source: row.source,
|
|
8721
|
+
path: row.path,
|
|
8722
|
+
reason: row.reason ?? "-"
|
|
8723
|
+
})),
|
|
8724
|
+
summary: `${results.filter((row) => row.status === "skipped").length} skipped of ${results.length}`
|
|
8725
|
+
});
|
|
8726
|
+
for (const row of results.filter((row) => row.status === "skipped")) process.stderr.write(`skipped ${row.target}: ${row.reason}\n`);
|
|
8727
|
+
process.stderr.write(`${opts.dryRun ? "Planned" : "Wrote"} repository metadata only; no marketplace publication, registration, installation, authentication, or provisioning occurred.\n`);
|
|
8728
|
+
} catch (err) {
|
|
8729
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
8730
|
+
process.exitCode = 1;
|
|
8731
|
+
}
|
|
8732
|
+
});
|
|
8733
|
+
}
|
|
8181
8734
|
function initCommand() {
|
|
8182
8735
|
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) => {
|
|
8183
8736
|
try {
|
|
@@ -8235,7 +8788,7 @@ function validateCommand$1() {
|
|
|
8235
8788
|
});
|
|
8236
8789
|
}
|
|
8237
8790
|
function marketplaceCommand() {
|
|
8238
|
-
return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(initCommand()).addCommand(validateCommand$1());
|
|
8791
|
+
return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(addCommand()).addCommand(initCommand()).addCommand(validateCommand$1());
|
|
8239
8792
|
}
|
|
8240
8793
|
//#endregion
|
|
8241
8794
|
//#region src/prepare/fs.ts
|
|
@@ -8490,7 +9043,7 @@ const realSyncVersionFs = realJsonIo;
|
|
|
8490
9043
|
//#endregion
|
|
8491
9044
|
//#region src/publish/sync-version.ts
|
|
8492
9045
|
/** The changesets-driven direction of the version flow: the number is decided by
|
|
8493
|
-
* `changeset version` in `<packagePath>/package.json
|
|
9046
|
+
* `changeset version` in `<packagePath>/package.json` (the plugin root's when none is declared), and this copies it into the canonical
|
|
8494
9047
|
* manifest. `plugin version` is the other direction — the number decided here, flowing out to
|
|
8495
9048
|
* `package.json`. The two differ **only** in where the version comes from, so they share
|
|
8496
9049
|
* `applyVersionPlan` and cannot drift; `package.json` is the source here, never rewritten. */
|
|
@@ -8498,11 +9051,11 @@ function syncVersion(root, syncFs) {
|
|
|
8498
9051
|
const manifestPath = path.join(root, "plugin.json");
|
|
8499
9052
|
if (!syncFs.exists(manifestPath)) throw new Error(`No plugin.json found at ${root}`);
|
|
8500
9053
|
const agentsConfigPath = path.join(root, ".agents", "universal-plugin.json");
|
|
8501
|
-
const
|
|
8502
|
-
|
|
9054
|
+
const declared = getPackagePath(syncFs.exists(agentsConfigPath) ? JSON.parse(syncFs.read(agentsConfigPath)) : {});
|
|
9055
|
+
const packagePath = declared ?? ".";
|
|
8503
9056
|
const manifest = JSON.parse(syncFs.read(manifestPath));
|
|
8504
9057
|
const pkgJsonPath = path.join(root, packagePath, "package.json");
|
|
8505
|
-
if (!syncFs.exists(pkgJsonPath)) throw new Error(`No package.json found at ${packagePath}`);
|
|
9058
|
+
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}`);
|
|
8506
9059
|
const version = JSON.parse(syncFs.read(pkgJsonPath))["version"];
|
|
8507
9060
|
if (!version || typeof version !== "string") throw new Error(`No version found in ${packagePath}/package.json`);
|
|
8508
9061
|
const current = manifest["version"];
|
|
@@ -8529,7 +9082,7 @@ function syncVersion(root, syncFs) {
|
|
|
8529
9082
|
//#region src/publish/cli.ts
|
|
8530
9083
|
function publishCommand() {
|
|
8531
9084
|
const cmd = new Command("publish").description("Prepare plugin for publishing").helpCommand(false);
|
|
8532
|
-
cmd.command("sync-version").description("Sync version from packagePath/package.json into plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
9085
|
+
cmd.command("sync-version").description("Sync version from packagePath/package.json (default: the plugin root) into plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
8533
9086
|
try {
|
|
8534
9087
|
const result = syncVersion(resolveRoot(opts.root), realSyncVersionFs);
|
|
8535
9088
|
output(result, {
|