@amsterdamdatalabs/enact-extensions 0.1.47 → 0.1.49

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.
Files changed (74) hide show
  1. package/bundled-extensions.json +1 -4
  2. package/dist/create/claude.d.ts.map +1 -1
  3. package/dist/create/claude.js +1 -1
  4. package/dist/create/claude.js.map +1 -1
  5. package/dist/create/codex.d.ts.map +1 -1
  6. package/dist/create/codex.js +2 -3
  7. package/dist/create/codex.js.map +1 -1
  8. package/dist/create/cursor.d.ts.map +1 -1
  9. package/dist/create/cursor.js +3 -7
  10. package/dist/create/cursor.js.map +1 -1
  11. package/dist/create/enact.d.ts.map +1 -1
  12. package/dist/create/enact.js.map +1 -1
  13. package/dist/create/index.d.ts +3 -3
  14. package/dist/create/index.d.ts.map +1 -1
  15. package/dist/create/index.js +4 -4
  16. package/dist/create/index.js.map +1 -1
  17. package/dist/create/kimi.d.ts.map +1 -1
  18. package/dist/create/kimi.js +2 -3
  19. package/dist/create/kimi.js.map +1 -1
  20. package/dist/create/opencode.d.ts.map +1 -1
  21. package/dist/create/opencode.js +5 -12
  22. package/dist/create/opencode.js.map +1 -1
  23. package/dist/index.d.ts +12 -13
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -9
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/agents.d.ts.map +1 -1
  28. package/dist/internal/agents.js +1 -1
  29. package/dist/internal/agents.js.map +1 -1
  30. package/dist/internal/extensions-table.d.ts +27 -6
  31. package/dist/internal/extensions-table.d.ts.map +1 -1
  32. package/dist/internal/extensions-table.js +85 -12
  33. package/dist/internal/extensions-table.js.map +1 -1
  34. package/dist/internal/file-ops.js +1 -1
  35. package/dist/internal/file-ops.js.map +1 -1
  36. package/dist/internal/io.d.ts.map +1 -1
  37. package/dist/internal/io.js.map +1 -1
  38. package/dist/internal/json-merge.d.ts.map +1 -1
  39. package/dist/internal/json-merge.js +4 -2
  40. package/dist/internal/json-merge.js.map +1 -1
  41. package/dist/internal/lock-ownership.d.ts.map +1 -1
  42. package/dist/internal/lock-ownership.js.map +1 -1
  43. package/dist/internal/pick.d.ts.map +1 -1
  44. package/dist/internal/pick.js.map +1 -1
  45. package/dist/internal/platform.d.ts.map +1 -1
  46. package/dist/internal/platform.js +7 -53
  47. package/dist/internal/platform.js.map +1 -1
  48. package/dist/internal/schema.d.ts.map +1 -1
  49. package/dist/internal/schema.js.map +1 -1
  50. package/dist/internal/toml-block.d.ts.map +1 -1
  51. package/dist/internal/toml-block.js.map +1 -1
  52. package/dist/projection.d.ts.map +1 -1
  53. package/dist/projection.js +2 -2
  54. package/dist/projection.js.map +1 -1
  55. package/dist/validate/index.d.ts +2 -2
  56. package/dist/validate/index.d.ts.map +1 -1
  57. package/dist/validate/index.js +3 -3
  58. package/dist/validate/index.js.map +1 -1
  59. package/dist/validate/run.d.ts.map +1 -1
  60. package/dist/validate/run.js +2 -5
  61. package/dist/validate/run.js.map +1 -1
  62. package/extensions/enact-repo/.agents/plugin.json +2 -2
  63. package/extensions/enact-repo/skills/interaction-design/references/animation-libraries-gsap.md +0 -1
  64. package/extensions/enact-repo/skills/interaction-design/references/animation-libraries.md +0 -1
  65. package/extensions/enact-repo/skills/interaction-design/references/scroll-animations.md +0 -1
  66. package/package.json +9 -8
  67. package/scripts/enact-extensions.mjs +331 -303
  68. package/scripts/lib/build-index.mjs +1 -3
  69. package/scripts/lib/resolve-bundle.mjs +1 -3
  70. package/scripts/lib/run-repo-doctor.mjs +50 -17
  71. package/scripts/lib/run-repo-install.mjs +130 -58
  72. package/scripts/lib/run-sync.mjs +3 -9
  73. package/scripts/lib/run-validate.mjs +1 -3
  74. package/scripts/lib/skill-validator.mjs +319 -95
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { resolveBundlePath } from "./lib/resolve-bundle.mjs";
3
- import { listBundles } from "./lib/list-bundles.mjs";
4
- import { buildIndex } from "./lib/build-index.mjs";
5
- import { runValidate } from "./lib/run-validate.mjs";
6
- import { runSync } from "./lib/run-sync.mjs";
7
- import { runRepoInstall, runRepoUninstall } from "./lib/run-repo-install.mjs";
8
- import { runRepoDoctor } from "./lib/run-repo-doctor.mjs";
9
- import { dirname, join, resolve } from "node:path";
10
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { fileURLToPath } from "node:url";
3
+ import { dirname, join, resolve } from "node:path";
12
4
  import { cwd as processCwd } from "node:process";
5
+ import { fileURLToPath } from "node:url";
13
6
  import { Ajv } from "ajv";
14
7
  import addFormatsModule from "ajv-formats";
8
+ import { buildIndex } from "./lib/build-index.mjs";
9
+ import { listBundles } from "./lib/list-bundles.mjs";
10
+ import { resolveBundlePath } from "./lib/resolve-bundle.mjs";
11
+ import { runRepoDoctor } from "./lib/run-repo-doctor.mjs";
12
+ import { runRepoInstall, runRepoUninstall } from "./lib/run-repo-install.mjs";
13
+ import { runSync } from "./lib/run-sync.mjs";
14
+ import { runValidate } from "./lib/run-validate.mjs";
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const PACKAGE_ROOT = resolve(__dirname, "..");
@@ -44,7 +44,7 @@ Usage:
44
44
  the real run would have. Touches nothing.
45
45
  enact-extensions uninstall <name> --repo <path> [--purge] [--force]
46
46
  Remove exactly what the repo lock records (--purge also removes
47
- the [skills] block install wrote to enact-config.toml, and the
47
+ the [extensions] block install wrote to enact-config.toml, and the
48
48
  file itself if install created it and only version = 1 remains)
49
49
  enact-extensions uninstall <name> --repo <path> --dry-run [--purge]
50
50
  Print exactly what uninstall would remove/restore, per file
@@ -56,7 +56,7 @@ Usage:
56
56
  (missing/drifted managed files), per-host status (claude,
57
57
  codex incl. trust_level, cursor, kimi incl. workspace-trust,
58
58
  opencode), required global binaries on PATH, the
59
- enact-config.toml marker ([skills] present; a stale
59
+ enact-config.toml marker ([extensions] present; a stale
60
60
  .agents/enact-repo.toml is reported), an "enact-hook doctor
61
61
  --repo" passthrough when available, and the enact-hook
62
62
  deny/activity log paths, existence, row counts, and (deny
@@ -80,7 +80,7 @@ Options:
80
80
  exit with the same success/failure the real run would have. Writes nothing.
81
81
  --force (install/uninstall --repo) Overwrite user-edited managed content and take over
82
82
  same-named entries not owned by the bundle. Never overwrites a user skill dir.
83
- --purge (uninstall --repo) Also remove the [skills] block install wrote to enact-config.toml.
83
+ --purge (uninstall --repo) Also remove the [extensions] block install wrote to enact-config.toml.
84
84
 
85
85
  Name resolution (bare plugin names):
86
86
  When a bare name (no "/" in it) is given, it is resolved in this order:
@@ -175,349 +175,377 @@ function parseArgs(argv) {
175
175
  }
176
176
 
177
177
  try {
178
- const rawArgv = process.argv.slice(2);
179
- if (rawArgv.length === 1 && (rawArgv[0] === "-v" || rawArgv[0] === "--version")) {
180
- console.log(`enact-extensions ${VERSION}`);
181
- exit(0);
182
- }
183
-
184
- const { command, path, options, unknown } = parseArgs(rawArgv);
185
-
186
- if (!command || command === "help") {
187
- console.log(HELP);
188
- exit(command ? 0 : 1);
189
- }
190
-
191
- if (!COMMANDS.has(command)) {
192
- console.error(`Unknown command: ${command}\n\n${REPO_USAGE_HINT}\nRun enact-extensions --help for usage.`);
193
- exit(1);
194
- }
178
+ const rawArgv = process.argv.slice(2);
179
+ if (rawArgv.length === 1 && (rawArgv[0] === "-v" || rawArgv[0] === "--version")) {
180
+ console.log(`enact-extensions ${VERSION}`);
181
+ exit(0);
182
+ }
195
183
 
196
- if (unknown.length > 0) {
197
- fail(`Unknown option(s) for ${command}: ${unknown.join(", ")}\n${REPO_USAGE_HINT}`);
198
- }
184
+ const { command, path, options, unknown } = parseArgs(rawArgv);
199
185
 
200
- if ((command === "install" || command === "uninstall") && !options.repo) {
201
- fail(`${command} requires --repo <path>.\n${REPO_USAGE_HINT}`);
202
- }
186
+ if (!command || command === "help") {
187
+ console.log(HELP);
188
+ exit(command ? 0 : 1);
189
+ }
203
190
 
204
- if (!["install", "uninstall"].includes(command) && (options.force || options.purge || options.host || options.dryRun)) {
205
- fail("--host/--force/--purge/--dry-run only apply to install|uninstall <name> --repo <path>");
206
- }
191
+ if (!COMMANDS.has(command)) {
192
+ console.error(`Unknown command: ${command}\n\n${REPO_USAGE_HINT}\nRun enact-extensions --help for usage.`);
193
+ exit(1);
194
+ }
207
195
 
208
- function printRepoDoctorReport(report) {
209
- const ok = (b) => (b ? "OK" : "FAIL");
210
- console.log(`[doctor enact-repo] repo: ${report.repo}`);
211
- console.log(` installed: ${report.installed ? "yes" : "NO"}`);
212
- if (!report.installed) {
213
- for (const p of report.problems) console.error(` problem: ${p}`);
214
- return;
196
+ if (unknown.length > 0) {
197
+ fail(`Unknown option(s) for ${command}: ${unknown.join(", ")}\n${REPO_USAGE_HINT}`);
215
198
  }
216
- console.log(
217
- ` version: installed=${report.version.installed} bundle=${report.version.bundle ?? "(unknown)"} outdated=${report.version.outdated ? "YES" : "no"}`,
218
- );
219
- if (report.version.contentOutdated.length > 0) {
220
- console.log(` content drifted from current bundle (${report.version.contentOutdated.length}): ${report.version.contentOutdated.join(", ")}`);
199
+
200
+ if ((command === "install" || command === "uninstall") && !options.repo) {
201
+ fail(`${command} requires --repo <path>.\n${REPO_USAGE_HINT}`);
221
202
  }
222
- if (report.version.note) console.log(` note: ${report.version.note}`);
223
-
224
- console.log(` lock integrity: ${ok(report.lock.valid)}`);
225
- if (!report.lock.valid) {
226
- if (report.lock.missingFiles.length) console.log(` missing: ${report.lock.missingFiles.join(", ")}`);
227
- if (report.lock.driftedFiles.length) console.log(` drifted: ${report.lock.driftedFiles.join(", ")}`);
228
- if (report.lock.jsonIssues.length) console.log(` json: ${report.lock.jsonIssues.join(", ")}`);
229
- if (report.lock.tomlIssues.length) console.log(` toml: ${report.lock.tomlIssues.join(", ")}`);
203
+
204
+ if (
205
+ !["install", "uninstall"].includes(command) &&
206
+ (options.force || options.purge || options.host || options.dryRun)
207
+ ) {
208
+ fail("--host/--force/--purge/--dry-run only apply to install|uninstall <name> --repo <path>");
230
209
  }
231
210
 
232
- for (const [host, info] of Object.entries(report.hosts)) {
233
- console.log(` host ${host}: targeted=${info.targeted} present=${info.targeted ? ok(info.present) : "n/a"}`);
234
- if (info.note) console.log(` ${info.note}`);
235
- if (info.trust) {
236
- console.log(` codex trust_level: ${info.trust.trustLevel ?? "(none)"}${info.trust.note ? ` -- ${info.trust.note}` : ""}`);
211
+ function printRepoDoctorReport(report) {
212
+ const ok = (b) => (b ? "OK" : "FAIL");
213
+ console.log(`[doctor enact-repo] repo: ${report.repo}`);
214
+ console.log(` installed: ${report.installed ? "yes" : "NO"}`);
215
+ if (!report.installed) {
216
+ for (const p of report.problems) console.error(` problem: ${p}`);
217
+ return;
237
218
  }
238
- if (info.globalHooksNote) console.log(` ${info.globalHooksNote}`);
239
- if (info.workspaceTrust) {
219
+ console.log(
220
+ ` version: installed=${report.version.installed} bundle=${report.version.bundle ?? "(unknown)"} outdated=${report.version.outdated ? "YES" : "no"}`,
221
+ );
222
+ if (report.version.contentOutdated.length > 0) {
240
223
  console.log(
241
- ` kimi workspace trust: ${info.workspaceTrust.trusted ? "TRUSTED" : "NOT TRUSTED"}${info.workspaceTrust.note ? ` -- ${info.workspaceTrust.note}` : ""}`,
224
+ ` content drifted from current bundle (${report.version.contentOutdated.length}): ${report.version.contentOutdated.join(", ")}`,
242
225
  );
243
226
  }
244
- if (host === "opencode" && info.targeted) {
245
- console.log(
246
- ` generated from current bundle: ${info.generatedFromCurrentBundle === false ? "NO (stale)" : info.generatedFromCurrentBundle === true ? "yes" : "n/a"}`,
247
- );
227
+ if (report.version.note) console.log(` note: ${report.version.note}`);
228
+
229
+ console.log(` lock integrity: ${ok(report.lock.valid)}`);
230
+ if (!report.lock.valid) {
231
+ if (report.lock.missingFiles.length) console.log(` missing: ${report.lock.missingFiles.join(", ")}`);
232
+ if (report.lock.driftedFiles.length) console.log(` drifted: ${report.lock.driftedFiles.join(", ")}`);
233
+ if (report.lock.jsonIssues.length) console.log(` json: ${report.lock.jsonIssues.join(", ")}`);
234
+ if (report.lock.tomlIssues.length) console.log(` toml: ${report.lock.tomlIssues.join(", ")}`);
248
235
  }
249
- }
250
-
251
- console.log(" binaries:");
252
- for (const [bin, info] of Object.entries(report.binaries)) {
253
- console.log(` ${bin}: ${info.found ? `${info.version ?? "(unknown version)"} @ ${info.path}` : "NOT FOUND"}`);
254
- }
255
236
 
256
- console.log(` marker (${report.marker.path}): ${ok(report.marker.valid)}${report.marker.note ? ` -- ${report.marker.note}` : ""}`);
257
- if (report.staleMarker.present) console.log(` stale marker (${report.staleMarker.path}): ${report.staleMarker.note}`);
237
+ for (const [host, info] of Object.entries(report.hosts)) {
238
+ console.log(` host ${host}: targeted=${info.targeted} present=${info.targeted ? ok(info.present) : "n/a"}`);
239
+ if (info.note) console.log(` ${info.note}`);
240
+ if (info.trust) {
241
+ console.log(
242
+ ` codex trust_level: ${info.trust.trustLevel ?? "(none)"}${info.trust.note ? ` -- ${info.trust.note}` : ""}`,
243
+ );
244
+ }
245
+ if (info.globalHooksNote) console.log(` ${info.globalHooksNote}`);
246
+ if (info.workspaceTrust) {
247
+ console.log(
248
+ ` kimi workspace trust: ${info.workspaceTrust.trusted ? "TRUSTED" : "NOT TRUSTED"}${info.workspaceTrust.note ? ` -- ${info.workspaceTrust.note}` : ""}`,
249
+ );
250
+ }
251
+ if (host === "opencode" && info.targeted) {
252
+ console.log(
253
+ ` generated from current bundle: ${info.generatedFromCurrentBundle === false ? "NO (stale)" : info.generatedFromCurrentBundle === true ? "yes" : "n/a"}`,
254
+ );
255
+ }
256
+ }
258
257
 
259
- console.log(` enact-hook doctor --repo: ${report.hookDoctor.available ? `exit ${report.hookDoctor.exitCode}` : `not available -- ${report.hookDoctor.note}`}`);
260
- if (report.hookDoctor.available && report.hookDoctor.stdout) {
261
- for (const line of report.hookDoctor.stdout.trim().split("\n")) {
262
- if (line) console.log(` ${line}`);
258
+ console.log(" binaries:");
259
+ for (const [bin, info] of Object.entries(report.binaries)) {
260
+ console.log(` ${bin}: ${info.found ? `${info.version ?? "(unknown version)"} @ ${info.path}` : "NOT FOUND"}`);
263
261
  }
264
- }
265
262
 
266
- if (report.hookLogs) {
267
- const { denyLog, activityLog } = report.hookLogs;
268
263
  console.log(
269
- ` hook logs: ${denyLog.path} ${denyLog.exists ? `(${denyLog.rowCount} row(s))` : "(not present)"}; ${activityLog.path} ${activityLog.exists ? `(${activityLog.rowCount} row(s))` : "(not present)"}`,
264
+ ` marker (${report.marker.path}): ${ok(report.marker.valid)}${report.marker.note ? ` -- ${report.marker.note}` : ""}`,
270
265
  );
271
- if (denyLog.lastRows.length > 0) {
272
- console.log(` last ${denyLog.lastRows.length} deny row(s):`);
273
- for (const row of denyLog.lastRows) {
274
- console.log(` ${row.timestamp} ${row.surface} ${row.reason_id} ${row.command} ${row.agent_message}`);
266
+ if (report.staleMarker.present)
267
+ console.log(` stale marker (${report.staleMarker.path}): ${report.staleMarker.note}`);
268
+
269
+ console.log(
270
+ ` enact-hook doctor --repo: ${report.hookDoctor.available ? `exit ${report.hookDoctor.exitCode}` : `not available -- ${report.hookDoctor.note}`}`,
271
+ );
272
+ if (report.hookDoctor.available && report.hookDoctor.stdout) {
273
+ for (const line of report.hookDoctor.stdout.trim().split("\n")) {
274
+ if (line) console.log(` ${line}`);
275
275
  }
276
276
  }
277
- }
278
277
 
279
- if (report.problems.length > 0) {
280
- console.error(`[doctor enact-repo] ${report.problems.length} problem(s):`);
281
- for (const p of report.problems) console.error(` - ${p}`);
282
- } else {
283
- console.log("[doctor enact-repo] all hard requirements OK");
284
- }
285
- }
278
+ if (report.hookLogs) {
279
+ const { denyLog, activityLog } = report.hookLogs;
280
+ console.log(
281
+ ` hook logs: ${denyLog.path} ${denyLog.exists ? `(${denyLog.rowCount} row(s))` : "(not present)"}; ${activityLog.path} ${activityLog.exists ? `(${activityLog.rowCount} row(s))` : "(not present)"}`,
282
+ );
283
+ if (denyLog.lastRows.length > 0) {
284
+ console.log(` last ${denyLog.lastRows.length} deny row(s):`);
285
+ for (const row of denyLog.lastRows) {
286
+ console.log(` ${row.timestamp} ${row.surface} ${row.reason_id} ${row.command} ${row.agent_message}`);
287
+ }
288
+ }
289
+ }
286
290
 
287
- // ---------------------------------------------------------------------------
288
- // `doctor <plugin> --repo <path>` — read-only repo-local health check.
289
- // Any plugin, bundled or not: bundled-extensions.json is the DEFAULT set, not
290
- // an allow-list. `<plugin>` is a bundled name or an explicit path, resolved
291
- // exactly like install/uninstall.
292
- // ---------------------------------------------------------------------------
293
- if (command === "doctor") {
294
- if (!path || !options.repo) {
295
- fail(`doctor requires: doctor <plugin> --repo <path> [--json]\n${REPO_USAGE_HINT}`);
296
- }
297
- let pluginRoot;
298
- try {
299
- pluginRoot = resolveBundlePath(path);
300
- } catch (err) {
301
- console.error(err instanceof Error ? err.message : String(err));
302
- exit(1);
303
- }
304
- let report;
305
- try {
306
- report = runRepoDoctor(pluginRoot, { repo: options.repo });
307
- } catch (err) {
308
- console.error(err instanceof Error ? err.message : String(err));
309
- exit(1);
310
- }
311
- if (options.json) {
312
- console.log(JSON.stringify(report, null, 2));
313
- } else {
314
- printRepoDoctorReport(report);
291
+ if (report.problems.length > 0) {
292
+ console.error(`[doctor enact-repo] ${report.problems.length} problem(s):`);
293
+ for (const p of report.problems) console.error(` - ${p}`);
294
+ } else {
295
+ console.log("[doctor enact-repo] all hard requirements OK");
296
+ }
315
297
  }
316
- exit(report.exitCode);
317
- }
318
298
 
319
- // ---------------------------------------------------------------------------
320
- // `list` command — enumerate available plugin bundles.
321
- // ---------------------------------------------------------------------------
322
- if (command === "list") {
323
- // The package's own extensions/ dir, resolved relative to this script.
324
- const PACKAGE_ROOT = resolve(__dirname, "..");
325
- const packageExtensions = join(PACKAGE_ROOT, "extensions");
326
-
327
- let roots;
328
- if (path) {
329
- // Explicit dir argument → scan only that dir.
330
- roots = [resolve(processCwd(), path)];
331
- } else {
332
- // Default: package-bundled extensions/ + cwd/extensions/ (if it differs).
333
- roots = [packageExtensions];
334
- const cwdExtensions = join(processCwd(), "extensions");
335
- if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
336
- roots.push(cwdExtensions);
299
+ // ---------------------------------------------------------------------------
300
+ // `doctor <plugin> --repo <path>` — read-only repo-local health check.
301
+ // Any plugin, bundled or not: bundled-extensions.json is the DEFAULT set, not
302
+ // an allow-list. `<plugin>` is a bundled name or an explicit path, resolved
303
+ // exactly like install/uninstall.
304
+ // ---------------------------------------------------------------------------
305
+ if (command === "doctor") {
306
+ if (!path || !options.repo) {
307
+ fail(`doctor requires: doctor <plugin> --repo <path> [--json]\n${REPO_USAGE_HINT}`);
308
+ }
309
+ let pluginRoot;
310
+ try {
311
+ pluginRoot = resolveBundlePath(path);
312
+ } catch (err) {
313
+ console.error(err instanceof Error ? err.message : String(err));
314
+ exit(1);
315
+ }
316
+ let report;
317
+ try {
318
+ report = runRepoDoctor(pluginRoot, { repo: options.repo });
319
+ } catch (err) {
320
+ console.error(err instanceof Error ? err.message : String(err));
321
+ exit(1);
337
322
  }
323
+ if (options.json) {
324
+ console.log(JSON.stringify(report, null, 2));
325
+ } else {
326
+ printRepoDoctorReport(report);
327
+ }
328
+ exit(report.exitCode);
338
329
  }
339
330
 
340
- const entries = listBundles(roots);
331
+ // ---------------------------------------------------------------------------
332
+ // `list` command — enumerate available plugin bundles.
333
+ // ---------------------------------------------------------------------------
334
+ if (command === "list") {
335
+ // The package's own extensions/ dir, resolved relative to this script.
336
+ const PACKAGE_ROOT = resolve(__dirname, "..");
337
+ const packageExtensions = join(PACKAGE_ROOT, "extensions");
338
+
339
+ let roots;
340
+ if (path) {
341
+ // Explicit dir argument → scan only that dir.
342
+ roots = [resolve(processCwd(), path)];
343
+ } else {
344
+ // Default: package-bundled extensions/ + cwd/extensions/ (if it differs).
345
+ roots = [packageExtensions];
346
+ const cwdExtensions = join(processCwd(), "extensions");
347
+ if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
348
+ roots.push(cwdExtensions);
349
+ }
350
+ }
341
351
 
342
- if (options.json) {
343
- console.log(JSON.stringify(entries, null, 2));
344
- exit(0);
345
- }
352
+ const entries = listBundles(roots);
346
353
 
347
- // Human-readable table.
348
- const TRUNC = 60;
349
- function trunc(str, len) {
350
- if (!str) return "";
351
- return str.length > len ? str.slice(0, len - 1) + "…" : str;
352
- }
354
+ if (options.json) {
355
+ console.log(JSON.stringify(entries, null, 2));
356
+ exit(0);
357
+ }
353
358
 
354
- if (entries.length === 0) {
355
- console.log("No plugin bundles found.");
356
- exit(0);
357
- }
359
+ // Human-readable table.
360
+ const TRUNC = 60;
361
+ function trunc(str, len) {
362
+ if (!str) return "";
363
+ return str.length > len ? str.slice(0, len - 1) + "…" : str;
364
+ }
365
+
366
+ if (entries.length === 0) {
367
+ console.log("No plugin bundles found.");
368
+ exit(0);
369
+ }
370
+
371
+ // Header.
372
+ const COL_NAME = 28;
373
+ const COL_VER = 8;
374
+ const COL_CAT = 18;
375
+ const COL_SURF = 22;
358
376
 
359
- // Header.
360
- const COL_NAME = 28;
361
- const COL_VER = 8;
362
- const COL_CAT = 18;
363
- const COL_SURF = 22;
364
-
365
- function padR(str, n) { return String(str ?? "").padEnd(n).slice(0, n); }
366
-
367
- const hr = "─".repeat(COL_NAME + COL_VER + COL_CAT + COL_SURF + 3 + TRUNC);
368
- console.log(hr);
369
- console.log(
370
- padR("Name", COL_NAME) + " " +
371
- padR("Ver", COL_VER) + " " +
372
- padR("Category", COL_CAT) + " " +
373
- padR("Surfaces", COL_SURF) + " " +
374
- "Description"
375
- );
376
- console.log(hr);
377
-
378
- for (const e of entries) {
379
- const surfaces = e.targets.join(", ");
377
+ function padR(str, n) {
378
+ return String(str ?? "")
379
+ .padEnd(n)
380
+ .slice(0, n);
381
+ }
382
+
383
+ const hr = "─".repeat(COL_NAME + COL_VER + COL_CAT + COL_SURF + 3 + TRUNC);
384
+ console.log(hr);
380
385
  console.log(
381
- padR(e.name, COL_NAME) + " " +
382
- padR(e.version, COL_VER) + " " +
383
- padR(e.category, COL_CAT) + " " +
384
- padR(surfaces, COL_SURF) + " " +
385
- trunc(e.description, TRUNC)
386
+ padR("Name", COL_NAME) +
387
+ " " +
388
+ padR("Ver", COL_VER) +
389
+ " " +
390
+ padR("Category", COL_CAT) +
391
+ " " +
392
+ padR("Surfaces", COL_SURF) +
393
+ " " +
394
+ "Description",
386
395
  );
387
- }
388
- console.log(hr);
389
- console.log(`${entries.length} bundle${entries.length === 1 ? "" : "s"} found.`);
390
- exit(0);
391
- }
396
+ console.log(hr);
392
397
 
393
- // ---------------------------------------------------------------------------
394
- // `index` command — generate a discovery index of all plugin bundles.
395
- //
396
- // The index is a LOCAL GENERATED ARTIFACT (never committed). The generated/
397
- // directory is gitignored. The schema at spec/index.json is the committed
398
- // contract. No network, no marketplace, no catalog/ — purely local.
399
- // ---------------------------------------------------------------------------
400
- if (command === "index") {
401
- const PACKAGE_ROOT = resolve(__dirname, "..");
402
- const packageExtensions = join(PACKAGE_ROOT, "extensions");
403
-
404
- // Same default roots as `list`.
405
- const roots = [packageExtensions];
406
- const cwdExtensions = join(processCwd(), "extensions");
407
- if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
408
- roots.push(cwdExtensions);
398
+ for (const e of entries) {
399
+ const surfaces = e.targets.join(", ");
400
+ console.log(
401
+ padR(e.name, COL_NAME) +
402
+ " " +
403
+ padR(e.version, COL_VER) +
404
+ " " +
405
+ padR(e.category, COL_CAT) +
406
+ " " +
407
+ padR(surfaces, COL_SURF) +
408
+ " " +
409
+ trunc(e.description, TRUNC),
410
+ );
411
+ }
412
+ console.log(hr);
413
+ console.log(`${entries.length} bundle${entries.length === 1 ? "" : "s"} found.`);
414
+ exit(0);
409
415
  }
410
416
 
411
- // Build the index. The wall-clock timestamp is computed here at the CLI
412
- // boundary and passed in — buildIndex itself stays pure (never calls Date).
413
- let index;
414
- try {
415
- index = buildIndex(roots, { now: new Date().toISOString(), packageRoot: PACKAGE_ROOT });
416
- } catch (err) {
417
- console.error(`[enact-extensions index] Failed to build index: ${err instanceof Error ? err.message : String(err)}`);
418
- exit(1);
419
- }
417
+ // ---------------------------------------------------------------------------
418
+ // `index` command — generate a discovery index of all plugin bundles.
419
+ //
420
+ // The index is a LOCAL GENERATED ARTIFACT (never committed). The generated/
421
+ // directory is gitignored. The schema at spec/index.json is the committed
422
+ // contract. No network, no marketplace, no catalog/ — purely local.
423
+ // ---------------------------------------------------------------------------
424
+ if (command === "index") {
425
+ const PACKAGE_ROOT = resolve(__dirname, "..");
426
+ const packageExtensions = join(PACKAGE_ROOT, "extensions");
427
+
428
+ // Same default roots as `list`.
429
+ const roots = [packageExtensions];
430
+ const cwdExtensions = join(processCwd(), "extensions");
431
+ if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
432
+ roots.push(cwdExtensions);
433
+ }
420
434
 
421
- // Validate against spec/index.json before writing.
422
- const schemaPath = join(PACKAGE_ROOT, "spec", "index.json");
423
- let schema;
424
- try {
425
- schema = JSON.parse(readFileSync(schemaPath, "utf8"));
426
- } catch (err) {
427
- console.error(`[enact-extensions index] Cannot read schema ${schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
428
- exit(1);
429
- }
435
+ // Build the index. The wall-clock timestamp is computed here at the CLI
436
+ // boundary and passed in — buildIndex itself stays pure (never calls Date).
437
+ let index;
438
+ try {
439
+ index = buildIndex(roots, { now: new Date().toISOString(), packageRoot: PACKAGE_ROOT });
440
+ } catch (err) {
441
+ console.error(
442
+ `[enact-extensions index] Failed to build index: ${err instanceof Error ? err.message : String(err)}`,
443
+ );
444
+ exit(1);
445
+ }
430
446
 
431
- const addFormats = addFormatsModule.default ?? addFormatsModule;
432
- const ajv = new Ajv({ allErrors: true, strict: false });
433
- addFormats(ajv);
434
- const validate = ajv.compile(schema);
435
- const valid = validate(index);
436
- if (!valid) {
437
- const errs = (validate.errors ?? []).map((e) => ` ${e.instancePath || "/"}: ${e.message}`).join("\n");
438
- console.error(`[enact-extensions index] Generated index failed schema validation:\n${errs}`);
439
- exit(1);
440
- }
447
+ // Validate against spec/index.json before writing.
448
+ const schemaPath = join(PACKAGE_ROOT, "spec", "index.json");
449
+ let schema;
450
+ try {
451
+ schema = JSON.parse(readFileSync(schemaPath, "utf8"));
452
+ } catch (err) {
453
+ console.error(
454
+ `[enact-extensions index] Cannot read schema ${schemaPath}: ${err instanceof Error ? err.message : String(err)}`,
455
+ );
456
+ exit(1);
457
+ }
441
458
 
442
- const json = JSON.stringify(index, null, 2);
443
- // --stdout is normalized to "-" during arg parsing, so this single check covers both.
444
- const toStdout = options.out === "-";
445
-
446
- if (toStdout) {
447
- // Write JSON to stdout; summary to stderr (keeps stdout pipe-clean).
448
- process.stdout.write(json + "\n");
449
- process.stderr.write(`[enact-extensions index] ${index.count} plugin${index.count === 1 ? "" : "s"} indexed.\n`);
450
- } else {
451
- const outPath = options.out
452
- ? resolve(processCwd(), options.out)
453
- : join(PACKAGE_ROOT, "generated", "index.json");
454
-
455
- // Ensure parent directory exists.
456
- const outDir = resolve(outPath, "..");
457
- mkdirSync(outDir, { recursive: true });
458
-
459
- writeFileSync(outPath, json + "\n", "utf8");
460
- process.stderr.write(`[enact-extensions index] Wrote ${index.count} plugin${index.count === 1 ? "" : "s"} to ${outPath}\n`);
461
- }
459
+ const addFormats = addFormatsModule.default ?? addFormatsModule;
460
+ const ajv = new Ajv({ allErrors: true, strict: false });
461
+ addFormats(ajv);
462
+ const validate = ajv.compile(schema);
463
+ const valid = validate(index);
464
+ if (!valid) {
465
+ const errs = (validate.errors ?? []).map((e) => ` ${e.instancePath || "/"}: ${e.message}`).join("\n");
466
+ console.error(`[enact-extensions index] Generated index failed schema validation:\n${errs}`);
467
+ exit(1);
468
+ }
462
469
 
463
- exit(0);
464
- }
470
+ const json = JSON.stringify(index, null, 2);
471
+ // --stdout is normalized to "-" during arg parsing, so this single check covers both.
472
+ const toStdout = options.out === "-";
465
473
 
466
- // ---------------------------------------------------------------------------
467
- // Commands that resolve a plugin root path: validate, sync, install/uninstall --repo.
468
- // ---------------------------------------------------------------------------
469
- let pluginRoot = null;
470
- try {
471
- pluginRoot = path ? resolveBundlePath(path) : resolve(processCwd());
472
- } catch (err) {
473
- // uninstall --repo must work even when the source bundle is gone: a bare
474
- // name that does not resolve is used verbatim as the plugin name.
475
- const isBareName = typeof path === "string" && path.length > 0 && !path.includes("/") && !path.includes("\\");
476
- if (!(command === "uninstall" && isBareName)) {
477
- fail(err instanceof Error ? err.message : String(err));
478
- }
479
- }
474
+ if (toStdout) {
475
+ // Write JSON to stdout; summary to stderr (keeps stdout pipe-clean).
476
+ process.stdout.write(json + "\n");
477
+ process.stderr.write(`[enact-extensions index] ${index.count} plugin${index.count === 1 ? "" : "s"} indexed.\n`);
478
+ } else {
479
+ const outPath = options.out ? resolve(processCwd(), options.out) : join(PACKAGE_ROOT, "generated", "index.json");
480
480
 
481
- function manifestName(root) {
482
- const manifestPath = root ? join(root, ".agents", "plugin.json") : null;
483
- return manifestPath && existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")).name : null;
484
- }
481
+ // Ensure parent directory exists.
482
+ const outDir = resolve(outPath, "..");
483
+ mkdirSync(outDir, { recursive: true });
485
484
 
486
- try {
487
- if (command === "validate") {
488
- const ok = runValidate(pluginRoot, undefined, { pluginValidate: true, skillValidate: true });
489
- exit(ok ? 0 : 1);
490
- }
485
+ writeFileSync(outPath, json + "\n", "utf8");
486
+ process.stderr.write(
487
+ `[enact-extensions index] Wrote ${index.count} plugin${index.count === 1 ? "" : "s"} to ${outPath}\n`,
488
+ );
489
+ }
491
490
 
492
- if (command === "sync") {
493
- runSync(pluginRoot, { name: options.name });
494
491
  exit(0);
495
492
  }
496
493
 
497
- // install | uninstall --repo
498
- const dryRun = options.dryRun === true;
499
- const repoPluginName = manifestName(pluginRoot) ?? path;
500
- if (!repoPluginName) {
501
- fail(`${command} --repo requires a plugin name`);
494
+ // ---------------------------------------------------------------------------
495
+ // Commands that resolve a plugin root path: validate, sync, install/uninstall --repo.
496
+ // ---------------------------------------------------------------------------
497
+ let pluginRoot = null;
498
+ try {
499
+ pluginRoot = path ? resolveBundlePath(path) : resolve(processCwd());
500
+ } catch (err) {
501
+ // uninstall --repo must work even when the source bundle is gone: a bare
502
+ // name that does not resolve is used verbatim as the plugin name.
503
+ const isBareName = typeof path === "string" && path.length > 0 && !path.includes("/") && !path.includes("\\");
504
+ if (!(command === "uninstall" && isBareName)) {
505
+ fail(err instanceof Error ? err.message : String(err));
506
+ }
502
507
  }
503
- const result =
504
- command === "install"
505
- ? runRepoInstall(pluginRoot, { repo: options.repo, host: options.host, force: options.force, dryRun })
506
- : runRepoUninstall(repoPluginName, { repo: options.repo, purge: options.purge, force: options.force, dryRun });
507
- if (dryRun) {
508
- if (result.actions.length === 0) {
509
- console.log("[dry-run] no filesystem changes — nothing touched");
510
- } else {
511
- for (const a of result.actions) {
512
- console.log(`[dry-run] would ${a.action}: ${a.file} — ${a.reason}`);
508
+
509
+ function manifestName(root) {
510
+ const manifestPath = root ? join(root, ".agents", "plugin.json") : null;
511
+ return manifestPath && existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")).name : null;
512
+ }
513
+
514
+ try {
515
+ if (command === "validate") {
516
+ const ok = runValidate(pluginRoot, undefined, { pluginValidate: true, skillValidate: true });
517
+ exit(ok ? 0 : 1);
518
+ }
519
+
520
+ if (command === "sync") {
521
+ runSync(pluginRoot, { name: options.name });
522
+ exit(0);
523
+ }
524
+
525
+ // install | uninstall --repo
526
+ const dryRun = options.dryRun === true;
527
+ const repoPluginName = manifestName(pluginRoot) ?? path;
528
+ if (!repoPluginName) {
529
+ fail(`${command} --repo requires a plugin name`);
530
+ }
531
+ const result =
532
+ command === "install"
533
+ ? runRepoInstall(pluginRoot, { repo: options.repo, host: options.host, force: options.force, dryRun })
534
+ : runRepoUninstall(repoPluginName, { repo: options.repo, purge: options.purge, force: options.force, dryRun });
535
+ if (dryRun) {
536
+ if (result.actions.length === 0) {
537
+ console.log("[dry-run] no filesystem changes — nothing touched");
538
+ } else {
539
+ for (const a of result.actions) {
540
+ console.log(`[dry-run] would ${a.action}: ${a.file} — ${a.reason}`);
541
+ }
513
542
  }
514
543
  }
544
+ exit(0);
545
+ } catch (err) {
546
+ if (err instanceof ExitSignal) throw err;
547
+ fail(err instanceof Error ? err.message : String(err));
515
548
  }
516
- exit(0);
517
- } catch (err) {
518
- if (err instanceof ExitSignal) throw err;
519
- fail(err instanceof Error ? err.message : String(err));
520
- }
521
549
  } catch (err) {
522
550
  // The only expected escape: a command asked to finish with a status.
523
551
  // Setting exitCode rather than exiting lets node drain stdout first.