@hyperfrontend/questions
Terminal prompting library with composable, functional API for text, select, confirm, and multiselect prompts
What is @hyperfrontend/questions?
A terminal prompting library built on functional programming principles. Create interactive CLI experiences with composable, type-safe prompts that return structured outcomes.
Key Features
- Pure Functions: Every prompt is a pure function returning
Promise<PromptOutcome<T>>, making results predictable and easily testable - Composable API: Build complex interactive flows by combining simple prompt functions
- Type-Safe: Full TypeScript support with discriminated unions for prompt outcomes
- Zero External Dependencies: Uses only Node.js built-ins and
@hyperfrontendutilities - Searchable Multiselect: Type-to-filter functionality for large option lists
- Clipboard Paste: Bracketed paste mode on TTYs (with a multi-character-chunk fallback elsewhere); pasted text is sanitized and never auto-submits
- Resize-Aware Rendering: Prompts hard-wrap to the terminal width and repaint on resize, preserving value, cursor, selection, and validation state
Architecture Highlights
Each prompt follows a functional state machine pattern:
- Immutable State: All prompt state is frozen; updates create new state objects
- Explicit Outcomes: Prompts return either
{ result: 'submitted', value: T }or{ result: 'cancelled', value: undefined } - Terminal Abstraction: Low-level I/O is encapsulated in a
Terminalinterface for testability - Token Input Stream: Raw input is tokenized into keys, pastes, and resize notifications by a persistent listener, with raw mode held for the whole prompt session and restored on close
- Frame Renderer: A width-aware screen helper erases and repaints exact frames; on resize it recomputes the previous frame's height at the new width (assumes a reflowing terminal; display width is code-point based, east-asian double width out of scope)
Why Use @hyperfrontend/questions?
When building CLI tools, you need interactive prompts that are:
- Predictable: Know exactly what a prompt returns, always
- Composable: Chain prompts without callback hell
- Cancellable: Handle Ctrl+C gracefully with structured cancellation
- Lightweight: No large dependency trees for simple prompts
This library provides all four while staying true to functional programming principles.
Installation
npm install @hyperfrontend/questions
Quick Start
import { text, confirm, select, multiselect, PromptResult } from '@hyperfrontend/questions'
// Text input
const nameResult = await text({
message: 'What is your name?',
validate: (value) => (value.length < 2 ? 'Name too short' : undefined),
})
if (nameResult.result === PromptResult.Submitted) {
console.log(`Hello, ${nameResult.value}!`)
}
// Text input with a live label — `renderMessage` is recomputed on every keystroke
import { style } from '@hyperfrontend/questions'
await text({
message: 'Title:',
renderMessage: (value) => {
const left = 72 - value.length
return `Title (${left >= 0 ? style.green(`${left} left`) : style.red(`${-left} over`)}):`
},
})
// Confirmation
const continueResult = await confirm({
message: 'Continue?',
initial: true,
})
// Single select
const colorResult = await select({
message: 'Pick a color:',
choices: [
{ label: 'Red', value: 'red' },
{ label: 'Green', value: 'green', hint: 'recommended' },
{ label: 'Blue', value: 'blue' },
],
})
// Multiselect with search
const featuresResult = await multiselect({
message: 'Select features:',
choices: [
{ label: 'TypeScript', value: 'ts' },
{ label: 'ESLint', value: 'eslint' },
{ label: 'Prettier', value: 'prettier' },
],
searchable: true,
min: 1,
})
API Overview
| Function | Description |
|---|---|
text | Free-form text input with optional validation and live-updating labels |
confirm | Yes/no confirmation prompt |
select | Single selection from a list of choices |
multiselect | Multiple selections with optional search |
style | ANSI colour helpers (green, yellow, red, cyan, bold, dim, gray) |
PromptResult | Discriminated union: 'submitted' | 'cancelled' |
All prompts return Promise<PromptOutcome<T>> where:
type PromptOutcome<T> = { result: 'submitted'; value: T } | { result: 'cancelled'; value: undefined }
Compatibility
| Environment | Supported |
|---|---|
| Node.js >= 18 | ✅ |
| TTY Terminal | ✅ |
| Bracketed paste (TTY) | ✅ |
| Resize redraw (SIGWINCH) | ✅ |
| Non-TTY streams (tests, pipes) | ✅ |
| Tree Shakeable | ✅ |
On TTY inputs a prompt session enables bracketed paste mode (ESC[?2004h) and restores it on close; terminals without bracketed paste still paste correctly because multi-character input chunks are treated as pastes. Single-line prompts collapse pasted newlines into spaces, so pasting can never submit a value.
License
Guides & tutorials for @hyperfrontend/questions
Build a setup wizard for your CLI
My CLI takes its setup from a wall of flags nobody remembers, and every attempt at an interactive version turns into raw-mode handling, signal handlers, and a validation branch after every question.
How to test interactive prompts without a terminal
My CLI's prompts are the only part of it with no tests, because a test runner has no TTY to give them and driving raw keystrokes from a spawned process is more machinery than the code being tested.
API Reference§
ƒFunctions
Pure functional prompt that asks a yes/no question and returns a boolean. Supports default values and responds to y/Y/n/N keys. A pasted
y/yes/n/no (trimmed, case-insensitive) is accepted; any other paste is ignored. The prompt repaints on terminal resize.Parameters
| Name | Type | Description |
|---|---|---|
§config | ConfirmConfig | Confirm prompt configuration |
Returns
Promise<PromptOutcome<boolean>>Examples
Basic confirmation
const outcome = await confirm({ message: 'Continue?' })
if (outcome.result === 'submitted' && outcome.value) {
console.log('Proceeding...')
}With default value
const outcome = await confirm({
message: 'Enable feature?',
initial: true, // Default to yes
})Pure functional prompt with arrow key navigation, space to toggle, scrolling support, min/max constraints, and optional type-to-filter search. In searchable mode, pasted text appends its first line to the filter query; pasting never toggles or submits. The prompt repaints on terminal resize, preserving cursor, selection, and scroll state.
Parameters
| Name | Type | Description |
|---|---|---|
§config | MultiselectConfig<T> | Multiselect prompt configuration |
Returns
Promise<PromptOutcome<unknown>>Examples
Basic multiselect
const outcome = await multiselect({
message: 'Select toppings:',
choices: [
{ label: 'Cheese', value: 'cheese' },
{ label: 'Pepperoni', value: 'pepperoni' },
{ label: 'Mushrooms', value: 'mushrooms' },
],
})
if (outcome.result === 'submitted') {
console.log(`You selected: ${outcome.value.join(', ')}`)
}With search and constraints
const outcome = await multiselect({
message: 'Select features:',
choices: features.map((f) => ({ label: f.name, value: f.id })),
searchable: true,
min: 1,
max: 5,
})Pre-selected values
const outcome = await multiselect({
message: 'Select permissions:',
choices: permissions,
initial: [0, 2], // First and third choices pre-selected
})Pure functional prompt with arrow key navigation, scrolling support, optional disabled choices, and optional type-to-filter search. In searchable mode, pasted text appends its first line to the filter query. The prompt repaints on terminal resize, preserving cursor and scroll state.
Parameters
| Name | Type | Description |
|---|---|---|
§config | SelectConfig<T> | Select prompt configuration |
Returns
Promise<PromptOutcome<T>>Examples
Basic select
const outcome = await select({
message: 'Choose a color:',
choices: [
{ label: 'Red', value: 'red' },
{ label: 'Green', value: 'green' },
{ label: 'Blue', value: 'blue' },
],
})
if (outcome.result === 'submitted') {
console.log(`You chose: ${outcome.value}`)
}With hints and disabled options
const outcome = await select({
message: 'Select plan:',
choices: [
{ label: 'Free', value: 'free', hint: '$0/month' },
{ label: 'Pro', value: 'pro', hint: '$10/month' },
{ label: 'Enterprise', value: 'enterprise', disabled: true },
],
initial: 1, // Start on Pro
})With search
const outcome = await select({
message: 'Pick a project:',
choices: projects.map((p) => ({ label: p.name, value: p.id })),
searchable: true,
})Pure functional prompt that reads text from the user with support for default values, input validation, and display formatting. Pasted text is sanitized (newlines collapse to spaces, control characters are removed) and inserted at the cursor without ever auto-submitting. The prompt repaints on terminal resize, preserving value, cursor, and any validation error.
Parameters
| Name | Type | Description |
|---|---|---|
§config | TextConfig | Text prompt configuration |
Returns
Promise<PromptOutcome<string>>Examples
Basic text input
const outcome = await text({ message: 'What is your name?' })
if (outcome.result === 'submitted') {
console.log(`Hello, ${outcome.value}!`)
}With validation
const outcome = await text({
message: 'Enter email:',
validate: (value) => {
if (!value.includes('@')) return 'Must be a valid email'
return undefined
},
})Password input with masking
const outcome = await text({
message: 'Password:',
format: (value) => '*'.repeat(value.length),
})◈Interfaces
Properties
Properties
Properties
Properties
Properties
Properties
Properties
◆Types
type PromptFunction = (config: TConfig) => Promise<PromptOutcome<TValue>>type PromptOutcome = PromptSubmittedOutcome<T> | PromptCancelledOutcometype PromptResult = indexedAccess