@theholocron/kyber 0.0.0-stage → 5.0.0-alpha.336
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/flags.d.mts +38 -0
- package/dist/flags.mjs +56 -0
- package/dist/non-interactive-error-Mm_Mn79M.mjs +12 -0
- package/dist/notifier.d.mts +33 -0
- package/dist/notifier.mjs +121 -0
- package/dist/prompts.d.mts +214 -0
- package/dist/prompts.mjs +263 -0
- package/dist/secrets.d.mts +191 -0
- package/dist/secrets.mjs +329 -0
- package/dist/testing.d.mts +83 -0
- package/dist/testing.mjs +139 -0
- package/dist/token-args-BgrCq_kK.mjs +38 -0
- package/dist/token-args.type-arNIDKsQ.d.mts +7 -0
- package/dist/ui.d.mts +151 -0
- package/dist/ui.mjs +391 -0
- package/package.json +126 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Newton Koumantzelis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@theholocron/kyber`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The reusable parts of a command-line tool: terminal output, prompts, the update notifier, safe handling of tokens, a
|
|
4
|
+
test harness and the global flags. They were written for the Holocron CLI and are moved here one subpath at a time, so
|
|
5
|
+
the next CLI does not copy them (spec `tech-kyber`, #1727).
|
|
6
|
+
|
|
7
|
+
Published, so a second CLI can import it. `@theholocron/cli` is its first consumer and `cli-template` is the next.
|
|
8
|
+
|
|
9
|
+
> The kyber crystal is the part in a lightsaber that makes it work.
|
|
10
|
+
|
|
11
|
+
## Subpaths
|
|
12
|
+
|
|
13
|
+
| Subpath | What it gives a CLI |
|
|
14
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
15
|
+
| `/ui` | Status glyphs and colours, padded status rows, a depth-limited JSON view, a spinner that only shows on a terminal, wait-for-Enter, open-in-file-browser, and the error for "a prompt was needed but there is no terminal". |
|
|
16
|
+
| `/notifier` | A cached, best-effort "a newer version exists" notice for an npm package: `checkForUpdates({ name, currentVersion, cacheDir })` returns a function that prints a boxed notice to stderr, or `null`. Silent in CI and with `NO_UPDATE_NOTIFIER`. |
|
|
17
|
+
| `/secrets` | Handling tokens safely, with no terminal UI: owner-only (`0600`) files, shredding a file that held tokens, "does git ignore this path", dotenv lines that survive quoting, reading a `.env` at the repo root, the clipboard, the OS keyring (`createKeyring({ service })`, best-effort, `@napi-rs/keyring` optional), an in-process store for credentials a provider returned, and `--token` / `--token vendor=value` parsing. |
|
|
18
|
+
| `/prompts` | A searchable menu (`pickMenuItem`), and prompting for the positionals and options a command left out (`promptForPositionals`, `promptForOptions`): required options always asked, the rest behind one checkbox, mutually exclusive `modes`, secret options masked, and a value that is asked last (`askLast`). They take small structural types (`PositionalCommand`, `OptionsCommand`), not the CLI's command registry. |
|
|
19
|
+
| `/testing` | A test harness for a CLI: `createCliRunner` runs a built binary in a fresh directory with an environment built from nothing, `runCommand` runs one yargs command module through a real parse, `ensureBuilt` is a Vitest global setup that builds only when `dist` is missing or stale, and `hermeticCreateRequire` resolves modules only from one directory. |
|
|
20
|
+
| `/flags` | The options every command of a tool takes: `addGlobalOptions(parser, { describe?, cwd? })` declares `--dry-run`, a repeatable `--token` and `--cwd` once on the root; `tokenContext(values, report)` parses the `--token` values and reports a bad one without a stack trace; `groupMenu(parent, open)` is the hidden default command that makes a bare group (`tool repo`) do something useful; `InferArgs` types a command's handler arguments from its builder. |
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createProgress, statusRow, style, viewJson } from "@theholocron/kyber/ui";
|
|
26
|
+
|
|
27
|
+
console.log(style.success("built"));
|
|
28
|
+
console.log(statusRow("ok", ["lint", "no problems"]));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { addGlobalOptions, tokenContext } from "@theholocron/kyber/flags";
|
|
33
|
+
import { createKeyring } from "@theholocron/kyber/secrets";
|
|
34
|
+
|
|
35
|
+
const parser = addGlobalOptions(yargs(hideBin(process.argv)), { describe: { cwd: "Where tool.config.json is" } });
|
|
36
|
+
const tokens = tokenContext(argv.token, console.error); // null (and exit code 1) for a malformed --token
|
|
37
|
+
const keyring = createKeyring({ service: "com.example.tool" }); // best-effort: never throws
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Each subpath's heavy libraries (`chalk`, `ora`, `@inquirer/prompts`, `yargs`, `dotenv`, `clipboardy`, `@napi-rs/keyring`) are
|
|
41
|
+
optional peer dependencies: a CLI installs the ones for the subpaths it imports.
|
|
42
|
+
|
|
43
|
+
`createProgress(print, enabled?, listen?)` takes an optional `listen` hook that can change the spinner's text while a step
|
|
44
|
+
runs, for instance to say that a request is being sent again; the Holocron CLI uses it for its retry notice.
|
package/dist/flags.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { t as ParsedTokenArgs } from "./token-args.type-arNIDKsQ.mjs";
|
|
2
|
+
import { Argv, CommandModule } from "yargs";
|
|
3
|
+
/** The options {@link addGlobalOptions} declares, as a command handler receives them. */
|
|
4
|
+
interface GlobalArgs {
|
|
5
|
+
/** `--dry-run`: print what would change instead of changing it. */
|
|
6
|
+
dryRun: boolean;
|
|
7
|
+
/** `--token`, repeated: a bare value, or `vendor=value`; see `parseTokenArgs`. */
|
|
8
|
+
token?: string[];
|
|
9
|
+
/** `--cwd`: the directory the command works from. */
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
/** What {@link addGlobalOptions} can be told. */
|
|
13
|
+
interface GlobalOptionsOptions {
|
|
14
|
+
/** The help text of each option, where the tool's meaning differs from the general one. */
|
|
15
|
+
describe?: Partial<Record<"dryRun" | "token" | "cwd", string>>;
|
|
16
|
+
/** The default for `--cwd`. Default `process.cwd()`, read when the options are declared. */
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Declares the options every command of a tool takes, once, on the root parser: `--dry-run`, a repeatable `--token`
|
|
21
|
+
* (a bare value, or `vendor=value`, parsed by `parseTokenArgs`) and `--cwd`. A tool adds its own to the same parser;
|
|
22
|
+
* a command module's arguments extend {@link GlobalArgs}.
|
|
23
|
+
*/
|
|
24
|
+
export declare function addGlobalOptions(parser: Argv, options?: GlobalOptionsOptions): Argv;
|
|
25
|
+
/**
|
|
26
|
+
* The hidden default command (`$0`) of a command group, so that typing just the group (`tool repo`) does something
|
|
27
|
+
* useful: `open` is called with the group's name and the parsed arguments, usually to open a menu of its subcommands.
|
|
28
|
+
*/
|
|
29
|
+
export declare const groupMenu: (parent: string, open: (parent: string, argv: Record<string, unknown>) => Promise<void>) => CommandModule<never, never>;
|
|
30
|
+
/** The arguments a command's `*.args.ts` builder produces: what its handler receives, besides the global options. */
|
|
31
|
+
type InferArgs<Builder> = Builder extends ((...args: never[]) => Argv<infer Args>) ? Args : never;
|
|
32
|
+
/**
|
|
33
|
+
* Parses the values of a repeatable `--token` flag. An empty or missing flag is `{}`. A value that does not parse is
|
|
34
|
+
* reported through `report` (as `--token: <why>`), sets `process.exitCode` to 1 and gives `null`, so the command can stop
|
|
35
|
+
* without a stack trace; any other error is thrown.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tokenContext(rawTokens: string[] | undefined, report: (message: string) => void): ParsedTokenArgs | null;
|
|
38
|
+
export type { GlobalArgs, GlobalOptionsOptions, InferArgs };
|
package/dist/flags.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { n as parseTokenArgs, t as TokenParseError } from "./token-args-BgrCq_kK.mjs";
|
|
2
|
+
// src/flags/global-options/global-options.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declares the options every command of a tool takes, once, on the root parser: `--dry-run`, a repeatable `--token`
|
|
5
|
+
* (a bare value, or `vendor=value`, parsed by `parseTokenArgs`) and `--cwd`. A tool adds its own to the same parser;
|
|
6
|
+
* a command module's arguments extend {@link GlobalArgs}.
|
|
7
|
+
*/
|
|
8
|
+
function addGlobalOptions(parser, options = {}) {
|
|
9
|
+
const describe = options.describe ?? {};
|
|
10
|
+
return parser.option("dry-run", {
|
|
11
|
+
type: "boolean",
|
|
12
|
+
default: false,
|
|
13
|
+
describe: describe.dryRun ?? "Print what would change without changing it. Read-only commands ignore it."
|
|
14
|
+
}).option("token", {
|
|
15
|
+
type: "string",
|
|
16
|
+
array: true,
|
|
17
|
+
describe: describe.token ?? "Token override. Bare form: --token <value>. Keyed form: --token vendor=value (repeat for each vendor)."
|
|
18
|
+
}).option("cwd", {
|
|
19
|
+
type: "string",
|
|
20
|
+
default: options.cwd ?? process.cwd(),
|
|
21
|
+
describe: describe.cwd ?? "The directory to work from"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// src/flags/group-menu/group-menu.ts
|
|
25
|
+
/**
|
|
26
|
+
* The hidden default command (`$0`) of a command group, so that typing just the group (`tool repo`) does something
|
|
27
|
+
* useful: `open` is called with the group's name and the parsed arguments, usually to open a menu of its subcommands.
|
|
28
|
+
*/
|
|
29
|
+
const groupMenu = (parent, open) => ({
|
|
30
|
+
command: "$0",
|
|
31
|
+
describe: false,
|
|
32
|
+
builder: (y) => y,
|
|
33
|
+
handler: async (argv) => {
|
|
34
|
+
await open(parent, argv);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// src/flags/token-context/token-context.ts
|
|
38
|
+
/**
|
|
39
|
+
* Parses the values of a repeatable `--token` flag. An empty or missing flag is `{}`. A value that does not parse is
|
|
40
|
+
* reported through `report` (as `--token: <why>`), sets `process.exitCode` to 1 and gives `null`, so the command can stop
|
|
41
|
+
* without a stack trace; any other error is thrown.
|
|
42
|
+
*/
|
|
43
|
+
function tokenContext(rawTokens, report) {
|
|
44
|
+
if (!rawTokens?.length) return {};
|
|
45
|
+
try {
|
|
46
|
+
return parseTokenArgs(rawTokens);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err instanceof TokenParseError) {
|
|
49
|
+
report(`--token: ${err.message}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export { addGlobalOptions, groupMenu, tokenContext };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/ui/non-interactive-error/non-interactive-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Raised when a prompt is needed but stdin isn't a TTY. A non-TTY run (CI, a
|
|
4
|
+
* script, a pipe) throws this instead of hanging on `@inquirer/prompts`, which
|
|
5
|
+
* otherwise throws its own `ExitPromptError` the moment stdin closes. `cli.ts`'s
|
|
6
|
+
* `USER_FACING_ERRORS` catch prints the message cleanly.
|
|
7
|
+
*/
|
|
8
|
+
var NonInteractiveError = class extends Error {
|
|
9
|
+
/** The error's name, `NonInteractiveError`, so a catch can tell it from any other error. */
|
|
10
|
+
name = "NonInteractiveError";
|
|
11
|
+
};
|
|
12
|
+
export { NonInteractiveError as t };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** What the notifier remembers between runs, so the registry is asked once a day. */
|
|
2
|
+
interface CacheEntry {
|
|
3
|
+
/** The newest version the registry named on this run's channel. */
|
|
4
|
+
latestVersion: string;
|
|
5
|
+
/** When it was asked, in milliseconds since the epoch. */
|
|
6
|
+
checkedAt: number;
|
|
7
|
+
}
|
|
8
|
+
/** What {@link checkForUpdates} needs to look for a newer version of a package. */
|
|
9
|
+
interface UpdateCheckOptions {
|
|
10
|
+
/** The package on npm to look up, and to name in the notice. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The version that is running now. A prerelease is compared on its own channel. */
|
|
13
|
+
currentVersion: string;
|
|
14
|
+
/** The directory the answer is kept in, created when it is first written. One per tool. */
|
|
15
|
+
cacheDir: string;
|
|
16
|
+
/** The variables that switch the notice off (`CI`, `NO_UPDATE_NOTIFIER`). Default `process.env`. */
|
|
17
|
+
env?: Record<string, string | undefined>;
|
|
18
|
+
/** How the notice tells people to update. Default `npm install -g <name>`. */
|
|
19
|
+
installCommand?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether `latest` is newer than `current`: release parts compare as numbers, a stable release is newer than any
|
|
23
|
+
* prerelease of it, and prereleases compare segment by segment. A leading `v` is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isUpdateAvailable(current: string, latest: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Looks for a newer version of a package on npm, at most once a day, and returns a function that prints a boxed notice to
|
|
28
|
+
* stderr, or `null` when there is nothing to say. It is best-effort: a network error, a bad answer or an unwritable cache
|
|
29
|
+
* is `null`, never a thrown error. It says nothing in CI or when `NO_UPDATE_NOTIFIER` is set. A prerelease is compared
|
|
30
|
+
* with the newest version on its own channel (`2.0.0-alpha.3` with the `alpha` tag).
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkForUpdates(options: UpdateCheckOptions): Promise<(() => void) | null>;
|
|
33
|
+
export type { CacheEntry, UpdateCheckOptions };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
// src/notifier/update-notifier/update-notifier.ts
|
|
5
|
+
const CACHE_FILE = "update-check.json";
|
|
6
|
+
const CACHE_TTL_MS = 864e5;
|
|
7
|
+
const FETCH_TIMEOUT_MS = 3e3;
|
|
8
|
+
function readCache(cacheDir) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(join(cacheDir, CACHE_FILE), "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeCache(cacheDir, entry) {
|
|
16
|
+
try {
|
|
17
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
18
|
+
writeFileSync(join(cacheDir, CACHE_FILE), JSON.stringify(entry));
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
async function fetchLatestVersion(name, channel) {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
24
|
+
if (!res.ok) return null;
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
return data["dist-tags"][channel] ?? data["dist-tags"]["latest"] ?? null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getChannel(version) {
|
|
32
|
+
return /^[^-]+-([a-zA-Z]+)/.exec(version)?.[1] ?? "latest";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether `latest` is newer than `current`: release parts compare as numbers, a stable release is newer than any
|
|
36
|
+
* prerelease of it, and prereleases compare segment by segment. A leading `v` is ignored.
|
|
37
|
+
*/
|
|
38
|
+
function isUpdateAvailable(current, latest) {
|
|
39
|
+
const normalize = (v) => v.replace(/^v/, "");
|
|
40
|
+
const c = normalize(current);
|
|
41
|
+
const l = normalize(latest);
|
|
42
|
+
if (c === l) return false;
|
|
43
|
+
const splitPre = (v) => {
|
|
44
|
+
const idx = v.indexOf("-");
|
|
45
|
+
return idx === -1 ? [v, ""] : [v.slice(0, idx), v.slice(idx + 1)];
|
|
46
|
+
};
|
|
47
|
+
const [cRelease, cPre] = splitPre(c);
|
|
48
|
+
const [lRelease, lPre] = splitPre(l);
|
|
49
|
+
const parseRelease = (r) => r.split(".").map(Number);
|
|
50
|
+
const cParts = parseRelease(cRelease);
|
|
51
|
+
const lParts = parseRelease(lRelease);
|
|
52
|
+
for (let i = 0; i < Math.max(cParts.length, lParts.length); i++) {
|
|
53
|
+
const cv = cParts[i] ?? 0;
|
|
54
|
+
const lv = lParts[i] ?? 0;
|
|
55
|
+
if (lv > cv) return true;
|
|
56
|
+
if (lv < cv) return false;
|
|
57
|
+
}
|
|
58
|
+
if (!lPre && cPre) return true;
|
|
59
|
+
if (lPre && !cPre) return false;
|
|
60
|
+
const cPreParts = cPre.split(".");
|
|
61
|
+
const lPreParts = lPre.split(".");
|
|
62
|
+
for (let i = 0; i < Math.max(cPreParts.length, lPreParts.length); i++) {
|
|
63
|
+
const cv = cPreParts[i] ?? "";
|
|
64
|
+
const lv = lPreParts[i] ?? "";
|
|
65
|
+
const cvNum = Number(cv);
|
|
66
|
+
const lvNum = Number(lv);
|
|
67
|
+
if (!isNaN(cvNum) && !isNaN(lvNum)) {
|
|
68
|
+
if (lvNum > cvNum) return true;
|
|
69
|
+
if (lvNum < cvNum) return false;
|
|
70
|
+
} else {
|
|
71
|
+
if (lv > cv) return true;
|
|
72
|
+
if (lv < cv) return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function formatNotice(current, latest, installCmd) {
|
|
78
|
+
const raw1 = `Update available: ${current} → ${latest}`;
|
|
79
|
+
const raw2 = `Run ${installCmd} to update`;
|
|
80
|
+
const width = Math.max(raw1.length, raw2.length) + 4;
|
|
81
|
+
const bar = chalk.yellow("─".repeat(width));
|
|
82
|
+
const border = chalk.yellow("│");
|
|
83
|
+
const pad = (raw, styled) => `${border} ${styled}${" ".repeat(width - 2 - raw.length)} ${border}`;
|
|
84
|
+
return [
|
|
85
|
+
"",
|
|
86
|
+
chalk.yellow(`╭${bar}╮`),
|
|
87
|
+
pad(raw1, `Update available: ${chalk.dim(current)} → ${chalk.green(latest)}`),
|
|
88
|
+
pad(raw2, `Run ${chalk.cyan(installCmd)} to update`),
|
|
89
|
+
chalk.yellow(`╰${bar}╯`),
|
|
90
|
+
""
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Looks for a newer version of a package on npm, at most once a day, and returns a function that prints a boxed notice to
|
|
95
|
+
* stderr, or `null` when there is nothing to say. It is best-effort: a network error, a bad answer or an unwritable cache
|
|
96
|
+
* is `null`, never a thrown error. It says nothing in CI or when `NO_UPDATE_NOTIFIER` is set. A prerelease is compared
|
|
97
|
+
* with the newest version on its own channel (`2.0.0-alpha.3` with the `alpha` tag).
|
|
98
|
+
*/
|
|
99
|
+
async function checkForUpdates(options) {
|
|
100
|
+
const { name, currentVersion, cacheDir } = options;
|
|
101
|
+
const env = options.env ?? process.env;
|
|
102
|
+
if (env["CI"] || env["NO_UPDATE_NOTIFIER"]) return null;
|
|
103
|
+
const channel = getChannel(currentVersion);
|
|
104
|
+
const cache = readCache(cacheDir);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
let latestVersion = null;
|
|
107
|
+
if (cache && now - cache.checkedAt < CACHE_TTL_MS) latestVersion = cache.latestVersion;
|
|
108
|
+
else {
|
|
109
|
+
latestVersion = await fetchLatestVersion(name, channel);
|
|
110
|
+
if (latestVersion) writeCache(cacheDir, {
|
|
111
|
+
latestVersion,
|
|
112
|
+
checkedAt: now
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (!latestVersion || !isUpdateAvailable(currentVersion, latestVersion)) return null;
|
|
116
|
+
const installCommand = options.installCommand ?? `npm install -g ${name}`;
|
|
117
|
+
return () => {
|
|
118
|
+
process.stderr.write(formatNotice(currentVersion, latestVersion, installCommand) + "\n");
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export { checkForUpdates, isUpdateAvailable };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** A row in one level of a nested menu: a command to run, a group to open, or "back". */
|
|
2
|
+
interface MenuItem {
|
|
3
|
+
/** The label shown in the picker. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** What the picker resolves to: a command name, `group:<path>` to open a submenu, or `back`. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** One line of help shown beside the label. */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Lower-cased text the search box matches against (a group includes everything under it). */
|
|
10
|
+
haystack: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Searchable autocomplete over one menu level's `items`. `select()` would work too
|
|
14
|
+
* at these list sizes, but `search()` degrades gracefully as the surface grows and
|
|
15
|
+
* costs nothing when it doesn't. Resolves to the picked item's `value`.
|
|
16
|
+
*/
|
|
17
|
+
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 };
|