@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
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
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 | ✅ |
| Tree Shakeable | ✅ |
License
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.
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.
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.
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.
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
readonly format?:(value: string) => string— readonly renderMessage?:(value: string) => string— message when presentreadonly validate?:(value: string) => string— ◆Types
type PromptFunction = (config: TConfig) => Promise<PromptOutcome<TValue>>type PromptOutcome = PromptSubmittedOutcome<T> | PromptCancelledOutcome●Variables
PromptResultReadonly...styleReadonly...