inquirerer
A powerful, TypeScript-first library for building beautiful command-line interfaces.Create interactive CLI tools with ease using intuitive prompts, validation, and rich user experiences.
Installation
npm install inquirerer
Features
- 🔌 CLI Builder - Build command-line utilties fast
- 🖊 Multiple Question Types - Support for text, autocomplete, checkbox, and confirm questions
- 🤖 Non-Interactive Mode - Fallback to defaults for CI/CD environments, great for testing
- ✅ Smart Validation - Built-in pattern matching, custom validators, and sanitizers
- 🔀 Conditional Logic - Show/hide questions based on previous answers
- 🎨 Interactive UX - Fuzzy search, keyboard navigation, and visual feedback
- 🔄 Dynamic Defaults - Auto-populate defaults from git config, date/time, or custom resolvers
Table of Contents
Quick Start
import { Inquirerer } from 'inquirerer';
const prompter = new Inquirerer();
const answers = await prompter.prompt({}, [
{
type: 'text',
name: 'username',
message: 'What is your username?',
required: true
},
{
type: 'confirm',
name: 'newsletter',
message: 'Subscribe to our newsletter?',
default: true
}
]);
console.log(answers);
Core Concepts
TypeScript Support
Import types for full type safety:
import {
Inquirerer,
Question,
TextQuestion,
NumberQuestion,
ConfirmQuestion,
BooleanQuestion,
JsonQuestion,
ListQuestion,
AutocompleteQuestion,
CheckboxQuestion,
InquirererOptions,
DefaultResolverRegistry,
registerDefaultResolver,
resolveDefault
} from 'inquirerer';
interface UserConfig {
name: string;
age: number;
newsletter: boolean;
}
const answers = await prompter.prompt<UserConfig>({}, questions);
Question Types
All questions support these base properties:
interface BaseQuestion {
name: string;
type: string;
_?: boolean;
alias?: string | string[];
message?: string;
description?: string;
default?: any;
defaultFrom?: string;
setFrom?: string;
optionsFrom?: string;
useDefault?: boolean;
required?: boolean;
skipPrompt?: boolean;
validate?: (input: any, answers: any) => boolean | Validation;
sanitize?: (input: any, answers: any) => any;
pattern?: string;
dependsOn?: string[];
when?: (answers: any) => boolean;
}
Skipping Prompts
Use skipPrompt: true to skip interactive prompting for a question entirely. The field is omitted from the answers object unless the user explicitly passes it via a CLI flag. This is useful for fields with backend-managed defaults where the CLI should not prompt, but should still allow overrides.
const questions: Question[] = [
{
type: 'text',
name: 'username',
message: 'Username',
required: true
},
{
type: 'text',
name: 'status',
message: 'Account status',
skipPrompt: true
}
];
const result = await prompter.prompt({}, questions);
const result2 = await prompter.prompt({ status: 'active' }, questions);
Key behaviors:
- The question still appears in generated man pages
- CLI flag overrides (e.g.
--status active) still work
- The field is simply left out of the answers if not provided
- Different from
when: skipPrompt is unconditional, while when depends on other answers
- Different from
useDefault: skipPrompt does not apply a default value
Non-Interactive Mode
When running in CI/CD or without a TTY, inquirerer automatically falls back to default values:
const prompter = new Inquirerer({
noTty: true,
useDefaults: true
});
API Reference
Inquirerer Class
Constructor Options
interface InquirererOptions {
noTty?: boolean;
input?: Readable;
output?: Writable;
useDefaults?: boolean;
globalMaxLines?: number;
mutateArgs?: boolean;
resolverRegistry?: DefaultResolverRegistry;
}
const prompter = new Inquirerer(options);
Methods
prompt<T>(argv: T, questions: Question[], options?: PromptOptions): Promise<T>
generateManPage(info: ManPageInfo): string
close(): void
exit(): void
Managing Multiple Instances
When working with multiple Prompter instances that share the same input stream (typically process.stdin), only one instance should be actively prompting at a time. Each instance attaches its own keyboard listener, so having multiple active instances will cause duplicate or unexpected keypress behavior.
Best practices:
-
Reuse a single instance - Create one Prompter instance and reuse it for all prompts:
const prompter = new Inquirerer();
const answers1 = await prompter.prompt({}, questions1);
const answers2 = await prompter.prompt({}, questions2);
prompter.close();
-
Close before creating another - If you need separate instances, close the first before using the second:
const prompter1 = new Prompter();
const answers1 = await prompter1.prompt({}, questions1);
prompter1.close();
const prompter2 = new Prompter();
const answers2 = await prompter2.prompt({}, questions2);
prompter2.close();
Question Types
Text Question
Collect string input from users.
{
type: 'text',
name: 'projectName',
message: 'What is your project name?',
default: 'my-app',
required: true,
pattern: '^[a-z0-9-]+$',
validate: (input) => {
if (input.length < 3) {
return { success: false, reason: 'Name must be at least 3 characters' };
}
return true;
}
}
Number Question
Collect numeric input.
{
type: 'number',
name: 'port',
message: 'Which port to use?',
default: 3000,
validate: (input) => {
if (input < 1 || input > 65535) {
return { success: false, reason: 'Port must be between 1 and 65535' };
}
return true;
}
}
Confirm Question
Yes/no questions.
{
type: 'confirm',
name: 'useTypeScript',
message: 'Use TypeScript?',
default: true
}
Boolean Question
Alias for confirm — provides a semantic name for boolean fields. Behaves identically to confirm (y/n prompt).
{
type: 'boolean',
name: 'isActive',
message: 'Is this record active?',
default: true
}
Useful when generating CLI prompts from schema types where the field type is Boolean rather than a yes/no confirmation.
JSON Question
Collect structured JSON input. Validates input with JSON.parse() — invalid JSON returns null.
{
type: 'json',
name: 'metadata',
message: 'Enter metadata',
default: { key: 'value' }
}
The prompt displays a (JSON) hint. Users enter raw JSON strings:
$ Enter metadata (JSON)
> {"email":"user@example.com","role":"admin"}
In non-interactive mode, returns the default value if provided, otherwise undefined.
List Question
Select one option from a list (no search).
{
type: 'list',
name: 'license',
message: 'Choose a license',
options: ['MIT', 'Apache-2.0', 'GPL-3.0', 'BSD-3-Clause'],
default: 'MIT',
maxDisplayLines: 5
}
Autocomplete Question
Select with fuzzy search capabilities.
{
type: 'autocomplete',
name: 'framework',
message: 'Choose your framework',
options: [
{ name: 'React', value: 'react' },
{ name: 'Vue.js', value: 'vue' },
{ name: 'Angular', value: 'angular' },
{ name: 'Svelte', value: 'svelte' }
],
allowCustomOptions: true,
maxDisplayLines: 8
}
Checkbox Question
Multi-select with search.
{
type: 'checkbox',
name: 'features',
message: 'Select features to include',
options: [
'Authentication',
'Database',
'API Routes',
'Testing',
'Documentation'
],
default: ['Authentication', 'API Routes'],
returnFullResults: false,
required: true
}
With returnFullResults: true, returns all options with selection status:
[
{ name: 'Authentication', value: 'Authentication', selected: true },
{ name: 'Database', value: 'Database', selected: false },
]
Advanced Question Options
Custom Validation
{
type: 'text',
name: 'email',
message: 'Enter your email',
pattern: '^[^@]+@[^@]+\\.[^@]+$',
validate: (email, answers) => {
if (email.endsWith('@example.com')) {
return {
success: false,
reason: 'Please use a real email address'
};
}
return { success: true };
}
}
Value Sanitization
{
type: 'text',
name: 'tags',
message: 'Enter tags (comma-separated)',
sanitize: (input) => {
return input.split(',').map(tag => tag.trim());
}
}
Conditional Questions
const questions: Question[] = [
{
type: 'confirm',
name: 'useDatabase',
message: 'Do you need a database?',
default: false
},
{
type: 'list',
name: 'database',
message: 'Which database?',
options: ['PostgreSQL', 'MySQL', 'MongoDB', 'SQLite'],
when: (answers) => answers.useDatabase === true
}
];
Question Dependencies
Ensure questions appear in the correct order:
[
{
type: 'checkbox',
name: 'services',
message: 'Select services',
options: ['Auth', 'Storage', 'Functions']
},
{
type: 'text',
name: 'authProvider',
message: 'Which auth provider?',
dependsOn: ['services'],
when: (answers) => {
const selected = answers.services.find(s => s.name === 'Auth');
return selected?.selected === true;
}
}
]
Positional Arguments
The _ property allows you to name positional parameters, enabling users to pass values without flags. This is useful for CLI tools where the first few arguments have obvious meanings.
Basic Usage
const questions: Question[] = [
{
_: true,
name: 'database',
type: 'text',
message: 'Database name',
required: true
}
];
const argv = minimist(process.argv.slice(2));
const result = await prompter.prompt(argv, questions);
Now users can run either:
node myprogram.js mydb1
node myprogram.js --database mydb1
Multiple Positional Arguments
Positional arguments are assigned in declaration order:
const questions: Question[] = [
{ _: true, name: 'source', type: 'text', message: 'Source file' },
{ name: 'verbose', type: 'confirm', default: false },
{ _: true, name: 'destination', type: 'text', message: 'Destination file' }
];
Named Arguments Take Precedence
When both positional and named arguments are provided, named arguments win and the positional slot is preserved for the next positional question:
const questions: Question[] = [
{ _: true, name: 'foo', type: 'text' },
{ _: true, name: 'bar', type: 'text' },
{ _: true, name: 'baz', type: 'text' }
];
In this example, bar gets its value from the named flag, so the two positional values go to foo and baz.
Alias
The alias property allows you to define short flag alternatives for question names, making CLI usage more convenient. Users can pass values using either the full name or any defined alias.
Basic Usage
const questions: Question[] = [
{
name: 'workspace',
type: 'confirm',
alias: 'w',
message: 'Create workspace?'
},
{
name: 'outputDir',
type: 'text',
alias: 'o',
message: 'Output directory',
default: './dist'
}
];
const argv = minimist(process.argv.slice(2));
const result = await prompter.prompt(argv, questions);
Now users can run either:
node myprogram.js --workspace --outputDir ./build
node myprogram.js -w -o ./build
Multiple Aliases
You can define multiple aliases for a single question using an array:
{
name: 'verbose',
type: 'confirm',
alias: ['v', 'V'],
message: 'Enable verbose output?'
}
Alias Priority
When both the main name and an alias are provided, the main name takes precedence:
When multiple aliases are provided in argv, the first alias in the definition order is used:
{
name: 'workspace',
type: 'text',
alias: ['w', 'ws']
}
Positional with Options
Positional arguments work with list, autocomplete, and checkbox questions. The value is mapped through the options:
const questions: Question[] = [
{
_: true,
name: 'framework',
type: 'list',
options: [
{ name: 'React', value: 'react' },
{ name: 'Vue', value: 'vue' }
]
}
];
Real-World Examples
Project Setup Wizard
import { Inquirerer, Question } from 'inquirerer';
import minimist from 'minimist';
const argv = minimist(process.argv.slice(2));
const prompter = new Inquirerer();
const questions: Question[] = [
{
type: 'text',
name: 'projectName',
message: 'Project name',
required: true,
pattern: '^[a-z0-9-]+$'
},
{
type: 'text',
name: 'description',
message: 'Project description',
default: 'My awesome project'
},
{
type: 'confirm',
name: 'typescript',
message: 'Use TypeScript?',
default: true
},
{
type: 'autocomplete',
name: 'framework',
message: 'Choose a framework',
options: ['React', 'Vue', 'Svelte', 'None'],
default: 'React'
},
{
type: 'checkbox',
name: 'tools',
message: 'Additional tools',
options: ['ESLint', 'Prettier', 'Jest', 'Husky'],
default: ['ESLint', 'Prettier']
}
];
const config = await prompter.prompt(argv, questions);
console.log('Creating project with:', config);
Run interactively:
node setup.js
Or with CLI args:
node setup.js --projectName=my-app --typescript --framework=React
Configuration Builder
interface AppConfig {
port: number;
host: string;
ssl: boolean;
sslCert?: string;
sslKey?: string;
database: string;
logLevel: string;
}
const questions: Question[] = [
{
type: 'number',
name: 'port',
message: 'Server port',
default: 3000,
validate: (port) => port > 0 && port < 65536
},
{
type: 'text',
name: 'host',
message: 'Server host',
default: '0.0.0.0'
},
{
type: 'confirm',
name: 'ssl',
message: 'Enable SSL?',
default: false
},
{
type: 'text',
name: 'sslCert',
message: 'SSL certificate path',
when: (answers) => answers.ssl === true,
required: true
},
{
type: 'text',
name: 'sslKey',
message: 'SSL key path',
when: (answers) => answers.ssl === true,
required: true
},
{
type: 'list',
name: 'database',
message: 'Database type',
options: ['PostgreSQL', 'MySQL', 'SQLite'],
default: 'PostgreSQL'
},
{
type: 'list',
name: 'logLevel',
message: 'Log level',
options: ['error', 'warn', 'info', 'debug'],
default: 'info'
}
];
const config = await prompter.prompt<AppConfig>(argv, questions);
fs.writeFileSync('config.json', JSON.stringify(config, null, 2));
CLI with Commander Integration
import { CLI, CommandHandler } from 'inquirerer';
import { Question } from 'inquirerer';
const handler: CommandHandler = async (argv, prompter, options) => {
const questions: Question[] = [
{
type: 'text',
name: 'name',
message: 'What is your name?',
required: true
},
{
type: 'number',
name: 'age',
message: 'What is your age?',
validate: (age) => age >= 0 && age <= 120
}
];
const answers = await prompter.prompt(argv, questions);
console.log('Hello,', answers.name);
};
const cli = new CLI(handler, {
version: 'myapp@1.0.0',
minimistOpts: {
alias: {
n: 'name',
a: 'age',
v: 'version'
}
}
});
await cli.run();
Dynamic Dependencies
const questions: Question[] = [
{
type: 'checkbox',
name: 'cloud',
message: 'Select cloud services',
options: ['AWS', 'Azure', 'GCP'],
returnFullResults: true
},
{
type: 'text',
name: 'awsRegion',
message: 'AWS Region',
dependsOn: ['cloud'],
when: (answers) => {
const aws = answers.cloud?.find(c => c.name === 'AWS');
return aws?.selected === true;
},
default: 'us-east-1'
},
{
type: 'text',
name: 'azureLocation',
message: 'Azure Location',
dependsOn: ['cloud'],
when: (answers) => {
const azure = answers.cloud?.find(c => c.name === 'Azure');
return azure?.selected === true;
},
default: 'eastus'
},
{
type: 'text',
name: 'gcpZone',
message: 'GCP Zone',
dependsOn: ['cloud'],
when: (answers) => {
const gcp = answers.cloud?.find(c => c.name === 'GCP');
return gcp?.selected === true;
},
default: 'us-central1-a'
}
];
const config = await prompter.prompt({}, questions);
Custom Validation
const questions: Question[] = [
{
type: 'text',
name: 'username',
message: 'Choose a username',
required: true,
pattern: '^[a-zA-Z0-9_]{3,20}$',
validate: async (username) => {
const available = await checkUsernameAvailability(username);
if (!available) {
return {
success: false,
reason: 'Username is already taken'
};
}
return { success: true };
}
},
{
type: 'text',
name: 'password',
message: 'Choose a password',
required: true,
validate: (password) => {
if (password.length < 8) {
return {
success: false,
reason: 'Password must be at least 8 characters'
};
}
if (!/[A-Z]/.test(password)) {
return {
success: false,
reason: 'Password must contain an uppercase letter'
};
}
if (!/[0-9]/.test(password)) {
return {
success: false,
reason: 'Password must contain a number'
};
}
return { success: true };
}
},
{
type: 'text',
name: 'confirmPassword',
message: 'Confirm password',
required: true,
dependsOn: ['password'],
validate: (confirm, answers) => {
if (confirm !== answers.password) {
return {
success: false,
reason: 'Passwords do not match'
};
}
return { success: true };
}
}
];
Dynamic Defaults with Resolvers
The defaultFrom feature allows you to automatically populate question defaults from dynamic sources like git configuration, environment variables, date/time values, or custom resolvers. This eliminates repetitive boilerplate code for common default values.
Quick Example
import { Inquirerer } from 'inquirerer';
const questions = [
{
type: 'text',
name: 'authorName',
message: 'Author name?',
defaultFrom: 'git.user.name'
},
{
type: 'text',
name: 'authorEmail',
message: 'Author email?',
defaultFrom: 'git.user.email'
},
{
type: 'text',
name: 'npmUser',
message: 'NPM username?',
defaultFrom: 'npm.whoami'
},
{
type: 'text',
name: 'copyrightYear',
message: 'Copyright year?',
defaultFrom: 'date.year'
}
];
const prompter = new Inquirerer();
const answers = await prompter.prompt({}, questions);
Built-in Resolvers
Prompter comes with several built-in resolvers ready to use:
Git Configuration
git.user.name | Git global user name | "John Doe" |
git.user.email | Git global user email | "john@example.com" |
NPM
npm.whoami | Currently logged in npm user | "johndoe" |
Date & Time
date.year | Current year | "2025" |
date.month | Current month (zero-padded) | "11" |
date.day | Current day (zero-padded) | "23" |
date.iso | ISO date (YYYY-MM-DD) | "2025-11-23" |
date.now | ISO timestamp | "2025-11-23T15:30:45.123Z" |
date.timestamp | Unix timestamp (ms) | "1732375845123" |
Workspace (nearest package.json)
workspace.name | Repo slug from repository URL (fallback: package.json name) | "dev-utils" |
workspace.repo.name | Repo name from repository URL | "dev-utils" |
workspace.repo.organization | Repo org/owner from repository URL | "constructive-io" |
workspace.organization.name | Alias for workspace.repo.organization | "constructive-io" |
workspace.license | License field from package.json | "MIT" |
workspace.author | Author name from package.json | "Constructive" |
workspace.author.name | Author name from package.json | "Constructive" |
workspace.author.email | Author email from package.json | "email@example.org" |
Priority Order
When resolving default values, inquirerer follows this priority:
- CLI Arguments - Values passed via command line (highest priority)
setFrom - Auto-set values (bypasses prompt entirely)
defaultFrom - Dynamically resolved default values
default - Static default values
undefined - No default available
{
type: 'text',
name: 'author',
defaultFrom: 'git.user.name',
default: 'Anonymous'
}
setFrom vs defaultFrom
Both setFrom and defaultFrom use resolvers to get values, but they behave differently:
| Sets value as | Default (user can override) | Final value (no prompt) |
| User prompted? | Yes, with pre-filled default | No, question is skipped |
| Use case | Suggested values | Auto-computed values |
defaultFrom - The resolved value becomes the default, but the user is still prompted and can change it:
{
type: 'text',
name: 'authorName',
message: 'Author name?',
defaultFrom: 'git.user.name'
}
setFrom - The resolved value is set directly and the question is skipped entirely:
{
type: 'text',
name: 'year',
message: 'Copyright year?',
setFrom: 'date.year'
}
When to use each
Use defaultFrom when:
- The value is a suggestion the user might want to change
- User confirmation is desired
Use setFrom when:
- The value should be computed automatically
- No user input is needed (e.g., timestamps, computed fields)
- You want to reduce the number of prompts
Combined example
const questions = [
{
type: 'text',
name: 'authorName',
message: 'Author name?',
defaultFrom: 'git.user.name'
},
{
type: 'text',
name: 'createdAt',
setFrom: 'date.iso'
},
{
type: 'text',
name: 'copyrightYear',
setFrom: 'date.year'
}
];
Custom Resolvers
Register your own custom resolvers for project-specific needs:
import { registerDefaultResolver } from 'inquirerer';
registerDefaultResolver('cwd.name', () => {
return process.cwd().split('/').pop();
});
registerDefaultResolver('env.user', () => {
return process.env.USER;
});
const questions = [
{
type: 'text',
name: 'projectName',
message: 'Project name?',
defaultFrom: 'cwd.name',
default: 'my-project'
},
{
type: 'text',
name: 'author',
message: 'Author?',
defaultFrom: 'env.user'
}
];
Instance-Specific Resolvers
For isolated resolver registries, use a custom resolver registry per Prompter instance:
import { DefaultResolverRegistry, Prompter } from 'inquirerer';
const customRegistry = new DefaultResolverRegistry();
customRegistry.register('app.name', () => 'my-app');
customRegistry.register('app.port', () => 3000);
const prompter = new Inquirerer({
resolverRegistry: customRegistry
});
const questions = [
{
type: 'text',
name: 'appName',
defaultFrom: 'app.name'
},
{
type: 'number',
name: 'port',
defaultFrom: 'app.port'
}
];
const answers = await prompter.prompt({}, questions);
Resolver Examples
System Information
import os from 'os';
import { registerDefaultResolver } from 'inquirerer';
registerDefaultResolver('system.hostname', () => os.hostname());
registerDefaultResolver('system.username', () => os.userInfo().username);
const questions = [
{
type: 'text',
name: 'hostname',
message: 'Hostname?',
defaultFrom: 'system.hostname'
}
];
Conditional Defaults
registerDefaultResolver('app.port', () => {
return process.env.NODE_ENV === 'production' ? 80 : 3000;
});
const questions = [
{
type: 'number',
name: 'port',
message: 'Port?',
defaultFrom: 'app.port'
}
];
Error Handling
Resolvers fail silently by default. If a resolver throws an error or returns undefined, inquirerer falls back to the static default value (if provided):
{
type: 'text',
name: 'author',
defaultFrom: 'git.user.name',
default: 'Anonymous',
required: true
}
For debugging, set DEBUG=inquirerer to see resolver errors:
DEBUG=inquirerer node your-cli.js
Real-World Use Case
import { Inquirerer, registerDefaultResolver } from 'inquirerer';
registerDefaultResolver('cwd.name', () => {
return process.cwd().split('/').pop();
});
const questions = [
{
type: 'text',
name: 'projectName',
message: 'Project name?',
defaultFrom: 'cwd.name',
required: true
},
{
type: 'text',
name: 'author',
message: 'Author?',
defaultFrom: 'git.user.name',
required: true
},
{
type: 'text',
name: 'email',
message: 'Email?',
defaultFrom: 'git.user.email',
required: true
},
{
type: 'text',
name: 'year',
message: 'Copyright year?',
defaultFrom: 'date.year'
}
];
const prompter = new Inquirerer();
const config = await prompter.prompt({}, questions);
With git configured, the prompts will show:
Project name? (my-project-dir)
Author? (John Doe)
Email? (john@example.com)
Copyright year? (2025)
All defaults automatically populated from git config, directory name, and current date!
CLI Helper
The CLI class provides integration with command-line argument parsing:
import { CLI, CommandHandler, CLIOptions } from 'inquirerer';
const options: Partial<CLIOptions> = {
version: 'myapp@1.0.0',
minimistOpts: {
alias: {
v: 'version',
h: 'help'
},
boolean: ['help', 'version'],
string: ['name', 'output']
}
};
const handler: CommandHandler = async (argv, prompter) => {
if (argv.help) {
console.log('Usage: myapp [options]');
process.exit(0);
}
const answers = await prompter.prompt(argv, questions);
};
const cli = new CLI(handler, options);
await cli.run();
CLI Utilities
inquirerer provides a complete set of utilities for building CLI applications, so you can import everything from a single package.
Package Information
Get information about your CLI's package.json for version display and other metadata:
import { getPackageJson, getPackageVersion, getPackageName } from 'inquirerer';
const pkg = getPackageJson(__dirname);
console.log(`${pkg.name}@${pkg.version}`);
if (argv.version) {
console.log(getPackageVersion(__dirname));
process.exit(0);
}
const toolName = getPackageName(__dirname);
console.log(`Welcome to ${toolName}!`);
Argument Parsing
Parse command-line arguments and extract subcommands:
import { parseArgv, extractFirst } from 'inquirerer';
const argv = parseArgv(process.argv);
const { first, newArgv } = extractFirst(argv);
switch (first) {
case 'generate':
await handleGenerate(newArgv);
break;
case 'init':
await handleInit(newArgv);
break;
default:
console.log('Unknown command');
}
Error Handling
Exit gracefully with error messages and optional cleanup:
import { cliExitWithError, CliExitOptions } from 'inquirerer';
try {
await riskyOperation();
} catch (error) {
await cliExitWithError(error, {
context: { operation: 'build', target: 'production' },
beforeExit: async () => {
await cleanup();
},
logger: customLogger
});
}
Complete CLI Example
Here's a complete example using all the CLI utilities:
import {
CLI,
CLIOptions,
Inquirerer,
extractFirst,
cliExitWithError,
getPackageVersion,
ParsedArgs
} from 'inquirerer';
const options: Partial<CLIOptions> = {
minimistOpts: {
alias: { v: 'version', h: 'help' },
boolean: ['help', 'version']
}
};
const handler = async (argv: Partial<ParsedArgs>, prompter: Inquirerer) => {
if (argv.version) {
console.log(getPackageVersion(__dirname));
process.exit(0);
}
const { first, newArgv } = extractFirst(argv);
try {
switch (first) {
case 'init':
await handleInit(newArgv, prompter);
break;
case 'build':
await handleBuild(newArgv, prompter);
break;
default:
console.log('Usage: mycli <command> [options]');
console.log('Commands: init, build');
}
} catch (error) {
await cliExitWithError(error, {
context: { command: first }
});
}
};
const cli = new CLI(handler, options);
await cli.run();
UI Components
inquirerer includes a set of UI components for building rich terminal interfaces beyond simple prompts. These are useful for showing progress, loading states, and streaming output.
Spinner
Show an animated spinner while performing async operations:
import { createSpinner } from 'inquirerer';
const spinner = createSpinner('Loading packages...');
spinner.start();
const data = await fetchPackages();
spinner.succeed('Loaded 42 packages');
Spinner styles:
import { createSpinner, SPINNER_STYLES } from 'inquirerer';
const spinner = createSpinner('Processing...', {
frames: SPINNER_STYLES.dots,
});
Update text while spinning:
spinner.start();
spinner.text('Step 1: Downloading...');
await download();
spinner.text('Step 2: Installing...');
await install();
spinner.succeed('Done!');
Progress Bar
Show progress for operations with known completion:
import { createProgress } from 'inquirerer';
const progress = createProgress('Installing dependencies');
progress.start();
for (let i = 0; i < packages.length; i++) {
await installPackage(packages[i]);
progress.update((i + 1) / packages.length);
}
progress.complete('All packages installed');
Increment progress:
const progress = createProgress('Processing files');
progress.start();
for (const file of files) {
await processFile(file);
progress.increment(1 / files.length);
}
progress.complete();
Streaming Text
Display streaming output like AI chat responses:
import { createStream } from 'inquirerer';
const stream = createStream({ showCursor: true });
stream.start();
for await (const token of llmResponse) {
stream.append(token);
}
stream.done();
With line prefix:
const stream = createStream({ prefix: '> ' });
stream.start();
stream.appendLine('First line of response');
stream.appendLine('Second line of response');
stream.done();
Custom UI with UIEngine
For fully custom interactive UIs, use the UIEngine directly:
import { UIEngine, Key } from 'inquirerer';
interface MyState {
items: string[];
selectedIndex: number;
}
const engine = new UIEngine();
const result = await engine.run<MyState, string>({
initialState: {
items: ['Option A', 'Option B', 'Option C'],
selectedIndex: 0
},
render: (state) => [
'Select an option:',
...state.items.map((item, i) =>
i === state.selectedIndex ? `> ${item}` : ` ${item}`
)
],
onEvent: (event, state) => {
if (event.type === 'key') {
switch (event.key) {
case Key.UP:
return {
state: {
...state,
selectedIndex: Math.max(0, state.selectedIndex - 1)
}
};
case Key.DOWN:
return {
state: {
...state,
selectedIndex: Math.min(state.items.length - 1, state.selectedIndex + 1)
}
};
case Key.ENTER:
return {
state,
done: true,
value: state.items[state.selectedIndex]
};
}
}
return { state };
}
});
console.log('You selected:', result);
With animations (tick events):
const engine = new UIEngine();
await engine.run({
initialState: { frame: 0, message: 'Loading' },
tickInterval: 100,
render: (state) => {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
return [`${frames[state.frame % frames.length]} ${state.message}...`];
},
onEvent: (event, state) => {
if (event.type === 'tick') {
return { state: { ...state, frame: state.frame + 1 } };
}
if (event.type === 'key' && event.key === Key.ENTER) {
return { state, done: true };
}
return { state };
}
});
Testing
For testing CLI applications built with inquirerer, use the companion
package @inquirerer/test
(also in this repo). It provides everything you need to mock stdin/stdout,
queue keypresses and readline responses, capture and snapshot output, and
even drive the built executable as a subprocess for end-to-end tests —
without you having to hand-roll any of that scaffolding.
npm install --save-dev @inquirerer/test
In-process testing (mock streams):
import { createTestEnvironment, KEY_SEQUENCES } from '@inquirerer/test';
import { Inquirerer } from 'inquirerer';
const env = createTestEnvironment();
env.sendKey(KEY_SEQUENCES.ENTER);
const prompter = new Inquirerer(env.options);
const result = await prompter.prompt({}, [
{ name: 'confirm', type: 'confirm', message: 'Continue?' }
]);
expect(result.confirm).toBe(true);
expect(env.getOutput()).toContain('Continue?');
Subprocess (E2E) testing — drive the actual built CLI:
import { runCli } from '@inquirerer/test';
const { stdout, exitCode } = await runCli('node', [CLI_ENTRY, 'search', 'hello']);
expect(exitCode).toBe(0);
expect(stdout).toContain('1 result');
See the @inquirerer/test README for the full API, including createTestFixture (temp-dir + commands runner), key-sequence constants, and snapshot normalisers.
Developing
Run the demos:
cd packages/inquirerer
pnpm dev:spinner
pnpm dev:chat
pnpm dev:upgrade
pnpm dev:prompts
Development
Setup
git clone https://github.com/constructive-io/dev-utils.git
cd dev-utils
pnpm install
pnpm build
- Test the package of interest:
cd packages/<packagename>
pnpm test:watch
Credits
🛠 Built by the Constructive team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on GitHub.
Disclaimer
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.