Claude
Skills
Sign in
Back

cli-builder

Included with Lifetime
$97 forever

Expert guide for building command-line interfaces with Node.js (Commander, Inquirer, Ora) or Python (Click, Typer, Rich). Use when creating CLI tools, terminal UX, argument parsing, or interactive prompts.

Design

What this skill does


# CLI Builder Skill

## Overview

This skill helps you build professional command-line interfaces with excellent user experience. Covers argument parsing, interactive prompts, progress indicators, colored output, and cross-platform compatibility.

## CLI Design Philosophy

### Principles of Good CLI Design
1. **Predictable**: Follow conventions users expect
2. **Helpful**: Provide clear help text and error messages
3. **Composable**: Work well with pipes and other tools
4. **Forgiving**: Accept common variations in input

### Design Guidelines
- **DO**: Use conventional flag names (`-v`, `--verbose`, `-h`, `--help`)
- **DO**: Provide meaningful exit codes
- **DO**: Support `--version` and `--help` on all commands
- **DO**: Use colors meaningfully (errors=red, success=green)
- **DON'T**: Require interactive input when running in pipes
- **DON'T**: Print to stdout when outputting errors
- **DON'T**: Ignore signals (Ctrl+C should exit cleanly)

## Node.js CLI Development

### Project Setup

```bash
# Initialize CLI project
mkdir my-cli && cd my-cli
npm init -y

# Install core dependencies
npm install commander chalk ora inquirer

# Optional: TypeScript support
npm install -D typescript @types/node @types/inquirer ts-node
```

### Package.json Configuration

```json
{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "A powerful CLI tool",
  "bin": {
    "mycli": "./bin/cli.js"
  },
  "files": [
    "bin",
    "dist"
  ],
  "scripts": {
    "build": "tsc",
    "dev": "ts-node src/cli.ts",
    "link": "npm link"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
```

### Commander.js - Command Structure

```typescript
// src/cli.ts
import { Command } from 'commander';
import { version } from '../package.json';

const program = new Command();

program
  .name('mycli')
  .description('A powerful CLI for doing awesome things')
  .version(version, '-v, --version', 'Display version number');

// Simple command
program
  .command('init')
  .description('Initialize a new project')
  .argument('[name]', 'Project name', 'my-project')
  .option('-t, --template <type>', 'Template to use', 'default')
  .option('--no-git', 'Skip git initialization')
  .option('-f, --force', 'Overwrite existing files')
  .action(async (name, options) => {
    console.log(`Creating project: ${name}`);
    console.log(`Template: ${options.template}`);
    console.log(`Git: ${options.git}`);
  });

// Command with subcommands
const config = program
  .command('config')
  .description('Manage configuration');

config
  .command('get <key>')
  .description('Get a configuration value')
  .action((key) => {
    console.log(`Getting config: ${key}`);
  });

config
  .command('set <key> <value>')
  .description('Set a configuration value')
  .action((key, value) => {
    console.log(`Setting ${key} = ${value}`);
  });

config
  .command('list')
  .description('List all configuration')
  .option('--json', 'Output as JSON')
  .action((options) => {
    if (options.json) {
      console.log(JSON.stringify({ key: 'value' }, null, 2));
    } else {
      console.log('key = value');
    }
  });

// Parse arguments
program.parse();
```

### Chalk - Colored Output

```typescript
// src/utils/logger.ts
import chalk from 'chalk';

export const logger = {
  info: (msg: string) => console.log(chalk.blue('info'), msg),
  success: (msg: string) => console.log(chalk.green('success'), msg),
  warning: (msg: string) => console.log(chalk.yellow('warning'), msg),
  error: (msg: string) => console.error(chalk.red('error'), msg),

  // Styled output
  title: (msg: string) => console.log(chalk.bold.underline(msg)),
  dim: (msg: string) => console.log(chalk.dim(msg)),

  // Formatted output
  list: (items: string[]) => {
    items.forEach(item => console.log(chalk.gray('  -'), item));
  },

  // Table-like output
  keyValue: (pairs: Record<string, string>) => {
    const maxKeyLen = Math.max(...Object.keys(pairs).map(k => k.length));
    Object.entries(pairs).forEach(([key, value]) => {
      console.log(
        chalk.cyan(key.padEnd(maxKeyLen)),
        chalk.gray(':'),
        value
      );
    });
  }
};

// Usage
logger.title('Project Configuration');
logger.keyValue({
  'Name': 'my-project',
  'Template': 'typescript',
  'Version': '1.0.0'
});
```

### Ora - Progress Spinners

```typescript
// src/utils/spinner.ts
import ora, { Ora } from 'ora';

export function createSpinner(text: string): Ora {
  return ora({
    text,
    spinner: 'dots',
    color: 'cyan'
  });
}

// Usage patterns
async function downloadWithProgress() {
  const spinner = createSpinner('Downloading dependencies...');
  spinner.start();

  try {
    await downloadFiles();
    spinner.succeed('Dependencies downloaded');
  } catch (error) {
    spinner.fail('Download failed');
    throw error;
  }
}

// Sequential spinners
async function setupProject() {
  const steps = [
    { text: 'Creating directory structure', fn: createDirs },
    { text: 'Installing dependencies', fn: installDeps },
    { text: 'Initializing git', fn: initGit },
    { text: 'Configuring project', fn: configure }
  ];

  for (const step of steps) {
    const spinner = createSpinner(step.text);
    spinner.start();
    try {
      await step.fn();
      spinner.succeed();
    } catch (error) {
      spinner.fail();
      throw error;
    }
  }
}
```

### Inquirer - Interactive Prompts

```typescript
// src/prompts/init.ts
import inquirer from 'inquirer';

interface ProjectAnswers {
  name: string;
  template: string;
  features: string[];
  initGit: boolean;
  installDeps: boolean;
}

export async function promptProjectSetup(): Promise<ProjectAnswers> {
  return inquirer.prompt([
    {
      type: 'input',
      name: 'name',
      message: 'Project name:',
      default: 'my-project',
      validate: (input) => {
        if (!/^[a-z0-9-]+$/.test(input)) {
          return 'Name must be lowercase alphanumeric with dashes';
        }
        return true;
      }
    },
    {
      type: 'list',
      name: 'template',
      message: 'Select a template:',
      choices: [
        { name: 'Minimal - Basic setup', value: 'minimal' },
        { name: 'Standard - Recommended defaults', value: 'standard' },
        { name: 'Full - Kitchen sink', value: 'full' }
      ],
      default: 'standard'
    },
    {
      type: 'checkbox',
      name: 'features',
      message: 'Select features:',
      choices: [
        { name: 'TypeScript', value: 'typescript', checked: true },
        { name: 'ESLint', value: 'eslint', checked: true },
        { name: 'Prettier', value: 'prettier', checked: true },
        { name: 'Testing (Jest)', value: 'jest' },
        { name: 'CI/CD (GitHub Actions)', value: 'github-actions' }
      ]
    },
    {
      type: 'confirm',
      name: 'initGit',
      message: 'Initialize git repository?',
      default: true
    },
    {
      type: 'confirm',
      name: 'installDeps',
      message: 'Install dependencies now?',
      default: true,
      when: (answers) => answers.template !== 'minimal'
    }
  ]);
}

// Advanced: Dynamic prompts
export async function promptWithContext(context: { hasExisting: boolean }) {
  const questions = [];

  if (context.hasExisting) {
    questions.push({
      type: 'confirm',
      name: 'overwrite',
      message: 'Directory exists. Overwrite?',
      default: false
    });
  }

  // Add more questions...

  return inquirer.prompt(questions);
}
```

### Complete CLI Example

```typescript
#!/usr/bin/env node
// bin/cli.ts

import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import inquirer from 'inquirer';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';

const program = new Command();

program
  .name('create-app')
  .description('Create a new application')
  .version('1.0.0');

program
  .command('create')
  .argument('[name]', 'Project name')
  .option('-t, --template <template>', 'Template to use')
  .option('-y, --yes', 'Skip pro
Files: 1
Size: 19.8 KB
Complexity: 24/100
Category: Design

Related in Design