@theholocron/cli 5.0.0-alpha.129 → 5.0.0-alpha.133

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/dist/cli.mjs CHANGED
@@ -184,6 +184,7 @@ const CARDINALITY = {
184
184
  analytics: "many",
185
185
  errors: "single",
186
186
  logs: "single",
187
+ monitoring: "single",
187
188
  wiki: "single",
188
189
  workers: "single"
189
190
  };
@@ -305,9 +306,9 @@ function resolveConfig(raw) {
305
306
  //#endregion
306
307
  //#region src/commands/contexts.ts
307
308
  /**
308
- * Command → context. Keys are the command name as it lands in yargs'
309
- * `argv._` — the full path for sub-commands (`"auth set"`, `"upgrade
310
- * node"`), the bare verb otherwise.
309
+ * Command → context. Keys are the full command path as it lands in
310
+ * yargs' `argv._` (`"auth set"`, `"repo upgrade node"`), the bare verb
311
+ * otherwise; a longer, more specific key wins over its parent.
311
312
  *
312
313
  * - **`global`** — needs nothing but the CLI binary. Works anywhere.
313
314
  * - **`repo-aware`** — reads `./holocron.config` + `./package.json`
@@ -317,33 +318,33 @@ function resolveConfig(raw) {
317
318
  */
318
319
  const COMMAND_CONTEXTS = {
319
320
  clone: "global",
320
- new: "global",
321
- "upgrade node": "global",
322
- "upgrade deps": "global",
323
- "plugin create": "global",
321
+ "new repo": "global",
322
+ "new plugin": "global",
323
+ "repo upgrade node": "global",
324
+ "repo upgrade deps": "global",
324
325
  "auth set": "global",
325
326
  "auth unset": "global",
326
327
  "auth list": "global",
327
328
  "auth check": "global",
328
- "bump-versions": "global",
329
- publish: "global",
329
+ "package bump-versions": "global",
330
+ "package publish": "global",
330
331
  "skills install": "global",
331
332
  "skills remove": "global",
332
333
  "skills update": "global",
333
334
  "run lint commit-msg": "global",
334
335
  run: "repo-aware",
335
336
  "run ci": "repo-aware",
336
- "deploy-on-release": "repo-aware",
337
+ "deploy on-release": "repo-aware",
337
338
  "config show": "repo-aware",
338
- "sync-readme": "repo-aware",
339
+ "repo sync readme": "repo-aware",
339
340
  doctor: "workspace",
340
- setup: "workspace",
341
- "secret set": "workspace",
341
+ "repo setup": "workspace",
342
+ "secrets set": "workspace",
342
343
  "secrets sync": "workspace",
343
344
  deploy: "workspace",
344
- "cleanup-preview": "workspace",
345
- sync: "workspace",
346
- "sync-github": "workspace"
345
+ "deploy cleanup-preview": "workspace",
346
+ "repo sync": "workspace",
347
+ "sync github": "workspace"
347
348
  };
348
349
  /**
349
350
  * The context for a command name from `argv._`. Falls back to the leading
@@ -352,8 +353,11 @@ const COMMAND_CONTEXTS = {
352
353
  */
353
354
  function contextForCommand(name) {
354
355
  const map = COMMAND_CONTEXTS;
355
- if (map[name]) return map[name];
356
- return map[name.split(" ")[0] ?? ""];
356
+ const tokens = name.split(" ").filter(Boolean);
357
+ for (let n = tokens.length; n > 0; n--) {
358
+ const ctx = map[tokens.slice(0, n).join(" ")];
359
+ if (ctx) return ctx;
360
+ }
357
361
  }
358
362
  /** Command names in a given context, in registration order. */
359
363
  function commandsInContext(context) {
@@ -1067,7 +1071,7 @@ async function runCleanupPreview(input) {
1067
1071
  // c8 ignore next -- real PluginLoader construction is integration-level; unit tests always supply loader
1068
1072
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
1069
1073
  await loader.load();
1070
- assertPluginsResolvable(loader, "cleanup-preview");
1074
+ assertPluginsResolvable(loader, "deploy cleanup-preview");
1071
1075
  logger.info({
1072
1076
  pr: input.prNumber,
1073
1077
  project: input.project
@@ -1790,6 +1794,14 @@ async function smokeCheck(key, provider, impl) {
1790
1794
  }
1791
1795
  return mk(key, provider, "ok", `provider: ${desc.provider}; env keys: ${desc.envKeys.join(", ")}`);
1792
1796
  }
1797
+ case "monitoring": {
1798
+ const monitoring = impl;
1799
+ if (monitoring.whoami) {
1800
+ const who = await monitoring.whoami();
1801
+ return mk(key, provider, who.ok ? "ok" : "fail", who.ok ? "token verified" : "token check failed");
1802
+ }
1803
+ return mk(key, provider, "skip", "loaded (no smoke check defined for this capability)");
1804
+ }
1793
1805
  default: return mk(key, provider, "skip", "loaded (no smoke check defined for this capability)");
1794
1806
  }
1795
1807
  } catch (err) {
@@ -1810,7 +1822,7 @@ function mk(capability, provider, status, message) {
1810
1822
  //#endregion
1811
1823
  //#region src/commands/new.ts
1812
1824
  /**
1813
- * `holocron new <type> <name>` — create a GitHub repo from a template and
1825
+ * `holocron new repo <type> <name>` — create a GitHub repo from a template and
1814
1826
  * bootstrap it by replacing all template-slug casing variants with the new
1815
1827
  * project name, then generate a `holocron.config.ts` from the answers given
1816
1828
  * during the interactive wizard.
@@ -2164,7 +2176,7 @@ async function runNew(input) {
2164
2176
  };
2165
2177
  }
2166
2178
  print("");
2167
- print(" Running holocron setup…");
2179
+ print(" Running holocron repo setup…");
2168
2180
  try {
2169
2181
  const setupArgs = ["setup"];
2170
2182
  const setupEnv = {};
@@ -2184,7 +2196,7 @@ async function runNew(input) {
2184
2196
  env: setupEnv
2185
2197
  });
2186
2198
  } catch {
2187
- print(" ✗ holocron setup failed — run it manually after checking your config");
2199
+ print(" ✗ holocron repo setup failed — run it manually after checking your config");
2188
2200
  }
2189
2201
  }
2190
2202
  print("");
@@ -2194,7 +2206,7 @@ async function runNew(input) {
2194
2206
  print(` 1. cd ${repoDir}`);
2195
2207
  if (input.noVerify) {
2196
2208
  print(` 2. pnpm install`);
2197
- print(` 3. holocron setup # wire up secrets, teams, labels, etc.`);
2209
+ print(` 3. holocron repo setup # wire up secrets, teams, labels, etc.`);
2198
2210
  print(` 4. git push -u origin HEAD`);
2199
2211
  } else print(` 2. git push -u origin HEAD`);
2200
2212
  logger.info({
@@ -2550,12 +2562,12 @@ function render$10(inputs) {
2550
2562
  version: "2.0.0-alpha.1",
2551
2563
  description: `Holocron plugin for ${inputs.vendorName}. Implements the ${inputs.capability} capability against ${inputs.vendorName}'s REST API, plus exports verifyToken + AUTH_HINT for \`holocron auth\`.`,
2552
2564
  keywords,
2553
- homepage: `https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-${inputs.slug}#readme`,
2565
+ homepage: `https://github.com/theholocron/holocron/tree/main/plugins/holocron-plugin-${inputs.slug}#readme`,
2554
2566
  bugs: "https://github.com/theholocron/holocron/issues",
2555
2567
  repository: {
2556
2568
  type: "git",
2557
2569
  url: "git+https://github.com/theholocron/holocron.git",
2558
- directory: `packages/holocron-plugin-${inputs.slug}`
2570
+ directory: `plugins/holocron-plugin-${inputs.slug}`
2559
2571
  },
2560
2572
  license: "MIT",
2561
2573
  author: "Newton Koumantzelis",
@@ -2680,7 +2692,7 @@ export * from "./auth.js";
2680
2692
  export { type ${clientClass}, ${factoryName} } from "./rest.js";
2681
2693
  export { ${inputs.capabilityClass} } from "./capabilities/${inputs.capability}.js";
2682
2694
  export { verifyToken } from "./verify-token.js";
2683
- export type { VerifyTokenResult, VerifyTokenSuccess, VerifyTokenFailure } from "./verify-token.js";
2695
+ export type { VerifyTokenFailure, VerifyTokenResult, VerifyTokenSuccess } from "@theholocron/cli";
2684
2696
  `;
2685
2697
  }
2686
2698
  //#endregion
@@ -2747,7 +2759,7 @@ TODO: fill in as capability methods land.
2747
2759
 
2748
2760
  ## Status
2749
2761
 
2750
- **\`v2.0.0-alpha.1\`** — scaffolded via \`holocron plugin create\`.
2762
+ **\`v2.0.0-alpha.1\`** — scaffolded via \`holocron new plugin\`.
2751
2763
  Not yet published; capability methods are stubs.
2752
2764
  `;
2753
2765
  }
@@ -2910,7 +2922,7 @@ function render$3(inputs) {
2910
2922
  * TODO: replace the positional args below with whatever your
2911
2923
  * capability's methods need (e.g., project id, environment slug,
2912
2924
  * secret name). Adapt the test steps to your capability's method
2913
- * surface. Model on \`packages/holocron-plugin-infisical/scripts/validate.mjs\`.
2925
+ * surface. Model on \`plugins/holocron-plugin-infisical/scripts/validate.mjs\`.
2914
2926
  */
2915
2927
 
2916
2928
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- filled in by operator
@@ -2935,7 +2947,7 @@ try {
2935
2947
  } catch (err) {
2936
2948
  if (err instanceof AuthError) {
2937
2949
  console.error(err.message);
2938
- console.error(" see: packages/holocron-plugin-${inputs.slug}/README.md#setup");
2950
+ console.error(" see: plugins/holocron-plugin-${inputs.slug}/README.md#setup");
2939
2951
  process.exit(2);
2940
2952
  }
2941
2953
  throw err;
@@ -3118,7 +3130,7 @@ export default defineConfig({
3118
3130
  //#endregion
3119
3131
  //#region src/commands/plugin-create/index.ts
3120
3132
  /**
3121
- * `holocron plugin create <slug> <vendor>` — scaffold a new plugin
3133
+ * `holocron new plugin <slug> <vendor>` — scaffold a new plugin
3122
3134
  * package matching the proven template.
3123
3135
  *
3124
3136
  * Design: see `docs/wiki/specifications/tool-plugin-create.spec.md`.
@@ -3126,11 +3138,11 @@ export default defineConfig({
3126
3138
  * Flow:
3127
3139
  * 1. Preflight — verify CWD is a workspace root (pnpm-workspace.yaml
3128
3140
  * + packages/ dir present).
3129
- * 2. Slug collision — packages/holocron-plugin-<slug>/ must not exist.
3141
+ * 2. Slug collision — plugins/holocron-plugin-<slug>/ must not exist.
3130
3142
  * 3. Capability sanity — must be one of the 14 known keys; warn for
3131
3143
  * many-cardinality caps.
3132
3144
  * 4. Generate — for each template, write to
3133
- * packages/holocron-plugin-<slug>/<path>.
3145
+ * plugins/holocron-plugin-<slug>/<path>.
3134
3146
  * 5. Verify (unless --no-verify) — runs pnpm install +
3135
3147
  * pnpm --filter <pkg> typecheck lint test.
3136
3148
  * 6. Print next steps.
@@ -3236,7 +3248,7 @@ function runPluginCreate(input) {
3236
3248
  preflight(cwd);
3237
3249
  validateSlug(input.slug);
3238
3250
  validateVendorName(input.vendorName);
3239
- const packageDir = path.join(cwd, "packages", `holocron-plugin-${input.slug}`);
3251
+ const packageDir = path.join(cwd, "plugins", `holocron-plugin-${input.slug}`);
3240
3252
  if (existsSync(packageDir)) throw new PluginCreateError(`\`${packageDir}\` already exists — edit in place or pick a different slug.`);
3241
3253
  validateCapability(input.capability, print);
3242
3254
  const derived = deriveDefaults$1({
@@ -3340,7 +3352,7 @@ function runPluginCreate(input) {
3340
3352
  };
3341
3353
  }
3342
3354
  function preflight(cwd) {
3343
- if (!existsSync(path.join(cwd, "pnpm-workspace.yaml"))) throw new PluginCreateError(`\`pnpm-workspace.yaml\` not found in \`${cwd}\`. Run \`holocron plugin create\` from the monorepo root.`);
3355
+ if (!existsSync(path.join(cwd, "pnpm-workspace.yaml"))) throw new PluginCreateError(`\`pnpm-workspace.yaml\` not found in \`${cwd}\`. Run \`holocron new plugin\` from the monorepo root.`);
3344
3356
  if (!existsSync(path.join(cwd, "packages"))) throw new PluginCreateError(`\`packages/\` directory not found in \`${cwd}\`.`);
3345
3357
  }
3346
3358
  /**
@@ -3397,7 +3409,7 @@ function printNextSteps$1(print, inputs) {
3397
3409
  //#endregion
3398
3410
  //#region src/commands/publish.ts
3399
3411
  /**
3400
- * `holocron publish --initial` — bottles up the chicken-and-egg bootstrap
3412
+ * `holocron package publish --initial` — bottles up the chicken-and-egg bootstrap
3401
3413
  * that every new npm-published holocron monorepo hits.
3402
3414
  *
3403
3415
  * npm requires a package to exist before Trusted Publishing can be
@@ -3411,14 +3423,14 @@ function printNextSteps$1(print, inputs) {
3411
3423
  *
3412
3424
  * $ pnpm install --frozen-lockfile
3413
3425
  * $ pnpm build
3414
- * $ pnpm exec tsx packages/cli/src/cli.ts publish --initial
3426
+ * $ pnpm exec tsx packages/cli/src/cli.ts package publish --initial
3415
3427
  *
3416
3428
  * `npm login --auth-type=web` runs automatically the first time — no
3417
3429
  * separate manual step. `pnpm install` + `pnpm build` stay outside the
3418
3430
  * command (no pnpm-inside-pnpm).
3419
3431
  *
3420
3432
  * `--initial` is required today — this command only implements the
3421
- * bootstrap publish. A non-initial `holocron publish` (for a manual publish
3433
+ * bootstrap publish. A non-initial `holocron package publish` (for a manual publish
3422
3434
  * outside the semantic-release/OIDC steady state) isn't built yet; the flag
3423
3435
  * exists so the surface doesn't need another rename when it is.
3424
3436
  *
@@ -3427,7 +3439,7 @@ function printNextSteps$1(print, inputs) {
3427
3439
  * setup` for the ephemeral GH admin PAT.
3428
3440
  */
3429
3441
  /**
3430
- * `holocron publish --sync-trust <old-file> <new-file>` — bulk-migrates npm
3442
+ * `holocron package publish --sync-trust <old-file> <new-file>` — bulk-migrates npm
3431
3443
  * Trusted Publisher (OIDC) config to a new workflow filename across every
3432
3444
  * public package. See issue #689 / `scripts/npm-trust-migrate.sh` (the
3433
3445
  * shell-script prototype this supersedes) for the full "why": npm ties
@@ -3709,7 +3721,7 @@ async function runPublish(input = {}) {
3709
3721
  if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
3710
3722
  print("");
3711
3723
  print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
3712
- print(` pnpm exec tsx packages/cli/src/cli.ts publish --initial --otp <6-digit-code>`);
3724
+ print(` pnpm exec tsx packages/cli/src/cli.ts package publish --initial --otp <6-digit-code>`);
3713
3725
  }
3714
3726
  logger.warn({
3715
3727
  tag,
@@ -3734,7 +3746,7 @@ async function runPublish(input = {}) {
3734
3746
  };
3735
3747
  }
3736
3748
  /**
3737
- * `holocron publish` (no `--initial`) — the steady-state publish
3749
+ * `holocron package publish` (no `--initial`) — the steady-state publish
3738
3750
  * `exec.publishCmd` in `release.config.ts` should call, replacing each
3739
3751
  * repo's hand-typed `pnpm -r --filter=./packages/* publish ...` shell (which
3740
3752
  * differs, today, only in monorepo-vs-single-package, provenance on/off, and
@@ -3874,7 +3886,7 @@ async function runSteadyPublish(input = {}) {
3874
3886
  if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
3875
3887
  print("");
3876
3888
  print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
3877
- print(` pnpm exec holocron publish --otp <6-digit-code>`);
3889
+ print(` pnpm exec holocron package publish --otp <6-digit-code>`);
3878
3890
  }
3879
3891
  logger.warn({
3880
3892
  tag,
@@ -4008,7 +4020,7 @@ const defaultLogin = async (cwd) => {
4008
4020
  //#endregion
4009
4021
  //#region src/commands/secret-set.ts
4010
4022
  /**
4011
- * `holocron secret set` — one-shot single-secret push.
4023
+ * `holocron secrets set` — one-shot single-secret push.
4012
4024
  *
4013
4025
  * Complements `holocron secrets sync` (bulk vault → destinations) for
4014
4026
  * ad-hoc scenarios:
@@ -4030,7 +4042,7 @@ async function runSecretSet(input) {
4030
4042
  const logger = input.logger ?? getLogger();
4031
4043
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
4032
4044
  await loader.load();
4033
- assertPluginsResolvable(loader, "secret set");
4045
+ assertPluginsResolvable(loader, "secrets set");
4034
4046
  const dryRun = input.context.dryRun ?? false;
4035
4047
  const scope = input.scope ?? { kind: "repo" };
4036
4048
  if (!loader.has("secrets")) throw new Error("`secrets` capability is not configured — add a `secrets` provider to holocron.config.json");
@@ -4129,7 +4141,7 @@ function describeScope(scope) {
4129
4141
  *
4130
4142
  * The vault MUST implement `readEnvironment(envId)` (optional method).
4131
4143
  * If it doesn't, the command errors with a hint to populate the keys
4132
- * individually via `holocron secret set` (also planned for v5.1).
4144
+ * individually via `holocron secrets set` (also planned for v5.1).
4133
4145
  */
4134
4146
  async function runSecretsSync(input) {
4135
4147
  const print = input.print ?? ((line) => console.log(line));
@@ -4334,7 +4346,7 @@ function deriveCapabilities(providers) {
4334
4346
  return Object.keys(providers ?? {}).sort();
4335
4347
  }
4336
4348
  /**
4337
- * `source` + `ci` are the minimal baseline every repo `holocron setup`
4349
+ * `source` + `ci` are the minimal baseline every repo `holocron repo setup`
4338
4350
  * touches wires — every `holocron.config.ts` in this org already satisfies
4339
4351
  * this, so `non-compliant` here means drift (a provider manually removed
4340
4352
  * after setup), not an unmet aspirational policy. Deliberately not a richer
@@ -4462,7 +4474,7 @@ var config_default = "# Configuration for sentiment-bot - https://github.com/beh
4462
4474
  //#endregion
4463
4475
  //#region src/utils/create-header.ts
4464
4476
  function createHeader(options) {
4465
- const { source, tool = "holocron setup", forPrimary = false } = options;
4477
+ const { source, tool = "holocron repo setup", forPrimary = false } = options;
4466
4478
  const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
4467
4479
  return {
4468
4480
  workflowHeader(format = "yaml") {
@@ -4486,7 +4498,7 @@ function createHeader(options) {
4486
4498
  },
4487
4499
  scaffoldHeader() {
4488
4500
  return [
4489
- `# Scaffolded by holocron setup — edit this file freely.`,
4501
+ `# Scaffolded by holocron repo setup — edit this file freely.`,
4490
4502
  `# Source: theholocron/holocron · ${source}`,
4491
4503
  ``
4492
4504
  ].join("\n");
@@ -4528,7 +4540,7 @@ const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cl
4528
4540
  /**
4529
4541
  * `.husky/commit-msg` — lints the commit message via `holocron run lint
4530
4542
  * commit-msg` (holocron#789), the real programmatic replacement for
4531
- * shelling out to `commitlint --edit`. Installed by `holocron setup` when
4543
+ * shelling out to `commitlint --edit`. Installed by `holocron repo setup` when
4532
4544
  * hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
4533
4545
  *
4534
4546
  * @param holocronScript - the command that invokes the CLI. Defaults to
@@ -4608,7 +4620,7 @@ var pre_commit_default = "# Run GitLeaks against staged changes only — a full
4608
4620
  const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/pre-commit/create-config.ts" });
4609
4621
  /**
4610
4622
  * `.husky/pre-commit` — runs GitLeaks (scoped to staged changes) and
4611
- * lint-staged before every commit. Installed by `holocron setup` when hooks
4623
+ * lint-staged before every commit. Installed by `holocron repo setup` when hooks
4612
4624
  * are enabled (default for `protection: "strict"`). `git commit --no-verify`
4613
4625
  * bypasses it.
4614
4626
  *
@@ -4629,7 +4641,7 @@ var pre_push_default = "# Run the merge-gating checks locally before pushing —
4629
4641
  const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/pre-push/create-config.ts" });
4630
4642
  /**
4631
4643
  * `.husky/pre-push` — runs `holocron run ci` (the merge-gating checks, in CI order)
4632
- * before every push. Installed by `holocron setup` when hooks are enabled
4644
+ * before every push. Installed by `holocron repo setup` when hooks are enabled
4633
4645
  * (default for `protection: "strict"`). `git push --no-verify` bypasses it.
4634
4646
  *
4635
4647
  * @param holocronScript - the command that invokes the CLI. Defaults to
@@ -4676,7 +4688,7 @@ var labeler_default = "bug:\n - \"^fix\"\n\nchore:\n - '^chore(?!\\(deps)'\n\n
4676
4688
  /**
4677
4689
  * Canonical AI engineering workflow role prompts.
4678
4690
  *
4679
- * Written to `.agents/prompts/<role>.md` by `holocron setup` when `agent` is
4691
+ * Written to `.agents/prompts/<role>.md` by `holocron repo setup` when `agent` is
4680
4692
  * configured. Paths are gitignored and regenerated on every setup run so the
4681
4693
  * content always reflects the current CLI version.
4682
4694
  *
@@ -5463,7 +5475,7 @@ async function updateSkillsGitignore(gitignorePath, existingContent, skills, sym
5463
5475
  //#endregion
5464
5476
  //#region src/commands/setup/run-setup.ts
5465
5477
  /**
5466
- * `holocron setup` — orchestrates per-capability setup actions across
5478
+ * `holocron repo setup` — orchestrates per-capability setup actions across
5467
5479
  * every plugin loaded from `holocron.config.json`.
5468
5480
  *
5469
5481
  * Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
@@ -5483,18 +5495,18 @@ async function updateSkillsGitignore(gitignorePath, existingContent, skills, sym
5483
5495
  */
5484
5496
  const { workflowHeader: workflowHeader$1 } = createHeader({
5485
5497
  source: "packages/cli/src/commands/setup/run-setup.ts",
5486
- tool: "holocron setup"
5498
+ tool: "holocron repo setup"
5487
5499
  });
5488
5500
  const { workflowHeader: thinCallerHeader } = createHeader({
5489
5501
  source: "packages/astromech/src/thin-callers.ts",
5490
- tool: "holocron setup"
5502
+ tool: "holocron repo setup"
5491
5503
  });
5492
5504
  async function runSetup(input) {
5493
5505
  const print = input.print ?? ((line) => console.log(line));
5494
5506
  const logger = getLogger();
5495
5507
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
5496
5508
  await withSpinner("Loading plugins…", () => loader.load());
5497
- assertPluginsResolvable(loader, "setup");
5509
+ assertPluginsResolvable(loader, "repo setup");
5498
5510
  const config = input.loaded.resolved;
5499
5511
  const dryRun = input.context.dryRun ?? false;
5500
5512
  const steps = [];
@@ -6036,6 +6048,17 @@ async function runSetup(input) {
6036
6048
  print(formatStep(steps[steps.length - 1]));
6037
6049
  }
6038
6050
  }
6051
+ if (loader.has("monitoring")) {
6052
+ const monitoring = loader.get("monitoring");
6053
+ print(style.step("monitoring"));
6054
+ for (const checkInput of monitoring.checks) {
6055
+ steps.push(await runStep("monitoring", `ensureCheck ${checkInput.name}`, dryRun, async () => {
6056
+ const result = await monitoring.ensureCheck(checkInput);
6057
+ return `${checkInput.name} ${result.alreadyExists ? "exists" : "created"}`;
6058
+ }));
6059
+ print(formatStep(steps[steps.length - 1]));
6060
+ }
6061
+ }
6039
6062
  if (loader.has("tooling")) {
6040
6063
  const tools = loader.get("tooling");
6041
6064
  print(style.step("tooling"));
@@ -6122,7 +6145,7 @@ async function runSetup(input) {
6122
6145
  print(style.hint(" · Metadata — read (repository permission, auto-included)"));
6123
6146
  print("");
6124
6147
  print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
6125
- print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
6148
+ print(style.hint(" Then re-run: holocron repo setup --token <your-admin-pat>"));
6126
6149
  print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
6127
6150
  }
6128
6151
  return {
@@ -6160,7 +6183,7 @@ async function ensurePrepareScript(repoRoot) {
6160
6183
  /**
6161
6184
  * `holocron skills` — install, remove, and update agent skills from @theholocron/skills.
6162
6185
  *
6163
- * Unlike `holocron setup`, this command is purely local (no GitHub token
6186
+ * Unlike `holocron repo setup`, this command is purely local (no GitHub token
6164
6187
  * required). It reads `agent` and `skills` from the config and installs
6165
6188
  * the listed skills into `.agents/skills/<name>/` with a symlink at the
6166
6189
  * agent-specific path (e.g. `.claude/skills/<name>` for Claude Code).
@@ -6627,7 +6650,7 @@ async function runSyncWiki(input) {
6627
6650
  //#region src/commands/sync.ts
6628
6651
  const { workflowHeader } = createHeader({
6629
6652
  source: "packages/astromech/src/thin-callers.ts",
6630
- tool: "holocron sync"
6653
+ tool: "holocron repo sync"
6631
6654
  });
6632
6655
  const SYNC_STEPS = [
6633
6656
  "labels",
@@ -7363,7 +7386,7 @@ const DEFAULT_REPO = "theholocron/.github";
7363
7386
  * The file set `sync-github` pushes to `theholocron/.github` — the reusable
7364
7387
  * `workflow_call` implementations, composite actions and starter-workflow
7365
7388
  * metadata, generated by `@theholocron/astromech`. Secondary repos get nothing:
7366
- * their thin callers are managed by `holocron sync --steps workflows`, which
7389
+ * their thin callers are managed by `holocron repo sync --steps workflows`, which
7367
7390
  * executes the resolved config (regex parsing here can't resolve preset spreads
7368
7391
  * like `...tasks` from `nodeDocs()` — see issue #464).
7369
7392
  */
@@ -7389,7 +7412,7 @@ async function runSyncGithub(input) {
7389
7412
  token,
7390
7413
  fetch: input.fetch
7391
7414
  });
7392
- print(`holocron sync-github${dryRun ? " (dry-run)" : ""}`);
7415
+ print(`holocron sync github${dryRun ? " (dry-run)" : ""}`);
7393
7416
  print(` repo: ${repo}`);
7394
7417
  if (branch) print(` branch: ${branch}`);
7395
7418
  print("");
@@ -7581,7 +7604,7 @@ async function runSyncGithub(input) {
7581
7604
  title: message.split("\n")[0],
7582
7605
  head: branch,
7583
7606
  base: "main",
7584
- body: "Auto-generated by `holocron sync-github`. Review and merge to apply template updates."
7607
+ body: "Auto-generated by `holocron sync github`. Review and merge to apply template updates."
7585
7608
  })).html_url;
7586
7609
  print(` → PR opened: ${prUrl}`);
7587
7610
  } catch (err) {
@@ -7599,7 +7622,7 @@ async function runSyncGithub(input) {
7599
7622
  //#endregion
7600
7623
  //#region src/commands/upgrade-deps.ts
7601
7624
  /**
7602
- * `holocron upgrade deps` — bring a repo's `@theholocron/*` dependency pins and
7625
+ * `holocron repo upgrade deps` — bring a repo's `@theholocron/*` dependency pins and
7603
7626
  * its `holocron.config.ts` up to the current major.
7604
7627
  *
7605
7628
  * Two concerns:
@@ -8191,13 +8214,119 @@ var NonInteractiveError = class extends Error {
8191
8214
  };
8192
8215
  const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
8193
8216
  /**
8217
+ * This repo's own resolved task manifest, filtered to what's actually
8218
+ * runnable locally — `Astromech.runnableTasks()`, reused rather than
8219
+ * reimplemented (deliberately *not* `packageScripts()`, whose
8220
+ * `syncScripts: false` short-circuit returns `{}` for repos — this one
8221
+ * included — that opt out of generated `package.json` scripts but still
8222
+ * have real runnable tasks). Empty (never throws) when there's no config to
8223
+ * resolve — feeds {@link buildRunMenuEntries}, which renders an empty list
8224
+ * as "run ›" simply having no children.
8225
+ */
8226
+ async function listRunnableTasks(argv) {
8227
+ const cwd = typeof argv.cwd === "string" ? argv.cwd : process.cwd();
8228
+ const config = await loadTasksConfig(cwd).catch(() => void 0);
8229
+ return createAstromech({
8230
+ cwd,
8231
+ config
8232
+ }).runnableTasks();
8233
+ }
8234
+ /**
8235
+ * Namespace display order for the `run` menu — deliberately its own thing,
8236
+ * not `@theholocron/astromech`'s `CI_ORDER`: that one is tuned for "cheapest
8237
+ * signal first" in an automated gate, a different goal from "how a person
8238
+ * would naturally work through a repo" (build it, clean up its style, check
8239
+ * it's secure, test it, validate it, document it). A namespace absent here
8240
+ * (a repo-specific one this list doesn't know about) sorts after every
8241
+ * named one, in whatever order it was first seen.
8242
+ */
8243
+ const RUN_MENU_NAMESPACE_ORDER = [
8244
+ "delivery",
8245
+ "sourceQuality",
8246
+ "security",
8247
+ "verification",
8248
+ "platform",
8249
+ "knowledge"
8250
+ ];
8251
+ /**
8252
+ * Splits `taskNames` (`"sourceQuality.staticAnalysis"`, …) into namespace
8253
+ * groups, ordered by {@link RUN_MENU_NAMESPACE_ORDER} — insertion order
8254
+ * within each group is `taskNames`' own order (already declaration order,
8255
+ * from {@link listRunnableTasks}). A name with no `.` at all (defensive;
8256
+ * every real registry task has a namespace) is its own bucket, keyed by the
8257
+ * full name, sorted after every namespaced group.
8258
+ */
8259
+ function groupByNamespace(taskNames) {
8260
+ const groups = /* @__PURE__ */ new Map();
8261
+ for (const name of taskNames) {
8262
+ const dot = name.indexOf(".");
8263
+ const key = dot === -1 ? name : name.slice(0, dot);
8264
+ const list = groups.get(key);
8265
+ if (list) list.push(name);
8266
+ else groups.set(key, [name]);
8267
+ }
8268
+ const ordered = /* @__PURE__ */ new Map();
8269
+ const byMenuOrder = (a, b) => {
8270
+ const ia = RUN_MENU_NAMESPACE_ORDER.indexOf(a);
8271
+ const ib = RUN_MENU_NAMESPACE_ORDER.indexOf(b);
8272
+ if (ia !== -1 || ib !== -1) return (ia === -1 ? Infinity : ia) - (ib === -1 ? Infinity : ib);
8273
+ return 0;
8274
+ };
8275
+ for (const key of [...groups.keys()].sort(byMenuOrder)) ordered.set(key, groups.get(key));
8276
+ return ordered;
8277
+ }
8278
+ /**
8279
+ * Dynamically-built `run` sub-entries for every locally-runnable task
8280
+ * ({@link listRunnableTasks}), grouped by namespace and merged into
8281
+ * `COMMAND_REGISTRY` at `cli.ts`'s `$0` call site — not a registry entry
8282
+ * itself, since the task set is per-repo, not a static surface this module
8283
+ * can hand-enumerate.
8284
+ *
8285
+ * Each namespace becomes a group with a leading "<namespace> (run it)" item
8286
+ * (`holocron run <namespace>` — every task in it, via
8287
+ * `Astromech.runGroup()`) alongside each of its individual tasks, the exact
8288
+ * same "a group that's also runnable" shape `menuItems` already renders for
8289
+ * `repo sync` / `deploy` — reached here for free by using a space, not the
8290
+ * task's own `.`, between namespace and leaf in `name` (`spawnArgs` carries
8291
+ * the real dotted invocation `buildChildArgv` actually spawns).
8292
+ */
8293
+ function buildRunMenuEntries(taskNames) {
8294
+ const entries = [];
8295
+ for (const [namespace, tasks] of groupByNamespace(taskNames)) {
8296
+ const isRealNamespace = tasks[0] !== namespace || tasks.length > 1;
8297
+ if (isRealNamespace) entries.push({
8298
+ name: `run ${namespace}`,
8299
+ description: `Run every ${namespace}.* task in sequence`,
8300
+ positionals: [],
8301
+ supportsDryRun: true,
8302
+ spawnArgs: ["run", namespace]
8303
+ });
8304
+ for (const task of tasks) {
8305
+ const leaf = task === namespace ? namespace : task.slice(namespace.length + 1);
8306
+ entries.push({
8307
+ name: isRealNamespace ? `run ${namespace} ${leaf}` : `run ${leaf}`,
8308
+ description: `Run ${task}`,
8309
+ positionals: [],
8310
+ supportsDryRun: true,
8311
+ spawnArgs: ["run", task]
8312
+ });
8313
+ }
8314
+ }
8315
+ return entries;
8316
+ }
8317
+ /**
8194
8318
  * The full command surface, hand-maintained in parallel with `cli.ts`'s
8195
8319
  * yargs registrations (this module does not introspect yargs at runtime).
8196
- * `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
8197
- * interactive prompt in front of it would work against that. (`run ci` is a
8198
- * concrete, prompt-free entry, so it stays.) `run lint commit-msg <file>` is
8199
- * excluded for the same reason: it's invoked exclusively from
8200
- * `.husky/commit-msg "$1"`, never typed by a human.
8320
+ * `run <task>` has no entry here at all — `cli.ts`'s `$0` handler merges in
8321
+ * {@link buildRunMenuEntries}'s dynamically-built ones instead, since the
8322
+ * task namespace is this repo's own resolved manifest, not a static set
8323
+ * this module can hand-enumerate the way every other entry's positionals
8324
+ * are. `job` and `passthrough` stay un-prompted either way; picking a task
8325
+ * and running it plain covers the common case, and typing
8326
+ * `holocron run <task> <job> -- <args>` directly is one step away for
8327
+ * everything else. `run lint commit-msg <file>` stays excluded entirely:
8328
+ * it's invoked exclusively from `.husky/commit-msg "$1"`, never typed by a
8329
+ * human.
8201
8330
  */
8202
8331
  const COMMAND_REGISTRY = [
8203
8332
  {
@@ -8218,13 +8347,13 @@ const COMMAND_REGISTRY = [
8218
8347
  supportsDryRun: true
8219
8348
  },
8220
8349
  {
8221
- name: "setup",
8350
+ name: "repo setup",
8222
8351
  description: "Apply infra setup actions across every configured capability",
8223
8352
  positionals: [],
8224
8353
  supportsDryRun: true
8225
8354
  },
8226
8355
  {
8227
- name: "secret set",
8356
+ name: "secrets set",
8228
8357
  description: "Set a single secret via the configured `secrets` capability",
8229
8358
  positionals: [{
8230
8359
  key: "name",
@@ -8254,7 +8383,7 @@ const COMMAND_REGISTRY = [
8254
8383
  supportsDryRun: true
8255
8384
  },
8256
8385
  {
8257
- name: "cleanup-preview",
8386
+ name: "deploy cleanup-preview",
8258
8387
  description: "List and delete Cloudflare Pages preview deployments for a GitHub PR",
8259
8388
  positionals: [{
8260
8389
  key: "pr",
@@ -8265,7 +8394,13 @@ const COMMAND_REGISTRY = [
8265
8394
  supportsDryRun: true
8266
8395
  },
8267
8396
  {
8268
- name: "bump-versions",
8397
+ name: "deploy on-release",
8398
+ description: "Deploy every workspace package whose delivery.deploy task is `on: release`",
8399
+ positionals: [],
8400
+ supportsDryRun: true
8401
+ },
8402
+ {
8403
+ name: "package bump-versions",
8269
8404
  description: "Bump all non-private package versions in lockstep (semantic-release prepareCmd)",
8270
8405
  positionals: [{
8271
8406
  key: "newVersion",
@@ -8276,13 +8411,13 @@ const COMMAND_REGISTRY = [
8276
8411
  supportsDryRun: true
8277
8412
  },
8278
8413
  {
8279
- name: "publish",
8414
+ name: "package publish",
8280
8415
  description: "Publish @theholocron/* packages to npm",
8281
8416
  positionals: [],
8282
8417
  supportsDryRun: true
8283
8418
  },
8284
8419
  {
8285
- name: "sync",
8420
+ name: "repo sync",
8286
8421
  description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
8287
8422
  positionals: [],
8288
8423
  supportsDryRun: true
@@ -8294,13 +8429,13 @@ const COMMAND_REGISTRY = [
8294
8429
  supportsDryRun: true
8295
8430
  },
8296
8431
  {
8297
- name: "sync-github",
8432
+ name: "sync github",
8298
8433
  description: "Sync workflow templates and composite actions to theholocron/.github",
8299
8434
  positionals: [],
8300
8435
  supportsDryRun: true
8301
8436
  },
8302
8437
  {
8303
- name: "sync-readme",
8438
+ name: "repo sync readme",
8304
8439
  description: "Sync the Installation + Usage block in README.md from package.json",
8305
8440
  positionals: [],
8306
8441
  supportsDryRun: true
@@ -8311,13 +8446,13 @@ const COMMAND_REGISTRY = [
8311
8446
  positionals: []
8312
8447
  },
8313
8448
  {
8314
- name: "new",
8449
+ name: "new repo",
8315
8450
  description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
8316
8451
  positionals: [],
8317
8452
  supportsDryRun: true
8318
8453
  },
8319
8454
  {
8320
- name: "plugin create",
8455
+ name: "new plugin",
8321
8456
  description: "Scaffold a new @theholocron/holocron-plugin-<slug> package",
8322
8457
  positionals: [{
8323
8458
  key: "slug",
@@ -8334,25 +8469,22 @@ const COMMAND_REGISTRY = [
8334
8469
  name: "skills install",
8335
8470
  description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
8336
8471
  positionals: [],
8337
- group: "skills",
8338
8472
  supportsDryRun: true
8339
8473
  },
8340
8474
  {
8341
8475
  name: "skills remove",
8342
8476
  description: "Remove installed skills via npx skills remove",
8343
8477
  positionals: [],
8344
- group: "skills",
8345
8478
  supportsDryRun: true
8346
8479
  },
8347
8480
  {
8348
8481
  name: "skills update",
8349
8482
  description: "Update installed skills to their latest upstream versions via npx skills update",
8350
8483
  positionals: [],
8351
- group: "skills",
8352
8484
  supportsDryRun: true
8353
8485
  },
8354
8486
  {
8355
- name: "upgrade node",
8487
+ name: "repo upgrade node",
8356
8488
  description: "Scan the repo and update every Node.js version pin to a new major",
8357
8489
  positionals: [{
8358
8490
  key: "to",
@@ -8360,14 +8492,12 @@ const COMMAND_REGISTRY = [
8360
8492
  type: "input",
8361
8493
  validate: numeric
8362
8494
  }],
8363
- group: "upgrade",
8364
8495
  supportsDryRun: true
8365
8496
  },
8366
8497
  {
8367
- name: "upgrade deps",
8498
+ name: "repo upgrade deps",
8368
8499
  description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
8369
8500
  positionals: [],
8370
- group: "upgrade",
8371
8501
  supportsDryRun: true
8372
8502
  },
8373
8503
  {
@@ -8377,8 +8507,7 @@ const COMMAND_REGISTRY = [
8377
8507
  key: "provider",
8378
8508
  message: "Provider name:",
8379
8509
  type: "input"
8380
- }],
8381
- group: "auth"
8510
+ }]
8382
8511
  },
8383
8512
  {
8384
8513
  name: "auth unset",
@@ -8388,8 +8517,7 @@ const COMMAND_REGISTRY = [
8388
8517
  message: "Provider:",
8389
8518
  type: "select",
8390
8519
  choices: listStoredProviders
8391
- }],
8392
- group: "auth"
8520
+ }]
8393
8521
  },
8394
8522
  {
8395
8523
  name: "auth check",
@@ -8399,14 +8527,12 @@ const COMMAND_REGISTRY = [
8399
8527
  message: "Provider:",
8400
8528
  type: "select",
8401
8529
  choices: listStoredProviders
8402
- }],
8403
- group: "auth"
8530
+ }]
8404
8531
  },
8405
8532
  {
8406
8533
  name: "auth list",
8407
8534
  description: "List every provider with a stored bootstrap token",
8408
- positionals: [],
8409
- group: "auth"
8535
+ positionals: []
8410
8536
  }
8411
8537
  ];
8412
8538
  /** Look up a {@link CommandEntry} by its full name — for Layer 3 call sites in `cli.ts`. */
@@ -8415,36 +8541,89 @@ function getEntry(name) {
8415
8541
  if (!entry) throw new Error(`interactive-menu: no COMMAND_REGISTRY entry named "${name}"`);
8416
8542
  return entry;
8417
8543
  }
8544
+ const GROUP = "group:";
8545
+ /** The "← back" item's value; only offered below the first level. */
8546
+ const BACK = "back";
8547
+ /**
8548
+ * One level of the menu, derived from the registered command names: the next word
8549
+ * of every entry under `path` (`[]` is the top). A word is
8550
+ *
8551
+ * - a **command** when nothing is nested under it (`doctor`);
8552
+ * - a **group** when commands are nested under it (`repo` → `repo setup`, `repo sync`, …),
8553
+ * which can nest as deep as the names go (`repo upgrade` → `repo upgrade node`);
8554
+ *
8555
+ * Every row is a single word, never a "<command> <subcommand>" path: `config show` is
8556
+ * `config ›` then `show`, even when a group holds just one command.
8557
+ *
8558
+ * A path that is itself runnable (`repo sync`, `deploy`) gets a leading "<word> (run it)" item.
8559
+ * Registration order is kept.
8560
+ */
8561
+ function menuItems(entries, path = []) {
8562
+ const prefix = path.join(" ");
8563
+ const own = prefix ? entries.find((e) => e.name === prefix) : void 0;
8564
+ const under = prefix ? entries.filter((e) => e.name.startsWith(`${prefix} `)) : entries;
8565
+ const haystackOf = (list) => list.map((e) => `${e.name} ${e.description}`.toLowerCase()).join(" ");
8566
+ const items = [];
8567
+ if (own) items.push({
8568
+ name: `${path[path.length - 1]} (run it)`,
8569
+ value: own.name,
8570
+ description: own.description,
8571
+ haystack: haystackOf([own])
8572
+ });
8573
+ const words = [...new Set(under.map((e) => e.name.split(" ")[path.length]))];
8574
+ for (const word of words) {
8575
+ const full = [...path, word].join(" ");
8576
+ const family = under.filter((e) => e.name === full || e.name.startsWith(`${full} `));
8577
+ const self = family.find((e) => e.name === full);
8578
+ const nested = family.filter((e) => e.name !== full);
8579
+ if (nested.length === 0 && self) items.push({
8580
+ name: word,
8581
+ value: full,
8582
+ description: self.description,
8583
+ haystack: haystackOf([self])
8584
+ });
8585
+ else {
8586
+ const children = [...new Set(nested.map((e) => e.name.split(" ")[path.length + 1]))];
8587
+ items.push({
8588
+ name: `${word} ›`,
8589
+ value: `${GROUP}${full}`,
8590
+ description: children.join(", "),
8591
+ haystack: haystackOf(family)
8592
+ });
8593
+ }
8594
+ }
8595
+ return items;
8596
+ }
8418
8597
  /**
8419
- * Searchable autocomplete over `entries` — the Layer 1 top-level picker, and
8420
- * (passed a `group`-filtered subset) the Layer 2 parent-command picker.
8421
- * `select()` would work too at these list sizes, but `search()` degrades
8422
- * gracefully as the surface grows and costs nothing when it doesn't.
8598
+ * Searchable autocomplete over one menu level's `items`. `select()` would work too
8599
+ * at these list sizes, but `search()` degrades gracefully as the surface grows and
8600
+ * costs nothing when it doesn't. Resolves to the picked item's `value`.
8423
8601
  */
8424
- async function pickCommand(entries, message = "What would you like to do?") {
8425
- const byName = new Map(entries.map((e) => [e.name, e]));
8426
- const picked = await search({
8602
+ async function pickMenuItem(items, message = "What would you like to do?") {
8603
+ return search({
8427
8604
  message,
8428
- source: (term) => searchChoices(entries, term)
8605
+ source: (term) => searchChoices(items, term)
8429
8606
  });
8430
- return byName.get(picked);
8431
8607
  }
8432
8608
  /**
8433
8609
  * `@inquirer/search`'s `source` callback, factored out as a plain function —
8434
8610
  * unit-testable directly instead of only through a mocked `search()` call.
8435
- * Empty/undefined `term` (nothing typed yet) returns every entry.
8611
+ * Empty/undefined `term` (nothing typed yet) returns every item.
8436
8612
  */
8437
- function searchChoices(entries, term) {
8438
- return (!term ? entries : entries.filter((e) => matches(e, term))).map((e) => ({
8439
- name: e.name,
8440
- value: e.name,
8441
- description: e.description
8613
+ function searchChoices(items, term) {
8614
+ const needle = term?.toLowerCase();
8615
+ const pool = !needle ? items : items.filter((i) => i.haystack.includes(needle));
8616
+ const rank = (i) => {
8617
+ if (!needle) return 0;
8618
+ const name = i.name.toLowerCase();
8619
+ return name.startsWith(needle) ? 0 : name.includes(needle) ? 1 : 2;
8620
+ };
8621
+ return [...pool].sort((a, b) => rank(a) - rank(b)).map((i) => ({
8622
+ name: i.name,
8623
+ value: i.value,
8624
+ description: i.description
8442
8625
  }));
8443
8626
  }
8444
- function matches(entry, term) {
8445
- const needle = term.toLowerCase();
8446
- return entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle);
8447
- }
8448
8627
  /**
8449
8628
  * For each of `entry.positionals` not already present in `argv`, prompts for
8450
8629
  * it (`input()` or `select()`, per the positional's `type`) and returns the
@@ -8498,10 +8677,10 @@ function forwardedFlags(argv, dryRunOverride) {
8498
8677
  if (dryRunOverride ?? argv.dryRun === true) out.push("--dry-run");
8499
8678
  return out;
8500
8679
  }
8501
- /** `entry.name` split into tokens, resolved positionals appended, then forwarded flags. */
8680
+ /** `entry.spawnArgs` if set, else `entry.name` split into tokens — resolved positionals appended, then forwarded flags. */
8502
8681
  function buildChildArgv(entry, positionals, parentArgv, dryRunOverride) {
8503
8682
  return [
8504
- ...entry.name.split(" "),
8683
+ ...entry.spawnArgs ?? entry.name.split(" "),
8505
8684
  ...positionals,
8506
8685
  ...forwardedFlags(parentArgv, dryRunOverride)
8507
8686
  ];
@@ -8554,14 +8733,17 @@ function shouldPreviewDryRun(entry, parentArgv) {
8554
8733
  return Boolean(entry.supportsDryRun) && parentArgv.skipDryRun !== true;
8555
8734
  }
8556
8735
  /**
8557
- * Layers 1 and 2: pick a command from `entries`, prompt for its required
8558
- * positionals, spawn it as a fresh `holocron` invocation, and set
8559
- * `process.exitCode` from the child — the caller (a `$0` handler in
8560
- * `cli.ts`) returns normally afterward so the parent's own telemetry
8561
- * flush / update-notifier tail still runs. The parent's own
8562
- * `command_completed` event fires with command name "unknown" (the
8563
- * middleware ran before any command was picked) — left as-is rather than
8564
- * suppressed; it's a real, useful signal ("the menu got used").
8736
+ * Layers 1 and 2: walk the nested menu over `entries` (starting at `startPath`,
8737
+ * `[]` for the top), prompt for the picked command's required positionals, spawn
8738
+ * it as a fresh `holocron` invocation, and set `process.exitCode` from the child —
8739
+ * the caller (a `$0` handler in `cli.ts`) returns normally afterward so the
8740
+ * parent's own telemetry flush / update-notifier tail still runs. The parent's
8741
+ * own `command_completed` event fires with command name "unknown" (the middleware
8742
+ * ran before any command was picked) — left as-is rather than suppressed; it's a
8743
+ * real, useful signal ("the menu got used").
8744
+ *
8745
+ * Picking a group opens its submenu, which has a "← back" item; a command ends
8746
+ * the walk. See {@link menuItems} for how a level is built.
8565
8747
  *
8566
8748
  * For a command that honours `--dry-run` ({@link CommandEntry.supportsDryRun})
8567
8749
  * and wasn't opted out via `--skip-dry-run` ({@link shouldPreviewDryRun}),
@@ -8573,9 +8755,24 @@ function shouldPreviewDryRun(entry, parentArgv) {
8573
8755
  * with the real run?" before the real spawn — the one actual decision
8574
8756
  * left to make, since this one does mutate something.
8575
8757
  */
8576
- async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
8758
+ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.", startPath = []) {
8577
8759
  if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
8578
- const picked = await pickCommand(entries, pickMessage);
8760
+ const stack = [startPath];
8761
+ let picked;
8762
+ while (!picked) {
8763
+ const path = stack[stack.length - 1];
8764
+ const items = menuItems(entries, path);
8765
+ if (stack.length > 1) items.push({
8766
+ name: "← back",
8767
+ value: BACK,
8768
+ description: "Return to the previous menu",
8769
+ haystack: "back"
8770
+ });
8771
+ const value = await pickMenuItem(items, stack.length === 1 ? pickMessage : `${path.join(" ")} — choose a subcommand:`);
8772
+ if (value === "back") stack.pop();
8773
+ else if (value.startsWith(GROUP)) stack.push(value.slice(6).split(" "));
8774
+ else picked = entries.find((e) => e.name === value);
8775
+ }
8579
8776
  const positionals = await promptForPositionals(picked, {});
8580
8777
  if (shouldPreviewDryRun(picked, parentArgv)) {
8581
8778
  const dryExit = await spawnChild(buildChildArgv(picked, positionals, parentArgv, true), { [DRY_RUN_PREVIEW_ENV_VAR]: "1" });
@@ -9048,6 +9245,10 @@ function tokenContext(rawTokens) {
9048
9245
  throw err;
9049
9246
  }
9050
9247
  }
9248
+ /** Layer 2: a bare group command (`holocron repo`, `holocron repo upgrade`, …) opens the menu at that level. */
9249
+ async function launchGroupMenu(parent, argv) {
9250
+ await launchMenu(COMMAND_REGISTRY, argv, `${parent} — choose a subcommand:`, `Run \`holocron ${parent} --help\` to see available ${parent} subcommands.`, parent.split(" "));
9251
+ }
9051
9252
  init(CLI_VERSION);
9052
9253
  const updateCheckPromise = checkForUpdates(CLI_VERSION);
9053
9254
  let finishCommand = () => {};
@@ -9090,7 +9291,7 @@ try {
9090
9291
  choices: LOG_LEVELS,
9091
9292
  describe: "Show structured logger output at this level. Local/interactive runs are quiet by default (no console output regardless of level) — this opts back in."
9092
9293
  }).middleware((argv) => {
9093
- const name = argv._.slice(0, 2).join(" ") || "unknown";
9294
+ const name = argv._.slice(0, 3).join(" ") || "unknown";
9094
9295
  printRunId = Boolean(argv.debug || argv.verbose);
9095
9296
  buildCliLogger(argv, {
9096
9297
  command: name,
@@ -9161,28 +9362,6 @@ try {
9161
9362
  org: resolveOrg(argv, loaded.resolved)
9162
9363
  }
9163
9364
  })).summary.fail > 0) process.exitCode = 1;
9164
- }).command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
9165
- type: "string",
9166
- describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
9167
- }).option("hooks", {
9168
- type: "boolean",
9169
- describe: "Install git hooks (.husky/pre-push → holocron run ci). Default: on for protection:'strict'. --no-hooks to skip."
9170
- }), async (argv) => {
9171
- const tokens = tokenContext(argv.token);
9172
- if (!tokens) return;
9173
- const loaded = await loadConfig(argv.cwd);
9174
- applyResolvedConfig(argv, loaded.resolved);
9175
- if ((await runSetup({
9176
- loaded,
9177
- context: {
9178
- repoRoot: argv.cwd,
9179
- dryRun: argv.dryRun,
9180
- ...argv.repo ? { repo: argv.repo } : {},
9181
- ...tokens,
9182
- org: resolveOrg(argv, loaded.resolved)
9183
- },
9184
- ...argv.hooks !== void 0 ? { hooks: argv.hooks } : {}
9185
- })).summary.fail > 0) process.exitCode = 1;
9186
9365
  }).command("skills", "Manage agent skills from the @theholocron/skills registry", (y) => y.command("install", "Copy skills from @theholocron/skills into .agents/ with agent symlinks", () => {}, async (argv) => {
9187
9366
  await runSkillsInstall({
9188
9367
  loaded: await loadConfig(argv.cwd),
@@ -9215,399 +9394,214 @@ try {
9215
9394
  ...argv.name ? { name: argv.name } : {}
9216
9395
  }).status === "fail") process.exitCode = 1;
9217
9396
  }).command("$0", false, () => {}, async (argv) => {
9218
- await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "skills"), argv, "skills — choose a subcommand:", "Run `holocron skills --help` to see available skills subcommands.");
9219
- }), () => {}).command("secret set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
9397
+ await launchGroupMenu("skills", argv);
9398
+ }), () => {}).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager. `run ci` runs every merge-gating check; `run lint commit-msg <file>` lints a commit message.", (y) => y.positional("task", {
9220
9399
  type: "string",
9221
- describe: "Secret name (e.g., NPM_TOKEN)"
9222
- }).positional("value", {
9400
+ demandOption: true,
9401
+ describe: "Task name (test, typecheck, lint, build, audit, or `ci` for the merge-gating suite). See `holocron run --help`."
9402
+ }).positional("job", {
9223
9403
  type: "string",
9224
- describe: "Secret value (positional). If omitted, sources from --from-stdin, --from-env, or env var matching <name>."
9225
- }).option("from-stdin", {
9404
+ describe: "Sub-job within the task (e.g. `holocron run audit performance`, `holocron run lint commit-msg <file>`). Omit to run every job."
9405
+ }).positional("passthrough", {
9406
+ type: "string",
9407
+ array: true,
9408
+ describe: "Args forwarded to the tool — put them after `--`."
9409
+ }).option("required", {
9226
9410
  type: "boolean",
9227
9411
  default: false,
9228
- describe: "Read the secret value from stdin"
9229
- }).option("from-env", {
9412
+ describe: "Fail (exit 1) if this repo has no such task, instead of skipping."
9413
+ }).option("all", {
9414
+ type: "boolean",
9415
+ default: false,
9416
+ describe: "With `run ci`: run every `ci: true` task, not just the required ones."
9417
+ }).option("filter", {
9230
9418
  type: "string",
9231
- describe: "Read the secret value from the named env var (otherwise: env var matching <name>)"
9232
- }).option("scope", {
9419
+ describe: "turbo --filter=<pkg> passthrough (monorepo)."
9420
+ }), async (argv) => {
9421
+ const { logger } = buildCliLogger(argv, { command: argv.task === "ci" ? "run ci" : argv.task === "lint" && argv.job === "commit-msg" ? "run lint commit-msg" : "run" });
9422
+ const passthrough = [...argv.passthrough ?? [], ...argv["--"] ?? []];
9423
+ if (argv.task === "lint" && argv.job === "commit-msg") {
9424
+ const file = passthrough[0];
9425
+ if (!file) {
9426
+ reportError("run lint commit-msg: missing <file> (the commit message file git passes as $1).");
9427
+ process.exitCode = 1;
9428
+ return;
9429
+ }
9430
+ const result = await lintCommitMsgFile(file, argv.cwd);
9431
+ if (!result.valid) {
9432
+ for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
9433
+ process.exitCode = 1;
9434
+ }
9435
+ return;
9436
+ }
9437
+ if (argv.task === "ci") {
9438
+ const ciConfig = await loadTasksConfig(argv.cwd).catch(() => void 0);
9439
+ if (createAstromech({
9440
+ cwd: argv.cwd,
9441
+ logger,
9442
+ config: ciConfig
9443
+ }).ci({
9444
+ dryRun: argv.dryRun,
9445
+ scope: argv.all ? "all" : "required",
9446
+ ...argv.filter ? { filter: argv.filter } : {}
9447
+ }).status === "fail") process.exitCode = 1;
9448
+ return;
9449
+ }
9450
+ const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
9451
+ const report = createAstromech({
9452
+ cwd: argv.cwd,
9453
+ logger,
9454
+ config
9455
+ }).run(argv.task, {
9456
+ ...argv.job !== void 0 ? { job: argv.job } : {},
9457
+ passthrough,
9458
+ dryRun: argv.dryRun,
9459
+ required: argv.required,
9460
+ ...argv.filter ? { filter: argv.filter } : {}
9461
+ });
9462
+ if (report.status === "fail" || report.status === "unknown") process.exitCode = 1;
9463
+ }).command("config show", "Print the resolved holocron config", (y) => y.option("depth", {
9464
+ type: "number",
9465
+ describe: "Collapse objects/arrays nested deeper than N levels (TTY output only)"
9466
+ }).option("interactive", {
9467
+ type: "boolean",
9468
+ describe: "Browse the config in a foldable tree (default on a TTY; --no-interactive for static output)"
9469
+ }), async (argv) => {
9470
+ await viewJson((await loadConfig(argv.cwd)).resolved, {
9471
+ depth: argv.depth,
9472
+ interactive: argv.interactive
9473
+ });
9474
+ }).command("auth <subcommand>", "Manage bootstrap credentials in the OS keyring", (y) => y.command("set [provider] [value]", "Verify + store a bootstrap token for a provider", (yy) => yy.positional("provider", { type: "string" }).positional("value", { type: "string" }), async (argv) => {
9475
+ const [provider] = await promptForPositionals(getEntry("auth set"), argv);
9476
+ if ((await runAuthSet({
9477
+ provider,
9478
+ ...argv.value ? { positional: argv.value } : {},
9479
+ ...argv.org ? { org: argv.org } : {}
9480
+ })).status === "fail") process.exitCode = 1;
9481
+ }).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
9482
+ const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
9483
+ runAuthUnset({ provider });
9484
+ }).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
9485
+ const [provider] = await promptForPositionals(getEntry("auth check"), argv);
9486
+ if ((await runAuthCheck({
9487
+ provider,
9488
+ ...argv.org ? { org: argv.org } : {}
9489
+ })).status === "fail") process.exitCode = 1;
9490
+ }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
9491
+ await runAuthList();
9492
+ }).command("$0", false, () => {}, async (argv) => {
9493
+ await launchGroupMenu("auth", argv);
9494
+ }), () => {}).command("$0", false, () => {}, async (argv) => {
9495
+ printVersionHeaderOnce();
9496
+ const runEntries = buildRunMenuEntries(await listRunnableTasks(argv));
9497
+ await launchMenu([...COMMAND_REGISTRY, ...runEntries], argv);
9498
+ }).command("repo", "Set up, sync and upgrade the current repo", (g) => g.command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
9233
9499
  type: "string",
9234
- default: "repo",
9235
- describe: "Scope: \"repo\" (default), \"env=<name>\", or \"org=<name>\""
9500
+ describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
9501
+ }).option("hooks", {
9502
+ type: "boolean",
9503
+ describe: "Install git hooks (.husky/pre-push → holocron run ci). Default: on for protection:'strict'. --no-hooks to skip."
9236
9504
  }), async (argv) => {
9237
9505
  const tokens = tokenContext(argv.token);
9238
9506
  if (!tokens) return;
9239
- const [name] = await promptForPositionals(getEntry("secret set"), argv);
9240
- const scopeArg = argv.scope;
9241
- const scope = parseScope(scopeArg);
9242
9507
  const loaded = await loadConfig(argv.cwd);
9243
9508
  applyResolvedConfig(argv, loaded.resolved);
9244
- if ((await runSecretSet({
9509
+ if ((await runSetup({
9245
9510
  loaded,
9246
9511
  context: {
9247
9512
  repoRoot: argv.cwd,
9248
9513
  dryRun: argv.dryRun,
9514
+ ...argv.repo ? { repo: argv.repo } : {},
9249
9515
  ...tokens,
9250
9516
  org: resolveOrg(argv, loaded.resolved)
9251
9517
  },
9252
- name,
9253
- ...argv.value ? { value: argv.value } : {},
9254
- ...argv.fromStdin ? { fromStdin: true } : {},
9255
- ...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
9256
- scope
9257
- })).status === "fail") process.exitCode = 1;
9258
- }).command("secrets sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
9259
- type: "string",
9260
- describe: "Vault environment id to read (1P Environment id, etc.)"
9261
- }).option("project-id", {
9518
+ ...argv.hooks !== void 0 ? { hooks: argv.hooks } : {}
9519
+ })).summary.fail > 0) process.exitCode = 1;
9520
+ }).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.command("readme", "Sync the Installation + Usage block in README.md from package.json", (y) => y.option("dry-run", {
9521
+ type: "boolean",
9522
+ describe: "Print what would change without writing",
9523
+ default: false
9524
+ }), async (argv) => {
9525
+ const loaded = await loadConfig(argv.cwd);
9526
+ applyResolvedConfig(argv, loaded.resolved);
9527
+ const report = await runSyncReadme({
9528
+ loaded,
9529
+ context: {
9530
+ repoRoot: argv.cwd,
9531
+ dryRun: argv.dryRun
9532
+ }
9533
+ });
9534
+ if (report.status === "fail") {
9535
+ if (report.message) reportError(`repo sync readme: ${report.message}`);
9536
+ process.exitCode = 1;
9537
+ }
9538
+ }).positional("steps", {
9262
9539
  type: "string",
9263
- describe: "Deployment project id (e.g., Vercel prj_*). Required when deployment is loaded."
9264
- }).option("target", {
9265
- type: "array",
9266
- default: ["production", "preview"],
9267
- describe: "Deployment targets to sync to. Defaults to production + preview."
9268
- }).option("github-secret", {
9269
- type: "array",
9270
- describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
9271
- }).option("github-secret-scope", {
9540
+ array: true,
9541
+ describe: "Steps to run: labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki (default: all)"
9542
+ }).option("repo", {
9272
9543
  type: "string",
9273
- default: "repo",
9274
- describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
9544
+ describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
9275
9545
  }), async (argv) => {
9276
9546
  const tokens = tokenContext(argv.token);
9277
9547
  if (!tokens) return;
9278
- const githubSecretScope = parseScope(argv.githubSecretScope);
9279
- const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
9280
9548
  const loaded = await loadConfig(argv.cwd);
9281
9549
  applyResolvedConfig(argv, loaded.resolved);
9282
- if ((await runSecretsSync({
9550
+ if ((await runSync({
9283
9551
  loaded,
9284
9552
  context: {
9285
9553
  repoRoot: argv.cwd,
9286
9554
  dryRun: argv.dryRun,
9555
+ ...argv.repo ? { repo: argv.repo } : {},
9287
9556
  ...tokens,
9288
9557
  org: resolveOrg(argv, loaded.resolved)
9289
9558
  },
9290
- environmentId,
9291
- ...argv.projectId ? { projectId: argv.projectId } : {},
9292
- ...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
9293
- githubSecretScope,
9294
- targets: argv.target
9559
+ ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
9295
9560
  })).summary.fail > 0) process.exitCode = 1;
9296
- }).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
9297
- type: "string",
9298
- describe: "Git branch to deploy (omit when using --files)"
9299
- }).option("project-id", {
9300
- type: "string",
9301
- demandOption: true,
9302
- describe: "Deployment project id (e.g., Vercel prj_*)"
9303
- }).option("target", {
9304
- type: "string",
9305
- choices: ["production", "staging"],
9306
- describe: "Named environment to deploy into. Omit for a branch preview."
9307
- }).option("files", {
9308
- type: "string",
9309
- describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
9310
- }).conflicts("files", "branch"), async (argv) => {
9311
- const tokens = tokenContext(argv.token);
9312
- if (!tokens) return;
9313
- const loaded = await loadConfig(argv.cwd);
9314
- applyResolvedConfig(argv, loaded.resolved);
9315
- const context = {
9316
- repoRoot: argv.cwd,
9317
- dryRun: argv.dryRun,
9318
- ...tokens,
9319
- org: resolveOrg(argv, loaded.resolved)
9320
- };
9321
- if (argv.files) {
9322
- if ((await runDeployFromFiles({
9323
- loaded,
9324
- context,
9325
- projectId: argv.projectId,
9326
- dir: argv.files,
9327
- ...argv.target ? { target: argv.target } : {}
9328
- })).status === "fail") process.exitCode = 1;
9329
- return;
9330
- }
9331
- const [branch] = await promptForPositionals(getEntry("deploy"), argv);
9332
- if ((await runDeploy({
9333
- loaded,
9334
- context,
9335
- projectId: argv.projectId,
9336
- branch,
9337
- ...argv.target ? { target: argv.target } : {}
9338
- })).status === "fail") process.exitCode = 1;
9339
- }).command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
9561
+ }).command("upgrade", "Upgrade toolchain version pins across the repo", (y) => y.command("node [to]", "Scan the repo and update every Node.js version pin to a new major", (yy) => yy.positional("to", {
9340
9562
  type: "number",
9341
- describe: "PR number to clean up"
9342
- }).option("project", {
9343
- type: "string",
9344
- demandOption: true,
9345
- describe: "Cloudflare Pages project name (e.g. theholocron-preview)"
9346
- }).option("repo", {
9347
- type: "string",
9348
- describe: "GitHub repo as owner/name — defaults to the repo in holocron.config"
9349
- }), async (argv) => {
9350
- const tokens = tokenContext(argv.token);
9351
- if (!tokens) return;
9352
- const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
9353
- const loaded = await loadConfig(argv.cwd);
9354
- applyResolvedConfig(argv, loaded.resolved);
9355
- if ((await runCleanupPreview({
9356
- loaded,
9357
- context: {
9358
- repoRoot: argv.cwd,
9359
- dryRun: argv.dryRun,
9360
- ...tokens,
9361
- org: resolveOrg(argv, loaded.resolved)
9362
- },
9363
- prNumber: Number(pr),
9364
- project: argv.project,
9365
- ...argv.repo ? { repo: argv.repo } : {}
9366
- })).status === "fail") process.exitCode = 1;
9367
- }).command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
9368
- type: "string",
9369
- describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
9370
- }), async (argv) => {
9371
- const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
9372
- if ((await runNpmBumpVersions({
9373
- version: newVersion,
9374
- cwd: argv.cwd,
9375
- dryRun: argv.dryRun
9376
- })).status === "fail") process.exitCode = 1;
9377
- }).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
9378
- type: "boolean",
9379
- default: false,
9380
- describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
9381
- }).option("tag", {
9382
- type: "string",
9383
- describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
9384
- }).option("otp", {
9385
- type: "string",
9386
- describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
9387
- }).option("sync-trust", {
9388
- type: "array",
9389
- describe: "Migrate npm Trusted Publisher config to a new workflow filename across every public package: --sync-trust <old-file> <new-file>. See issue #689 — npm requires fresh interactive 2FA per package; re-run after approving the printed authUrl if it stops early."
9390
- }).option("repo", {
9391
- type: "string",
9392
- describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
9393
- }).option("provenance", {
9394
- type: "boolean",
9395
- default: true,
9396
- describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
9397
- }).option("skip-already-published", {
9398
- type: "boolean",
9399
- default: false,
9400
- describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
9401
- }), async (argv) => {
9402
- if (argv.syncTrust) {
9403
- const [oldFile, newFile] = argv.syncTrust;
9404
- if (!oldFile || !newFile) {
9405
- reportError("publish --sync-trust requires exactly two values: <old-file> <new-file>");
9406
- process.exitCode = 1;
9407
- return;
9408
- }
9409
- if ((await runSyncTrust({
9410
- cwd: argv.cwd,
9411
- oldFile,
9412
- newFile,
9413
- dryRun: argv.dryRun,
9414
- ...argv.repo ? { repo: argv.repo } : {}
9415
- })).status === "fail") process.exitCode = 1;
9416
- return;
9417
- }
9418
- if (argv.initial) {
9419
- if ((await runPublish({
9420
- cwd: argv.cwd,
9421
- dryRun: argv.dryRun,
9422
- ...argv.tag ? { tag: argv.tag } : {},
9423
- ...argv.otp ? { otp: argv.otp } : {}
9424
- })).status === "fail") process.exitCode = 1;
9425
- return;
9426
- }
9427
- if ((await runSteadyPublish({
9428
- cwd: argv.cwd,
9429
- dryRun: argv.dryRun,
9430
- provenance: argv.provenance,
9431
- skipAlreadyPublished: argv.skipAlreadyPublished,
9432
- ...argv.tag ? { tag: argv.tag } : {},
9433
- ...argv.otp ? { otp: argv.otp } : {}
9434
- })).status === "fail") process.exitCode = 1;
9435
- }).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.positional("steps", {
9436
- type: "string",
9437
- array: true,
9438
- describe: "Steps to run: labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki (default: all)"
9439
- }).option("repo", {
9440
- type: "string",
9441
- describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
9442
- }), async (argv) => {
9443
- const tokens = tokenContext(argv.token);
9444
- if (!tokens) return;
9445
- const loaded = await loadConfig(argv.cwd);
9446
- applyResolvedConfig(argv, loaded.resolved);
9447
- if ((await runSync({
9448
- loaded,
9449
- context: {
9450
- repoRoot: argv.cwd,
9451
- dryRun: argv.dryRun,
9452
- ...argv.repo ? { repo: argv.repo } : {},
9453
- ...tokens,
9454
- org: resolveOrg(argv, loaded.resolved)
9455
- },
9456
- ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
9457
- })).summary.fail > 0) process.exitCode = 1;
9458
- }).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager. `run ci` runs every merge-gating check; `run lint commit-msg <file>` lints a commit message.", (y) => y.positional("task", {
9459
- type: "string",
9460
- demandOption: true,
9461
- describe: "Task name (test, typecheck, lint, build, audit, or `ci` for the merge-gating suite). See `holocron run --help`."
9462
- }).positional("job", {
9463
- type: "string",
9464
- describe: "Sub-job within the task (e.g. `holocron run audit performance`, `holocron run lint commit-msg <file>`). Omit to run every job."
9465
- }).positional("passthrough", {
9466
- type: "string",
9467
- array: true,
9468
- describe: "Args forwarded to the tool — put them after `--`."
9469
- }).option("required", {
9470
- type: "boolean",
9471
- default: false,
9472
- describe: "Fail (exit 1) if this repo has no such task, instead of skipping."
9473
- }).option("all", {
9474
- type: "boolean",
9475
- default: false,
9476
- describe: "With `run ci`: run every `ci: true` task, not just the required ones."
9477
- }).option("filter", {
9478
- type: "string",
9479
- describe: "turbo --filter=<pkg> passthrough (monorepo)."
9480
- }), async (argv) => {
9481
- const { logger } = buildCliLogger(argv, { command: argv.task === "ci" ? "run ci" : argv.task === "lint" && argv.job === "commit-msg" ? "run lint commit-msg" : "run" });
9482
- const passthrough = [...argv.passthrough ?? [], ...argv["--"] ?? []];
9483
- if (argv.task === "lint" && argv.job === "commit-msg") {
9484
- const file = passthrough[0];
9485
- if (!file) {
9486
- reportError("run lint commit-msg: missing <file> (the commit message file git passes as $1).");
9487
- process.exitCode = 1;
9488
- return;
9489
- }
9490
- const result = await lintCommitMsgFile(file, argv.cwd);
9491
- if (!result.valid) {
9492
- for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
9493
- process.exitCode = 1;
9494
- }
9495
- return;
9496
- }
9497
- if (argv.task === "ci") {
9498
- const ciConfig = await loadTasksConfig(argv.cwd).catch(() => void 0);
9499
- if (createAstromech({
9500
- cwd: argv.cwd,
9501
- logger,
9502
- config: ciConfig
9503
- }).ci({
9504
- dryRun: argv.dryRun,
9505
- scope: argv.all ? "all" : "required",
9506
- ...argv.filter ? { filter: argv.filter } : {}
9507
- }).status === "fail") process.exitCode = 1;
9508
- return;
9509
- }
9510
- const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
9511
- const report = createAstromech({
9512
- cwd: argv.cwd,
9513
- logger,
9514
- config
9515
- }).run(argv.task, {
9516
- ...argv.job !== void 0 ? { job: argv.job } : {},
9517
- passthrough,
9518
- dryRun: argv.dryRun,
9519
- required: argv.required,
9520
- ...argv.filter ? { filter: argv.filter } : {}
9521
- });
9522
- if (report.status === "fail" || report.status === "unknown") process.exitCode = 1;
9523
- }).command("deploy-on-release", "Deploy every workspace package whose delivery.deploy task is `on: release` — run by the delivery.publish workflow's deploy job", (y) => y.option("channel", {
9524
- type: "string",
9525
- default: "",
9526
- describe: "The release's channel: its prerelease identifier (`alpha`); empty or `main` for a stable release."
9563
+ describe: "Target Node.js major version (e.g., 22)"
9527
9564
  }).option("from", {
9528
- type: "string",
9529
- describe: "The previous release's commit, to diff against. Omit when there is none."
9530
- }).option("to", {
9531
- type: "string",
9532
- describe: "The release commit. Default HEAD."
9565
+ type: "number",
9566
+ describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
9533
9567
  }), async (argv) => {
9534
- if ((await deployOnRelease({
9568
+ const [to] = await promptForPositionals(getEntry("repo upgrade node"), argv);
9569
+ let extra = [];
9570
+ try {
9571
+ const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
9572
+ const extraRaw = JSON.parse(raw).upgrade?.node?.extra;
9573
+ if (Array.isArray(extraRaw)) extra = extraRaw;
9574
+ } catch {}
9575
+ const report = await runUpgradeNode({
9576
+ to: Number(to),
9577
+ ...argv.from != null ? { from: argv.from } : {},
9535
9578
  cwd: argv.cwd,
9536
- channel: argv.channel,
9537
- ...argv.from ? { from: argv.from } : {},
9538
- ...argv.to ? { to: argv.to } : {},
9539
9579
  dryRun: argv.dryRun,
9540
- print: (line) => console.log(line)
9541
- })).status === "fail") process.exitCode = 1;
9542
- }).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
9543
- type: "string",
9544
- default: "theholocron/.github",
9545
- describe: "Target org/repo (default: theholocron/.github)"
9546
- }).option("branch", {
9547
- type: "string",
9548
- describe: "Push to this branch instead of the default branch (enables PR-based workflow for protected repos)"
9549
- }).option("pr", {
9550
- type: "boolean",
9551
- default: false,
9552
- describe: "Open a PR after pushing to --branch (no-op without --branch)"
9553
- }).option("message", {
9554
- type: "string",
9555
- describe: "Commit message (default: chore: sync from theholocron/holocron)"
9556
- }).option("output-dir", {
9557
- type: "string",
9558
- describe: "Write generated files to this local directory instead of pushing (for validation)"
9559
- }), async (argv) => {
9560
- const outputDir = argv["output-dir"];
9561
- const parsed = tokenContext(argv.token);
9562
- if (!parsed) return;
9563
- let token;
9564
- if (outputDir) token = "no-token-needed";
9565
- else try {
9566
- token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
9567
- } catch (err) {
9568
- reportError(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
9580
+ extra
9581
+ });
9582
+ if (report.status === "fail") {
9583
+ if (report.message) reportError(`upgrade node: ${report.message}`);
9569
9584
  process.exitCode = 1;
9570
- return;
9571
9585
  }
9572
- if ((await runSyncGithub({
9573
- token,
9574
- repo: argv.repo,
9575
- dryRun: argv.dryRun,
9576
- ...argv.branch ? { branch: argv.branch } : {},
9577
- ...argv.pr ? { createPr: true } : {},
9578
- ...argv.message ? { message: argv.message } : {},
9579
- ...outputDir ? { outputDir } : {}
9580
- })).status === "fail") process.exitCode = 1;
9581
- }).command("sync-readme", "Sync the Installation + Usage block in README.md from package.json", (y) => y.option("dry-run", {
9586
+ }).command("deps", "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API", (yy) => yy.option("pins-only", {
9582
9587
  type: "boolean",
9583
- describe: "Print what would change without writing",
9584
- default: false
9588
+ default: false,
9589
+ describe: "Only bump the catalog pins — skip the holocron.config.ts migration"
9585
9590
  }), async (argv) => {
9586
- const loaded = await loadConfig(argv.cwd);
9587
- applyResolvedConfig(argv, loaded.resolved);
9588
- const report = await runSyncReadme({
9589
- loaded,
9590
- context: {
9591
- repoRoot: argv.cwd,
9592
- dryRun: argv.dryRun
9593
- }
9591
+ const report = await runUpgradeDeps({
9592
+ cwd: argv.cwd,
9593
+ dryRun: argv.dryRun,
9594
+ pinsOnly: argv.pinsOnly
9594
9595
  });
9595
9596
  if (report.status === "fail") {
9596
- if (report.message) reportError(`sync-readme: ${report.message}`);
9597
+ if (report.message) reportError(`upgrade deps: ${report.message}`);
9597
9598
  process.exitCode = 1;
9598
9599
  }
9599
- }).command("config show", "Print the resolved holocron config", (y) => y.option("depth", {
9600
- type: "number",
9601
- describe: "Collapse objects/arrays nested deeper than N levels (TTY output only)"
9602
- }).option("interactive", {
9603
- type: "boolean",
9604
- describe: "Browse the config in a foldable tree (default on a TTY; --no-interactive for static output)"
9605
- }), async (argv) => {
9606
- await viewJson((await loadConfig(argv.cwd)).resolved, {
9607
- depth: argv.depth,
9608
- interactive: argv.interactive
9609
- });
9610
- }).command("new [type] [name]", "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)", (y) => y.positional("type", {
9600
+ }).command("$0", false, () => {}, async (argv) => {
9601
+ await launchGroupMenu("repo upgrade", argv);
9602
+ }), () => {}).command("$0", false, () => {}, async (argv) => {
9603
+ await launchGroupMenu("repo", argv);
9604
+ }), () => {}).command("new", "Scaffold a new repo or plugin", (g) => g.command("repo [type] [name]", "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)", (y) => y.positional("type", {
9611
9605
  type: "string",
9612
9606
  describe: "Template type — maps to theholocron/<type>-template (e.g. cli, react, nextjs, node, monorepo, base)"
9613
9607
  }).positional("name", {
@@ -9656,7 +9650,7 @@ try {
9656
9650
  }).option("verify", {
9657
9651
  type: "boolean",
9658
9652
  default: true,
9659
- describe: "Run pnpm install + holocron setup after bootstrapping (--no-verify skips)"
9653
+ describe: "Run pnpm install + holocron repo setup after bootstrapping (--no-verify skips)"
9660
9654
  }), async (argv) => {
9661
9655
  try {
9662
9656
  const tokens = tokenContext(argv.token);
@@ -9872,129 +9866,351 @@ try {
9872
9866
  }
9873
9867
  throw err;
9874
9868
  }
9875
- }).command("plugin create [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
9869
+ }).command("plugin [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
9870
+ type: "string",
9871
+ describe: "Package slug (kebab-case)"
9872
+ }).positional("vendor", {
9873
+ type: "string",
9874
+ describe: "Vendor display name (PascalCase)"
9875
+ }).option("capability", {
9876
+ type: "string",
9877
+ describe: "Capability key: source|ci|secrets|environments|issues|deployment|storage|auth|vault|dns|tooling|notifications|analytics|errors|logs|wiki|workers"
9878
+ }).option("token-env", {
9879
+ type: "string",
9880
+ describe: "Holocron env var name (defaults to HOLOCRON_<VENDOR>_TOKEN)"
9881
+ }).option("vendor-env", {
9882
+ type: "string",
9883
+ describe: "Vendor-native env var name"
9884
+ }).option("base-url", {
9885
+ type: "string",
9886
+ describe: "REST base URL"
9887
+ }).option("verify", {
9888
+ type: "boolean",
9889
+ default: true,
9890
+ describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
9891
+ }), async (argv) => {
9892
+ try {
9893
+ const [slug, vendor] = await promptForPositionals(getEntry("new plugin"), argv);
9894
+ const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
9895
+ capability: argv.capability,
9896
+ vendorEnv: argv.vendorEnv,
9897
+ baseUrl: argv.baseUrl
9898
+ }, {
9899
+ selectCapability: () => select({
9900
+ message: "Capability:",
9901
+ choices: Object.keys(CARDINALITY).map((k) => ({
9902
+ name: k,
9903
+ value: k
9904
+ }))
9905
+ }),
9906
+ inputVendorEnv: () => input({ message: `Vendor-native env var for the ${vendor} token (e.g. MYVENDOR_API_KEY):` }),
9907
+ inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
9908
+ });
9909
+ if (runPluginCreate({
9910
+ slug,
9911
+ vendorName: vendor,
9912
+ capability,
9913
+ vendorEnv,
9914
+ baseUrl,
9915
+ ...argv.tokenEnv ? { tokenEnv: argv.tokenEnv } : {},
9916
+ dryRun: argv.dryRun,
9917
+ noVerify: !argv.verify,
9918
+ cwd: argv.cwd
9919
+ }).status === "fail") process.exitCode = 1;
9920
+ } catch (err) {
9921
+ if (err instanceof PluginCreateError) {
9922
+ reportError(`plugin create: ${err.message}`);
9923
+ process.exitCode = 1;
9924
+ return;
9925
+ }
9926
+ throw err;
9927
+ }
9928
+ }).command("$0", false, () => {}, async (argv) => {
9929
+ await launchGroupMenu("new", argv);
9930
+ }), () => {}).command("secrets", "Set secrets and sync vault environments", (g) => g.command("set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
9931
+ type: "string",
9932
+ describe: "Secret name (e.g., NPM_TOKEN)"
9933
+ }).positional("value", {
9934
+ type: "string",
9935
+ describe: "Secret value (positional). If omitted, sources from --from-stdin, --from-env, or env var matching <name>."
9936
+ }).option("from-stdin", {
9937
+ type: "boolean",
9938
+ default: false,
9939
+ describe: "Read the secret value from stdin"
9940
+ }).option("from-env", {
9941
+ type: "string",
9942
+ describe: "Read the secret value from the named env var (otherwise: env var matching <name>)"
9943
+ }).option("scope", {
9944
+ type: "string",
9945
+ default: "repo",
9946
+ describe: "Scope: \"repo\" (default), \"env=<name>\", or \"org=<name>\""
9947
+ }), async (argv) => {
9948
+ const tokens = tokenContext(argv.token);
9949
+ if (!tokens) return;
9950
+ const [name] = await promptForPositionals(getEntry("secrets set"), argv);
9951
+ const scopeArg = argv.scope;
9952
+ const scope = parseScope(scopeArg);
9953
+ const loaded = await loadConfig(argv.cwd);
9954
+ applyResolvedConfig(argv, loaded.resolved);
9955
+ if ((await runSecretSet({
9956
+ loaded,
9957
+ context: {
9958
+ repoRoot: argv.cwd,
9959
+ dryRun: argv.dryRun,
9960
+ ...tokens,
9961
+ org: resolveOrg(argv, loaded.resolved)
9962
+ },
9963
+ name,
9964
+ ...argv.value ? { value: argv.value } : {},
9965
+ ...argv.fromStdin ? { fromStdin: true } : {},
9966
+ ...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
9967
+ scope
9968
+ })).status === "fail") process.exitCode = 1;
9969
+ }).command("sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
9970
+ type: "string",
9971
+ describe: "Vault environment id to read (1P Environment id, etc.)"
9972
+ }).option("project-id", {
9973
+ type: "string",
9974
+ describe: "Deployment project id (e.g., Vercel prj_*). Required when deployment is loaded."
9975
+ }).option("target", {
9976
+ type: "array",
9977
+ default: ["production", "preview"],
9978
+ describe: "Deployment targets to sync to. Defaults to production + preview."
9979
+ }).option("github-secret", {
9980
+ type: "array",
9981
+ describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
9982
+ }).option("github-secret-scope", {
9983
+ type: "string",
9984
+ default: "repo",
9985
+ describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
9986
+ }), async (argv) => {
9987
+ const tokens = tokenContext(argv.token);
9988
+ if (!tokens) return;
9989
+ const githubSecretScope = parseScope(argv.githubSecretScope);
9990
+ const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
9991
+ const loaded = await loadConfig(argv.cwd);
9992
+ applyResolvedConfig(argv, loaded.resolved);
9993
+ if ((await runSecretsSync({
9994
+ loaded,
9995
+ context: {
9996
+ repoRoot: argv.cwd,
9997
+ dryRun: argv.dryRun,
9998
+ ...tokens,
9999
+ org: resolveOrg(argv, loaded.resolved)
10000
+ },
10001
+ environmentId,
10002
+ ...argv.projectId ? { projectId: argv.projectId } : {},
10003
+ ...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
10004
+ githubSecretScope,
10005
+ targets: argv.target
10006
+ })).summary.fail > 0) process.exitCode = 1;
10007
+ }).command("$0", false, () => {}, async (argv) => {
10008
+ await launchGroupMenu("secrets", argv);
10009
+ }), () => {}).command("sync", "Sync generated files to other repos", (g) => g.command("github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
10010
+ type: "string",
10011
+ default: "theholocron/.github",
10012
+ describe: "Target org/repo (default: theholocron/.github)"
10013
+ }).option("branch", {
10014
+ type: "string",
10015
+ describe: "Push to this branch instead of the default branch (enables PR-based workflow for protected repos)"
10016
+ }).option("pr", {
10017
+ type: "boolean",
10018
+ default: false,
10019
+ describe: "Open a PR after pushing to --branch (no-op without --branch)"
10020
+ }).option("message", {
9876
10021
  type: "string",
9877
- describe: "Package slug (kebab-case)"
9878
- }).positional("vendor", {
10022
+ describe: "Commit message (default: chore: sync from theholocron/holocron)"
10023
+ }).option("output-dir", {
9879
10024
  type: "string",
9880
- describe: "Vendor display name (PascalCase)"
9881
- }).option("capability", {
10025
+ describe: "Write generated files to this local directory instead of pushing (for validation)"
10026
+ }), async (argv) => {
10027
+ const outputDir = argv["output-dir"];
10028
+ const parsed = tokenContext(argv.token);
10029
+ if (!parsed) return;
10030
+ let token;
10031
+ if (outputDir) token = "no-token-needed";
10032
+ else try {
10033
+ token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
10034
+ } catch (err) {
10035
+ reportError(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
10036
+ process.exitCode = 1;
10037
+ return;
10038
+ }
10039
+ if ((await runSyncGithub({
10040
+ token,
10041
+ repo: argv.repo,
10042
+ dryRun: argv.dryRun,
10043
+ ...argv.branch ? { branch: argv.branch } : {},
10044
+ ...argv.pr ? { createPr: true } : {},
10045
+ ...argv.message ? { message: argv.message } : {},
10046
+ ...outputDir ? { outputDir } : {}
10047
+ })).status === "fail") process.exitCode = 1;
10048
+ }).command("$0", false, () => {}, async (argv) => {
10049
+ await launchGroupMenu("sync", argv);
10050
+ }), () => {}).command("package", "Version and publish workspace packages", (g) => g.command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
9882
10051
  type: "string",
9883
- describe: "Capability key: source|ci|secrets|environments|issues|deployment|storage|auth|vault|dns|tooling|notifications|analytics|errors|logs|wiki|workers"
9884
- }).option("token-env", {
10052
+ describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
10053
+ }), async (argv) => {
10054
+ const [newVersion] = await promptForPositionals(getEntry("package bump-versions"), argv);
10055
+ if ((await runNpmBumpVersions({
10056
+ version: newVersion,
10057
+ cwd: argv.cwd,
10058
+ dryRun: argv.dryRun
10059
+ })).status === "fail") process.exitCode = 1;
10060
+ }).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
10061
+ type: "boolean",
10062
+ default: false,
10063
+ describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
10064
+ }).option("tag", {
9885
10065
  type: "string",
9886
- describe: "Holocron env var name (defaults to HOLOCRON_<VENDOR>_TOKEN)"
9887
- }).option("vendor-env", {
10066
+ describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
10067
+ }).option("otp", {
9888
10068
  type: "string",
9889
- describe: "Vendor-native env var name"
9890
- }).option("base-url", {
10069
+ describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
10070
+ }).option("sync-trust", {
10071
+ type: "array",
10072
+ describe: "Migrate npm Trusted Publisher config to a new workflow filename across every public package: --sync-trust <old-file> <new-file>. See issue #689 — npm requires fresh interactive 2FA per package; re-run after approving the printed authUrl if it stops early."
10073
+ }).option("repo", {
9891
10074
  type: "string",
9892
- describe: "REST base URL"
9893
- }).option("verify", {
10075
+ describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
10076
+ }).option("provenance", {
9894
10077
  type: "boolean",
9895
10078
  default: true,
9896
- describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
10079
+ describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
10080
+ }).option("skip-already-published", {
10081
+ type: "boolean",
10082
+ default: false,
10083
+ describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
9897
10084
  }), async (argv) => {
9898
- try {
9899
- const [slug, vendor] = await promptForPositionals(getEntry("plugin create"), argv);
9900
- const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
9901
- capability: argv.capability,
9902
- vendorEnv: argv.vendorEnv,
9903
- baseUrl: argv.baseUrl
9904
- }, {
9905
- selectCapability: () => select({
9906
- message: "Capability:",
9907
- choices: Object.keys(CARDINALITY).map((k) => ({
9908
- name: k,
9909
- value: k
9910
- }))
9911
- }),
9912
- inputVendorEnv: () => input({ message: `Vendor-native env var for the ${vendor} token (e.g. MYVENDOR_API_KEY):` }),
9913
- inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
9914
- });
9915
- if (runPluginCreate({
9916
- slug,
9917
- vendorName: vendor,
9918
- capability,
9919
- vendorEnv,
9920
- baseUrl,
9921
- ...argv.tokenEnv ? { tokenEnv: argv.tokenEnv } : {},
9922
- dryRun: argv.dryRun,
9923
- noVerify: !argv.verify,
9924
- cwd: argv.cwd
9925
- }).status === "fail") process.exitCode = 1;
9926
- } catch (err) {
9927
- if (err instanceof PluginCreateError) {
9928
- reportError(`plugin create: ${err.message}`);
10085
+ if (argv.syncTrust) {
10086
+ const [oldFile, newFile] = argv.syncTrust;
10087
+ if (!oldFile || !newFile) {
10088
+ reportError("publish --sync-trust requires exactly two values: <old-file> <new-file>");
9929
10089
  process.exitCode = 1;
9930
10090
  return;
9931
10091
  }
9932
- throw err;
10092
+ if ((await runSyncTrust({
10093
+ cwd: argv.cwd,
10094
+ oldFile,
10095
+ newFile,
10096
+ dryRun: argv.dryRun,
10097
+ ...argv.repo ? { repo: argv.repo } : {}
10098
+ })).status === "fail") process.exitCode = 1;
10099
+ return;
9933
10100
  }
9934
- }).command("upgrade", "Upgrade toolchain version pins across the repo", (y) => y.command("node [to]", "Scan the repo and update every Node.js version pin to a new major", (yy) => yy.positional("to", {
10101
+ if (argv.initial) {
10102
+ if ((await runPublish({
10103
+ cwd: argv.cwd,
10104
+ dryRun: argv.dryRun,
10105
+ ...argv.tag ? { tag: argv.tag } : {},
10106
+ ...argv.otp ? { otp: argv.otp } : {}
10107
+ })).status === "fail") process.exitCode = 1;
10108
+ return;
10109
+ }
10110
+ if ((await runSteadyPublish({
10111
+ cwd: argv.cwd,
10112
+ dryRun: argv.dryRun,
10113
+ provenance: argv.provenance,
10114
+ skipAlreadyPublished: argv.skipAlreadyPublished,
10115
+ ...argv.tag ? { tag: argv.tag } : {},
10116
+ ...argv.otp ? { otp: argv.otp } : {}
10117
+ })).status === "fail") process.exitCode = 1;
10118
+ }).command("$0", false, () => {}, async (argv) => {
10119
+ await launchGroupMenu("package", argv);
10120
+ }), () => {}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
9935
10121
  type: "number",
9936
- describe: "Target Node.js major version (e.g., 22)"
10122
+ describe: "PR number to clean up"
10123
+ }).option("project", {
10124
+ type: "string",
10125
+ demandOption: true,
10126
+ describe: "Cloudflare Pages project name (e.g. theholocron-preview)"
10127
+ }).option("repo", {
10128
+ type: "string",
10129
+ describe: "GitHub repo as owner/name — defaults to the repo in holocron.config"
10130
+ }), async (argv) => {
10131
+ const tokens = tokenContext(argv.token);
10132
+ if (!tokens) return;
10133
+ const [pr] = await promptForPositionals(getEntry("deploy cleanup-preview"), argv);
10134
+ const loaded = await loadConfig(argv.cwd);
10135
+ applyResolvedConfig(argv, loaded.resolved);
10136
+ if ((await runCleanupPreview({
10137
+ loaded,
10138
+ context: {
10139
+ repoRoot: argv.cwd,
10140
+ dryRun: argv.dryRun,
10141
+ ...tokens,
10142
+ org: resolveOrg(argv, loaded.resolved)
10143
+ },
10144
+ prNumber: Number(pr),
10145
+ project: argv.project,
10146
+ ...argv.repo ? { repo: argv.repo } : {}
10147
+ })).status === "fail") process.exitCode = 1;
10148
+ }).command("on-release", "Deploy every workspace package whose delivery.deploy task is `on: release` — run by the delivery.publish workflow's deploy job", (y) => y.option("channel", {
10149
+ type: "string",
10150
+ default: "",
10151
+ describe: "The release's channel: its prerelease identifier (`alpha`); empty or `main` for a stable release."
9937
10152
  }).option("from", {
9938
- type: "number",
9939
- describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
10153
+ type: "string",
10154
+ describe: "The previous release's commit, to diff against. Omit when there is none."
10155
+ }).option("to", {
10156
+ type: "string",
10157
+ describe: "The release commit. Default HEAD."
9940
10158
  }), async (argv) => {
9941
- const [to] = await promptForPositionals(getEntry("upgrade node"), argv);
9942
- let extra = [];
9943
- try {
9944
- const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
9945
- const extraRaw = JSON.parse(raw).upgrade?.node?.extra;
9946
- if (Array.isArray(extraRaw)) extra = extraRaw;
9947
- } catch {}
9948
- const report = await runUpgradeNode({
9949
- to: Number(to),
9950
- ...argv.from != null ? { from: argv.from } : {},
10159
+ if ((await deployOnRelease({
9951
10160
  cwd: argv.cwd,
10161
+ channel: argv.channel,
10162
+ ...argv.from ? { from: argv.from } : {},
10163
+ ...argv.to ? { to: argv.to } : {},
9952
10164
  dryRun: argv.dryRun,
9953
- extra
9954
- });
9955
- if (report.status === "fail") {
9956
- if (report.message) reportError(`upgrade node: ${report.message}`);
10165
+ print: (line) => console.log(line)
10166
+ })).status === "fail") process.exitCode = 1;
10167
+ }).positional("branch", {
10168
+ type: "string",
10169
+ describe: "Git branch to deploy (omit when using --files)"
10170
+ }).option("project-id", {
10171
+ type: "string",
10172
+ describe: "Deployment project id (required to trigger a deployment, e.g. Vercel prj_*)"
10173
+ }).option("target", {
10174
+ type: "string",
10175
+ choices: ["production", "staging"],
10176
+ describe: "Named environment to deploy into. Omit for a branch preview."
10177
+ }).option("files", {
10178
+ type: "string",
10179
+ describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
10180
+ }).conflicts("files", "branch"), async (argv) => {
10181
+ if (!argv.projectId) {
10182
+ reportError("deploy: missing required argument --project-id");
9957
10183
  process.exitCode = 1;
10184
+ return;
9958
10185
  }
9959
- }).command("deps", "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API", (yy) => yy.option("pins-only", {
9960
- type: "boolean",
9961
- default: false,
9962
- describe: "Only bump the catalog pins — skip the holocron.config.ts migration"
9963
- }), async (argv) => {
9964
- const report = await runUpgradeDeps({
9965
- cwd: argv.cwd,
10186
+ const tokens = tokenContext(argv.token);
10187
+ if (!tokens) return;
10188
+ const loaded = await loadConfig(argv.cwd);
10189
+ applyResolvedConfig(argv, loaded.resolved);
10190
+ const context = {
10191
+ repoRoot: argv.cwd,
9966
10192
  dryRun: argv.dryRun,
9967
- pinsOnly: argv.pinsOnly
9968
- });
9969
- if (report.status === "fail") {
9970
- if (report.message) reportError(`upgrade deps: ${report.message}`);
9971
- process.exitCode = 1;
10193
+ ...tokens,
10194
+ org: resolveOrg(argv, loaded.resolved)
10195
+ };
10196
+ if (argv.files) {
10197
+ if ((await runDeployFromFiles({
10198
+ loaded,
10199
+ context,
10200
+ projectId: argv.projectId,
10201
+ dir: argv.files,
10202
+ ...argv.target ? { target: argv.target } : {}
10203
+ })).status === "fail") process.exitCode = 1;
10204
+ return;
9972
10205
  }
9973
- }).command("$0", false, () => {}, async (argv) => {
9974
- await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "upgrade"), argv, "upgrade — choose a subcommand:", "Run `holocron upgrade --help` to see available upgrade subcommands.");
9975
- }), () => {}).command("auth <subcommand>", "Manage bootstrap credentials in the OS keyring", (y) => y.command("set [provider] [value]", "Verify + store a bootstrap token for a provider", (yy) => yy.positional("provider", { type: "string" }).positional("value", { type: "string" }), async (argv) => {
9976
- const [provider] = await promptForPositionals(getEntry("auth set"), argv);
9977
- if ((await runAuthSet({
9978
- provider,
9979
- ...argv.value ? { positional: argv.value } : {},
9980
- ...argv.org ? { org: argv.org } : {}
9981
- })).status === "fail") process.exitCode = 1;
9982
- }).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
9983
- const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
9984
- runAuthUnset({ provider });
9985
- }).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
9986
- const [provider] = await promptForPositionals(getEntry("auth check"), argv);
9987
- if ((await runAuthCheck({
9988
- provider,
9989
- ...argv.org ? { org: argv.org } : {}
10206
+ const [branch] = await promptForPositionals(getEntry("deploy"), argv);
10207
+ if ((await runDeploy({
10208
+ loaded,
10209
+ context,
10210
+ projectId: argv.projectId,
10211
+ branch,
10212
+ ...argv.target ? { target: argv.target } : {}
9990
10213
  })).status === "fail") process.exitCode = 1;
9991
- }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
9992
- await runAuthList();
9993
- }).command("$0", false, () => {}, async (argv) => {
9994
- await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
9995
- }), () => {}).command("$0", false, () => {}, async (argv) => {
9996
- printVersionHeaderOnce();
9997
- await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
9998
10214
  }).strict().help().epilogue(`Execution contexts:
9999
10215
  global works from a bare 'npm i -g': ${commandsInContext("global").join(", ")}\n repo-aware needs ./holocron.config in cwd: ${commandsInContext("repo-aware").join(", ")}\n workspace also needs the plugin packages: ${commandsInContext("workspace").join(", ")}\n https://theholocron.github.io/holocron/execution-contexts`).fail((msg, err) => {
10000
10216
  if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {