@theholocron/kyber 0.0.0-stage → 5.0.0-alpha.338

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.
@@ -0,0 +1,263 @@
1
+ import { t as NonInteractiveError } from "./non-interactive-error-Mm_Mn79M.mjs";
2
+ import search from "@inquirer/search";
3
+ import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
4
+ // src/prompts/menu/menu.ts
5
+ /**
6
+ * Searchable autocomplete over one menu level's `items`. `select()` would work too
7
+ * at these list sizes, but `search()` degrades gracefully as the surface grows and
8
+ * costs nothing when it doesn't. Resolves to the picked item's `value`.
9
+ */
10
+ async function pickMenuItem(items, message = "What would you like to do?") {
11
+ return search({
12
+ message,
13
+ source: (term) => searchChoices(items, term)
14
+ });
15
+ }
16
+ /**
17
+ * `@inquirer/search`'s `source` callback, factored out as a plain function —
18
+ * unit-testable directly instead of only through a mocked `search()` call.
19
+ * Empty/undefined `term` (nothing typed yet) returns every item.
20
+ */
21
+ function searchChoices(items, term) {
22
+ const needle = term?.toLowerCase();
23
+ const pool = !needle ? items : items.filter((i) => i.haystack.includes(needle));
24
+ const rank = (i) => {
25
+ if (!needle) return 0;
26
+ const name = i.name.toLowerCase();
27
+ return name.startsWith(needle) ? 0 : name.includes(needle) ? 1 : 2;
28
+ };
29
+ return [...pool].sort((a, b) => rank(a) - rank(b)).map((i) => ({
30
+ name: i.name,
31
+ value: i.value,
32
+ description: i.description,
33
+ short: i.name.replace(/ ›$/, "")
34
+ }));
35
+ }
36
+ // src/prompts/prompt-options/prompt-options.ts
37
+ /**
38
+ * A required option must not resolve to an empty answer — `optionArgv`/
39
+ * `secretEnv` (child-argv.ts) both drop an empty-string value entirely,
40
+ * which would silently invoke the child without a flag the caller asked
41
+ * for. Passed as `validate` so inquirer re-prompts instead of accepting it.
42
+ */
43
+ const requireNonEmpty = (value) => value.trim().length > 0 || "required";
44
+ async function promptOne(option) {
45
+ if (option.secret || option.type === "password") return await password({
46
+ message: option.message,
47
+ ...option.required ? { validate: requireNonEmpty } : {}
48
+ });
49
+ if (option.type === "confirm") return await confirm({
50
+ message: option.message,
51
+ default: option.default
52
+ });
53
+ if (option.type === "select") {
54
+ const choices = typeof option.choices === "function" ? option.choices() : option.choices ?? [];
55
+ return await select({
56
+ message: option.message,
57
+ choices: choices.map((c) => ({ value: c }))
58
+ });
59
+ }
60
+ if (option.arity && option.arity > 1) {
61
+ const values = [];
62
+ for (let n = 0; n < option.arity; n++) values.push(await input({
63
+ message: `${option.message} (${n + 1} of ${option.arity})`,
64
+ ...option.required ? { validate: requireNonEmpty } : {}
65
+ }));
66
+ return values;
67
+ }
68
+ return await input({
69
+ message: option.message,
70
+ default: option.default,
71
+ ...option.required ? { validate: requireNonEmpty } : {}
72
+ });
73
+ }
74
+ /**
75
+ * For each of `options` not already present in `argv`, resolves a value —
76
+ * `required` ones are always included, like required positionals, never
77
+ * behind a selector; everything else is offered through one `checkbox()`
78
+ * ("set any options?") and only the picked ones are then prompted in
79
+ * `options`' own declaration order. An empty list (or every optional one
80
+ * left unpicked) means zero prompts.
81
+ *
82
+ * A picked option (whether required or checkbox-picked) with
83
+ * `askLast: true` (holocron#1052 follow-up — a time-sensitive secret like
84
+ * an OTP, which should be typed as close as possible to when it's actually
85
+ * used, or one that's always needed for a specific operator's own account
86
+ * but not everyone's) isn't given a value here yet: it's returned in
87
+ * `deferred` instead, for the caller to resolve via
88
+ * `promptForDeferredOptions` right before the real (non-dry-run) spawn.
89
+ *
90
+ * `when`, evaluated against answers collected so far in that same
91
+ * declaration order, can skip a picked option entirely. Skipped this way,
92
+ * the option is simply absent from the result, exactly as if it had never
93
+ * been picked — acceptable for a minor, genuinely optional dependency, but
94
+ * prefer `OptionMode` (see `promptForOptions`) when the options have a
95
+ * real either/or relationship, so nothing contingent is shown at all.
96
+ */
97
+ async function promptForFlatOptions(commandName, options, argv) {
98
+ if (options.length === 0) return {
99
+ answers: {},
100
+ deferred: []
101
+ };
102
+ const answers = {};
103
+ const deferred = [];
104
+ const needsPrompt = (option) => {
105
+ const existing = argv[option.key];
106
+ return existing === void 0 || existing === "";
107
+ };
108
+ const required = options.filter((o) => o.required);
109
+ const optional = options.filter((o) => !o.required);
110
+ if ((required.some(needsPrompt) || optional.some(needsPrompt)) && !process.stdin.isTTY) throw new NonInteractiveError(`\`${commandName}\` has options only settable interactively — pass them directly: holocron ${commandName} [options]`);
111
+ for (const option of required) {
112
+ if (!needsPrompt(option)) {
113
+ answers[option.key] = argv[option.key];
114
+ continue;
115
+ }
116
+ if (option.askLast) {
117
+ deferred.push(option);
118
+ continue;
119
+ }
120
+ answers[option.key] = await promptOne(option);
121
+ }
122
+ for (const option of optional) if (!needsPrompt(option)) answers[option.key] = argv[option.key];
123
+ const selectable = optional.filter(needsPrompt);
124
+ if (selectable.length === 0) return {
125
+ answers,
126
+ deferred
127
+ };
128
+ const picked = await checkbox({
129
+ message: "Set any options? (space to select, enter to skip)",
130
+ choices: selectable.map((o) => ({
131
+ name: o.message,
132
+ value: o.key
133
+ }))
134
+ });
135
+ if (picked.length === 0) return {
136
+ answers,
137
+ deferred
138
+ };
139
+ const pickedSet = new Set(picked);
140
+ for (const option of optional) {
141
+ if (!pickedSet.has(option.key)) continue;
142
+ if (option.when && !option.when(answers)) continue;
143
+ if (option.askLast) {
144
+ deferred.push(option);
145
+ continue;
146
+ }
147
+ answers[option.key] = await promptOne(option);
148
+ }
149
+ return {
150
+ answers,
151
+ deferred
152
+ };
153
+ }
154
+ /**
155
+ * For a `modes`-based command (holocron#1052 follow-up): asks which
156
+ * mutually exclusive workflow first ("What would you like to do?"), then
157
+ * offers only that mode's own options, via the exact same
158
+ * `promptForFlatOptions` flow. The picked mode's `flag`, if it has one, is
159
+ * folded in as an already-resolved `confirm`-type option so
160
+ * `buildChildArgv` renders it with no special-casing — it just sees
161
+ * another boolean option whose answer happens to already be `true`.
162
+ */
163
+ async function promptForModes(entry, modes, argv) {
164
+ if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` has options only settable interactively — pass them directly: holocron ${entry.name} [options]`);
165
+ const modeKey = await select({
166
+ message: "What would you like to do?",
167
+ choices: modes.map((m) => ({
168
+ name: m.message,
169
+ value: m.key,
170
+ description: m.description
171
+ }))
172
+ });
173
+ const mode = modes.find((m) => m.key === modeKey);
174
+ const options = mode.options ?? [];
175
+ const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
176
+ if (!mode.flag) return {
177
+ options,
178
+ answers,
179
+ deferred
180
+ };
181
+ const flagOption = {
182
+ key: `__mode_${mode.key}`,
183
+ flag: mode.flag,
184
+ type: "confirm",
185
+ message: mode.message
186
+ };
187
+ return {
188
+ options: [flagOption, ...options],
189
+ answers: {
190
+ [flagOption.key]: true,
191
+ ...answers
192
+ },
193
+ deferred
194
+ };
195
+ }
196
+ /**
197
+ * Resolves `entry`'s options into a {@link ResolvedOptions} ready for
198
+ * `buildChildArgv`/`secretEnv` — delegating to {@link promptForModes} when
199
+ * `entry.modes` is set, or treating `entry.options` as a flat list
200
+ * otherwise. See `promptForFlatOptions` for the flat-list prompting
201
+ * algorithm itself. `result.deferred` still needs
202
+ * {@link promptForDeferredOptions} before the real spawn.
203
+ */
204
+ async function promptForOptions(entry, argv) {
205
+ if (entry.modes && entry.modes.length > 0) return promptForModes(entry, entry.modes, argv);
206
+ const options = entry.options ?? [];
207
+ const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
208
+ return {
209
+ options,
210
+ answers,
211
+ deferred
212
+ };
213
+ }
214
+ /**
215
+ * Collects a value for each of `deferred` — the `askLast: true` options
216
+ * `promptForOptions` deliberately left unresolved. Call this as the very
217
+ * last thing before the real (non-dry-run) spawn, never before: a dry-run
218
+ * preview never reaches the real npm call these options exist for (an OTP
219
+ * is never checked during `--dry-run`), and a time-sensitive code typed
220
+ * any earlier — before a dry-run preview runs, before "proceed with the
221
+ * real run?" waits on a human — risks going stale before it's actually
222
+ * used. A no-op (no prompt at all) when `deferred` is empty.
223
+ */
224
+ async function promptForDeferredOptions(deferred) {
225
+ const answers = {};
226
+ for (const option of deferred) answers[option.key] = await promptOne(option);
227
+ return answers;
228
+ }
229
+ // src/prompts/prompt-positionals/prompt-positionals.ts
230
+ /**
231
+ * For each of `entry.positionals` not already present in `argv`, prompts for
232
+ * it (`input()` or `select()`, per the positional's `type`) and returns the
233
+ * resolved values in positional order — ready to append to a spawned child's
234
+ * argv, or to use directly in the current handler (Layer 3).
235
+ *
236
+ * Throws {@link NonInteractiveError} the first time it would need to prompt
237
+ * on a non-TTY stdin, and again if a `select` positional's choices resolve
238
+ * empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
239
+ */
240
+ async function promptForPositionals(entry, argv) {
241
+ const out = [];
242
+ for (const positional of entry.positionals) {
243
+ const existing = argv[positional.key];
244
+ if (existing !== void 0 && existing !== "") {
245
+ out.push(String(existing));
246
+ continue;
247
+ }
248
+ if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` needs "${positional.key}" — pass it directly: holocron ${entry.name} <${positional.cliArg ?? positional.key}>`);
249
+ if (positional.type === "select") {
250
+ const choices = typeof positional.choices === "function" ? positional.choices() : positional.choices ?? [];
251
+ if (choices.length === 0) throw new NonInteractiveError(`\`${entry.name}\`: no ${positional.key} to choose from — nothing stored yet`);
252
+ out.push(await select({
253
+ message: positional.message,
254
+ choices: choices.map((c) => ({ value: c }))
255
+ }));
256
+ } else out.push(await input({
257
+ message: positional.message,
258
+ ...positional.validate ? { validate: positional.validate } : {}
259
+ }));
260
+ }
261
+ return out;
262
+ }
263
+ export { pickMenuItem, promptForDeferredOptions, promptForOptions, promptForPositionals, searchChoices };
@@ -0,0 +1,191 @@
1
+ import { t as ParsedTokenArgs } from "./token-args.type-arNIDKsQ.mjs";
2
+ export declare class TokenParseError extends Error {
3
+ name: string;
4
+ }
5
+ /**
6
+ * Converts raw --token CLI values into a typed result.
7
+ *
8
+ * Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
9
+ * Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
10
+ * Mixed: --token github=ghp_xxx --token v_yyy
11
+ * → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
12
+ *
13
+ * Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
14
+ */
15
+ export declare function parseTokenArgs(tokens: string[]): ParsedTokenArgs;
16
+ /** Puts `text` on the system clipboard through `clipboardy`, which uses the platform's own tool. Loaded only when called. */
17
+ export declare function copyToClipboard(text: string): Promise<void>;
18
+ /**
19
+ * The variables the `credentials` providers returned, kept for the life of the process.
20
+ *
21
+ * The plugin loader reads each provider once (`setCredentials`), and the token resolvers look a token up here
22
+ * (`getCredential`) after the flag and the process environment and before the keyring. It is in memory only: the
23
+ * values were already in this process when the provider returned them, and nothing here writes them anywhere.
24
+ *
25
+ * A source is one provider's answer for one environment. They are kept in the order they were set, which is the
26
+ * order `holocron.config` lists the providers, and the first source that has a value wins.
27
+ */
28
+ /** Remember `variables` as what the source `source` returned. Setting a source again replaces its variables. */
29
+ export declare function setCredentials(source: string, variables: Record<string, string>): void;
30
+ /** Whether the source `source` has been read already, so a second loader in the same process does not read it again. */
31
+ export declare function hasCredentials(source: string): boolean;
32
+ /**
33
+ * The first non-empty value for any of `names`, from the first source that has one. Sources are tried in the order
34
+ * they were set; within a source the names are tried in the order given (the project's own variable name before the
35
+ * vendor's). `undefined` when no source has one.
36
+ */
37
+ export declare function getCredential(...names: string[]): string | undefined;
38
+ /** Forget every source. For tests, and for a process that wants the next load to read the providers again. */
39
+ export declare function clearCredentials(): void;
40
+ /**
41
+ * The file {@link writeCredentialsFile} wrote: its device and inode, and an open descriptor for it. The descriptor is
42
+ * held until cleanup, so the inode cannot be freed and handed to some other file, which makes comparing device and inode
43
+ * a sound way to tell this file from one that has since taken its path. Whoever receives it must give it to
44
+ * {@link shredFile}, or close `fd`.
45
+ */
46
+ interface FileIdentity {
47
+ /** The device the file is on. */
48
+ dev: number;
49
+ /** The file's inode number on that device. */
50
+ ino: number;
51
+ /** An open, writable descriptor for the file, used to overwrite its contents wherever the path points by then. */
52
+ fd: number;
53
+ }
54
+ /**
55
+ * What {@link shredFile} did: `deleted` the file it was told about, found it `missing` (already gone), or left the
56
+ * path alone because it is no longer that file (`replaced`: another process swapped it, or it is not a regular file).
57
+ */
58
+ type ShredOutcome = "deleted" | "missing" | "replaced";
59
+ /**
60
+ * Write `text` to `path` as a file only its owner can read (mode `0600`), and return which file that is, with its
61
+ * descriptor still open, so cleanup can remove only it. Without `force` it will not replace a file that is already
62
+ * there. A write that fails removes the file it had just created, so a failed write never leaves plaintext tokens behind.
63
+ *
64
+ * With `force` the replacement is written to a temporary file beside `path` first and then renamed over it, which is
65
+ * atomic, so a replacement that cannot be written or swapped in leaves the old file exactly as it was. Only once the
66
+ * new file is in place is the old file's content overwritten with zeros through a descriptor opened on it before the
67
+ * swap, so the tokens of an earlier run are not left recoverable. A symlink at `path` is replaced, not followed.
68
+ *
69
+ * Windows does not honour these modes, so the caller does not use this there. `write` is how the text is written; it is
70
+ * a parameter so a test can make the write fail after the file exists.
71
+ */
72
+ export declare function writeCredentialsFile(path: string, text: string, force?: boolean, write?: (fd: number, text: string) => unknown): FileIdentity;
73
+ /** The name of the file, at the repo root, that holds a developer's own tokens. */
74
+ export declare const DOTENV_FILE = ".env";
75
+ /**
76
+ * Read `<repoRoot>/.env` into a record, or `null` when the file is not there (or cannot be read).
77
+ *
78
+ * Only the repo root is looked at, never the directory the command happens to run in, so a `.env` in a checkout you
79
+ * cloned and ran the CLI from is not trusted. The file is optional: nothing asks for it, and a project that keeps its
80
+ * tokens in a `credentials` provider or the keyring does not need one.
81
+ */
82
+ export declare function readDotenvFile(repoRoot: string): Record<string, string> | null;
83
+ /**
84
+ * `NAME=value` in a form dotenv reads back as the same value, or `null` when there is none.
85
+ *
86
+ * dotenv has no escape for a quote or a backslash: a double-quoted value only turns `\n` and `\r` into line breaks, and
87
+ * leaves every other backslash, an escaped quote included, in the value. So nothing is escaped here. A plain value is
88
+ * written bare; any other is wrapped in the first of `'`, `"` and `` ` `` it does not contain; a value with a line
89
+ * break can only be double-quoted (with `\n` and `\r`) and so must not contain `"` or a backslash. A value that fits
90
+ * none of these is `null`, and the caller says so rather than exporting a different token.
91
+ */
92
+ export declare function dotenvLine(name: string, value: string): string | null;
93
+ /**
94
+ * What git says about a path: `ignored` when a rule ignores it, `tracked` when git would track it, `no-repo` when the
95
+ * path is not in a git repository (or git is not installed, in which case nothing can commit it), and `unknown` when
96
+ * git failed in some other way.
97
+ */
98
+ type GitIgnoreState = "ignored" | "tracked" | "no-repo" | "unknown";
99
+ /**
100
+ * Ask git whether it ignores `path` (see {@link GitIgnoreState}). A caller that must not leave a file where it could be
101
+ * committed accepts only `ignored` and `no-repo`. Reads only; it changes nothing.
102
+ */
103
+ export declare function isGitIgnored(path: string): GitIgnoreState;
104
+ /** One stored secret, as the OS keyring binding hands it out. */
105
+ interface KeyringEntry {
106
+ /** Store or replace the secret. Throws when the platform has no credential store. */
107
+ setPassword(password: string): void;
108
+ /** The secret, or `null` when nothing is stored. Throws when the platform has no credential store. */
109
+ getPassword(): string | null;
110
+ /** Remove the secret, and say whether there was one. Throws when the platform has no credential store. */
111
+ deletePassword(): boolean;
112
+ }
113
+ /** The part of `@napi-rs/keyring` that {@link createKeyring} uses, so a test can stand in for the OS. */
114
+ interface KeyringBinding {
115
+ /** A handle on one secret: a service (the scope) and an account (the name within it). */
116
+ Entry: new (service: string, account: string) => KeyringEntry;
117
+ /** Every secret stored under `service`. Throws when the platform has no credential store. */
118
+ findCredentials(service: string): Array<{
119
+ account: string;
120
+ }>;
121
+ }
122
+ /** What {@link createKeyring} needs. */
123
+ interface KeyringOptions {
124
+ /**
125
+ * The scope every secret is stored under, usually a reverse-DNS name (`com.example.tool`), so one tool's secrets
126
+ * do not mix with another's.
127
+ */
128
+ service: string;
129
+ /**
130
+ * The OS keyring binding. Default: `@napi-rs/keyring`, loaded the first time it is needed; a keyring that cannot be
131
+ * loaded behaves like one the platform does not support.
132
+ */
133
+ binding?: KeyringBinding | null;
134
+ }
135
+ /** A tool's secrets in the OS keyring. Every method is best-effort: where the keyring cannot be used it answers `null`, `false` or an empty list and never throws. */
136
+ interface Keyring {
137
+ /**
138
+ * The secret stored for `account`, or `null` for both "not stored" and "keyring unavailable". The answer, a miss
139
+ * included, is remembered for the life of the process, so the keyring (which can prompt on every read) is asked once
140
+ * per account.
141
+ */
142
+ get(account: string): string | null;
143
+ /** Store or replace the secret for `account`. `true` on success, `false` when the keyring is unavailable. */
144
+ set(account: string, secret: string): boolean;
145
+ /** Remove the secret for `account`. `true` when one was removed, `false` when there was none or the keyring is unavailable. */
146
+ delete(account: string): boolean;
147
+ /** The accounts that have a secret under this service. */
148
+ list(): string[];
149
+ /** Whether the OS keyring can be used at all, for a caller that must tell an outage from an empty keyring. */
150
+ isAvailable(): boolean;
151
+ /** Forget what `get` remembered, so the next read asks the keyring again. For a test, or after the keyring was changed from outside the process. */
152
+ clearCache(): void;
153
+ }
154
+ /**
155
+ * A tool's secrets in the OS keyring, under one service name.
156
+ *
157
+ * Keyring access is best-effort. Platforms without a supported credential store (some Linux CI images, sandboxed
158
+ * environments) throw from the underlying library, and `@napi-rs/keyring` is an optional peer that may not be installed
159
+ * at all. Every method catches that and answers `null`, `false` or an empty list rather than propagating, so a caller's
160
+ * chain (a flag, then the environment, then the keyring) falls through to the paths that do work, which is how CI is
161
+ * meant to run.
162
+ *
163
+ * Reads are remembered in memory only, for the life of the process. A cache that outlived the process would mean writing
164
+ * secrets somewhere else, which is what the keyring is there to avoid.
165
+ */
166
+ export declare function createKeyring(options: KeyringOptions): Keyring;
167
+ /**
168
+ * Create `path` as a new file only its owner can read (mode `0600`, set again on the open file so a loose `umask`
169
+ * cannot widen it), write `text` to it and return which file it is, with its descriptor left open. It fails if the path
170
+ * exists, a symlink included, so nothing is written through a planted link or over a file that is there. When the
171
+ * write fails after the file exists, it closes and removes that file, so no plaintext is left behind. `write` is how the
172
+ * text is written; it is a parameter so a test can make the write fail.
173
+ */
174
+ export declare function createPrivateFile(path: string, text: string, write?: (fd: number, text: string) => unknown): FileIdentity;
175
+ /**
176
+ * Overwrite the file `identity` names with zeros, close it, and remove it from `path`, so the tokens it held are not
177
+ * left in a deleted-but-recoverable file.
178
+ *
179
+ * The overwrite goes through the descriptor held since the file was written, not through the path, so it zeroes that
180
+ * file wherever the path points now and never follows a symlink: a symlink or another file swapped in is not touched.
181
+ * The path itself is removed only if it is still that file (same device and inode, which the held descriptor keeps
182
+ * from being reused); otherwise it is left alone (`replaced`), so a file another process put there, or another
183
+ * export's file, survives. A path that is already gone is `missing`, not an error; any other failure to remove it
184
+ * throws, so the caller never reports a file as deleted while it is still there.
185
+ *
186
+ * The overwrite is best effort: a journaling or copy-on-write filesystem and an SSD may keep older blocks it does not
187
+ * reach, which is why the file is also kept private and short-lived. One that fails does not stop the removal.
188
+ * `remove` is how the path is deleted; it is a parameter so a test can look at the file just before it goes.
189
+ */
190
+ export declare function shredFile(path: string, identity: FileIdentity, remove?: (path: string) => void): ShredOutcome;
191
+ export type { FileIdentity, GitIgnoreState, Keyring, KeyringBinding, KeyringEntry, KeyringOptions, ParsedTokenArgs, ShredOutcome };