CLI Tool
Build a distributable CLI in Node.js, Go, or Rust and ship via npm/brew/cargo.
Design & Interface
Define commands, flags, output format, and the UX of a great terminal tool.
Why
CLI design is a UX problem in text. Good CLIs are discoverable (--help), consistent (flags work everywhere), and scriptable (stable output, exit codes). A poorly designed CLI gets aliased-around and abandoned; a great one (like ripgrep) becomes a daily habit. The interface is the product.
When
Before implementation. 1-3 days to nail down commands, flags, and output format. Sketch the help output first.
How
Example
Common Problems
- ⚠Inconsistent flags across subcommands (--prod works on deploy but not logs) — breaks muscle memory
- ⚠Color output that breaks pipes — always detect TTY and respect NO_COLOR
- ⚠No machine-readable output — users resort to grep/awk on human text, which is fragile
Best Practices
- ✓Write the --help text before coding — it forces you to design the full interface upfront
- ✓Always emit errors to stderr and results to stdout so the tool composes in Unix pipes
- ✓Provide --json output from day 1; scripting users will thank you and never leave
Resources for this stage
Tools
Templates
Implementation & Testing
Build the CLI with a framework, tests for flags/output, and a release build.
Why
CLIs are run in pipelines and CI — a non-zero exit code on success, or human text where JSON was expected, breaks automation downstream. Testing flag parsing, exit codes, and output format catches these before users script around them and get locked into buggy behavior.
When
After the interface is designed. Implement the happy path first, then edge cases (missing args, invalid config, network failure).
How
Example
Common Problems
- ⚠Returning exit 0 on errors — breaks CI pipelines that rely on non-zero to fail the step
- ⚠Printing errors to stdout — corrupts --json output for downstream parsers
- ⚠No integration tests on the binary — unit tests pass but the built CLI has a broken flag
Best Practices
- ✓Test the built binary, not just library functions — run it and assert exit code + output
- ✓Snapshot the --help output so any interface change is a deliberate, reviewed diff
- ✓Build a GOOS/GOARCH (or target triple) matrix in CI so every release ships all platforms
Distribution & Releases
Ship via npm/brew/cargo and automate versioned releases with changelogs.
Why
Distribution friction kills CLIs — if install is more than one command, adoption stalls. npm gives npx one-liner installs; brew taps reach macOS devs; cargo install reaches Rustaceans. Automated, signed releases with a changelog turn 'did I update?' into a solved problem.
When
When the CLI builds and tests pass on all target platforms. Set up the release pipeline before v1.0 so the first public release is automated.
How
Example
Common Problems
- ⚠Manual releases — forgotten platform, wrong version bump, no changelog, users stuck on old
- ⚠No shell completions — power users tab-complete nothing and switch to a rival CLI
- ⚠Breaking changes without a migration doc — users upgrade, things break, they never update again
Best Practices
- ✓Automate releases on git tag — one command ships npm + brew + cargo + GitHub binaries at once
- ✓Generate and ship shell completions with every release; document the one-line install
Full Developer Lifecycle
A 14-stage developer roadmap from idea to maintenance — visualize dependencies and track progress.
Next Step