cli-builder

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cli-builder" with this command: npx skills add eddiebe147/claude-settings/eddiebe147-claude-settings-cli-builder

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

  • Predictable: Follow conventions users expect

  • Helpful: Provide clear help text and error messages

  • Composable: Work well with pipes and other tools

  • 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

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

{ "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

// 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

// 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

// 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

// 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

#!/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 prompts with defaults') .action(async (name, options) => { try { // Get project name if not provided if (!name) { const { projectName } = await inquirer.prompt([{ type: 'input', name: 'projectName', message: 'Project name:', default: 'my-app' }]); name = projectName; }

  // Check if directory exists
  const projectDir = join(process.cwd(), name);
  if (existsSync(projectDir)) {
    const { overwrite } = await inquirer.prompt([{
      type: 'confirm',
      name: 'overwrite',
      message: `Directory ${name} exists. Overwrite?`,
      default: false
    }]);

    if (!overwrite) {
      console.log(chalk.yellow('Aborted.'));
      process.exit(0);
    }
  }

  // Get template if not provided
  let template = options.template;
  if (!template &#x26;&#x26; !options.yes) {
    const { selectedTemplate } = await inquirer.prompt([{
      type: 'list',
      name: 'selectedTemplate',
      message: 'Select template:',
      choices: ['minimal', 'standard', 'typescript']
    }]);
    template = selectedTemplate;
  }
  template = template || 'standard';

  console.log();
  console.log(chalk.bold(`Creating ${name} with ${template} template...`));
  console.log();

  // Create project
  const spinner = ora('Creating directory structure').start();
  mkdirSync(projectDir, { recursive: true });
  spinner.succeed();

  spinner.start('Generating files');
  writeFileSync(
    join(projectDir, 'package.json'),
    JSON.stringify({ name, version: '1.0.0' }, null, 2)
  );
  spinner.succeed();

  // Success message
  console.log();
  console.log(chalk.green.bold('Success!'), `Created ${name}`);
  console.log();
  console.log('Next steps:');
  console.log(chalk.cyan(`  cd ${name}`));
  console.log(chalk.cyan('  npm install'));
  console.log(chalk.cyan('  npm start'));
  console.log();

} catch (error) {
  console.error(chalk.red('Error:'), error.message);
  process.exit(1);
}

});

// Handle unknown commands program.on('command:*', () => { console.error(chalk.red('Unknown command:'), program.args.join(' ')); console.log('Run', chalk.cyan('create-app --help'), 'for usage'); process.exit(1); });

// Parse and handle no command program.parse();

if (!process.argv.slice(2).length) { program.help(); }

Python CLI Development

Typer - Modern Python CLI

cli.py

import typer from typing import Optional, List from enum import Enum from rich.console import Console from rich.table import Table from rich.progress import track

app = typer.Typer( name="mycli", help="A powerful CLI for doing awesome things", add_completion=True ) console = Console()

class Template(str, Enum): minimal = "minimal" standard = "standard" full = "full"

@app.command() def init( name: str = typer.Argument("my-project", help="Project name"), template: Template = typer.Option( Template.standard, "--template", "-t", help="Template to use" ), features: List[str] = typer.Option( [], "--feature", "-f", help="Features to include" ), no_git: bool = typer.Option( False, "--no-git", help="Skip git initialization" ), force: bool = typer.Option( False, "--force", "-f", help="Overwrite existing files" ) ): """Initialize a new project.""" console.print(f"[bold]Creating project:[/bold] {name}") console.print(f"[dim]Template:[/dim] {template.value}")

# Progress indicator
for step in track(range(5), description="Setting up..."):
    # Do work
    pass

console.print("[green]Success![/green] Project created")

@app.command() def config( key: str = typer.Argument(..., help="Configuration key"), value: Optional[str] = typer.Argument(None, help="Value to set") ): """Get or set configuration values.""" if value is None: # Get config console.print(f"{key} = some_value") else: # Set config console.print(f"Set {key} = {value}")

@app.command() def status(): """Show project status.""" table = Table(title="Project Status") table.add_column("Property", style="cyan") table.add_column("Value", style="green")

table.add_row("Name", "my-project")
table.add_row("Version", "1.0.0")
table.add_row("Template", "standard")

console.print(table)

Subcommand group

db_app = typer.Typer(help="Database operations") app.add_typer(db_app, name="db")

@db_app.command("migrate") def db_migrate( direction: str = typer.Option("up", "--direction", "-d"), steps: int = typer.Option(1, "--steps", "-n") ): """Run database migrations.""" console.print(f"Running {steps} migration(s) {direction}")

@db_app.command("seed") def db_seed(): """Seed the database.""" console.print("Seeding database...")

if name == "main": app()

Click - Flexible Python CLI

cli_click.py

import click from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn

console = Console()

@click.group() @click.version_option(version="1.0.0") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.pass_context def cli(ctx, verbose): """A powerful CLI for doing awesome things.""" ctx.ensure_object(dict) ctx.obj["verbose"] = verbose

@cli.command() @click.argument("name", default="my-project") @click.option( "--template", "-t", type=click.Choice(["minimal", "standard", "full"]), default="standard", help="Template to use" ) @click.option("--no-git", is_flag=True, help="Skip git initialization") @click.confirmation_option(prompt="Create project?") @click.pass_context def init(ctx, name, template, no_git): """Initialize a new project.""" if ctx.obj["verbose"]: console.print(f"[dim]Verbose mode enabled[/dim]")

with Progress(
    SpinnerColumn(),
    TextColumn("[progress.description]{task.description}"),
    transient=True,
) as progress:
    task = progress.add_task("Creating project...", total=None)
    # Do work
    import time
    time.sleep(1)

console.print(f"[green]Created {name} with {template} template[/green]")

@cli.group() def config(): """Manage configuration.""" pass

@config.command("get") @click.argument("key") def config_get(key): """Get a configuration value.""" console.print(f"{key} = value")

@config.command("set") @click.argument("key") @click.argument("value") def config_set(key, value): """Set a configuration value.""" console.print(f"Set {key} = {value}")

@cli.command() @click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") def status(format): """Show project status.""" if format == "json": click.echo('{"status": "ok"}') else: console.print("[bold]Status:[/bold] OK")

if name == "main": cli()

Advanced Patterns

Configuration Management

// src/config.ts import { homedir } from 'os'; import { join } from 'path'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';

interface Config { apiKey?: string; defaultTemplate?: string; analytics?: boolean; }

class ConfigManager { private configDir: string; private configPath: string; private config: Config;

constructor() { this.configDir = join(homedir(), '.mycli'); this.configPath = join(this.configDir, 'config.json'); this.config = this.load(); }

private load(): Config { if (!existsSync(this.configPath)) { return {}; } try { return JSON.parse(readFileSync(this.configPath, 'utf-8')); } catch { return {}; } }

private save(): void { if (!existsSync(this.configDir)) { mkdirSync(this.configDir, { recursive: true }); } writeFileSync(this.configPath, JSON.stringify(this.config, null, 2)); }

get<K extends keyof Config>(key: K): Config[K] { return this.config[key]; }

set<K extends keyof Config>(key: K, value: Config[K]): void { this.config[key] = value; this.save(); }

getAll(): Config { return { ...this.config }; }

clear(): void { this.config = {}; this.save(); } }

export const config = new ConfigManager();

Error Handling

// src/errors.ts import chalk from 'chalk';

export class CLIError extends Error { constructor( message: string, public readonly code: string = 'ERROR', public readonly suggestion?: string ) { super(message); this.name = 'CLIError'; } }

export function handleError(error: unknown): never { if (error instanceof CLIError) { console.error(chalk.red(Error [${error.code}]:), error.message); if (error.suggestion) { console.error(chalk.yellow('Suggestion:'), error.suggestion); } process.exit(1); }

if (error instanceof Error) { console.error(chalk.red('Unexpected error:'), error.message); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); }

console.error(chalk.red('Unknown error occurred')); process.exit(1); }

// Usage process.on('uncaughtException', handleError); process.on('unhandledRejection', handleError);

Non-Interactive Mode Detection

// src/utils/tty.ts import { stdin, stdout } from 'process';

export function isInteractive(): boolean { return stdin.isTTY && stdout.isTTY; }

export function requireInteractive(message?: string): void { if (!isInteractive()) { console.error(message || 'This command requires an interactive terminal'); process.exit(1); } }

// Usage in command async function initCommand(options: { yes?: boolean }) { if (options.yes || !isInteractive()) { // Use defaults, skip prompts return runWithDefaults(); }

// Interactive prompts const answers = await promptUser(); return runWithAnswers(answers); }

Output Formatting

// src/utils/output.ts import { stdout } from 'process';

export type OutputFormat = 'text' | 'json' | 'table';

export function output(data: unknown, format: OutputFormat = 'text'): void { switch (format) { case 'json': console.log(JSON.stringify(data, null, 2)); break; case 'table': console.table(data); break; case 'text': default: if (typeof data === 'string') { console.log(data); } else { console.log(JSON.stringify(data, null, 2)); } } }

// Check if output is piped export function isPiped(): boolean { return !stdout.isTTY; }

// Suppress decorative output when piped export function log(message: string): void { if (!isPiped()) { console.log(message); } }

CLI Checklist

Core Features

  • --help on all commands

  • --version flag

  • Meaningful exit codes

  • Error messages to stderr

  • Support for environment variables

User Experience

  • Progress indicators for long operations

  • Colored output (with NO_COLOR support)

  • Interactive prompts (with non-interactive fallback)

  • Tab completion setup

Best Practices

  • Works in pipes (echo "data" | mycli process )

  • Handles Ctrl+C gracefully

  • Configuration file support

  • Debug/verbose mode

  • Consistent command structure

Distribution

  • npm/PyPI package configured

  • Binary entry point set up

  • README with installation and usage

  • Changelog maintained

When to Use This Skill

Invoke this skill when:

  • Creating new CLI tools from scratch

  • Adding commands to existing CLIs

  • Building interactive prompts and wizards

  • Implementing progress indicators

  • Setting up argument parsing

  • Creating configuration management

  • Designing CLI UX patterns

  • Publishing CLI tools to npm or PyPI

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

referral program designer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

client onboarding designer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cli-builder

No summary provided by upstream source.

Repository SourceNeeds Review