first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# `prompt`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Thin, test-injectable wrappers around `@inquirer/prompts`' password prompt
for the two interactive flows every server-keys bin needs: a single
silent password entry (`promptForPassword`) and a confirm-twice new
password entry (`promptForNewPassword`). The wrappers add three things
upstream lacks: a structured `ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)`
on mismatch (instead of a recursive re-prompt that bypasses the bin's
top-level handler), a typed dependency-injection seam for the prompt
function (so tests can run without touching stdin/stdout), and explicit
threading of `input/output/signal` into inquirer's `context` so the
prompts compose with the bin's `AbortController` and with non-TTY test
runners.
The minimum password length is exported as `MIN_PASSWORD_LENGTH` so callers
and tests share the constant.
## Features
- `mask: "*"` non-echo entry — passwords never appear in the buffered
terminal.
- `validate: (input) => ...` rejects empty input and (on new-password)
enforces a minimum length, surfacing the message through inquirer's
re-prompt loop.
- `SERVER_KEYS.PASSWORD_MISMATCH` raised on confirmation mismatch — the
caller's `runFunction` wrapper translates it into a process-exit
banner.
- Injectable `passwordPrompt` for fully deterministic tests; injectable
`inputStream`, `outputStream`, and `signal` for partial stubs that
still drive the real inquirer prompt under a non-TTY harness.
## Usage: Basic
```ts
import {
promptForPassword,
promptForNewPassword,
} from "@suaveplan/server-keys/prompt/prompt";
const existing = await promptForPassword("Enter root vault password");
const fresh = await promptForNewPassword("Enter password for the new vault");
```
## Usage: Advanced
Inject a fake prompt for tests, then exercise both happy and mismatch
paths without any TTY:
```ts
import { PassThrough } from "node:stream";
import {
promptForNewPassword,
type PasswordPrompt,
} from "@suaveplan/server-keys/prompt/prompt";
const calls: string[] = [];
const responses = ["correcthorse", "correcthorse"];
const fake: PasswordPrompt = async (cfg) => {
calls.push(cfg.message);
return responses.shift() as string;
};
const pw = await promptForNewPassword("Enter password", {
passwordPrompt: fake,
});
expect(pw).toBe("correcthorse");
expect(calls).toEqual(["Enter password", "Re-enter password to confirm"]);
// Mismatch raises a typed error:
await expect(
promptForNewPassword("x", {
passwordPrompt: async () =>
(["abcdefgh", "ABCDEFGH"].shift() ?? "") as string,
}),
).rejects.toMatchObject({ code: "SERVER_KEYS.PASSWORD_MISMATCH" });
// Production with a SIGINT-bound AbortController:
const ac = new AbortController();
process.once("SIGINT", () => ac.abort());
const pw2 = await promptForPassword("Enter vault password", {
signal: ac.signal,
});
```
## API Reference
- `MIN_PASSWORD_LENGTH = 8` — new-password length floor.
- `PasswordPrompt` — function type matching `@inquirer/prompts.password`.
- `PromptOptions = { passwordPrompt?, inputStream?, outputStream?, signal? }`.
- `promptForPassword(message?, options?) => Promise<string>`.
- `promptForNewPassword(message?, options?) => Promise<string>` — throws
`ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)` on mismatch.
## Implementation Notes
The validators are kept in-prompt (instead of after the await) so
inquirer's re-prompt UX still works for empty / too-short input. The
mismatch check lives outside the prompt so the failure can be caught by
the top-level handler instead of trapping the user in an infinite re-ask
loop. `buildContext` only assigns keys that were actually provided so
inquirer's defaults apply for the rest, and returns `undefined` for the
empty case so the prompt can resolve its own stdin / stdout / signal
defaults.