Building Command-Line Tools
Node isn't only for servers — it's an excellent platform for command-line tools. npm, eslint, prettier, vite, tsc, and most of the tooling you use daily are Node CLIs. A CLI is just a script you can run from the terminal that reads arguments, does work, prints output, and exits with a status code. This page covers what makes a script a command — the shebang line, the executable bit, and the bin field — plus the building blocks every CLI uses: process.argv, exit codes, stdin/stdout/stderr, and the conventions (--help, --version, piping) that make a tool feel native to the shell.
From script to command
greet.js
#!/usr/bin/env node
// ^ the SHEBANG: tells the OS to run this file with node, so you can call it directly.
const name = process.argv[2] || 'world' // argv[0]=node, argv[1]=script, argv[2]=first arg
console.log(`Hello, ${name}!`)
process.exit(0) // 0 = successchmod +x greet.js # make it executable (Unix) ./greet.js Ada # run it directly — no "node" prefix needed node greet.js Ada # or run it explicitly with node
Hello, Ada!
Reading arguments — process.argv
// node app.js build --watch --out=dist file.txt console.log(process.argv) // [ // '/usr/bin/node', // [0] the node binary // '/path/to/app.js', // [1] the script // 'build', // [2..] everything the user typed // '--watch', // '--out=dist', // 'file.txt', // ] const args = process.argv.slice(2) // the part you actually care about
Exit codes — how the shell knows success or failure
try {
doWork()
process.exit(0) // 0 = success (also the default if you don't call exit)
} catch (err) {
console.error('Error:', err.message) // errors go to STDERR, not stdout
process.exit(1) // non-zero = failure; pick codes for distinct error types
}mytool build && echo "ok" # the && only runs echo if mytool exited 0 echo $? # $? holds the exit code of the last command
Standard streams — stdout, stderr, stdin
Stream | Purpose | In Node |
|---|---|---|
stdout | The program's actual output / result — meant to be piped |
|
stderr | Diagnostics: errors, logs, progress — kept out of the data |
|
stdin | Input piped or typed into the program |
|
# Because results go to stdout and logs go to stderr, this works cleanly:
mytool extract data.json > result.txt # only the RESULT lands in the file
# progress/errors still show on screen (stderr)
cat data.json | mytool transform | wc -l # tools compose via pipesConventions that make a CLI feel native
--help/-h— print usage, options, and examples; users expect it and so does every CLI framework.--version/-v— print the version (usually read frompackage.json).Sensible exit codes —
0success, non-zero failure; document distinct codes if you have them.Read from stdin when no file is given — lets your tool work in pipes as a filter.
Respect
NO_COLORand detect a TTY — only emit colors/spinners when output is an interactive terminal (process.stdout.isTTY), not when piped.Fail loudly to stderr — clear, actionable error messages; never swallow errors and exit
0.Be quiet on success — print only what's asked for; offer
--verbosefor more and--quietfor less.