Lab 15: Building a Node.js CLI Tool

Objective

Build a complete, professional command-line tool in Node.js — parse arguments, add subcommands, handle stdin/stdout, read config files, add color output, and package it as a runnable script.

Background

The command line is where developers live. Node.js is uniquely powerful for CLI tools because it has built-in HTTP, file system, and process APIs, npm access to 2M+ packages, and runs everywhere. Tools like npm, GitHub CLI, Vercel CLI, and Prettier are all Node.js CLI apps. By the end of this lab you'll have built a fully functional file statistics tool from scratch.

Time

50 minutes

Prerequisites

  • Lab 08 (Modules)

  • Lab 09 (Node.js File System)

  • Lab 11 (Regex)

Tools

  • Node.js 20 LTS

  • Docker image: innozverse-js:latest


Lab Instructions

Step 1: The Basics — process.argv and process.exit

Every CLI starts with reading arguments and returning proper exit codes.

💡 process.exit(0) is success, anything else is failure. Shell scripts check $? after every command — returning the right exit code lets your tool compose with pipes, &&, and || in shell scripts. Always exit non-zero on errors.

📸 Verified Output:


Step 2: Reading stdin — Unix Pipe Support

Professional CLIs work with pipes: cat file.txt | your-tool.

💡 process.stdin.isTTY is true when running interactively (terminal), undefined/falsy when piped. This pattern lets a tool work both ways: node tool.js file.txt and cat file.txt | node tool.js. Unix philosophy: tools should compose.

📸 Verified Output:

(input: "the quick brown fox jumps over the lazy dog the quick dog")


Step 3: ANSI Colors — Terminal Styling

Add colors without external dependencies using ANSI escape codes.

💡 Always check process.stdout.isTTY before adding colors. When output is piped to a file or another command (node tool.js | grep error), ANSI codes appear as garbage characters. Respecting this is the mark of a well-behaved CLI.

📸 Verified Output:

(colors appear in terminal; shown as plain text here)


Step 4: Config Files — JSON and .env Patterns

Load configuration from files with fallback to defaults.

💡 Config precedence (lowest to highest): hardcoded defaults → config file → environment variables → CLI flags. This is the pattern used by Prettier, ESLint, Webpack, and Babel. It lets users set project-wide config in a file and override per-run with env vars or flags.

📸 Verified Output:


Step 5: Subcommands — Multi-Command CLI

Structure a CLI with subcommands like git commit, npm install.

💡 The subcommand pattern (git, npm, docker) groups related functionality under one binary. Each subcommand is an independent module — easy to add, test, and document separately. Real CLIs use this same structure: parse the first arg, dispatch to a handler, pass remaining args.

📸 Verified Output:


Step 6: Spinners & Progress — User Feedback

Keep users informed during long operations.

💡 \r (carriage return) moves the cursor to the start of the current line without newline, allowing in-place updates. Combined with ANSI cursor control, this creates smooth animations. Always check isTTY — animation codes look terrible in log files.

📸 Verified Output:

(spinner animation visible in terminal)


Step 7: Error Handling & Signals — Graceful Shutdown

Handle errors and interrupts professionally.

💡 SIGTERM is how process managers (systemd, Docker, Kubernetes) stop your process — they send SIGTERM and wait, then SIGKILL. Your SIGTERM handler has typically 10–30 seconds to clean up. Handling it gracefully means no corrupted files, closed database connections, and flushed logs.

📸 Verified Output:


Step 8: Complete CLI Tool — filestats

Build the complete filestats tool combining everything from this lab.

Run it on itself to test:

💡 The #!/usr/bin/env node shebang at line 1 lets the OS run your script directly without typing node: chmod +x filestats.js && ./filestats.js. Combined with npm link or adding to PATH, your script becomes a real CLI command available system-wide.

📸 Verified Output:


Verification

Expected: Prints line count and confirms JSON output.

Common Mistakes

Mistake
Fix

Not slicing process.argv

Always process.argv.slice(2) to skip node + script path

Forgetting isTTY check for colors

ANSI codes in piped output = garbage characters

process.exit() without cleanup

Register SIGTERM/SIGINT handlers for graceful shutdown

No exit code on error

Always process.exit(1) on error — shells depend on this

Blocking the event loop with sync IO

Use fs.promises for large files; sync OK for small configs

Summary

You've built a complete professional CLI tool: argument parsing, stdin piping, ANSI colors, config file loading, subcommand dispatch, spinners, signal handling, and a full filestats utility. These skills transfer directly to building developer tools, automation scripts, and deploy pipelines.

🎉 JavaScript Foundations Complete!

You've finished all 15 JavaScript Foundations labs:

  • Labs 1–4: Language basics, functions, arrays & objects

  • Labs 5–7: OOP, async/await, error handling

  • Labs 8–9: Modules, Node.js file system

  • Labs 10–12: HTTP/fetch, regex, iterators & generators

  • Labs 13–15: Functional programming, testing, CLI tools

Next: JavaScript Practitionerarrow-up-right — Express APIs, databases, TypeScript, and more.

Further Reading

Last updated