npm package: @kelceyp/clibuilder
Description: Build CLIs quickly with consistent UX, typed params, and interactive prompting
Author: Paul Kelcey
Latest version: 0.2.1

# 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

```bash
# Placeholder - package not yet published
npm install @kelceyp/clibuilder
# or
bun add @kelceyp/clibuilder
```

## Getting Started

Here's a comprehensive example showing the full builder chain:

```typescript
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

```bash
# 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 --debug
```

## Key Features

### Provenance Tracking

Every parameter value includes provenance metadata showing where it came from and the original raw input:

```typescript
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 variables
- `stdin` - From piped/redirected input
- `default` - From default values
- `prompt` - From interactive prompts (TTY only)

### Debug Output with --debug Flag

Add `--debug` to any command to see how parameters were resolved:

```bash
mycli db backup mydb --output ./backups --debug
```

Output:
```
📊 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:

```typescript
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()`:

```typescript
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:**

```bash
# Interactive mode (prompts user)
mycli deploy production

# Non-interactive mode (uses defaults, skips confirm prompts)
mycli deploy production --yes
mycli deploy production -y
```

In non-interactive mode:
- `confirm` prompts return `true`
- `input` prompts return their default value (or empty string if no default)
- `multiselect` prompts return empty array
- `select` prompts 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:

```typescript
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

```bash
# Install dependencies
bun install

# Build
bun run build

# Test
bun run test

# Type check
bun run typecheck

# Lint
bun run lint
```

**Note:** 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 TypeScript
- `bun run test` → `scripts/test.ts` - Run Bun tests
- `bun run lint` → `scripts/lint.ts` - Lint code
- `bun run typecheck` → `scripts/typecheck.ts` - Type check
- `bun 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 needed
```

## Development 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:

1. ✅ **Foundation** - Types, interfaces, builder skeletons
2. ✅ **Param System** - Parsing, coercion, resolution pipeline
3. ✅ **Command System** - Composite pattern, dispatch, routing
4. ✅ **Execution** - Context, prompts, validation, run handlers
5. ✅ **Provenance** - Parameter source tracking, debug output
6. ✅ **Corrections** - Serializable specs, declarative validation
7. ✅ **UX** - Help generation, colors, spinners, errors
8. ✅ **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