Crust logoCrust

Prompts

Interactive input, password, confirm, select, multiselect, filter, and multifilter prompts.

@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.

Testing custom prompts

const prompt = renderPrompt(myPrompt, { message: "Name?" });
prompt.type("Ada");
prompt.keys("return");
expect(await prompt.answer).toBe("Ada");

Import renderPrompt from @crustjs/prompts/testing. Its screen() method returns the latest ANSI-stripped frame, while answer settles after submission.

The subpath also exports createPromptIO, encodeKey, and their key and harness types. Custom prompt functions passed to renderPrompt accept (options, io?), like built-in prompts.

Building custom prompts

import { runPrompt, submit, handleTextEdit, fuzzyFilter } from "@crustjs/prompts";

The root exports the renderer, submission, fuzzy matching, list rendering, line formatting, text editing, and shared glyph helpers used by the built-ins. Use these exports when a custom prompt should follow the same terminal behavior.

Prompts

import {
  input,
  password,
  confirm,
  select,
  multiselect,
  filter,
  multifilter,
} from "@crustjs/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" });

A PromptTheme contains prefix, message, placeholder, cursor, selected, unselected, error, success, hint, and filterMatch. Instance overrides from createPrompts() sit between defaultTheme and per-prompt overrides.

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

const result = await runPrompt({
  initialState: { count: 0 },
  render: (state) => `Count: ${state.count}`,
  handleKey: (key, state) => (key.name === "return" ? submit(state.count) : state),
});

runPrompt() owns rendering, input, submission, cancellation, and theme merging. Only one prompt may use a given input or output stream at a time. An error thrown by render, handleKey, or renderSubmitted rejects the returned promise after the terminal and stream reservations are restored. Pending key-handler results are ignored after cleanup, so they cannot write into a replacement prompt.

Use handleTextEdit() for shared cursor editing. It returns null for keys it does not handle.

Exports

import { input, runPrompt, createPrompts, defaultTheme } from "@crustjs/prompts";

The root includes all seven prompts, renderer and IO helpers, theme APIs, fuzzy and text-editing helpers, render-format helpers, glyphs, errors, and their public types. Testing helpers remain in @crustjs/prompts/testing.

See the source inventory in packages/prompts/src/index.ts.

Design

stdout: application data
stderr: prompt frames

The package depends on @crustjs/style for ANSI styling. It keeps prompt output off stdout by default and shares terminal IO with progress indicators.

On this page