enquirer
enquirer asks questions in a terminal. You hand it a question object with a type, a name, and a message, and it returns a promise resolving to an answers object keyed by name. Around twenty prompt types ship in the box: input, password, confirm, select, multiselect, autocomplete, form, list, numeral, scale, survey, snippet, sort, toggle, quiz, and more, each with its own options for validation, formatting, and default values. Two ways to call it exist side by side. The prompt() function takes one question or an array of them and runs them in sequence, which is what most scripts use. Or you import a prompt class such as Select directly and call run() on it, which gives you the instance, its events, and its styling hooks. Everything is CommonJS, there are two small dependencies, and it has no plugin system to learn.
A wide, quick, dependency-light prompt library that still works fine, but it has been frozen since 2023 with a large open issue backlog and TypeScript support that stops at the front door. Use it in a CommonJS script that needs its unusual prompt types; start new TypeScript CLIs on @inquirer/prompts or @clack/prompts.
Use it if
- You want a lot of prompt types from one small dependency: form, scale, survey, snippet, and sort are built in, where other libraries make you install or write them
- Your CLI asks a branching series of questions and you want skip and initial as functions of the answers collected so far, so later questions adapt to earlier ones
- You are staying on CommonJS. enquirer is plain require()-able with no ESM migration pending, which matters for older Node scripts and generators
- You want to subclass a prompt and override its render or its styles to build something custom without adopting a whole framework
- Maintenance matters to you. The last npm release was 2.4.1 in July 2023, the last commit to the default branch was June 2024, and there are roughly 169 open issues plus more PRs waiting. Nothing is broken, but nothing is being fixed either
- You write TypeScript and want typed prompts. The bundled index.d.ts types only the Enquirer class and prompt(); the individual prompt classes such as Select and MultiSelect are not typed at all, and the file uses export =, so you need esModuleInterop and a fair amount of any
- You need clean cancellation. Pressing Ctrl+C rejects the promise with the prompt's formatted error, which is usually an empty string, so a catch block reading error.message crashes on undefined and every script needs a guard
- You want a modern, actively developed prompt kit with per-prompt packages, better theming, and current accessibility work. @inquirer/prompts and @clack/prompts are both far more active
- Your CLI must run unattended. These prompts expect a TTY, so a script that reaches a prompt in CI or behind a pipe stalls instead of failing fast; you have to check process.stdin.isTTY and provide flags yourself
Setup reality
npm install enquirer and require it; there is no config, no theme file, and only two dependencies (ansi-colors and strip-ansi). The awkward parts are elsewhere. TypeScript users hit export = plus untyped prompt classes, so importing Select cleanly usually means a local declaration file or a require cast. Windows terminals need a reasonably modern console for the unicode pointers to render, and the older cmd.exe falls back to garbled glyphs. Every prompt needs an interactive stdin, so wrap prompt calls in a TTY check before they hang a CI job. And because the package predates the current Node.js releases, its engines field still says Node 8.6 or newer, which tells you how long the API has sat still.
Patterns
Ask one questionsingle-question
const { prompt } = require('enquirer');
const { username } = await prompt({
type: 'input',
name: 'username',
message: 'What is your username?'
});prompt() always resolves to an object keyed by name, never to the bare value. Destructuring at the call site is the usual way to avoid an extra variable.
Run several questions in orderquestion-sequence
const { prompt } = require('enquirer');
const answers = await prompt([
{ type: 'input', name: 'name', message: 'Project name?' },
{ type: 'select', name: 'lang', message: 'Language?', choices: ['ts', 'js'] },
{ type: 'confirm', name: 'git', message: 'Initialise a git repo?' }
]);Questions run one at a time and answers accumulate into a single object. If a name uses dot notation like 'author.email', the result is nested accordingly.
Pick one option from a listselect-one
const { Select } = require('enquirer');
const answer = await new Select({
name: 'env',
message: 'Deploy to which environment?',
choices: [
{ name: 'dev', message: 'Development' },
{ name: 'stg', message: 'Staging' },
{ name: 'prd', message: 'Production' }
]
}).run();
// answer === 'dev'The class form resolves to the value itself instead of an answers object. Selecting resolves to choice.name, not choice.message, so keep names stable and put the pretty text in message.
Select several and get their valuesmultiselect-values
const { MultiSelect } = require('enquirer');
const selected = await new MultiSelect({
name: 'features',
message: 'Enable which features?',
limit: 7,
choices: [
{ name: 'auth', value: '@app/auth' },
{ name: 'billing', value: '@app/billing' },
{ name: 'admin', value: '@app/admin' }
],
result(names) {
return this.map(names);
}
}).run();
// { auth: '@app/auth', admin: '@app/admin' }Without the result function you get an array of names and the value fields are simply lost. this.map converts the selected names into a name-to-value object, which is the documented workaround.
Yes/no and masked inputconfirm-and-password
const { prompt } = require('enquirer');
const { proceed } = await prompt({
type: 'confirm',
name: 'proceed',
message: 'Overwrite existing files?',
initial: false
});
if (proceed) {
const { token } = await prompt({
type: 'password',
name: 'token',
message: 'Paste your API token'
});
}confirm resolves to a boolean. For a secret that should leave no trace at all on screen, the invisible type hides the input entirely instead of masking it with asterisks.
Reject bad input before it returnsvalidate-input
const { prompt } = require('enquirer');
const { port } = await prompt({
type: 'numeral',
name: 'port',
message: 'Port to listen on',
initial: 3000,
validate(value) {
if (value < 1024 || value > 65535) return 'Pick a port between 1024 and 65535';
return true;
}
});Return true to accept, or a string to show as the error and keep the prompt open. Returning false shows a generic message, which is rarely what you want.
Skip a question based on earlier answersconditional-skip
const { prompt } = require('enquirer');
const answers = await prompt([
{ type: 'confirm', name: 'custom', message: 'Use a custom registry?' },
{
type: 'input',
name: 'registry',
message: 'Registry URL',
initial: 'https://registry.npmjs.org',
skip() {
return this.state.answers.custom !== true;
}
}
]);A skipped prompt still resolves, using its initial value, so downstream code always sees the key. Read prior answers from this.state.answers inside skip, initial, or message.
Type-ahead over a long listautocomplete-filter
const { AutoComplete } = require('enquirer');
const pkg = await new AutoComplete({
name: 'pkg',
message: 'Which package?',
limit: 10,
choices: allPackageNames,
suggest(input, choices) {
return choices.filter(c => c.message.toLowerCase().includes(input.toLowerCase()));
}
}).run();The default suggest does a greedy substring match on choice.message. Override it for fuzzy matching or to sort by relevance; limit controls how many rows are visible, not how many are searched.
Fill several fields on one screenform-multiple-fields
const { Form } = require('enquirer');
const author = await new Form({
name: 'author',
message: 'Package author:',
choices: [
{ name: 'name', message: 'Name', initial: 'Jane Doe' },
{ name: 'email', message: 'Email' },
{ name: 'url', message: 'URL', initial: 'https://' }
]
}).run();
// { name: '...', email: '...', url: '...' }Form keeps all fields visible and moves between them with the arrow keys, which reads better than three sequential input prompts for related fields.
Survive Ctrl+C without a crashhandle-cancel
const { prompt } = require('enquirer');
try {
const answers = await prompt(questions);
await run(answers);
} catch (error) {
// Ctrl+C rejects with the prompt's formatted error, often an empty string
if (!error || typeof error === 'string') {
console.log('\nCancelled.');
process.exit(130);
}
throw error;
}The rejection value is not always an Error, so error.message can be undefined and blow up your handler. Check the type before touching properties, and exit with 130 to report an interrupt honestly.
Fail fast when there is no terminalguard-non-tty
const { prompt } = require('enquirer');
function ask(questions) {
if (!process.stdin.isTTY) {
throw new Error('Interactive input required. Pass --name and --env instead.');
}
return prompt(questions);
}In CI or behind a pipe a prompt waits on a stdin that will never deliver a keypress, so the job hangs until its timeout. An explicit check turns that into a clear error message.
Use it from TypeScripttypescript-usage
// tsconfig: "esModuleInterop": true
import Enquirer from 'enquirer';
interface Answers { name: string; env: string }
const answers = await Enquirer.prompt<Answers>([
{ type: 'input', name: 'name', message: 'Name?' },
{ type: 'select', name: 'env', message: 'Env?', choices: ['dev', 'prd'] }
]);
// Prompt classes are untyped; require them and cast
const { Select } = require('enquirer') as any;prompt() is generic over the answers shape, which is the only well-typed part. The bundled definitions never declare Select, MultiSelect, or the other classes, so importing them by name does not type-check.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @inquirer/prompts | npm | You want the actively maintained mainstream option with per-prompt packages and real TypeScript types. |
| @clack/prompts | npm | You care about how the CLI looks: grouped flows, spinners, and cancel handling built in. |
| prompts | npm | You want the smallest dependency footprint and a single simple function to run a question list. |