@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.
- package/README.md +23 -10
- package/dist/config.d.mts +81 -0
- package/dist/config.mjs +111 -0
- package/dist/index-DpXxO_wH.d.mts +214 -0
- package/dist/menu.d.mts +200 -0
- package/dist/menu.mjs +322 -0
- package/dist/notifier.mjs +1 -1
- package/dist/observability.d.mts +156 -0
- package/dist/observability.mjs +254 -0
- package/dist/prompts.d.mts +2 -214
- package/dist/prompts.mjs +1 -1
- package/dist/secrets.d.mts +81 -1
- package/dist/secrets.mjs +74 -2
- package/dist/testing.mjs +2 -2
- package/dist/ui.d.mts +57 -3
- package/dist/ui.mjs +80 -4
- package/package.json +33 -5
package/dist/menu.d.mts
ADDED
|
@@ -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;
|