cli
Generate modern CLI tools using TypeScript (Node.js or Bun) or Go with best practices for LLM-assisted development. Use this skill when the user wants to create a CLI tool, build a command-line application, make a terminal utility, or set up CLI tooling. Helps with stack selection (Node.js/Bun/Go), build configuration, safety patterns (confirmations, dry-run), and distribution setup. Triggers on "create a cli", "build a cli tool", "make a command-line tool", or when CLI development is requested.
git clone https://github.com/alexanderchan/cli-skill
skill.mdModern CLI Development Best Practices
You are an expert at building modern command-line tools following current best practices. Use this guide when creating CLI applications.
Initial Questions
Before generating the CLI tool, ask the user about these key aspects:
-
Language & Distribution
- TypeScript (quick iteration, Node.js ecosystems) or Go (standalone binaries, no runtime)?
- Will this be distributed as an npm package or standalone binary?
-
Interaction Mode
- Interactive prompts by default, or non-interactive?
- Should destructive operations require confirmation?
- Support for
flag to preview changes?--dry-run
-
Configuration & Defaults
- Does this tool need persistent configuration?
- Where should config be stored (
, project~/.config/[name]
file, both)?.rc - What sensible defaults should be provided?
- Support for environment variable overrides?
-
Build & Distribution Setup
- Should I set up build tooling (tsup, esbuild, goreleaser)?
- Need GitHub Actions for releases?
- Want automated binary builds for multiple platforms?
- Should include installation instructions (npm, homebrew, direct download)?
-
Project Scope
- Is this a project-local script or a distributed tool?
- Single command or multiple subcommands?
- Need for plugins or extensibility?
Core Stack Recommendations
TypeScript with Node.js (Recommended for Node.js ecosystems)
Use these libraries for rapid CLI development with strong typing:
- @commander-js/extra-typings - Command parsing with inferred types
- @clack/prompts - Interactive prompts with beautiful UI
- zx - Shell command execution with JavaScript
- chalk - Terminal string styling (optional)
TypeScript with Bun (Recommended for fast standalone executables)
Bun is a fast all-in-one JavaScript runtime with built-in TypeScript support:
- Built-in bundler - No need for tsup or esbuild
- Compile to executables -
creates standalone binariesbun build --compile - Same libraries - Use @commander-js/extra-typings, @clack/prompts, etc.
- Fast startup - Significantly faster than Node.js
- No runtime needed - Compiled binaries include the Bun runtime
Go (Recommended for maximum portability)
Use these libraries for production CLIs:
- Bubble Tea - TUI framework for interactive applications
- Lip Gloss - Styling and layout for terminal output
- Huh - Forms and interactive inputs
- modernc.org/sqlite - SQLite database without C dependencies
Why TypeScript Over Shell Scripts
Modern LLMs are significantly better at TypeScript and Go than bash. As complexity grows:
- Shell scripts become unmaintainable with complex logic
- TypeScript/Go offer better error handling and type safety
- Portability is easier (no bash/sh/zsh compatibility issues)
- Better tooling and testing support
LLM Development Pattern
When asking an LLM to generate CLI code, provide this context once:
I'm building a CLI tool in [TypeScript/Go]. Use these libraries: [List your chosen libraries from above] Follow these guidelines: - Use non-destructive operations with confirmation prompts by default - Support --yes or --non-interactive flags for automation - Store configuration in ~/.config/[app-name]/ (or OS equivalent) - Include helpful error messages and usage examples
Modern LLMs know these libraries well and will generate working code with this simple prompt pattern.
Key Best Practices
Safety First
- Use confirmation prompts for destructive operations by default
- Implement
or--yes
flags for automation/CI--non-interactive - Validate inputs before performing operations
- Provide clear error messages with recovery suggestions
Configuration Management
- Store config in standard locations:
- Linux/Mac:
~/.config/[app-name]/ - Windows:
%APPDATA%\[app-name]\
- Linux/Mac:
- Support config file and environment variables
- Allow command-line flags to override config
User Experience
- Provide helpful error messages
- Include progress indicators for long operations
- Support both interactive and non-interactive modes
- Show usage examples in help text
- Use colors and formatting thoughtfully (not excessively)
Code Organization
- Separate project-local scripts from distributed CLIs
- Keep commands modular and testable
- Document installation and usage clearly
Tool Selection Guide
Choose TypeScript + Node.js when:
- Building tools for Node.js/JavaScript projects
- Rapid prototyping is priority
- You need quick iteration with LLM assistance
- Users already have Node.js installed
- Distribution via npm is acceptable
Choose TypeScript + Bun when:
- You want TypeScript but need standalone executables
- Fast startup time is important
- You want simpler build setup (no separate bundler needed)
- Cross-platform binaries are needed (Bun compiles for Linux/macOS/Windows)
- You want the speed of Go but familiarity of TypeScript
Choose Go when:
- Maximum portability is required
- Smallest possible binary size is needed
- Building system-level tools
- Performance is absolutely critical
- You need the most mature cross-compilation tooling
Project Structure & Build Setup
TypeScript CLI with Node.js (Full Setup)
my-cli/ ├── package.json # Dependencies and scripts ├── tsconfig.json # TypeScript configuration ├── tsup.config.ts # Build configuration (optional) ├── .github/ │ └── workflows/ │ └── release.yml # Automated releases ├── src/ │ ├── index.ts # Entry point with commander setup │ ├── commands/ # Individual command implementations │ ├── config.ts # Configuration management │ └── utils/ # Shared utilities ├── bin/ │ └── cli.js # Executable wrapper (#!/usr/bin/env node) ├── tests/ # Test files └── README.md
Build Tools:
There are three main approaches for building TypeScript CLIs:
Option 1: esbuild (simple, single file)
- Fast and simple for single CLI tools
- Manual setup for multiple entry points
- Good for straightforward projects
{ "bin": { "my-cli": "./dist/cli.js" }, "scripts": { "build": "esbuild --bundle --platform=node --target=node22 src/cli.ts --outfile=dist/cli.js" } }
For multiple CLIs with esbuild:
{ "bin": { "my-cli": "./dist/my-cli.js", "another-cli": "./dist/another-cli.js" }, "scripts": { "build": "npm run build:my-cli && npm run build:another-cli", "build:my-cli": "esbuild --bundle --platform=node --target=node22 src/cli/my-cli.ts --outfile=dist/my-cli.js", "build:another-cli": "esbuild --bundle --platform=node --target=node22 src/cli/another-cli.ts --outfile=dist/another-cli.js" } }
Option 2: tsdown (multiple CLIs, better DX)
- Config-based approach for multiple entry points
- Cleaner than esbuild for complex builds
- Built-in watch mode
Install:
npm install -D tsdown
Create
tsdown.config.js:
import { defineConfig } from "tsdown"; export default defineConfig({ entry: [ "./src/cli/my-cli.ts", "./src/cli/another-cli.ts", ], format: "commonjs", });
Update
package.json:
{ "bin": { "my-cli": "./dist/my-cli.js", "another-cli": "./dist/another-cli.js" }, "scripts": { "build": "tsdown", "build:watch": "tsdown --watch" }, "files": ["dist"] }
Option 3: bun build (fast, modern)
- Extremely fast (10-20x faster than esbuild)
- Simple glob patterns for multiple CLIs
- Built-in minification
- Can create standalone executables with
--compile
For a single CLI:
{ "bin": { "my-cli": "./dist/cli.js" }, "scripts": { "build": "bun build --compile --target=node --outfile=dist/cli.js src/cli.ts" } }
For multiple CLIs:
{ "bin": { "my-cli": "./dist/my-cli.js", "another-cli": "./dist/another-cli.js" }, "scripts": { "build": "bun build --target=node --outdir=dist src/cli/*.ts" } }
Creating standalone executables (no Node.js required):
# Build a completely standalone binary with Bun runtime embedded bun build --compile --target=bun src/cli.ts --outfile=my-cli # Now distribute the single binary - no runtime needed! ./my-cli
When to use which bundler:
- esbuild: Simple single CLI, minimal configuration
- tsdown: Multiple CLIs with better developer experience
- bun build: Want fastest build times or standalone executables
TypeScript CLI with Bun (Fast & Flexible)
Bun offers two approaches: bundling for Node.js or creating standalone executables.
my-cli/ ├── package.json # Dependencies (Bun compatible) ├── tsconfig.json # TypeScript configuration (optional, Bun has defaults) ├── .github/ │ └── workflows/ │ └── release.yml # Automated releases with Bun ├── src/ │ ├── index.ts # Entry point │ ├── commands/ # Individual command implementations │ ├── config.ts # Configuration management │ └── utils/ # Shared utilities ├── dist/ # Bundled output for Node.js ├── build/ # Compiled standalone binaries (optional) └── README.md
Approach 1: Bundle for Node.js (requires Node.js runtime)
{ "bin": { "my-cli": "./dist/cli.js" }, "scripts": { "build": "bun build --target=node --outfile=dist/cli.js src/index.ts", "build:multiple": "bun build --target=node --outdir=dist src/cli/*.ts", "dev": "bun run src/index.ts" } }
Approach 2: Standalone executables (no runtime required)
{ "scripts": { "build": "bun build src/index.ts --compile --outfile build/my-cli", "build:all": "bun run build:linux && bun run build:macos && bun run build:windows", "build:linux": "bun build src/index.ts --compile --target=bun-linux-x64 --outfile build/my-cli-linux", "build:macos": "bun build src/index.ts --compile --target=bun-darwin-arm64 --outfile build/my-cli-macos", "build:windows": "bun build src/index.ts --compile --target=bun-windows-x64 --outfile build/my-cli-windows.exe", "dev": "bun run src/index.ts" } }
Key Bun advantages:
- Extremely fast - 10-20x faster than esbuild
- Simple glob patterns -
automatically creates multiple outputssrc/cli/*.ts - Built-in minification -
flag for smaller bundles--minify - Two distribution modes - Bundle for Node.js or compile to standalone executable
- No separate bundler needed - Bun has it built-in
- TypeScript out of the box - No configuration required
- Compatible with most npm packages - Works with @commander-js, @clack/prompts, etc.
- Smaller learning curve than Go - For JS/TS developers
When to use bun build:
- You want the fastest build times
- You're building standalone executables for easy distribution
- You want simple configuration with good defaults
- Your project already uses bun as the runtime
Go CLI (Full Setup)
my-cli/ ├── go.mod ├── go.sum ├── .goreleaser.yml # Release automation ├── .github/ │ └── workflows/ │ └── release.yml # Automated releases with GoReleaser ├── main.go # Entry point ├── cmd/ │ ├── root.go # Root command setup │ └── subcommand.go # Subcommands ├── internal/ │ ├── config/ # Configuration management │ └── ui/ # UI components (Bubble Tea) ├── pkg/ # Public packages └── README.md
Build Tools:
- GoReleaser - Automated releases with cross-compilation
- Task - Modern Make alternative (optional)
- Air - Live reload for development (optional)
Makefile or Taskfile.yml:
build: go build -o dist/my-cli main.go install: go install release: goreleaser release --clean
Getting Started
When generating a new CLI tool:
- Choose your stack based on distribution needs
- Set up the project with recommended libraries
- Define commands and their arguments
- Implement safety checks and confirmations
- Add configuration support
- Test both interactive and non-interactive modes
- Write clear documentation
Common Patterns
Confirmation Prompt (TypeScript with @clack/prompts)
import * as p from '@clack/prompts'; const shouldContinue = await p.confirm({ message: 'This will delete files. Continue?', initialValue: false }); if (p.isCancel(shouldContinue) || !shouldContinue) { p.cancel('Operation cancelled'); process.exit(0); }
Non-Interactive Flag Support
const program = new Command() .option('-y, --yes', 'skip confirmation prompts') .option('--non-interactive', 'run without prompts'); // Later in code if (!options.yes && !options.nonInteractive) { // Show confirmation prompt }
Config File Loading with Defaults
import { readFile, mkdir, writeFile } from 'fs/promises'; import { join } from 'path'; import { homedir } from 'os'; const defaultConfig = { theme: 'auto', verbose: false, timeout: 30000 }; async function loadConfig() { const configDir = join(homedir(), '.config', 'my-cli'); const configPath = join(configDir, 'config.json'); try { const config = JSON.parse(await readFile(configPath, 'utf-8')); return { ...defaultConfig, ...config }; } catch { // Create default config if it doesn't exist await mkdir(configDir, { recursive: true }); await writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); return defaultConfig; } }
Dry Run Support
const program = new Command() .option('--dry-run', 'preview changes without executing') .option('-y, --yes', 'skip confirmation prompts'); async function deleteFiles(files: string[], options: { dryRun?: boolean }) { if (options.dryRun) { console.log('Would delete:', files); return; } // Actually delete files for (const file of files) { await unlink(file); } }
Build & Release Automation
TypeScript + Node.js with GitHub Actions
# .github/workflows/release.yml name: Release on: push: tags: ['v*'] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - run: npm test - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
TypeScript + Bun with GitHub Actions
# .github/workflows/release.yml name: Release on: push: tags: ['v*'] jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v1 with: bun-version: latest - run: bun install - run: bun test - run: bun build src/index.ts --compile --outfile my-cli-${{ runner.os }} - uses: actions/upload-artifact@v4 with: name: my-cli-${{ runner.os }} path: my-cli-${{ runner.os }}* release: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 - uses: softprops/action-gh-release@v1 with: files: | my-cli-*/*
Go with GoReleaser
# .goreleaser.yml builds: - env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - amd64 - arm64 archives: - format: tar.gz name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' brews: - name: my-cli repository: owner: myorg name: homebrew-tap folder: Formula homepage: https://github.com/myorg/my-cli description: My awesome CLI tool
Remember: Modern LLMs excel at generating CLI code with these libraries. After gathering requirements through the initial questions, provide clear context about the chosen stack and let the LLM handle implementation details with these patterns in mind.