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

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/README.md CHANGED
@@ -54,11 +54,11 @@ entirely, straight to the real run, for when you already know what you want.
54
54
 
55
55
  Every command is tagged with how much of a repo it needs:
56
56
 
57
- | Context | Needs | Examples |
58
- | ------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
59
- | `global` | the CLI binary alone | `version`, `clone`, `new`, `upgrade node`, `auth set` / `check`, `plugin create`, `skills …` |
60
- | `repo-aware` | `./holocron.config`, walking up from cwd, no plugins | `run`, `ci`, `config show`, `sync-readme` |
61
- | `workspace` | the `@theholocron/holocron-plugin-*` packages resolvable | `doctor`, `setup`, `sync`, `secrets sync`, `deploy` |
57
+ | Context | Needs | Examples |
58
+ | ------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
59
+ | `global` | the CLI binary alone | `version`, `clone`, `new`, `upgrade node`, `auth set` / `check`, `plugin create`, `run lint commit-msg`, `skills …` |
60
+ | `repo-aware` | `./holocron.config`, walking up from cwd, no plugins | `run`, `run ci`, `config show`, `sync-readme` |
61
+ | `workspace` | the `@theholocron/holocron-plugin-*` packages resolvable | `doctor`, `setup`, `sync`, `secrets sync`, `deploy` |
62
62
 
63
63
  A `workspace` command run from a bare global install (no plugins next to the
64
64
  CLI) fails with one actionable line — install the plugins as devDependencies or
@@ -66,6 +66,21 @@ use `pnpm exec holocron <cmd>`. `auth check` degrades to "token present,
66
66
  verification skipped" instead. `holocron --help` prints the grouping; see
67
67
  [Execution Contexts](https://theholocron.github.io/holocron/execution-contexts).
68
68
 
69
+ ### Viewing the resolved config
70
+
71
+ `holocron config show` prints the resolved config. On a TTY it opens a foldable
72
+ tree by default; piped or redirected, it always emits plain, valid JSON
73
+ (`holocron config show | jq`).
74
+
75
+ ```console
76
+ holocron config show # TTY: foldable tree (↑↓ move, →/⏎ expand, ← collapse, E/C all, q quit)
77
+ holocron config show --depth 2 # static coloured tree, anything nested deeper than 2 levels collapsed
78
+ holocron config show --no-interactive # static coloured tree, fully expanded
79
+ ```
80
+
81
+ Passing `--interactive` explicitly needs a TTY on stdin and stdout and fails
82
+ with a one-line message otherwise; the default falls back to static output. It has no extra dependencies — nothing to install.
83
+
69
84
  ## Config file
70
85
 
71
86
  Holocron reads `holocron.config.{json,js,ts}` from the project root
@@ -237,7 +252,7 @@ export default defineConfig({
237
252
  });
238
253
  ```
239
254
 
240
- `holocron setup` writes `.husky/pre-push` (runs `holocron ci` before every
255
+ `holocron setup` writes `.husky/pre-push` (runs `holocron run ci` before every
241
256
  push) and `.husky/pre-commit` (runs `gitleaks protect --staged` + `lint-staged`
242
257
  before every commit — scoped to staged changes, not the whole history, so a
243
258
  past false positive can't permanently block future commits) and sets
@@ -283,7 +298,7 @@ export default defineConfig({
283
298
  `@theholocron/astromech`'s `loadTasksConfig` merges it over
284
299
  `holocron.config`'s `tasks` — **task arrays concatenate**
285
300
  (`holocron.config` first), the dedicated file **wins on scalars**,
286
- `extraRequiredChecks` concatenate. `holocron run` / `holocron ci` /
301
+ `extraRequiredChecks` concatenate. `holocron run` / `holocron run ci` /
287
302
  `holocron setup` / `holocron sync` all read the merged result, so the
288
303
  split is transparent. Useful for keeping the capability/provider config
289
304
  and the "what this repo runs" manifest in separate files (this repo does).
@@ -328,10 +343,10 @@ delivery.deploy`. A package that doesn't apply is skipped; a failed deploy
328
343
  exits non-zero. The shared `delivery.publish` workflow runs it as its own
329
344
  `deploy` job after the release job (opt in with `deploy-on-release: true`).
330
345
 
331
- ### `holocron ci`
346
+ ### `holocron run ci`
332
347
 
333
348
  ```bash
334
- holocron ci [--all] [--filter <pkg>] [--dry-run]
349
+ holocron run ci [--all] [--filter <pkg>] [--dry-run]
335
350
  ```
336
351
 
337
352
  Runs the merge-gating checks locally, in CI order, exiting non-zero on the
@@ -451,7 +466,7 @@ export default acmeConfig;
451
466
  shape (`const { repo, workflows } = node()` + `repo.requiredChecks` +
452
467
  `workflows:`) to the current composed-preset API (`const preset = node()` +
453
468
  `...preset` + `tasks:` + `extraRequiredChecks:`). Mechanical transforms only —
454
- review the result, then `pnpm install && holocron ci`. `--pins-only` skips
469
+ review the result, then `pnpm install && holocron run ci`. `--pins-only` skips
455
470
  the config migration; `--dry-run` prints without writing.
456
471
 
457
472
  ## Auth — fine-grained tokens
package/dist/cli.mjs CHANGED
@@ -330,9 +330,9 @@ const COMMAND_CONTEXTS = {
330
330
  "skills install": "global",
331
331
  "skills remove": "global",
332
332
  "skills update": "global",
333
- "lint commit-msg": "global",
333
+ "run lint commit-msg": "global",
334
334
  run: "repo-aware",
335
- ci: "repo-aware",
335
+ "run ci": "repo-aware",
336
336
  "deploy-on-release": "repo-aware",
337
337
  "config show": "repo-aware",
338
338
  "sync-readme": "repo-aware",
@@ -4521,12 +4521,12 @@ function createIgnoreConfig$1() {
4521
4521
  }
4522
4522
  //#endregion
4523
4523
  //#region src/templates/configs/commit-msg/commit-msg
4524
- var commit_msg_default = "__HOLOCRON_SCRIPT__ lint commit-msg \"$1\"\n";
4524
+ var commit_msg_default = "__HOLOCRON_SCRIPT__ run lint commit-msg \"$1\"\n";
4525
4525
  //#endregion
4526
4526
  //#region src/templates/configs/commit-msg/create-config.ts
4527
4527
  const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/commit-msg/create-config.ts" });
4528
4528
  /**
4529
- * `.husky/commit-msg` — lints the commit message via `holocron lint
4529
+ * `.husky/commit-msg` — lints the commit message via `holocron run lint
4530
4530
  * commit-msg` (holocron#789), the real programmatic replacement for
4531
4531
  * shelling out to `commitlint --edit`. Installed by `holocron setup` when
4532
4532
  * hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
@@ -4623,12 +4623,12 @@ function createConfig$3() {
4623
4623
  }
4624
4624
  //#endregion
4625
4625
  //#region src/templates/configs/pre-push/pre-push
4626
- var pre_push_default = "# Run the merge-gating checks locally before pushing — the same set CI runs.\n# Bypass once with: git push --no-verify\n\n# CI runners (semantic-release, super-linter auto-commit, bots) push without a\n# built dev environment — the hook has nothing to add there.\n[ -n \"$CI\" ] && exit 0\n\n__HOLOCRON_SCRIPT__ ci\n";
4626
+ var pre_push_default = "# Run the merge-gating checks locally before pushing — the same set CI runs.\n# Bypass once with: git push --no-verify\n\n# CI runners (semantic-release, super-linter auto-commit, bots) push without a\n# built dev environment — the hook has nothing to add there.\n[ -n \"$CI\" ] && exit 0\n\n__HOLOCRON_SCRIPT__ run ci\n";
4627
4627
  //#endregion
4628
4628
  //#region src/templates/configs/pre-push/create-config.ts
4629
4629
  const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/pre-push/create-config.ts" });
4630
4630
  /**
4631
- * `.husky/pre-push` — runs `holocron ci` (the merge-gating checks, in CI order)
4631
+ * `.husky/pre-push` — runs `holocron run ci` (the merge-gating checks, in CI order)
4632
4632
  * before every push. Installed by `holocron setup` when hooks are enabled
4633
4633
  * (default for `protection: "strict"`). `git push --no-verify` bypasses it.
4634
4634
  *
@@ -5647,7 +5647,7 @@ async function runSetup(input) {
5647
5647
  if (hooksEnabled) {
5648
5648
  steps.push(await runStep("source", "write .husky/pre-push", dryRun, async () => {
5649
5649
  await source.writeRepoFile(".husky/pre-push", createConfig$2(config.holocronScript));
5650
- return "runs `holocron ci` before push — bypass with `git push --no-verify`";
5650
+ return "runs `holocron run ci` before push — bypass with `git push --no-verify`";
5651
5651
  }));
5652
5652
  print(formatStep(steps[steps.length - 1]));
5653
5653
  steps.push(await runStep("source", "write .husky/pre-commit", dryRun, async () => {
@@ -7614,7 +7614,7 @@ async function runSyncGithub(input) {
7614
7614
  * This applies the mechanical transforms and flags anything that needs a
7615
7615
  * human eye.
7616
7616
  *
7617
- * After running, `pnpm install && holocron ci`.
7617
+ * After running, `pnpm install && holocron run ci`.
7618
7618
  */
7619
7619
  const FETCH_TIMEOUT_MS$1 = 4e3;
7620
7620
  /** Resolve a package's `latest` dist-tag from the npm registry. */
@@ -7681,7 +7681,7 @@ function isLegacyConfig(content) {
7681
7681
  }
7682
7682
  /**
7683
7683
  * Apply the mechanical 7.x → 8.x transforms to a `holocron.config.ts`. Best
7684
- * effort — the result should always be reviewed and run through `holocron ci`.
7684
+ * effort — the result should always be reviewed and run through `holocron run ci`.
7685
7685
  */
7686
7686
  function migrateConfig(content) {
7687
7687
  const transforms = [];
@@ -7807,7 +7807,7 @@ async function runUpgradeDeps(input = {}) {
7807
7807
  print("");
7808
7808
  if (bumps.length === 0 && !configMigrated) print("Nothing to do — already up to date.");
7809
7809
  else {
7810
- print(`Next: ${dryRun ? "run without --dry-run, then " : ""}\`pnpm install && holocron ci\``);
7810
+ print(`Next: ${dryRun ? "run without --dry-run, then " : ""}\`pnpm install && holocron run ci\``);
7811
7811
  if (configMigrated) print(" review holocron.config.ts before committing.");
7812
7812
  }
7813
7813
  const status = dryRun ? "dry-run" : "ok";
@@ -8050,7 +8050,7 @@ async function lintCommitMessage(message, loaded) {
8050
8050
  //#endregion
8051
8051
  //#region src/commit-lint/lint-commit-msg-file.ts
8052
8052
  /**
8053
- * `holocron lint commit-msg <file>` — a real programmatic replacement for
8053
+ * `holocron run lint commit-msg <file>` — a real programmatic replacement for
8054
8054
  * shelling out to `commitlint --edit "$1"` from `.husky/commit-msg`
8055
8055
  * (holocron#789). Matches that CLI's own contract exactly: `file` is a
8056
8056
  * path to the commit message being written (the arg git's `commit-msg`
@@ -8194,9 +8194,10 @@ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
8194
8194
  * The full command surface, hand-maintained in parallel with `cli.ts`'s
8195
8195
  * yargs registrations (this module does not introspect yargs at runtime).
8196
8196
  * `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
8197
- * interactive prompt in front of it would work against that. `lint
8198
- * commit-msg <file>` is excluded for the same reason: it's invoked
8199
- * exclusively from `.husky/commit-msg "$1"`, never typed by a human.
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.
8200
8201
  */
8201
8202
  const COMMAND_REGISTRY = [
8202
8203
  {
@@ -8287,7 +8288,7 @@ const COMMAND_REGISTRY = [
8287
8288
  supportsDryRun: true
8288
8289
  },
8289
8290
  {
8290
- name: "ci",
8291
+ name: "run ci",
8291
8292
  description: "Run the merge-gating checks locally, in CI order — 'will CI pass?'",
8292
8293
  positionals: [],
8293
8294
  supportsDryRun: true
@@ -8599,6 +8600,225 @@ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessag
8599
8600
  process.exitCode = await spawnChild(buildChildArgv(picked, positionals, parentArgv));
8600
8601
  }
8601
8602
  //#endregion
8603
+ //#region src/ui/json-view.ts
8604
+ function isContainer(value) {
8605
+ return typeof value === "object" && value !== null;
8606
+ }
8607
+ function childrenOf(value) {
8608
+ return Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
8609
+ }
8610
+ function scalar(value) {
8611
+ if (typeof value === "string") return chalk.green(JSON.stringify(value));
8612
+ if (typeof value === "number") return chalk.yellow(String(value));
8613
+ if (typeof value === "boolean") return chalk.magenta(String(value));
8614
+ return chalk.dim(String(value === void 0 ? null : value));
8615
+ }
8616
+ function summary(value) {
8617
+ const n = childrenOf(value).length;
8618
+ const unit = Array.isArray(value) ? n === 1 ? "item" : "items" : n === 1 ? "key" : "keys";
8619
+ return chalk.dim(Array.isArray(value) ? `[… ${n} ${unit}]` : `{… ${n} ${unit}}`);
8620
+ }
8621
+ /**
8622
+ * Paths of every container deeper than `depth` levels are left out, so those
8623
+ * render collapsed. `depth: 0` collapses the root; `Infinity` expands all.
8624
+ */
8625
+ function expandToDepth(value, depth) {
8626
+ const out = /* @__PURE__ */ new Set();
8627
+ const walk = (v, path, level) => {
8628
+ if (!isContainer(v) || level >= depth) return;
8629
+ out.add(path);
8630
+ for (const [k, child] of childrenOf(v)) walk(child, `${path}/${k}`, level + 1);
8631
+ };
8632
+ walk(value, "", 0);
8633
+ return out;
8634
+ }
8635
+ /** Flatten `value` into the lines visible under `expanded`. Pure — no I/O. */
8636
+ function renderTree(value, { expanded }) {
8637
+ const lines = [];
8638
+ const walk = (v, path, level, label, trailing) => {
8639
+ const indent = " ".repeat(level);
8640
+ const prefix = `${indent}${label}`;
8641
+ if (!isContainer(v)) {
8642
+ lines.push({
8643
+ path,
8644
+ text: `${prefix}${scalar(v)}${trailing}`,
8645
+ foldable: false,
8646
+ expanded: false,
8647
+ level
8648
+ });
8649
+ return;
8650
+ }
8651
+ const kids = childrenOf(v);
8652
+ const [open, close] = Array.isArray(v) ? ["[", "]"] : ["{", "}"];
8653
+ if (kids.length === 0) {
8654
+ lines.push({
8655
+ path,
8656
+ text: `${prefix}${open}${close}${trailing}`,
8657
+ foldable: false,
8658
+ expanded: false,
8659
+ level
8660
+ });
8661
+ return;
8662
+ }
8663
+ if (!expanded.has(path)) {
8664
+ lines.push({
8665
+ path,
8666
+ text: `${prefix}${summary(v)}${trailing}`,
8667
+ foldable: true,
8668
+ expanded: false,
8669
+ level
8670
+ });
8671
+ return;
8672
+ }
8673
+ lines.push({
8674
+ path,
8675
+ text: `${prefix}${open}`,
8676
+ foldable: true,
8677
+ expanded: true,
8678
+ level
8679
+ });
8680
+ kids.forEach(([k, child], i) => {
8681
+ const comma = i < kids.length - 1 ? "," : "";
8682
+ const childLabel = Array.isArray(v) ? "" : `${chalk.cyan(JSON.stringify(k))}: `;
8683
+ walk(child, `${path}/${k}`, level + 1, childLabel, comma);
8684
+ });
8685
+ lines.push({
8686
+ path: `${path}#close`,
8687
+ text: `${indent}${close}${trailing}`,
8688
+ foldable: false,
8689
+ expanded: false,
8690
+ level
8691
+ });
8692
+ };
8693
+ walk(value, "", 0, "", "");
8694
+ return lines;
8695
+ }
8696
+ /** Pure key reducer: `(state, key) -> state`. `rows` is the viewport height. */
8697
+ function reduceKey(state, key, value, rows) {
8698
+ const lines = renderTree(value, { expanded: state.expanded });
8699
+ const cur = lines[state.cursor];
8700
+ let { cursor } = state;
8701
+ const expanded = new Set(state.expanded);
8702
+ switch (key) {
8703
+ case "up":
8704
+ cursor = Math.max(0, cursor - 1);
8705
+ break;
8706
+ case "down":
8707
+ cursor = Math.min(lines.length - 1, cursor + 1);
8708
+ break;
8709
+ case "right":
8710
+ if (cur?.foldable) expanded.add(cur.path);
8711
+ break;
8712
+ case "left":
8713
+ if (cur?.foldable && cur.expanded) expanded.delete(cur.path);
8714
+ else if (cur) {
8715
+ const parent = lines.findLastIndex((l, i) => i < cursor && l.foldable && l.expanded && l.level < cur.level);
8716
+ if (parent >= 0) cursor = parent;
8717
+ }
8718
+ break;
8719
+ case "expandAll": return {
8720
+ cursor,
8721
+ top: state.top,
8722
+ expanded: expandToDepth(value, Infinity)
8723
+ };
8724
+ case "collapseAll": return {
8725
+ cursor: 0,
8726
+ top: 0,
8727
+ expanded: /* @__PURE__ */ new Set()
8728
+ };
8729
+ }
8730
+ const count = renderTree(value, { expanded }).length;
8731
+ cursor = Math.min(cursor, count - 1);
8732
+ let top = state.top;
8733
+ if (cursor < top) top = cursor;
8734
+ if (cursor >= top + rows) top = cursor - rows + 1;
8735
+ return {
8736
+ cursor,
8737
+ top: Math.max(0, top),
8738
+ expanded
8739
+ };
8740
+ }
8741
+ const KEYMAP = {
8742
+ "\x1B[A": "up",
8743
+ k: "up",
8744
+ "\x1B[B": "down",
8745
+ j: "down",
8746
+ "\x1B[C": "right",
8747
+ l: "right",
8748
+ "\r": "right",
8749
+ " ": "right",
8750
+ "\x1B[D": "left",
8751
+ h: "left",
8752
+ E: "expandAll",
8753
+ C: "collapseAll",
8754
+ q: "quit",
8755
+ "\x1B": "quit",
8756
+ "": "quit"
8757
+ };
8758
+ function draw(out, value, state, rows) {
8759
+ const lines = renderTree(value, { expanded: state.expanded });
8760
+ const view = lines.slice(state.top, state.top + rows).map((l, i) => {
8761
+ const row = `${l.foldable ? l.expanded ? "▾ " : "▸ " : " "}${l.text}`;
8762
+ return state.top + i === state.cursor ? chalk.inverse(row) : row;
8763
+ });
8764
+ const footer = chalk.dim(`${state.cursor + 1}/${lines.length} ↑↓ move →/⏎ expand ← collapse E/C all q quit`);
8765
+ out.write(`\u001b[H\u001b[J${view.join("\n")}\n${footer}`);
8766
+ }
8767
+ async function runInteractive(value, depth) {
8768
+ const { stdin, stdout } = process;
8769
+ if (!stdin.isTTY || !stdout.isTTY) throw new NonInteractiveError("--interactive needs a TTY on stdin and stdout.");
8770
+ const rows = () => Math.max(3, (stdout.rows ?? 24) - 1);
8771
+ let state = {
8772
+ cursor: 0,
8773
+ top: 0,
8774
+ expanded: expandToDepth(value, depth)
8775
+ };
8776
+ const wasRaw = stdin.isRaw;
8777
+ stdin.setRawMode(true);
8778
+ stdin.resume();
8779
+ stdout.write("\x1B[?1049h\x1B[?25l");
8780
+ const redraw = () => draw(stdout, value, state, rows());
8781
+ stdout.on("resize", redraw);
8782
+ try {
8783
+ redraw();
8784
+ await new Promise((resolve) => {
8785
+ const onData = (buf) => {
8786
+ const key = KEYMAP[buf.toString()];
8787
+ if (key === "quit") {
8788
+ stdin.off("data", onData);
8789
+ resolve();
8790
+ return;
8791
+ }
8792
+ if (key) {
8793
+ state = reduceKey(state, key, value, rows());
8794
+ redraw();
8795
+ }
8796
+ };
8797
+ stdin.on("data", onData);
8798
+ });
8799
+ } finally {
8800
+ stdout.off("resize", redraw);
8801
+ stdout.write("\x1B[?25h\x1B[?1049l");
8802
+ stdin.setRawMode(wasRaw);
8803
+ stdin.pause();
8804
+ }
8805
+ }
8806
+ /**
8807
+ * Print `value` as JSON. Non-TTY stdout always gets plain, valid
8808
+ * `JSON.stringify` output (so `| jq` and redirects keep working); a TTY gets a
8809
+ * coloured tree, collapsed past `depth`. `interactive` opens a folding viewer (the default on a TTY).
8810
+ */
8811
+ async function viewJson(value, options = {}) {
8812
+ const depth = options.depth ?? Infinity;
8813
+ if (options.interactive ?? (options.depth === void 0 && Boolean(process.stdin.isTTY && process.stdout.isTTY))) return runInteractive(value, options.depth ?? 1);
8814
+ if (!process.stdout.isTTY) {
8815
+ console.log(JSON.stringify(value, null, 2));
8816
+ return;
8817
+ }
8818
+ const lines = renderTree(value, { expanded: expandToDepth(value, depth) });
8819
+ console.log(lines.map((l) => l.text).join("\n"));
8820
+ }
8821
+ //#endregion
8602
8822
  //#region src/ui/open-path.ts
8603
8823
  /** Platform-appropriate command to open a path in the OS's file browser. */
8604
8824
  function openCommand(path) {
@@ -8946,7 +9166,7 @@ try {
8946
9166
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
8947
9167
  }).option("hooks", {
8948
9168
  type: "boolean",
8949
- describe: "Install git hooks (.husky/pre-push → holocron ci). Default: on for protection:'strict'. --no-hooks to skip."
9169
+ describe: "Install git hooks (.husky/pre-push → holocron run ci). Default: on for protection:'strict'. --no-hooks to skip."
8950
9170
  }), async (argv) => {
8951
9171
  const tokens = tokenContext(argv.token);
8952
9172
  if (!tokens) return;
@@ -9235,13 +9455,13 @@ try {
9235
9455
  },
9236
9456
  ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
9237
9457
  })).summary.fail > 0) process.exitCode = 1;
9238
- }).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager", (y) => y.positional("task", {
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", {
9239
9459
  type: "string",
9240
9460
  demandOption: true,
9241
- describe: "Task name (test, typecheck, lint, build, audit). See `holocron run --help`."
9461
+ describe: "Task name (test, typecheck, lint, build, audit, or `ci` for the merge-gating suite). See `holocron run --help`."
9242
9462
  }).positional("job", {
9243
9463
  type: "string",
9244
- describe: "Sub-job within the task (e.g. `holocron run audit performance`). Omit to run every job. Only `audit` has jobs today."
9464
+ describe: "Sub-job within the task (e.g. `holocron run audit performance`, `holocron run lint commit-msg <file>`). Omit to run every job."
9245
9465
  }).positional("passthrough", {
9246
9466
  type: "string",
9247
9467
  array: true,
@@ -9250,11 +9470,43 @@ try {
9250
9470
  type: "boolean",
9251
9471
  default: false,
9252
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."
9253
9477
  }).option("filter", {
9254
9478
  type: "string",
9255
9479
  describe: "turbo --filter=<pkg> passthrough (monorepo)."
9256
9480
  }), async (argv) => {
9257
- const { logger } = buildCliLogger(argv, { command: "run" });
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
+ }
9258
9510
  const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
9259
9511
  const report = createAstromech({
9260
9512
  cwd: argv.cwd,
@@ -9262,31 +9514,12 @@ try {
9262
9514
  config
9263
9515
  }).run(argv.task, {
9264
9516
  ...argv.job !== void 0 ? { job: argv.job } : {},
9265
- passthrough: [...argv.passthrough ?? [], ...argv["--"] ?? []],
9517
+ passthrough,
9266
9518
  dryRun: argv.dryRun,
9267
9519
  required: argv.required,
9268
9520
  ...argv.filter ? { filter: argv.filter } : {}
9269
9521
  });
9270
9522
  if (report.status === "fail" || report.status === "unknown") process.exitCode = 1;
9271
- }).command("ci", "Run the merge-gating checks locally, in CI order — 'will CI pass?'", (y) => y.option("all", {
9272
- type: "boolean",
9273
- default: false,
9274
- describe: "Run every `ci: true` task, not just the required ones."
9275
- }).option("filter", {
9276
- type: "string",
9277
- describe: "turbo --filter=<pkg> passthrough (monorepo)."
9278
- }), async (argv) => {
9279
- const { logger } = buildCliLogger(argv, { command: "ci" });
9280
- const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
9281
- if (createAstromech({
9282
- cwd: argv.cwd,
9283
- logger,
9284
- config
9285
- }).ci({
9286
- dryRun: argv.dryRun,
9287
- scope: argv.all ? "all" : "required",
9288
- ...argv.filter ? { filter: argv.filter } : {}
9289
- }).status === "fail") process.exitCode = 1;
9290
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", {
9291
9524
  type: "string",
9292
9525
  default: "",
@@ -9306,17 +9539,7 @@ try {
9306
9539
  dryRun: argv.dryRun,
9307
9540
  print: (line) => console.log(line)
9308
9541
  })).status === "fail") process.exitCode = 1;
9309
- }).command("lint", "Lint content locally against the org's shared rules", (y) => y.command("commit-msg <file>", "Lint a commit message file — matches `commitlint --edit`'s own contract, for use from .husky/commit-msg", (yy) => yy.positional("file", {
9310
- type: "string",
9311
- demandOption: true,
9312
- describe: "Path to the commit message file (git's commit-msg hook passes this as $1)."
9313
- }), async (argv) => {
9314
- const result = await lintCommitMsgFile(argv.file, argv.cwd);
9315
- if (!result.valid) {
9316
- for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
9317
- process.exitCode = 1;
9318
- }
9319
- }), () => {}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
9542
+ }).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
9320
9543
  type: "string",
9321
9544
  default: "theholocron/.github",
9322
9545
  describe: "Target org/repo (default: theholocron/.github)"
@@ -9373,9 +9596,17 @@ try {
9373
9596
  if (report.message) reportError(`sync-readme: ${report.message}`);
9374
9597
  process.exitCode = 1;
9375
9598
  }
9376
- }).command("config show", "Print the resolved holocron config", () => {}, async (argv) => {
9377
- const loaded = await loadConfig(argv.cwd);
9378
- console.log(JSON.stringify(loaded.resolved, null, 2));
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
+ });
9379
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", {
9380
9611
  type: "string",
9381
9612
  describe: "Template type — maps to theholocron/<type>-template (e.g. cli, react, nextjs, node, monorepo, base)"