universal-plugin 0.1.0 → 0.2.1
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/LICENSE +21 -0
- package/bin/universal-plugin.mjs +0 -0
- package/dist/cli.mjs +314 -16
- package/governances/plugin-design.md +279 -0
- package/package.json +58 -59
- package/readme.md +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 unional
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/universal-plugin.mjs
CHANGED
|
File without changes
|
package/dist/cli.mjs
CHANGED
|
@@ -186,6 +186,140 @@ function output(data, readable) {
|
|
|
186
186
|
else readable();
|
|
187
187
|
}
|
|
188
188
|
//#endregion
|
|
189
|
+
//#region src/pin/fs.ts
|
|
190
|
+
const TEXT_EXTENSIONS = new Set([
|
|
191
|
+
".md",
|
|
192
|
+
".mdx",
|
|
193
|
+
".markdown",
|
|
194
|
+
".json",
|
|
195
|
+
".yaml",
|
|
196
|
+
".yml",
|
|
197
|
+
".txt"
|
|
198
|
+
]);
|
|
199
|
+
/** Resolves the plugin's skills directory: the manifest's `skills` field under `root`,
|
|
200
|
+
* defaulting to `<root>/skills/`. */
|
|
201
|
+
function resolveSkillsDir(root, manifestSkills) {
|
|
202
|
+
return path.join(root, manifestSkills ?? "skills/");
|
|
203
|
+
}
|
|
204
|
+
function walk(dir) {
|
|
205
|
+
if (!fsNode.existsSync(dir)) return [];
|
|
206
|
+
const files = [];
|
|
207
|
+
for (const entry of fsNode.readdirSync(dir, { withFileTypes: true })) {
|
|
208
|
+
const entryPath = path.join(dir, entry.name);
|
|
209
|
+
if (entry.isDirectory()) files.push(...walk(entryPath));
|
|
210
|
+
else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name))) files.push(entryPath);
|
|
211
|
+
}
|
|
212
|
+
return files;
|
|
213
|
+
}
|
|
214
|
+
function realPinFs(skillsDir) {
|
|
215
|
+
return {
|
|
216
|
+
listSkillFiles: () => walk(skillsDir),
|
|
217
|
+
readFile: (p) => fsNode.readFileSync(p, "utf8"),
|
|
218
|
+
writeFile: (p, c) => fsNode.writeFileSync(p, c, "utf8")
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/pin/pin.ts
|
|
223
|
+
const PIN_PATTERN = /npx\s+(?:--yes\s+|-y\s+)?([@a-z0-9/._-]+)@(\S+)/g;
|
|
224
|
+
/** Strips a trailing backtick, quote, or paren that isn't part of the version token. */
|
|
225
|
+
function stripTrailing(raw) {
|
|
226
|
+
return raw.replace(/[`'")]+$/, "");
|
|
227
|
+
}
|
|
228
|
+
function extractPins(text) {
|
|
229
|
+
const pins = [];
|
|
230
|
+
for (const match of text.matchAll(PIN_PATTERN)) {
|
|
231
|
+
const pkg = match[1];
|
|
232
|
+
const current = match[2];
|
|
233
|
+
if (!pkg || !current) continue;
|
|
234
|
+
pins.push({
|
|
235
|
+
pkg,
|
|
236
|
+
current: stripTrailing(current),
|
|
237
|
+
file: ""
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return pins;
|
|
241
|
+
}
|
|
242
|
+
const SEMVER_PATTERN = /^[~^]?\d+\.\d+\.\d+$/;
|
|
243
|
+
function isValidSemver(version) {
|
|
244
|
+
return SEMVER_PATTERN.test(version);
|
|
245
|
+
}
|
|
246
|
+
/** True when a pin is a concrete version (optionally range-prefixed), not a placeholder like `<version>`. */
|
|
247
|
+
function isConcreteVersion(version) {
|
|
248
|
+
return isValidSemver(version);
|
|
249
|
+
}
|
|
250
|
+
function majorOf(version) {
|
|
251
|
+
const [major] = version.replace(/^[~^]/, "").split(".");
|
|
252
|
+
return Number(major);
|
|
253
|
+
}
|
|
254
|
+
/** Pure semver compare: -1 if a < b, 0 if equal, 1 if a > b. Ignores build/prerelease metadata. */
|
|
255
|
+
function compareSemver(a, b) {
|
|
256
|
+
const partsA = a.split(".").map(Number);
|
|
257
|
+
const partsB = b.split(".").map(Number);
|
|
258
|
+
for (let i = 0; i < 3; i++) {
|
|
259
|
+
const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
|
|
260
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
function pickTarget(current, available, opts) {
|
|
265
|
+
if (!isValidSemver(current)) return available.latest;
|
|
266
|
+
const currentBare = current.replace(/^[~^]/, "");
|
|
267
|
+
const currentMajor = majorOf(current);
|
|
268
|
+
const candidates = available.versions.filter((v) => {
|
|
269
|
+
if (!isValidSemver(v)) return false;
|
|
270
|
+
if (opts.allowMajor) return true;
|
|
271
|
+
return majorOf(v) === currentMajor;
|
|
272
|
+
});
|
|
273
|
+
if (candidates.length === 0) return currentBare;
|
|
274
|
+
let best = candidates[0];
|
|
275
|
+
for (const candidate of candidates.slice(1)) if (compareSemver(candidate, best) > 0) best = candidate;
|
|
276
|
+
if (compareSemver(best, currentBare) < 0) return currentBare;
|
|
277
|
+
return best;
|
|
278
|
+
}
|
|
279
|
+
function styleRange(version, style) {
|
|
280
|
+
switch (style) {
|
|
281
|
+
case "exact": return version;
|
|
282
|
+
case "tilde": return `~${version}`;
|
|
283
|
+
case "caret": return `^${version}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function normalizeRange(raw) {
|
|
287
|
+
switch (raw) {
|
|
288
|
+
case "exact": return "exact";
|
|
289
|
+
case "tilde":
|
|
290
|
+
case "~": return "tilde";
|
|
291
|
+
case "caret":
|
|
292
|
+
case "^": return "caret";
|
|
293
|
+
default: throw new Error(`Invalid --range value "${raw}" — expected exact, tilde, caret, ~, or ^`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/pin/registry.ts
|
|
298
|
+
function encodePkg(pkg) {
|
|
299
|
+
return pkg.replace("/", "%2f");
|
|
300
|
+
}
|
|
301
|
+
/** Registry client backed by the global `fetch` (Node ≥22). Resilient — network errors and
|
|
302
|
+
* non-2xx responses resolve to `null` rather than throwing. */
|
|
303
|
+
function realRegistryClient(registryBase) {
|
|
304
|
+
const base = registryBase.replace(/\/+$/, "");
|
|
305
|
+
return { async fetchVersions(pkg) {
|
|
306
|
+
try {
|
|
307
|
+
const res = await fetch(`${base}/${encodePkg(pkg)}`);
|
|
308
|
+
if (!res.ok) return null;
|
|
309
|
+
const doc = await res.json();
|
|
310
|
+
const latest = doc["dist-tags"]?.latest;
|
|
311
|
+
const versions = doc.versions ? Object.keys(doc.versions) : [];
|
|
312
|
+
if (!latest) return null;
|
|
313
|
+
return {
|
|
314
|
+
latest,
|
|
315
|
+
versions
|
|
316
|
+
};
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
} };
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
189
323
|
//#region src/build/build.ts
|
|
190
324
|
const VENDOR_OUTPUT = {
|
|
191
325
|
"claude-code": ".claude-plugin/plugin.json",
|
|
@@ -194,6 +328,11 @@ const VENDOR_OUTPUT = {
|
|
|
194
328
|
"copilot-cli": "plugin.json"
|
|
195
329
|
};
|
|
196
330
|
const KNOWN_VENDORS = new Set(Object.keys(VENDOR_OUTPUT));
|
|
331
|
+
function detectIndent$1(json) {
|
|
332
|
+
const match = json.match(/\n([ \t]+)/);
|
|
333
|
+
if (!match) return " ";
|
|
334
|
+
return match[1].startsWith(" ") ? " " : match[1].length;
|
|
335
|
+
}
|
|
197
336
|
function readManifest(root) {
|
|
198
337
|
const manifestPath = path.join(root, ".plugin", "plugin.json");
|
|
199
338
|
if (!fsNode.existsSync(manifestPath)) throw new Error(`No .plugin/plugin.json found at ${root}`);
|
|
@@ -207,6 +346,9 @@ function validateManifest(manifest) {
|
|
|
207
346
|
return errors;
|
|
208
347
|
}
|
|
209
348
|
function buildPlugin(root, opts = {}) {
|
|
349
|
+
const manifestPath = path.join(root, ".plugin", "plugin.json");
|
|
350
|
+
if (!fsNode.existsSync(manifestPath)) throw new Error(`No .plugin/plugin.json found at ${root}`);
|
|
351
|
+
const indent = detectIndent$1(fsNode.readFileSync(manifestPath, "utf8"));
|
|
210
352
|
const manifest = readManifest(root);
|
|
211
353
|
const errors = validateManifest(manifest);
|
|
212
354
|
if (errors.length > 0) throw new Error(`plugin.json validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
@@ -248,7 +390,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
248
390
|
if (!opts.dryRun) {
|
|
249
391
|
if (opts.clean && fsNode.existsSync(outputPath)) fsNode.unlinkSync(outputPath);
|
|
250
392
|
fsNode.mkdirSync(outputDir, { recursive: true });
|
|
251
|
-
fsNode.writeFileSync(outputPath, `${JSON.stringify(vendorManifest, null,
|
|
393
|
+
fsNode.writeFileSync(outputPath, `${JSON.stringify(vendorManifest, null, indent)}\n`);
|
|
252
394
|
}
|
|
253
395
|
written.push(outputPath);
|
|
254
396
|
}
|
|
@@ -258,26 +400,112 @@ function buildPlugin(root, opts = {}) {
|
|
|
258
400
|
warnings
|
|
259
401
|
};
|
|
260
402
|
}
|
|
403
|
+
function escapeRegExp(value) {
|
|
404
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
405
|
+
}
|
|
406
|
+
/** Rewrites every `npx <pkg>@<anything>` occurrence to `npx <pkg>@<next>`, regardless of the
|
|
407
|
+
* version each occurrence currently carries — so a plugin's references to one CLI converge
|
|
408
|
+
* (a concrete pin, a placeholder, and a stale pin all land on the same resolved value). */
|
|
409
|
+
function rewritePin(content, pkg, next) {
|
|
410
|
+
const pattern = new RegExp(`(npx\\s+(?:--yes\\s+|-y\\s+)?${escapeRegExp(pkg)}@)[^\\s\`'")]+`, "g");
|
|
411
|
+
return content.replace(pattern, `$1${next}`);
|
|
412
|
+
}
|
|
413
|
+
/** Scans the plugin's skills for `npx <pkg>@<pin>` references and resolves each distinct
|
|
414
|
+
* in-scope package's current version from the registry, rewriting the pins in place. */
|
|
415
|
+
async function resolvePins(root, opts, client, pinFs) {
|
|
416
|
+
const range = opts.range ?? "exact";
|
|
417
|
+
const files = pinFs.listSkillFiles();
|
|
418
|
+
const contents = /* @__PURE__ */ new Map();
|
|
419
|
+
const currentsByPkg = /* @__PURE__ */ new Map();
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
const content = pinFs.readFile(file);
|
|
422
|
+
contents.set(file, content);
|
|
423
|
+
for (const pin of extractPins(content)) {
|
|
424
|
+
const list = currentsByPkg.get(pin.pkg) ?? [];
|
|
425
|
+
list.push(pin.current);
|
|
426
|
+
currentsByPkg.set(pin.pkg, list);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const scope = opts.packages && opts.packages.length > 0 ? new Set(opts.packages) : null;
|
|
430
|
+
const inScope = [...currentsByPkg.entries()].filter(([pkg]) => !scope || scope.has(pkg));
|
|
431
|
+
const results = [];
|
|
432
|
+
results.warnings = [];
|
|
433
|
+
for (const [pkg, currents] of inScope) {
|
|
434
|
+
const anchor = currents.find(isConcreteVersion) ?? currents[0];
|
|
435
|
+
const available = await client.fetchVersions(pkg);
|
|
436
|
+
if (!available) {
|
|
437
|
+
results.warnings.push(`registry lookup for "${pkg}" failed — skipped`);
|
|
438
|
+
results.push({
|
|
439
|
+
package: pkg,
|
|
440
|
+
current: anchor,
|
|
441
|
+
resolved: anchor,
|
|
442
|
+
status: "skipped"
|
|
443
|
+
});
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const written = styleRange(pickTarget(anchor, available, { allowMajor: opts.allowMajor ?? false }), range);
|
|
447
|
+
let changed = false;
|
|
448
|
+
for (const file of files) {
|
|
449
|
+
const content = contents.get(file);
|
|
450
|
+
const rewritten = rewritePin(content, pkg, written);
|
|
451
|
+
if (rewritten !== content) {
|
|
452
|
+
changed = true;
|
|
453
|
+
contents.set(file, rewritten);
|
|
454
|
+
if (!opts.dryRun) pinFs.writeFile(file, rewritten);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
results.push({
|
|
458
|
+
package: pkg,
|
|
459
|
+
current: anchor,
|
|
460
|
+
resolved: written,
|
|
461
|
+
status: changed ? "updated" : "unchanged"
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return results;
|
|
465
|
+
}
|
|
261
466
|
//#endregion
|
|
262
467
|
//#region src/build/cli.ts
|
|
468
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
469
|
+
function collect(value, previous) {
|
|
470
|
+
return [...previous, value];
|
|
471
|
+
}
|
|
263
472
|
function buildCommand() {
|
|
264
473
|
const cmd = new Command("build").description("Generate vendor manifests from .plugin/plugin.json");
|
|
265
|
-
cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--format <format>", "Output format: json or text (default: text)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).action((opts) => {
|
|
474
|
+
cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--format <format>", "Output format: json or text (default: text)").option("--registry <url>", "Registry to resolve pinned CLI versions from", DEFAULT_REGISTRY).option("--range <style>", "Pin style to write: exact, tilde, or caret (~/^ accepted)", "exact").option("--package <name>", "Limit pin resolution to this package (repeatable)", collect, []).option("--allow-major", "Allow pin resolution to cross a major version boundary").option("--skip-pins", "Skip pin resolution entirely (manifests only)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code --range tilde\n").action(async (opts) => {
|
|
266
475
|
try {
|
|
267
|
-
const
|
|
476
|
+
const root = resolveRoot(opts.root);
|
|
477
|
+
const result = buildPlugin(root, {
|
|
268
478
|
vendor: opts.vendor,
|
|
269
479
|
dryRun: opts.dryRun,
|
|
270
480
|
verbose: opts.verbose,
|
|
271
481
|
clean: opts.clean
|
|
272
482
|
});
|
|
273
483
|
for (const warning of result.warnings) process.stderr.write(`warn: ${warning}\n`);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
484
|
+
let pins = Object.assign([], { warnings: [] });
|
|
485
|
+
if (!opts.skipPins) {
|
|
486
|
+
const skillsDir = resolveSkillsDir(root, readManifest(root).skills);
|
|
487
|
+
const client = realRegistryClient(opts.registry);
|
|
488
|
+
const pinFs = realPinFs(skillsDir);
|
|
489
|
+
pins = await resolvePins(root, {
|
|
490
|
+
packages: opts.package,
|
|
491
|
+
allowMajor: opts.allowMajor,
|
|
492
|
+
dryRun: opts.dryRun,
|
|
493
|
+
range: normalizeRange(opts.range)
|
|
494
|
+
}, client, pinFs);
|
|
495
|
+
for (const warning of pins.warnings) process.stderr.write(`warn: ${warning}\n`);
|
|
496
|
+
}
|
|
497
|
+
output({
|
|
498
|
+
...result,
|
|
499
|
+
pins
|
|
500
|
+
}, () => {
|
|
501
|
+
if (result.vendors.length > 0) {
|
|
502
|
+
printFields({ vendors: result.vendors.join(", ") });
|
|
503
|
+
printTable(result.written.map((p) => ({ path: p })), [{
|
|
504
|
+
label: "output",
|
|
505
|
+
get: (r) => opts.dryRun ? `(dry-run) ${r.path}` : r.path
|
|
506
|
+
}]);
|
|
507
|
+
}
|
|
508
|
+
for (const p of pins) console.log(`${p.package} ${p.current} → ${p.resolved} ${p.status}`);
|
|
281
509
|
});
|
|
282
510
|
} catch (err) {
|
|
283
511
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -309,6 +537,9 @@ function getUserDir() {
|
|
|
309
537
|
function getProjectDir(root) {
|
|
310
538
|
return path.join(root, "governances");
|
|
311
539
|
}
|
|
540
|
+
function getLocalDir(root) {
|
|
541
|
+
return path.join(root, ".agents", "governances");
|
|
542
|
+
}
|
|
312
543
|
function getPackageDir() {
|
|
313
544
|
const thisFile = fileURLToPath(import.meta.url);
|
|
314
545
|
return path.join(path.dirname(thisFile), "..", "governances");
|
|
@@ -323,6 +554,10 @@ function getScopedPaths(root) {
|
|
|
323
554
|
scope: "project",
|
|
324
555
|
dir: getProjectDir(root)
|
|
325
556
|
},
|
|
557
|
+
{
|
|
558
|
+
scope: "local",
|
|
559
|
+
dir: getLocalDir(root)
|
|
560
|
+
},
|
|
326
561
|
{
|
|
327
562
|
scope: "user",
|
|
328
563
|
dir: getUserDir()
|
|
@@ -395,8 +630,8 @@ function readGlobalState() {
|
|
|
395
630
|
}
|
|
396
631
|
}
|
|
397
632
|
function governanceCommand() {
|
|
398
|
-
const cmd = new Command("governance").description("Manage plugin governances").
|
|
399
|
-
cmd.command("show <name>").description("Show a governance by name").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((name, opts) => {
|
|
633
|
+
const cmd = new Command("governance").description("Manage plugin governances").helpCommand(false);
|
|
634
|
+
cmd.command("show <name>").description("Show a governance by name").option("--format <format>", "Output format: json or text (default: text)").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((name, opts) => {
|
|
400
635
|
const result = showGovernance(name, resolveRoot(opts.root), realGovernanceFs, {
|
|
401
636
|
state: readGlobalState(),
|
|
402
637
|
globalStorePath: globalStorePath()
|
|
@@ -409,7 +644,7 @@ function governanceCommand() {
|
|
|
409
644
|
process.stdout.write(result.content);
|
|
410
645
|
});
|
|
411
646
|
});
|
|
412
|
-
cmd.command("list").description("List available governances").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((opts) => {
|
|
647
|
+
cmd.command("list").description("List available governances").option("--format <format>", "Output format: json or text (default: text)").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((opts) => {
|
|
413
648
|
const entries = listGovernances(resolveRoot(opts.root), realGovernanceFs);
|
|
414
649
|
output(entries, () => {
|
|
415
650
|
printTable(entries, [{
|
|
@@ -655,6 +890,63 @@ function prepareCommand() {
|
|
|
655
890
|
});
|
|
656
891
|
}
|
|
657
892
|
//#endregion
|
|
893
|
+
//#region src/publish/fs.ts
|
|
894
|
+
const realSyncVersionFs = {
|
|
895
|
+
exists: (p) => fsNode.existsSync(p),
|
|
896
|
+
read: (p) => fsNode.readFileSync(p, "utf8"),
|
|
897
|
+
write: (p, content) => fsNode.writeFileSync(p, content)
|
|
898
|
+
};
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/publish/sync-version.ts
|
|
901
|
+
function detectIndent(json) {
|
|
902
|
+
const match = json.match(/\n([ \t]+)/);
|
|
903
|
+
if (!match) return " ";
|
|
904
|
+
return match[1].startsWith(" ") ? " " : match[1].length;
|
|
905
|
+
}
|
|
906
|
+
function syncVersion(root, syncFs) {
|
|
907
|
+
const manifestPath = path.join(root, ".plugin", "plugin.json");
|
|
908
|
+
if (!syncFs.exists(manifestPath)) throw new Error(`No .plugin/plugin.json found at ${root}`);
|
|
909
|
+
const agentsConfigPath = path.join(root, ".agents", "universal-plugin.json");
|
|
910
|
+
const packagePath = (syncFs.exists(agentsConfigPath) ? JSON.parse(syncFs.read(agentsConfigPath)) : {})["packagePath"];
|
|
911
|
+
if (!packagePath || typeof packagePath !== "string") throw new Error("packagePath is required in .agents/universal-plugin.json");
|
|
912
|
+
const raw = syncFs.read(manifestPath);
|
|
913
|
+
const manifest = JSON.parse(raw);
|
|
914
|
+
const pkgJsonPath = path.join(root, packagePath, "package.json");
|
|
915
|
+
if (!syncFs.exists(pkgJsonPath)) throw new Error(`No package.json found at ${packagePath}`);
|
|
916
|
+
const version = JSON.parse(syncFs.read(pkgJsonPath))["version"];
|
|
917
|
+
if (!version || typeof version !== "string") throw new Error(`No version found in ${packagePath}/package.json`);
|
|
918
|
+
const indent = detectIndent(raw);
|
|
919
|
+
const updated = {
|
|
920
|
+
...manifest,
|
|
921
|
+
version
|
|
922
|
+
};
|
|
923
|
+
syncFs.write(manifestPath, `${JSON.stringify(updated, null, indent)}\n`);
|
|
924
|
+
return {
|
|
925
|
+
version,
|
|
926
|
+
manifestPath
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/publish/cli.ts
|
|
931
|
+
function publishCommand() {
|
|
932
|
+
const cmd = new Command("publish").description("Prepare plugin for publishing").helpCommand(false);
|
|
933
|
+
cmd.command("sync-version").description("Sync version from packagePath/package.json into .plugin/plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
934
|
+
try {
|
|
935
|
+
const result = syncVersion(resolveRoot(opts.root), realSyncVersionFs);
|
|
936
|
+
output(result, () => {
|
|
937
|
+
printFields({
|
|
938
|
+
version: result.version,
|
|
939
|
+
manifest: result.manifestPath
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
} catch (err) {
|
|
943
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
return cmd;
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
658
950
|
//#region src/self-update/fs.ts
|
|
659
951
|
function globalStatePath$1() {
|
|
660
952
|
return path.join(os.homedir(), ".agents", "universal-plugin.json");
|
|
@@ -776,7 +1068,7 @@ function realSyncFs() {
|
|
|
776
1068
|
};
|
|
777
1069
|
}
|
|
778
1070
|
function syncCommand() {
|
|
779
|
-
const cmd = new Command("sync").description("Manage cross-vendor plugin sync").
|
|
1071
|
+
const cmd = new Command("sync").description("Manage cross-vendor plugin sync").helpCommand(false);
|
|
780
1072
|
cmd.command("apply").description("Apply a pending sync action").argument("<action-id>", "Action ID from ~/.agents/universal-plugin.json").action((actionId) => {
|
|
781
1073
|
const result = applySyncAction({
|
|
782
1074
|
actionId,
|
|
@@ -799,11 +1091,17 @@ function syncCommand() {
|
|
|
799
1091
|
//#endregion
|
|
800
1092
|
//#region src/cli.ts
|
|
801
1093
|
const program = new Command();
|
|
802
|
-
program.name("universal-plugin").description("Universal AI agent plugin build tool").version("0.0.0").
|
|
803
|
-
|
|
1094
|
+
program.name("universal-plugin").description("Universal AI agent plugin build tool").version("0.0.0").helpCommand(false);
|
|
1095
|
+
function pluginCommand() {
|
|
1096
|
+
const cmd = new Command("plugin").description("Author the canonical plugin manifest (build; validate, init planned)");
|
|
1097
|
+
cmd.addCommand(buildCommand());
|
|
1098
|
+
return cmd;
|
|
1099
|
+
}
|
|
1100
|
+
program.addCommand(pluginCommand());
|
|
804
1101
|
program.addCommand(cleanCommand());
|
|
805
1102
|
program.addCommand(governanceCommand());
|
|
806
1103
|
program.addCommand(prepareCommand());
|
|
1104
|
+
program.addCommand(publishCommand());
|
|
807
1105
|
program.addCommand(syncCommand());
|
|
808
1106
|
program.addCommand(selfUpdateCommand());
|
|
809
1107
|
program.parseAsync(process.argv).catch((err) => {
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Plugin Design
|
|
2
|
+
|
|
3
|
+
Authoritative rules for creating, validating, and transforming cross-vendor agent plugins. Apply when creating, auditing, or distributing a plugin; see **skill-design** governance for standalone skill authoring rules.
|
|
4
|
+
|
|
5
|
+
A **plugin** is the distribution unit — it bundles skills, MCP servers, hooks, commands, agents, and other extensions into a single installable package. A **skill** is the capability unit inside a plugin. Install plugins; invoke skills.
|
|
6
|
+
|
|
7
|
+
## Source of Truth: `.plugin/plugin.json`
|
|
8
|
+
|
|
9
|
+
Author `.plugin/plugin.json` as the canonical manifest. All vendor manifests are derived from it via `build`. This file is never read directly by vendors at runtime; it is the single source that the build layer transforms into each vendor's manifest.
|
|
10
|
+
|
|
11
|
+
Schema declaration (first field):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "$schema": "https://schema.cyberuni.dev/universal-agent-plugin/v1.json" }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Required fields
|
|
18
|
+
|
|
19
|
+
| Field | Type | Constraint |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `name` | string | 1–64 chars. Pattern: `^[a-z0-9]([a-z0-9\-.]*[a-z0-9])?$`. Lowercase letters, digits, hyphens, periods only. No leading/trailing `-` or `.`. No `--` or `..`. |
|
|
22
|
+
|
|
23
|
+
### Optional metadata fields
|
|
24
|
+
|
|
25
|
+
| Field | Type | Notes |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `version` | string | semver. **Required by Codex** — build fails if absent. |
|
|
28
|
+
| `description` | string | ≤ 1024 chars. **Required by Codex** — build fails if absent. |
|
|
29
|
+
| `author` | object | `{ name, email, url }` — all sub-fields optional. |
|
|
30
|
+
| `homepage` | string | URL. |
|
|
31
|
+
| `repository` | string \| object | URL string or `{ type, url }` object. |
|
|
32
|
+
| `license` | string | SPDX identifier (e.g. `"MIT"`). |
|
|
33
|
+
| `keywords` | string[] | Searchable tags for marketplace discovery. |
|
|
34
|
+
|
|
35
|
+
### Component path fields
|
|
36
|
+
|
|
37
|
+
Each accepts `string | string[] | { paths: string[] }`. Every path must start with `./`. No `../` segments — traversal rejected by conformant hosts.
|
|
38
|
+
|
|
39
|
+
| Field | Component | Core? | Notes |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| `skills` | Skill directories containing `SKILL.md` | Yes | Default: `./skills/` |
|
|
42
|
+
| `mcpServers` | `.mcp.json` path or inline MCP config | Yes | Default: `./.mcp.json` |
|
|
43
|
+
| `commands` | Slash command `.md` files | Extended | Default: `./commands/` |
|
|
44
|
+
| `agents` | Agent `.md` files | Extended | Default: `./agents/` |
|
|
45
|
+
| `rules` | Context rule `.mdc` files | Extended | Cursor-only; ignored by other hosts |
|
|
46
|
+
| `hooks` | `hooks.json` path or inline hook config | Extended | Canonical uses PascalCase events; build translates per vendor |
|
|
47
|
+
| `lspServers` | `.lsp.json` path | Extended | Claude Code only |
|
|
48
|
+
| `outputStyles` | Output style resources directory | Extended | Claude Code only |
|
|
49
|
+
|
|
50
|
+
A conformant host must support at least one core component (`skills` or `mcpServers`). Extended types are silently ignored on non-supporting hosts — do not rely on them for core plugin functionality.
|
|
51
|
+
|
|
52
|
+
### `vendorExtensions` field
|
|
53
|
+
|
|
54
|
+
Declares which vendor manifests `build` generates, and provides vendor-specific fields for each. Each key is a recognized vendor ID; its presence drives build output. An empty `{}` opts into that vendor's output with no vendor-specific fields.
|
|
55
|
+
|
|
56
|
+
| Vendor ID | Output path | Required beyond `name` |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `claude-code` | `.claude-plugin/plugin.json` | none |
|
|
59
|
+
| `cursor` | `.cursor-plugin/plugin.json` | none |
|
|
60
|
+
| `codex` | `.codex-plugin/plugin.json` | `version`, `description` |
|
|
61
|
+
| `copilot-cli` | `plugin.json` (repo root) | none |
|
|
62
|
+
|
|
63
|
+
Vendor-specific extension fields:
|
|
64
|
+
|
|
65
|
+
| Field | `claude-code` | `cursor` | `codex` | `copilot-cli` |
|
|
66
|
+
| --- | --- | --- | --- | --- |
|
|
67
|
+
| `displayName` | ✓ | — | — | — |
|
|
68
|
+
| `defaultEnabled` | ✓ (bool) | — | — | — |
|
|
69
|
+
| `userConfig` | ✓ (prompted at enable) | — | — | — |
|
|
70
|
+
| `channels` | ✓ | — | — | — |
|
|
71
|
+
| `dependencies` | ✓ (inter-plugin) | — | — | — |
|
|
72
|
+
| `themes` | ✓ | — | — | — |
|
|
73
|
+
| `monitors` | ✓ | — | — | — |
|
|
74
|
+
| `logo` | — | ✓ | — | — |
|
|
75
|
+
| `publisher` | — | ✓ | — | — |
|
|
76
|
+
| `category` | — | ✓ | — | ✓ |
|
|
77
|
+
| `tags` | — | ✓ | — | ✓ |
|
|
78
|
+
| `apps` | — | — | ✓ (→ `.app.json`) | — |
|
|
79
|
+
| `interface` | — | — | ✓ (marketplace metadata) | — |
|
|
80
|
+
|
|
81
|
+
## Canonical Directory Layout
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
<plugin-name>/
|
|
85
|
+
├── .plugin/
|
|
86
|
+
│ └── plugin.json ← canonical source of truth
|
|
87
|
+
│
|
|
88
|
+
├── skills/<skill-name>/SKILL.md ← shared: all vendors, identical format
|
|
89
|
+
│
|
|
90
|
+
├── commands/setup.md ← required when rules/ is present
|
|
91
|
+
├── commands/<cmd-name>.md ← Claude Code + Cursor + Copilot CLI
|
|
92
|
+
├── agents/<agent-name>.md ← Claude Code + Cursor + Copilot CLI
|
|
93
|
+
├── rules/<rule-name>.mdc ← Cursor-only always-on
|
|
94
|
+
│
|
|
95
|
+
├── hooks/hooks.json ← canonical hooks (PascalCase, ${PLUGIN_ROOT})
|
|
96
|
+
│
|
|
97
|
+
├── .mcp.json ← source of truth (all vendors)
|
|
98
|
+
├── mcp.json -> .mcp.json ← symlink (Cursor + open-plugin-spec)
|
|
99
|
+
│
|
|
100
|
+
└── README.md
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Generated build artifacts (gitignore or commit — author's choice):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
├── .claude-plugin/
|
|
107
|
+
│ ├── plugin.json ← generated
|
|
108
|
+
│ └── hooks/hooks.json ← generated (PascalCase, ${CLAUDE_PLUGIN_ROOT})
|
|
109
|
+
├── .cursor-plugin/
|
|
110
|
+
│ ├── plugin.json ← generated
|
|
111
|
+
│ └── hooks/hooks.json ← generated (camelCase, ${PLUGIN_ROOT} pass-through)
|
|
112
|
+
├── .codex-plugin/
|
|
113
|
+
│ ├── plugin.json ← generated
|
|
114
|
+
│ └── hooks/hooks.json ← generated (PascalCase, ${PLUGIN_ROOT} native)
|
|
115
|
+
└── plugin.json ← generated (copilot-cli root manifest)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Vendor Manifest Derivation
|
|
119
|
+
|
|
120
|
+
Build reads `.plugin/plugin.json`, applies the rules below, writes each vendor's output.
|
|
121
|
+
|
|
122
|
+
### Metadata field mapping
|
|
123
|
+
|
|
124
|
+
| Canonical field | Claude Code | Cursor | Codex | Copilot CLI |
|
|
125
|
+
| --- | --- | --- | --- | --- |
|
|
126
|
+
| `name` | ✓ required | ✓ required | ✓ required | ✓ required |
|
|
127
|
+
| `version` | ✓ optional | ✓ optional | ✓ **required** | ✓ optional |
|
|
128
|
+
| `description` | ✓ optional | ✓ optional | ✓ **required** | ✓ optional |
|
|
129
|
+
| `author`, `homepage`, `repository`, `license`, `keywords` | ✓ | ✓ | ✓ | ✓ |
|
|
130
|
+
|
|
131
|
+
### Component path field mapping
|
|
132
|
+
|
|
133
|
+
| Canonical field | Claude Code | Cursor | Codex | Copilot CLI |
|
|
134
|
+
| --- | --- | --- | --- | --- |
|
|
135
|
+
| `skills` | ✓ | ✓ | ✓ | ✓ |
|
|
136
|
+
| `mcpServers` | ✓ → `./.mcp.json` | adapt → `./mcp.json` (symlink) | ✓ → `./.mcp.json` | ✓ |
|
|
137
|
+
| `commands` | ✓ | ✓ | **omit** | ✓ |
|
|
138
|
+
| `agents` | ✓ | ✓ | **omit** | ✓ |
|
|
139
|
+
| `rules` | **omit** | ✓ | **omit** | **omit** |
|
|
140
|
+
| `hooks` | adapt → PascalCase, `${CLAUDE_PLUGIN_ROOT}` | adapt → camelCase, pass-through env | ✓ → PascalCase, `${PLUGIN_ROOT}` native | adapt → camelCase, pass-through env |
|
|
141
|
+
| `lspServers` | ✓ | **omit** | **omit** | **omit** |
|
|
142
|
+
| `outputStyles` | ✓ | **omit** | **omit** | **omit** |
|
|
143
|
+
|
|
144
|
+
## Hook Event Name Mapping
|
|
145
|
+
|
|
146
|
+
Canonical hooks file uses **PascalCase** event names. Build translates per vendor.
|
|
147
|
+
|
|
148
|
+
| Canonical (PascalCase) | Claude Code | Cursor | Codex | Copilot CLI |
|
|
149
|
+
| --- | --- | --- | --- | --- |
|
|
150
|
+
| `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` | `preToolUse` |
|
|
151
|
+
| `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` | `postToolUse` |
|
|
152
|
+
| `PostToolUseFailure` | `PostToolUseFailure` | — (drop+warn) | — (drop+warn) | — (drop+warn) |
|
|
153
|
+
| `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` | `sessionStart` |
|
|
154
|
+
| `SessionEnd` | `SessionEnd` | `sessionEnd` | `SessionEnd` | `sessionEnd` |
|
|
155
|
+
| `Stop` | `Stop` | — (drop+warn) | — (drop+warn) | `agentStop` |
|
|
156
|
+
| `UserPromptSubmit` | `UserPromptSubmit` | `beforeSubmitPrompt` | — (drop+warn) | — (drop+warn) |
|
|
157
|
+
| `Notification` | `Notification` | — (drop+warn) | — (drop+warn) | `notification` |
|
|
158
|
+
| `PermissionRequest` | `PermissionRequest` | — (drop+warn) | — (drop+warn) | `permissionRequest` |
|
|
159
|
+
| `SubagentStart` | — | `subagentStart` | — | — |
|
|
160
|
+
| `SubagentStop` | — | `subagentStop` | — | — |
|
|
161
|
+
| `PreCompact` | `PreCompact` | `preCompact` | — | — |
|
|
162
|
+
|
|
163
|
+
Events not in this table: pass through for `claude-code`; drop + warn for all others.
|
|
164
|
+
|
|
165
|
+
**Canonical hooks file** (`hooks/hooks.json` — PascalCase, `${PLUGIN_ROOT}`):
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"hooks": {
|
|
170
|
+
"PreToolUse": [
|
|
171
|
+
{
|
|
172
|
+
"matcher": "Write|Edit",
|
|
173
|
+
"hooks": [{ "type": "command", "command": "${PLUGIN_ROOT}/hooks/impl.sh", "timeout": 10 }]
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"SessionStart": [
|
|
177
|
+
{
|
|
178
|
+
"hooks": [{ "type": "command", "command": "${PLUGIN_ROOT}/hooks/impl.sh", "timeout": 10 }]
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Build generates vendor-specific versions. Do not hand-author the generated hook files.
|
|
186
|
+
|
|
187
|
+
## MCP: Symlink Rule
|
|
188
|
+
|
|
189
|
+
`.mcp.json` is the source of truth. `mcp.json` is always a symlink — never a regular file.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
ln -sf .mcp.json mcp.json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Runtime | Reads |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| Claude Code, Codex | `.mcp.json` |
|
|
198
|
+
| Cursor, open-plugin-spec | `mcp.json` (via symlink) |
|
|
199
|
+
|
|
200
|
+
If the repo needs explicit symlink tracking: `mcp.json symlink` in `.gitattributes`. MCP server startup failures are non-fatal.
|
|
201
|
+
|
|
202
|
+
## Environment Variable Mapping
|
|
203
|
+
|
|
204
|
+
| Canonical | Claude Code | Cursor | Codex | Copilot CLI |
|
|
205
|
+
| --- | --- | --- | --- | --- |
|
|
206
|
+
| `${PLUGIN_ROOT}` | `${CLAUDE_PLUGIN_ROOT}` | pass-through (undocumented) | `${PLUGIN_ROOT}` (native) | pass-through (undocumented) |
|
|
207
|
+
| `${PLUGIN_DATA}` | `${CLAUDE_PLUGIN_DATA}` | pass-through (undocumented) | `${PLUGIN_DATA}` (native) | pass-through (undocumented) |
|
|
208
|
+
|
|
209
|
+
`${PLUGIN_ROOT}` is ephemeral (changes on update). `${PLUGIN_DATA}` survives updates; use it for caches and generated artifacts. `${CLAUDE_PROJECT_DIR}` (Claude Code only) is the project root the agent launched from.
|
|
210
|
+
|
|
211
|
+
## Component Authoring Rules
|
|
212
|
+
|
|
213
|
+
**Skills:** Author `skills/<name>/SKILL.md` following the **skill-design** governance. Within a plugin, reference MCP tools by fully qualified name: `{plugin-name}:{server-name}__{tool-name}`.
|
|
214
|
+
|
|
215
|
+
**Commands:** One `.md` file per command in `commands/`. Filename (minus extension) is the command identifier. Optional frontmatter: `description`, `argument-hint`, `allowed-tools`, `disable-model-invocation`. `$ARGUMENTS` expands to user input.
|
|
216
|
+
|
|
217
|
+
**Agents:** One `.md` file per agent in `agents/`. Required frontmatter: `name` (1–64 lowercase alphanumeric + hyphens), `description` (≤ 1024 chars). Body is the agent system prompt.
|
|
218
|
+
|
|
219
|
+
**Rules (Cursor-only):** `.mdc` files in `rules/`. Required frontmatter: `description`. Optional: `alwaysApply` (bool), `globs` (file-pattern array). Bundle `commands/setup.md` to merge rule content into project's `AGENTS.md` — after that merge, `.mdc` files are redundant.
|
|
220
|
+
|
|
221
|
+
**Decision tree for always-on guidance:**
|
|
222
|
+
- Situation-triggered → **skill** (all agents)
|
|
223
|
+
- Always-on, cross-agent → merge into **AGENTS.md**
|
|
224
|
+
- Always-on, Cursor-only → `rules/` + `commands/setup.md`
|
|
225
|
+
|
|
226
|
+
## Namespacing
|
|
227
|
+
|
|
228
|
+
| Component | Format |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| Skills | `{plugin-name}:{skill-name}` |
|
|
231
|
+
| MCP tools | `mcp__plugin_{plugin-name}_{server-name}__{tool-name}` |
|
|
232
|
+
| Commands / agents | `{plugin-name}:{component-name}` |
|
|
233
|
+
|
|
234
|
+
## Distribution
|
|
235
|
+
|
|
236
|
+
| Scope | Claude Code | Cursor | Codex |
|
|
237
|
+
| --- | --- | --- | --- |
|
|
238
|
+
| **Personal** | `~/.claude/plugins/local/<name>` symlink | `~/.cursor/plugins/local/<name>` symlink + reload | `~/.agents/plugins/marketplace.json` |
|
|
239
|
+
| **Team** | npm private package | Cursor Teams admin import | `.agents/plugins/marketplace.json` in repo |
|
|
240
|
+
| **Public** | PR to `anthropics/claude-plugins-official` | `cursor.com/marketplace/publish` | `codex plugin marketplace add` |
|
|
241
|
+
|
|
242
|
+
Default scope: **team**.
|
|
243
|
+
|
|
244
|
+
**npm distribution:** All manifest directories (`.plugin/`, `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`) and component directories (`skills/`, `commands/`, `agents/`, `hooks/`) must be in `package.json#files`. `package.json` carries distribution metadata only — no plugin semantics.
|
|
245
|
+
|
|
246
|
+
## Anti-Patterns
|
|
247
|
+
|
|
248
|
+
- Using `../` in any manifest-declared path
|
|
249
|
+
- Hardcoding absolute paths instead of `${PLUGIN_ROOT}` or `${PLUGIN_DATA}`
|
|
250
|
+
- Committing `mcp.json` as a regular file — must always be a symlink to `.mcp.json`
|
|
251
|
+
- Hand-authoring vendor hook files — use canonical `hooks/hooks.json` and let build generate vendor versions
|
|
252
|
+
- Relying on extended component types (commands, rules, agents, hooks) for core functionality — silently ignored on non-supporting hosts
|
|
253
|
+
- Duplicating SKILL.md content in `plugin.json` — the manifest is a path index, never a content mirror
|
|
254
|
+
- Using `rules/` for cross-agent always-on guidance — use AGENTS.md instead
|
|
255
|
+
- Putting install-time metadata in `plugin.json` instead of `skill.json`
|
|
256
|
+
|
|
257
|
+
## Cross-Platform Portability
|
|
258
|
+
|
|
259
|
+
| Runtime | SKILL.md native | Plugin manifest |
|
|
260
|
+
| --- | --- | --- |
|
|
261
|
+
| Claude Code | Yes | `.claude-plugin/plugin.json` |
|
|
262
|
+
| Codex | Yes | `.codex-plugin/plugin.json` (`version` + `description` required) |
|
|
263
|
+
| Cursor | Yes (conversion) | `.cursor-plugin/plugin.json` |
|
|
264
|
+
| Copilot CLI | Yes | `plugin.json` at repo root |
|
|
265
|
+
| Gemini CLI, GitHub Copilot, Amp | Yes | Different manifest paths — require separate authoring |
|
|
266
|
+
| Windsurf | Needs conversion | 6,000 char/file hard limit; 12,000 chars total |
|
|
267
|
+
| Zed, Aider, Continue.dev, Cline | No | Incompatible formats |
|
|
268
|
+
|
|
269
|
+
Portability rules for skill bodies: keep each `SKILL.md` body under 6,000 chars; use forward slashes in path references; declare environment requirements in `compatibility` frontmatter; do not embed vendor-specific syntax in skill bodies.
|
|
270
|
+
|
|
271
|
+
## References
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
npx universal-plugin@<version> governance show skill-design
|
|
275
|
+
npx universal-plugin@<version> governance show skill-repo-structure
|
|
276
|
+
npx universal-plugin@<version> governance show agent-tool-output
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Spec: https://github.com/cyberuni/universal-plugin/blob/main/spec/universal-plugin-system.md
|
package/package.json
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
2
|
+
"name": "universal-plugin",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Universal AI agent plugin build tool",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-plugin",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"cursor",
|
|
9
|
+
"codex",
|
|
10
|
+
"copilot-cli"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/cyberuni/cyberplace.git",
|
|
15
|
+
"directory": "packages/universal-plugin"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "unional <homawong@gmail.com>",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"universal-plugin": "bin/universal-plugin.mjs"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"dist",
|
|
29
|
+
"governances"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^14.0.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^24.10.1",
|
|
36
|
+
"knip": "^6.14.1",
|
|
37
|
+
"tsdown": "^0.22.0",
|
|
38
|
+
"tsx": "^4.22.3",
|
|
39
|
+
"typescript": "^6.0.0",
|
|
40
|
+
"vitest": "^4.1.7"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"dev": "tsx src/cli.ts",
|
|
52
|
+
"knip": "knip",
|
|
53
|
+
"lint": "biome check .",
|
|
54
|
+
"test": "pnpm build && vitest run src",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"verify": "pnpm typecheck && pnpm lint && pnpm test"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# universal-plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/universal-plugin)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Universal AI agent plugin build tool. Author one canonical plugin manifest (`.plugin/plugin.json`) and generate vendor-specific manifests for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
No install required — run with `npx`:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx universal-plugin <command>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or pin to an exact version for reproducible builds:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx universal-plugin@0.2.0 <command>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### plugin — author the canonical manifest
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Generate vendor manifests from .plugin/plugin.json
|
|
28
|
+
npx universal-plugin plugin build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`validate` and `init` are specified but implementation is deferred.
|
|
32
|
+
|
|
33
|
+
### sync — cross-vendor plugin sync
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
# Detect cross-vendor sync actions from a vendor's manifest
|
|
37
|
+
npx universal-plugin prepare <vendor-id> # e.g. claude-code
|
|
38
|
+
npx universal-plugin prepare <vendor-id> --scope project --root <path>
|
|
39
|
+
npx universal-plugin prepare <vendor-id> --dry-run # print action count without writing state
|
|
40
|
+
|
|
41
|
+
# Apply a pending sync action
|
|
42
|
+
npx universal-plugin sync apply <action-id>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### publish
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# Sync version from packagePath/package.json into .plugin/plugin.json
|
|
49
|
+
npx universal-plugin publish sync-version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### governance
|
|
53
|
+
|
|
54
|
+
Version-pinned agent-tool contracts, read at runtime.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx universal-plugin governance list
|
|
58
|
+
npx universal-plugin governance show plugin-design
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Housekeeping
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx universal-plugin clean # remove the asset store
|
|
65
|
+
npx universal-plugin self-update <version> # update the version pin in hook files
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|