@theholocron/cli 4.17.1 → 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/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
4
  import path, { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { checkbox, input, select } from "@inquirer/prompts";
6
- import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, reusableTemplates } from "@theholocron/astromech";
6
+ import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, readWorkspacePackages, reusableTemplates } from "@theholocron/astromech";
7
7
  import { loadTasksConfig } from "@theholocron/astromech/config";
8
8
  import yargs from "yargs";
9
9
  import { hideBin } from "yargs/helpers";
@@ -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();
@@ -3570,86 +3571,6 @@ function createIgnoreConfig() {
3570
3571
  return `${workflowHeader$6()}${alexignore_default}`;
3571
3572
  }
3572
3573
  //#endregion
3573
- //#region src/templates/configs/codecov/codecov.yml
3574
- var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
3575
- //#endregion
3576
- //#region src/templates/configs/codecov/utils.ts
3577
- const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
3578
- function codecovComponentBlock(packages) {
3579
- if (packages.length === 0) return "\n []\n";
3580
- return "\n" + packages.flatMap(({ slug }) => [
3581
- ` - component_id: ${slug}`,
3582
- ` name: "${slug}"`,
3583
- ` paths:`,
3584
- ` - packages/${slug}/**`,
3585
- ``
3586
- ]).join("\n");
3587
- }
3588
- /**
3589
- * Ensure every codecov status default carries `if_not_found: success`, so an
3590
- * infra-only PR (dep bump, config, lockfile — no coverable change) passes
3591
- * instead of hanging forever on a required check codecov never posts.
3592
- *
3593
- * Injects the key on the line after each `target:` that sits inside a status
3594
- * block (`coverage.status.*.default` or a `statuses:` list item — indent ≥ 8,
3595
- * or ≥ 8 for a `- type:` list entry), when the immediate sibling lines do not
3596
- * already declare it.
3597
- */
3598
- function ensureIfNotFound(yaml) {
3599
- const lines = yaml.split("\n");
3600
- const out = [];
3601
- for (let i = 0; i < lines.length; i++) {
3602
- const line = lines[i];
3603
- out.push(line);
3604
- const m = /^(\s{8,})target:\s+\S+\s*$/.exec(line);
3605
- if (!m) continue;
3606
- const indent = m[1];
3607
- let alreadySet = false;
3608
- for (let j = i + 1; j < lines.length; j++) {
3609
- const sib = lines[j];
3610
- if (sib.trim() === "") continue;
3611
- const sibIndent = sib.match(/^\s*/)[0].length;
3612
- if (sibIndent < indent.length) break;
3613
- if (sibIndent === indent.length && /^\s*if_not_found\s*:/.test(sib)) {
3614
- alreadySet = true;
3615
- break;
3616
- }
3617
- if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
3618
- }
3619
- if (!alreadySet) out.push(`${indent}if_not_found: success`);
3620
- }
3621
- return out.join("\n");
3622
- }
3623
- function mergeCodecovComponents(existing, packages) {
3624
- const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
3625
- if (idx === -1) return ensureIfNotFound(existing);
3626
- return ensureIfNotFound(existing.slice(0, idx + 24)) + codecovComponentBlock(packages);
3627
- }
3628
- async function readWorkspacePackages(repoRoot) {
3629
- const packagesDir = join(repoRoot, "packages");
3630
- const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
3631
- if (!entries) return [];
3632
- const packages = [];
3633
- for (const entry of entries) {
3634
- if (!entry.isDirectory()) continue;
3635
- try {
3636
- const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
3637
- const pkg = JSON.parse(raw);
3638
- if (typeof pkg.name === "string") packages.push({
3639
- slug: entry.name,
3640
- name: pkg.name
3641
- });
3642
- } catch {}
3643
- }
3644
- return packages.sort((a, b) => a.slug.localeCompare(b.slug));
3645
- }
3646
- //#endregion
3647
- //#region src/templates/configs/codecov/create-config.ts
3648
- const { scaffoldHeader } = createHeader({ source: "packages/cli/src/templates/configs/codecov/create-config.ts" });
3649
- function createConfig$5(packages) {
3650
- return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
3651
- }
3652
- //#endregion
3653
3574
  //#region src/templates/configs/devmoji/create-config.ts
3654
3575
  const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
3655
3576
  function createConfig$4() {
@@ -4720,7 +4641,6 @@ async function runSetup(input) {
4720
4641
  }
4721
4642
  {
4722
4643
  const hasTestWorkflow = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("test");
4723
- const packages = await readWorkspacePackages(input.context.repoRoot);
4724
4644
  const existing = await readFile(join(input.context.repoRoot, "codecov.yml"), "utf8").catch(() => null);
4725
4645
  if (!hasTestWorkflow && existing == null) steps.push({
4726
4646
  capability: "source",
@@ -4729,9 +4649,10 @@ async function runSetup(input) {
4729
4649
  message: "no test workflow configured"
4730
4650
  });
4731
4651
  else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
4732
- const content = existing != null ? mergeCodecovComponents(existing, packages) : createConfig$5(packages);
4652
+ const content = createAstromech({ cwd: input.context.repoRoot }).codecovConfig(existing);
4733
4653
  await source.writeRepoFile("codecov.yml", content);
4734
- return packages.length > 0 ? `${packages.length} components` : "no components";
4654
+ const packageCount = readWorkspacePackages(input.context.repoRoot).length;
4655
+ return packageCount > 0 ? `${packageCount} components` : "no components";
4735
4656
  }));
4736
4657
  print(formatStep(steps[steps.length - 1]));
4737
4658
  }
@@ -6958,6 +6879,350 @@ function parseGitRemoteUrl(url) {
6958
6879
  if (sshMatch) return sshMatch[1];
6959
6880
  }
6960
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
6961
7226
  //#region src/update-notifier.ts
6962
7227
  const PACKAGE_NAME = "@theholocron/cli";
6963
7228
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
@@ -7083,7 +7348,11 @@ const resolveSyncToken = createFeatureResolver({
7083
7348
  * Error class names whose `.message` is a complete, actionable sentence —
7084
7349
  * the top-level catch prints it and suppresses the stack trace.
7085
7350
  */
7086
- const USER_FACING_ERRORS = /* @__PURE__ */ new Set(["WorkspaceContextError", "ConfigFileError"]);
7351
+ const USER_FACING_ERRORS = /* @__PURE__ */ new Set([
7352
+ "WorkspaceContextError",
7353
+ "ConfigFileError",
7354
+ "NonInteractiveError"
7355
+ ]);
7087
7356
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
7088
7357
  /** Whether to print the correlation id at command end (`--debug` / `--verbose`). */
7089
7358
  let printRunId = false;
@@ -7271,9 +7540,10 @@ try {
7271
7540
  },
7272
7541
  ...argv.name ? { name: argv.name } : {}
7273
7542
  }).status === "fail") process.exitCode = 1;
7274
- }).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", {
7275
7546
  type: "string",
7276
- demandOption: true,
7277
7547
  describe: "Secret name (e.g., NPM_TOKEN)"
7278
7548
  }).positional("value", {
7279
7549
  type: "string",
@@ -7292,6 +7562,7 @@ try {
7292
7562
  }), async (argv) => {
7293
7563
  const tokens = tokenContext(argv.token);
7294
7564
  if (!tokens) return;
7565
+ const [name] = await promptForPositionals(getEntry("secret set"), argv);
7295
7566
  const scopeArg = argv.scope;
7296
7567
  const scope = parseScope(scopeArg);
7297
7568
  const loaded = await loadConfig(argv.cwd);
@@ -7304,15 +7575,14 @@ try {
7304
7575
  ...tokens,
7305
7576
  org: resolveOrg(argv, loaded.resolved)
7306
7577
  },
7307
- name: argv.name,
7578
+ name,
7308
7579
  ...argv.value ? { value: argv.value } : {},
7309
7580
  ...argv.fromStdin ? { fromStdin: true } : {},
7310
7581
  ...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
7311
7582
  scope
7312
7583
  })).status === "fail") process.exitCode = 1;
7313
- }).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", {
7314
7585
  type: "string",
7315
- demandOption: true,
7316
7586
  describe: "Vault environment id to read (1P Environment id, etc.)"
7317
7587
  }).option("project-id", {
7318
7588
  type: "string",
@@ -7324,6 +7594,7 @@ try {
7324
7594
  }), async (argv) => {
7325
7595
  const tokens = tokenContext(argv.token);
7326
7596
  if (!tokens) return;
7597
+ const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
7327
7598
  const loaded = await loadConfig(argv.cwd);
7328
7599
  applyResolvedConfig(argv, loaded.resolved);
7329
7600
  if ((await runSecretsSync({
@@ -7334,13 +7605,12 @@ try {
7334
7605
  ...tokens,
7335
7606
  org: resolveOrg(argv, loaded.resolved)
7336
7607
  },
7337
- environmentId: argv.environmentId,
7608
+ environmentId,
7338
7609
  ...argv.projectId ? { projectId: argv.projectId } : {},
7339
7610
  targets: argv.target
7340
7611
  })).summary.fail > 0) process.exitCode = 1;
7341
- }).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", {
7342
7613
  type: "string",
7343
- demandOption: true,
7344
7614
  describe: "Git branch to deploy"
7345
7615
  }).option("project-id", {
7346
7616
  type: "string",
@@ -7353,6 +7623,7 @@ try {
7353
7623
  }), async (argv) => {
7354
7624
  const tokens = tokenContext(argv.token);
7355
7625
  if (!tokens) return;
7626
+ const [branch] = await promptForPositionals(getEntry("deploy"), argv);
7356
7627
  const loaded = await loadConfig(argv.cwd);
7357
7628
  applyResolvedConfig(argv, loaded.resolved);
7358
7629
  if ((await runDeploy({
@@ -7364,12 +7635,11 @@ try {
7364
7635
  org: resolveOrg(argv, loaded.resolved)
7365
7636
  },
7366
7637
  projectId: argv.projectId,
7367
- branch: argv.branch,
7638
+ branch,
7368
7639
  ...argv.target ? { target: argv.target } : {}
7369
7640
  })).status === "fail") process.exitCode = 1;
7370
- }).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", {
7371
7642
  type: "number",
7372
- demandOption: true,
7373
7643
  describe: "PR number to clean up"
7374
7644
  }).option("project", {
7375
7645
  type: "string",
@@ -7381,6 +7651,7 @@ try {
7381
7651
  }), async (argv) => {
7382
7652
  const tokens = tokenContext(argv.token);
7383
7653
  if (!tokens) return;
7654
+ const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
7384
7655
  const loaded = await loadConfig(argv.cwd);
7385
7656
  applyResolvedConfig(argv, loaded.resolved);
7386
7657
  if ((await runCleanupPreview({
@@ -7391,17 +7662,17 @@ try {
7391
7662
  ...tokens,
7392
7663
  org: resolveOrg(argv, loaded.resolved)
7393
7664
  },
7394
- prNumber: argv.pr,
7665
+ prNumber: Number(pr),
7395
7666
  project: argv.project,
7396
7667
  ...argv.repo ? { repo: argv.repo } : {}
7397
7668
  })).status === "fail") process.exitCode = 1;
7398
- }).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", {
7399
7670
  type: "string",
7400
- demandOption: true,
7401
7671
  describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
7402
7672
  }), async (argv) => {
7673
+ const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
7403
7674
  if ((await runNpmBumpVersions({
7404
- version: argv.newVersion,
7675
+ version: newVersion,
7405
7676
  cwd: argv.cwd,
7406
7677
  dryRun: argv.dryRun
7407
7678
  })).status === "fail") process.exitCode = 1;
@@ -7824,13 +8095,11 @@ try {
7824
8095
  }
7825
8096
  throw err;
7826
8097
  }
7827
- }).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", {
7828
8099
  type: "string",
7829
- demandOption: true,
7830
8100
  describe: "Package slug (kebab-case)"
7831
8101
  }).positional("vendor", {
7832
8102
  type: "string",
7833
- demandOption: true,
7834
8103
  describe: "Vendor display name (PascalCase)"
7835
8104
  }).option("capability", {
7836
8105
  type: "string",
@@ -7850,7 +8119,7 @@ try {
7850
8119
  describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
7851
8120
  }), async (argv) => {
7852
8121
  try {
7853
- const vendor = argv.vendor;
8122
+ const [slug, vendor] = await promptForPositionals(getEntry("plugin create"), argv);
7854
8123
  const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
7855
8124
  capability: argv.capability,
7856
8125
  vendorEnv: argv.vendorEnv,
@@ -7867,7 +8136,7 @@ try {
7867
8136
  inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
7868
8137
  });
7869
8138
  if (runPluginCreate({
7870
- slug: argv.slug,
8139
+ slug,
7871
8140
  vendorName: vendor,
7872
8141
  capability,
7873
8142
  vendorEnv,
@@ -7885,14 +8154,14 @@ try {
7885
8154
  }
7886
8155
  throw err;
7887
8156
  }
7888
- }).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", {
7889
8158
  type: "number",
7890
- demandOption: true,
7891
8159
  describe: "Target Node.js major version (e.g., 22)"
7892
8160
  }).option("from", {
7893
8161
  type: "number",
7894
8162
  describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
7895
8163
  }), async (argv) => {
8164
+ const [to] = await promptForPositionals(getEntry("upgrade node"), argv);
7896
8165
  let extra = [];
7897
8166
  try {
7898
8167
  const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
@@ -7900,7 +8169,7 @@ try {
7900
8169
  if (Array.isArray(extraRaw)) extra = extraRaw;
7901
8170
  } catch {}
7902
8171
  const report = await runUpgradeNode({
7903
- to: argv.to,
8172
+ to: Number(to),
7904
8173
  ...argv.from != null ? { from: argv.from } : {},
7905
8174
  cwd: argv.cwd,
7906
8175
  dryRun: argv.dryRun,
@@ -7924,31 +8193,31 @@ try {
7924
8193
  if (report.message) getLogger().error(`upgrade deps: ${report.message}`);
7925
8194
  process.exitCode = 1;
7926
8195
  }
7927
- }).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", {
7928
- type: "string",
7929
- demandOption: true
7930
- }).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);
7931
8200
  if ((await runAuthSet({
7932
- provider: argv.provider,
8201
+ provider,
7933
8202
  ...argv.value ? { positional: argv.value } : {},
7934
8203
  ...argv.org ? { org: argv.org } : {}
7935
8204
  })).status === "fail") process.exitCode = 1;
7936
- }).command("unset <provider>", "Remove a stored bootstrap token", (yy) => yy.positional("provider", {
7937
- type: "string",
7938
- demandOption: true
7939
- }), (argv) => {
7940
- runAuthUnset({ provider: argv.provider });
7941
- }).command("check <provider>", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", {
7942
- type: "string",
7943
- demandOption: true
7944
- }), 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);
7945
8210
  if ((await runAuthCheck({
7946
- provider: argv.provider,
8211
+ provider,
7947
8212
  ...argv.org ? { org: argv.org } : {}
7948
8213
  })).status === "fail") process.exitCode = 1;
7949
8214
  }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
7950
8215
  await runAuthList();
7951
- }).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:
7952
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) => {
7953
8222
  if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
7954
8223
  captureException(err);