@theholocron/kyber 5.0.0-alpha.339 → 5.0.0-alpha.341

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@theholocron/kyber`
2
2
 
3
- The reusable parts of a command-line tool: terminal output, prompts, the update notifier, safe handling of tokens, a
3
+ The reusable parts of a command-line tool: terminal output, prompts, the interactive menu, the update notifier, safe handling of tokens, a
4
4
  test harness and the global flags. They were written for the Holocron CLI and are moved here one subpath at a time, so
5
5
  the next CLI does not copy them (spec `tech-kyber`, #1727).
6
6
 
@@ -10,14 +10,16 @@ Published, so a second CLI can import it. `@theholocron/cli` is its first consum
10
10
 
11
11
  ## Subpaths
12
12
 
13
- | Subpath | What it gives a CLI |
14
- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
15
- | `/ui` | Status glyphs and colours, padded status rows, a depth-limited JSON view, a spinner that only shows on a terminal, wait-for-Enter, open-in-file-browser, and the error for "a prompt was needed but there is no terminal". |
16
- | `/notifier` | A cached, best-effort "a newer version exists" notice for an npm package: `checkForUpdates({ name, currentVersion, cacheDir })` returns a function that prints a boxed notice to stderr, or `null`. Silent in CI and with `NO_UPDATE_NOTIFIER`. |
17
- | `/secrets` | Handling tokens safely, with no terminal UI: owner-only (`0600`) files, shredding a file that held tokens, "does git ignore this path", dotenv lines that survive quoting, reading a `.env` at the repo root, the clipboard, the OS keyring (`createKeyring({ service })`, best-effort, `@napi-rs/keyring` optional), an in-process store for credentials a provider returned, and `--token` / `--token vendor=value` parsing. |
18
- | `/prompts` | A searchable menu (`pickMenuItem`), and prompting for the positionals and options a command left out (`promptForPositionals`, `promptForOptions`): required options always asked, the rest behind one checkbox, mutually exclusive `modes`, secret options masked, and a value that is asked last (`askLast`). They take small structural types (`PositionalCommand`, `OptionsCommand`), not the CLI's command registry. |
19
- | `/testing` | A test harness for a CLI: `createCliRunner` runs a built binary in a fresh directory with an environment built from nothing, `runCommand` runs one yargs command module through a real parse, `ensureBuilt` is a Vitest global setup that builds only when `dist` is missing or stale, and `hermeticCreateRequire` resolves modules only from one directory. |
20
- | `/flags` | The options every command of a tool takes: `addGlobalOptions(parser, { describe?, cwd? })` declares `--dry-run`, a repeatable `--token` and `--cwd` once on the root; `tokenContext(values, report)` parses the `--token` values and reports a bad one without a stack trace; `groupMenu(parent, open)` is the hidden default command that makes a bare group (`tool repo`) do something useful; `InferArgs` types a command's handler arguments from its builder. |
13
+ | Subpath | What it gives a CLI |
14
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `/ui` | Status glyphs and colours, padded status rows, a depth-limited JSON view, a spinner that only shows on a terminal, wait-for-Enter, open-in-file-browser, the error for "a prompt was needed but there is no terminal", and the cleaning of error text before it reaches a terminal (`describeError`, `commandFailureMessage`, `logReason`, `sanitizeDetail`, `stripControl`). |
16
+ | `/notifier` | A cached, best-effort "a newer version exists" notice for an npm package: `checkForUpdates({ name, currentVersion, cacheDir })` returns a function that prints a boxed notice to stderr, or `null`. Silent in CI and with `NO_UPDATE_NOTIFIER`. |
17
+ | `/secrets` | Handling tokens safely, with no terminal UI: owner-only (`0600`) files, shredding a file that held tokens, "does git ignore this path", dotenv lines that survive quoting, reading a `.env` at the repo root, the clipboard, the OS keyring (`createKeyring({ service })`, best-effort, `@napi-rs/keyring` optional), an in-process store for credentials a provider returned, `--token` / `--token vendor=value` parsing, and resolving a token from the flag, the environment, that store and the keyring (`createResolveToken`, `createFeatureResolver`, `AuthError`). |
18
+ | `/prompts` | A searchable menu (`pickMenuItem`), and prompting for the positionals and options a command left out (`promptForPositionals`, `promptForOptions`): required options always asked, the rest behind one checkbox, mutually exclusive `modes`, secret options masked, and a value that is asked last (`askLast`). They take small structural types (`PositionalCommand`, `OptionsCommand`), not the CLI's command registry. |
19
+ | `/menu` | The interactive menu of a CLI, driven by data: `launchMenu(entries, argv, { bin, envPrefix })` shows a searchable, nested picker over a list of `MenuEntry`, asks for the positionals and options the command needs, then spawns the CLI again with the answers. A command that supports `--dry-run` is previewed first and then confirmed (`--skip-dry-run` opts out); a preview that reports `DRY_RUN_NOOP_EXIT_CODE` skips the question. Secret options never reach argv: they travel as `<envPrefix>_<OPTION>` env vars, and a preview child gets `dryRunPreviewEnvVar(envPrefix)`. Also `menuItems`, `buildChildArgv`, `forwardedFlags`, `secretEnv`, `shouldPreviewDryRun`. |
20
+ | `/testing` | A test harness for a CLI: `createCliRunner` runs a built binary in a fresh directory with an environment built from nothing, `runCommand` runs one yargs command module through a real parse, `ensureBuilt` is a Vitest global setup that builds only when `dist` is missing or stale, and `hermeticCreateRequire` resolves modules only from one directory. |
21
+ | `/config` | A `<name>.config.{ts,js,mjs,cjs,json}` for a new CLI: `createConfigSystem({ name, derive?, resolve, merge })` returns `defineConfig`, `loadConfig` (walks up to `.git`) and `compose()` (dedupe by `id`, all unmet `requires` in one `ConfigError`, per-key merge). Strategies: `lastWins`, `shallowMerge`, `union`, `mergeByKey`. `@theholocron/datapad` is an optional peer. |
22
+ | `/flags` | The options every command of a tool takes: `addGlobalOptions(parser, { describe?, cwd? })` declares `--dry-run`, a repeatable `--token` and `--cwd` once on the root; `tokenContext(values, report)` parses the `--token` values and reports a bad one without a stack trace; `groupMenu(parent, open)` is the hidden default command that makes a bare group (`tool repo`) do something useful; `InferArgs` types a command's handler arguments from its builder. |
21
23
 
22
24
  ## Usage
23
25
 
@@ -28,6 +30,16 @@ console.log(style.success("built"));
28
30
  console.log(statusRow("ok", ["lint", "no problems"]));
29
31
  ```
30
32
 
33
+ ```ts
34
+ import { launchMenu, type MenuEntry } from "@theholocron/kyber/menu";
35
+
36
+ const entries: MenuEntry[] = [
37
+ { name: "doctor", description: "Check the setup", positionals: [], supportsDryRun: true },
38
+ { name: "repo sync", description: "Sync the repo", positionals: [] },
39
+ ];
40
+ await launchMenu(entries, argv, { bin: "tool", envPrefix: "TOOL" }); // a secret `otp` option reaches the child as TOOL_OTP
41
+ ```
42
+
31
43
  ```ts
32
44
  import { addGlobalOptions, tokenContext } from "@theholocron/kyber/flags";
33
45
  import { createKeyring } from "@theholocron/kyber/secrets";
@@ -37,7 +49,7 @@ const tokens = tokenContext(argv.token, console.error); // null (and exit code 1
37
49
  const keyring = createKeyring({ service: "com.example.tool" }); // best-effort: never throws
38
50
  ```
39
51
 
40
- Each subpath's heavy libraries (`chalk`, `ora`, `@inquirer/prompts`, `yargs`, `dotenv`, `clipboardy`, `@napi-rs/keyring`) are
52
+ Each subpath's heavy libraries (`chalk`, `ora`, `@inquirer/prompts`, `yargs`, `dotenv`, `clipboardy`, `@napi-rs/keyring`, `@theholocron/datapad`) are
41
53
  optional peer dependencies: a CLI installs the ones for the subpaths it imports.
42
54
 
43
55
  `createProgress(print, enabled?, listen?)` takes an optional `listen` hook that can change the spinner's text while a step
@@ -0,0 +1,81 @@
1
+ import { ConfigFileError, Loaded } from "@theholocron/datapad";
2
+ /** How the values several fragments give one key combine. */
3
+ interface Strategy<V> {
4
+ /** The value to start from. A strategy without one leaves the key out of the result until a fragment sets it. */
5
+ empty?: V;
6
+ /** Combine what has been merged so far with the next fragment's value. */
7
+ merge: (prev: V, next: V) => V;
8
+ }
9
+ /** A composable piece of config: an id to dedupe by, and the ids it needs alongside it. */
10
+ interface ConfigFragment {
11
+ /** Unique identifier; a later fragment with the same id replaces an earlier one. */
12
+ id: string;
13
+ /** Ids that must be present in the same compose call. */
14
+ requires?: string[];
15
+ }
16
+ /** How each key of the composed result is merged from the fragments' values for that key. */
17
+ type MergeStrategies<Composed> = { [K in keyof Composed]-?: Strategy<NonNullable<Composed[K]>>; };
18
+ /**
19
+ * Thrown when a config is invalid, or when `compose()` is given fragments whose `requires` are not met. A caller that
20
+ * may load a second copy of this package matches it by `name`, not `instanceof`.
21
+ */
22
+ export declare class ConfigError extends Error {
23
+ /** The error name, `ConfigError`. */
24
+ name: string;
25
+ }
26
+ /** What {@link createConfigSystem} needs to build a CLI's config system. */
27
+ interface ConfigSystemOptions<Raw, Resolved, Composed> {
28
+ /** The CLI's name; its config file is `<name>.config.{ts,js,mjs,cjs,json}`. */
29
+ name: string;
30
+ /** Fill in defaults on the loaded config (from `package.json`, git, other files) before it is resolved. */
31
+ derive?: (found: Loaded<Raw>) => Raw | Promise<Raw>;
32
+ /** Check the config and turn it into the shape the CLI works with; throw `ConfigError` when it is invalid. */
33
+ resolve: (raw: Raw) => Resolved;
34
+ /** How `compose()` merges each key of the fragments into one `Composed` object. */
35
+ merge: MergeStrategies<Composed>;
36
+ }
37
+ /** A loaded and resolved config. */
38
+ interface LoadedConfig<Resolved> {
39
+ /** The config after `resolve`. */
40
+ resolved: Resolved;
41
+ /** Absolute path to the file the config was read from. */
42
+ filepath: string;
43
+ }
44
+ /** The functions {@link createConfigSystem} returns. */
45
+ interface ConfigSystem<Raw, Resolved, Fragment extends ConfigFragment, Composed> {
46
+ /** Returns the config unchanged; it exists to type and autocomplete a config file. */
47
+ defineConfig: (config: Raw) => Raw;
48
+ /**
49
+ * Find, load, derive and resolve the config, starting at `cwd` and walking up to the nearest `.git`. Throws
50
+ * `ConfigFileError` when none is found or a file cannot be loaded, or whatever `resolve` throws.
51
+ */
52
+ loadConfig: (cwd: string) => Promise<LoadedConfig<Resolved>>;
53
+ /** Merge fragments (or arrays of them) into one object that spreads into `defineConfig()`. */
54
+ compose: (...fragments: (Fragment | Fragment[])[]) => Composed;
55
+ }
56
+ /**
57
+ * Build the config system of a new CLI: a `<name>.config.{ts,js,mjs,cjs,json}` file, a typing helper for it, a loader
58
+ * that walks up to the repo root, and a `compose()` that merges reusable fragments.
59
+ *
60
+ * @example
61
+ * const { defineConfig, loadConfig, compose } = createConfigSystem<Raw, Resolved, Fragment, Composed>({
62
+ * name: "mycli",
63
+ * resolve: (raw) => validate(raw),
64
+ * merge: { plugins: union<string>(), settings: shallowMerge(), title: lastWins() },
65
+ * });
66
+ */
67
+ export declare function createConfigSystem<Raw, Resolved, Fragment extends ConfigFragment, Composed>(options: ConfigSystemOptions<Raw, Resolved, Composed>): ConfigSystem<Raw, Resolved, Fragment, Composed>;
68
+ /** Merge strategy where the last fragment to set the key wins. The key is absent until a fragment sets it. */
69
+ export declare function lastWins<V>(): Strategy<V>;
70
+ /**
71
+ * Merge strategy for lists of entries that share an identity. An entry whose key was already seen replaces the earlier
72
+ * one in place; new keys append.
73
+ *
74
+ * @param keyOf - The key an entry merges under.
75
+ */
76
+ export declare function mergeByKey<T>(keyOf: (entry: T) => string): Strategy<T[]>;
77
+ /** Merge strategy that spreads each fragment's object over the last, so a later fragment overrides per top-level key. */
78
+ export declare function shallowMerge<V extends object>(): Strategy<V>;
79
+ /** Merge strategy that adds lists up, dropping repeats and keeping first-seen order. */
80
+ export declare function union<T>(): Strategy<T[]>;
81
+ export { ConfigFileError, type ConfigFragment, type ConfigSystem, type ConfigSystemOptions, type LoadedConfig, type MergeStrategies, type Strategy };
@@ -0,0 +1,111 @@
1
+ import { ConfigFileError, ConfigFileError as ConfigFileError$1, loadConfigFile } from "@theholocron/datapad";
2
+ // src/config/config-error/config-error.ts
3
+ /**
4
+ * Thrown when a config is invalid, or when `compose()` is given fragments whose `requires` are not met. A caller that
5
+ * may load a second copy of this package matches it by `name`, not `instanceof`.
6
+ */
7
+ var ConfigError = class extends Error {
8
+ /** The error name, `ConfigError`. */
9
+ name = "ConfigError";
10
+ };
11
+ // src/config/compose-fragments/compose-fragments.ts
12
+ /**
13
+ * Combine config fragments into one object. Fragments dedupe by `id` (the last one with an id wins, in the position of
14
+ * the first), every unmet `requires` is reported in ONE {@link ConfigError}, then each key merges with its strategy.
15
+ *
16
+ * @param strategies - A merge strategy per key of the composed result.
17
+ * @param args - Fragments, or arrays of them.
18
+ */
19
+ function composeFragments(strategies, args) {
20
+ const byId = /* @__PURE__ */ new Map();
21
+ for (const fragment of args.flat()) byId.set(fragment.id, fragment);
22
+ const fragments = [...byId.values()];
23
+ const errors = [];
24
+ for (const fragment of fragments) for (const req of fragment.requires ?? []) if (!byId.has(req)) errors.push(`"${fragment.id}" requires "${req}"`);
25
+ if (errors.length > 0) throw new ConfigError(`compose(): unmet dependencies — ${errors.join(", ")}`);
26
+ const result = {};
27
+ for (const [key, strategy] of Object.entries(strategies)) {
28
+ let acc = strategy.empty;
29
+ for (const fragment of fragments) {
30
+ const value = fragment[key];
31
+ if (value === void 0) continue;
32
+ acc = acc === void 0 ? value : strategy.merge(acc, value);
33
+ }
34
+ if (acc !== void 0) result[key] = acc;
35
+ }
36
+ return result;
37
+ }
38
+ // src/config/create-config-system/create-config-system.ts
39
+ /**
40
+ * Build the config system of a new CLI: a `<name>.config.{ts,js,mjs,cjs,json}` file, a typing helper for it, a loader
41
+ * that walks up to the repo root, and a `compose()` that merges reusable fragments.
42
+ *
43
+ * @example
44
+ * const { defineConfig, loadConfig, compose } = createConfigSystem<Raw, Resolved, Fragment, Composed>({
45
+ * name: "mycli",
46
+ * resolve: (raw) => validate(raw),
47
+ * merge: { plugins: union<string>(), settings: shallowMerge(), title: lastWins() },
48
+ * });
49
+ */
50
+ function createConfigSystem(options) {
51
+ const { name, derive, resolve, merge } = options;
52
+ return {
53
+ defineConfig: (config) => config,
54
+ async loadConfig(cwd) {
55
+ const found = await loadConfigFile({
56
+ cwd,
57
+ name,
58
+ walkUp: true
59
+ });
60
+ if (!found) throw new ConfigFileError$1(`no ${name}.config.{ts,js,mjs,cjs,json} found in ${cwd}. Create one — see the README for the schema.`);
61
+ const raw = derive ? await derive(found) : found.config;
62
+ return {
63
+ resolved: resolve(raw),
64
+ filepath: found.filepath
65
+ };
66
+ },
67
+ compose: (...fragments) => composeFragments(merge, fragments)
68
+ };
69
+ }
70
+ // src/config/last-wins/last-wins.ts
71
+ /** Merge strategy where the last fragment to set the key wins. The key is absent until a fragment sets it. */
72
+ function lastWins() {
73
+ return { merge: (_prev, next) => next };
74
+ }
75
+ // src/config/merge-by-key/merge-by-key.ts
76
+ /**
77
+ * Merge strategy for lists of entries that share an identity. An entry whose key was already seen replaces the earlier
78
+ * one in place; new keys append.
79
+ *
80
+ * @param keyOf - The key an entry merges under.
81
+ */
82
+ function mergeByKey(keyOf) {
83
+ return {
84
+ empty: [],
85
+ merge: (prev, next) => {
86
+ const byKey = new Map(prev.map((e) => [keyOf(e), e]));
87
+ for (const e of next) byKey.set(keyOf(e), e);
88
+ return [...byKey.values()];
89
+ }
90
+ };
91
+ }
92
+ // src/config/shallow-merge/shallow-merge.ts
93
+ /** Merge strategy that spreads each fragment's object over the last, so a later fragment overrides per top-level key. */
94
+ function shallowMerge() {
95
+ return {
96
+ empty: {},
97
+ merge: (prev, next) => ({
98
+ ...prev,
99
+ ...next
100
+ })
101
+ };
102
+ }
103
+ // src/config/union/union.ts
104
+ /** Merge strategy that adds lists up, dropping repeats and keeping first-seen order. */
105
+ function union() {
106
+ return {
107
+ empty: [],
108
+ merge: (prev, next) => [.../* @__PURE__ */ new Set([...prev, ...next])]
109
+ };
110
+ }
111
+ export { ConfigError, ConfigFileError, createConfigSystem, lastWins, mergeByKey, shallowMerge, union };
@@ -0,0 +1,214 @@
1
+ /** A row in one level of a nested menu: a command to run, a group to open, or "back". */
2
+ interface MenuItem {
3
+ /** The label shown in the picker. */
4
+ name: string;
5
+ /** What the picker resolves to: a command name, `group:<path>` to open a submenu, or `back`. */
6
+ value: string;
7
+ /** One line of help shown beside the label. */
8
+ description: string;
9
+ /** Lower-cased text the search box matches against (a group includes everything under it). */
10
+ haystack: string;
11
+ }
12
+ /**
13
+ * Searchable autocomplete over one menu level's `items`. `select()` would work too
14
+ * at these list sizes, but `search()` degrades gracefully as the surface grows and
15
+ * costs nothing when it doesn't. Resolves to the picked item's `value`.
16
+ */
17
+ declare function pickMenuItem(items: MenuItem[], message?: string): Promise<string>;
18
+ /**
19
+ * `@inquirer/search`'s `source` callback, factored out as a plain function —
20
+ * unit-testable directly instead of only through a mocked `search()` call.
21
+ * Empty/undefined `term` (nothing typed yet) returns every item.
22
+ */
23
+ declare function searchChoices(items: MenuItem[], term: string | undefined): Array<{
24
+ name: string;
25
+ value: string;
26
+ description: string;
27
+ short: string;
28
+ }>;
29
+ /** A resolved option answer — the shapes `buildChildArgv`/`secretEnv` turn into argv tokens or env entries. */
30
+ type OptionAnswer = string | boolean | string[];
31
+ /** One option to offer: how to ask for it and how the answer is turned into a flag. */
32
+ interface OptionPrompt {
33
+ /** The yargs option's camelCased argv key, e.g. `"otp"`. */
34
+ key: string;
35
+ /** The literal flag as typed at the CLI, e.g. `"--otp"`. */
36
+ flag: string;
37
+ /** The kind of prompt: a yes/no, free text, a pick from `choices`, or masked text. */
38
+ type: "confirm" | "input" | "select" | "password";
39
+ /** The question shown to the user. */
40
+ message: string;
41
+ /**
42
+ * Required when `type === "select"`. A plain list for a fixed set, or a
43
+ * thunk for a set resolved at prompt time — same shape as
44
+ * `PositionalPrompt.choices`.
45
+ */
46
+ choices?: string[] | (() => string[]);
47
+ /** The answer proposed when the user just presses Enter. */
48
+ default?: string | boolean;
49
+ /**
50
+ * Collected with a masked `password()` prompt and delivered to the
51
+ * child through an environment variable instead of argv (`ps` and
52
+ * shell history expose argv) — never written to the dry-run preview
53
+ * echo, telemetry, or logs. Always paired with `type: "password"`.
54
+ */
55
+ secret?: boolean;
56
+ /**
57
+ * Always included, like a required positional — never behind the
58
+ * multi-select, and never subject to `when`. Combines with `askLast`
59
+ * cleanly: `required` only decides whether this sits behind the
60
+ * checkbox, not when its value gets collected.
61
+ */
62
+ required?: boolean;
63
+ /** Boolean whose yargs default is `true` — answering "no" emits `--no-<flag>` instead of omitting the flag. */
64
+ negatable?: boolean;
65
+ /** Array option taking exactly `arity` values (e.g. `--sync-trust <old> <new>`) — prompted once per value. */
66
+ arity?: number;
67
+ /**
68
+ * Defer collecting this option's value until right before the real
69
+ * (non-dry-run) spawn, instead of upfront with everything else
70
+ * (holocron#1052 follow-up) — for a time-sensitive secret (an OTP code)
71
+ * that can go stale if typed too early: a dry-run preview and a human
72
+ * confirming "proceed with the real run?" can both take a while, and
73
+ * neither one actually needs the value anyway. Works whether this
74
+ * option also has `required: true` (always included, no checkbox pick
75
+ * needed) or not (offered through the checkbox as usual) — `askLast`
76
+ * only ever changes *when* the value is collected, never whether it's
77
+ * included. See `promptForDeferredOptions`.
78
+ */
79
+ askLast?: boolean;
80
+ /**
81
+ * Evaluated against answers collected so far, in declaration order —
82
+ * `false` skips prompting (and so emitting) this option entirely, as if
83
+ * it had never been selected. Not applied to `required` options. For a
84
+ * command whose options have real either/or relationships, prefer
85
+ * {@link OptionMode} instead — nothing contingent is ever shown in the
86
+ * first place, rather than silently dropped after being picked.
87
+ */
88
+ when?: (answers: Readonly<Record<string, OptionAnswer>>) => boolean;
89
+ }
90
+ /**
91
+ * One of a command's mutually exclusive workflows (holocron#1052 follow-up)
92
+ * — `package publish`'s steady-state publish, `--initial` bootstrap, and
93
+ * `--sync-trust` maintenance mode are three different things to do, not
94
+ * three independent flags. The mode picked first decides which options are
95
+ * even offered, so nothing contingent on another mode is ever shown.
96
+ */
97
+ interface OptionMode {
98
+ /** Unique within the command's `modes` — identifies the pick, never shown. */
99
+ key: string;
100
+ /** Shown as this mode's choice label in the "what would you like to do?" prompt — keep it short; put caveats in `description` instead. */
101
+ message: string;
102
+ /**
103
+ * Longer explanation shown as helper text under this choice only while
104
+ * it's highlighted (`select()`'s own per-choice `description`) — the
105
+ * place for a caveat ("use only for a package's very first publish")
106
+ * that would make `message` too long to scan at a glance.
107
+ */
108
+ description?: string;
109
+ /**
110
+ * Emitted unconditionally once this mode is picked, before any of its
111
+ * own options — e.g. `"--initial"` for a bootstrap-publish mode. Omit
112
+ * for a mode that implies no flag of its own (steady-state here: no
113
+ * flag means "neither of the others").
114
+ */
115
+ flag?: string;
116
+ /** This mode's own options, prompted exactly like a flat command's `options` — required ones always asked, the rest behind one checkbox. */
117
+ options?: OptionPrompt[];
118
+ }
119
+ /**
120
+ * `promptForOptions`' result: every {@link OptionPrompt} relevant to
121
+ * rendering, in order, paired with the answers collected for them.
122
+ * `options` is the flat `OptionsCommand.options` list directly for a
123
+ * flat command, or the picked mode's own `options` (plus a synthetic
124
+ * entry for its `flag`, when it has one) for a `modes`-based command —
125
+ * `buildChildArgv`/`secretEnv` render either shape identically, with no
126
+ * need to know which kind of command produced it.
127
+ */
128
+ interface ResolvedOptions {
129
+ /** The options that apply, in the order they were asked. */
130
+ options: OptionPrompt[];
131
+ /** What was answered, by each option's `key`. */
132
+ answers: Record<string, OptionAnswer>;
133
+ /** Picked `askLast: true` options with no value yet — resolve via `promptForDeferredOptions` right before the real spawn. Undefined/empty is the common case: nothing deferred. */
134
+ deferred?: OptionPrompt[];
135
+ }
136
+ /** The part of a command that {@link promptForOptions} needs: its name and the options (or modes) to offer. */
137
+ interface OptionsCommand {
138
+ /** Full command name as typed at the CLI, e.g. `"package publish"`. */
139
+ name: string;
140
+ /** Declared options, in order. Undefined/empty means nothing to offer — no prompt at all. Ignored when `modes` is set. */
141
+ options?: OptionPrompt[];
142
+ /**
143
+ * Mutually exclusive workflows — when set, a mode is picked first and
144
+ * `options` above is ignored entirely; only the picked mode's own
145
+ * options are then offered. Use this only for a command whose options
146
+ * have a real either/or relationship; plain `options` covers
147
+ * everything else and stays the simpler default.
148
+ */
149
+ modes?: OptionMode[];
150
+ }
151
+ /**
152
+ * Resolves `entry`'s options into a {@link ResolvedOptions} ready for
153
+ * `buildChildArgv`/`secretEnv` — delegating to {@link promptForModes} when
154
+ * `entry.modes` is set, or treating `entry.options` as a flat list
155
+ * otherwise. See `promptForFlatOptions` for the flat-list prompting
156
+ * algorithm itself. `result.deferred` still needs
157
+ * {@link promptForDeferredOptions} before the real spawn.
158
+ */
159
+ declare function promptForOptions(entry: OptionsCommand, argv: Record<string, unknown>): Promise<ResolvedOptions>;
160
+ /**
161
+ * Collects a value for each of `deferred` — the `askLast: true` options
162
+ * `promptForOptions` deliberately left unresolved. Call this as the very
163
+ * last thing before the real (non-dry-run) spawn, never before: a dry-run
164
+ * preview never reaches the real npm call these options exist for (an OTP
165
+ * is never checked during `--dry-run`), and a time-sensitive code typed
166
+ * any earlier — before a dry-run preview runs, before "proceed with the
167
+ * real run?" waits on a human — risks going stale before it's actually
168
+ * used. A no-op (no prompt at all) when `deferred` is empty.
169
+ */
170
+ declare function promptForDeferredOptions(deferred: OptionPrompt[]): Promise<Record<string, OptionAnswer>>;
171
+ /** One positional argument to ask for when it was not given on the command line. */
172
+ interface PositionalPrompt {
173
+ /** The yargs positional's camelCased argv key (e.g. `"newVersion"`). */
174
+ key: string;
175
+ /**
176
+ * The literal token in the command string, when it differs from `key`
177
+ * (yargs positionals are kebab-case there — `<new-version>` — but
178
+ * camelCased in argv). Shown in the non-interactive fallback message.
179
+ * Defaults to `key`.
180
+ */
181
+ cliArg?: string;
182
+ /** Label shown to the user. */
183
+ message: string;
184
+ /** Free text, or a pick from `choices`. */
185
+ type: "input" | "select";
186
+ /**
187
+ * Required when `type === "select"`. A plain list for a fixed set, or a
188
+ * thunk for a set resolved at prompt time (e.g. `auth unset`'s provider
189
+ * choices are whatever currently has a stored token — there's no fixed
190
+ * provider list, providers are open-ended plugin packages).
191
+ */
192
+ choices?: string[] | (() => string[]);
193
+ /** Optional `input()` validation — e.g. numeric positionals. */
194
+ validate?: (value: string) => boolean | string;
195
+ }
196
+ /** The part of a command that {@link promptForPositionals} needs: its name and the positionals to ask for. */
197
+ interface PositionalCommand {
198
+ /** Full command name as typed at the CLI, e.g. `"auth set"`. */
199
+ name: string;
200
+ /** Required positionals only, in order. */
201
+ positionals: PositionalPrompt[];
202
+ }
203
+ /**
204
+ * For each of `entry.positionals` not already present in `argv`, prompts for
205
+ * it (`input()` or `select()`, per the positional's `type`) and returns the
206
+ * resolved values in positional order — ready to append to a spawned child's
207
+ * argv, or to use directly in the current handler (Layer 3).
208
+ *
209
+ * Throws {@link NonInteractiveError} the first time it would need to prompt
210
+ * on a non-TTY stdin, and again if a `select` positional's choices resolve
211
+ * empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
212
+ */
213
+ declare function promptForPositionals(entry: PositionalCommand, argv: Record<string, unknown>): Promise<string[]>;
214
+ export { promptForOptions as a, OptionPrompt as c, pickMenuItem as d, searchChoices as f, promptForDeferredOptions as i, OptionsCommand as l, PositionalCommand as n, OptionAnswer as o, MenuItem as p, PositionalPrompt as r, OptionMode as s, promptForPositionals as t, ResolvedOptions as u };