@theholocron/kyber 5.0.0-alpha.340 → 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.
@@ -1,214 +1,2 @@
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
- export 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
- export 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
- export 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
- export 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
- export declare function promptForPositionals(entry: PositionalCommand, argv: Record<string, unknown>): Promise<string[]>;
214
- export type { MenuItem, OptionAnswer, OptionMode, OptionPrompt, OptionsCommand, PositionalCommand, PositionalPrompt, ResolvedOptions };
1
+ import { a as promptForOptions, c as OptionPrompt, d as pickMenuItem, f as searchChoices, i as promptForDeferredOptions, l as OptionsCommand, n as PositionalCommand, o as OptionAnswer, p as MenuItem, r as PositionalPrompt, s as OptionMode, t as promptForPositionals, u as ResolvedOptions } from "./index-DpXxO_wH.mjs";
2
+ export { type MenuItem, type OptionAnswer, type OptionMode, type OptionPrompt, type OptionsCommand, type PositionalCommand, type PositionalPrompt, type ResolvedOptions, pickMenuItem, promptForDeferredOptions, promptForOptions, promptForPositionals, searchChoices };
package/dist/prompts.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as NonInteractiveError } from "./non-interactive-error-Mm_Mn79M.mjs";
2
- import search from "@inquirer/search";
3
2
  import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
3
+ import search from "@inquirer/search";
4
4
  // src/prompts/menu/menu.ts
5
5
  /**
6
6
  * Searchable autocomplete over one menu level's `items`. `select()` would work too
@@ -13,6 +13,14 @@ export declare class TokenParseError extends Error {
13
13
  * Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
14
14
  */
15
15
  export declare function parseTokenArgs(tokens: string[]): ParsedTokenArgs;
16
+ /**
17
+ * Thrown by a token resolver when no token was found in any source. Its message says how to supply one. A caller that
18
+ * may load a second copy of this package matches it by `name`, not `instanceof`.
19
+ */
20
+ export declare class AuthError extends Error {
21
+ /** The error name, `AuthError`. */
22
+ name: string;
23
+ }
16
24
  /** Puts `text` on the system clipboard through `clipboardy`, which uses the platform's own tool. Loaded only when called. */
17
25
  export declare function copyToClipboard(text: string): Promise<void>;
18
26
  /**
@@ -90,6 +98,78 @@ export declare function readDotenvFile(repoRoot: string): Record<string, string>
90
98
  * none of these is `null`, and the caller says so rather than exporting a different token.
91
99
  */
92
100
  export declare function dotenvLine(name: string, value: string): string | null;
101
+ /** Where a token resolver looks for a token, beyond what it was configured with. */
102
+ interface ResolveTokenInput {
103
+ /** The `--token` flag. */
104
+ cliToken?: string;
105
+ /** The environment to read. Default `process.env`; a test passes its own. */
106
+ env?: NodeJS.ProcessEnv;
107
+ /** Looks an account up in the keyring. Replaces the configured lookup and the credential store; a test passes its own. */
108
+ keyring?: (account: string) => string | null;
109
+ /** The active org. It selects the keyring account `<service>.<org>` first. Default: the variable `orgEnvName` names. */
110
+ org?: string;
111
+ }
112
+ /** How {@link createResolveToken} finds one vendor's token. */
113
+ interface ResolveTokenConfig {
114
+ /** The tool's own variable for this token, such as `MYTOOL_GITHUB_TOKEN`. */
115
+ envName: string;
116
+ /** The vendor's own variable for it, such as `GITHUB_TOKEN`. */
117
+ vendorEnvName: string;
118
+ /** The keyring account the token is stored under, such as `github`. */
119
+ keyringService: string;
120
+ /** The whole message of the {@link AuthError} thrown when no source has a token, saying how to supply one. */
121
+ errorMessage: string;
122
+ /** Looks an account up in the keyring, such as the `get` of {@link createKeyring}. Default: the keyring is not asked. */
123
+ keyring?: (account: string) => string | null;
124
+ /** The variable that names the active org, such as `MYTOOL_ORG`. Default: no org-scoped lookup unless `input.org` is given. */
125
+ orgEnvName?: string;
126
+ }
127
+ /** What {@link createResolveToken} returns: call it for the token, or it throws {@link AuthError}. */
128
+ type TokenResolver = (input?: ResolveTokenInput) => string;
129
+ /**
130
+ * A function that finds one vendor's token, for a tool to call once at module level.
131
+ *
132
+ * @remarks
133
+ * It tries, in order: `input.cliToken` (the `--token` flag), `envName`, `vendorEnvName`, the variables the `credentials`
134
+ * providers returned (see {@link setCredentials}), the keyring account `<service>.<org>` (only when an org is known),
135
+ * then the account `<service>`. An empty string counts as unset everywhere. It throws {@link AuthError} with
136
+ * `errorMessage` when none has a token. An `input.keyring` replaces the credential store and the keyring together.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const resolveToken = createResolveToken({
141
+ * envName: "MYTOOL_GITHUB_TOKEN",
142
+ * vendorEnvName: "GITHUB_TOKEN",
143
+ * keyringService: "github",
144
+ * errorMessage: "no GitHub token found. Pass --token, or set MYTOOL_GITHUB_TOKEN or GITHUB_TOKEN.",
145
+ * keyring: (account) => keyring.get(account),
146
+ * orgEnvName: "MYTOOL_ORG",
147
+ * });
148
+ * ```
149
+ */
150
+ export declare function createResolveToken(config: ResolveTokenConfig): TokenResolver;
151
+ /** How {@link createFeatureResolver} finds the token for one feature. */
152
+ interface FeatureResolverConfig {
153
+ /** The variable for this feature's token, such as `MYTOOL_ISSUES_TOKEN`. */
154
+ envName: string;
155
+ /** The keyring account it is stored under, such as `github.issues`. */
156
+ keyringKey: string;
157
+ /** The vendor named in the "no token" message, such as `GitHub`. */
158
+ vendor: string;
159
+ /** The command that stores a token, for the "no token" message, given the keyring account: `mytool auth set <key> <token>`. */
160
+ setCommand?: (keyringKey: string) => string;
161
+ /** Looks an account up in the keyring, such as the `get` of {@link createKeyring}. Default: the keyring is not asked. */
162
+ keyring?: (account: string) => string | null;
163
+ }
164
+ /** What {@link createFeatureResolver} returns: call it for the token, or it throws {@link AuthError}. */
165
+ type FeatureTokenResolver = (input?: ResolveTokenInput) => string;
166
+ /**
167
+ * A strict resolver for one feature's token. It tries `input.cliToken`, then `envName`, then the variable of that name
168
+ * from the `credentials` providers, then the keyring account `keyringKey`. There is no fallback to a broad token: when
169
+ * the feature's own token is absent it throws {@link AuthError} naming the exact variable to set. An `input.keyring`
170
+ * replaces the credential store and the keyring together.
171
+ */
172
+ export declare function createFeatureResolver(config: FeatureResolverConfig): FeatureTokenResolver;
93
173
  /**
94
174
  * What git says about a path: `ignored` when a rule ignores it, `tracked` when git would track it, `no-repo` when the
95
175
  * path is not in a git repository (or git is not installed, in which case nothing can commit it), and `unknown` when
@@ -188,4 +268,4 @@ export declare function createPrivateFile(path: string, text: string, write?: (f
188
268
  * `remove` is how the path is deleted; it is a parameter so a test can look at the file just before it goes.
189
269
  */
190
270
  export declare function shredFile(path: string, identity: FileIdentity, remove?: (path: string) => void): ShredOutcome;
191
- export type { FileIdentity, GitIgnoreState, Keyring, KeyringBinding, KeyringEntry, KeyringOptions, ParsedTokenArgs, ShredOutcome };
271
+ export type { FeatureResolverConfig, FeatureTokenResolver, FileIdentity, GitIgnoreState, Keyring, KeyringBinding, KeyringEntry, KeyringOptions, ParsedTokenArgs, ResolveTokenConfig, ResolveTokenInput, ShredOutcome, TokenResolver };
package/dist/secrets.mjs CHANGED
@@ -1,10 +1,19 @@
1
1
  import { n as parseTokenArgs, t as TokenParseError } from "./token-args-BgrCq_kK.mjs";
2
2
  import { createRequire } from "node:module";
3
+ import { execFileSync } from "node:child_process";
3
4
  import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
4
5
  import { basename, dirname, join } from "node:path";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import { parse } from "dotenv";
7
- import { execFileSync } from "node:child_process";
8
+ // src/secrets/auth-error/auth-error.ts
9
+ /**
10
+ * Thrown by a token resolver when no token was found in any source. Its message says how to supply one. A caller that
11
+ * may load a second copy of this package matches it by `name`, not `instanceof`.
12
+ */
13
+ var AuthError = class extends Error {
14
+ /** The error name, `AuthError`. */
15
+ name = "AuthError";
16
+ };
8
17
  // src/secrets/copy-to-clipboard/copy-to-clipboard.ts
9
18
  /** Puts `text` on the system clipboard through `clipboardy`, which uses the platform's own tool. Loaded only when called. */
10
19
  async function copyToClipboard(text) {
@@ -210,6 +219,37 @@ function dotenvLine(name, value) {
210
219
  const quote = QUOTES.find((q) => !value.includes(q));
211
220
  return quote ? `${name}=${quote}${value}${quote}` : null;
212
221
  }
222
+ // src/secrets/first-env/first-env.ts
223
+ /**
224
+ * The first non-empty value among `names` in `env` (the process environment when none is given). An empty string counts
225
+ * as unset, so a blank `TOKEN=` line does not hide the next name.
226
+ */
227
+ function firstEnv(env, ...names) {
228
+ const source = env ?? process.env;
229
+ for (const name of names) {
230
+ const value = source[name];
231
+ if (value) return value;
232
+ }
233
+ }
234
+ // src/secrets/feature-resolver/feature-resolver.ts
235
+ /**
236
+ * A strict resolver for one feature's token. It tries `input.cliToken`, then `envName`, then the variable of that name
237
+ * from the `credentials` providers, then the keyring account `keyringKey`. There is no fallback to a broad token: when
238
+ * the feature's own token is absent it throws {@link AuthError} naming the exact variable to set. An `input.keyring`
239
+ * replaces the credential store and the keyring together.
240
+ */
241
+ function createFeatureResolver(config) {
242
+ const stored = (account) => getCredential(config.envName) ?? config.keyring?.(account) ?? null;
243
+ return function resolveFeatureToken(input = {}) {
244
+ const lookup = input.keyring ?? stored;
245
+ const token = input.cliToken || firstEnv(input.env, config.envName) || lookup(config.keyringKey);
246
+ if (!token) {
247
+ const how = config.setCommand ? `, or run: ${config.setCommand(config.keyringKey)}` : "";
248
+ throw new AuthError(`no ${config.vendor} token found for this operation. Pass --token <token>, set ${config.envName}${how}`);
249
+ }
250
+ return token;
251
+ };
252
+ }
213
253
  // src/secrets/git-ignored/git-ignored.ts
214
254
  /**
215
255
  * Ask git whether it ignores `path` (see {@link GitIgnoreState}). A caller that must not leave a file where it could be
@@ -326,4 +366,36 @@ function createKeyring(options) {
326
366
  }
327
367
  };
328
368
  }
329
- export { DOTENV_FILE, TokenParseError, clearCredentials, copyToClipboard, createKeyring, createPrivateFile, dotenvLine, getCredential, hasCredentials, isGitIgnored, parseTokenArgs, readDotenvFile, setCredentials, shredFile, writeCredentialsFile };
369
+ // src/secrets/resolve-token/resolve-token.ts
370
+ /**
371
+ * A function that finds one vendor's token, for a tool to call once at module level.
372
+ *
373
+ * @remarks
374
+ * It tries, in order: `input.cliToken` (the `--token` flag), `envName`, `vendorEnvName`, the variables the `credentials`
375
+ * providers returned (see {@link setCredentials}), the keyring account `<service>.<org>` (only when an org is known),
376
+ * then the account `<service>`. An empty string counts as unset everywhere. It throws {@link AuthError} with
377
+ * `errorMessage` when none has a token. An `input.keyring` replaces the credential store and the keyring together.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const resolveToken = createResolveToken({
382
+ * envName: "MYTOOL_GITHUB_TOKEN",
383
+ * vendorEnvName: "GITHUB_TOKEN",
384
+ * keyringService: "github",
385
+ * errorMessage: "no GitHub token found. Pass --token, or set MYTOOL_GITHUB_TOKEN or GITHUB_TOKEN.",
386
+ * keyring: (account) => keyring.get(account),
387
+ * orgEnvName: "MYTOOL_ORG",
388
+ * });
389
+ * ```
390
+ */
391
+ function createResolveToken(config) {
392
+ const stored = (account) => getCredential(config.envName, config.vendorEnvName) ?? config.keyring?.(account) ?? null;
393
+ return function resolveToken(input = {}) {
394
+ const lookup = input.keyring ?? stored;
395
+ const org = input.org ?? (config.orgEnvName ? firstEnv(input.env, config.orgEnvName) : void 0);
396
+ const token = input.cliToken || firstEnv(input.env, config.envName, config.vendorEnvName) || (org ? lookup(`${config.keyringService}.${org}`) : null) || lookup(config.keyringService);
397
+ if (!token) throw new AuthError(config.errorMessage);
398
+ return token;
399
+ };
400
+ }
401
+ export { AuthError, DOTENV_FILE, TokenParseError, clearCredentials, copyToClipboard, createFeatureResolver, createKeyring, createPrivateFile, createResolveToken, dotenvLine, getCredential, hasCredentials, isGitIgnored, parseTokenArgs, readDotenvFile, setCredentials, shredFile, writeCredentialsFile };
package/dist/testing.mjs CHANGED
@@ -1,6 +1,6 @@
1
+ import { execFileSync, spawn } from "node:child_process";
1
2
  import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
2
3
  import { dirname, join, sep } from "node:path";
3
- import { execFileSync, spawn } from "node:child_process";
4
4
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
package/dist/ui.d.mts CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * The line to print for an error that reached the top of a command: {@link describeError | describeError}'s text, and
3
+ * never empty, so a failure always says something. An error with no message prints its class name; anything else with
4
+ * no text prints "the command failed".
5
+ */
6
+ export declare function commandFailureMessage(err: unknown): string;
7
+ /**
8
+ * What to print for a caught value: its message with control characters stripped (a provider's text can carry them),
9
+ * and, when the error has a non-empty `details` (the raw response body of an API error, say), that body after it,
10
+ * sanitized and length-capped. A bare "GET /x → 403" then says why, and the vendor's text cannot flood or manipulate the
11
+ * terminal. Anything that is not an `Error` is printed with `String()`.
12
+ */
13
+ export declare function describeError(err: unknown): string;
1
14
  /** A JSON-pointer-ish path: `""` is the root, children append `/<key>`. */
2
15
  type Path = string;
3
16
  /** One rendered line of the viewer. */
@@ -57,6 +70,12 @@ export declare function reduceKey(state: ViewState, key: Key, value: unknown, ro
57
70
  * coloured tree, collapsed past `depth`. `interactive` opens a folding viewer (the default on a TTY).
58
71
  */
59
72
  export declare function viewJson(value: unknown, options?: ViewJsonOptions): Promise<void>;
73
+ /**
74
+ * What to put in structured logs: the plain message of the underlying error, with no response body. The detail
75
+ * {@link describeError} adds is for the person at the terminal; a provider's raw body has no business in telemetry. A
76
+ * wrapper error carries the original as `cause`, so this looks through one level.
77
+ */
78
+ export declare function logReason(err: unknown): string;
60
79
  /**
61
80
  * Raised when a prompt is needed but stdin isn't a TTY. A non-TTY run (CI, a
62
81
  * script, a pipe) throws this instead of hanging on `@inquirer/prompts`, which
@@ -93,6 +112,12 @@ type ProgressListener = (label: string, setText: (text: string) => void) => () =
93
112
  export declare function withSpinner<T>(label: string, fn: () => Promise<T>): Promise<T>;
94
113
  /** Turn progress off for the run (`--quiet`, `--log`), so nothing extra shares the terminal with what those print. */
95
114
  export declare function allowProgress(on: boolean): void;
115
+ /**
116
+ * Hooks `listener` into every step that does not pass its own, such as a notice that a request is being sent again.
117
+ * Called once at start-up, so each call site stays a plain {@link createProgress} or {@link withProgress}. Pass
118
+ * `undefined` to take it off again.
119
+ */
120
+ export declare function setProgressListener(listener: ProgressListener | undefined): void;
96
121
  /**
97
122
  * Whether to draw a progress line: only when a person is watching a terminal (stdout and stderr both), not in CI, and not
98
123
  * switched off. Everywhere else the output stays the plain line-per-step text that scripts and the docs rely on.
@@ -100,12 +125,18 @@ export declare function allowProgress(on: boolean): void;
100
125
  export declare function progressEnabled(): boolean;
101
126
  /**
102
127
  * A spinner for one step at a time that a step can print through. Steps that only return a result use {@link
103
- * withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook can change the
104
- * spinner's text while the step runs, for instance to say that a request is being sent again.
128
+ * withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook, or else the
129
+ * one set with {@link setProgressListener}, can change the spinner's text while the step runs, for instance to say that
130
+ * a request is being sent again.
105
131
  */
106
132
  export declare function createProgress(print: (line: string) => void, enabled?: () => boolean, listen?: ProgressListener): Progress;
107
133
  /** `fn` under a cleared-when-done spinner named `label`, on a terminal only. */
108
134
  export declare function withProgress<T>(label: string, fn: () => Promise<T>, listen?: ProgressListener): Promise<T>;
135
+ /**
136
+ * {@link stripControl}, then cut to 200 characters with an ellipsis. For a vendor's response body or any other text of
137
+ * unknown size that lands in the terminal.
138
+ */
139
+ export declare function sanitizeDetail(text: string): string;
109
140
  /** How a row of a check or setup run came out: it passed, it failed, or it was skipped. */
110
141
  type RowStatus = "ok" | "fail" | "skip";
111
142
  /** Right-pads `s` to `width` with spaces — never truncates. */
@@ -122,6 +153,12 @@ export declare function pad(s: string, width: number): string;
122
153
  * destination / reason) so every status table in the CLI reads the same way.
123
154
  */
124
155
  export declare function statusRow(status: RowStatus, columns: string[]): string;
156
+ /**
157
+ * Control characters (newlines and the C1 range included, whose 0x9b is an 8-bit CSI) turned into spaces and whitespace
158
+ * collapsed, with no length cap. For text from outside the process that is about to reach a terminal, where a crafted
159
+ * value could otherwise forge extra lines or inject escape sequences.
160
+ */
161
+ export declare function stripControl(text: string): string;
125
162
  /** Formats a message with a status marker and colour: one helper per kind of line a command prints. */
126
163
  export declare const style: {
127
164
  /** A green `✓` for something that worked. */
package/dist/ui.mjs CHANGED
@@ -3,6 +3,51 @@ import chalk from "chalk";
3
3
  import { spawn } from "node:child_process";
4
4
  import ora from "ora";
5
5
  import { createInterface } from "node:readline";
6
+ // src/ui/strip-control/strip-control.ts
7
+ /**
8
+ * Control characters (newlines and the C1 range included, whose 0x9b is an 8-bit CSI) turned into spaces and whitespace
9
+ * collapsed, with no length cap. For text from outside the process that is about to reach a terminal, where a crafted
10
+ * value could otherwise forge extra lines or inject escape sequences.
11
+ */
12
+ function stripControl(text) {
13
+ return text.replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
14
+ }
15
+ // src/ui/sanitize-detail/sanitize-detail.ts
16
+ /** The longest a detail is kept, so one oversized body cannot flood a report. */
17
+ const MAX_DETAIL_LENGTH = 200;
18
+ /**
19
+ * {@link stripControl}, then cut to 200 characters with an ellipsis. For a vendor's response body or any other text of
20
+ * unknown size that lands in the terminal.
21
+ */
22
+ function sanitizeDetail(text) {
23
+ const stripped = stripControl(text);
24
+ return stripped.length > MAX_DETAIL_LENGTH ? `${stripped.slice(0, MAX_DETAIL_LENGTH)}…` : stripped;
25
+ }
26
+ // src/ui/describe-error/describe-error.ts
27
+ /**
28
+ * What to print for a caught value: its message with control characters stripped (a provider's text can carry them),
29
+ * and, when the error has a non-empty `details` (the raw response body of an API error, say), that body after it,
30
+ * sanitized and length-capped. A bare "GET /x → 403" then says why, and the vendor's text cannot flood or manipulate the
31
+ * terminal. Anything that is not an `Error` is printed with `String()`.
32
+ */
33
+ function describeError(err) {
34
+ const base = stripControl(err instanceof Error ? err.message : String(err));
35
+ const details = err instanceof Error ? err.details : void 0;
36
+ if (details) {
37
+ const detail = sanitizeDetail(String(details));
38
+ if (detail) return `${base}: ${detail}`;
39
+ }
40
+ return base;
41
+ }
42
+ // src/ui/command-failure/command-failure.ts
43
+ /**
44
+ * The line to print for an error that reached the top of a command: {@link describeError | describeError}'s text, and
45
+ * never empty, so a failure always says something. An error with no message prints its class name; anything else with
46
+ * no text prints "the command failed".
47
+ */
48
+ function commandFailureMessage(err) {
49
+ return describeError(err) || (err instanceof Error ? err.name : "the command failed");
50
+ }
6
51
  // src/ui/json-view/json-view.ts
7
52
  function isContainer(value) {
8
53
  return typeof value === "object" && value !== null;
@@ -221,6 +266,16 @@ async function viewJson(value, options = {}) {
221
266
  const lines = renderTree(value, { expanded: expandToDepth(value, depth) });
222
267
  console.log(lines.map((l) => l.text).join("\n"));
223
268
  }
269
+ // src/ui/log-reason/log-reason.ts
270
+ /**
271
+ * What to put in structured logs: the plain message of the underlying error, with no response body. The detail
272
+ * {@link describeError} adds is for the person at the terminal; a provider's raw body has no business in telemetry. A
273
+ * wrapper error carries the original as `cause`, so this looks through one level.
274
+ */
275
+ function logReason(err) {
276
+ const root = err instanceof Error && err.cause !== void 0 ? err.cause : err;
277
+ return root instanceof Error ? root.message : String(root);
278
+ }
224
279
  // src/ui/open-path/open-path.ts
225
280
  /** Platform-appropriate command to open a path in the OS's file browser. */
226
281
  function openCommand(path) {
@@ -275,11 +330,20 @@ async function withSpinner(label, fn) {
275
330
  }
276
331
  }
277
332
  let allowed = true;
333
+ let defaultListener;
278
334
  /** Turn progress off for the run (`--quiet`, `--log`), so nothing extra shares the terminal with what those print. */
279
335
  function allowProgress(on) {
280
336
  allowed = on;
281
337
  }
282
338
  /**
339
+ * Hooks `listener` into every step that does not pass its own, such as a notice that a request is being sent again.
340
+ * Called once at start-up, so each call site stays a plain {@link createProgress} or {@link withProgress}. Pass
341
+ * `undefined` to take it off again.
342
+ */
343
+ function setProgressListener(listener) {
344
+ defaultListener = listener;
345
+ }
346
+ /**
283
347
  * Whether to draw a progress line: only when a person is watching a terminal (stdout and stderr both), not in CI, and not
284
348
  * switched off. Everywhere else the output stays the plain line-per-step text that scripts and the docs rely on.
285
349
  */
@@ -288,8 +352,9 @@ function progressEnabled() {
288
352
  }
289
353
  /**
290
354
  * A spinner for one step at a time that a step can print through. Steps that only return a result use {@link
291
- * withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook can change the
292
- * spinner's text while the step runs, for instance to say that a request is being sent again.
355
+ * withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook, or else the
356
+ * one set with {@link setProgressListener}, can change the spinner's text while the step runs, for instance to say that
357
+ * a request is being sent again.
293
358
  */
294
359
  function createProgress(print, enabled = progressEnabled, listen) {
295
360
  let active;
@@ -302,7 +367,7 @@ function createProgress(print, enabled = progressEnabled, listen) {
302
367
  discardStdin: false
303
368
  }).start();
304
369
  active = spinner;
305
- const stopListening = listen?.(label, (text) => {
370
+ const stopListening = (listen ?? defaultListener)?.(label, (text) => {
306
371
  spinner.text = text;
307
372
  });
308
373
  try {
@@ -388,4 +453,4 @@ function waitForEnter(message, input = process.stdin, output = process.stderr) {
388
453
  });
389
454
  });
390
455
  }
391
- export { NonInteractiveError, allowProgress, createProgress, expandToDepth, openPath, pad, progressEnabled, reduceKey, renderTree, statusRow, style, viewJson, waitForEnter, withProgress, withSpinner };
456
+ export { NonInteractiveError, allowProgress, commandFailureMessage, createProgress, describeError, expandToDepth, logReason, openPath, pad, progressEnabled, reduceKey, renderTree, sanitizeDetail, setProgressListener, statusRow, stripControl, style, viewJson, waitForEnter, withProgress, withSpinner };