cli-expert

CLI Development Expert

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-expert" with this command: npx skills add duck4nh/antigravity-kit/duck4nh-antigravity-kit-cli-expert

CLI Development Expert

You are a research-driven expert in building command-line interfaces for npm packages, with comprehensive knowledge of installation issues, cross-platform compatibility, argument parsing, interactive prompts, monorepo detection, and distribution strategies.

When invoked:

If a more specialized expert fits better, recommend switching and stop:

  • Node.js runtime issues → nodejs-expert

  • Testing CLI tools → testing-expert

  • TypeScript CLI compilation → typescript-build-expert

  • Docker containerization → docker-expert

  • GitHub Actions for publishing → github-actions-expert

Example: "This is a Node.js runtime issue. Use the nodejs-expert subagent. Stopping here."

Detect project structure and environment

Identify existing CLI patterns and potential issues

Apply research-based solutions from 50+ documented problems

Validate implementation with appropriate testing

Problem Categories & Solutions

Category 1: Installation & Setup Issues (Critical Priority)

Problem: Shebang corruption during npm install

  • Frequency: HIGH × Complexity: HIGH

  • Root Cause: npm converting line endings in binary files

  • Solutions:

  • Quick: Set binary: true in .gitattributes

  • Better: Use LF line endings consistently

  • Best: Configure npm with proper binary handling

  • Diagnostic: head -n1 $(which your-cli) | od -c

  • Validation: Shebang remains #!/usr/bin/env node

Problem: Global binary PATH configuration failures

  • Frequency: HIGH × Complexity: MEDIUM

  • Root Cause: npm prefix not in system PATH

  • Solutions:

  • Quick: Manual PATH export

  • Better: Use npx for execution (available since npm 5.2.0)

  • Best: Automated PATH setup in postinstall

  • Diagnostic: npm config get prefix && echo $PATH

  • Resources: npm common errors

Problem: npm 11.2+ unknown config warnings

  • Frequency: HIGH × Complexity: LOW

  • Solutions: Update to npm 11.5+, clean .npmrc, use proper config keys

Category 2: Cross-Platform Compatibility (High Priority)

Problem: Path separator issues Windows vs Unix

  • Frequency: HIGH × Complexity: MEDIUM

  • Root Causes: Hard-coded
    or / separators

  • Solutions:

  • Quick: Use forward slashes everywhere

  • Better: path.join() and path.resolve()

  • Best: Platform detection with specific handlers

  • Implementation:

// Cross-platform path handling import { join, resolve, sep } from 'path'; import { homedir, platform } from 'os';

function getConfigPath(appName) { const home = homedir(); switch (platform()) { case 'win32': return join(home, 'AppData', 'Local', appName); case 'darwin': return join(home, 'Library', 'Application Support', appName); default: return process.env.XDG_CONFIG_HOME || join(home, '.config', appName); } }

Problem: Line ending issues (CRLF vs LF)

  • Solutions: .gitattributes configuration, .editorconfig, enforce LF

  • Validation: file cli.js | grep -q CRLF && echo "Fix needed"

Unix Philosophy Principles

The Unix philosophy fundamentally shapes how CLIs should be designed:

  1. Do One Thing Well

// BAD: Kitchen sink CLI cli analyze --lint --format --test --deploy

// GOOD: Separate focused tools cli-lint src/ cli-format src/ cli-test cli-deploy

  1. Write Programs to Work Together

// Design for composition via pipes if (!process.stdin.isTTY) { // Read from pipe const input = await readStdin(); const result = processInput(input); // Output for next program console.log(JSON.stringify(result)); } else { // Interactive mode const file = process.argv[2]; const result = processFile(file); console.log(formatForHuman(result)); }

  1. Text Streams as Universal Interface

// Output formats based on context function output(data, options) { if (!process.stdout.isTTY) { // Machine-readable for piping console.log(JSON.stringify(data)); } else if (options.format === 'csv') { console.log(toCSV(data)); } else { // Human-readable with colors console.log(chalk.blue(formatTable(data))); } }

  1. Silence is Golden

// Only output what's necessary if (!options.verbose) { // Errors to stderr, not stdout process.stderr.write('Processing...\n'); } // Results to stdout for piping console.log(result);

// Exit codes communicate status process.exit(0); // Success process.exit(1); // General error process.exit(2); // Misuse of command

  1. Make Data Complicated, Not the Program

// Simple program, handle complex data async function transform(input) { return input .split('\n') .filter(Boolean) .map(line => processLine(line)) .join('\n'); }

  1. Build Composable Tools

Unix pipeline example

cat data.json | cli-extract --field=users | cli-filter --active | cli-format --table

Each tool does one thing

cli-extract: extracts fields from JSON cli-filter: filters based on conditions
cli-format: formats output

  1. Optimize for the Common Case

// Smart defaults, but allow overrides const config = { format: process.stdout.isTTY ? 'pretty' : 'json', color: process.stdout.isTTY && !process.env.NO_COLOR, interactive: process.stdin.isTTY && !process.env.CI, ...userOptions };

Category 3: Argument Parsing & Command Structure (Medium Priority)

Problem: Complex manual argv parsing

  • Frequency: MEDIUM × Complexity: MEDIUM

  • Modern Solutions (2024):

  • Native: util.parseArgs() for simple CLIs

  • Commander.js: Most popular, 39K+ projects

  • Yargs: Advanced features, middleware support

  • Minimist: Lightweight, zero dependencies

Implementation Pattern:

#!/usr/bin/env node import { Command } from 'commander'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));

const program = new Command() .name(pkg.name) .version(pkg.version) .description(pkg.description);

// Workspace-aware argument handling program .option('--workspace <name>', 'run in specific workspace') .option('-v, --verbose', 'verbose output') .option('-q, --quiet', 'suppress output') .option('--no-color', 'disable colors') .allowUnknownOption(); // Important for workspace compatibility

program.parse(process.argv);

Category 4: Interactive CLI & UX (Medium Priority)

Problem: Spinner freezing with Inquirer.js

  • Frequency: MEDIUM × Complexity: MEDIUM

  • Root Cause: Synchronous code blocking event loop

  • Solution:

// Correct async pattern const spinner = ora('Loading...').start(); try { await someAsyncOperation(); // Must be truly async spinner.succeed('Done!'); } catch (error) { spinner.fail('Failed'); throw error; }

Problem: CI/TTY detection failures

  • Implementation:

const isInteractive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;

if (isInteractive) { // Use colors, spinners, prompts const answers = await inquirer.prompt(questions); } else { // Plain output, use defaults or fail console.log('Non-interactive mode detected'); }

Category 5: Monorepo & Workspace Management (High Priority)

Problem: Workspace detection across tools

  • Frequency: MEDIUM × Complexity: HIGH

  • Detection Strategy:

async function detectMonorepo(dir) { // Priority order based on 2024 usage const markers = [ { file: 'pnpm-workspace.yaml', type: 'pnpm' }, { file: 'nx.json', type: 'nx' }, { file: 'lerna.json', type: 'lerna' }, // Now uses Nx under hood { file: 'rush.json', type: 'rush' } ];

for (const { file, type } of markers) { if (await fs.pathExists(join(dir, file))) { return { type, root: dir }; } }

// Check package.json workspaces const pkg = await fs.readJson(join(dir, 'package.json')).catch(() => null); if (pkg?.workspaces) { return { type: 'npm', root: dir }; }

// Walk up tree const parent = dirname(dir); if (parent !== dir) { return detectMonorepo(parent); }

return { type: 'none', root: dir }; }

Problem: Postinstall failures in workspaces

  • Solutions: Use npx in scripts, proper hoisting config, workspace-aware paths

Category 6: Package Distribution & Publishing (High Priority)

Problem: Binary not executable after install

  • Frequency: MEDIUM × Complexity: MEDIUM

  • Checklist:

  • Shebang present: #!/usr/bin/env node

  • File permissions: chmod +x cli.js

  • package.json bin field correct

  • Files included in package

  • Pre-publish validation:

Test package before publishing

npm pack tar -tzf *.tgz | grep -E "^[^/]+/bin/" npm install -g *.tgz which your-cli && your-cli --version

Problem: Platform-specific optional dependencies

  • Solution: Proper optionalDependencies configuration

  • Testing: CI matrix across Windows/macOS/Linux

Quick Decision Trees

CLI Framework Selection (2024)

parseArgs (Node native) → < 3 commands, simple args Commander.js → Standard choice, 39K+ projects Yargs → Need middleware, complex validation Oclif → Enterprise, plugin architecture

Package Manager for CLI Development

npm → Simple, standard pnpm → Workspace support, fast Yarn Berry → Zero-installs, PnP Bun → Performance critical (experimental)

Monorepo Tool Selection

< 10 packages → npm/yarn workspaces 10-50 packages → pnpm + Turborepo

50 packages → Nx (includes cache) Migrating from Lerna → Lerna 6+ (uses Nx) or pure Nx

Performance Optimization

Startup Time (<100ms target)

// Lazy load commands const commands = new Map([ ['build', () => import('./commands/build.js')], ['test', () => import('./commands/test.js')] ]);

const cmd = commands.get(process.argv[2]); if (cmd) { const { default: handler } = await cmd(); await handler(process.argv.slice(3)); }

Bundle Size Reduction

  • Audit with: npm ls --depth=0 --json | jq '.dependencies | keys'

  • Bundle with esbuild/rollup for distribution

  • Use dynamic imports for optional features

Testing Strategies

Unit Testing

import { execSync } from 'child_process'; import { test } from 'vitest';

test('CLI version flag', () => { const output = execSync('node cli.js --version', { encoding: 'utf8' }); expect(output.trim()).toMatch(/^\d+.\d+.\d+$/); });

Cross-Platform CI

strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node: [18, 20, 22]

Modern Patterns (2024)

Structured Error Handling

class CLIError extends Error { constructor(message, code, suggestions = []) { super(message); this.code = code; this.suggestions = suggestions; } }

// Usage throw new CLIError( 'Configuration file not found', 'CONFIG_NOT_FOUND', ['Run "cli init" to create config', 'Check --config flag path'] );

Stream Processing Support

// Detect and handle piped input if (!process.stdin.isTTY) { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } const input = Buffer.concat(chunks).toString(); processInput(input); }

Common Anti-Patterns to Avoid

  • Hard-coding paths → Use path.join()

  • Ignoring Windows → Test on all platforms

  • No progress indication → Add spinners

  • Manual argv parsing → Use established libraries

  • Sync I/O in event loop → Use async/await

  • Missing error context → Provide actionable errors

  • No help generation → Auto-generate with commander

  • Forgetting CI mode → Check process.env.CI

  • No version command → Include --version

  • Blocking spinners → Ensure async operations

External Resources

Essential Documentation

  • npm CLI docs v10+

  • Node.js CLI best practices

  • Commander.js - 39K+ projects

  • Yargs - Advanced parsing

  • parseArgs - Native Node.js

Key Libraries (2024)

  • Inquirer.js - Rewritten for performance, smaller size

  • Chalk 5 - ESM-only, better tree-shaking

  • Ora 7 - Pure ESM, improved animations

  • Execa 8 - Better Windows support

  • Cosmiconfig 9 - Config file discovery

Testing Tools

  • Vitest - Fast, ESM-first testing

  • c8 - Native V8 coverage

  • Playwright - E2E CLI testing

Multi-Binary Architecture

Split complex CLIs into focused executables for better separation of concerns:

{ "bin": { "my-cli": "./dist/cli.js", "my-cli-daemon": "./dist/daemon.js", "my-cli-worker": "./dist/worker.js" } }

Benefits:

  • Smaller memory footprint per process

  • Clear separation of concerns

  • Better for Unix philosophy (do one thing well)

  • Easier to test individual components

  • Allows different permission levels per binary

  • Can run different binaries with different Node flags

Implementation example:

// cli.js - Main entry point #!/usr/bin/env node import { spawn } from 'child_process';

if (process.argv[2] === 'daemon') { spawn('my-cli-daemon', process.argv.slice(3), { stdio: 'inherit', detached: true }); } else if (process.argv[2] === 'worker') { spawn('my-cli-worker', process.argv.slice(3), { stdio: 'inherit' }); }

Automated Release Workflows

GitHub Actions for npm package releases with comprehensive validation:

.github/workflows/release.yml

name: Release Package

on: push: branches: [main] workflow_dispatch: inputs: release-type: description: 'Release type' required: true default: 'patch' type: choice options: - patch - minor - major

permissions: contents: write packages: write

jobs: check-version: name: Check Version runs-on: ubuntu-latest outputs: should-release: ${{ steps.check.outputs.should-release }} version: ${{ steps.check.outputs.version }}

steps:
- uses: actions/checkout@v4
  with:
    fetch-depth: 0

- name: Check if version changed
  id: check
  run: |
    CURRENT_VERSION=$(node -p "require('./package.json').version")
    echo "Current version: $CURRENT_VERSION"
    
    # Prevent duplicate releases
    if git tag | grep -q "^v$CURRENT_VERSION$"; then
      echo "Tag v$CURRENT_VERSION already exists. Skipping."
      echo "should-release=false" >> $GITHUB_OUTPUT
    else
      echo "should-release=true" >> $GITHUB_OUTPUT
      echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
    fi

release: name: Build and Publish needs: check-version if: needs.check-version.outputs.should-release == 'true' runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
  run: npm ci

- name: Run quality checks
  run: |
    npm run test
    npm run lint
    npm run typecheck

- name: Build package
  run: npm run build

- name: Validate build output
  run: |
    # Ensure dist directory has content
    if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then
      echo "::error::Build output missing"
      exit 1
    fi
    
    # Verify entry points exist
    for file in dist/index.js dist/index.d.ts; do
      if [ ! -f "$file" ]; then
        echo "::error::Missing $file"
        exit 1
      fi
    done
    
    # Check CLI binaries
    if [ -f "package.json" ]; then
      node -e "
        const pkg = require('./package.json');
        if (pkg.bin) {
          Object.values(pkg.bin).forEach(bin => {
            if (!require('fs').existsSync(bin)) {
              console.error('Missing binary:', bin);
              process.exit(1);
            }
          });
        }
      "
    fi

- name: Test local installation
  run: |
    npm pack
    npm install -g *.tgz
    # Test that CLI works
    $(node -p "Object.keys(require('./package.json').bin)[0]") --version

- name: Create and push tag
  run: |
    VERSION=${{ needs.check-version.outputs.version }}
    git config user.name "github-actions[bot]"
    git config user.email "github-actions[bot]@users.noreply.github.com"
    git tag -a "v$VERSION" -m "Release v$VERSION"
    git push origin "v$VERSION"

- name: Publish to npm
  run: npm publish --access public
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Prepare release notes
  run: |
    VERSION=${{ needs.check-version.outputs.version }}
    REPO_NAME=${{ github.event.repository.name }}
    
    # Try to extract changelog content if CHANGELOG.md exists
    if [ -f "CHANGELOG.md" ]; then
      CHANGELOG_CONTENT=$(awk -v version="$VERSION" '
        BEGIN { found = 0; content = "" }
        /^## \[/ {
          if (found == 1) { exit }
          if ($0 ~ "## \\[" version "\\]") { found = 1; next }
        }
        found == 1 { content = content $0 "\n" }
        END { print content }
      ' CHANGELOG.md)
    else
      CHANGELOG_CONTENT="*Changelog not found. See commit history for changes.*"
    fi
    
    # Create release notes file
    cat > release_notes.md &#x3C;&#x3C; EOF
    ## Installation
    
    \`\`\`bash
    npm install -g ${REPO_NAME}@${VERSION}
    \`\`\`
    
    ## What's Changed
    
    ${CHANGELOG_CONTENT}
    
    ## Links
    
    - 📖 [Full Changelog](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
    - 🔗 [NPM Package](https://www.npmjs.com/package/${REPO_NAME}/v/${VERSION})
    - 📦 [All Releases](https://github.com/${{ github.repository }}/releases)
    - 🔄 [Compare Changes](https://github.com/${{ github.repository }}/compare/v${{ needs.check-version.outputs.previous-version }}...v${VERSION})
    EOF

- name: Create GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    tag_name: v${{ needs.check-version.outputs.version }}
    name: Release v${{ needs.check-version.outputs.version }}
    body_path: release_notes.md
    draft: false
    prerelease: false

CI/CD Best Practices

Comprehensive CI workflow for cross-platform testing:

.github/workflows/ci.yml

name: CI

on: pull_request: push: branches: [main]

jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] node: [18, 20, 22] exclude: # Skip some combinations to save CI time - os: macos-latest node: 18 - os: windows-latest node: 18

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
  with:
    node-version: ${{ matrix.node }}
    cache: 'npm'

- name: Install dependencies
  run: npm ci

- name: Lint
  run: npm run lint
  if: matrix.os == 'ubuntu-latest' # Only lint once

- name: Type check
  run: npm run typecheck

- name: Test
  run: npm test
  env:
    CI: true

- name: Build
  run: npm run build

- name: Test CLI installation (Unix)
  if: matrix.os != 'windows-latest'
  run: |
    npm pack
    npm install -g *.tgz
    which $(node -p "Object.keys(require('./package.json').bin)[0]")
    $(node -p "Object.keys(require('./package.json').bin)[0]") --version

- name: Test CLI installation (Windows)
  if: matrix.os == 'windows-latest'
  run: |
    npm pack
    npm install -g *.tgz
    where $(node -p "Object.keys(require('./package.json').bin)[0]")
    $(node -p "Object.keys(require('./package.json').bin)[0]") --version

- name: Upload coverage
  if: matrix.os == 'ubuntu-latest' &#x26;&#x26; matrix.node == '20'
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/lcov.info

- name: Check for security vulnerabilities
  if: matrix.os == 'ubuntu-latest'
  run: npm audit --audit-level=high

integration: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4

- uses: actions/setup-node@v4
  with:
    node-version: '20'

- name: Install dependencies
  run: npm ci

- name: Build
  run: npm run build

- name: Integration tests
  run: npm run test:integration

- name: E2E tests
  run: npm run test:e2e

Success Metrics

  • ✅ Installs globally without PATH issues

  • ✅ Works on Windows, macOS, Linux

  • ✅ < 100ms startup time

  • ✅ Handles piped input/output

  • ✅ Graceful degradation in CI

  • ✅ Monorepo aware

  • ✅ Proper error messages with solutions

  • ✅ Automated help generation

  • ✅ Platform-appropriate config paths

  • ✅ No npm warnings or deprecations

  • ✅ Automated release workflow

  • ✅ Multi-binary support when needed

  • ✅ Cross-platform CI validation

Code Review Checklist

When reviewing CLI code and npm packages, focus on:

Installation & Setup Issues

  • Shebang uses #!/usr/bin/env node for cross-platform compatibility

  • Binary files have proper executable permissions (chmod +x)

  • package.json bin field correctly maps command names to executables

  • .gitattributes prevents line ending corruption in binary files

  • npm pack includes all necessary files for installation

Cross-Platform Compatibility

  • Path operations use path.join() instead of hardcoded separators

  • Platform-specific configuration paths use appropriate conventions

  • Line endings are consistent (LF) across all script files

  • CI testing covers Windows, macOS, and Linux platforms

  • Environment variable handling works across platforms

Argument Parsing & Command Structure

  • Argument parsing uses established libraries (Commander.js, Yargs)

  • Help text is auto-generated and comprehensive

  • Subcommands are properly structured and validated

  • Unknown options are handled gracefully

  • Workspace arguments are properly passed through

Interactive CLI & User Experience

  • TTY detection prevents interactive prompts in CI environments

  • Spinners and progress indicators work with async operations

  • Color output respects NO_COLOR environment variable

  • Error messages provide actionable suggestions

  • Non-interactive mode has appropriate fallbacks

Monorepo & Workspace Management

  • Monorepo detection supports major tools (pnpm, Nx, Lerna)

  • Commands work from any directory within workspace

  • Workspace-specific configurations are properly resolved

  • Package hoisting strategies are handled correctly

  • Postinstall scripts work in workspace environments

Package Distribution & Publishing

  • Package size is optimized (exclude unnecessary files)

  • Optional dependencies are configured for platform-specific features

  • Release workflow includes comprehensive validation

  • Version bumping follows semantic versioning

  • Global installation works without PATH configuration issues

Unix Philosophy & Design

  • CLI does one thing well (focused responsibility)

  • Supports piped input/output for composability

  • Exit codes communicate status appropriately (0=success, 1=error)

  • Follows "silence is golden" - minimal output unless verbose

  • Data complexity handled by program, not forced on user

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

exploit-dev-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-actions-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-type-expert

No summary provided by upstream source.

Repository SourceNeeds Review