@theholocron/kyber 5.0.0-alpha.340 → 5.0.0-alpha.342

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.
@@ -0,0 +1,200 @@
1
+ import { c as OptionPrompt, p as MenuItem, r as PositionalPrompt, s as OptionMode, u as ResolvedOptions } from "./index-DpXxO_wH.mjs";
2
+ /**
3
+ * One command in an interactive menu: how it is named, listed, prompted for and spawned. A CLI declares its
4
+ * whole surface as a list of these and hands it to {@link launchMenu}; kyber never knows which commands exist.
5
+ */
6
+ interface MenuEntry {
7
+ /** Full command name as typed at the CLI, e.g. `"auth set"`. */
8
+ name: string;
9
+ /** One-liner, matches the yargs registration's description. */
10
+ description: string;
11
+ /** Required positionals only, in order. Empty when the command needs none. */
12
+ positionals: PositionalPrompt[];
13
+ /**
14
+ * This command's yargs options, offered through the menu
15
+ * (`promptForOptions`) — in declaration order, which also governs
16
+ * `when` evaluation order . Global flags
17
+ * (`--token`/`--org`/`--cwd`/`--dry-run`) are never listed here; those
18
+ * are inherited from the parent invocation via `forwardedFlags`
19
+ * regardless of this command. Ignored when `modes` is set.
20
+ */
21
+ options?: OptionPrompt[];
22
+ /**
23
+ * Mutually exclusive workflows — a mode is
24
+ * picked first ("What would you like to do?"), and only that mode's
25
+ * own options are then offered, so nothing contingent on a different
26
+ * mode is ever shown. Use this only when `options` would otherwise
27
+ * need `when` to express a real either/or relationship between them
28
+ * (`package publish`'s steady-state / `--initial` / `--sync-trust`);
29
+ * plain `options` stays the simpler default for everything else.
30
+ */
31
+ modes?: OptionMode[];
32
+ /**
33
+ * This command's yargs options deliberately NOT offered through the
34
+ * menu, with the reason recorded in review rather than in code — kept
35
+ * so a future drift test can tell "considered and skipped" apart from
36
+ * "never considered."
37
+ */
38
+ skipOptions?: string[];
39
+ /**
40
+ * Whether this command's handler honours the global `--dry-run` flag.
41
+ * Drives `launchMenu`'s dry-run offer: a menu-launched command with this
42
+ * set gets asked "run a dry run first?" (skipped if `--dry-run` was
43
+ * already passed at the top level) and, after a successful dry run,
44
+ * "proceed with the real run?". Hand-maintained in parallel with each
45
+ * handler's own `dryRun: argv.dryRun` usage.
46
+ */
47
+ supportsDryRun?: boolean;
48
+ /**
49
+ * What the menu shows for this entry when it is a plain leaf, instead of its last word. For a variant of
50
+ * its parent that is run with a flag (`doctor all` is `doctor --all`), so it reads as a choice beside
51
+ * `doctor (run it)`.
52
+ */
53
+ menuLabel?: string;
54
+ /** The command opens a menu of its own (`probe`): shown with the same " ›" as a group, though nothing is nested under it. */
55
+ opensMenu?: boolean;
56
+ /**
57
+ * Override for {@link buildChildArgv}: the real CLI argv tokens to spawn,
58
+ * when they don't match a naive space-split of `name`. Every static
59
+ * entry's `name` already *is* its own argv (`"repo upgrade node"` →
60
+ * `["repo", "upgrade", "node"]`, three real yargs tokens) — this exists
61
+ * for dynamically-built entries (a per-repo task list, say), whose
62
+ * `name` uses a space per namespace segment purely to get free nesting
63
+ * out of {@link menuItems} (`"run sourceQuality staticAnalysis"`), but
64
+ * whose real invocation is one dotted positional
65
+ * (`["run", "sourceQuality.staticAnalysis"]`), not three separate ones.
66
+ */
67
+ spawnArgs?: string[];
68
+ }
69
+ /**
70
+ * `entry.spawnArgs` if set, else `entry.name` split into tokens — resolved
71
+ * positionals, then non-secret option flags, then forwarded flags, appended
72
+ * in that order. `resolved` is whatever `promptForOptions` returned —
73
+ * for a `modes`-based command, that's already the picked mode's own options
74
+ * (plus a synthetic entry for its `flag`, if any), so this renders either
75
+ * shape identically with no need to know which kind of command it came from.
76
+ */
77
+ export declare function buildChildArgv(entry: MenuEntry, positionals: string[], resolved: ResolvedOptions, parentArgv: Record<string, unknown>, dryRunOverride?: boolean): string[];
78
+ /** How {@link launchMenu} is bound to one particular CLI. */
79
+ interface LaunchMenuConfig {
80
+ /** The CLI's binary name, used in the default non-interactive hint (`Run \`<bin> --help\` …`). */
81
+ bin: string;
82
+ /**
83
+ * Upper-case prefix of the env vars the menu hands a spawned child: secret options travel as
84
+ * `<envPrefix>_<OPTION>` and a dry-run preview is flagged with `<envPrefix>_DRY_RUN_PREVIEW`.
85
+ */
86
+ envPrefix: string;
87
+ /** The question shown at the first level. Defaults to the picker's own default. */
88
+ pickMessage?: string;
89
+ /** The error shown when stdin is not a TTY. Defaults to ``Run `<bin> --help` to see available commands.`` */
90
+ nonInteractiveMessage?: string;
91
+ /** The group to open first, as words (`["repo"]`); `[]` (the default) opens the top level. */
92
+ startPath?: string[];
93
+ }
94
+ /**
95
+ * Walk the nested menu over `entries` (starting at `config.startPath`, `[]` for the top), prompt for the picked
96
+ * command's required positionals and options, spawn it as a fresh invocation of the running CLI, and set
97
+ * `process.exitCode` from the child — the caller (a `$0` handler) returns normally afterward so the parent's own
98
+ * telemetry flush / update-notifier tail still runs.
99
+ *
100
+ * Picking a group opens its submenu, which has a "← back" item; a command ends the walk. See {@link menuItems}
101
+ * for how a level is built. Throws {@link NonInteractiveError} when stdin is not a TTY.
102
+ *
103
+ * For a command that honours `--dry-run` ({@link MenuEntry.supportsDryRun}) and wasn't opted out via
104
+ * `--skip-dry-run` ({@link shouldPreviewDryRun}), this always previews first — a dry run doesn't mutate anything,
105
+ * so there's nothing to ask permission for. A preview that exits non-zero stops here; one that reports
106
+ * {@link DRY_RUN_NOOP_EXIT_CODE} (nothing would change) stops here too, skipping the follow-up question since the
107
+ * real run would have nothing to do either. Otherwise it asks "proceed with the real run?" before the real spawn.
108
+ *
109
+ * Secret options are never put in argv: they reach the child as `<envPrefix>_<OPTION>` env vars (see
110
+ * {@link secretEnv}), and a preview child additionally gets {@link dryRunPreviewEnvVar}.
111
+ *
112
+ * @param entries - The whole command surface, as data.
113
+ * @param parentArgv - The parsed argv of the invocation that opened the menu; its global flags are forwarded.
114
+ * @param config - Binds the menu to one CLI: its binary name, env prefix and optional messages.
115
+ */
116
+ export declare function launchMenu(entries: MenuEntry[], parentArgv: Record<string, unknown>, config: LaunchMenuConfig): Promise<void>;
117
+ /** The per-call knobs of a bound menu: everything in {@link LaunchMenuConfig} except what {@link createMenu} already fixed. */
118
+ export type BoundMenuOptions = Omit<LaunchMenuConfig, "bin" | "envPrefix">;
119
+ /** A menu engine bound to one CLI by {@link createMenu}. */
120
+ export interface Menu {
121
+ /** {@link launchMenu} with the CLI's `bin` and `envPrefix` already applied. */
122
+ launchMenu: (entries: MenuEntry[], parentArgv: Record<string, unknown>, options?: BoundMenuOptions) => Promise<void>;
123
+ /** The env var a dry-run preview child gets (`<envPrefix>_DRY_RUN_PREVIEW`); see {@link dryRunPreviewEnvVar}. */
124
+ dryRunPreviewEnvVar: string;
125
+ }
126
+ /**
127
+ * Bind the menu engine to one CLI once, so callers stop repeating `bin` and `envPrefix`: the returned
128
+ * `launchMenu(entries, parentArgv, options?)` is {@link launchMenu} with both applied, and `dryRunPreviewEnvVar` is the
129
+ * matching env var name a dry-run preview child receives.
130
+ *
131
+ * @param binding - The CLI's binary name and env prefix.
132
+ */
133
+ export declare function createMenu({ bin, envPrefix }: Pick<LaunchMenuConfig, "bin" | "envPrefix">): Menu;
134
+ /**
135
+ * Reserved exit code a dry-run-capable handler may use — only when the
136
+ * {@link dryRunPreviewEnvVar} is set — to report "ran clean, nothing would
137
+ * change." {@link launchMenu} treats it as success but skips the "proceed with
138
+ * the real run?" question, since there's nothing for the real run to do
139
+ * either.
140
+ */
141
+ export declare const DRY_RUN_NOOP_EXIT_CODE = 2;
142
+ /**
143
+ * The env var {@link launchMenu} sets on a dry-run preview child (`<ENV_PREFIX>_DRY_RUN_PREVIEW`), so its
144
+ * handler knows this specific `--dry-run` came from the menu's own preview-then-ask flow, not a direct
145
+ * `<bin> <cmd> --dry-run` invocation. A handler that supports it uses this — never bare `argv.dryRun` — to
146
+ * decide whether to report {@link DRY_RUN_NOOP_EXIT_CODE}: that exit code only has special meaning to
147
+ * `launchMenu` itself, so a direct/scripted `--dry-run` call must never produce it — doing so unconditionally
148
+ * would silently change the public exit-code contract for a successful dry run.
149
+ */
150
+ export declare function dryRunPreviewEnvVar(envPrefix: string): string;
151
+ /**
152
+ * `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
153
+ * menu-launched child should inherit from the parent invocation. Everything
154
+ * else (command-specific options) was never captured by the top-level `$0`
155
+ * handler in the first place, so there's nothing else to forward.
156
+ *
157
+ * `dryRunOverride`, when passed, wins over `argv.dryRun` — {@link launchMenu}'s
158
+ * dry-run offer needs to force `--dry-run` on for the preview spawn and
159
+ * force it back off for the real-run spawn that follows, regardless of
160
+ * what was on the original invocation.
161
+ */
162
+ export declare function forwardedFlags(argv: Record<string, unknown>, dryRunOverride?: boolean): string[];
163
+ /** Prefix of a menu item's `value` that opens a submenu rather than running a command. */
164
+ export declare const GROUP = "group:";
165
+ /** The "← back" item's value; only offered below the first level. */
166
+ export declare const BACK = "back";
167
+ /**
168
+ * One level of the menu, derived from the registered command names: the next word
169
+ * of every entry under `path` (`[]` is the top). A word is
170
+ *
171
+ * - a **command** when nothing is nested under it (`doctor`);
172
+ * - a **group** when commands are nested under it (`repo` → `repo setup`, `repo sync`, …),
173
+ * which can nest as deep as the names go (`repo upgrade` → `repo upgrade node`);
174
+ *
175
+ * Every row is a single word, never a "<command> <subcommand>" path: `config show` is
176
+ * `config ›` then `show`, even when a group holds just one command.
177
+ *
178
+ * A path that is itself runnable (`repo sync`, `deploy`) gets a leading "<word> (run it)" item.
179
+ * Registration order is kept.
180
+ */
181
+ export declare function menuItems(entries: MenuEntry[], path?: string[]): MenuItem[];
182
+ /**
183
+ * Secret option answers, keyed by the env var their handler reads instead
184
+ * of argv (`ps` and shell history expose argv, never a child's own env) —
185
+ * `<ENV_PREFIX>_<KEY_IN_SCREAMING_SNAKE_CASE>`, e.g. `otp` with prefix
186
+ * `MYCLI` → `MYCLI_OTP`. Merged into the spawn's extra env alongside
187
+ * `NO_UPDATE_NOTIFIER` and (for a preview spawn) the
188
+ * {@link dryRunPreviewEnvVar}.
189
+ */
190
+ export declare function secretEnv(resolved: ResolvedOptions, envPrefix: string): Record<string, string>;
191
+ /**
192
+ * Whether {@link launchMenu} should auto-preview a picked command before running
193
+ * it for real — only when the command's handler actually honours
194
+ * `--dry-run` ({@link MenuEntry.supportsDryRun}) and the top-level
195
+ * invocation didn't pass `--skip-dry-run` to opt out of the preview
196
+ * entirely (an escape hatch for "I already know what I want, skip
197
+ * straight to the real run").
198
+ */
199
+ export declare function shouldPreviewDryRun(entry: MenuEntry, parentArgv: Record<string, unknown>): boolean;
200
+ export type { LaunchMenuConfig, MenuEntry };
package/dist/menu.mjs ADDED
@@ -0,0 +1,322 @@
1
+ import { t as NonInteractiveError } from "./non-interactive-error-Mm_Mn79M.mjs";
2
+ import { pickMenuItem, promptForDeferredOptions, promptForOptions, promptForPositionals } from "./prompts.mjs";
3
+ import { confirm } from "@inquirer/prompts";
4
+ import { spawn } from "node:child_process";
5
+ // src/menu/forwarded-flags/forwarded-flags.ts
6
+ /**
7
+ * `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
8
+ * menu-launched child should inherit from the parent invocation. Everything
9
+ * else (command-specific options) was never captured by the top-level `$0`
10
+ * handler in the first place, so there's nothing else to forward.
11
+ *
12
+ * `dryRunOverride`, when passed, wins over `argv.dryRun` — {@link launchMenu}'s
13
+ * dry-run offer needs to force `--dry-run` on for the preview spawn and
14
+ * force it back off for the real-run spawn that follows, regardless of
15
+ * what was on the original invocation.
16
+ */
17
+ function forwardedFlags(argv, dryRunOverride) {
18
+ const out = [];
19
+ const tokens = argv.token;
20
+ for (const t of tokens ?? []) out.push("--token", t);
21
+ if (typeof argv.org === "string") out.push("--org", argv.org);
22
+ if (typeof argv.cwd === "string") out.push("--cwd", argv.cwd);
23
+ if (dryRunOverride ?? argv.dryRun === true) out.push("--dry-run");
24
+ return out;
25
+ }
26
+ // src/menu/is-secret-option/is-secret-option.ts
27
+ /**
28
+ * Whether `option`'s answer must be delivered via env, never argv — true for
29
+ * an explicit `secret: true`, and also for any `type: "password"` option
30
+ * even if `secret` was left unset. `promptForOptions` already treats these
31
+ * two as interchangeable triggers for masking the input itself;
32
+ * {@link buildChildArgv}/{@link secretEnv} need to agree, or a password-typed
33
+ * option that forgot to also set `secret: true` would mask the keystrokes
34
+ * on screen while still leaking the typed value into the child's argv.
35
+ */
36
+ function isSecretOption(option) {
37
+ return Boolean(option.secret) || option.type === "password";
38
+ }
39
+ // src/menu/option-argv/option-argv.ts
40
+ /**
41
+ * `option`'s own argv tokens for `value` (never called for a `secret`
42
+ * option — those are delivered via {@link secretEnv} instead, never argv).
43
+ *
44
+ * - boolean `true` → `--flag`; `false` → `--no-flag` when the yargs
45
+ * default is `true` (`negatable: true` — answering "no" must actively
46
+ * override it, not just omit the flag and silently keep the default),
47
+ * otherwise omitted (the yargs default is already `false`).
48
+ * - string/select → `--flag value`, omitted when empty.
49
+ * - `arity`-valued array → `--flag v1 v2 ...`.
50
+ */
51
+ function optionArgv(option, value) {
52
+ if (value === void 0) return [];
53
+ if (typeof value === "boolean") {
54
+ if (value) return [option.flag];
55
+ return option.negatable ? [`--no-${option.flag.replace(/^--/, "")}`] : [];
56
+ }
57
+ if (Array.isArray(value)) return value.length === 0 ? [] : [option.flag, ...value];
58
+ return value === "" ? [] : [option.flag, value];
59
+ }
60
+ // src/menu/build-child-argv/build-child-argv.ts
61
+ /**
62
+ * `entry.spawnArgs` if set, else `entry.name` split into tokens — resolved
63
+ * positionals, then non-secret option flags, then forwarded flags, appended
64
+ * in that order. `resolved` is whatever `promptForOptions` returned —
65
+ * for a `modes`-based command, that's already the picked mode's own options
66
+ * (plus a synthetic entry for its `flag`, if any), so this renders either
67
+ * shape identically with no need to know which kind of command it came from.
68
+ */
69
+ function buildChildArgv(entry, positionals, resolved, parentArgv, dryRunOverride) {
70
+ const optionArgs = resolved.options.flatMap((option) => isSecretOption(option) ? [] : optionArgv(option, resolved.answers[option.key]));
71
+ return [
72
+ ...entry.spawnArgs ?? entry.name.split(" "),
73
+ ...positionals,
74
+ ...optionArgs,
75
+ ...forwardedFlags(parentArgv, dryRunOverride)
76
+ ];
77
+ }
78
+ // src/menu/dry-run-preview-env-var/dry-run-preview-env-var.ts
79
+ /**
80
+ * The env var {@link launchMenu} sets on a dry-run preview child (`<ENV_PREFIX>_DRY_RUN_PREVIEW`), so its
81
+ * handler knows this specific `--dry-run` came from the menu's own preview-then-ask flow, not a direct
82
+ * `<bin> <cmd> --dry-run` invocation. A handler that supports it uses this — never bare `argv.dryRun` — to
83
+ * decide whether to report {@link DRY_RUN_NOOP_EXIT_CODE}: that exit code only has special meaning to
84
+ * `launchMenu` itself, so a direct/scripted `--dry-run` call must never produce it — doing so unconditionally
85
+ * would silently change the public exit-code contract for a successful dry run.
86
+ */
87
+ function dryRunPreviewEnvVar(envPrefix) {
88
+ return `${envPrefix}_DRY_RUN_PREVIEW`;
89
+ }
90
+ // src/menu/dry-run-noop-exit-code/dry-run-noop-exit-code.ts
91
+ /**
92
+ * Reserved exit code a dry-run-capable handler may use — only when the
93
+ * {@link dryRunPreviewEnvVar} is set — to report "ran clean, nothing would
94
+ * change." {@link launchMenu} treats it as success but skips the "proceed with
95
+ * the real run?" question, since there's nothing for the real run to do
96
+ * either.
97
+ */
98
+ const DRY_RUN_NOOP_EXIT_CODE = 2;
99
+ // src/menu/menu-items/menu-items.ts
100
+ /** Prefix of a menu item's `value` that opens a submenu rather than running a command. */
101
+ const GROUP = "group:";
102
+ /** The "← back" item's value; only offered below the first level. */
103
+ const BACK = "back";
104
+ /**
105
+ * One level of the menu, derived from the registered command names: the next word
106
+ * of every entry under `path` (`[]` is the top). A word is
107
+ *
108
+ * - a **command** when nothing is nested under it (`doctor`);
109
+ * - a **group** when commands are nested under it (`repo` → `repo setup`, `repo sync`, …),
110
+ * which can nest as deep as the names go (`repo upgrade` → `repo upgrade node`);
111
+ *
112
+ * Every row is a single word, never a "<command> <subcommand>" path: `config show` is
113
+ * `config ›` then `show`, even when a group holds just one command.
114
+ *
115
+ * A path that is itself runnable (`repo sync`, `deploy`) gets a leading "<word> (run it)" item.
116
+ * Registration order is kept.
117
+ */
118
+ function menuItems(entries, path = []) {
119
+ const prefix = path.join(" ");
120
+ const own = prefix ? entries.find((e) => e.name === prefix) : void 0;
121
+ const under = prefix ? entries.filter((e) => e.name.startsWith(`${prefix} `)) : entries;
122
+ const haystackOf = (list) => list.map((e) => `${e.name} ${e.description}`.toLowerCase()).join(" ");
123
+ const items = [];
124
+ if (own) items.push({
125
+ name: `${path[path.length - 1]} (run it)`,
126
+ value: own.name,
127
+ description: own.description,
128
+ haystack: haystackOf([own])
129
+ });
130
+ const words = [...new Set(under.map((e) => e.name.split(" ")[path.length]))];
131
+ for (const word of words) {
132
+ const full = [...path, word].join(" ");
133
+ const family = under.filter((e) => e.name === full || e.name.startsWith(`${full} `));
134
+ const self = family.find((e) => e.name === full);
135
+ const nested = family.filter((e) => e.name !== full);
136
+ if (nested.length === 0 && self) items.push({
137
+ name: self.menuLabel ?? (self.opensMenu ? `${word} ›` : word),
138
+ value: full,
139
+ description: self.description,
140
+ haystack: haystackOf([self])
141
+ });
142
+ else {
143
+ const children = [...new Set(nested.map((e) => e.name.split(" ")[path.length + 1]))];
144
+ items.push({
145
+ name: `${word} ›`,
146
+ value: `${GROUP}${full}`,
147
+ description: children.join(", "),
148
+ haystack: haystackOf(family)
149
+ });
150
+ }
151
+ }
152
+ return items;
153
+ }
154
+ // src/menu/resolve-for-real-spawn/resolve-for-real-spawn.ts
155
+ /**
156
+ * Collects `resolved.deferred`'s values and folds them into
157
+ * `resolved.answers` — call this once, right before building the real
158
+ * (non-dry-run) spawn's argv, never any earlier: a dry-run preview and
159
+ * a human confirming "proceed with the real run?" can both take a while,
160
+ * and a time-sensitive secret (an OTP) typed before either risks going
161
+ * stale before it's actually used. A no-op when nothing was deferred.
162
+ */
163
+ async function resolveForRealSpawn(resolved) {
164
+ if (!resolved.deferred || resolved.deferred.length === 0) return resolved;
165
+ const deferredAnswers = await promptForDeferredOptions(resolved.deferred);
166
+ return {
167
+ ...resolved,
168
+ answers: {
169
+ ...resolved.answers,
170
+ ...deferredAnswers
171
+ }
172
+ };
173
+ }
174
+ // src/menu/secret-env/secret-env.ts
175
+ /**
176
+ * Secret option answers, keyed by the env var their handler reads instead
177
+ * of argv (`ps` and shell history expose argv, never a child's own env) —
178
+ * `<ENV_PREFIX>_<KEY_IN_SCREAMING_SNAKE_CASE>`, e.g. `otp` with prefix
179
+ * `MYCLI` → `MYCLI_OTP`. Merged into the spawn's extra env alongside
180
+ * `NO_UPDATE_NOTIFIER` and (for a preview spawn) the
181
+ * {@link dryRunPreviewEnvVar}.
182
+ */
183
+ function secretEnv(resolved, envPrefix) {
184
+ const out = {};
185
+ for (const option of resolved.options) {
186
+ if (!isSecretOption(option)) continue;
187
+ const value = resolved.answers[option.key];
188
+ if (typeof value !== "string" || value === "") continue;
189
+ out[`${envPrefix}_${option.key.replace(/([A-Z])/g, "_$1").toUpperCase()}`] = value;
190
+ }
191
+ return out;
192
+ }
193
+ // src/menu/should-preview-dry-run/should-preview-dry-run.ts
194
+ /**
195
+ * Whether {@link launchMenu} should auto-preview a picked command before running
196
+ * it for real — only when the command's handler actually honours
197
+ * `--dry-run` ({@link MenuEntry.supportsDryRun}) and the top-level
198
+ * invocation didn't pass `--skip-dry-run` to opt out of the preview
199
+ * entirely (an escape hatch for "I already know what I want, skip
200
+ * straight to the real run").
201
+ */
202
+ function shouldPreviewDryRun(entry, parentArgv) {
203
+ return Boolean(entry.supportsDryRun) && parentArgv.skipDryRun !== true;
204
+ }
205
+ // src/menu/spawn-child/spawn-child.ts
206
+ /**
207
+ * Spawn the running CLI again with `args`, inheriting stdio; resolve with its exit code.
208
+ *
209
+ * `NO_UPDATE_NOTIFIER` is forced on in the child's env — the parent's own
210
+ * update-notifier tail still runs after this resolves (see {@link launchMenu}),
211
+ * so without this the freshly-spawned child independently runs its own
212
+ * update check too and the notice prints twice: once from the child right
213
+ * after its command output, once more from the parent once the child exits.
214
+ * `extraEnv` layers on top — used for the {@link dryRunPreviewEnvVar} on the
215
+ * dry-run preview spawn and for secret option answers.
216
+ */
217
+ function spawnChild(args, extraEnv) {
218
+ return new Promise((resolve) => {
219
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
220
+ stdio: "inherit",
221
+ env: {
222
+ ...process.env,
223
+ NO_UPDATE_NOTIFIER: "1",
224
+ ...extraEnv
225
+ }
226
+ });
227
+ child.on("exit", (code) => resolve(code ?? 0));
228
+ child.on("error", () => resolve(1));
229
+ });
230
+ }
231
+ // src/menu/launch-menu/launch-menu.ts
232
+ /**
233
+ * Walk the nested menu over `entries` (starting at `config.startPath`, `[]` for the top), prompt for the picked
234
+ * command's required positionals and options, spawn it as a fresh invocation of the running CLI, and set
235
+ * `process.exitCode` from the child — the caller (a `$0` handler) returns normally afterward so the parent's own
236
+ * telemetry flush / update-notifier tail still runs.
237
+ *
238
+ * Picking a group opens its submenu, which has a "← back" item; a command ends the walk. See {@link menuItems}
239
+ * for how a level is built. Throws {@link NonInteractiveError} when stdin is not a TTY.
240
+ *
241
+ * For a command that honours `--dry-run` ({@link MenuEntry.supportsDryRun}) and wasn't opted out via
242
+ * `--skip-dry-run` ({@link shouldPreviewDryRun}), this always previews first — a dry run doesn't mutate anything,
243
+ * so there's nothing to ask permission for. A preview that exits non-zero stops here; one that reports
244
+ * {@link DRY_RUN_NOOP_EXIT_CODE} (nothing would change) stops here too, skipping the follow-up question since the
245
+ * real run would have nothing to do either. Otherwise it asks "proceed with the real run?" before the real spawn.
246
+ *
247
+ * Secret options are never put in argv: they reach the child as `<envPrefix>_<OPTION>` env vars (see
248
+ * {@link secretEnv}), and a preview child additionally gets {@link dryRunPreviewEnvVar}.
249
+ *
250
+ * @param entries - The whole command surface, as data.
251
+ * @param parentArgv - The parsed argv of the invocation that opened the menu; its global flags are forwarded.
252
+ * @param config - Binds the menu to one CLI: its binary name, env prefix and optional messages.
253
+ */
254
+ async function launchMenu(entries, parentArgv, config) {
255
+ const { bin, envPrefix, pickMessage, nonInteractiveMessage = `Run \`${bin} --help\` to see available commands.`, startPath = [] } = config;
256
+ if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
257
+ const stack = [startPath];
258
+ let picked;
259
+ while (!picked) {
260
+ const path = stack[stack.length - 1];
261
+ const items = menuItems(entries, path);
262
+ if (stack.length > 1) items.push({
263
+ name: "← back",
264
+ value: BACK,
265
+ description: "Return to the previous menu",
266
+ haystack: "back"
267
+ });
268
+ const message = stack.length === 1 ? pickMessage : `${path.join(" ")} — choose a subcommand:`;
269
+ const value = await pickMenuItem(items, message);
270
+ if (value === "back") stack.pop();
271
+ else if (value.startsWith("group:")) stack.push(value.slice(6).split(" "));
272
+ else picked = entries.find((e) => e.name === value);
273
+ }
274
+ const positionals = await promptForPositionals(picked, {});
275
+ const resolvedOptions = await promptForOptions(picked, {});
276
+ const extraEnv = secretEnv(resolvedOptions, envPrefix);
277
+ if (shouldPreviewDryRun(picked, parentArgv)) {
278
+ const dryExit = await spawnChild(buildChildArgv(picked, positionals, resolvedOptions, parentArgv, true), {
279
+ [dryRunPreviewEnvVar(envPrefix)]: "1",
280
+ ...extraEnv
281
+ });
282
+ if (dryExit === 2) {
283
+ process.exitCode = 0;
284
+ return;
285
+ }
286
+ if (dryExit !== 0) {
287
+ process.exitCode = dryExit;
288
+ return;
289
+ }
290
+ if (!await confirm({
291
+ message: "Dry run complete — proceed with the real run?",
292
+ default: false
293
+ })) {
294
+ process.exitCode = 0;
295
+ return;
296
+ }
297
+ const forReal = await resolveForRealSpawn(resolvedOptions);
298
+ process.exitCode = await spawnChild(buildChildArgv(picked, positionals, forReal, parentArgv, false), secretEnv(forReal, envPrefix));
299
+ return;
300
+ }
301
+ const forReal = await resolveForRealSpawn(resolvedOptions);
302
+ process.exitCode = await spawnChild(buildChildArgv(picked, positionals, forReal, parentArgv), secretEnv(forReal, envPrefix));
303
+ }
304
+ // src/menu/create-menu/create-menu.ts
305
+ /**
306
+ * Bind the menu engine to one CLI once, so callers stop repeating `bin` and `envPrefix`: the returned
307
+ * `launchMenu(entries, parentArgv, options?)` is {@link launchMenu} with both applied, and `dryRunPreviewEnvVar` is the
308
+ * matching env var name a dry-run preview child receives.
309
+ *
310
+ * @param binding - The CLI's binary name and env prefix.
311
+ */
312
+ function createMenu({ bin, envPrefix }) {
313
+ return {
314
+ launchMenu: (entries, parentArgv, options = {}) => launchMenu(entries, parentArgv, {
315
+ bin,
316
+ envPrefix,
317
+ ...options
318
+ }),
319
+ dryRunPreviewEnvVar: dryRunPreviewEnvVar(envPrefix)
320
+ };
321
+ }
322
+ export { BACK, DRY_RUN_NOOP_EXIT_CODE, GROUP, buildChildArgv, createMenu, dryRunPreviewEnvVar, forwardedFlags, launchMenu, menuItems, secretEnv, shouldPreviewDryRun };
package/dist/notifier.mjs CHANGED
@@ -1,6 +1,6 @@
1
+ import chalk from "chalk";
1
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- import chalk from "chalk";
4
4
  // src/notifier/update-notifier/update-notifier.ts
5
5
  const CACHE_FILE = "update-check.json";
6
6
  const CACHE_TTL_MS = 864e5;