universal-plugin 0.1.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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const dir = dirname(fileURLToPath(import.meta.url))
6
+ await import(join(dir, '..', 'dist', 'cli.mjs'))
package/dist/cli.mjs ADDED
@@ -0,0 +1,814 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import * as fsNode from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import * as childProcess from "node:child_process";
8
+ //#region src/cli-options.ts
9
+ /** Repo root; resolves to cwd when omitted. */
10
+ const ROOT_OPTION = new Option("--root <path>", "Plugin root directory");
11
+ function resolveRoot(root) {
12
+ return root ?? process.cwd();
13
+ }
14
+ //#endregion
15
+ //#region src/state/state.ts
16
+ const KNOWN_ACTION_TYPES = new Set([
17
+ "install",
18
+ "upgrade",
19
+ "remove"
20
+ ]);
21
+ function emptyState() {
22
+ return {
23
+ schemaVersion: 1,
24
+ snapshots: {},
25
+ dismissed: {},
26
+ pendingActions: [],
27
+ uniPluginUpdates: {},
28
+ plugins: {},
29
+ assets: {}
30
+ };
31
+ }
32
+ function mergeSafeState(raw) {
33
+ return {
34
+ ...raw,
35
+ plugins: raw.plugins ?? {},
36
+ assets: raw.assets ?? {},
37
+ pendingActions: (raw.pendingActions ?? []).filter((a) => KNOWN_ACTION_TYPES.has(a.type))
38
+ };
39
+ }
40
+ function writePluginIndex(state, vendorId, pluginName, entry) {
41
+ return {
42
+ ...state,
43
+ plugins: {
44
+ ...state.plugins,
45
+ [vendorId]: {
46
+ ...state.plugins[vendorId] ?? {},
47
+ [pluginName]: entry
48
+ }
49
+ }
50
+ };
51
+ }
52
+ function writeAssetIndex(state, pluginName, entry) {
53
+ return {
54
+ ...state,
55
+ assets: {
56
+ ...state.assets,
57
+ [pluginName]: entry
58
+ }
59
+ };
60
+ }
61
+ function takeSnapshot(state, vendorId, scope, plugins, takenAt) {
62
+ return {
63
+ ...state,
64
+ snapshots: {
65
+ ...state.snapshots,
66
+ [vendorId]: {
67
+ ...state.snapshots[vendorId] ?? {},
68
+ [scope]: {
69
+ takenAt,
70
+ plugins
71
+ }
72
+ }
73
+ }
74
+ };
75
+ }
76
+ function isDismissed(state, vendorId, scope, plugin, version) {
77
+ const entry = state.dismissed[`${vendorId}/${scope}/${plugin}`];
78
+ if (!entry) return false;
79
+ if (entry.reason === "keep") return true;
80
+ return entry.version === version;
81
+ }
82
+ function addPendingAction(state, action) {
83
+ const key = `${action.type}|${action.plugin}|${action.toVendor}|${action.scope}`;
84
+ if (state.pendingActions.some((a) => `${a.type}|${a.plugin}|${a.toVendor}|${a.scope}` === key)) return state;
85
+ return {
86
+ ...state,
87
+ pendingActions: [...state.pendingActions, action]
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region src/asset-store/asset-store.ts
92
+ const ASSET_DIRS = [
93
+ "governances",
94
+ "disciplines",
95
+ "guidelines",
96
+ "templates"
97
+ ];
98
+ function globalStorePath() {
99
+ return path.join(os.homedir(), ".agents", ".universal-plugin", "plugins");
100
+ }
101
+ function projectStorePath(root) {
102
+ return path.join(root, ".agents", ".universal-plugin", "plugins");
103
+ }
104
+ function storeEntryPath(storePath, segment) {
105
+ return path.join(storePath, segment);
106
+ }
107
+ //#endregion
108
+ //#region src/asset-store/fs.ts
109
+ function entryExists(entryPath) {
110
+ return fsNode.existsSync(entryPath);
111
+ }
112
+ function populateEntry(entryPath, pluginRoot) {
113
+ for (const dir of ASSET_DIRS) {
114
+ const src = path.join(pluginRoot, dir);
115
+ const dest = path.join(entryPath, dir);
116
+ if (!fsNode.existsSync(src)) continue;
117
+ fsNode.mkdirSync(dest, { recursive: true });
118
+ for (const file of fsNode.readdirSync(src)) fsNode.copyFileSync(path.join(src, file), path.join(dest, file));
119
+ }
120
+ }
121
+ function removeStore(storePath) {
122
+ fsNode.rmSync(storePath, {
123
+ recursive: true,
124
+ force: true
125
+ });
126
+ }
127
+ //#endregion
128
+ //#region src/asset-store/cli.ts
129
+ function globalStatePath$3() {
130
+ return path.join(os.homedir(), ".agents", "universal-plugin.json");
131
+ }
132
+ function clearStateIndex(statePath) {
133
+ try {
134
+ const cleared = {
135
+ ...mergeSafeState(JSON.parse(fsNode.readFileSync(statePath, "utf8"))),
136
+ plugins: {},
137
+ assets: {}
138
+ };
139
+ fsNode.writeFileSync(statePath, JSON.stringify(cleared, null, 2) + "\n", "utf8");
140
+ } catch (err) {
141
+ if (err.code !== "ENOENT") throw err;
142
+ }
143
+ }
144
+ function cleanCommand() {
145
+ return new Command("clean").description("Remove the asset store").addOption(new Option("--state", "Also clear plugins and assets from state JSON")).addOption(new Option("--scope <scope>", "global or project").choices(["global", "project"]).default("global")).option("--root <path>", "Project root (for --scope project)").action((opts) => {
146
+ const storePath = opts.scope === "project" ? projectStorePath(resolveRoot(opts.root)) : globalStorePath();
147
+ removeStore(storePath);
148
+ process.stdout.write(`Removed ${storePath}\n`);
149
+ if (opts.state) {
150
+ clearStateIndex(globalStatePath$3());
151
+ process.stdout.write("Cleared plugins and assets from state JSON\n");
152
+ }
153
+ });
154
+ }
155
+ //#endregion
156
+ //#region src/output.ts
157
+ function printJson(data) {
158
+ console.log(JSON.stringify(data, null, 2));
159
+ }
160
+ function printFields(fields) {
161
+ const entries = Object.entries(fields).filter(([, v]) => v != null);
162
+ const width = Math.max(...entries.map(([k]) => k.length));
163
+ for (const [key, val] of entries) console.log(`${key.padEnd(width)} ${val}`);
164
+ }
165
+ function printTable(items, cols) {
166
+ if (items.length === 0) {
167
+ console.log("(none)");
168
+ return;
169
+ }
170
+ const widths = cols.map((c) => Math.max(c.label.length, ...items.map((i) => c.get(i).length)));
171
+ console.log(cols.map((c, i) => c.label.toUpperCase().padEnd(widths[i])).join(" "));
172
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
173
+ for (const item of items) console.log(cols.map((c, i) => c.get(item).padEnd(widths[i])).join(" "));
174
+ }
175
+ function getFormat() {
176
+ const argv = process.argv;
177
+ const fmtIdx = argv.indexOf("--format");
178
+ if (fmtIdx !== -1) return argv[fmtIdx + 1];
179
+ if (argv.includes("--json")) return "json";
180
+ }
181
+ function isJsonOutput() {
182
+ return getFormat() === "json";
183
+ }
184
+ function output(data, readable) {
185
+ if (isJsonOutput()) printJson(data);
186
+ else readable();
187
+ }
188
+ //#endregion
189
+ //#region src/build/build.ts
190
+ const VENDOR_OUTPUT = {
191
+ "claude-code": ".claude-plugin/plugin.json",
192
+ cursor: ".cursor-plugin/plugin.json",
193
+ codex: ".codex-plugin/plugin.json",
194
+ "copilot-cli": "plugin.json"
195
+ };
196
+ const KNOWN_VENDORS = new Set(Object.keys(VENDOR_OUTPUT));
197
+ function readManifest(root) {
198
+ const manifestPath = path.join(root, ".plugin", "plugin.json");
199
+ if (!fsNode.existsSync(manifestPath)) throw new Error(`No .plugin/plugin.json found at ${root}`);
200
+ return JSON.parse(fsNode.readFileSync(manifestPath, "utf8"));
201
+ }
202
+ function validateManifest(manifest) {
203
+ const errors = [];
204
+ if (!manifest.name) errors.push("name is required");
205
+ if (manifest.vendorExtensions?.codex && !manifest.description) errors.push("description is required when targeting codex");
206
+ if (manifest.vendorExtensions?.codex && !manifest.version) errors.push("version is required when targeting codex");
207
+ return errors;
208
+ }
209
+ function buildPlugin(root, opts = {}) {
210
+ const manifest = readManifest(root);
211
+ const errors = validateManifest(manifest);
212
+ if (errors.length > 0) throw new Error(`plugin.json validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
213
+ const warnings = [];
214
+ const vendorExtensions = manifest.vendorExtensions ?? {};
215
+ let vendors = Object.keys(vendorExtensions).filter((v) => {
216
+ if (!KNOWN_VENDORS.has(v)) {
217
+ warnings.push(`Unknown vendor "${v}" in vendorExtensions — skipped`);
218
+ return false;
219
+ }
220
+ return true;
221
+ });
222
+ if (opts.vendor) {
223
+ if (!vendors.includes(opts.vendor)) throw new Error(`Vendor "${opts.vendor}" not declared in vendorExtensions`);
224
+ vendors = [opts.vendor];
225
+ }
226
+ if (vendors.length === 0) {
227
+ warnings.push("No vendors declared in vendorExtensions — nothing to build");
228
+ return {
229
+ vendors: [],
230
+ written: [],
231
+ warnings
232
+ };
233
+ }
234
+ const written = [];
235
+ const { vendorExtensions: _ext, $schema: _schema, ...canonical } = manifest;
236
+ for (const vendor of vendors) {
237
+ const outputPath = path.join(root, VENDOR_OUTPUT[vendor]);
238
+ const outputDir = path.dirname(outputPath);
239
+ const vendorFields = vendorExtensions[vendor] ?? {};
240
+ const vendorManifest = {
241
+ ...canonical,
242
+ ...vendorFields
243
+ };
244
+ if (opts.verbose) {
245
+ console.log(`[${vendor}] → ${outputPath}`);
246
+ for (const key of Object.keys(vendorFields)) console.log(` + ${key} (from vendorExtensions)`);
247
+ }
248
+ if (!opts.dryRun) {
249
+ if (opts.clean && fsNode.existsSync(outputPath)) fsNode.unlinkSync(outputPath);
250
+ fsNode.mkdirSync(outputDir, { recursive: true });
251
+ fsNode.writeFileSync(outputPath, `${JSON.stringify(vendorManifest, null, 2)}\n`);
252
+ }
253
+ written.push(outputPath);
254
+ }
255
+ return {
256
+ vendors,
257
+ written,
258
+ warnings
259
+ };
260
+ }
261
+ //#endregion
262
+ //#region src/build/cli.ts
263
+ function buildCommand() {
264
+ 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) => {
266
+ try {
267
+ const result = buildPlugin(resolveRoot(opts.root), {
268
+ vendor: opts.vendor,
269
+ dryRun: opts.dryRun,
270
+ verbose: opts.verbose,
271
+ clean: opts.clean
272
+ });
273
+ for (const warning of result.warnings) process.stderr.write(`warn: ${warning}\n`);
274
+ output(result, () => {
275
+ if (result.vendors.length === 0) return;
276
+ printFields({ vendors: result.vendors.join(", ") });
277
+ printTable(result.written.map((p) => ({ path: p })), [{
278
+ label: "output",
279
+ get: (r) => opts.dryRun ? `(dry-run) ${r.path}` : r.path
280
+ }]);
281
+ });
282
+ } catch (err) {
283
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
284
+ process.exit(1);
285
+ }
286
+ });
287
+ return cmd;
288
+ }
289
+ //#endregion
290
+ //#region src/governance/fs.ts
291
+ const realGovernanceFs = {
292
+ exists: (p) => fsNode.existsSync(p),
293
+ read: (p) => fsNode.readFileSync(p, "utf8"),
294
+ list: (dir) => {
295
+ if (!fsNode.existsSync(dir)) return [];
296
+ return fsNode.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
297
+ }
298
+ };
299
+ //#endregion
300
+ //#region src/governance/governance.ts
301
+ function getManagedDir() {
302
+ if (process.platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
303
+ if (process.platform === "win32") return path.join(process.env["ProgramData"] ?? "C:\\ProgramData", "UniPlugin", "governances");
304
+ return "/etc/universal-plugin/governances";
305
+ }
306
+ function getUserDir() {
307
+ return path.join(os.homedir(), ".agents", "governances");
308
+ }
309
+ function getProjectDir(root) {
310
+ return path.join(root, "governances");
311
+ }
312
+ function getPackageDir() {
313
+ const thisFile = fileURLToPath(import.meta.url);
314
+ return path.join(path.dirname(thisFile), "..", "governances");
315
+ }
316
+ function getScopedPaths(root) {
317
+ return [
318
+ {
319
+ scope: "managed",
320
+ dir: getManagedDir()
321
+ },
322
+ {
323
+ scope: "project",
324
+ dir: getProjectDir(root)
325
+ },
326
+ {
327
+ scope: "user",
328
+ dir: getUserDir()
329
+ },
330
+ {
331
+ scope: "package",
332
+ dir: getPackageDir()
333
+ }
334
+ ];
335
+ }
336
+ function showGovernance(name, root, govFs, storeOpts) {
337
+ const slashIdx = name.indexOf("/");
338
+ if (slashIdx !== -1 && storeOpts) {
339
+ const pluginName = name.slice(0, slashIdx);
340
+ const assetName = name.slice(slashIdx + 1);
341
+ for (const scope of [
342
+ "managed",
343
+ "project",
344
+ "user"
345
+ ]) {
346
+ let dir;
347
+ if (scope === "managed") dir = getManagedDir();
348
+ else if (scope === "project") dir = getProjectDir(root);
349
+ else dir = getUserDir();
350
+ const filePath = path.join(dir, pluginName, `${assetName}.md`);
351
+ if (govFs.exists(filePath)) return {
352
+ content: govFs.read(filePath),
353
+ scope
354
+ };
355
+ }
356
+ const entry = storeOpts.state.assets[pluginName];
357
+ if (!entry) return null;
358
+ const segment = path.join(entry.source, `${pluginName}@${entry.version}`);
359
+ const storePath = path.join(storeOpts.globalStorePath, segment, "governances", `${assetName}.md`);
360
+ if (govFs.exists(storePath)) return {
361
+ content: govFs.read(storePath),
362
+ scope: "store"
363
+ };
364
+ return null;
365
+ }
366
+ for (const { scope, dir } of getScopedPaths(root)) {
367
+ const filePath = path.join(dir, `${name}.md`);
368
+ if (govFs.exists(filePath)) return {
369
+ content: govFs.read(filePath),
370
+ scope
371
+ };
372
+ }
373
+ return null;
374
+ }
375
+ function listGovernances(root, govFs) {
376
+ const seen = /* @__PURE__ */ new Set();
377
+ const entries = [];
378
+ for (const { scope, dir } of getScopedPaths(root)) for (const name of govFs.list(dir)) if (!seen.has(name)) {
379
+ seen.add(name);
380
+ entries.push({
381
+ name,
382
+ scope
383
+ });
384
+ }
385
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
386
+ }
387
+ //#endregion
388
+ //#region src/governance/cli.ts
389
+ function readGlobalState() {
390
+ const p = path.join(os.homedir(), ".agents", "universal-plugin.json");
391
+ try {
392
+ return mergeSafeState(JSON.parse(fsNode.readFileSync(p, "utf8")));
393
+ } catch {
394
+ return emptyState();
395
+ }
396
+ }
397
+ function governanceCommand() {
398
+ const cmd = new Command("governance").description("Manage plugin governances").addHelpCommand(false);
399
+ cmd.command("show <name>").description("Show a governance by name").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((name, opts) => {
400
+ const result = showGovernance(name, resolveRoot(opts.root), realGovernanceFs, {
401
+ state: readGlobalState(),
402
+ globalStorePath: globalStorePath()
403
+ });
404
+ if (!result) {
405
+ process.stderr.write(`Governance "${name}" not found\n`);
406
+ process.exit(1);
407
+ }
408
+ output(result, () => {
409
+ process.stdout.write(result.content);
410
+ });
411
+ });
412
+ cmd.command("list").description("List available governances").addOption(ROOT_OPTION).addOption(new Option("--json").hideHelp()).action((opts) => {
413
+ const entries = listGovernances(resolveRoot(opts.root), realGovernanceFs);
414
+ output(entries, () => {
415
+ printTable(entries, [{
416
+ label: "name",
417
+ get: (e) => e.name
418
+ }, {
419
+ label: "scope",
420
+ get: (e) => e.scope
421
+ }]);
422
+ });
423
+ });
424
+ return cmd;
425
+ }
426
+ //#endregion
427
+ //#region src/vendor-registry/vendor-registry.ts
428
+ function lookupVendor(registry, vendorId) {
429
+ return registry[vendorId] ?? null;
430
+ }
431
+ function mergeRegistries(base, override) {
432
+ const result = { ...base };
433
+ for (const [id, config] of Object.entries(override)) result[id] = {
434
+ ...base[id] ?? {},
435
+ ...config
436
+ };
437
+ return result;
438
+ }
439
+ //#endregion
440
+ //#region src/vendor-registry/fs.ts
441
+ function bundledRegistryPath() {
442
+ const thisFile = fileURLToPath(import.meta.url);
443
+ return path.join(path.dirname(thisFile), "data", "vendors.json");
444
+ }
445
+ function userOverridePath() {
446
+ return path.join(os.homedir(), ".agents", "universal-plugin-vendors.json");
447
+ }
448
+ function loadRegistry() {
449
+ const bundled = JSON.parse(fsNode.readFileSync(bundledRegistryPath(), "utf8"));
450
+ try {
451
+ return mergeRegistries(bundled, JSON.parse(fsNode.readFileSync(userOverridePath(), "utf8")));
452
+ } catch (err) {
453
+ if (err.code === "ENOENT") return bundled;
454
+ throw err;
455
+ }
456
+ }
457
+ //#endregion
458
+ //#region src/prepare/fs.ts
459
+ function expandHome(p) {
460
+ return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
461
+ }
462
+ function globalStatePath$2() {
463
+ return path.join(os.homedir(), ".agents", "universal-plugin.json");
464
+ }
465
+ function projectStatePath(root) {
466
+ return path.join(root, ".agents", "universal-plugin.json");
467
+ }
468
+ function readStateFile(filePath) {
469
+ try {
470
+ return mergeSafeState(JSON.parse(fsNode.readFileSync(filePath, "utf8")));
471
+ } catch (err) {
472
+ if (err.code === "ENOENT") return null;
473
+ throw err;
474
+ }
475
+ }
476
+ function writeStateFile(filePath, state) {
477
+ fsNode.mkdirSync(path.dirname(filePath), { recursive: true });
478
+ fsNode.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
479
+ }
480
+ function realPrepareFs(vendor, projectRoot) {
481
+ return {
482
+ readManifest() {
483
+ if (!vendor.globalManifest) return {};
484
+ const manifestPath = expandHome(vendor.globalManifest);
485
+ try {
486
+ const raw = JSON.parse(fsNode.readFileSync(manifestPath, "utf8"));
487
+ return Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === "object" && v !== null && "version" in v ? String(v.version) : String(v)]));
488
+ } catch (err) {
489
+ if (err.code === "ENOENT") return {};
490
+ throw err;
491
+ }
492
+ },
493
+ readPluginRoots() {
494
+ if (!vendor.globalPluginDir) return {};
495
+ const pluginDir = expandHome(vendor.globalPluginDir);
496
+ const manifest = this.readManifest();
497
+ const home = os.homedir();
498
+ return Object.fromEntries(Object.keys(manifest).map((name) => {
499
+ const absPath = path.join(pluginDir, name);
500
+ return [name, absPath.startsWith(home) ? "~" + absPath.slice(home.length) : absPath];
501
+ }));
502
+ },
503
+ readGlobalState: () => readStateFile(globalStatePath$2()) ?? emptyState(),
504
+ readProjectState: () => projectRoot ? readStateFile(projectStatePath(projectRoot)) : null,
505
+ writeGlobalState: (s) => writeStateFile(globalStatePath$2(), s),
506
+ writeProjectState: (s) => {
507
+ if (projectRoot) writeStateFile(projectStatePath(projectRoot), s);
508
+ }
509
+ };
510
+ }
511
+ function populateStoreFromVendorCache(pluginRoots, versions) {
512
+ const storePath = globalStorePath();
513
+ for (const [pluginName, pluginRoot] of Object.entries(pluginRoots)) {
514
+ const entryPath = storeEntryPath(storePath, `npm/${pluginName}@${versions[pluginName] ?? "unknown"}`);
515
+ if (entryExists(entryPath)) continue;
516
+ populateEntry(entryPath, pluginRoot);
517
+ }
518
+ }
519
+ //#endregion
520
+ //#region src/prepare/delta.ts
521
+ function computeDelta(input) {
522
+ const { vendorId, scope, currentPlugins, state, now } = input;
523
+ const previous = state.snapshots[vendorId]?.[scope];
524
+ if (!previous) return [];
525
+ const previousPlugins = previous.plugins;
526
+ const added = Object.keys(currentPlugins).filter((p) => !(p in previousPlugins));
527
+ const removed = Object.keys(previousPlugins).filter((p) => !(p in currentPlugins));
528
+ const otherVendors = Object.entries(state.snapshots).filter(([id]) => id !== vendorId);
529
+ const actions = [];
530
+ let counter = 0;
531
+ const makeId = () => `delta-${now}-${++counter}`;
532
+ for (const plugin of added) {
533
+ const version = currentPlugins[plugin];
534
+ for (const [otherId, otherScopes] of otherVendors) {
535
+ const otherScope = otherScopes[scope];
536
+ if (!otherScope) continue;
537
+ if (plugin in otherScope.plugins) continue;
538
+ if (isDismissed(state, otherId, scope, plugin, version)) continue;
539
+ actions.push({
540
+ id: makeId(),
541
+ type: "install",
542
+ plugin,
543
+ version,
544
+ fromVendor: vendorId,
545
+ toVendor: otherId,
546
+ scope,
547
+ detectedAt: now
548
+ });
549
+ }
550
+ }
551
+ for (const plugin of removed) for (const [otherId, otherScopes] of otherVendors) {
552
+ const otherScope = otherScopes[scope];
553
+ if (!otherScope) continue;
554
+ if (!(plugin in otherScope.plugins)) continue;
555
+ const otherVersion = otherScope.plugins[plugin];
556
+ if (isDismissed(state, otherId, scope, plugin, otherVersion)) continue;
557
+ actions.push({
558
+ id: makeId(),
559
+ type: "remove",
560
+ plugin,
561
+ version: otherVersion,
562
+ fromVendor: vendorId,
563
+ toVendor: otherId,
564
+ scope,
565
+ detectedAt: now
566
+ });
567
+ }
568
+ for (const [otherId, otherScopes] of otherVendors) {
569
+ const otherScope = otherScopes[scope];
570
+ if (!otherScope) continue;
571
+ for (const [plugin, version] of Object.entries(currentPlugins)) {
572
+ const otherVersion = otherScope.plugins[plugin];
573
+ if (!otherVersion || otherVersion === version) continue;
574
+ if (!isNewerSemver(version, otherVersion)) continue;
575
+ if (isDismissed(state, otherId, scope, plugin, version)) continue;
576
+ actions.push({
577
+ id: makeId(),
578
+ type: "upgrade",
579
+ plugin,
580
+ version,
581
+ fromVendor: vendorId,
582
+ toVendor: otherId,
583
+ scope,
584
+ detectedAt: now
585
+ });
586
+ }
587
+ }
588
+ return actions;
589
+ }
590
+ function isNewerSemver(a, b) {
591
+ const [aMajor = 0, aMinor = 0, aPatch = 0] = a.split(".").map(Number);
592
+ const [bMajor = 0, bMinor = 0, bPatch = 0] = b.split(".").map(Number);
593
+ if (aMajor !== bMajor) return aMajor > bMajor;
594
+ if (aMinor !== bMinor) return aMinor > bMinor;
595
+ return aPatch > bPatch;
596
+ }
597
+ //#endregion
598
+ //#region src/prepare/prepare.ts
599
+ function runPrepare(opts) {
600
+ const { vendorId, scope, fs: prepareFs, now, dryRun } = opts;
601
+ const currentPlugins = prepareFs.readManifest();
602
+ const state = scope === "global" ? prepareFs.readGlobalState() : prepareFs.readProjectState() ?? emptyState();
603
+ const actions = computeDelta({
604
+ vendorId,
605
+ scope,
606
+ currentPlugins,
607
+ state,
608
+ now
609
+ });
610
+ let updatedState = takeSnapshot(state, vendorId, scope, currentPlugins, now);
611
+ for (const action of actions) updatedState = addPendingAction(updatedState, action);
612
+ const pluginRoots = prepareFs.readPluginRoots();
613
+ for (const [pluginName, pluginPath] of Object.entries(pluginRoots)) {
614
+ const version = currentPlugins[pluginName] ?? "unknown";
615
+ updatedState = writePluginIndex(updatedState, vendorId, pluginName, {
616
+ source: "npm",
617
+ path: pluginPath,
618
+ version
619
+ });
620
+ updatedState = writeAssetIndex(updatedState, pluginName, {
621
+ source: "npm",
622
+ version
623
+ });
624
+ }
625
+ if (!dryRun) if (scope === "global") prepareFs.writeGlobalState(updatedState);
626
+ else prepareFs.writeProjectState(updatedState);
627
+ return { newActionCount: actions.length };
628
+ }
629
+ //#endregion
630
+ //#region src/prepare/cli.ts
631
+ function prepareCommand() {
632
+ return new Command("prepare").description("Detect cross-vendor plugin sync actions").argument("<vendor-id>", "Vendor to read manifest from (e.g. claude-code)").addOption(new Option("--scope <scope>", "global or project").default("global").choices(["global", "project"])).option("--root <path>", "Project root for project-scope state file").option("--dry-run", "Print action count without writing state").action((vendorId, opts) => {
633
+ const vendor = lookupVendor(loadRegistry(), vendorId);
634
+ if (!vendor) {
635
+ process.stderr.write(`Unknown vendor: ${vendorId}\n`);
636
+ process.exit(1);
637
+ }
638
+ if (opts.scope === "project" && !opts.root) {
639
+ process.stderr.write("--root is required when --scope is project\n");
640
+ process.exit(1);
641
+ }
642
+ const now = (/* @__PURE__ */ new Date()).toISOString();
643
+ const prepareFs = realPrepareFs(vendor, opts.root);
644
+ const pluginRoots = prepareFs.readPluginRoots();
645
+ const manifest = prepareFs.readManifest();
646
+ const { newActionCount } = runPrepare({
647
+ vendorId,
648
+ scope: opts.scope,
649
+ fs: prepareFs,
650
+ now,
651
+ dryRun: opts.dryRun
652
+ });
653
+ populateStoreFromVendorCache(pluginRoots, manifest);
654
+ if (newActionCount > 0) process.stdout.write(`${newActionCount} plugin sync action(s) pending. Run /sync to review.\n`);
655
+ });
656
+ }
657
+ //#endregion
658
+ //#region src/self-update/fs.ts
659
+ function globalStatePath$1() {
660
+ return path.join(os.homedir(), ".agents", "universal-plugin.json");
661
+ }
662
+ function realSelfUpdateFs(hookFilePaths) {
663
+ return {
664
+ globHookFiles: () => hookFilePaths,
665
+ readFile: (p) => fsNode.readFileSync(p, "utf8"),
666
+ writeFile: (p, c) => fsNode.writeFileSync(p, c, "utf8"),
667
+ readGlobalState: () => {
668
+ try {
669
+ return mergeSafeState(JSON.parse(fsNode.readFileSync(globalStatePath$1(), "utf8")));
670
+ } catch (err) {
671
+ if (err.code === "ENOENT") return emptyState();
672
+ throw err;
673
+ }
674
+ },
675
+ writeGlobalState: (s) => {
676
+ fsNode.mkdirSync(path.dirname(globalStatePath$1()), { recursive: true });
677
+ fsNode.writeFileSync(globalStatePath$1(), JSON.stringify(s, null, 2) + "\n");
678
+ }
679
+ };
680
+ }
681
+ //#endregion
682
+ //#region src/self-update/self-update.ts
683
+ function runSelfUpdate(opts) {
684
+ const { toVersion, fs } = opts;
685
+ const [major] = toVersion.split(".");
686
+ if (!major || !/^\d+$/.test(major)) throw new Error(`Invalid version format: ${toVersion}`);
687
+ const pattern = new RegExp(`(npx universal-plugin@)${major}\\.[0-9]+\\.[0-9]+`, "g");
688
+ let updatedCount = 0;
689
+ for (const filePath of fs.globHookFiles()) {
690
+ const content = fs.readFile(filePath);
691
+ pattern.lastIndex = 0;
692
+ if (!pattern.test(content)) continue;
693
+ pattern.lastIndex = 0;
694
+ const updated = content.replace(pattern, `$1${toVersion}`);
695
+ if (updated !== content) {
696
+ fs.writeFile(filePath, updated);
697
+ updatedCount++;
698
+ }
699
+ }
700
+ const state = fs.readGlobalState();
701
+ const updates = { ...state.uniPluginUpdates };
702
+ delete updates[major];
703
+ fs.writeGlobalState({
704
+ ...state,
705
+ uniPluginUpdates: updates
706
+ });
707
+ return { updatedCount };
708
+ }
709
+ //#endregion
710
+ //#region src/self-update/cli.ts
711
+ function selfUpdateCommand() {
712
+ return new Command("self-update").description("Update universal-plugin version pin in universal-plugin hook files").argument("<version>", "Target version (e.g. 1.5.0)").action((toVersion) => {
713
+ const registry = loadRegistry();
714
+ const { updatedCount } = runSelfUpdate({
715
+ toVersion,
716
+ fs: realSelfUpdateFs(Object.values(registry).map((v) => v.hookGlob).filter((g) => g !== null).map((g) => g.replace("~", os.homedir())).filter((p) => {
717
+ try {
718
+ fsNode.accessSync(p);
719
+ return true;
720
+ } catch {
721
+ return false;
722
+ }
723
+ }))
724
+ });
725
+ process.stdout.write(`Updated ${updatedCount} hook file(s).\n`);
726
+ });
727
+ }
728
+ //#endregion
729
+ //#region src/sync/sync.ts
730
+ function applySyncAction(opts) {
731
+ const { actionId, registry, fs } = opts;
732
+ const state = fs.readGlobalState();
733
+ const action = state.pendingActions.find((a) => a.id === actionId);
734
+ if (!action) return { outcome: "not-found" };
735
+ const vendor = registry[action.toVendor];
736
+ const templateCommand = action.type === "install" ? vendor?.installCommand : action.type === "remove" ? vendor?.removeCommand : vendor?.updateCommand;
737
+ const withoutAction = {
738
+ ...state,
739
+ pendingActions: state.pendingActions.filter((a) => a.id !== actionId)
740
+ };
741
+ if (!templateCommand) {
742
+ fs.writeGlobalState(withoutAction);
743
+ return {
744
+ outcome: "manual",
745
+ instruction: `Install ${action.plugin}@${action.version} in ${action.toVendor} via its marketplace, then run: sync apply ${action.id}`
746
+ };
747
+ }
748
+ const command = templateCommand.replace("{name}", action.plugin).replace("{version}", action.version);
749
+ if (fs.shell(command) !== 0) return { outcome: "failed" };
750
+ fs.writeGlobalState(withoutAction);
751
+ return { outcome: "applied" };
752
+ }
753
+ //#endregion
754
+ //#region src/sync/cli.ts
755
+ function globalStatePath() {
756
+ return path.join(os.homedir(), ".agents", "universal-plugin.json");
757
+ }
758
+ function realSyncFs() {
759
+ return {
760
+ readGlobalState: () => {
761
+ try {
762
+ return mergeSafeState(JSON.parse(fsNode.readFileSync(globalStatePath(), "utf8")));
763
+ } catch (err) {
764
+ if (err.code === "ENOENT") return emptyState();
765
+ throw err;
766
+ }
767
+ },
768
+ writeGlobalState: (s) => {
769
+ fsNode.mkdirSync(path.dirname(globalStatePath()), { recursive: true });
770
+ fsNode.writeFileSync(globalStatePath(), JSON.stringify(s, null, 2) + "\n");
771
+ },
772
+ shell: (cmd) => childProcess.spawnSync(cmd, {
773
+ shell: true,
774
+ stdio: "inherit"
775
+ }).status ?? 1
776
+ };
777
+ }
778
+ function syncCommand() {
779
+ const cmd = new Command("sync").description("Manage cross-vendor plugin sync").addHelpCommand(false);
780
+ cmd.command("apply").description("Apply a pending sync action").argument("<action-id>", "Action ID from ~/.agents/universal-plugin.json").action((actionId) => {
781
+ const result = applySyncAction({
782
+ actionId,
783
+ registry: loadRegistry(),
784
+ fs: realSyncFs(),
785
+ now: (/* @__PURE__ */ new Date()).toISOString()
786
+ });
787
+ if (result.outcome === "applied") process.stdout.write("Applied.\n");
788
+ else if (result.outcome === "manual") process.stdout.write(`${result.instruction}\n`);
789
+ else if (result.outcome === "not-found") {
790
+ process.stderr.write(`Action "${actionId}" not found in state file.\n`);
791
+ process.exit(1);
792
+ } else {
793
+ process.stderr.write("Apply failed. Try again or install manually.\n");
794
+ process.exit(1);
795
+ }
796
+ });
797
+ return cmd;
798
+ }
799
+ //#endregion
800
+ //#region src/cli.ts
801
+ const program = new Command();
802
+ program.name("universal-plugin").description("Universal AI agent plugin build tool").version("0.0.0").addHelpCommand(false);
803
+ program.addCommand(buildCommand());
804
+ program.addCommand(cleanCommand());
805
+ program.addCommand(governanceCommand());
806
+ program.addCommand(prepareCommand());
807
+ program.addCommand(syncCommand());
808
+ program.addCommand(selfUpdateCommand());
809
+ program.parseAsync(process.argv).catch((err) => {
810
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
811
+ process.exit(1);
812
+ });
813
+ //#endregion
814
+ export {};
@@ -0,0 +1,46 @@
1
+ {
2
+ "claude-code": {
3
+ "sessionStartEvent": "SessionStart",
4
+ "globalManifest": "~/.claude/plugins/installed_plugins.json",
5
+ "projectManifest": null,
6
+ "hookGlob": "~/.claude/plugins/universal-plugin/hooks/hooks.json",
7
+ "globalPluginDir": "~/.claude/plugins/",
8
+ "pluginRootSuffix": ".claude-plugin/plugin.json",
9
+ "installCommand": "claude plugin install {name}",
10
+ "removeCommand": "claude plugin remove {name}",
11
+ "updateCommand": "claude plugin update {name}@{version}"
12
+ },
13
+ "cursor": {
14
+ "sessionStartEvent": "sessionStart",
15
+ "globalManifest": null,
16
+ "projectManifest": null,
17
+ "hookGlob": null,
18
+ "globalPluginDir": null,
19
+ "pluginRootSuffix": ".cursor-plugin/plugin.json",
20
+ "installCommand": null,
21
+ "removeCommand": null,
22
+ "updateCommand": null
23
+ },
24
+ "codex": {
25
+ "sessionStartEvent": "SessionStart",
26
+ "globalManifest": null,
27
+ "projectManifest": null,
28
+ "hookGlob": null,
29
+ "globalPluginDir": null,
30
+ "pluginRootSuffix": ".codex-plugin/plugin.json",
31
+ "installCommand": null,
32
+ "removeCommand": null,
33
+ "updateCommand": null
34
+ },
35
+ "copilot-cli": {
36
+ "sessionStartEvent": "sessionStart",
37
+ "globalManifest": null,
38
+ "projectManifest": null,
39
+ "hookGlob": null,
40
+ "globalPluginDir": null,
41
+ "pluginRootSuffix": "plugin.json",
42
+ "installCommand": null,
43
+ "removeCommand": null,
44
+ "updateCommand": null
45
+ }
46
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "universal-plugin",
3
+ "version": "0.1.0",
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/cyber-universal-agent-plugin.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
+ ],
30
+ "scripts": {
31
+ "build": "tsdown",
32
+ "dev": "tsx src/cli.ts",
33
+ "knip": "knip",
34
+ "lint": "biome check .",
35
+ "prepack": "pnpm build",
36
+ "test": "pnpm build && vitest run src",
37
+ "test:watch": "vitest",
38
+ "typecheck": "tsc --noEmit",
39
+ "verify": "pnpm typecheck && pnpm lint && pnpm test"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^14.0.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^24.10.1",
46
+ "knip": "^6.14.1",
47
+ "tsdown": "^0.22.0",
48
+ "tsx": "^4.22.3",
49
+ "typescript": "^6.0.0",
50
+ "vitest": "^4.1.7"
51
+ },
52
+ "packageManager": "pnpm@11.0.0",
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "provenance": true
59
+ }
60
+ }