@kelceyp/clibuilder
v0.2.1
Published
Build CLIs quickly with consistent UX, typed params, and interactive prompting
Downloads
78
Maintainers
Readme
CLI Builder
⚠️ API Draft - Subject to Change
Build CLIs quickly with consistent UX, typed params, and interactive prompting.
Current Status
🎉 PROJECT COMPLETE - All 9 implementation bunches finished! 🎉
Final Metrics:
- 943 tests passing (100%)
- Sub-millisecond dispatch performance
- 1000+ lines of user documentation
- Full requirements compliance
- Fully integrated help system
- Production-ready for npm publish! 🚀
Track progress: See tracking/STATUS.md for current version status (v0.1 complete, v0.2 in progress)
Project Goals
Build command-line interfaces with:
- Fast Development: Fluent builder API for quick CLI construction
- Type Safety: TypeScript generics infer parameter types automatically
- Consistent UX: Built-in colors, spinners, and error formatting
- Flexible Resolution: Multi-source parameter resolution (argv → stdin → env → default → prompt)
- Nested Commands: Composite pattern for arbitrary command nesting (
mycli db migrations create) - Interactive Prompts: Auto-prompting with TTY detection (safe for CI/non-TTY)
- Works Everywhere: Node ≥18 and Bun ≥1.1; bash/zsh/fish compatible
Full spec: @tracking/v0.1/requirements.md (v0.1 complete), @tracking/v0.2/requirements.md (v0.2 in progress)
Installation
# Placeholder - package not yet published
npm install @kelceyp/clibuilder
# or
bun add @kelceyp/clibuilderGetting Started
Here's a comprehensive example showing the full builder chain:
import { create, createCommand, createCommandGroup, createParam } from '@kelceyp/clibuilder';
// Define a command with typed parameters
const backupCommand = createCommand('backup')
.summary('Create a database backup')
.description('Creates a compressed backup of the specified database')
.aliases(['bak'])
// Positional parameter (required)
.param((p) =>
p.name('database')
.type('string')
.positional(0)
.required()
.validate((value) => {
if (value.length === 0) return 'Database name cannot be empty';
return true;
})
)
// Optional flag parameter with environment variable fallback
.param((p) =>
p.name('output')
.type('string')
.flag('output', 'o')
.env('BACKUP_OUTPUT_DIR')
.default('./backups')
.optional()
)
// Boolean flag parameter
.param((p) =>
p.name('compress')
.type('boolean')
.flag('compress', 'c')
.default(false)
)
// Array parameter
.param((p) =>
p.name('exclude')
.type('string[]')
.flag('exclude', 'e')
.optional()
)
// Define the command handler with typed context
.run(async (ctx) => {
const { database, output, compress, exclude } = ctx.params;
// ctx.params is fully typed based on the params defined above:
// - database: string (required)
// - output: string (has default, always present)
// - compress: boolean (has default, always present)
// - exclude: string[] | undefined (optional)
// Access provenance to see where values came from
const dbProvenance = ctx.provenance.database;
if (database === 'prod' && dbProvenance.source === 'default') {
throw new Error('Production database must be explicitly specified, not defaulted');
}
const spinner = ctx.spinner('Creating backup...');
spinner.start();
try {
// Simulate backup operation
await new Promise((resolve) => setTimeout(resolve, 2000));
spinner.succeed(`Backed up ${database} to ${output}`);
} catch (error) {
spinner.fail('Backup failed');
throw error;
}
})
.onError({ exitCode: 1, showStack: 'auto' })
.build();
// Define a migration command
const createMigrationCommand = createCommand('create')
.summary('Create a new migration file')
.param((p) =>
p.name('name')
.type('string')
.positional(0)
.required()
)
.run(async (ctx) => {
ctx.logger.info(`Creating migration: ${ctx.params.name}`);
})
.build();
const runMigrationsCommand = createCommand('run')
.summary('Run pending migrations')
.param((p) =>
p.name('target')
.type('string')
.flag('target', 't')
.optional()
)
.run(async (ctx) => {
ctx.logger.info('Running migrations...');
})
.build();
// Create a nested command group structure
const dbGroup = createCommandGroup('db')
.summary('Database operations')
.description('Commands for database management and maintenance')
.command(backupCommand)
.defaultChild('backup') // Run backup command when 'mycli db' is invoked alone
// Nested command group for migrations
.commandGroup('migrations')
.summary('Migration management')
.command(createMigrationCommand)
.command(runMigrationsCommand)
.defaultChild('run') // Run migrations when 'mycli db migrations' is invoked alone
.build() // Returns to parent group
.build();
// Create the top-level app and register commands/groups
const app = create()
.name('mycli')
.version('1.0.0')
.description('My awesome CLI tool')
.strict(true)
.colorScheme({
primary: '#00D9FF',
accent: '#A855F7',
warn: '#FBBF24',
error: '#EF4444'
})
.spinner({ style: 'dots', color: 'cyan' })
// Register the command group
.group(dbGroup)
// Register a top-level command
.command(
createCommand('init')
.summary('Initialize configuration')
.run(async (ctx) => {
ctx.logger.info('Initializing...');
})
.build()
);
// Run the CLI (in your CLI entry file)
// app.run(process.argv.slice(2));Usage Examples
# Run a nested command with parameters
mycli db backup mydb --output ./backups --compress --exclude logs --exclude cache
# Use short flags
mycli db backup mydb -o ./backups -c
# Run nested migration commands
mycli db migrations create add_users_table
mycli db migrations run --target 20231215
# Use defaultChild - 'mycli db' automatically runs 'mycli db backup'
mycli db mydb # Equivalent to: mycli db backup mydb
# Get help at any level
mycli --help
mycli db --help
mycli db migrations --help
mycli db backup --help
# Debug parameter resolution with --debug flag
mycli db backup mydb --output ./backups --debugKey Features
Provenance Tracking
Every parameter value includes provenance metadata showing where it came from and the original raw input:
const cmd = createCommand('deploy')
.param((p) => p.name('env').type('string').positional(0).required())
.run(async (ctx) => {
const env = ctx.params.env;
// Access provenance to see where the value came from
const envProvenance = ctx.provenance.env;
console.log(`Environment: ${envProvenance.value}`);
console.log(`Source: ${envProvenance.source}`); // 'argv' | 'env' | 'stdin' | 'default' | 'prompt'
console.log(`Raw input: ${envProvenance.raw}`); // Original string before type coercion
// Use provenance for custom validation
if (env === 'production' && envProvenance.source === 'default') {
throw new Error('Production environment must be explicitly specified');
}
})
.build();Provenance sources:
argv- From command-line arguments (flags or positional)env- From environment variablesstdin- From piped/redirected inputdefault- From default valuesprompt- From interactive prompts (TTY only)
Debug Output with --debug Flag
Add --debug to any command to see how parameters were resolved:
mycli db backup mydb --output ./backups --debugOutput:
📊 Parameter Resolution Debug Info
┌───────────┬───────────┬─────────┬───────────┐
│ Parameter │ Value │ Source │ Raw │
├───────────┼───────────┼─────────┼───────────┤
│ database │ mydb │ argv │ mydb │
│ output │ ./backups │ argv │ ./backups │
│ compress │ false │ default │ - │
│ exclude │ undefined │ default │ - │
└───────────┴───────────┴─────────┴───────────┘The debug table shows:
- Parameter: Parameter name
- Value: Resolved value after type coercion
- Source: Where the value came from (argv, env, stdin, default, prompt)
- Raw: Original input string before coercion (or "-" for defaults)
Default Child for Command Groups
Command groups can specify a default child command to run when invoked without an explicit subcommand:
const dbGroup = createCommandGroup('db')
.command(statusCommand)
.command(backupCommand)
.command(restoreCommand)
.defaultChild('status') // Run status when 'mycli db' is invoked
.build();
// Usage:
// mycli db -> runs 'mycli db status' (via defaultChild)
// mycli db backup -> runs 'mycli db backup' (explicit override)
// mycli db restore -> runs 'mycli db restore' (explicit override)Benefits:
- Provides sensible defaults for common operations
- Reduces typing for frequently-used commands
- Explicit subcommands always override the default
- Validated at build time (throws error if defaultChild references non-existent child)
Runtime Prompts
Ask for user confirmation or input during command execution with ctx.prompt():
const deployCommand = createCommand('deploy')
.param((p) => p.name('env').type('string').positional(0).required())
.run(async (ctx) => {
const { env } = ctx.params;
// Contextual confirmation prompt
if (env === 'production') {
const confirmed = await ctx.prompt({
type: 'confirm',
message: `Deploy to PRODUCTION? This will affect live users.`,
default: false
});
if (!confirmed) {
ctx.logger.info('Deployment cancelled');
return;
}
}
// Input prompt for additional data
const tag = await ctx.prompt({
type: 'input',
message: 'Enter release tag:',
default: `v${new Date().toISOString().split('T')[0]}`
});
ctx.logger.info(`Deploying to ${env} with tag ${tag}...`);
// ... deployment logic
})
.build();Non-interactive mode with --yes flag:
# Interactive mode (prompts user)
mycli deploy production
# Non-interactive mode (uses defaults, skips confirm prompts)
mycli deploy production --yes
mycli deploy production -yIn non-interactive mode:
confirmprompts returntrueinputprompts return their default value (or empty string if no default)multiselectprompts return empty arrayselectprompts throw error if no default is provided
Prompt types:
confirm- Yes/no question (returns boolean)input- Text input (returns string)select- Single choice from list (returns string)multiselect- Multiple choices from list (returns string[])
Runtime prompts automatically respect TTY detection and the --yes flag, making them safe for CI/CD environments.
Pre-Execution Validation Hooks
Validate parameters BEFORE prompting users, saving time when conditions aren't met:
const deployCommand = createCommand('deploy')
.param((p) => p.name('env').type('string').positional(0).required())
.param((p) => p.name('confirm').type('boolean').flag('confirm', 'c').optional())
// Validate before prompting
.preValidate((ctx) => {
// Access raw argv, stdin, and environment
const { flags, positionals } = ctx.argv;
const { isCI } = ctx.env;
// Require --confirm in CI mode
if (isCI && !flags.confirm) {
return 'Confirmation required in CI mode. Use --confirm flag.';
}
return true; // Validation passed
})
.run(async (ctx) => {
// Only runs if pre-validation passes
ctx.logger.info(`Deploying to ${ctx.params.env}...`);
})
.build();Use cases:
- Mutually exclusive flags
- Stdin conflict detection
- Environment-based requirements
- Complex cross-parameter validation
Features:
- Sync or async validation
- Timeout protection (2s default)
- Access to raw argv, stdin detection, environment metadata
- Exit code 2 on validation failure
- Help/version bypass validation
Tech Stack
- Runtime: Node ≥18 and Bun ≥1.1
- Language: TypeScript (strict mode)
- Module System: ESM only
- Testing: Bun's native test runner
- Dependencies: Minimal (chalk, ora)
Quick Start
# Install dependencies
bun install
# Build
bun run build
# Test
bun run test
# Type check
bun run typecheck
# Lint
bun run lintNote: This project uses Bun. While npm works, bun install is recommended for faster installs and consistency with the runtime.
Scripts Are Source of Truth
Automation lives in scripts/. Each script is executable and referenced in package.json.
Available scripts:
bun run build→scripts/build.ts- Compile TypeScriptbun run test→scripts/test.ts- Run Bun testsbun run lint→scripts/lint.ts- Lint codebun run typecheck→scripts/typecheck.ts- Type checkbun run clean→scripts/clean.ts- Remove build artifacts
See: @docs/cartridges/runbooks/creating-scripts.md for script patterns
Project Structure
scripts/ Source of truth for automation
src/ Implementation (types, builders, errors)
tests/ Test files (mirrors src/)
tracking/ Progress tracking, checklists, requirements
├── STATUS.md Version status overview
├── v0.1/ v0.1 artifacts (completed)
│ ├── STATUS.md v0.1 completion summary
│ ├── requirements.md v0.1 spec
│ └── implementation-plan.md v0.1 phases
└── v0.2/ v0.2 artifacts (in progress)
├── STATUS.md Current v0.2 status
└── requirements.md v0.2 spec
docs/
└── cartridges/ Reusable knowledge loaded when neededDevelopment Workflow
This project uses structured do/review loops with checklists tracking progress.
For details: @docs/cartridges/methodology/dev-runbook.md
Key points:
- Work organized into "bunches" (phases)
- Each bunch has a detailed checklist
- Every change reviewed before proceeding
- Checklists updated as work progresses
For AI Agents
Supervisors: Read @docs/cartridges/methodology/supervisor.md for orchestrating work
Workers: Check current checklist's "Required Reading" section before starting
Implementation Bunches
All 8 bunches complete:
- ✅ Foundation - Types, interfaces, builder skeletons
- ✅ Param System - Parsing, coercion, resolution pipeline
- ✅ Command System - Composite pattern, dispatch, routing
- ✅ Execution - Context, prompts, validation, run handlers
- ✅ Provenance - Parameter source tracking, debug output
- ✅ Corrections - Serializable specs, declarative validation
- ✅ UX - Help generation, colors, spinners, errors
- ✅ Polish & Integration - Signals, edge cases, examples, CI
See: tracking/v0.1/implementation-plan.md for v0.1 detailed breakdown
See: tracking/v0.1/STATUS.md for v0.1 completion summary
See: tracking/v0.2/STATUS.md for v0.2 current status
License
MIT
