Arguments & flags
Attach a prompt to a flag or argument with withFallbackPrompt so it only
runs when the value is missing.
Read more
The Prompt module (effect/unstable/cli) builds interactive terminal
questions: text fields, password masks, confirmations, single- and multi-select
menus, autocomplete, numbers, dates, and file pickers. A prompt renders frames,
reads keyboard input, validates the response, and produces a value.
The key idea is that a Prompt<A> is an Effect:
interface Prompt<Output> extends Effect.Effect<Output, Terminal.QuitError, Prompt.Environment> {}So you run a prompt by yield*-ing it inside any handler, exactly like any
other effect. It may fail with Terminal.QuitError (the user pressed Ctrl-C or
quit), and it requires the prompt Environment — Terminal, FileSystem, and
Path — which NodeServices.layer provides.
The typical use is a command handler that asks for input it does not already have on the command line, then uses the result:
import { NodeRuntime, NodeServices } from "@effect/platform-node"import { Console, Effect } from "effect"import { Command, Prompt } from "effect/unstable/cli"
const init = Command.make("init", {}, () => Effect.gen(function*() { // A prompt is just an Effect — yield* it to get its value. const name = yield* Prompt.text({ message: "Project name:" })
const private_ = yield* Prompt.confirm({ message: "Make the repository private?", initial: false })
yield* Console.log( `Creating ${private_ ? "private" : "public"} project "${name}"…` ) }))
init.pipe( Command.run({ version: "1.0.0" }), // NodeServices.layer provides Terminal + FileSystem + Path, which every // prompt needs. Effect.provide(NodeServices.layer), NodeRuntime.runMain)Instead of writing the prompt by hand in the handler, you can attach it to a
flag or argument with withFallbackPrompt: if the value is missing on the
command line, the prompt runs to fill it in. This keeps the handler unaware of
where the value came from. See Arguments & flags.
import { Flag, Prompt } from "effect/unstable/cli"
// `--name` if provided, otherwise prompt for it.const name = Flag.string("name").pipe( Flag.withFallbackPrompt(Prompt.text({ message: "Project name:" })))The fallback only triggers for missing required input — an invalid value still fails normally, and cancelling the prompt re-fails with the original missing error.
Any prompt is an effect, so you can also run it directly (Prompt.run is the
explicit form, but yield* works because prompts are Effects):
import { Effect } from "effect"import { Prompt } from "effect/unstable/cli"
const program = Effect.gen(function*() { const answer = yield* Prompt.text({ message: "Your name:" }) // ...use answer})
// equivalent explicit executionconst program2 = Prompt.run(Prompt.text({ message: "Your name:" }))Every constructor below returns a Prompt<A>. Run them with yield*.
A single-line text field that echoes input. TextOptions: message,
default?, and an effectful validate? that returns the cleaned value or fails
with a string error message shown to the user.
import { Effect } from "effect"import { Prompt } from "effect/unstable/cli"
const slug = Prompt.text({ message: "Package slug:", default: "my-app", validate: (value) => /^[a-z0-9-]+$/.test(value) ? Effect.succeed(value) : Effect.fail("Use only lowercase letters, digits, and dashes")})// => Prompt<string>A text field that masks typed characters and returns the value wrapped in
Redacted, so the secret is not accidentally logged. Same TextOptions as
Prompt.text.
import { Prompt } from "effect/unstable/cli"import { Redacted } from "effect"
const secret = Prompt.password({ message: "API token:" })// => Prompt<Redacted>
// unwrap only at the boundary that needs the secret:// const raw = Redacted.value(yield* secret)Like Prompt.password but shows nothing at all as you type (no mask
characters). Also returns a Redacted.
import { Prompt } from "effect/unstable/cli"
const passphrase = Prompt.hidden({ message: "Passphrase:" })// => Prompt<Redacted>A yes/no question submitted with a single keypress. ConfirmOptions: message,
initial? (defaults to false), label? (the { confirm, deny } text shown
after answering — defaults "yes" / "no"), and placeholder?
({ defaultConfirm, defaultDeny }, defaults "(Y/n)" / "(y/N)").
import { Prompt } from "effect/unstable/cli"
const overwrite = Prompt.confirm({ message: "File exists. Overwrite?", initial: false, label: { confirm: "overwrite", deny: "keep" }})// => Prompt<boolean>An interactive on/off switch you flip with the arrow keys, then submit with
Enter — unlike confirm, which submits immediately. ToggleOptions: message,
initial? (false), active? ("on"), inactive? ("off").
import { Prompt } from "effect/unstable/cli"
const analytics = Prompt.toggle({ message: "Telemetry:", initial: true, active: "enabled", inactive: "disabled"})// => Prompt<boolean>Pick one value from a list. SelectOptions<A>: message, choices
(SelectChoice<A>[]), and maxPerPage? (defaults to 10). Each
SelectChoice<A> has { title, value, description?, disabled?, selected? };
at most one choice may set selected: true as the default highlight.
import { Prompt } from "effect/unstable/cli"
const license = Prompt.select({ message: "Choose a license:", choices: [ { title: "MIT", value: "mit", description: "Permissive", selected: true }, { title: "Apache 2.0", value: "apache-2.0" }, { title: "GPL 3.0", value: "gpl-3.0" }, { title: "Proprietary", value: "none", disabled: true } ]})// => Prompt<"mit" | "apache-2.0" | "gpl-3.0" | "none">Pick several values; returns them as an array. Takes SelectOptions<A> plus
MultiSelectOptions: selectAll? / selectNone? / inverseSelection? (bulk
command labels) and min? / max? selection counts. Mark defaults with
selected: true on individual choices.
import { Prompt } from "effect/unstable/cli"
const features = Prompt.multiSelect({ message: "Enable features:", min: 1, choices: [ { title: "TypeScript", value: "ts", selected: true }, { title: "ESLint", value: "eslint" }, { title: "Prettier", value: "prettier" } ]})// => Prompt<Array<"ts" | "eslint" | "prettier">>A select menu the user can filter by typing. Extends SelectOptions<A> with
filterLabel? ("filter"), filterPlaceholder? ("type to filter"), and
emptyMessage? ("No matches").
import { Prompt } from "effect/unstable/cli"
const language = Prompt.autoComplete({ message: "Primary language:", filterPlaceholder: "start typing…", choices: [ { title: "TypeScript", value: "ts" }, { title: "Rust", value: "rs" }, { title: "Kotlin", value: "kt" } ]})// => Prompt<"ts" | "rs" | "kt">Whole-number entry with arrow-key stepping. IntegerOptions: message,
default? (0), min? / max? bounds, incrementBy? / decrementBy? (arrow
step, default 1), and an effectful validate?. Values outside min/max are
rejected automatically.
import { Prompt } from "effect/unstable/cli"
const port = Prompt.integer({ message: "Port:", default: 3000, min: 1, max: 65535})// => Prompt<number>Decimal entry. Extends IntegerOptions with precision? (number of decimal
places, default 2).
import { Prompt } from "effect/unstable/cli"
const ratio = Prompt.float({ message: "Sample rate (0–1):", default: 0.5, min: 0, max: 1, precision: 3})// => Prompt<number>Edit a formatted date/time field part by part. DateOptions: message,
initial? (defaults to the current Date), dateMask? (defaults to
"YYYY-MM-DD HH:mm:ss"), effectful validate?, and locales? for custom month
and weekday names.
import { Prompt } from "effect/unstable/cli"
const when = Prompt.date({ message: "Release date:", dateMask: "YYYY-MM-DD"})// => Prompt<Date>A text field whose submitted value is split into an array of strings. Extends
TextOptions with delimiter? (defaults to ",").
import { Prompt } from "effect/unstable/cli"
const tags = Prompt.list({ message: "Tags (comma-separated):", delimiter: ","})// => Prompt<Array<string>> e.g. "a, b" => ["a", " b"]A file-system browser that returns the selected path. FileOptions (all
optional): type? ("file" | "directory" | "either", default "file"),
message? ("Choose a file"), startingPath? (default: cwd), default?,
maxPerPage? (10), and filter? — a predicate or effect that keeps an entry
when it returns true.
import { Prompt } from "effect/unstable/cli"
const config = Prompt.file({ message: "Pick a config file:", type: "file", filter: (file) => file.endsWith(".json")})// => Prompt<string> e.g. "/home/me/project/tsconfig.json"Because prompts compose, you can transform results, chain dependent prompts, and collect several at once.
Transform the output value of a prompt.
import { Prompt } from "effect/unstable/cli"
const upper = Prompt.text({ message: "Name:" }).pipe( Prompt.map((name) => name.toUpperCase()))// => Prompt<string>Use one prompt’s output to build the next one — a wizard where later questions depend on earlier answers.
import { Prompt } from "effect/unstable/cli"
const driver = Prompt.select({ message: "Database:", choices: [ { title: "PostgreSQL", value: "pg" as const }, { title: "SQLite", value: "sqlite" as const } ]}).pipe( Prompt.flatMap((db) => db === "pg" ? Prompt.text({ message: "Connection URL:" }) : Prompt.file({ message: "Database file:", type: "file" }) ))// => Prompt<string>Run several prompts in sequence and collect their results, preserving the input shape — a tuple or a struct.
import { Prompt } from "effect/unstable/cli"
// As a struct -> Prompt<{ username: string; password: Redacted }>const credentials = Prompt.all({ username: Prompt.text({ message: "Username:" }), password: Prompt.password({ message: "Password:" })})
// As a tuple -> Prompt<[string, boolean]>const pair = Prompt.all([ Prompt.text({ message: "Name:" }), Prompt.confirm({ message: "Continue?" })])A constant prompt that immediately yields a value without touching the terminal.
Useful as a branch in flatMap or Prompt.all when no input is needed.
import { Prompt } from "effect/unstable/cli"
const noop = Prompt.succeed(42)// => Prompt<number>, resolves to 42 with no renderingExecute a prompt, producing Effect<Output, Terminal.QuitError, Environment>.
Equivalent to yield*-ing the prompt; use it when you want the effect
explicitly (e.g. to pass it somewhere expecting an Effect).
import { Prompt } from "effect/unstable/cli"
const program = Prompt.run(Prompt.text({ message: "Name:" }))// => Effect<string, Terminal.QuitError, Prompt.Environment>Type guard that returns true if a value is a Prompt.
import { Prompt } from "effect/unstable/cli"
Prompt.isPrompt(Prompt.text({ message: "x" })) // => truePrompt.isPrompt(42) // => falseBuild a bespoke prompt from an initial state (a value or an effect computing it)
and a set of Handlers<State, Output>. The prompt runs as a render loop:
render(state, action) returns ANSI output for the current frame, the terminal
reads a Terminal.UserInput, process(input, state) returns the next
Prompt.Action, and clear(state, action) returns ANSI output that erases the
previous frame.
Action<State, Output> is a Data.TaggedEnum with three cases: Beep (reject
input with a bell), NextFrame({ state }) (advance to a new state), and
Submit({ value }) (finish with an output value).
import { Effect } from "effect"import { Prompt } from "effect/unstable/cli"
// Sketch: most fields render/clear ANSI strings and inspect input.key.name.declare const handlers: Prompt.Handlers<number, number>
const counter: Prompt.Prompt<number> = Prompt.custom(0, handlers)// => Prompt<number>Arguments & flags
Attach a prompt to a flag or argument with withFallbackPrompt so it only
runs when the value is missing.
Read more