@theholocron/kyber 5.0.0-alpha.340 → 5.0.0-alpha.342
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -10
- package/dist/config.d.mts +81 -0
- package/dist/config.mjs +111 -0
- package/dist/index-DpXxO_wH.d.mts +214 -0
- package/dist/menu.d.mts +200 -0
- package/dist/menu.mjs +322 -0
- package/dist/notifier.mjs +1 -1
- package/dist/observability.d.mts +156 -0
- package/dist/observability.mjs +254 -0
- package/dist/prompts.d.mts +2 -214
- package/dist/prompts.mjs +1 -1
- package/dist/secrets.d.mts +81 -1
- package/dist/secrets.mjs +74 -2
- package/dist/testing.mjs +2 -2
- package/dist/ui.d.mts +57 -3
- package/dist/ui.mjs +80 -4
- package/package.json +33 -5
package/dist/secrets.d.mts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
* spinner's text while the step runs, for instance to say that
|
|
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
|
|
292
|
-
* spinner's text while the step runs, for instance to say that
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
},
|