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.
- 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 +503 -69
- 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 +2 -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. */
|
|
@@ -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
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8247
|
+
dryRun,
|
|
7906
8248
|
verbose: opts.verbose,
|
|
7907
8249
|
clean: opts.clean
|
|
7908
8250
|
});
|
|
7909
|
-
const scripts = await compileScripts(root, { dryRun
|
|
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
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
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
|
|
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 =
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
catalogPath
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
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,
|
|
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
|
|
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))
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
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
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.
|
|
@@ -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.
|
|
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
|