Crust logoCrust

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/prompts

Quick 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-app

Injecting 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 required

A 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); // => 2

runPrompt() 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, or renderSubmitted rejects the promise after the terminal is restored.
  • handleTextEdit() provides the built-ins' cursor editing and returns null for keys it does not handle. The root also exports the fuzzy matching, list rendering, formatting, and glyph helpers the built-ins use; see index.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.

On this page