@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.
@@ -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,8 +1,8 @@
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
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
4
  import { tmpdir } from "node:os";
5
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import yargs from "yargs";
8
8
  // src/testing/cli-runner/cli-runner.ts
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,35 @@ 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
+ /** What {@link createRetryNotice} reads from one retry. Structurally compatible with `@theholocron/http-client`'s `RetryEvent`. */
136
+ interface RetryNoticeEvent {
137
+ /** The service being called, when the client knows it. */
138
+ vendor?: string | undefined;
139
+ /** The attempt about to be made. */
140
+ attempt: number;
141
+ /** How many attempts there are in all. */
142
+ of: number;
143
+ }
144
+ /** Registers a handler for retries and returns a function that removes it. */
145
+ type RetrySubscribe = (handler: (event: RetryNoticeEvent) => void) => () => void;
146
+ /**
147
+ * Builds a {@link ProgressListener} that says on the spinner when a request is being sent again, so a slow step is
148
+ * not mistaken for a stuck one. `subscribe` is the HTTP client's retry hook (e.g. `onRetry`), which keeps kyber free
149
+ * of any client dependency.
150
+ */
151
+ export declare function createRetryNotice(subscribe: RetrySubscribe): ProgressListener;
152
+ /**
153
+ * {@link stripControl}, then cut to 200 characters with an ellipsis. For a vendor's response body or any other text of
154
+ * unknown size that lands in the terminal.
155
+ */
156
+ export declare function sanitizeDetail(text: string): string;
109
157
  /** How a row of a check or setup run came out: it passed, it failed, or it was skipped. */
110
158
  type RowStatus = "ok" | "fail" | "skip";
111
159
  /** Right-pads `s` to `width` with spaces — never truncates. */
@@ -122,6 +170,12 @@ export declare function pad(s: string, width: number): string;
122
170
  * destination / reason) so every status table in the CLI reads the same way.
123
171
  */
124
172
  export declare function statusRow(status: RowStatus, columns: string[]): string;
173
+ /**
174
+ * Control characters (newlines and the C1 range included, whose 0x9b is an 8-bit CSI) turned into spaces and whitespace
175
+ * collapsed, with no length cap. For text from outside the process that is about to reach a terminal, where a crafted
176
+ * value could otherwise forge extra lines or inject escape sequences.
177
+ */
178
+ export declare function stripControl(text: string): string;
125
179
  /** Formats a message with a status marker and colour: one helper per kind of line a command prints. */
126
180
  export declare const style: {
127
181
  /** A green `✓` for something that worked. */
@@ -148,4 +202,4 @@ export declare const style: {
148
202
  * streams are parameters so a test can answer for the user.
149
203
  */
150
204
  export declare function waitForEnter(message: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<"enter" | "closed">;
151
- export type { Container, Key, Line, Path, Progress, ProgressListener, RenderOptions, RowStatus, ViewJsonOptions, ViewState };
205
+ export type { Container, Key, Line, Path, Progress, ProgressListener, RenderOptions, RetryNoticeEvent, RetrySubscribe, RowStatus, ViewJsonOptions, ViewState };
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 {
@@ -326,6 +391,17 @@ function createProgress(print, enabled = progressEnabled, listen) {
326
391
  function withProgress(label, fn, listen) {
327
392
  return createProgress(() => {}, progressEnabled, listen).run(label, fn);
328
393
  }
394
+ // src/ui/retry-notice/retry-notice.ts
395
+ /**
396
+ * Builds a {@link ProgressListener} that says on the spinner when a request is being sent again, so a slow step is
397
+ * not mistaken for a stuck one. `subscribe` is the HTTP client's retry hook (e.g. `onRetry`), which keeps kyber free
398
+ * of any client dependency.
399
+ */
400
+ function createRetryNotice(subscribe) {
401
+ return (label, setText) => subscribe((e) => {
402
+ setText(`${label} (retrying ${e.vendor ?? "the request"}, attempt ${e.attempt} of ${e.of})`);
403
+ });
404
+ }
329
405
  // src/ui/style/style.ts
330
406
  /** Formats a message with a status marker and colour: one helper per kind of line a command prints. */
331
407
  const style = {
@@ -388,4 +464,4 @@ function waitForEnter(message, input = process.stdin, output = process.stderr) {
388
464
  });
389
465
  });
390
466
  }
391
- export { NonInteractiveError, allowProgress, createProgress, expandToDepth, openPath, pad, progressEnabled, reduceKey, renderTree, statusRow, style, viewJson, waitForEnter, withProgress, withSpinner };
467
+ export { NonInteractiveError, allowProgress, commandFailureMessage, createProgress, createRetryNotice, describeError, expandToDepth, logReason, openPath, pad, progressEnabled, reduceKey, renderTree, sanitizeDetail, setProgressListener, statusRow, stripControl, style, viewJson, waitForEnter, withProgress, withSpinner };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@theholocron/kyber",
3
- "version": "5.0.0-alpha.340",
4
- "description": "The reusable parts of a command-line tool: terminal output, prompts, the update notifier, safe handling of tokens, a test harness and the global flags.",
3
+ "version": "5.0.0-alpha.342",
4
+ "description": "The reusable parts of a command-line tool: terminal output, prompts, the interactive menu, the update notifier, safe handling of tokens, a test harness and the global flags.",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "holocron",
@@ -20,16 +20,31 @@
20
20
  "sideEffects": false,
21
21
  "type": "module",
22
22
  "exports": {
23
+ "./config": {
24
+ "types": "./dist/config.d.mts",
25
+ "import": "./dist/config.mjs",
26
+ "default": "./dist/config.mjs"
27
+ },
23
28
  "./flags": {
24
29
  "types": "./dist/flags.d.mts",
25
30
  "import": "./dist/flags.mjs",
26
31
  "default": "./dist/flags.mjs"
27
32
  },
33
+ "./menu": {
34
+ "types": "./dist/menu.d.mts",
35
+ "import": "./dist/menu.mjs",
36
+ "default": "./dist/menu.mjs"
37
+ },
28
38
  "./notifier": {
29
39
  "types": "./dist/notifier.d.mts",
30
40
  "import": "./dist/notifier.mjs",
31
41
  "default": "./dist/notifier.mjs"
32
42
  },
43
+ "./observability": {
44
+ "types": "./dist/observability.d.mts",
45
+ "import": "./dist/observability.mjs",
46
+ "default": "./dist/observability.mjs"
47
+ },
33
48
  "./prompts": {
34
49
  "types": "./dist/prompts.d.mts",
35
50
  "import": "./dist/prompts.mjs",
@@ -54,6 +69,9 @@
54
69
  "files": [
55
70
  "dist"
56
71
  ],
72
+ "dependencies": {
73
+ "@theholocron/env-utils": "^1.17.0-alpha.1"
74
+ },
57
75
  "devDependencies": {
58
76
  "@inquirer/prompts": "^8.7.5",
59
77
  "@inquirer/search": "^4.3.6",
@@ -73,9 +91,11 @@
73
91
  "vitest": "^5.0.5",
74
92
  "yargs": "^18.1.0",
75
93
  "@theholocron/tsconfig": "^8.9.0-alpha.8",
76
- "@theholocron/vitest-config": "^8.9.0-alpha.8",
94
+ "@theholocron/eslint-config": "^8.9.0-alpha.8",
95
+ "@theholocron/observability": "0.7.0-alpha.1",
77
96
  "@theholocron/tsdown-config": "^8.9.0-alpha.8",
78
- "@theholocron/eslint-config": "^8.9.0-alpha.8"
97
+ "@theholocron/vitest-config": "^8.9.0-alpha.8",
98
+ "@theholocron/datapad": "5.0.0-alpha.342"
79
99
  },
80
100
  "peerDependencies": {
81
101
  "@inquirer/prompts": "^8.0.0",
@@ -85,7 +105,9 @@
85
105
  "clipboardy": "^5.0.0",
86
106
  "dotenv": "^17.0.0",
87
107
  "ora": "^9.0.0",
88
- "yargs": "^18.0.0"
108
+ "yargs": "^18.0.0",
109
+ "@theholocron/datapad": "^5.0.0-alpha.342",
110
+ "@theholocron/observability": "^0.7.0-alpha.1"
89
111
  },
90
112
  "peerDependenciesMeta": {
91
113
  "@inquirer/prompts": {
@@ -97,6 +119,12 @@
97
119
  "@napi-rs/keyring": {
98
120
  "optional": true
99
121
  },
122
+ "@theholocron/datapad": {
123
+ "optional": true
124
+ },
125
+ "@theholocron/observability": {
126
+ "optional": true
127
+ },
100
128
  "chalk": {
101
129
  "optional": true
102
130
  },