mrkeyoor.com_
Thu 06 Aug 01:02 UTC
npmCLI & Toolingupdated 05 Aug 2026

prompts

prompts asks a terminal user questions and hands you back an object of answers. You describe each question as a plain object with a type, a name, and a message, pass one or an array of them to the prompts() function, and await the result. It covers twelve input types: text, password, invisible, number, confirm, list, toggle, select, multiselect, autocompleteMultiselect, autocomplete, and date. Every property can also be a function evaluated just before the question is asked, which is how conditional and dependent questions work: return a falsy type and the question is skipped. It has exactly two dependencies, kleur for color and sisteransi for cursor control, and it became the default scaffolding prompt library because it is small and the API fits in one page.

Verdict

Still a pleasant API and still tiny, but it has not shipped a release in nearly five years while sitting on 115 open issues, so you are adopting a frozen dependency. Fine for an internal script, harder to justify for a CLI you intend to support.

API stability5/5Nothing has changed since October 2021, so there is no migration risk whatsoever; that score is a statement about the release history, not about active design discipline.
Docs4/5The README is thorough: every prompt type has an options table, an animated example, and a code sample, plus full coverage of inject, override, onSubmit, and onCancel. What it never explains is that cancellation resolves rather than rejects, which is the behavior most people get wrong.
Maintenance1/5Last publish October 2021, last repo push May 2025, 115 open issues plus open PRs, and no roadmap or release notes since; treat it as feature-frozen.
Ecosystem4/5About 55.7M weekly downloads and embedded in a large number of create-* scaffolders, but new projects increasingly reach for @inquirer/prompts or @clack/prompts, so the transitive share is coasting rather than growing.

Use it if

  • You are writing a scaffolding or init command and need five or six questions answered without adding a framework
  • You want conditional questions driven by earlier answers, which the function-valued type and message properties handle without any branching code around the call
  • You need the prompt flow to be testable: prompts.inject() feeds canned answers and prompts.override() pre-answers from parsed CLI flags, both built in
  • Install size matters to you. Two dependencies and about 19 KB gzipped is a fraction of what the Inquirer family pulls in
Skip it if

Setup reality

npm install prompts and you are running; there is no config, no peer dependency, and no build step. The gaps show up afterward. Types are a separate npm install --save-dev @types/prompts. package.json declares engines node >= 6 and the entry file branches to a transpiled dist build for anything below Node 8.6, while the README says Node 14 and above, so treat the engines field as historical rather than accurate. It writes to process.stdout and reads process.stdin by default, which means it hangs or misbehaves in a non-TTY context such as a CI job or a piped shell, and you have to guard on process.stdin.isTTY yourself. There is no ESM entry point, so in a type: module project you use a default import and cannot destructure named exports from it.

Patterns

Ask one questionsingle-prompt

const prompts = require('prompts');

const { projectName } = await prompts({
  type: 'text',
  name: 'projectName',
  message: 'Project name?',
  initial: 'my-app',
});

The name property is the key you read off the response object, so it must be unique across a chain or a later answer silently overwrites an earlier one.

Exit properly when the user presses Ctrl+Chandle-cancel

const prompts = require('prompts');

const onCancel = () => {
  console.log('Aborted.');
  process.exit(1);
};

const answers = await prompts(questions, { onCancel });

Without onCancel, Ctrl+C or Esc resolves the promise with only the answers collected so far and exit code 0, so your script happily continues with undefined values. Wire this up before anything else.

Ask a list of questions in orderprompt-chain

const questions = [
  { type: 'text', name: 'username', message: 'GitHub username?' },
  { type: 'password', name: 'token', message: 'Personal access token?' },
  { type: 'confirm', name: 'save', message: 'Save to keychain?', initial: true },
];

const { username, token, save } = await prompts(questions, { onCancel });

Questions run sequentially and each one sees the answers so far, which is what makes function-valued properties useful; there is no parallel mode.

Skip a question based on a previous answerconditional-question

const questions = [
  { type: 'confirm', name: 'useTypescript', message: 'Use TypeScript?' },
  {
    type: prev => (prev ? 'select' : null),
    name: 'strictness',
    message: 'tsconfig strictness',
    choices: [
      { title: 'strict', value: 'strict' },
      { title: 'loose', value: 'loose' },
    ],
  },
];

Returning a falsy type skips the question entirely, and no key is set on the response object, so destructure with a default rather than assuming the property exists.

Reject bad input before moving onvalidate-input

{
  type: 'text',
  name: 'email',
  message: 'Work email?',
  validate: value =>
    /.+@.+\..+/.test(value) ? true : 'That does not look like an email',
}

Return true to accept, a string to show that message, or false for a generic error. validate can be async, which is how you check name availability against an API before continuing.

Single choice from a listselect-from-choices

{
  type: 'select',
  name: 'pm',
  message: 'Package manager',
  choices: [
    { title: 'npm', value: 'npm' },
    { title: 'pnpm', value: 'pnpm', description: 'fast, disk efficient' },
    { title: 'yarn', value: 'yarn', disabled: true },
  ],
  initial: 1,
}

initial is a zero-based index into choices, not a value, which trips people up when the list is generated. Omitting value on a choice makes the answer the title string.

Pick several options at oncemultiselect

{
  type: 'multiselect',
  name: 'features',
  message: 'Add features',
  choices: [
    { title: 'ESLint', value: 'eslint', selected: true },
    { title: 'Prettier', value: 'prettier' },
    { title: 'Vitest', value: 'vitest' },
  ],
  hint: 'space to select, enter to submit',
  instructions: false,
}

The answer is always an array, empty when nothing was picked, so there is no undefined case to handle. Set instructions: false to suppress the multi-line help block, which otherwise dominates a short list.

Transform the value before it reaches your codeformat-answer

{
  type: 'text',
  name: 'slug',
  message: 'URL slug',
  format: val => val.trim().toLowerCase().replace(/\s+/g, '-'),
}

format runs after validate, so validate sees the raw input and your program sees the formatted value; putting normalization in validate instead is a common cause of confusing error messages.

Drive the prompt flow from a testinject-answers-in-tests

const prompts = require('prompts');

prompts.inject(['my-app', ['eslint', 'vitest'], true]);

const answers = await runInitCommand();
expect(answers.projectName).toBe('my-app');

Injected values are consumed in order and removed from the queue, and an Error instance simulates the user cancelling. The README marks this as testing-only, so do not build production behavior on it.

Let CLI flags pre-answer questionsoverride-from-flags

const prompts = require('prompts');
const argv = require('minimist')(process.argv.slice(2));

prompts.override(argv);

// --projectName=my-app now skips that question entirely
const answers = await prompts(questions, { onCancel });

override matches on the question name, so your flag names and prompt names have to agree. This is the clean way to make an interactive command scriptable without a second code path.

Filter a large list as the user typesautocomplete-async

{
  type: 'autocomplete',
  name: 'pkg',
  message: 'Search npm',
  choices: [],
  suggest: async input => {
    const hits = await searchRegistry(input);
    return hits.map(h => ({ title: h.name, value: h.name }));
  },
  limit: 10,
}

suggest is called on every keystroke, so debounce or cache inside it if it hits the network; the built-in default only does a case-insensitive title match against the static choices array.

Do not prompt when there is no terminalguard-non-tty

if (!process.stdin.isTTY) {
  console.error('Non-interactive shell: pass --projectName and --pm as flags.');
  process.exit(1);
}

const answers = await prompts(questions, { onCancel });

The library reads process.stdin directly with no interactive-environment check, so in CI or behind a pipe it either hangs until the job times out or takes garbage as input. This guard has to be yours.

Alternatives

PackageRegistryPick it when
@inquirer/promptsnpmYou want the actively maintained option with per-prompt packages, real TypeScript types, and ESM plus CJS builds
@clack/promptsnpmYou want a modern-looking flow with grouped steps, spinners, and cancellation that is hard to ignore
enquirernpmYou need prompt types this library lacks, such as forms, surveys, sorting, and scale questions