npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

inquirer-recursive-prompt

v0.2.0

Published

Recursive prompt utility for @inquirer with nested flows and typed plugin support

Downloads

30

Readme

inquirer-recursive-prompt

A TypeScript library for creating recursive prompts with @inquirer/core. Allows users to repeatedly answer the same set of questions in a loop with flexible conditions and custom plugin support.

Features

  • Recursive loops: Ask a series of questions repeatedly with a continue prompt
  • Nesting support: Recursive prompts within recursive prompts (up to 3 levels deep)
  • Custom plugins: Register third-party Inquirer prompts as plugins
  • Conditional questions: Use when, filter, validate on individual questions
  • Early exit: exitWhen option to stop the loop based on conditions
  • Type-safe: Full TypeScript support with proper types
  • Depth safety: Prevents infinite recursion (configurable with bypassDepthLimit)
  • Theming: Global and per-question theme customization, including the loop prompt
  • Dynamic choices: choices can be a function receiving execution context
  • Nested field names: name: "user.profile.name" writes deeply into the result object
  • askAnswered: Re-ask a question already answered in the current iteration
  • transformer: Display-level transformation with full execution context
  • addAdditionalFields: Enrich answers after a prompt is validated, without an extra question
  • Theming: Global and per-question theme customization, including the loop prompt
  • Dynamic choices: choices can be a function receiving execution context
  • Nested field names: name: "user.profile.name" writes deeply into the result object
  • askAnswered: Re-ask a question already answered in the current iteration
  • transformer: Display-level transformation with full execution context
  • addAdditionalFields: Enrich answers after a prompt is validated, without an extra question

Installation

npm install inquirer-recursive-prompt @inquirer/core @inquirer/prompts

Quick Start

import { recursivePrompt } from "inquirer-recursive-prompt";

const result = await recursivePrompt({
  message: "Add another item?",
  questionType: "confirm",
  default: true,
  prompts: [
    {
      name: "itemName",
      type: "input",
      message: "Item name:",
    },
    {
      name: "itemPrice",
      type: "number",
      message: "Price:",
    },
  ],
});

console.log(result);
// Output: [
//   { itemName: "Apple", itemPrice: 1.5 },
//   { itemName: "Banana", itemPrice: 0.8 },
// ]

API Reference

recursivePrompt(options: RecursivePromptOptions): Promise<RecursiveAnswers[]>

Run a recursive prompt flow and return collected answers.

RecursivePromptOptions

interface RecursivePromptOptions {
  /**
   * Message when asking to continue/loop again
   * @default "Would you like to loop again?"
   */
  message?: string | (() => string);

  /**
   * Type of continue prompt: "confirm" or "select"
   * @default "confirm"
   */
  questionType?: "confirm" | "select";

  /**
   * Default value for continue prompt
   * @default true
   */
  default?: boolean;

  /**
   * Labels for select-type continue prompt
   */
  options?: {
    yesLabel?: string;
    noLabel?: string;
  };

  /**
   * Exit condition - if true, loop stops immediately
   */
  exitWhen?:
    | boolean
    | ((context: RecursiveExitWhenContext) => Promise<boolean> | boolean);

  /**
   * Bypass recursion depth limit (3 levels) with warning
   * @default false
   */
  bypassDepthLimit?: boolean;

  /**
   * Registered custom prompt plugins
   */
  plugins?: RecursivePromptPlugin[];

  /**
   * List of questions to ask recursively
   */
  prompts: RecursiveQuestion[];
}

Question Options

Each question in the prompts array can include:

{
  // Required
  name: string;             // Field name in results. Supports dot notation: "user.profile.name"
  type: RecursivePromptType; // "input", "select", "number", etc.

  // Native Inquirer options are directly on the question object (flat API)
  message?: string;
  theme?: PartialDeep<Theme>; // Per-question theme override

  // Lifecycle callbacks (all receive RecursiveQuestionExecutionContext)
  when?: boolean | ((context) => Promise<boolean> | boolean);
  filter?: (value, context) => Promise<unknown> | unknown;
  validate?: (value, context) => Promise<boolean | string> | boolean | string;
  transformer?: (value, context, flags?) => string;

  // Post-answer hook: runs after the answer is stored, no return value
  addAdditionalFields?: (value, context) => Promise<void> | void;

  // Re-ask even if this name is already answered in the current iteration
  askAnswered?: boolean;

  // Dynamic choices: resolved before displaying the prompt
  choices?: Choice[] | ((context) => Promise<Choice[]> | Choice[]);
}

Execution Context

Available in when, filter, validate, transformer, and addAdditionalFields callbacks:

{
  answers: Record<string, unknown>; // Answers collected in the current iteration
  allAnswers: RecursiveAnswers[];   // All iterations already completed
  depth: number;                   // Recursion depth (starts at 1)
  iteration: number;               // Current iteration number
  setField: (path: string, value: unknown) => void; // Write a field by path (supports dot notation)
}

ExitWhen Context

Available in exitWhen callback:

{
  answers: RecursiveAnswers[]; // All loop entries already collected
  depth: number; // Recursion depth (starts at 1)
  iteration: number; // Current iteration number
}

Examples

| Example | Script | Description | |---|---|---| | simple.ts | npm run example:simple | Basic loop with input, select, number | | nested-recursive.ts | npm run example:nested | Nested recursive prompts | | with-plugin.ts | npm run example:plugin | Custom Inquirer plugin registration | | with-exit-condition.ts | npm run example:exit | exitWhen based on a user-defined limit | | with-themes.ts | npm run example:themes | Global and per-question theming, recursivePrompt loop theme | | with-month-revenue.ts | npm run example:months | Dynamic choices, transformer, addAdditionalFields, setField |

Advanced Features

Theming

Apply Inquirer themes globally by prompt type. The recursivePrompt key styles the built-in loop prompt (confirm/select) independently.

const result = await recursivePrompt({
  theme: {
    input: {
      validationFailureMode: "keep",
    },
    select: {
      icon: { cursor: "❯" },
      indexMode: "number",
    },
    recursivePrompt: {
      style: {
        message: (text) => "\x1b[31m" + text + "\x1b[0m", // red
        answer: (text) => "\x1b[32m" + text + "\x1b[0m",  // green
      },
    },
  },
  prompts: [...],
});

Per-question theme overrides global values for that prompt only:

{
  name: "priority",
  type: "select",
  message: "Priority:",
  choices: [...],
  theme: { icon: { cursor: "▶" } }, // overrides global select cursor
}

Plugin themes

Declare theme keys on a plugin with the 4th generic, then use them via theme["plugin-type"]:

const myPlugin: RecursivePromptPlugin<
  "my-plugin",
  Value,
  Config,
  { border?: string; headerColor?: string }  // 4th generic = theme shape
> = {
  name: "My plugin",
  type: "my-plugin",
  prompt: myPluginFn,
  themes: { border: "single", headerColor: "cyan" },
};

const options: RecursivePromptOptions<[typeof myPlugin]> = {
  plugins: [myPlugin],
  theme: {
    "my-plugin": { border: "double" }, // TypeScript validates the keys
  },
  prompts: [...],
};

Dynamic Choices

choices can be a function receiving the execution context. Useful for filtering already-selected items across iterations:

{
  name: "month",
  type: "select",
  message: "Choose a month:",
  choices: ({ allAnswers }) => {
    const used = new Set(allAnswers.map((a) => a.month));
    return ALL_MONTHS.filter((m) => !used.has(m.value));
  },
}

Nested Field Names

Use dot notation in name to write nested objects:

{
  name: "user.profile.name",
  type: "input",
  message: "Your name:",
}
// Result: { user: { profile: { name: "Alice" } } }

askAnswered

By default, if two questions share the same name in one iteration, only the first is asked. Set askAnswered: true to force re-asking:

{
  name: "confirm",
  type: "input",
  message: "Confirm value:",
  askAnswered: true,
}

transformer

Display a formatted string while the user types, without altering the stored value. Receives the full execution context:

{
  name: "dailyRevenue",
  type: "input",
  message: "Daily revenue:",
  transformer: (value, { answers }) => {
    const days = getDaysInMonth(answers.month);
    return `${value} / day → ${Number(value) * days} total`;
  },
}

addAdditionalFields

Runs after the answer is stored. Use it to compute and inject extra fields into the current iteration answers without asking additional questions:

{
  name: "dailyRevenue",
  type: "input",
  message: "Daily revenue:",
  filter: (value) => Number(value),
  addAdditionalFields: (value, { answers, setField }) => {
    const days = getDaysInMonth(answers.month);
    setField("daysInMonth", days);
    setField("monthTotal", Number(value) * days);
  },
}
// Result includes: { dailyRevenue, daysInMonth, monthTotal }

setField(path, value) supports dot notation and is equivalent to writing answers.key = value.

Custom Plugins

Register a custom Inquirer prompt as a plugin:

import tableMultiple from "@bartheleway/inquirer-table-multiple";
import type { RecursivePromptOptions, RecursivePromptPlugin } from "inquirer-recursive-prompt";

type TableMultipleConfig = Parameters<typeof tableMultiple>[0];

const tableMultiplePlugin: RecursivePromptPlugin<
  "table-multiple",
  unknown,
  TableMultipleConfig
> = {
  name: "Table Multiple",
  type: "table-multiple",
  prompt: tableMultiple,
};

const options: RecursivePromptOptions<[typeof tableMultiplePlugin]> = {
  plugins: [tableMultiplePlugin],
  prompts: [...],
};

Then use it in prompts:

{
  name: "selectedItems",
  type: "table-multiple",  // Matches the registered plugin type
  message: "Select items:",
  choices: [...],
}

Conditional Questions

Skip questions based on current iteration answers or all previous iterations:

{
  name: "email",
  type: "input",
  message: "Email:",
  when: ({ answers, allAnswers }) =>
    answers.wantEmail === true && allAnswers.length < 3,
}

Input Transformation

Transform user input before storing:

{
  name: "quantity",
  type: "number",
  message: "Quantity:",
  filter: (value) => Math.max(1, Number(value)),
}

Validation with Custom Messages

Validate input with contextual error messages:

{
  name: "username",
  type: "input",
  message: "Username:",
  validate: (value, { iteration }) =>
    String(value).length > 3 || `Username too short (iteration ${iteration})`,
}

Exit on Condition

Stop the loop when a condition is met:

const result = await recursivePrompt({
  prompts: [...],
  // answers is the full list of already collected loop answers
  exitWhen: ({ iteration, answers }) =>
    iteration >= 5 || answers.length >= 3,
});

Type Safety

Recursive prompts for nested data:

{
  name: "skills",
  type: "recursive",
  message: "Add another skill?",
  prompts: [
    { name: "skillName", type: "input", message: "Name:" },
    { name: "level", type: "select", message: "Level:", choices: [...] },
  ],
}

The skills answer will be an array of collected skill objects.

Depth Limits

By default, recursive nesting is limited to 3 levels to prevent accidental infinite loops.

Override with caution:

const result = await recursivePrompt({
  prompts: [...],
  bypassDepthLimit: true,  // ⚠️ Warning printed to console
});

Error Handling

Plugin Validation

Duplicate plugin types or functions throw an error:

// This will throw:
plugins: [
  { type: "myPlugin", ... },
  { type: "myPlugin", ... },  // ❌ Duplicate type
]

Reserved types (input, select, etc.) cannot be overridden.

License

MIT - See LICENSE file

Contributing

Contributions welcome! Please open issues and pull requests on GitHub.