Claude Code Plugins

Community-maintained marketplace

Feedback

Apply modern CLI development toolchain patterns: Commander.js (default), oclif, Ink for Node.js command-line tools. Use when building CLI applications, choosing CLI frameworks, or discussing terminal UX.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name cli-toolchain
description Apply modern CLI development toolchain patterns: Commander.js (default), oclif, Ink for Node.js command-line tools. Use when building CLI applications, choosing CLI frameworks, or discussing terminal UX.

CLI Toolchain

Modern command-line application development with Node.js and TypeScript.

Recommended Stack: Commander.js (Default)

Why Commander.js (2025):

  • Lightweight library (not a framework)
  • Unopinionated (full control over structure)
  • Standard in Node.js ecosystem (24M+ downloads/week)
  • Excellent TypeScript support
  • Simple API for argument parsing and subcommands
  • Battle-tested (used by Vue CLI, Create React App, etc.)
# Install
npm install commander

# Create CLI entry point
// bin/cli.ts
#!/usr/bin/env node
import { Command } from 'commander'
import { version } from '../package.json'

const program = new Command()

program
  .name('my-cli')
  .description('CLI tool for awesome things')
  .version(version)

program
  .command('create <name>')
  .description('Create a new project')
  .option('-t, --template <type>', 'Project template', 'default')
  .action((name, options) => {
    console.log(`Creating project: ${name}`)
    console.log(`Template: ${options.template}`)
  })

program.parse()

When to Use Commander.js

✅ Standard CLIs with subcommands and options ✅ Want lightweight, minimal overhead ✅ Need full control over implementation ✅ Simple argument parsing requirements ✅ Most use cases (90%+)

Alternative: oclif

Enterprise-grade CLI framework:

  • Full framework (not just parsing)
  • Plugin system
  • Auto-generated documentation
  • Testing utilities
  • Used by Salesforce, Heroku CLIs
# Create new CLI with oclif
npx oclif generate my-cli

# Structure enforced by framework
my-cli/
├── src/
│   ├── commands/       # Command files
│   └── hooks/          # Lifecycle hooks
├── test/
└── package.json

When to Use oclif

✅ Large CLIs with many commands (20+) ✅ Need plugin architecture ✅ Auto-documentation required ✅ Enterprise/team projects ✅ Want opinionated structure

Alternative: Ink

React for CLIs:

  • Build interactive UIs with React components
  • Flexbox layout for terminal
  • Rich, interactive experiences (dashboards, progress, forms)
# Install
npm install ink react

# Create interactive CLI
// bin/ui.tsx
import React from 'react'
import { render, Box, Text } from 'ink'

const App = () => (
  <Box flexDirection="column">
    <Text color="green">✓ Task completed</Text>
    <Text>Processing...</Text>
  </Box>
)

render(<App />)

When to Use Ink

✅ Rich interactive UI needed (dashboards, loaders) ✅ Complex terminal layouts ✅ Team familiar with React ⚠️ Overkill for simple CLIs (use Commander.js)

Toolchain Comparison

Commander.js oclif Ink
Type Library Framework UI Library
Setup Minimal Scaffold Manual
Use Case General purpose Large/complex Interactive UI
Learning Curve Low Medium Medium (React)
Bundle Size Small Large Medium
Flexibility High Medium High
Documentation Good Excellent Good

Project Structure (Commander.js)

my-cli/
├── src/
│   ├── commands/           # Command implementations
│   │   ├── create.ts
│   │   ├── build.ts
│   │   └── deploy.ts
│   ├── utils/              # Shared utilities
│   │   ├── logger.ts
│   │   ├── config.ts
│   │   └── spinner.ts
│   ├── types/              # TypeScript types
│   └── index.ts            # Main CLI entry
├── bin/
│   └── cli                 # Executable (symlink to dist)
├── package.json
└── tsconfig.json

Essential Patterns

Subcommands

// src/index.ts
import { Command } from 'commander'
import { createCommand } from './commands/create'
import { buildCommand } from './commands/build'

const program = new Command()

program
  .addCommand(createCommand)
  .addCommand(buildCommand)

program.parse()
// src/commands/create.ts
import { Command } from 'commander'

export const createCommand = new Command('create')
  .description('Create a new project')
  .argument('<name>', 'Project name')
  .option('-t, --template <type>', 'Template type', 'default')
  .action(async (name, options) => {
    console.log(`Creating: ${name}`)
    // Implementation
  })

Arguments & Options

program
  .command('deploy')
  // Required argument
  .argument('<app>', 'Application to deploy')
  // Optional argument with default
  .argument('[environment]', 'Environment', 'production')
  // Boolean option
  .option('-d, --dry-run', 'Dry run mode')
  // Option with value
  .option('-r, --region <region>', 'Deployment region', 'us-east-1')
  // Option with choices
  .option('-l, --log-level <level>', 'Log level', 'info')
  .choices(['debug', 'info', 'warn', 'error'])
  // Variadic option (multiple values)
  .option('-e, --env <pairs...>', 'Environment variables')
  .action((app, environment, options) => {
    console.log({ app, environment, ...options })
  })

Interactive Prompts

# Install prompts library
npm install inquirer @types/inquirer
import inquirer from 'inquirer'

program
  .command('init')
  .action(async () => {
    const answers = await inquirer.prompt([
      {
        type: 'input',
        name: 'projectName',
        message: 'Project name:',
        default: 'my-app',
      },
      {
        type: 'list',
        name: 'template',
        message: 'Choose template:',
        choices: ['React', 'Vue', 'Vanilla'],
      },
      {
        type: 'confirm',
        name: 'useTypeScript',
        message: 'Use TypeScript?',
        default: true,
      },
    ])

    console.log('Creating project with:', answers)
  })

Progress & Spinners

npm install ora chalk
import ora from 'ora'
import chalk from 'chalk'

async function deploy() {
  const spinner = ora('Deploying application...').start()

  try {
    await performDeploy()
    spinner.succeed(chalk.green('Deployed successfully!'))
  } catch (error) {
    spinner.fail(chalk.red('Deployment failed'))
    console.error(error)
    process.exit(1)
  }
}

Configuration Files

// src/utils/config.ts
import { cosmiconfigSync } from 'cosmiconfig'

export function loadConfig() {
  const explorer = cosmiconfigSync('my-cli')
  const result = explorer.search()

  if (!result) {
    return {} // Default config
  }

  return result.config
}

// Looks for:
// - .my-clirc
// - .my-clirc.json
// - .my-clirc.yaml
// - my-cli.config.js
// - "my-cli" field in package.json

Essential Libraries

# Argument parsing (if not using Commander.js)
npm install yargs

# Interactive prompts
npm install inquirer

# Terminal styling
npm install chalk

# Progress indicators
npm install ora cli-progress

# Tables
npm install cli-table3

# File system utilities
npm install fs-extra

# Configuration loading
npm install cosmiconfig

Testing Strategy

npm install --save-dev vitest @types/node
// src/commands/create.test.ts
import { describe, it, expect, vi } from 'vitest'
import { execSync } from 'child_process'

describe('create command', () => {
  it('creates project with default template', () => {
    const output = execSync('node dist/index.js create test-app')
    expect(output.toString()).toContain('Creating project: test-app')
  })

  it('uses specified template', () => {
    const output = execSync('node dist/index.js create test-app --template react')
    expect(output.toString()).toContain('Template: react')
  })
})

Publishing to npm

// package.json
{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "my-cli": "./dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}
# Build
npm run build

# Test locally
npm link
my-cli --version

# Publish
npm publish

Quality Gates Integration

# .github/workflows/cli-ci.yml
name: CLI CI

on: [pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [20, 22]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm run build
      - run: npm test

  publish:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Error Handling

import chalk from 'chalk'

program
  .command('risky-operation')
  .action(async () => {
    try {
      await performOperation()
    } catch (error) {
      console.error(chalk.red('Error:'), error.message)

      if (process.env.DEBUG) {
        console.error(error.stack)
      }

      process.exit(1)
    }
  })

// Global error handler
program.exitOverride((err) => {
  if (err.code === 'commander.unknownCommand') {
    console.error(chalk.red('Unknown command. See --help for available commands.'))
    process.exit(1)
  }
  throw err
})

Performance Optimization

// Lazy load heavy dependencies
program
  .command('build')
  .action(async () => {
    // Only import when command is used
    const { build } = await import('./commands/build')
    await build()
  })

// Use dynamic imports for optional features
if (options.analyze) {
  const { analyze } = await import('./utils/analyzer')
  await analyze()
}

UX Best Practices

  • Clear help text: Use .description() liberally
  • Sensible defaults: Minimize required options
  • Consistent naming: Use kebab-case for flags
  • Progress feedback: Show spinners for long operations
  • Color sparingly: Red for errors, green for success, yellow for warnings
  • Respect --quiet: Suppress non-critical output
  • Support --help: Always implement comprehensive help
  • Version info: Include --version flag

Recommendation Flow

New CLI tool:
├─ Standard commands/options → Commander.js ✅
├─ Large enterprise CLI (20+ commands) → oclif
└─ Rich interactive UI needed → Ink

Combine approaches:
├─ Commander.js + Ink → Interactive commands when needed
└─ Commander.js + inquirer → Simple prompts

When agents design CLI tools, they should:

  • Default to Commander.js for most use cases
  • Use inquirer for interactive prompts
  • Apply chalk sparingly for colored output
  • Show ora spinners for long operations
  • Load config with cosmiconfig
  • Apply quality-gates skill for testing/CI
  • Test on multiple OS (Linux, macOS, Windows)
  • Publish to npm with proper bin configuration
  • Lazy load heavy dependencies for performance
  • Provide comprehensive --help documentation