universal-plugin 0.14.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.14.1",
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.14.1",
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.14.1",
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./** 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>). Codex documents npm alongside a
5241
- * local path. Copilot CLI and Cursor document local paths only, which is why an npm entry reaches
5242
- * two catalogs rather than four (`.research/local-marketplaces`, and issue #86). */
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: ["path", "npm"],
5252
- copilot: ["path"],
5253
- cursor: ["path"]
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. */
@@ -6920,6 +7108,82 @@ function addEntry(config, key, entry) {
6920
7108
  };
6921
7109
  }
6922
7110
  //#endregion
7111
+ //#region src/compile/invisible.ts
7112
+ /** Invisible characters in compiled output (issue #190).
7113
+ *
7114
+ * A bundler prints a dependency's `'\u{FEFF}'` as the raw character, so a committed script holds
7115
+ * code points a reader cannot see — the ones skill checks reject because they can hide
7116
+ * instructions. Inside a string, template or regular-expression literal the character can be
7117
+ * written as an escape with the same meaning; anywhere else it can only be reported. */
7118
+ /** Controls, format characters (zero-width, bidirectional, BOM, soft hyphen, tags), line and paragraph
7119
+ * separators, and default-ignorable code points such as variation selectors. Tab, line feed and
7120
+ * carriage return are ordinary whitespace. */
7121
+ const INVISIBLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
7122
+ function isInvisible(char) {
7123
+ return char !== " " && char !== "\n" && char !== "\r" && INVISIBLE.test(char);
7124
+ }
7125
+ /** `\uXXXX` per UTF-16 code unit — valid in strings, templates and regular expressions, with or
7126
+ * without the `u` flag (a surrogate pair of escapes reads as one code point under `u`). */
7127
+ function escapeChar(char) {
7128
+ let out = "";
7129
+ for (let i = 0; i < char.length; i++) out += `\\u${char.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0")}`;
7130
+ return out;
7131
+ }
7132
+ /** Rewrites every invisible character inside `spans` as an escape that keeps the literal's value. */
7133
+ function escapeInvisible(code, spans) {
7134
+ const sorted = [...spans].sort((a, b) => a.start - b.start);
7135
+ let out = "";
7136
+ let copied = 0;
7137
+ for (const span of sorted) {
7138
+ if (span.start < copied) continue;
7139
+ out += code.slice(copied, span.start);
7140
+ out += escapeSpan(code.slice(span.start, span.end), span.kind);
7141
+ copied = span.end;
7142
+ }
7143
+ return out + code.slice(copied);
7144
+ }
7145
+ function escapeSpan(text, kind) {
7146
+ let out = "";
7147
+ let i = 0;
7148
+ while (i < text.length) {
7149
+ const char = String.fromCodePoint(text.codePointAt(i));
7150
+ if (char !== "\\") {
7151
+ out += isInvisible(char) ? escapeChar(char) : char;
7152
+ i += char.length;
7153
+ continue;
7154
+ }
7155
+ const next = i + 1 < text.length ? String.fromCodePoint(text.codePointAt(i + 1)) : "";
7156
+ i += 1 + next.length;
7157
+ if (!isInvisible(next)) out += `\\${next}`;
7158
+ else if (kind !== "regex" && (next === "\u2028" || next === "\u2029")) continue;
7159
+ else out += escapeChar(next);
7160
+ }
7161
+ return out;
7162
+ }
7163
+ /** Every invisible character left in `code`, 1-based line and column, for the build to warn about. */
7164
+ function findInvisible(code) {
7165
+ const found = [];
7166
+ let line = 1;
7167
+ let column = 1;
7168
+ for (const char of code) {
7169
+ if (char === "\n") {
7170
+ line++;
7171
+ column = 1;
7172
+ continue;
7173
+ }
7174
+ if (isInvisible(char)) found.push({
7175
+ line,
7176
+ column,
7177
+ codePoint: char.codePointAt(0)
7178
+ });
7179
+ column++;
7180
+ }
7181
+ return found;
7182
+ }
7183
+ function formatCodePoint(codePoint) {
7184
+ return `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`;
7185
+ }
7186
+ //#endregion
6923
7187
  //#region src/compile/scripts.ts
6924
7188
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
6925
7189
  * library it inlines, reads it; universal-plugin depends on neither. */
@@ -6970,18 +7234,23 @@ function parseEntry(output, value) {
6970
7234
  source: value
6971
7235
  };
6972
7236
  if (value === null || typeof value !== "object" || Array.isArray(value)) return `scripts "${output}" must map to a source path or { source, command }`;
6973
- const { source, command } = value;
7237
+ const { source, command, alias, keepNames } = value;
6974
7238
  if (typeof source !== "string" || source.length === 0) return `scripts "${output}" has no source`;
6975
7239
  if (command !== void 0 && (typeof command !== "string" || command.trim().length === 0)) return `scripts "${output}" has a command that is not a command path`;
6976
- return command === void 0 ? {
6977
- output: normalized,
6978
- source
6979
- } : {
7240
+ if (alias !== void 0 && !isStringMap(alias)) return `scripts "${output}" has an alias that is not a map from import specifier to replacement`;
7241
+ if (keepNames !== void 0 && typeof keepNames !== "boolean") return `scripts "${output}" has a keepNames that is not true or false`;
7242
+ return {
6980
7243
  output: normalized,
6981
7244
  source,
6982
- command: command.trim().split(/\s+/).join(" ")
7245
+ ...command === void 0 ? {} : { command: command.trim().split(/\s+/).join(" ") },
7246
+ ...alias === void 0 ? {} : { alias },
7247
+ ...keepNames === void 0 ? {} : { keepNames }
6983
7248
  };
6984
7249
  }
7250
+ function isStringMap(value) {
7251
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
7252
+ return Object.entries(value).every(([k, v]) => k.length > 0 && typeof v === "string" && v.length > 0);
7253
+ }
6985
7254
  /** A `/`-separated path relative to the plugin root, without `./`; `null` when it is absolute or
6986
7255
  * climbs out of the root. */
6987
7256
  function normalizeRelative(p) {
@@ -7101,7 +7370,7 @@ async function compileScripts(root, opts, compiler) {
7101
7370
  entry: entry.output,
7102
7371
  commands
7103
7372
  };
7104
- content = composeScript(await compiler.compile(sourcePath, root), entry, data);
7373
+ content = composeScript(await compiler.compile(sourcePath, root, bundleOptions(entry)), entry, data);
7105
7374
  } catch (err) {
7106
7375
  warnings.push(`Failed to compile ${entry.source} → ${entry.output}: ${err instanceof Error ? err.message : String(err)}`);
7107
7376
  rows.push({
@@ -7114,6 +7383,8 @@ async function compileScripts(root, opts, compiler) {
7114
7383
  const bytes = Buffer.byteLength(content);
7115
7384
  const overBudget = sizeWarning(entry.output, bytes, bin);
7116
7385
  if (overBudget !== null) warnings.push(overBudget);
7386
+ const hidden = findInvisible(content);
7387
+ if (hidden.length > 0) warnings.push(invisibleWarning(entry.output, hidden));
7117
7388
  const comparison = compareScript(fs$1.existsSync(outputPath) ? fs$1.readFileSync(outputPath, "utf8") : null, content);
7118
7389
  if (opts.dryRun || comparison === "unchanged") {
7119
7390
  rows.push({
@@ -7141,6 +7412,19 @@ async function compileScripts(root, opts, compiler) {
7141
7412
  summary: summarize(rows)
7142
7413
  };
7143
7414
  }
7415
+ /** The bundle options the entry sets, and none it does not. */
7416
+ function bundleOptions({ alias, keepNames }) {
7417
+ return {
7418
+ ...alias === void 0 ? {} : { alias },
7419
+ ...keepNames === void 0 ? {} : { keepNames }
7420
+ };
7421
+ }
7422
+ /** The compiler escapes invisible characters inside literals; one left elsewhere (a comment, an
7423
+ * identifier, a tagged template) is named so the author can find it before a skill check does. */
7424
+ function invisibleWarning(output, hidden) {
7425
+ const [first] = hidden;
7426
+ return `${`${output}:${first?.line}:${first?.column} ${formatCodePoint(first?.codePoint ?? 0)}`}${hidden.length > 1 ? ` and ${hidden.length - 1} more` : ""}: invisible character in the compiled script that the build could not escape — skill checks reject it; write it as an escape in a string literal in the source`;
7427
+ }
7144
7428
  /** The `package.json` at `packagePath` (`.agents/universal-plugin.json`), which names the package and
7145
7429
  * bin a script falls back to; `null` when the plugin ships to no package. Its version is never read. */
7146
7430
  function readPackageJson(root) {
@@ -7275,34 +7559,40 @@ function onlyEntryMain() {
7275
7559
  *
7276
7560
  * An entry that inlines only its own source keeps readable output — in effect its types are
7277
7561
  * stripped. One that inlines a dependency is minified, because that is the code the size budget is
7278
- * for. rolldown is loaded only when a plugin declares a script. */
7279
- const rolldownCompiler = { async compile(sourcePath, root) {
7562
+ * for. rolldown is loaded only when a plugin declares a script.
7563
+ *
7564
+ * An entry's `alias` becomes `resolve.alias`, with a relative target resolved from the plugin root;
7565
+ * its `keepNames` is passed to both outputs, so minifying keeps the names too. */
7566
+ const rolldownCompiler = { async compile(sourcePath, root, options = {}) {
7280
7567
  const { rolldown } = await import("rolldown");
7281
7568
  const unresolved = [];
7569
+ const alias = resolveAlias(options, root);
7570
+ const output = {
7571
+ format: "esm",
7572
+ codeSplitting: false,
7573
+ keepNames: options.keepNames ?? false
7574
+ };
7282
7575
  const bundle = await rolldown({
7283
7576
  input: sourcePath,
7284
7577
  cwd: root,
7285
7578
  platform: "node",
7286
7579
  plugins: [onlyEntryMain()],
7580
+ ...alias === void 0 ? {} : { resolve: { alias } },
7287
7581
  onLog(_level, log) {
7288
7582
  if (log.code === "UNRESOLVED_IMPORT") unresolved.push(log.message);
7289
7583
  }
7290
7584
  });
7291
7585
  try {
7292
7586
  if (unresolved.length > 0) throw new Error(unresolved.join("\n"));
7293
- const [chunk, ...rest] = (await bundle.generate({
7294
- format: "esm",
7295
- codeSplitting: false
7296
- })).output;
7587
+ const [chunk, ...rest] = (await bundle.generate(output)).output;
7297
7588
  if (chunk?.type !== "chunk" || rest.some((o) => o.type === "chunk")) throw new Error("compiles to more than one file");
7298
- const external = [...chunk.imports, ...chunk.dynamicImports].filter((id) => !isBuiltin(id));
7589
+ const external = [...chunk.imports, ...chunk.dynamicImports].filter((id) => !isBuiltin(id) && id !== chunk.fileName);
7299
7590
  if (external.length > 0) throw new Error(`imports ${external.join(", ")}, which could not be inlined — install it where the plugin root resolves it`);
7300
- if (!chunk.moduleIds.some((id) => id.split(path$1.sep).includes("node_modules"))) return chunk.code;
7301
- return (await bundle.generate({
7302
- format: "esm",
7303
- codeSplitting: false,
7591
+ if (!chunk.moduleIds.some((id) => id.split(path$1.sep).includes("node_modules"))) return await escapeLiterals(chunk.code);
7592
+ return await escapeLiterals((await bundle.generate({
7593
+ ...output,
7304
7594
  minify: true
7305
- })).output[0].code;
7595
+ })).output[0].code);
7306
7596
  } catch (error) {
7307
7597
  const spec = failedExtends(error);
7308
7598
  throw spec ? new Error(explainTsconfigFailure(sourcePath, root, spec), { cause: error }) : error;
@@ -7310,6 +7600,57 @@ const rolldownCompiler = { async compile(sourcePath, root) {
7310
7600
  await bundle.close();
7311
7601
  }
7312
7602
  } };
7603
+ /** The entry's alias with each relative target (`./`, `../`) made absolute from the plugin root, so it
7604
+ * does not depend on where rolldown resolves from; `undefined` when the entry sets none. */
7605
+ function resolveAlias({ alias }, root) {
7606
+ if (alias === void 0 || Object.keys(alias).length === 0) return void 0;
7607
+ return Object.fromEntries(Object.entries(alias).map(([from, to]) => [from, /^\.\.?\//.test(to) ? path$1.resolve(root, to) : to]));
7608
+ }
7609
+ /** rolldown prints a dependency's escaped invisible characters raw (issue #190); this writes them back
7610
+ * as escapes inside every string, untagged template and regular-expression literal. */
7611
+ async function escapeLiterals(code) {
7612
+ if (![...code].some(isInvisible)) return code;
7613
+ const { parseAst } = await import("rolldown/parseAst");
7614
+ const spans = [];
7615
+ collectSpans(parseAst(code), spans);
7616
+ return escapeInvisible(code, spans);
7617
+ }
7618
+ function isNode(value) {
7619
+ return typeof value === "object" && value !== null && typeof value.type === "string";
7620
+ }
7621
+ function collectSpans(node, spans) {
7622
+ if (node.type === "Literal") {
7623
+ if (typeof node["value"] === "string") spans.push({
7624
+ start: node.start,
7625
+ end: node.end,
7626
+ kind: "string"
7627
+ });
7628
+ else if (node["regex"] !== void 0) spans.push({
7629
+ start: node.start,
7630
+ end: node.end,
7631
+ kind: "regex"
7632
+ });
7633
+ return;
7634
+ }
7635
+ if (node.type === "TaggedTemplateExpression") {
7636
+ visit(node["tag"], spans);
7637
+ for (const expression of node["quasi"]["expressions"]) collectSpans(expression, spans);
7638
+ return;
7639
+ }
7640
+ if (node.type === "TemplateElement") {
7641
+ spans.push({
7642
+ start: node.start,
7643
+ end: node.end,
7644
+ kind: "template"
7645
+ });
7646
+ return;
7647
+ }
7648
+ for (const [key, value] of Object.entries(node)) if (key !== "parent") visit(value, spans);
7649
+ }
7650
+ function visit(value, spans) {
7651
+ if (Array.isArray(value)) for (const item of value) visit(item, spans);
7652
+ else if (isNode(value)) collectSpans(value, spans);
7653
+ }
7313
7654
  //#endregion
7314
7655
  //#region ../../node_modules/.pnpm/@toon-format+toon@4.1.1/node_modules/@toon-format/toon/dist/index.mjs
7315
7656
  const NULL_LITERAL = "null";
@@ -7897,16 +8238,17 @@ function nextStep$1(result, cwd) {
7897
8238
  }
7898
8239
  function buildCommand() {
7899
8240
  const cmd = new Command("build").description("Generate vendor manifests from plugin.json");
7900
- cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--strict-dependencies", "Fail when a refreshed catalog cannot resolve a declared dependency").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action(async (opts) => {
8241
+ cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--check", "Write nothing; exit 1 when a compiled script is missing, stale, or on another version").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--strict-dependencies", "Fail when a refreshed catalog cannot resolve a declared dependency").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action(async (opts) => {
7901
8242
  try {
7902
8243
  const root = resolveRoot(opts.root);
8244
+ const dryRun = opts.dryRun || opts.check;
7903
8245
  const result = buildPlugin(root, {
7904
8246
  vendor: opts.vendor,
7905
- dryRun: opts.dryRun,
8247
+ dryRun,
7906
8248
  verbose: opts.verbose,
7907
8249
  clean: opts.clean
7908
8250
  });
7909
- const scripts = await compileScripts(root, { dryRun: opts.dryRun }, rolldownCompiler);
8251
+ const scripts = await compileScripts(root, { dryRun }, rolldownCompiler);
7910
8252
  for (const warning of [...result.warnings, ...scripts.warnings]) process.stderr.write(`warn: ${warning}\n`);
7911
8253
  const { built, skipped, failed, canonical } = result.summary;
7912
8254
  const jsonResult = {
@@ -7941,6 +8283,11 @@ function buildCommand() {
7941
8283
  });
7942
8284
  process.stderr.write(nextStep$1(result, process.cwd()));
7943
8285
  if (failed > 0 || scripts.summary.failed > 0) process.exitCode = 1;
8286
+ if (opts.check && scripts.summary.behind > 0) {
8287
+ const behind = scripts.rows.filter((r) => r.status === "missing" || r.status === "stale" || r.status === "version").map((r) => `${r.path} (${r.status})`);
8288
+ process.stderr.write(`error: ${behind.length} compiled ${behind.length === 1 ? "script is" : "scripts are"} behind: ${behind.join(", ")} — run "universal-plugin plugin build" and commit the result (--check)\n`);
8289
+ process.exitCode = 1;
8290
+ }
7944
8291
  if (opts.strictDependencies && result.dependencyIssues.length > 0) {
7945
8292
  process.stderr.write(`error: ${result.dependencyIssues.length} declared ${result.dependencyIssues.length === 1 ? "dependency" : "dependencies"} cannot be resolved through the refreshed catalogs (--strict-dependencies)\n`);
7946
8293
  process.exitCode = 1;
@@ -16413,19 +16760,64 @@ function normalizeJson(value) {
16413
16760
  const record = value;
16414
16761
  return Object.fromEntries(Object.keys(record).sort().map((key) => [key, normalizeJson(record[key])]));
16415
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
+ }
16416
16780
  function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16417
16781
  const root = path$1.resolve(rootInput);
16418
16782
  const metadata = deriveMetadata(root, fs, opts);
16419
16783
  const plugins = discoverPlugins(root, fs, opts.scanDirs);
16420
- const derived = selectedTargets$1(opts.targets).map((target) => ({
16421
- target,
16422
- artifacts: serializeTarget(target, metadata, plugins)
16423
- }));
16424
- for (const { artifacts } of derived) for (const artifact of artifacts) assertContained(root, path$1.join(root, artifact.path), fs, "selected artifact");
16425
- const planned = derived.map(({ target, artifacts }) => ({
16426
- target,
16427
- artifacts: artifacts.map((artifact) => mergeDiscoveredCatalog(artifact, readCatalog(fs, path$1.join(root, artifact.path))))
16428
- }));
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
+ }
16429
16821
  for (const { target, artifacts } of planned) for (const artifact of artifacts) {
16430
16822
  const issues = validateCatalogContent(target, artifact.content);
16431
16823
  if (issues.length > 0) throw new Error(formatCatalogIssues(artifact.path, issues));
@@ -16436,7 +16828,7 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16436
16828
  if (fs.exists(output) && !sameArtifact(fs, output, artifact.content) && !opts.force) conflicts.push(artifact.path);
16437
16829
  }
16438
16830
  if (conflicts.length > 0) throw new Error(`error: generated artifact differs: ${conflicts.join(", ")}; rerun with --force to replace selected artifacts`);
16439
- const results = planned.map(({ target, artifacts }) => {
16831
+ const results = planned.map(({ target, artifacts, omitted }) => {
16440
16832
  if (plugins.length === 0) return {
16441
16833
  target,
16442
16834
  status: "empty",
@@ -16449,7 +16841,9 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16449
16841
  target,
16450
16842
  status: opts.dryRun ? "planned" : unchanged ? "unchanged" : "generated",
16451
16843
  paths: artifacts.map((artifact) => artifact.path),
16452
- 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` } : {}
16453
16847
  };
16454
16848
  });
16455
16849
  if (!opts.dryRun && plugins.length > 0) writeArtifacts(fs, planned.flatMap((entry) => entry.artifacts), root);
@@ -16651,11 +17045,6 @@ const ENTRY_METADATA_FIELDS = [
16651
17045
  "license",
16652
17046
  "keywords"
16653
17047
  ];
16654
- /** The kind name used to decide whether a runtime accepts a source, from the source itself. */
16655
- function sourceKindOf(source) {
16656
- if (isLocalCatalogSource(source)) return "path";
16657
- return typeof source === "string" ? "path" : source.source;
16658
- }
16659
17048
  /** A source rendered for the result table — one short cell, not a JSON blob. */
16660
17049
  function describeSource(source) {
16661
17050
  if (typeof source === "string") return source;
@@ -16777,27 +17166,46 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
16777
17166
  source: resolved.source,
16778
17167
  metadata: entryMetadata(resolved.metadata, opts.metadata)
16779
17168
  };
16780
- const kind = sourceKindOf(resolved.source);
16781
17169
  const rendered = describeSource(resolved.source);
16782
- const planned = selectedTargets(opts.targets).map((target) => {
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) => {
16783
17177
  const catalogPath = TARGET_CATALOG_PATHS[target];
16784
17178
  const file = path$1.join(root, catalogPath);
16785
- const existing = fs.exists(file) ? fs.read(file) : void 0;
16786
- if (!TARGET_SOURCE_KINDS[target].includes(kind)) return {
16787
- target,
16788
- catalogPath,
16789
- file,
16790
- existing,
16791
- skipped: `${kind} source is not supported by ${target}`,
16792
- artifact: void 0
16793
- };
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);
16794
17198
  return {
16795
17199
  target,
16796
17200
  catalogPath,
16797
17201
  file,
16798
17202
  existing,
16799
17203
  skipped: void 0,
16800
- artifact: mergeCatalogEntry(target, metadata, plugin, () => existing, { keepForeignSource: false })
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
16801
17209
  };
16802
17210
  });
16803
17211
  for (const entry of planned) {
@@ -16818,23 +17226,30 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
16818
17226
  source: rendered,
16819
17227
  path: entry.catalogPath
16820
17228
  };
16821
- if (entry.skipped || !entry.artifact) return {
17229
+ if (entry.skipped) return {
16822
17230
  ...row,
16823
17231
  status: "skipped",
16824
- source: rendered,
16825
17232
  reason: entry.skipped
16826
17233
  };
17234
+ if (!entry.artifact) return {
17235
+ ...row,
17236
+ status: "skipped"
17237
+ };
17238
+ const mapped = "mapped" in entry ? { reason: entry.mapped } : {};
16827
17239
  if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) return {
16828
17240
  ...row,
17241
+ ...mapped,
16829
17242
  status: "unchanged"
16830
17243
  };
16831
17244
  if (opts.dryRun) return {
16832
17245
  ...row,
17246
+ ...mapped,
16833
17247
  status: "planned"
16834
17248
  };
16835
17249
  const existed = entry.existing !== void 0 && catalogEntry(entry.existing, name) !== void 0;
16836
17250
  return {
16837
17251
  ...row,
17252
+ ...mapped,
16838
17253
  status: existed ? "updated" : "added"
16839
17254
  };
16840
17255
  });
@@ -16931,15 +17346,30 @@ function validateMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16931
17346
  ]).map((target) => {
16932
17347
  const relative = TARGET_CATALOG_PATHS[target];
16933
17348
  const file = path$1.join(root, relative);
16934
- if (!fs.exists(file)) return {
16935
- target,
16936
- path: relative,
16937
- status: opts.required ? "invalid" : "missing",
16938
- issues: opts.required ? [{
16939
- path: "",
16940
- message: "no catalog at this path"
16941
- }] : []
16942
- };
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
+ }
16943
17373
  const content = fs.read(file);
16944
17374
  const issues = [...validateCatalogContent(target, content), ...parseAndCheck(content, (parsed) => [...checkSources(root, fs, parsed), ...target === "claude" ? checkDependencies(root, fs, parsed, content, relative) : []])];
16945
17375
  return {
@@ -17050,6 +17480,10 @@ function initCommand() {
17050
17480
  })),
17051
17481
  summary: `${results.length} targets`
17052
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
+ }
17053
17487
  process.stderr.write("Generated repository metadata only; no marketplace publication, registration, installation, authentication, or provisioning occurred.\n");
17054
17488
  } catch (err) {
17055
17489
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
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.14.1",
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.14.1","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"}};
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.14.1`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
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.
@@ -58,6 +58,7 @@ The command never prompts, so it is safe to run unattended.
58
58
  | `--vendor <id>` | Build one vendor (`claude-code`, `cursor`, `codex`, `copilot-cli`), and refresh only its catalog |
59
59
  | `--verbose` | Print each field-level decision the derivation made |
60
60
  | `--strict-dependencies` | Fail when a refreshed catalog cannot resolve a declared dependency. Use in CI |
61
+ | `--check` | Write nothing; exit 1 when a compiled script is `missing`, `stale`, or `version`. Use in CI |
61
62
  | `--format json` | The same result as JSON, for scripts |
62
63
 
63
64
  `--clean` also exists. It deletes derived manifests before rewriting them, so reach for it only
@@ -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.14.1","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"}};
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.14.1`, pinned. This check matters most where the release tool picks the
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.14.1","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"}};
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.14.1`. Add `--npm` when an npm package ships the plugin,
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.14.1","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"}};
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. The reverse does not hold: Claude Code rejects the Codex catalog for its missing
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. Only a repository path reaches all four:
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, url, git-subdir | yes | — | — | — |
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
- A target that cannot resolve the source is reported `skipped` with the reason, and no file is
59
- written for it. That is deliberate: a source a runtime refuses is an install failure in someone
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
- With no target flags all four are selected, so the skips are the normal case for an npm package.
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
- Both rewritten forms are Claude Code source types, so expect Codex, Copilot CLI, and Cursor to be
131
- skipped for a subdirectory plugin.
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.14.1","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"}};
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.14.1","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"}};
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 = pluginNames(copilot);
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 = pluginNames(codex);
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.14.1","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"}};
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.14.1","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"}};
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
@@ -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.14.1` pinned to this skill's version.
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.14.1","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"}};
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