@theholocron/cli 4.17.2 → 4.18.1

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
@@ -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();
@@ -4483,6 +4484,7 @@ const { workflowHeader: workflowHeader$1 } = createHeader({
4483
4484
  });
4484
4485
  async function runSetup(input) {
4485
4486
  const print = input.print ?? ((line) => console.log(line));
4487
+ const logger = getLogger();
4486
4488
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
4487
4489
  await withSpinner("Loading plugins…", () => loader.load());
4488
4490
  assertPluginsResolvable(loader, "setup");
@@ -4529,7 +4531,8 @@ async function runSetup(input) {
4529
4531
  if (config.extraRequiredChecks) tasksConfig.extraRequiredChecks = config.extraRequiredChecks;
4530
4532
  const requiredChecks = effectivePreset === "strict" ? ["DCO", ...createAstromech({
4531
4533
  cwd: input.context.repoRoot,
4532
- config: tasksConfig
4534
+ config: tasksConfig,
4535
+ logger
4533
4536
  }).requiredChecks()] : [];
4534
4537
  steps.push(await upsertBranchProtection(source, dryRun, requiredChecks));
4535
4538
  print(formatStep(steps[steps.length - 1]));
@@ -4545,7 +4548,8 @@ async function runSetup(input) {
4545
4548
  orgContext: {
4546
4549
  org: config.org,
4547
4550
  domain: config.domain
4548
- }
4551
+ },
4552
+ logger
4549
4553
  }).thinCallers();
4550
4554
  for (const entry of tasks) {
4551
4555
  const name = typeof entry === "string" ? entry : entry.name;
@@ -4648,7 +4652,10 @@ async function runSetup(input) {
4648
4652
  message: "no test workflow configured"
4649
4653
  });
4650
4654
  else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
4651
- const content = createAstromech({ cwd: input.context.repoRoot }).codecovConfig(existing);
4655
+ const content = createAstromech({
4656
+ cwd: input.context.repoRoot,
4657
+ logger
4658
+ }).codecovConfig(existing);
4652
4659
  await source.writeRepoFile("codecov.yml", content);
4653
4660
  const packageCount = readWorkspacePackages(input.context.repoRoot).length;
4654
4661
  return packageCount > 0 ? `${packageCount} components` : "no components";
@@ -5787,7 +5794,8 @@ async function runSync(input) {
5787
5794
  orgContext: {
5788
5795
  org: config.org,
5789
5796
  domain: config.domain
5790
- }
5797
+ },
5798
+ logger
5791
5799
  }).thinCallers();
5792
5800
  for (const entry of taskEntries) {
5793
5801
  const name = typeof entry === "string" ? entry : entry.name;
@@ -5825,7 +5833,8 @@ async function runSync(input) {
5825
5833
  if (config.holocronScript !== void 0) tasksConfig.holocronScript = config.holocronScript;
5826
5834
  const desired = createAstromech({
5827
5835
  cwd: input.context.repoRoot,
5828
- config: tasksConfig
5836
+ config: tasksConfig,
5837
+ logger
5829
5838
  }).packageScripts();
5830
5839
  steps.push(await runSyncStep("local", "sync scripts", dryRun, async () => {
5831
5840
  const changed = await mergePackageJsonScripts(input.context.repoRoot, desired);
@@ -6878,6 +6887,350 @@ function parseGitRemoteUrl(url) {
6878
6887
  if (sshMatch) return sshMatch[1];
6879
6888
  }
6880
6889
  //#endregion
6890
+ //#region src/interactive-menu.ts
6891
+ /**
6892
+ * The interactive fallback — replaces every "run --help to see commands"
6893
+ * dead end with a prompt. Three layers, all driven by {@link COMMAND_REGISTRY}:
6894
+ *
6895
+ * 1. `holocron` with no command → searchable picker over every command
6896
+ * 2. a parent command with no sub → `select()` over that group's commands
6897
+ * 3. a leaf command missing a → `input()` / `select()` per positional
6898
+ * required positional
6899
+ *
6900
+ * Layers 1–2 are driven from `cli.ts`'s `$0` handlers via {@link launchMenu}:
6901
+ * pick a command, prompt for its required positionals, then **spawn a fresh
6902
+ * `holocron <command> …` child process** (`stdio: "inherit"`) rather than
6903
+ * re-entering yargs' own parse pipeline — see the spec's "spawn vs re-parse"
6904
+ * section (theholocron/holocron#438) for why. Layer 3 is driven directly from
6905
+ * each leaf command's own handler in `cli.ts`, calling
6906
+ * {@link promptForPositionals} for just that one command — no spawn needed,
6907
+ * the handler already has everything else it needs.
6908
+ *
6909
+ * Every prompt is preceded by a `process.stdin.isTTY` check. A non-TTY run
6910
+ * (CI, a script, a pipe) throws {@link NonInteractiveError} instead of
6911
+ * hanging on `@inquirer/prompts` (which otherwise throws its own
6912
+ * `ExitPromptError` the moment stdin closes) — `cli.ts`'s `USER_FACING_ERRORS`
6913
+ * catch prints its message cleanly, restoring the old demandCommand-style
6914
+ * hard failure for non-interactive callers.
6915
+ */
6916
+ /** Raised when a prompt is needed but stdin isn't a TTY — see the module doc. */
6917
+ var NonInteractiveError = class extends Error {
6918
+ name = "NonInteractiveError";
6919
+ };
6920
+ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
6921
+ /**
6922
+ * The full command surface, hand-maintained in parallel with `cli.ts`'s
6923
+ * yargs registrations (this module does not introspect yargs at runtime).
6924
+ * `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
6925
+ * interactive prompt in front of it would work against that.
6926
+ */
6927
+ const COMMAND_REGISTRY = [
6928
+ {
6929
+ name: "version",
6930
+ description: "Print the CLI version",
6931
+ positionals: []
6932
+ },
6933
+ {
6934
+ name: "clone",
6935
+ description: "Clone all repos in a GitHub org as siblings under a single directory",
6936
+ positionals: []
6937
+ },
6938
+ {
6939
+ name: "doctor",
6940
+ description: "Load the config and run a smoke check against every provider",
6941
+ positionals: []
6942
+ },
6943
+ {
6944
+ name: "setup",
6945
+ description: "Apply infra setup actions across every configured capability",
6946
+ positionals: []
6947
+ },
6948
+ {
6949
+ name: "secret set",
6950
+ description: "Set a single secret via the configured `secrets` capability",
6951
+ positionals: [{
6952
+ key: "name",
6953
+ message: "Secret name:",
6954
+ type: "input"
6955
+ }]
6956
+ },
6957
+ {
6958
+ name: "secrets sync",
6959
+ description: "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars",
6960
+ positionals: [{
6961
+ key: "environmentId",
6962
+ message: "Vault environment id:",
6963
+ type: "input"
6964
+ }]
6965
+ },
6966
+ {
6967
+ name: "deploy",
6968
+ description: "Trigger a deployment via the configured `deployment` capability",
6969
+ positionals: [{
6970
+ key: "branch",
6971
+ message: "Branch to deploy:",
6972
+ type: "input"
6973
+ }]
6974
+ },
6975
+ {
6976
+ name: "cleanup-preview",
6977
+ description: "List and delete Cloudflare Pages preview deployments for a GitHub PR",
6978
+ positionals: [{
6979
+ key: "pr",
6980
+ message: "PR number:",
6981
+ type: "input",
6982
+ validate: numeric
6983
+ }]
6984
+ },
6985
+ {
6986
+ name: "bump-versions",
6987
+ description: "Bump all non-private package versions in lockstep (semantic-release prepareCmd)",
6988
+ positionals: [{
6989
+ key: "newVersion",
6990
+ cliArg: "new-version",
6991
+ message: "New version:",
6992
+ type: "input"
6993
+ }]
6994
+ },
6995
+ {
6996
+ name: "publish",
6997
+ description: "Publish @theholocron/* packages to npm",
6998
+ positionals: []
6999
+ },
7000
+ {
7001
+ name: "sync",
7002
+ description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
7003
+ positionals: []
7004
+ },
7005
+ {
7006
+ name: "ci",
7007
+ description: "Run the merge-gating checks locally, in CI order — 'will CI pass?'",
7008
+ positionals: []
7009
+ },
7010
+ {
7011
+ name: "sync-github",
7012
+ description: "Sync workflow templates and composite actions to theholocron/.github",
7013
+ positionals: []
7014
+ },
7015
+ {
7016
+ name: "sync-readme",
7017
+ description: "Sync the Installation + Usage block in README.md from package.json",
7018
+ positionals: []
7019
+ },
7020
+ {
7021
+ name: "config show",
7022
+ description: "Print the resolved holocron config",
7023
+ positionals: []
7024
+ },
7025
+ {
7026
+ name: "new",
7027
+ description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
7028
+ positionals: []
7029
+ },
7030
+ {
7031
+ name: "plugin create",
7032
+ description: "Scaffold a new @theholocron/holocron-plugin-<slug> package",
7033
+ positionals: [{
7034
+ key: "slug",
7035
+ message: "Package slug (kebab-case):",
7036
+ type: "input"
7037
+ }, {
7038
+ key: "vendor",
7039
+ message: "Vendor display name (PascalCase):",
7040
+ type: "input"
7041
+ }]
7042
+ },
7043
+ {
7044
+ name: "skills install",
7045
+ description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
7046
+ positionals: [],
7047
+ group: "skills"
7048
+ },
7049
+ {
7050
+ name: "skills remove",
7051
+ description: "Remove installed skills via npx skills remove",
7052
+ positionals: [],
7053
+ group: "skills"
7054
+ },
7055
+ {
7056
+ name: "skills update",
7057
+ description: "Update installed skills to their latest upstream versions via npx skills update",
7058
+ positionals: [],
7059
+ group: "skills"
7060
+ },
7061
+ {
7062
+ name: "upgrade node",
7063
+ description: "Scan the repo and update every Node.js version pin to a new major",
7064
+ positionals: [{
7065
+ key: "to",
7066
+ message: "Target Node.js major version:",
7067
+ type: "input",
7068
+ validate: numeric
7069
+ }],
7070
+ group: "upgrade"
7071
+ },
7072
+ {
7073
+ name: "upgrade deps",
7074
+ description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
7075
+ positionals: [],
7076
+ group: "upgrade"
7077
+ },
7078
+ {
7079
+ name: "auth set",
7080
+ description: "Verify + store a bootstrap token for a provider",
7081
+ positionals: [{
7082
+ key: "provider",
7083
+ message: "Provider name:",
7084
+ type: "input"
7085
+ }],
7086
+ group: "auth"
7087
+ },
7088
+ {
7089
+ name: "auth unset",
7090
+ description: "Remove a stored bootstrap token",
7091
+ positionals: [{
7092
+ key: "provider",
7093
+ message: "Provider:",
7094
+ type: "select",
7095
+ choices: listStoredProviders
7096
+ }],
7097
+ group: "auth"
7098
+ },
7099
+ {
7100
+ name: "auth check",
7101
+ description: "Re-verify a stored bootstrap token",
7102
+ positionals: [{
7103
+ key: "provider",
7104
+ message: "Provider:",
7105
+ type: "select",
7106
+ choices: listStoredProviders
7107
+ }],
7108
+ group: "auth"
7109
+ },
7110
+ {
7111
+ name: "auth list",
7112
+ description: "List every provider with a stored bootstrap token",
7113
+ positionals: [],
7114
+ group: "auth"
7115
+ }
7116
+ ];
7117
+ /** Look up a {@link CommandEntry} by its full name — for Layer 3 call sites in `cli.ts`. */
7118
+ function getEntry(name) {
7119
+ const entry = COMMAND_REGISTRY.find((e) => e.name === name);
7120
+ if (!entry) throw new Error(`interactive-menu: no COMMAND_REGISTRY entry named "${name}"`);
7121
+ return entry;
7122
+ }
7123
+ /**
7124
+ * Searchable autocomplete over `entries` — the Layer 1 top-level picker, and
7125
+ * (passed a `group`-filtered subset) the Layer 2 parent-command picker.
7126
+ * `select()` would work too at these list sizes, but `search()` degrades
7127
+ * gracefully as the surface grows and costs nothing when it doesn't.
7128
+ */
7129
+ async function pickCommand(entries, message = "What would you like to do?") {
7130
+ const byName = new Map(entries.map((e) => [e.name, e]));
7131
+ const picked = await search({
7132
+ message,
7133
+ source: (term) => searchChoices(entries, term)
7134
+ });
7135
+ return byName.get(picked);
7136
+ }
7137
+ /**
7138
+ * `@inquirer/search`'s `source` callback, factored out as a plain function —
7139
+ * unit-testable directly instead of only through a mocked `search()` call.
7140
+ * Empty/undefined `term` (nothing typed yet) returns every entry.
7141
+ */
7142
+ function searchChoices(entries, term) {
7143
+ return (!term ? entries : entries.filter((e) => matches(e, term))).map((e) => ({
7144
+ name: e.name,
7145
+ value: e.name,
7146
+ description: e.description
7147
+ }));
7148
+ }
7149
+ function matches(entry, term) {
7150
+ const needle = term.toLowerCase();
7151
+ return entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle);
7152
+ }
7153
+ /**
7154
+ * For each of `entry.positionals` not already present in `argv`, prompts for
7155
+ * it (`input()` or `select()`, per the positional's `type`) and returns the
7156
+ * resolved values in positional order — ready to append to a spawned child's
7157
+ * argv, or to use directly in the current handler (Layer 3).
7158
+ *
7159
+ * Throws {@link NonInteractiveError} the first time it would need to prompt
7160
+ * on a non-TTY stdin, and again if a `select` positional's choices resolve
7161
+ * empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
7162
+ */
7163
+ async function promptForPositionals(entry, argv) {
7164
+ const out = [];
7165
+ for (const positional of entry.positionals) {
7166
+ const existing = argv[positional.key];
7167
+ if (existing !== void 0 && existing !== "") {
7168
+ out.push(String(existing));
7169
+ continue;
7170
+ }
7171
+ if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` needs "${positional.key}" — pass it directly: holocron ${entry.name} <${positional.cliArg ?? positional.key}>`);
7172
+ if (positional.type === "select") {
7173
+ const choices = typeof positional.choices === "function" ? positional.choices() : positional.choices ?? [];
7174
+ if (choices.length === 0) throw new NonInteractiveError(`\`${entry.name}\`: no ${positional.key} to choose from — nothing stored yet`);
7175
+ out.push(await select({
7176
+ message: positional.message,
7177
+ choices: choices.map((c) => ({ value: c }))
7178
+ }));
7179
+ } else out.push(await input({
7180
+ message: positional.message,
7181
+ ...positional.validate ? { validate: positional.validate } : {}
7182
+ }));
7183
+ }
7184
+ return out;
7185
+ }
7186
+ /**
7187
+ * `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
7188
+ * menu-launched child should inherit from the parent invocation. Everything
7189
+ * else (command-specific options) was never captured by the top-level `$0`
7190
+ * handler in the first place, so there's nothing else to forward.
7191
+ */
7192
+ function forwardedFlags(argv) {
7193
+ const out = [];
7194
+ const tokens = argv.token;
7195
+ for (const t of tokens ?? []) out.push("--token", t);
7196
+ if (typeof argv.org === "string") out.push("--org", argv.org);
7197
+ if (typeof argv.cwd === "string") out.push("--cwd", argv.cwd);
7198
+ if (argv.dryRun === true) out.push("--dry-run");
7199
+ return out;
7200
+ }
7201
+ /** `entry.name` split into tokens, resolved positionals appended, then forwarded flags. */
7202
+ function buildChildArgv(entry, positionals, parentArgv) {
7203
+ return [
7204
+ ...entry.name.split(" "),
7205
+ ...positionals,
7206
+ ...forwardedFlags(parentArgv)
7207
+ ];
7208
+ }
7209
+ /** Spawn `holocron <...args>` inheriting stdio; resolve with its exit code. */
7210
+ function spawnChild(args) {
7211
+ return new Promise((resolve) => {
7212
+ const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: "inherit" });
7213
+ child.on("exit", (code) => resolve(code ?? 0));
7214
+ child.on("error", () => resolve(1));
7215
+ });
7216
+ }
7217
+ /**
7218
+ * Layers 1 and 2: pick a command from `entries`, prompt for its required
7219
+ * positionals, spawn it as a fresh `holocron` invocation, and set
7220
+ * `process.exitCode` from the child — the caller (a `$0` handler in
7221
+ * `cli.ts`) returns normally afterward so the parent's own telemetry
7222
+ * flush / update-notifier tail still runs. The parent's own
7223
+ * `command_completed` event fires with command name "unknown" (the
7224
+ * middleware ran before any command was picked) — left as-is rather than
7225
+ * suppressed; it's a real, useful signal ("the menu got used").
7226
+ */
7227
+ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
7228
+ if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
7229
+ const picked = await pickCommand(entries, pickMessage);
7230
+ const childArgv = buildChildArgv(picked, await promptForPositionals(picked, {}), parentArgv);
7231
+ process.exitCode = await spawnChild(childArgv);
7232
+ }
7233
+ //#endregion
6881
7234
  //#region src/update-notifier.ts
6882
7235
  const PACKAGE_NAME = "@theholocron/cli";
6883
7236
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
@@ -7003,7 +7356,11 @@ const resolveSyncToken = createFeatureResolver({
7003
7356
  * Error class names whose `.message` is a complete, actionable sentence —
7004
7357
  * the top-level catch prints it and suppresses the stack trace.
7005
7358
  */
7006
- const USER_FACING_ERRORS = /* @__PURE__ */ new Set(["WorkspaceContextError", "ConfigFileError"]);
7359
+ const USER_FACING_ERRORS = /* @__PURE__ */ new Set([
7360
+ "WorkspaceContextError",
7361
+ "ConfigFileError",
7362
+ "NonInteractiveError"
7363
+ ]);
7007
7364
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
7008
7365
  /** Whether to print the correlation id at command end (`--debug` / `--verbose`). */
7009
7366
  let printRunId = false;
@@ -7191,9 +7548,10 @@ try {
7191
7548
  },
7192
7549
  ...argv.name ? { name: argv.name } : {}
7193
7550
  }).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", {
7551
+ }).command("$0", false, () => {}, async (argv) => {
7552
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "skills"), argv, "skills — choose a subcommand:", "Run `holocron skills --help` to see available skills subcommands.");
7553
+ }), () => {}).command("secret set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
7195
7554
  type: "string",
7196
- demandOption: true,
7197
7555
  describe: "Secret name (e.g., NPM_TOKEN)"
7198
7556
  }).positional("value", {
7199
7557
  type: "string",
@@ -7212,6 +7570,7 @@ try {
7212
7570
  }), async (argv) => {
7213
7571
  const tokens = tokenContext(argv.token);
7214
7572
  if (!tokens) return;
7573
+ const [name] = await promptForPositionals(getEntry("secret set"), argv);
7215
7574
  const scopeArg = argv.scope;
7216
7575
  const scope = parseScope(scopeArg);
7217
7576
  const loaded = await loadConfig(argv.cwd);
@@ -7224,15 +7583,14 @@ try {
7224
7583
  ...tokens,
7225
7584
  org: resolveOrg(argv, loaded.resolved)
7226
7585
  },
7227
- name: argv.name,
7586
+ name,
7228
7587
  ...argv.value ? { value: argv.value } : {},
7229
7588
  ...argv.fromStdin ? { fromStdin: true } : {},
7230
7589
  ...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
7231
7590
  scope
7232
7591
  })).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", {
7592
+ }).command("secrets sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
7234
7593
  type: "string",
7235
- demandOption: true,
7236
7594
  describe: "Vault environment id to read (1P Environment id, etc.)"
7237
7595
  }).option("project-id", {
7238
7596
  type: "string",
@@ -7244,6 +7602,7 @@ try {
7244
7602
  }), async (argv) => {
7245
7603
  const tokens = tokenContext(argv.token);
7246
7604
  if (!tokens) return;
7605
+ const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
7247
7606
  const loaded = await loadConfig(argv.cwd);
7248
7607
  applyResolvedConfig(argv, loaded.resolved);
7249
7608
  if ((await runSecretsSync({
@@ -7254,13 +7613,12 @@ try {
7254
7613
  ...tokens,
7255
7614
  org: resolveOrg(argv, loaded.resolved)
7256
7615
  },
7257
- environmentId: argv.environmentId,
7616
+ environmentId,
7258
7617
  ...argv.projectId ? { projectId: argv.projectId } : {},
7259
7618
  targets: argv.target
7260
7619
  })).summary.fail > 0) process.exitCode = 1;
7261
- }).command("deploy <branch>", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
7620
+ }).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
7262
7621
  type: "string",
7263
- demandOption: true,
7264
7622
  describe: "Git branch to deploy"
7265
7623
  }).option("project-id", {
7266
7624
  type: "string",
@@ -7273,6 +7631,7 @@ try {
7273
7631
  }), async (argv) => {
7274
7632
  const tokens = tokenContext(argv.token);
7275
7633
  if (!tokens) return;
7634
+ const [branch] = await promptForPositionals(getEntry("deploy"), argv);
7276
7635
  const loaded = await loadConfig(argv.cwd);
7277
7636
  applyResolvedConfig(argv, loaded.resolved);
7278
7637
  if ((await runDeploy({
@@ -7284,12 +7643,11 @@ try {
7284
7643
  org: resolveOrg(argv, loaded.resolved)
7285
7644
  },
7286
7645
  projectId: argv.projectId,
7287
- branch: argv.branch,
7646
+ branch,
7288
7647
  ...argv.target ? { target: argv.target } : {}
7289
7648
  })).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", {
7649
+ }).command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
7291
7650
  type: "number",
7292
- demandOption: true,
7293
7651
  describe: "PR number to clean up"
7294
7652
  }).option("project", {
7295
7653
  type: "string",
@@ -7301,6 +7659,7 @@ try {
7301
7659
  }), async (argv) => {
7302
7660
  const tokens = tokenContext(argv.token);
7303
7661
  if (!tokens) return;
7662
+ const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
7304
7663
  const loaded = await loadConfig(argv.cwd);
7305
7664
  applyResolvedConfig(argv, loaded.resolved);
7306
7665
  if ((await runCleanupPreview({
@@ -7311,17 +7670,17 @@ try {
7311
7670
  ...tokens,
7312
7671
  org: resolveOrg(argv, loaded.resolved)
7313
7672
  },
7314
- prNumber: argv.pr,
7673
+ prNumber: Number(pr),
7315
7674
  project: argv.project,
7316
7675
  ...argv.repo ? { repo: argv.repo } : {}
7317
7676
  })).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", {
7677
+ }).command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
7319
7678
  type: "string",
7320
- demandOption: true,
7321
7679
  describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
7322
7680
  }), async (argv) => {
7681
+ const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
7323
7682
  if ((await runNpmBumpVersions({
7324
- version: argv.newVersion,
7683
+ version: newVersion,
7325
7684
  cwd: argv.cwd,
7326
7685
  dryRun: argv.dryRun
7327
7686
  })).status === "fail") process.exitCode = 1;
@@ -7744,13 +8103,11 @@ try {
7744
8103
  }
7745
8104
  throw err;
7746
8105
  }
7747
- }).command("plugin create <slug> <vendor>", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
8106
+ }).command("plugin create [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
7748
8107
  type: "string",
7749
- demandOption: true,
7750
8108
  describe: "Package slug (kebab-case)"
7751
8109
  }).positional("vendor", {
7752
8110
  type: "string",
7753
- demandOption: true,
7754
8111
  describe: "Vendor display name (PascalCase)"
7755
8112
  }).option("capability", {
7756
8113
  type: "string",
@@ -7770,7 +8127,7 @@ try {
7770
8127
  describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
7771
8128
  }), async (argv) => {
7772
8129
  try {
7773
- const vendor = argv.vendor;
8130
+ const [slug, vendor] = await promptForPositionals(getEntry("plugin create"), argv);
7774
8131
  const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
7775
8132
  capability: argv.capability,
7776
8133
  vendorEnv: argv.vendorEnv,
@@ -7787,7 +8144,7 @@ try {
7787
8144
  inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
7788
8145
  });
7789
8146
  if (runPluginCreate({
7790
- slug: argv.slug,
8147
+ slug,
7791
8148
  vendorName: vendor,
7792
8149
  capability,
7793
8150
  vendorEnv,
@@ -7805,14 +8162,14 @@ try {
7805
8162
  }
7806
8163
  throw err;
7807
8164
  }
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", {
8165
+ }).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
8166
  type: "number",
7810
- demandOption: true,
7811
8167
  describe: "Target Node.js major version (e.g., 22)"
7812
8168
  }).option("from", {
7813
8169
  type: "number",
7814
8170
  describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
7815
8171
  }), async (argv) => {
8172
+ const [to] = await promptForPositionals(getEntry("upgrade node"), argv);
7816
8173
  let extra = [];
7817
8174
  try {
7818
8175
  const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
@@ -7820,7 +8177,7 @@ try {
7820
8177
  if (Array.isArray(extraRaw)) extra = extraRaw;
7821
8178
  } catch {}
7822
8179
  const report = await runUpgradeNode({
7823
- to: argv.to,
8180
+ to: Number(to),
7824
8181
  ...argv.from != null ? { from: argv.from } : {},
7825
8182
  cwd: argv.cwd,
7826
8183
  dryRun: argv.dryRun,
@@ -7844,31 +8201,31 @@ try {
7844
8201
  if (report.message) getLogger().error(`upgrade deps: ${report.message}`);
7845
8202
  process.exitCode = 1;
7846
8203
  }
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) => {
8204
+ }).command("$0", false, () => {}, async (argv) => {
8205
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "upgrade"), argv, "upgrade — choose a subcommand:", "Run `holocron upgrade --help` to see available upgrade subcommands.");
8206
+ }), () => {}).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) => {
8207
+ const [provider] = await promptForPositionals(getEntry("auth set"), argv);
7851
8208
  if ((await runAuthSet({
7852
- provider: argv.provider,
8209
+ provider,
7853
8210
  ...argv.value ? { positional: argv.value } : {},
7854
8211
  ...argv.org ? { org: argv.org } : {}
7855
8212
  })).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) => {
8213
+ }).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
8214
+ const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
8215
+ runAuthUnset({ provider });
8216
+ }).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
8217
+ const [provider] = await promptForPositionals(getEntry("auth check"), argv);
7865
8218
  if ((await runAuthCheck({
7866
- provider: argv.provider,
8219
+ provider,
7867
8220
  ...argv.org ? { org: argv.org } : {}
7868
8221
  })).status === "fail") process.exitCode = 1;
7869
8222
  }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
7870
8223
  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:
8224
+ }).command("$0", false, () => {}, async (argv) => {
8225
+ await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
8226
+ }), () => {}).command("$0", false, () => {}, async (argv) => {
8227
+ await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
8228
+ }).strict().help().epilogue(`Execution contexts:
7872
8229
  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
8230
  if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
7874
8231
  captureException(err);