universal-plugin 0.15.0 → 0.16.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 +322 -47
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/adopt-upx/scripts/rewrite-upx.mjs +1 -1
- package/skills/build-plugin/SKILL.md +1 -1
- package/skills/build-plugin/scripts/build.mjs +1 -1
- package/skills/doctor-universal-plugin/SKILL.md +1 -1
- package/skills/doctor-universal-plugin/scripts/doctor.mjs +1 -1
- package/skills/init-universal-plugin/references/create.md +1 -1
- package/skills/init-universal-plugin/scripts/init.mjs +1 -1
- package/skills/marketplace/SKILL.md +5 -1
- package/skills/marketplace/references/add.md +21 -9
- package/skills/marketplace/references/validate.md +5 -0
- package/skills/marketplace/scripts/add.mjs +1 -1
- package/skills/marketplace/scripts/install-docs.mjs +123 -6
- package/skills/marketplace/scripts/marketplace.mjs +1 -1
- package/skills/marketplace/scripts/validate.mjs +1 -1
- package/skills/version/SKILL.md +1 -1
- package/skills/version/scripts/version.mjs +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
package/dist/cli.mjs
CHANGED
|
@@ -5232,14 +5232,19 @@ function absoluteSource(origin, source) {
|
|
|
5232
5232
|
path: subdir
|
|
5233
5233
|
};
|
|
5234
5234
|
}
|
|
5235
|
-
/** The source forms each runtime installs from
|
|
5235
|
+
/** The source forms each runtime installs from.
|
|
5236
5236
|
*
|
|
5237
5237
|
* Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
|
|
5238
5238
|
* cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
|
|
5239
5239
|
* cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
|
|
5240
|
-
* (<https://json.schemastore.org/claude-code-marketplace.json>).
|
|
5241
|
-
*
|
|
5242
|
-
*
|
|
5240
|
+
* (<https://json.schemastore.org/claude-code-marketplace.json>). The rest was found by running the
|
|
5241
|
+
* CLIs (`.research/local-marketplaces`, "Remote entry sources"):
|
|
5242
|
+
*
|
|
5243
|
+
* - Codex installs `npm`, `url` and `git-subdir`, and silently drops a `github` entry.
|
|
5244
|
+
* - Copilot CLI installs `github` (also with `path`) and `url`, and refuses the *whole*
|
|
5245
|
+
* marketplace over a single `npm` or `git-subdir` entry.
|
|
5246
|
+
* - Cursor's shipped bundle resolves `github`, `url` and `git-subdir`, all by git URL, and has no
|
|
5247
|
+
* `npm` source. Read from the bundle, not run. */
|
|
5243
5248
|
const TARGET_SOURCE_KINDS = {
|
|
5244
5249
|
claude: [
|
|
5245
5250
|
"path",
|
|
@@ -5248,10 +5253,164 @@ const TARGET_SOURCE_KINDS = {
|
|
|
5248
5253
|
"url",
|
|
5249
5254
|
"git-subdir"
|
|
5250
5255
|
],
|
|
5251
|
-
codex: [
|
|
5252
|
-
|
|
5253
|
-
|
|
5256
|
+
codex: [
|
|
5257
|
+
"path",
|
|
5258
|
+
"npm",
|
|
5259
|
+
"url",
|
|
5260
|
+
"git-subdir"
|
|
5261
|
+
],
|
|
5262
|
+
copilot: [
|
|
5263
|
+
"path",
|
|
5264
|
+
"github",
|
|
5265
|
+
"url"
|
|
5266
|
+
],
|
|
5267
|
+
cursor: [
|
|
5268
|
+
"path",
|
|
5269
|
+
"github",
|
|
5270
|
+
"url",
|
|
5271
|
+
"git-subdir"
|
|
5272
|
+
]
|
|
5254
5273
|
};
|
|
5274
|
+
/** The kind name used to decide whether a runtime accepts a source: a repository path, or the tag. */
|
|
5275
|
+
function sourceKindOf(source) {
|
|
5276
|
+
if (isLocalCatalogSource(source)) return "path";
|
|
5277
|
+
return typeof source === "string" ? "path" : source.source;
|
|
5278
|
+
}
|
|
5279
|
+
/** The runtimes that read `.claude-plugin/marketplace.json` when they find no catalog of their own
|
|
5280
|
+
* ahead of it. Copilot CLI looks in `.github/plugin/` before `.claude-plugin/` (verified by running
|
|
5281
|
+
* it against a repository carrying both). Cursor's bundle lists `.cursor-plugin/marketplace.json`
|
|
5282
|
+
* first (read, not run). Codex prefers `.agents/plugins/marketplace.json` (E-CODEX-M22). So a
|
|
5283
|
+
* catalog of the runtime's own shields it from whatever the Claude catalog holds. */
|
|
5284
|
+
const CLAUDE_CATALOG_READERS = [
|
|
5285
|
+
"codex",
|
|
5286
|
+
"copilot",
|
|
5287
|
+
"cursor"
|
|
5288
|
+
];
|
|
5289
|
+
function pickPins(source) {
|
|
5290
|
+
const pins = {};
|
|
5291
|
+
if (typeof source.ref === "string") pins.ref = source.ref;
|
|
5292
|
+
if (typeof source.sha === "string") pins.sha = source.sha;
|
|
5293
|
+
return pins;
|
|
5294
|
+
}
|
|
5295
|
+
/** A git URL ending in `.git`, which Cursor requires of `url` and `git-subdir`. Only a GitHub URL is
|
|
5296
|
+
* known to serve the same repository with the suffix, so any other host's URL is left as written. */
|
|
5297
|
+
function withGitSuffix(url) {
|
|
5298
|
+
if (/\.git$/.test(url)) return url;
|
|
5299
|
+
return GITHUB_URL.test(url) ? `${url.replace(/\/+$/, "")}.git` : void 0;
|
|
5300
|
+
}
|
|
5301
|
+
/** Turns a source into the closest one a runtime installs, or says why none exists.
|
|
5302
|
+
*
|
|
5303
|
+
* A mapping is made only where it is lossless: `github` becomes a `url` (or, with a `path`, a
|
|
5304
|
+
* `git-subdir`) on Codex, and a `git-subdir` on a GitHub URL becomes `github` + `path` on Copilot
|
|
5305
|
+
* CLI. `ref` and `sha` pins cross with it. Anything else the runtime cannot install is omitted,
|
|
5306
|
+
* because writing it would have the runtime drop the entry or refuse the whole catalog. */
|
|
5307
|
+
function adaptSource(target, source) {
|
|
5308
|
+
const kind = sourceKindOf(source);
|
|
5309
|
+
if (kind === "path" || typeof source === "string") return { source };
|
|
5310
|
+
const supported = TARGET_SOURCE_KINDS[target].includes(kind);
|
|
5311
|
+
if ((target === "codex" || target === "cursor" && typeof source.path === "string") && kind === "github" && typeof source.repo === "string") {
|
|
5312
|
+
const url = `https://github.com/${source.repo}.git`;
|
|
5313
|
+
return typeof source.path === "string" ? { source: {
|
|
5314
|
+
source: "git-subdir",
|
|
5315
|
+
url,
|
|
5316
|
+
path: source.path,
|
|
5317
|
+
...pickPins(source)
|
|
5318
|
+
} } : { source: {
|
|
5319
|
+
source: "url",
|
|
5320
|
+
url,
|
|
5321
|
+
...pickPins(source)
|
|
5322
|
+
} };
|
|
5323
|
+
}
|
|
5324
|
+
if (target === "copilot" && kind === "git-subdir" && typeof source.url === "string" && typeof source.path === "string") {
|
|
5325
|
+
const match = GITHUB_URL.exec(source.url.trim());
|
|
5326
|
+
if (match) return { source: {
|
|
5327
|
+
source: "github",
|
|
5328
|
+
repo: match[1],
|
|
5329
|
+
path: source.path,
|
|
5330
|
+
...pickPins(source)
|
|
5331
|
+
} };
|
|
5332
|
+
return { omitted: "git-subdir is installable only from a GitHub URL, which is mapped to github with a path" };
|
|
5333
|
+
}
|
|
5334
|
+
if (!supported) return { omitted: `${kind} source is not supported by ${target}` };
|
|
5335
|
+
if (target === "cursor" && (kind === "url" || kind === "git-subdir") && typeof source.url === "string") {
|
|
5336
|
+
const url = withGitSuffix(source.url);
|
|
5337
|
+
if (url === void 0) return { omitted: `cursor requires a ".git" URL for a ${kind} source` };
|
|
5338
|
+
return { source: url === source.url ? source : {
|
|
5339
|
+
...source,
|
|
5340
|
+
url
|
|
5341
|
+
} };
|
|
5342
|
+
}
|
|
5343
|
+
return { source };
|
|
5344
|
+
}
|
|
5345
|
+
/** Applies `adaptSource` to every entry of a serialized catalog: an entry is rewritten where a
|
|
5346
|
+
* mapping exists and removed where none does. The content comes back untouched when nothing
|
|
5347
|
+
* changed, so a catalog that needed no adapting is not reformatted. */
|
|
5348
|
+
function adaptCatalogContent(target, content) {
|
|
5349
|
+
let parsed;
|
|
5350
|
+
try {
|
|
5351
|
+
parsed = JSON.parse(content);
|
|
5352
|
+
} catch {
|
|
5353
|
+
return {
|
|
5354
|
+
content,
|
|
5355
|
+
omitted: []
|
|
5356
|
+
};
|
|
5357
|
+
}
|
|
5358
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.plugins)) return {
|
|
5359
|
+
content,
|
|
5360
|
+
omitted: []
|
|
5361
|
+
};
|
|
5362
|
+
const catalog = parsed;
|
|
5363
|
+
const omitted = [];
|
|
5364
|
+
let changed = false;
|
|
5365
|
+
const plugins = [];
|
|
5366
|
+
for (const entry of catalog.plugins) {
|
|
5367
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
5368
|
+
plugins.push(entry);
|
|
5369
|
+
continue;
|
|
5370
|
+
}
|
|
5371
|
+
const record = entry;
|
|
5372
|
+
const source = record.source;
|
|
5373
|
+
if (typeof source !== "string" && (typeof source !== "object" || source === null)) {
|
|
5374
|
+
plugins.push(entry);
|
|
5375
|
+
continue;
|
|
5376
|
+
}
|
|
5377
|
+
const adapted = adaptSource(target, source);
|
|
5378
|
+
if ("omitted" in adapted) {
|
|
5379
|
+
omitted.push({
|
|
5380
|
+
name: typeof record.name === "string" ? record.name : "(unnamed)",
|
|
5381
|
+
reason: adapted.omitted
|
|
5382
|
+
});
|
|
5383
|
+
changed = true;
|
|
5384
|
+
} else if (adapted.source !== source) {
|
|
5385
|
+
plugins.push({
|
|
5386
|
+
...record,
|
|
5387
|
+
source: adapted.source
|
|
5388
|
+
});
|
|
5389
|
+
changed = true;
|
|
5390
|
+
} else plugins.push(entry);
|
|
5391
|
+
}
|
|
5392
|
+
if (!changed) return {
|
|
5393
|
+
content,
|
|
5394
|
+
omitted
|
|
5395
|
+
};
|
|
5396
|
+
return {
|
|
5397
|
+
content: json({
|
|
5398
|
+
...catalog,
|
|
5399
|
+
plugins
|
|
5400
|
+
}),
|
|
5401
|
+
omitted
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
/** The catalog a runtime that also reads the Claude catalog should carry of its own, derived from
|
|
5405
|
+
* the Claude one: the same identity and entries, each adapted to what that runtime installs. */
|
|
5406
|
+
function deriveSharedCatalog(target, claudeContent) {
|
|
5407
|
+
const claude = parseCatalog(claudeContent, TARGET_CATALOG_PATHS.claude);
|
|
5408
|
+
const metadata = existingMetadata(claude);
|
|
5409
|
+
const base = JSON.parse(serializeTarget(target, metadata, [])[0]?.content ?? "{}");
|
|
5410
|
+
const adapted = adaptCatalogContent(target, json({ plugins: Array.isArray(claude.plugins) ? claude.plugins : [] }));
|
|
5411
|
+
base.plugins = JSON.parse(adapted.content).plugins;
|
|
5412
|
+
return json(base);
|
|
5413
|
+
}
|
|
5255
5414
|
/** Folds one plugin's entry into a catalog that may already exist, and returns the artifact to
|
|
5256
5415
|
* write. An existing catalog keeps its own top-level fields — its name, its owner, a description
|
|
5257
5416
|
* someone wrote — and every entry it lists for other plugins, in place. Only this plugin's entry is
|
|
@@ -5781,13 +5940,42 @@ function validateCodex(catalog) {
|
|
|
5781
5940
|
});
|
|
5782
5941
|
return issues;
|
|
5783
5942
|
}
|
|
5943
|
+
/** What a runtime does with a source kind it cannot install: Copilot CLI refuses the whole
|
|
5944
|
+
* catalog, Codex drops the entry without a word, Cursor skips it. All three leave the plugin
|
|
5945
|
+
* uninstallable, so each is an issue rather than a note
|
|
5946
|
+
* (`.research/local-marketplaces`, "Remote entry sources"). */
|
|
5947
|
+
const REJECTION = {
|
|
5948
|
+
copilot: "Copilot CLI refuses the whole catalog",
|
|
5949
|
+
codex: "Codex silently drops the entry",
|
|
5950
|
+
cursor: "Cursor skips the entry"
|
|
5951
|
+
};
|
|
5952
|
+
/** Entries whose source `target` cannot install, one issue each. A source `adaptSource` can map is
|
|
5953
|
+
* still an issue here: this checks what a catalog says, not what the generator could have written. */
|
|
5954
|
+
function sourceKindIssues(target, catalog) {
|
|
5955
|
+
if (!isObject$1(catalog) || !Array.isArray(catalog.plugins) || target === "claude") return [];
|
|
5956
|
+
const issues = [];
|
|
5957
|
+
catalog.plugins.forEach((entry, index) => {
|
|
5958
|
+
if (!isObject$1(entry)) return;
|
|
5959
|
+
const source = entry.source;
|
|
5960
|
+
if (typeof source !== "string" && !isObject$1(source)) return;
|
|
5961
|
+
const adapted = adaptSource(target, source);
|
|
5962
|
+
if ("source" in adapted && adapted.source === source) return;
|
|
5963
|
+
const kind = sourceKindOf(source);
|
|
5964
|
+
const fix = "source" in adapted ? ` — write ${JSON.stringify(adapted.source)}` : " — omit the entry";
|
|
5965
|
+
issues.push({
|
|
5966
|
+
path: `plugins[${index}].source`,
|
|
5967
|
+
message: `${kind} source is not installable by ${target}; ${REJECTION[target]}${fix}`
|
|
5968
|
+
});
|
|
5969
|
+
});
|
|
5970
|
+
return issues;
|
|
5971
|
+
}
|
|
5784
5972
|
/** Every issue a target's runtime would raise against this catalog, empty when it loads. */
|
|
5785
5973
|
function validateCatalog(target, catalog) {
|
|
5786
5974
|
if (!isObject$1(catalog)) return [{
|
|
5787
5975
|
path: "",
|
|
5788
5976
|
message: `catalog must be a JSON object, not ${typeName(catalog)}`
|
|
5789
5977
|
}];
|
|
5790
|
-
return target === "codex" ? validateCodex(catalog) : validateClaudeShaped(catalog);
|
|
5978
|
+
return [...target === "codex" ? validateCodex(catalog) : validateClaudeShaped(catalog), ...sourceKindIssues(target, catalog)];
|
|
5791
5979
|
}
|
|
5792
5980
|
/** The same check against a catalog still in its serialized form. Text that does not parse is one
|
|
5793
5981
|
* issue rather than a thrown error, so a caller checking four files reports all four. */
|
|
@@ -16572,19 +16760,64 @@ function normalizeJson(value) {
|
|
|
16572
16760
|
const record = value;
|
|
16573
16761
|
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, normalizeJson(record[key])]));
|
|
16574
16762
|
}
|
|
16763
|
+
/** The entries a runtime with no catalog of its own would have read from the Claude one, as the
|
|
16764
|
+
* starting point for its catalog. Codex states an entry its own way, so those are re-shaped. */
|
|
16765
|
+
function sharedStart(target, claudeContent) {
|
|
16766
|
+
if (claudeContent === void 0) return void 0;
|
|
16767
|
+
if (target !== "codex") return claudeContent;
|
|
16768
|
+
const plugins = JSON.parse(claudeContent).plugins ?? [];
|
|
16769
|
+
return JSON.stringify({ plugins: plugins.map((entry) => ({
|
|
16770
|
+
name: entry.name,
|
|
16771
|
+
...typeof entry.version === "string" ? { version: entry.version } : {},
|
|
16772
|
+
source: entry.source,
|
|
16773
|
+
policy: {
|
|
16774
|
+
installation: "AVAILABLE",
|
|
16775
|
+
authentication: "ON_INSTALL"
|
|
16776
|
+
},
|
|
16777
|
+
category: "Productivity"
|
|
16778
|
+
})) });
|
|
16779
|
+
}
|
|
16575
16780
|
function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
16576
16781
|
const root = path$1.resolve(rootInput);
|
|
16577
16782
|
const metadata = deriveMetadata(root, fs, opts);
|
|
16578
16783
|
const plugins = discoverPlugins(root, fs, opts.scanDirs);
|
|
16579
|
-
const
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16784
|
+
const requested = selectedTargets$1(opts.targets);
|
|
16785
|
+
const targets = requested.includes("claude") ? ["claude", ...requested.filter((t) => t !== "claude")] : requested;
|
|
16786
|
+
const derived = new Map(targets.map((target) => [target, serializeTarget(target, metadata, plugins)]));
|
|
16787
|
+
const readOwn = (artifact) => readCatalog(fs, path$1.join(root, artifact.path));
|
|
16788
|
+
const planTarget = (target, claudeContent) => {
|
|
16789
|
+
const omitted = [];
|
|
16790
|
+
return {
|
|
16791
|
+
target,
|
|
16792
|
+
artifacts: (derived.get(target) ?? []).map((artifact) => {
|
|
16793
|
+
assertContained(root, path$1.join(root, artifact.path), fs, "selected artifact");
|
|
16794
|
+
const shared = CLAUDE_CATALOG_READERS.includes(target) ? sharedStart(target, claudeContent) : void 0;
|
|
16795
|
+
const merged = mergeDiscoveredCatalog(artifact, readOwn(artifact) ?? shared);
|
|
16796
|
+
const adapted = adaptCatalogContent(target, merged.content);
|
|
16797
|
+
omitted.push(...adapted.omitted);
|
|
16798
|
+
return {
|
|
16799
|
+
path: merged.path,
|
|
16800
|
+
content: adapted.content
|
|
16801
|
+
};
|
|
16802
|
+
}),
|
|
16803
|
+
omitted
|
|
16804
|
+
};
|
|
16805
|
+
};
|
|
16806
|
+
const planned = [];
|
|
16807
|
+
let claudeContent;
|
|
16808
|
+
for (const target of targets) {
|
|
16809
|
+
const plan = planTarget(target, claudeContent);
|
|
16810
|
+
if (target === "claude") claudeContent = plan.artifacts[0]?.content;
|
|
16811
|
+
planned.push(plan);
|
|
16812
|
+
}
|
|
16813
|
+
const shielded = /* @__PURE__ */ new Set();
|
|
16814
|
+
if (claudeContent !== void 0) for (const target of ["copilot", "cursor"]) {
|
|
16815
|
+
if (targets.includes(target)) continue;
|
|
16816
|
+
if (adaptCatalogContent(target, claudeContent).omitted.length === 0) continue;
|
|
16817
|
+
derived.set(target, serializeTarget(target, metadata, plugins));
|
|
16818
|
+
planned.push(planTarget(target, claudeContent));
|
|
16819
|
+
shielded.add(target);
|
|
16820
|
+
}
|
|
16588
16821
|
for (const { target, artifacts } of planned) for (const artifact of artifacts) {
|
|
16589
16822
|
const issues = validateCatalogContent(target, artifact.content);
|
|
16590
16823
|
if (issues.length > 0) throw new Error(formatCatalogIssues(artifact.path, issues));
|
|
@@ -16595,7 +16828,7 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
16595
16828
|
if (fs.exists(output) && !sameArtifact(fs, output, artifact.content) && !opts.force) conflicts.push(artifact.path);
|
|
16596
16829
|
}
|
|
16597
16830
|
if (conflicts.length > 0) throw new Error(`error: generated artifact differs: ${conflicts.join(", ")}; rerun with --force to replace selected artifacts`);
|
|
16598
|
-
const results = planned.map(({ target, artifacts }) => {
|
|
16831
|
+
const results = planned.map(({ target, artifacts, omitted }) => {
|
|
16599
16832
|
if (plugins.length === 0) return {
|
|
16600
16833
|
target,
|
|
16601
16834
|
status: "empty",
|
|
@@ -16608,7 +16841,9 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
16608
16841
|
target,
|
|
16609
16842
|
status: opts.dryRun ? "planned" : unchanged ? "unchanged" : "generated",
|
|
16610
16843
|
paths: artifacts.map((artifact) => artifact.path),
|
|
16611
|
-
plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))]
|
|
16844
|
+
plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))],
|
|
16845
|
+
...omitted.length > 0 ? { omitted } : {},
|
|
16846
|
+
...shielded.has(target) ? { reason: `written so ${target} does not read ${TARGET_CATALOG_PATHS.claude}, which lists entries it cannot install` } : {}
|
|
16612
16847
|
};
|
|
16613
16848
|
});
|
|
16614
16849
|
if (!opts.dryRun && plugins.length > 0) writeArtifacts(fs, planned.flatMap((entry) => entry.artifacts), root);
|
|
@@ -16810,11 +17045,6 @@ const ENTRY_METADATA_FIELDS = [
|
|
|
16810
17045
|
"license",
|
|
16811
17046
|
"keywords"
|
|
16812
17047
|
];
|
|
16813
|
-
/** The kind name used to decide whether a runtime accepts a source, from the source itself. */
|
|
16814
|
-
function sourceKindOf(source) {
|
|
16815
|
-
if (isLocalCatalogSource(source)) return "path";
|
|
16816
|
-
return typeof source === "string" ? "path" : source.source;
|
|
16817
|
-
}
|
|
16818
17048
|
/** A source rendered for the result table — one short cell, not a JSON blob. */
|
|
16819
17049
|
function describeSource(source) {
|
|
16820
17050
|
if (typeof source === "string") return source;
|
|
@@ -16936,27 +17166,46 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
|
|
|
16936
17166
|
source: resolved.source,
|
|
16937
17167
|
metadata: entryMetadata(resolved.metadata, opts.metadata)
|
|
16938
17168
|
};
|
|
16939
|
-
const kind = sourceKindOf(resolved.source);
|
|
16940
17169
|
const rendered = describeSource(resolved.source);
|
|
16941
|
-
const
|
|
17170
|
+
const targets = selectedTargets(opts.targets);
|
|
17171
|
+
const readCatalogFile = (target) => {
|
|
17172
|
+
const file = path$1.join(root, TARGET_CATALOG_PATHS[target]);
|
|
17173
|
+
return fs.exists(file) ? fs.read(file) : void 0;
|
|
17174
|
+
};
|
|
17175
|
+
const claudeAfter = targets.includes("claude") ? mergeCatalogEntry("claude", metadata, plugin, () => readCatalogFile("claude"), { keepForeignSource: false }).content : readCatalogFile("claude");
|
|
17176
|
+
const planned = targets.map((target) => {
|
|
16942
17177
|
const catalogPath = TARGET_CATALOG_PATHS[target];
|
|
16943
17178
|
const file = path$1.join(root, catalogPath);
|
|
16944
|
-
const existing =
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
catalogPath
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
17179
|
+
const existing = readCatalogFile(target);
|
|
17180
|
+
const adapted = adaptSource(target, resolved.source);
|
|
17181
|
+
if ("omitted" in adapted) {
|
|
17182
|
+
const reason = `${adapted.omitted}; "${name}" is left out of ${catalogPath}`;
|
|
17183
|
+
const shielded = existing === void 0 && CLAUDE_CATALOG_READERS.includes(target) && target !== "codex" && claudeAfter !== void 0 ? {
|
|
17184
|
+
path: catalogPath,
|
|
17185
|
+
content: deriveSharedCatalog(target, claudeAfter)
|
|
17186
|
+
} : void 0;
|
|
17187
|
+
return {
|
|
17188
|
+
target,
|
|
17189
|
+
catalogPath,
|
|
17190
|
+
file,
|
|
17191
|
+
existing,
|
|
17192
|
+
skipped: shielded ? `${reason}; wrote the catalog without it so ${target} does not read the Claude one` : reason,
|
|
17193
|
+
artifact: shielded
|
|
17194
|
+
};
|
|
17195
|
+
}
|
|
17196
|
+
const changed = adapted.source !== resolved.source;
|
|
17197
|
+
const base = existing ?? (CLAUDE_CATALOG_READERS.includes(target) && target !== "codex" && claudeAfter !== void 0 && adaptCatalogContent(target, claudeAfter).omitted.length > 0 ? deriveSharedCatalog(target, claudeAfter) : void 0);
|
|
16953
17198
|
return {
|
|
16954
17199
|
target,
|
|
16955
17200
|
catalogPath,
|
|
16956
17201
|
file,
|
|
16957
17202
|
existing,
|
|
16958
17203
|
skipped: void 0,
|
|
16959
|
-
artifact: mergeCatalogEntry(target, metadata,
|
|
17204
|
+
artifact: mergeCatalogEntry(target, metadata, {
|
|
17205
|
+
...plugin,
|
|
17206
|
+
source: adapted.source
|
|
17207
|
+
}, () => base, { keepForeignSource: false }),
|
|
17208
|
+
mapped: changed ? `${sourceKindOf(resolved.source)} source written as ${describeSource(adapted.source)}` : void 0
|
|
16960
17209
|
};
|
|
16961
17210
|
});
|
|
16962
17211
|
for (const entry of planned) {
|
|
@@ -16977,23 +17226,30 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
|
|
|
16977
17226
|
source: rendered,
|
|
16978
17227
|
path: entry.catalogPath
|
|
16979
17228
|
};
|
|
16980
|
-
if (entry.skipped
|
|
17229
|
+
if (entry.skipped) return {
|
|
16981
17230
|
...row,
|
|
16982
17231
|
status: "skipped",
|
|
16983
|
-
source: rendered,
|
|
16984
17232
|
reason: entry.skipped
|
|
16985
17233
|
};
|
|
17234
|
+
if (!entry.artifact) return {
|
|
17235
|
+
...row,
|
|
17236
|
+
status: "skipped"
|
|
17237
|
+
};
|
|
17238
|
+
const mapped = "mapped" in entry ? { reason: entry.mapped } : {};
|
|
16986
17239
|
if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) return {
|
|
16987
17240
|
...row,
|
|
17241
|
+
...mapped,
|
|
16988
17242
|
status: "unchanged"
|
|
16989
17243
|
};
|
|
16990
17244
|
if (opts.dryRun) return {
|
|
16991
17245
|
...row,
|
|
17246
|
+
...mapped,
|
|
16992
17247
|
status: "planned"
|
|
16993
17248
|
};
|
|
16994
17249
|
const existed = entry.existing !== void 0 && catalogEntry(entry.existing, name) !== void 0;
|
|
16995
17250
|
return {
|
|
16996
17251
|
...row,
|
|
17252
|
+
...mapped,
|
|
16997
17253
|
status: existed ? "updated" : "added"
|
|
16998
17254
|
};
|
|
16999
17255
|
});
|
|
@@ -17090,15 +17346,30 @@ function validateMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
|
|
|
17090
17346
|
]).map((target) => {
|
|
17091
17347
|
const relative = TARGET_CATALOG_PATHS[target];
|
|
17092
17348
|
const file = path$1.join(root, relative);
|
|
17093
|
-
if (!fs.exists(file))
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17349
|
+
if (!fs.exists(file)) {
|
|
17350
|
+
const claudeFile = path$1.join(root, TARGET_CATALOG_PATHS.claude);
|
|
17351
|
+
if (CLAUDE_CATALOG_READERS.includes(target) && fs.exists(claudeFile)) {
|
|
17352
|
+
const shared = parseAndCheck(fs.read(claudeFile), (parsed) => sourceKindIssues(target, parsed)).map((issue) => ({
|
|
17353
|
+
path: issue.path,
|
|
17354
|
+
message: `${issue.message} (read from ${TARGET_CATALOG_PATHS.claude}; write ${relative} to shield ${target} from it)`
|
|
17355
|
+
}));
|
|
17356
|
+
if (shared.length > 0) return {
|
|
17357
|
+
target,
|
|
17358
|
+
path: TARGET_CATALOG_PATHS.claude,
|
|
17359
|
+
status: "invalid",
|
|
17360
|
+
issues: shared
|
|
17361
|
+
};
|
|
17362
|
+
}
|
|
17363
|
+
return {
|
|
17364
|
+
target,
|
|
17365
|
+
path: relative,
|
|
17366
|
+
status: opts.required ? "invalid" : "missing",
|
|
17367
|
+
issues: opts.required ? [{
|
|
17368
|
+
path: "",
|
|
17369
|
+
message: "no catalog at this path"
|
|
17370
|
+
}] : []
|
|
17371
|
+
};
|
|
17372
|
+
}
|
|
17102
17373
|
const content = fs.read(file);
|
|
17103
17374
|
const issues = [...validateCatalogContent(target, content), ...parseAndCheck(content, (parsed) => [...checkSources(root, fs, parsed), ...target === "claude" ? checkDependencies(root, fs, parsed, content, relative) : []])];
|
|
17104
17375
|
return {
|
|
@@ -17209,6 +17480,10 @@ function initCommand() {
|
|
|
17209
17480
|
})),
|
|
17210
17481
|
summary: `${results.length} targets`
|
|
17211
17482
|
});
|
|
17483
|
+
for (const row of results) {
|
|
17484
|
+
for (const entry of row.omitted ?? []) process.stderr.write(`warning: omitted "${entry.name}" from ${row.target}: ${entry.reason}\n`);
|
|
17485
|
+
if (row.reason) process.stderr.write(`${row.target}: ${row.reason}\n`);
|
|
17486
|
+
}
|
|
17212
17487
|
process.stderr.write("Generated repository metadata only; no marketplace publication, registration, installation, authentication, or provisioning occurred.\n");
|
|
17213
17488
|
} catch (err) {
|
|
17214
17489
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "universal-plugin",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/rewrite-upx.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
//#region src/skill-scripts/rewrite-upx.ts
|
|
@@ -47,7 +47,7 @@ node scripts/build.mjs --root <plugin-root>
|
|
|
47
47
|
|
|
48
48
|
Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI the project installed
|
|
49
49
|
(`pnpm exec universal-plugin`), else the CLI pinned to the version this skill shipped with
|
|
50
|
-
(`npx universal-plugin@0.
|
|
50
|
+
(`npx universal-plugin@0.16.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
|
|
51
51
|
must name the directory that holds the canonical `plugin.json`.
|
|
52
52
|
|
|
53
53
|
The command never prompts, so it is safe to run unattended.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
|
@@ -172,7 +172,7 @@ it does not, offer to add it — ask first, because both are the user's files:
|
|
|
172
172
|
semantic-release bot opens.
|
|
173
173
|
|
|
174
174
|
Use the locked `universal-plugin` from the repository's devDependencies when it has one; otherwise
|
|
175
|
-
`npx universal-plugin@0.
|
|
175
|
+
`npx universal-plugin@0.16.0`, pinned. This check matters most where the release tool picks the
|
|
176
176
|
number, because nothing else runs between that bump and publish.
|
|
177
177
|
|
|
178
178
|
## Unreleased content
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/doctor.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/doctor-universal-plugin/scripts/doctor.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
@@ -43,7 +43,7 @@ node scripts/init.mjs --name <plugin-name> --vendor claude-code --vendor cursor
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Resolve `scripts/init.mjs` against this skill's directory; it runs the project's installed CLI, else
|
|
46
|
-
the pinned `npx universal-plugin@0.
|
|
46
|
+
the pinned `npx universal-plugin@0.16.0`. Add `--npm` when an npm package ships the plugin,
|
|
47
47
|
which also wires that `package.json`'s `files` to carry the derived manifests, copies its `version`
|
|
48
48
|
into the manifest, and, in a changesets repository, wires the root `version` script to run
|
|
49
49
|
`plugin version --from-package` (SKILL.md Phase 3 asks before that rewrite).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/init-universal-plugin/scripts/init.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
|
@@ -56,7 +56,11 @@ disk is usually `add`.
|
|
|
56
56
|
| Cursor | `.cursor-plugin/marketplace.json` | nothing; an admin imports the repository as a team marketplace |
|
|
57
57
|
|
|
58
58
|
Three of the four read `.claude-plugin/marketplace.json`, so one file covers Claude Code, Codex, and
|
|
59
|
-
Copilot CLI.
|
|
59
|
+
Copilot CLI. Not every plugin in it installs everywhere: a runtime installs only the source kinds it
|
|
60
|
+
supports (`references/add.md` has the table), so a catalog can list plugins a runtime cannot install.
|
|
61
|
+
Copilot CLI refuses the whole catalog over one `npm` or `git-subdir` entry. The generator therefore
|
|
62
|
+
writes each runtime's own catalog with only what that runtime installs, and Copilot CLI and Cursor read
|
|
63
|
+
their own catalog before the Claude one, which shields them. State per runtime which plugins install. The reverse does not hold: Claude Code rejects the Codex catalog for its missing
|
|
60
64
|
`owner`.
|
|
61
65
|
|
|
62
66
|
## Rules for every route
|
|
@@ -47,20 +47,32 @@ must be the full 40-character hash, which the schema requires.
|
|
|
47
47
|
|
|
48
48
|
## 2. Choose targets, and know what will be skipped
|
|
49
49
|
|
|
50
|
-
Not every runtime installs from every source.
|
|
50
|
+
Not every runtime installs from every source. Found by running the CLIs, not from the schema:
|
|
51
51
|
|
|
52
52
|
| Source | Claude Code | Codex | Copilot CLI | Cursor |
|
|
53
53
|
| --- | --- | --- | --- | --- |
|
|
54
54
|
| path | yes | yes | yes | yes |
|
|
55
55
|
| npm | yes | yes | — | — |
|
|
56
|
-
| github
|
|
56
|
+
| github | yes | written as `url` | yes (also with `path`) | yes; with `path`, written as `git-subdir` |
|
|
57
|
+
| url | yes | yes | yes | yes, `.git` URL |
|
|
58
|
+
| git-subdir | yes | yes | on a GitHub URL, written as `github` + `path`; otherwise — | yes, `.git` URL |
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
else's terminal. Say which targets were skipped and why — do not report "added" and leave the user
|
|
61
|
-
to discover that two catalogs have no entry.
|
|
60
|
+
Cursor's row is read from its shipped bundle, not run. Where the table says "written as", the command
|
|
61
|
+
rewrites the source for that catalog and keeps `ref` and `sha`; nothing is lost.
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
A target that still cannot resolve the source is reported `skipped` with the reason, naming the entry
|
|
64
|
+
and the runtime, and the entry is not written to that catalog. That is deliberate: Copilot CLI refuses
|
|
65
|
+
the **whole** catalog over one `npm` or `git-subdir` entry, and Codex drops a `github` entry without a
|
|
66
|
+
word. Say which targets were skipped and why — do not report "added" and leave the user to discover
|
|
67
|
+
that two catalogs have no entry.
|
|
68
|
+
|
|
69
|
+
Copilot CLI and Cursor also read `.claude-plugin/marketplace.json` when they have no catalog of their
|
|
70
|
+
own. So a skip for either one writes its catalog anyway, derived from the Claude one without the entry:
|
|
71
|
+
a catalog of its own is read first and shields it. Tell the user those two files appeared and why.
|
|
72
|
+
|
|
73
|
+
With no target flags all four are selected, so skips are the normal case for an npm package.
|
|
74
|
+
Plugins that only npm can deliver reach Copilot CLI and Cursor only if they are vendored into this
|
|
75
|
+
repository as a `./` path, which this command does not do.
|
|
64
76
|
|
|
65
77
|
## 3. Supply the metadata
|
|
66
78
|
|
|
@@ -127,8 +139,8 @@ own git remote — which is also how `--from` resolves one. A marketplace with n
|
|
|
127
139
|
there is no URL to rewrite against, and a bare path would name a directory this repository does not
|
|
128
140
|
have.
|
|
129
141
|
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
A subdirectory plugin reaches Codex and Cursor as `git-subdir`, and Copilot CLI as `github` + `path`
|
|
143
|
+
when the repository is on GitHub; on another host Copilot CLI is skipped.
|
|
132
144
|
|
|
133
145
|
## 6. Validate
|
|
134
146
|
|
|
@@ -26,6 +26,11 @@ error: catalog ".claude-plugin/marketplace.json" does not match the marketplace
|
|
|
26
26
|
| `invalid` | the runtime would refuse it; the issues say why |
|
|
27
27
|
| `missing` | no catalog at that path, which is only a failure under `--required` |
|
|
28
28
|
|
|
29
|
+
A source the runtime cannot install is an issue too: `npm` or `git-subdir` for Copilot CLI (it refuses
|
|
30
|
+
the whole catalog), `github` for Codex (silently dropped), `npm` for Cursor. A runtime with no catalog
|
|
31
|
+
of its own is checked against `.claude-plugin/marketplace.json`, because that is the file it reads;
|
|
32
|
+
the fix is to run `init` or `add` so the runtime gets a catalog of its own.
|
|
33
|
+
|
|
29
34
|
## Fixing what it reports
|
|
30
35
|
|
|
31
36
|
Fix an issue in the source it comes from, then regenerate. An entry's fields are derived, so an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/marketplace/scripts/add.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
|
@@ -1,9 +1,121 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/install-docs.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/marketplace/scripts/install-docs.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
//#region src/marketplace/marketplace.ts
|
|
8
|
+
/** Whether a catalog entry's `source` names a place inside this repository — a `./`-prefixed path in
|
|
9
|
+
* the Claude-shaped catalogs, or Codex's `{ source: "local", path }`. Every other form names a
|
|
10
|
+
* plugin distributed from somewhere else: an npm package, a GitHub repository, a URL. Discovery
|
|
11
|
+
* walks directories, so it can produce the first kind and can say nothing about the second. */
|
|
12
|
+
function isLocalCatalogSource(source) {
|
|
13
|
+
if (typeof source === "string") return true;
|
|
14
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return false;
|
|
15
|
+
return source.source === "local";
|
|
16
|
+
}
|
|
17
|
+
const GITHUB_URL = /^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:)([^/]+\/[^/]+?)(?:\.git)?\/?$/;
|
|
18
|
+
/** The source forms each runtime installs from.
|
|
19
|
+
*
|
|
20
|
+
* Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
|
|
21
|
+
* cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
|
|
22
|
+
* cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
|
|
23
|
+
* (<https://json.schemastore.org/claude-code-marketplace.json>). The rest was found by running the
|
|
24
|
+
* CLIs (`.research/local-marketplaces`, "Remote entry sources"):
|
|
25
|
+
*
|
|
26
|
+
* - Codex installs `npm`, `url` and `git-subdir`, and silently drops a `github` entry.
|
|
27
|
+
* - Copilot CLI installs `github` (also with `path`) and `url`, and refuses the *whole*
|
|
28
|
+
* marketplace over a single `npm` or `git-subdir` entry.
|
|
29
|
+
* - Cursor's shipped bundle resolves `github`, `url` and `git-subdir`, all by git URL, and has no
|
|
30
|
+
* `npm` source. Read from the bundle, not run. */
|
|
31
|
+
const TARGET_SOURCE_KINDS = {
|
|
32
|
+
claude: [
|
|
33
|
+
"path",
|
|
34
|
+
"npm",
|
|
35
|
+
"github",
|
|
36
|
+
"url",
|
|
37
|
+
"git-subdir"
|
|
38
|
+
],
|
|
39
|
+
codex: [
|
|
40
|
+
"path",
|
|
41
|
+
"npm",
|
|
42
|
+
"url",
|
|
43
|
+
"git-subdir"
|
|
44
|
+
],
|
|
45
|
+
copilot: [
|
|
46
|
+
"path",
|
|
47
|
+
"github",
|
|
48
|
+
"url"
|
|
49
|
+
],
|
|
50
|
+
cursor: [
|
|
51
|
+
"path",
|
|
52
|
+
"github",
|
|
53
|
+
"url",
|
|
54
|
+
"git-subdir"
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
/** The kind name used to decide whether a runtime accepts a source: a repository path, or the tag. */
|
|
58
|
+
function sourceKindOf(source) {
|
|
59
|
+
if (isLocalCatalogSource(source)) return "path";
|
|
60
|
+
return typeof source === "string" ? "path" : source.source;
|
|
61
|
+
}
|
|
62
|
+
function pickPins(source) {
|
|
63
|
+
const pins = {};
|
|
64
|
+
if (typeof source.ref === "string") pins.ref = source.ref;
|
|
65
|
+
if (typeof source.sha === "string") pins.sha = source.sha;
|
|
66
|
+
return pins;
|
|
67
|
+
}
|
|
68
|
+
/** A git URL ending in `.git`, which Cursor requires of `url` and `git-subdir`. Only a GitHub URL is
|
|
69
|
+
* known to serve the same repository with the suffix, so any other host's URL is left as written. */
|
|
70
|
+
function withGitSuffix(url) {
|
|
71
|
+
if (/\.git$/.test(url)) return url;
|
|
72
|
+
return GITHUB_URL.test(url) ? `${url.replace(/\/+$/, "")}.git` : void 0;
|
|
73
|
+
}
|
|
74
|
+
/** Turns a source into the closest one a runtime installs, or says why none exists.
|
|
75
|
+
*
|
|
76
|
+
* A mapping is made only where it is lossless: `github` becomes a `url` (or, with a `path`, a
|
|
77
|
+
* `git-subdir`) on Codex, and a `git-subdir` on a GitHub URL becomes `github` + `path` on Copilot
|
|
78
|
+
* CLI. `ref` and `sha` pins cross with it. Anything else the runtime cannot install is omitted,
|
|
79
|
+
* because writing it would have the runtime drop the entry or refuse the whole catalog. */
|
|
80
|
+
function adaptSource(target, source) {
|
|
81
|
+
const kind = sourceKindOf(source);
|
|
82
|
+
if (kind === "path" || typeof source === "string") return { source };
|
|
83
|
+
const supported = TARGET_SOURCE_KINDS[target].includes(kind);
|
|
84
|
+
if ((target === "codex" || target === "cursor" && typeof source.path === "string") && kind === "github" && typeof source.repo === "string") {
|
|
85
|
+
const url = `https://github.com/${source.repo}.git`;
|
|
86
|
+
return typeof source.path === "string" ? { source: {
|
|
87
|
+
source: "git-subdir",
|
|
88
|
+
url,
|
|
89
|
+
path: source.path,
|
|
90
|
+
...pickPins(source)
|
|
91
|
+
} } : { source: {
|
|
92
|
+
source: "url",
|
|
93
|
+
url,
|
|
94
|
+
...pickPins(source)
|
|
95
|
+
} };
|
|
96
|
+
}
|
|
97
|
+
if (target === "copilot" && kind === "git-subdir" && typeof source.url === "string" && typeof source.path === "string") {
|
|
98
|
+
const match = GITHUB_URL.exec(source.url.trim());
|
|
99
|
+
if (match) return { source: {
|
|
100
|
+
source: "github",
|
|
101
|
+
repo: match[1],
|
|
102
|
+
path: source.path,
|
|
103
|
+
...pickPins(source)
|
|
104
|
+
} };
|
|
105
|
+
return { omitted: "git-subdir is installable only from a GitHub URL, which is mapped to github with a path" };
|
|
106
|
+
}
|
|
107
|
+
if (!supported) return { omitted: `${kind} source is not supported by ${target}` };
|
|
108
|
+
if (target === "cursor" && (kind === "url" || kind === "git-subdir") && typeof source.url === "string") {
|
|
109
|
+
const url = withGitSuffix(source.url);
|
|
110
|
+
if (url === void 0) return { omitted: `cursor requires a ".git" URL for a ${kind} source` };
|
|
111
|
+
return { source: url === source.url ? source : {
|
|
112
|
+
...source,
|
|
113
|
+
url
|
|
114
|
+
} };
|
|
115
|
+
}
|
|
116
|
+
return { source };
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
7
119
|
//#region src/skill-scripts/install-docs.ts
|
|
8
120
|
const argv = process.argv.slice(2);
|
|
9
121
|
const rootFlag = argv.indexOf("--root");
|
|
@@ -50,6 +162,11 @@ const cursor = readJson(path.join(root, ".cursor-plugin/marketplace.json"));
|
|
|
50
162
|
const targets = [];
|
|
51
163
|
const sections = [];
|
|
52
164
|
const pluginNames = (catalog) => (catalog?.plugins ?? []).map((p) => p.name).filter(Boolean);
|
|
165
|
+
const installableNames = (target, catalog) => (catalog?.plugins ?? []).filter((p) => p?.name && p.source !== void 0 && !("omitted" in adaptSource(target, p.source))).map((p) => p.name);
|
|
166
|
+
const notInstallable = (installed, label) => {
|
|
167
|
+
const missing = pluginNames(claude).filter((name) => !installed.includes(name));
|
|
168
|
+
return missing.length === 0 ? "" : `\n\nNot installable on ${label}: ${missing.join(", ")}.`;
|
|
169
|
+
};
|
|
53
170
|
if (claude) {
|
|
54
171
|
const names = pluginNames(claude);
|
|
55
172
|
const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `/plugin install ${n}@${claude.name}`);
|
|
@@ -64,20 +181,20 @@ if (claude) {
|
|
|
64
181
|
].join("\n"));
|
|
65
182
|
}
|
|
66
183
|
if (copilot) {
|
|
67
|
-
const names =
|
|
184
|
+
const names = installableNames("copilot", copilot);
|
|
68
185
|
const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `copilot plugin install ${n}@${copilot.name}`);
|
|
69
186
|
targets.push("copilot-cli");
|
|
70
|
-
sections.push([
|
|
187
|
+
sections.push([[
|
|
71
188
|
"**GitHub Copilot CLI**",
|
|
72
189
|
"",
|
|
73
190
|
"```",
|
|
74
191
|
`copilot plugin marketplace add ${repoRef}`,
|
|
75
192
|
...installs,
|
|
76
193
|
"```"
|
|
77
|
-
].join("\n"));
|
|
194
|
+
].join("\n"), notInstallable(names, "Copilot CLI")].join(""));
|
|
78
195
|
}
|
|
79
196
|
if (codex) {
|
|
80
|
-
const names =
|
|
197
|
+
const names = installableNames("codex", codex);
|
|
81
198
|
const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `codex plugin add ${n}@${codex.name}`);
|
|
82
199
|
targets.push("codex");
|
|
83
200
|
sections.push([
|
|
@@ -89,7 +206,7 @@ if (codex) {
|
|
|
89
206
|
"```",
|
|
90
207
|
"",
|
|
91
208
|
"Start a new session before using the plugin."
|
|
92
|
-
].join("\n"));
|
|
209
|
+
].join("\n").concat(notInstallable(names, "Codex")));
|
|
93
210
|
}
|
|
94
211
|
if (cursor) {
|
|
95
212
|
targets.push("cursor");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/marketplace/scripts/marketplace.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/marketplace/scripts/validate.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
package/skills/version/SKILL.md
CHANGED
|
@@ -74,7 +74,7 @@ node scripts/version.mjs --from-package
|
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Resolve that path against this skill's own directory; it runs the project's installed CLI
|
|
77
|
-
(`pnpm exec universal-plugin`), else `npx universal-plugin@0.
|
|
77
|
+
(`pnpm exec universal-plugin`), else `npx universal-plugin@0.16.0` pinned to this skill's version.
|
|
78
78
|
`universal-plugin plugin version` on the `PATH` is the fallback. The command never prompts, so it is safe to run unattended once the
|
|
79
79
|
source is decided.
|
|
80
80
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
-
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.0","entry":"skills/version/scripts/version.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
//#region src/compile/scripts.ts
|
|
6
6
|
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|