@theholocron/cli 4.17.2 → 4.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,27 @@ holocron --help
14
14
 
15
15
  ```
16
16
 
17
+ ## Interactive mode
18
+
19
+ Run `holocron` with no command, a parent command with no subcommand
20
+ (`holocron auth`, `holocron skills`, `holocron upgrade`), or a leaf command
21
+ missing a required positional (`holocron deploy`, `holocron secret set`), and
22
+ you get a prompt instead of a `--help` dead end:
23
+
24
+ ```
25
+ $ holocron
26
+ ? What would you like to do? › dep
27
+ ────────────────────────────────────────────────
28
+ ❯ deploy Trigger a deployment via the configured `deployment` capability
29
+
30
+ ? Branch to deploy: › main
31
+ ```
32
+
33
+ The picked command spawns as a normal `holocron <command> …` invocation —
34
+ `--token`, `--org`, `--cwd`, and `--dry-run` on the original invocation carry
35
+ through. A non-interactive shell (CI, a script, a pipe) gets the old
36
+ hard-failure message instead of hanging on a prompt.
37
+
17
38
  ## Execution contexts
18
39
 
19
40
  Every command is tagged with how much of a repo it needs:
@@ -426,6 +447,10 @@ holds the orchestration + the Holocron-specific credential resolution
426
447
  - `src/commands/` — `setup`, `sync`, `doctor`, `deploy`, `secret set`,
427
448
  `secrets sync`, `publish`, `bump-versions`, `sync-github`, `upgrade node`,
428
449
  `upgrade deps`, `plugin create`, `auth`
450
+ - `src/interactive-menu.ts` — the interactive fallback: `COMMAND_REGISTRY`,
451
+ `launchMenu` (Layers 1–2, picks + spawns a fresh `holocron <command>`),
452
+ `promptForPositionals` (Layer 3, called inline from each leaf command's
453
+ `cli.ts` handler)
429
454
 
430
455
  ## Status
431
456
 
package/dist/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/observability/logger";
15
15
  import ora from "ora";
16
16
  import chalk from "chalk";
17
- import { execFile, execFileSync, spawnSync } from "node:child_process";
17
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
18
18
  import { homedir, hostname, userInfo } from "node:os";
19
19
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
20
20
  import { createHash } from "node:crypto";
@@ -26,6 +26,7 @@ import { NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/obs
26
26
  import { SentrySink } from "@theholocron/observability/errors";
27
27
  import { promisify } from "node:util";
28
28
  import { ConfigFileError, loadConfigFile } from "@theholocron/datapad";
29
+ import search from "@inquirer/search";
29
30
  //#region src/env.ts
30
31
  /** Singleton env for simple global lookups throughout the CLI. */
31
32
  const env = createEnvLookup();
@@ -6878,6 +6879,350 @@ function parseGitRemoteUrl(url) {
6878
6879
  if (sshMatch) return sshMatch[1];
6879
6880
  }
6880
6881
  //#endregion
6882
+ //#region src/interactive-menu.ts
6883
+ /**
6884
+ * The interactive fallback — replaces every "run --help to see commands"
6885
+ * dead end with a prompt. Three layers, all driven by {@link COMMAND_REGISTRY}:
6886
+ *
6887
+ * 1. `holocron` with no command → searchable picker over every command
6888
+ * 2. a parent command with no sub → `select()` over that group's commands
6889
+ * 3. a leaf command missing a → `input()` / `select()` per positional
6890
+ * required positional
6891
+ *
6892
+ * Layers 1–2 are driven from `cli.ts`'s `$0` handlers via {@link launchMenu}:
6893
+ * pick a command, prompt for its required positionals, then **spawn a fresh
6894
+ * `holocron <command> …` child process** (`stdio: "inherit"`) rather than
6895
+ * re-entering yargs' own parse pipeline — see the spec's "spawn vs re-parse"
6896
+ * section (theholocron/holocron#438) for why. Layer 3 is driven directly from
6897
+ * each leaf command's own handler in `cli.ts`, calling
6898
+ * {@link promptForPositionals} for just that one command — no spawn needed,
6899
+ * the handler already has everything else it needs.
6900
+ *
6901
+ * Every prompt is preceded by a `process.stdin.isTTY` check. A non-TTY run
6902
+ * (CI, a script, a pipe) throws {@link NonInteractiveError} instead of
6903
+ * hanging on `@inquirer/prompts` (which otherwise throws its own
6904
+ * `ExitPromptError` the moment stdin closes) — `cli.ts`'s `USER_FACING_ERRORS`
6905
+ * catch prints its message cleanly, restoring the old demandCommand-style
6906
+ * hard failure for non-interactive callers.
6907
+ */
6908
+ /** Raised when a prompt is needed but stdin isn't a TTY — see the module doc. */
6909
+ var NonInteractiveError = class extends Error {
6910
+ name = "NonInteractiveError";
6911
+ };
6912
+ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
6913
+ /**
6914
+ * The full command surface, hand-maintained in parallel with `cli.ts`'s
6915
+ * yargs registrations (this module does not introspect yargs at runtime).
6916
+ * `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
6917
+ * interactive prompt in front of it would work against that.
6918
+ */
6919
+ const COMMAND_REGISTRY = [
6920
+ {
6921
+ name: "version",
6922
+ description: "Print the CLI version",
6923
+ positionals: []
6924
+ },
6925
+ {
6926
+ name: "clone",
6927
+ description: "Clone all repos in a GitHub org as siblings under a single directory",
6928
+ positionals: []
6929
+ },
6930
+ {
6931
+ name: "doctor",
6932
+ description: "Load the config and run a smoke check against every provider",
6933
+ positionals: []
6934
+ },
6935
+ {
6936
+ name: "setup",
6937
+ description: "Apply infra setup actions across every configured capability",
6938
+ positionals: []
6939
+ },
6940
+ {
6941
+ name: "secret set",
6942
+ description: "Set a single secret via the configured `secrets` capability",
6943
+ positionals: [{
6944
+ key: "name",
6945
+ message: "Secret name:",
6946
+ type: "input"
6947
+ }]
6948
+ },
6949
+ {
6950
+ name: "secrets sync",
6951
+ description: "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars",
6952
+ positionals: [{
6953
+ key: "environmentId",
6954
+ message: "Vault environment id:",
6955
+ type: "input"
6956
+ }]
6957
+ },
6958
+ {
6959
+ name: "deploy",
6960
+ description: "Trigger a deployment via the configured `deployment` capability",
6961
+ positionals: [{
6962
+ key: "branch",
6963
+ message: "Branch to deploy:",
6964
+ type: "input"
6965
+ }]
6966
+ },
6967
+ {
6968
+ name: "cleanup-preview",
6969
+ description: "List and delete Cloudflare Pages preview deployments for a GitHub PR",
6970
+ positionals: [{
6971
+ key: "pr",
6972
+ message: "PR number:",
6973
+ type: "input",
6974
+ validate: numeric
6975
+ }]
6976
+ },
6977
+ {
6978
+ name: "bump-versions",
6979
+ description: "Bump all non-private package versions in lockstep (semantic-release prepareCmd)",
6980
+ positionals: [{
6981
+ key: "newVersion",
6982
+ cliArg: "new-version",
6983
+ message: "New version:",
6984
+ type: "input"
6985
+ }]
6986
+ },
6987
+ {
6988
+ name: "publish",
6989
+ description: "Publish @theholocron/* packages to npm",
6990
+ positionals: []
6991
+ },
6992
+ {
6993
+ name: "sync",
6994
+ description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
6995
+ positionals: []
6996
+ },
6997
+ {
6998
+ name: "ci",
6999
+ description: "Run the merge-gating checks locally, in CI order — 'will CI pass?'",
7000
+ positionals: []
7001
+ },
7002
+ {
7003
+ name: "sync-github",
7004
+ description: "Sync workflow templates and composite actions to theholocron/.github",
7005
+ positionals: []
7006
+ },
7007
+ {
7008
+ name: "sync-readme",
7009
+ description: "Sync the Installation + Usage block in README.md from package.json",
7010
+ positionals: []
7011
+ },
7012
+ {
7013
+ name: "config show",
7014
+ description: "Print the resolved holocron config",
7015
+ positionals: []
7016
+ },
7017
+ {
7018
+ name: "new",
7019
+ description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
7020
+ positionals: []
7021
+ },
7022
+ {
7023
+ name: "plugin create",
7024
+ description: "Scaffold a new @theholocron/holocron-plugin-<slug> package",
7025
+ positionals: [{
7026
+ key: "slug",
7027
+ message: "Package slug (kebab-case):",
7028
+ type: "input"
7029
+ }, {
7030
+ key: "vendor",
7031
+ message: "Vendor display name (PascalCase):",
7032
+ type: "input"
7033
+ }]
7034
+ },
7035
+ {
7036
+ name: "skills install",
7037
+ description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
7038
+ positionals: [],
7039
+ group: "skills"
7040
+ },
7041
+ {
7042
+ name: "skills remove",
7043
+ description: "Remove installed skills via npx skills remove",
7044
+ positionals: [],
7045
+ group: "skills"
7046
+ },
7047
+ {
7048
+ name: "skills update",
7049
+ description: "Update installed skills to their latest upstream versions via npx skills update",
7050
+ positionals: [],
7051
+ group: "skills"
7052
+ },
7053
+ {
7054
+ name: "upgrade node",
7055
+ description: "Scan the repo and update every Node.js version pin to a new major",
7056
+ positionals: [{
7057
+ key: "to",
7058
+ message: "Target Node.js major version:",
7059
+ type: "input",
7060
+ validate: numeric
7061
+ }],
7062
+ group: "upgrade"
7063
+ },
7064
+ {
7065
+ name: "upgrade deps",
7066
+ description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
7067
+ positionals: [],
7068
+ group: "upgrade"
7069
+ },
7070
+ {
7071
+ name: "auth set",
7072
+ description: "Verify + store a bootstrap token for a provider",
7073
+ positionals: [{
7074
+ key: "provider",
7075
+ message: "Provider name:",
7076
+ type: "input"
7077
+ }],
7078
+ group: "auth"
7079
+ },
7080
+ {
7081
+ name: "auth unset",
7082
+ description: "Remove a stored bootstrap token",
7083
+ positionals: [{
7084
+ key: "provider",
7085
+ message: "Provider:",
7086
+ type: "select",
7087
+ choices: listStoredProviders
7088
+ }],
7089
+ group: "auth"
7090
+ },
7091
+ {
7092
+ name: "auth check",
7093
+ description: "Re-verify a stored bootstrap token",
7094
+ positionals: [{
7095
+ key: "provider",
7096
+ message: "Provider:",
7097
+ type: "select",
7098
+ choices: listStoredProviders
7099
+ }],
7100
+ group: "auth"
7101
+ },
7102
+ {
7103
+ name: "auth list",
7104
+ description: "List every provider with a stored bootstrap token",
7105
+ positionals: [],
7106
+ group: "auth"
7107
+ }
7108
+ ];
7109
+ /** Look up a {@link CommandEntry} by its full name — for Layer 3 call sites in `cli.ts`. */
7110
+ function getEntry(name) {
7111
+ const entry = COMMAND_REGISTRY.find((e) => e.name === name);
7112
+ if (!entry) throw new Error(`interactive-menu: no COMMAND_REGISTRY entry named "${name}"`);
7113
+ return entry;
7114
+ }
7115
+ /**
7116
+ * Searchable autocomplete over `entries` — the Layer 1 top-level picker, and
7117
+ * (passed a `group`-filtered subset) the Layer 2 parent-command picker.
7118
+ * `select()` would work too at these list sizes, but `search()` degrades
7119
+ * gracefully as the surface grows and costs nothing when it doesn't.
7120
+ */
7121
+ async function pickCommand(entries, message = "What would you like to do?") {
7122
+ const byName = new Map(entries.map((e) => [e.name, e]));
7123
+ const picked = await search({
7124
+ message,
7125
+ source: (term) => searchChoices(entries, term)
7126
+ });
7127
+ return byName.get(picked);
7128
+ }
7129
+ /**
7130
+ * `@inquirer/search`'s `source` callback, factored out as a plain function —
7131
+ * unit-testable directly instead of only through a mocked `search()` call.
7132
+ * Empty/undefined `term` (nothing typed yet) returns every entry.
7133
+ */
7134
+ function searchChoices(entries, term) {
7135
+ return (!term ? entries : entries.filter((e) => matches(e, term))).map((e) => ({
7136
+ name: e.name,
7137
+ value: e.name,
7138
+ description: e.description
7139
+ }));
7140
+ }
7141
+ function matches(entry, term) {
7142
+ const needle = term.toLowerCase();
7143
+ return entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle);
7144
+ }
7145
+ /**
7146
+ * For each of `entry.positionals` not already present in `argv`, prompts for
7147
+ * it (`input()` or `select()`, per the positional's `type`) and returns the
7148
+ * resolved values in positional order — ready to append to a spawned child's
7149
+ * argv, or to use directly in the current handler (Layer 3).
7150
+ *
7151
+ * Throws {@link NonInteractiveError} the first time it would need to prompt
7152
+ * on a non-TTY stdin, and again if a `select` positional's choices resolve
7153
+ * empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
7154
+ */
7155
+ async function promptForPositionals(entry, argv) {
7156
+ const out = [];
7157
+ for (const positional of entry.positionals) {
7158
+ const existing = argv[positional.key];
7159
+ if (existing !== void 0 && existing !== "") {
7160
+ out.push(String(existing));
7161
+ continue;
7162
+ }
7163
+ if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` needs "${positional.key}" — pass it directly: holocron ${entry.name} <${positional.cliArg ?? positional.key}>`);
7164
+ if (positional.type === "select") {
7165
+ const choices = typeof positional.choices === "function" ? positional.choices() : positional.choices ?? [];
7166
+ if (choices.length === 0) throw new NonInteractiveError(`\`${entry.name}\`: no ${positional.key} to choose from — nothing stored yet`);
7167
+ out.push(await select({
7168
+ message: positional.message,
7169
+ choices: choices.map((c) => ({ value: c }))
7170
+ }));
7171
+ } else out.push(await input({
7172
+ message: positional.message,
7173
+ ...positional.validate ? { validate: positional.validate } : {}
7174
+ }));
7175
+ }
7176
+ return out;
7177
+ }
7178
+ /**
7179
+ * `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
7180
+ * menu-launched child should inherit from the parent invocation. Everything
7181
+ * else (command-specific options) was never captured by the top-level `$0`
7182
+ * handler in the first place, so there's nothing else to forward.
7183
+ */
7184
+ function forwardedFlags(argv) {
7185
+ const out = [];
7186
+ const tokens = argv.token;
7187
+ for (const t of tokens ?? []) out.push("--token", t);
7188
+ if (typeof argv.org === "string") out.push("--org", argv.org);
7189
+ if (typeof argv.cwd === "string") out.push("--cwd", argv.cwd);
7190
+ if (argv.dryRun === true) out.push("--dry-run");
7191
+ return out;
7192
+ }
7193
+ /** `entry.name` split into tokens, resolved positionals appended, then forwarded flags. */
7194
+ function buildChildArgv(entry, positionals, parentArgv) {
7195
+ return [
7196
+ ...entry.name.split(" "),
7197
+ ...positionals,
7198
+ ...forwardedFlags(parentArgv)
7199
+ ];
7200
+ }
7201
+ /** Spawn `holocron <...args>` inheriting stdio; resolve with its exit code. */
7202
+ function spawnChild(args) {
7203
+ return new Promise((resolve) => {
7204
+ const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: "inherit" });
7205
+ child.on("exit", (code) => resolve(code ?? 0));
7206
+ child.on("error", () => resolve(1));
7207
+ });
7208
+ }
7209
+ /**
7210
+ * Layers 1 and 2: pick a command from `entries`, prompt for its required
7211
+ * positionals, spawn it as a fresh `holocron` invocation, and set
7212
+ * `process.exitCode` from the child — the caller (a `$0` handler in
7213
+ * `cli.ts`) returns normally afterward so the parent's own telemetry
7214
+ * flush / update-notifier tail still runs. The parent's own
7215
+ * `command_completed` event fires with command name "unknown" (the
7216
+ * middleware ran before any command was picked) — left as-is rather than
7217
+ * suppressed; it's a real, useful signal ("the menu got used").
7218
+ */
7219
+ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
7220
+ if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
7221
+ const picked = await pickCommand(entries, pickMessage);
7222
+ const childArgv = buildChildArgv(picked, await promptForPositionals(picked, {}), parentArgv);
7223
+ process.exitCode = await spawnChild(childArgv);
7224
+ }
7225
+ //#endregion
6881
7226
  //#region src/update-notifier.ts
6882
7227
  const PACKAGE_NAME = "@theholocron/cli";
6883
7228
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
@@ -7003,7 +7348,11 @@ const resolveSyncToken = createFeatureResolver({
7003
7348
  * Error class names whose `.message` is a complete, actionable sentence —
7004
7349
  * the top-level catch prints it and suppresses the stack trace.
7005
7350
  */
7006
- const USER_FACING_ERRORS = /* @__PURE__ */ new Set(["WorkspaceContextError", "ConfigFileError"]);
7351
+ const USER_FACING_ERRORS = /* @__PURE__ */ new Set([
7352
+ "WorkspaceContextError",
7353
+ "ConfigFileError",
7354
+ "NonInteractiveError"
7355
+ ]);
7007
7356
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
7008
7357
  /** Whether to print the correlation id at command end (`--debug` / `--verbose`). */
7009
7358
  let printRunId = false;
@@ -7191,9 +7540,10 @@ try {
7191
7540
  },
7192
7541
  ...argv.name ? { name: argv.name } : {}
7193
7542
  }).status === "fail") process.exitCode = 1;
7194
- }).demandCommand(1, "Run `holocron skills --help` to see available skills subcommands."), () => {}).command("secret set <name> [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
7543
+ }).command("$0", false, () => {}, async (argv) => {
7544
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "skills"), argv, "skills — choose a subcommand:", "Run `holocron skills --help` to see available skills subcommands.");
7545
+ }), () => {}).command("secret set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
7195
7546
  type: "string",
7196
- demandOption: true,
7197
7547
  describe: "Secret name (e.g., NPM_TOKEN)"
7198
7548
  }).positional("value", {
7199
7549
  type: "string",
@@ -7212,6 +7562,7 @@ try {
7212
7562
  }), async (argv) => {
7213
7563
  const tokens = tokenContext(argv.token);
7214
7564
  if (!tokens) return;
7565
+ const [name] = await promptForPositionals(getEntry("secret set"), argv);
7215
7566
  const scopeArg = argv.scope;
7216
7567
  const scope = parseScope(scopeArg);
7217
7568
  const loaded = await loadConfig(argv.cwd);
@@ -7224,15 +7575,14 @@ try {
7224
7575
  ...tokens,
7225
7576
  org: resolveOrg(argv, loaded.resolved)
7226
7577
  },
7227
- name: argv.name,
7578
+ name,
7228
7579
  ...argv.value ? { value: argv.value } : {},
7229
7580
  ...argv.fromStdin ? { fromStdin: true } : {},
7230
7581
  ...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
7231
7582
  scope
7232
7583
  })).status === "fail") process.exitCode = 1;
7233
- }).command("secrets sync <environmentId>", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
7584
+ }).command("secrets sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
7234
7585
  type: "string",
7235
- demandOption: true,
7236
7586
  describe: "Vault environment id to read (1P Environment id, etc.)"
7237
7587
  }).option("project-id", {
7238
7588
  type: "string",
@@ -7244,6 +7594,7 @@ try {
7244
7594
  }), async (argv) => {
7245
7595
  const tokens = tokenContext(argv.token);
7246
7596
  if (!tokens) return;
7597
+ const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
7247
7598
  const loaded = await loadConfig(argv.cwd);
7248
7599
  applyResolvedConfig(argv, loaded.resolved);
7249
7600
  if ((await runSecretsSync({
@@ -7254,13 +7605,12 @@ try {
7254
7605
  ...tokens,
7255
7606
  org: resolveOrg(argv, loaded.resolved)
7256
7607
  },
7257
- environmentId: argv.environmentId,
7608
+ environmentId,
7258
7609
  ...argv.projectId ? { projectId: argv.projectId } : {},
7259
7610
  targets: argv.target
7260
7611
  })).summary.fail > 0) process.exitCode = 1;
7261
- }).command("deploy <branch>", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
7612
+ }).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
7262
7613
  type: "string",
7263
- demandOption: true,
7264
7614
  describe: "Git branch to deploy"
7265
7615
  }).option("project-id", {
7266
7616
  type: "string",
@@ -7273,6 +7623,7 @@ try {
7273
7623
  }), async (argv) => {
7274
7624
  const tokens = tokenContext(argv.token);
7275
7625
  if (!tokens) return;
7626
+ const [branch] = await promptForPositionals(getEntry("deploy"), argv);
7276
7627
  const loaded = await loadConfig(argv.cwd);
7277
7628
  applyResolvedConfig(argv, loaded.resolved);
7278
7629
  if ((await runDeploy({
@@ -7284,12 +7635,11 @@ try {
7284
7635
  org: resolveOrg(argv, loaded.resolved)
7285
7636
  },
7286
7637
  projectId: argv.projectId,
7287
- branch: argv.branch,
7638
+ branch,
7288
7639
  ...argv.target ? { target: argv.target } : {}
7289
7640
  })).status === "fail") process.exitCode = 1;
7290
- }).command("cleanup-preview <pr>", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
7641
+ }).command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
7291
7642
  type: "number",
7292
- demandOption: true,
7293
7643
  describe: "PR number to clean up"
7294
7644
  }).option("project", {
7295
7645
  type: "string",
@@ -7301,6 +7651,7 @@ try {
7301
7651
  }), async (argv) => {
7302
7652
  const tokens = tokenContext(argv.token);
7303
7653
  if (!tokens) return;
7654
+ const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
7304
7655
  const loaded = await loadConfig(argv.cwd);
7305
7656
  applyResolvedConfig(argv, loaded.resolved);
7306
7657
  if ((await runCleanupPreview({
@@ -7311,17 +7662,17 @@ try {
7311
7662
  ...tokens,
7312
7663
  org: resolveOrg(argv, loaded.resolved)
7313
7664
  },
7314
- prNumber: argv.pr,
7665
+ prNumber: Number(pr),
7315
7666
  project: argv.project,
7316
7667
  ...argv.repo ? { repo: argv.repo } : {}
7317
7668
  })).status === "fail") process.exitCode = 1;
7318
- }).command("bump-versions <new-version>", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
7669
+ }).command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
7319
7670
  type: "string",
7320
- demandOption: true,
7321
7671
  describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
7322
7672
  }), async (argv) => {
7673
+ const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
7323
7674
  if ((await runNpmBumpVersions({
7324
- version: argv.newVersion,
7675
+ version: newVersion,
7325
7676
  cwd: argv.cwd,
7326
7677
  dryRun: argv.dryRun
7327
7678
  })).status === "fail") process.exitCode = 1;
@@ -7744,13 +8095,11 @@ try {
7744
8095
  }
7745
8096
  throw err;
7746
8097
  }
7747
- }).command("plugin create <slug> <vendor>", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
8098
+ }).command("plugin create [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
7748
8099
  type: "string",
7749
- demandOption: true,
7750
8100
  describe: "Package slug (kebab-case)"
7751
8101
  }).positional("vendor", {
7752
8102
  type: "string",
7753
- demandOption: true,
7754
8103
  describe: "Vendor display name (PascalCase)"
7755
8104
  }).option("capability", {
7756
8105
  type: "string",
@@ -7770,7 +8119,7 @@ try {
7770
8119
  describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
7771
8120
  }), async (argv) => {
7772
8121
  try {
7773
- const vendor = argv.vendor;
8122
+ const [slug, vendor] = await promptForPositionals(getEntry("plugin create"), argv);
7774
8123
  const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
7775
8124
  capability: argv.capability,
7776
8125
  vendorEnv: argv.vendorEnv,
@@ -7787,7 +8136,7 @@ try {
7787
8136
  inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
7788
8137
  });
7789
8138
  if (runPluginCreate({
7790
- slug: argv.slug,
8139
+ slug,
7791
8140
  vendorName: vendor,
7792
8141
  capability,
7793
8142
  vendorEnv,
@@ -7805,14 +8154,14 @@ try {
7805
8154
  }
7806
8155
  throw err;
7807
8156
  }
7808
- }).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", {
8157
+ }).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", {
7809
8158
  type: "number",
7810
- demandOption: true,
7811
8159
  describe: "Target Node.js major version (e.g., 22)"
7812
8160
  }).option("from", {
7813
8161
  type: "number",
7814
8162
  describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
7815
8163
  }), async (argv) => {
8164
+ const [to] = await promptForPositionals(getEntry("upgrade node"), argv);
7816
8165
  let extra = [];
7817
8166
  try {
7818
8167
  const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
@@ -7820,7 +8169,7 @@ try {
7820
8169
  if (Array.isArray(extraRaw)) extra = extraRaw;
7821
8170
  } catch {}
7822
8171
  const report = await runUpgradeNode({
7823
- to: argv.to,
8172
+ to: Number(to),
7824
8173
  ...argv.from != null ? { from: argv.from } : {},
7825
8174
  cwd: argv.cwd,
7826
8175
  dryRun: argv.dryRun,
@@ -7844,31 +8193,31 @@ try {
7844
8193
  if (report.message) getLogger().error(`upgrade deps: ${report.message}`);
7845
8194
  process.exitCode = 1;
7846
8195
  }
7847
- }).demandCommand(1, "Run `holocron upgrade --help` to see available upgrade subcommands."), () => {}).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", {
7848
- type: "string",
7849
- demandOption: true
7850
- }).positional("value", { type: "string" }), async (argv) => {
8196
+ }).command("$0", false, () => {}, async (argv) => {
8197
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "upgrade"), argv, "upgrade — choose a subcommand:", "Run `holocron upgrade --help` to see available upgrade subcommands.");
8198
+ }), () => {}).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) => {
8199
+ const [provider] = await promptForPositionals(getEntry("auth set"), argv);
7851
8200
  if ((await runAuthSet({
7852
- provider: argv.provider,
8201
+ provider,
7853
8202
  ...argv.value ? { positional: argv.value } : {},
7854
8203
  ...argv.org ? { org: argv.org } : {}
7855
8204
  })).status === "fail") process.exitCode = 1;
7856
- }).command("unset <provider>", "Remove a stored bootstrap token", (yy) => yy.positional("provider", {
7857
- type: "string",
7858
- demandOption: true
7859
- }), (argv) => {
7860
- runAuthUnset({ provider: argv.provider });
7861
- }).command("check <provider>", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", {
7862
- type: "string",
7863
- demandOption: true
7864
- }), async (argv) => {
8205
+ }).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
8206
+ const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
8207
+ runAuthUnset({ provider });
8208
+ }).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
8209
+ const [provider] = await promptForPositionals(getEntry("auth check"), argv);
7865
8210
  if ((await runAuthCheck({
7866
- provider: argv.provider,
8211
+ provider,
7867
8212
  ...argv.org ? { org: argv.org } : {}
7868
8213
  })).status === "fail") process.exitCode = 1;
7869
8214
  }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
7870
8215
  await runAuthList();
7871
- }).demandCommand(1, "Run `holocron auth --help` to see available auth subcommands."), () => {}).demandCommand(1, "Run `holocron --help` to see available commands.").strict().help().epilogue(`Execution contexts:
8216
+ }).command("$0", false, () => {}, async (argv) => {
8217
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
8218
+ }), () => {}).command("$0", false, () => {}, async (argv) => {
8219
+ await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
8220
+ }).strict().help().epilogue(`Execution contexts:
7872
8221
  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) => {
7873
8222
  if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
7874
8223
  captureException(err);