Building CLI Tools with inquirerer
A comprehensive guide to building interactive command-line interfaces using inquirerer, the TypeScript-first CLI library used across Constructive projects.
When to Apply
Use this skill when:
-
Creating a new CLI application
-
Adding interactive prompts to an existing tool
-
Building project scaffolding or setup wizards
-
Creating configuration builders
-
Implementing any command-line interface in a Constructive project
Installation
pnpm add inquirerer
Quick Start
import { Inquirerer } from 'inquirerer';
const prompter = new Inquirerer();
const answers = await prompter.prompt({}, [ { type: 'text', name: 'projectName', message: 'What is your project name?', required: true }, { type: 'confirm', name: 'useTypeScript', message: 'Use TypeScript?', default: true } ]);
console.log(answers); prompter.close();
Question Types
inquirerer supports six question types:
Text Question
Collect string input:
{ type: 'text', name: 'username', message: 'Enter your username', required: true, pattern: '^[a-z0-9_]+$', // Regex validation default: 'user' }
Number Question
Collect numeric input:
{ type: 'number', name: 'port', message: 'Server port?', default: 3000, validate: (port) => port > 0 && port < 65536 }
Confirm Question
Yes/no questions:
{ type: 'confirm', name: 'proceed', message: 'Continue with installation?', default: true }
List Question
Select one option (no search):
{ type: 'list', name: 'license', message: 'Choose a license', options: ['MIT', 'Apache-2.0', 'GPL-3.0'], default: 'MIT', maxDisplayLines: 5 }
Autocomplete Question
Select with fuzzy search:
{ type: 'autocomplete', name: 'framework', message: 'Choose a framework', options: [ { name: 'React', value: 'react' }, { name: 'Vue.js', value: 'vue' }, { name: 'Angular', value: 'angular' } ], allowCustomOptions: true, maxDisplayLines: 8 }
Checkbox Question
Multi-select with search:
{ type: 'checkbox', name: 'features', message: 'Select features', options: ['Auth', 'Database', 'API', 'Testing'], default: ['Auth', 'API'], returnFullResults: false, // Only return selected items required: true }
Question Properties
All questions support these base properties:
Property Type Description
name
string Property name in result object
type
string Question type
message
string Prompt message to display
default
any Default value
required
boolean Whether input is required
validate
function Custom validation function
sanitize
function Transform input before storing
pattern
string Regex pattern for validation
when
function Conditional display
dependsOn
string[] Question dependencies
_
boolean Mark as positional argument
alias
string/string[] Short flag aliases
defaultFrom
string Dynamic default from resolver
setFrom
string Auto-set value from resolver
Validation
Pattern Validation
{ type: 'text', name: 'email', message: 'Enter email', pattern: '^[^@]+@[^@]+\.[^@]+$' }
Custom Validation
{ type: 'text', name: 'password', message: 'Enter password', validate: (input) => { if (input.length < 8) { return { success: false, reason: 'Must be at least 8 characters' }; } return { success: true }; } }
Sanitization
{ type: 'text', name: 'tags', message: 'Enter tags (comma-separated)', sanitize: (input) => input.split(',').map(t => t.trim()) }
Conditional Questions
Show questions based on previous answers:
const questions = [ { type: 'confirm', name: 'useDatabase', message: 'Need a database?', default: false }, { type: 'list', name: 'database', message: 'Which database?', options: ['PostgreSQL', 'MySQL', 'SQLite'], when: (answers) => answers.useDatabase === true } ];
Question Dependencies
Ensure questions appear in correct order:
[ { type: 'checkbox', name: 'services', message: 'Select services', options: ['Auth', 'Storage', 'Functions'] }, { type: 'text', name: 'authProvider', message: 'Auth provider?', dependsOn: ['services'], when: (answers) => answers.services?.includes('Auth') } ]
Positional Arguments
Allow values without flags using _: true :
const questions = [ { _: true, name: 'source', type: 'text', message: 'Source file' }, { _: true, name: 'dest', type: 'text', message: 'Destination' } ];
// Users can run: mycli input.txt output.txt // Instead of: mycli --source input.txt --dest output.txt
Aliases
Define short flags:
{ name: 'workspace', type: 'confirm', alias: 'w', // or ['w', 'ws'] for multiple message: 'Create workspace?' }
// Users can run: mycli -w // Instead of: mycli --workspace
Dynamic Defaults with Resolvers
Auto-populate defaults from git, npm, or custom sources:
const questions = [ { type: 'text', name: 'author', message: 'Author name?', defaultFrom: 'git.user.name' // Auto-fills from git config }, { type: 'text', name: 'email', message: 'Email?', defaultFrom: 'git.user.email' }, { type: 'text', name: 'year', message: 'Copyright year?', defaultFrom: 'date.year' } ];
Built-in Resolvers
Resolver Description
git.user.name
Git global user name
git.user.email
Git global user email
npm.whoami
Logged in npm user
date.year
Current year
date.month
Current month
date.day
Current day
date.iso
ISO date (YYYY-MM-DD)
workspace.name
Package name from nearest package.json
workspace.license
License from package.json
workspace.author
Author from package.json
Custom Resolvers
import { registerDefaultResolver } from 'inquirerer';
registerDefaultResolver('cwd.name', () => { return process.cwd().split('/').pop(); });
// Use in questions { type: 'text', name: 'projectName', defaultFrom: 'cwd.name' }
setFrom vs defaultFrom
-
defaultFrom : Sets as default, user can override
-
setFrom : Auto-sets value, skips prompt entirely
{ type: 'text', name: 'createdAt', setFrom: 'date.iso' // Auto-set, no prompt shown }
CLI Class
For complete CLI applications with argument parsing:
import { CLI, CommandHandler, CLIOptions } from 'inquirerer';
const handler: CommandHandler = async (argv, prompter, options) => { const answers = await prompter.prompt(argv, [ { type: 'text', name: 'name', message: 'Name?', required: true } ]); console.log('Hello,', answers.name); };
const options: Partial<CLIOptions> = { version: 'myapp@1.0.0', minimistOpts: { alias: { v: 'version', h: 'help' } } };
const cli = new CLI(handler, options); await cli.run();
CLI Utilities
inquirerer provides utilities for building CLIs:
import { parseArgv, // Parse command-line arguments extractFirst, // Extract subcommand getPackageVersion, // Get version from package.json cliExitWithError // Exit with error message } from 'inquirerer';
const argv = parseArgv(process.argv); const { first: command, newArgv } = extractFirst(argv);
switch (command) { case 'init': await handleInit(newArgv); break; case 'build': await handleBuild(newArgv); break; default: console.log('Unknown command'); }
UI Components
Spinner
import { createSpinner } from 'inquirerer';
const spinner = createSpinner('Loading...'); spinner.start(); await doWork(); spinner.succeed('Done!'); // Or: spinner.fail('Failed'), spinner.warn('Warning')
Progress Bar
import { createProgress } from 'inquirerer';
const progress = createProgress('Installing'); progress.start(); for (let i = 0; i < items.length; i++) { await processItem(items[i]); progress.update((i + 1) / items.length); } progress.complete('Installed');
Streaming Text
import { createStream } from 'inquirerer';
const stream = createStream({ showCursor: true }); stream.start(); for await (const token of llmResponse) { stream.append(token); } stream.done();
Non-Interactive Mode
For CI/CD environments:
const prompter = new Inquirerer({ noTty: true, // Disable interactive mode useDefaults: true // Use defaults without prompting });
Complete Example
import { Inquirerer, Question, parseArgv } from 'inquirerer';
interface ProjectConfig { name: string; description: string; typescript: boolean; features: string[]; }
const argv = parseArgv(process.argv); const prompter = new Inquirerer();
const questions: Question[] = [ { _: true, type: 'text', name: 'name', message: 'Project name', required: true, pattern: '^[a-z0-9-]+$', defaultFrom: 'cwd.name' }, { type: 'text', name: 'description', message: 'Description', default: 'My awesome project' }, { type: 'confirm', name: 'typescript', alias: 'ts', message: 'Use TypeScript?', default: true }, { type: 'checkbox', name: 'features', message: 'Select features', options: ['ESLint', 'Prettier', 'Jest', 'Husky'], default: ['ESLint', 'Prettier'] } ];
const config = await prompter.prompt<ProjectConfig>(argv, questions); console.log('Creating project:', config); prompter.close();
Run interactively or with CLI args:
Interactive
node setup.js
With args
node setup.js my-project --ts --features ESLint,Jest
Best Practices
-
Always close the prompter when done: prompter.close()
-
Use TypeScript interfaces for type-safe answers
-
Provide defaults for better UX
-
Use defaultFrom for dynamic defaults from git/npm
-
Support non-interactive mode for CI/CD
-
Use positional arguments for common inputs
-
Add aliases for frequently used flags
-
Validate early with patterns and custom validators
References
-
npm package: https://www.npmjs.com/package/inquirerer
-
Related skill: inquirerer-anti-patterns for what NOT to do
-
Related skill: pnpm-workspace for monorepo setup