Prompts
Interactive input, password, confirm, select, and filter prompts for the terminal
@crustjs/prompts exports seven terminal prompts, shared themes, injected terminal IO, and renderer helpers. Prompt UI writes to stderr by default.
Install
npm install @crustjs/promptsQuick example
const name = await input({
message: "Project name?",
validate(value) {
if (!value.trim()) throw new Error("Name is required");
},
});
const runtime = await select({ message: "Runtime?", choices: ["bun", "node", "deno"] });
const features = await multiselect({
message: "Features?",
choices: ["lint", "test", "release"],
required: true,
});
const proceed = await confirm({ message: `Create ${name} for ${runtime}?`, default: true });
console.log({ features, proceed });Built-in prompts return the submitted value. input() and password() also accept a Standard Schema v1 schema or a throw-based validate callback, but not both.
┃ Project name?
crust-app│
✓ Project name? crust-appInjecting IO
declare const io: PromptIO;
await withTerminalIO(io, () => input({ message: "Name?" }));Each prompt accepts an optional second PromptIO argument with optional input and output. Resolution is explicit IO, ambient withTerminalIO() IO, then process.stdin and process.stderr.
withPromptIO() is an output-compatible alias. The ambient scope is shared with @crustjs/progress.
Prompts
All prompt functions accept (options, io?). Choice functions preserve literal choice value types when their choices are not widened. A plain-string choice is its own value, so Choice<T> only accepts plain strings that are themselves a T; non-string values such as numbers must use { label, value } objects.
input(options, io?)
const name = await input({ message: "Name?", default: "anonymous" });Single-line text input supports cursor editing, placeholder text, defaults, and validation.
Prop
Type
password(options, io?)
const token = await password({ message: "Token?", mask: "*" });Password input masks active text and shows a fixed-length mask after submission. Its default mask is *.
Prop
Type
confirm(options, io?)
const proceed = await confirm({ message: "Continue?", default: false });Confirm returns a boolean. Left, Right, and Tab toggle the value; y and h select true, while n and l select false.
Prop
Type
select(options, io?)
const runtime = await select({ message: "Runtime?", choices: ["bun", "node"] });Select returns one choice value. Object-valued defaults match by reference, so pass the same object used in choices.
Prop
Type
multiselect(options, io?)
const tools = await multiselect({ message: "Tools?", choices: ["lint", "test"], max: 2 });Multiselect returns selected values in choice order. required prevents an empty submission, and max limits toggle-all and invert operations.
Prop
Type
filter(options, io?)
const language = await filter({ message: "Language?", choices: ["TypeScript", "Rust"] });Filter fuzzy-matches typed text and returns one highlighted choice.
Prop
Type
multifilter(options, io?)
const features = await multifilter({ message: "Features?", choices: ["lint", "test"] });Multifilter combines fuzzy matching with multi-selection and returns values in original choice order.
Prop
Type
Validation
┃ Project name?
│
Name is requiredA validate callback throws to reject input, and its error message renders inline. A Standard Schema parses the raw string, returns its transformed output type, and renders the first issue or Validation failed.
Async validators and schemas are supported. initial and non-TTY default values also pass through a configured schema before being returned.
Themes
const prompts = createPrompts({ theme: { prefix: magenta, success: cyan } });
await prompts.input({ message: "Name?", initial: "Ada" });Instance overrides from createPrompts() sit between defaultTheme and per-prompt overrides. Each theme slot is a style function:
Prop
Type
Style capability behavior comes from @crustjs/style.
Non-interactive environments
# input({ message: "Name?", default: "ci-user" }) on a non-TTY
ci-user
# input({ message: "Name?" }) on a non-TTY
NonInteractiveError: Prompts require an interactive terminal (TTY).Every prompt returns initial without rendering when it is provided. On a non-TTY, all prompts except password() can return an explicitly supplied default; otherwise they reject with NonInteractiveError.
Ctrl+C rejects with a DOMException whose name is AbortError.
Custom prompts
import { type PromptIO, runPrompt, submit } from "@crustjs/prompts";
import { renderPrompt } from "@crustjs/prompts/testing";
function counter(options: { message: string }, promptIO?: PromptIO) {
return runPrompt(
{
initialState: 0,
render: (count) => `${options.message} ${count}`,
handleKey: (key, count) => (key.name === "return" ? submit(count) : count + 1),
},
promptIO,
);
}
const prompt = renderPrompt(counter, { message: "Presses:" });
prompt.type("ab");
prompt.keys("return");
console.log(await prompt.answer); // => 2runPrompt() owns rendering, input, submission, cancellation, and theme merging. Accept (options, io?) and forward the IO argument to runPrompt(), like the built-ins, so callers and tests can inject streams.
- Only one prompt may use a given input or output stream at a time.
- An error thrown by
render,handleKey, orrenderSubmittedrejects the promise after the terminal is restored. handleTextEdit()provides the built-ins' cursor editing and returnsnullfor keys it does not handle. The root also exports the fuzzy matching, list rendering, formatting, and glyph helpers the built-ins use; seeindex.ts.
Testing custom prompts
renderPrompt() from @crustjs/prompts/testing runs a prompt against a fake TTY, as in the example above. type() and keys() send input, screen() returns the latest ANSI-stripped frame, and answer settles after submission. The subpath also exports createPromptIO() and encodeKey() for lower-level harnesses.