inquirerer-cli-building
npx skills add https://github.com/constructive-io/constructive-skills --skill inquirerer-cli-building
Agent 安装分布
Skill 文档
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 overridesetFrom: 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
defaultFromfor 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-patternsfor what NOT to do - Related skill:
pnpm-workspacefor monorepo setup