5 Advanced CLI Engineering Patterns in Node.js & Go (Building Production Tools)
Command line utilities (CLIs) are the backbone of modern developer workflows. From package managers to security scanners, a well-engineered CLI tool can boost developer velocity tenfold.
Drawing from production patterns behind open-source CLI tools like node-reaper and port-sniper, here are 5 essential engineering patterns for building high-performance CLI utilities.
1. Graceful Process Signal Handling (SIGINT / SIGTERM)
Always handle Ctrl+C cleanly to release ports, clean up temporary files, and restore cursor states.
🔴 Node.js Signal Handler Pattern:
import process from 'node:process';
function setupGracefulShutdown(cleanupFn: () => Promise<void>) {
const shutdown = async (signal: string) => {
console.log(`\n\n[INFO] Received ${signal}. Cleaning up resources...`);
try {
await cleanupFn();
console.log("[SUCCESS] Cleanup complete. Exiting.");
process.exit(0);
} catch (err) {
console.error("[ERROR] Cleanup failed:", err);
process.exit(1);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
2. Interactive Terminal Prompts & Selection
Instead of forcing users to memorize complex flags, provide interactive dropdown menus when flags are omitted.
🔴 Interactive Dropdown Selection:
import { select } from '@inquirer/prompts';
export async function promptTargetSelection(processList: { pid: number; port: number; name: string }[]) {
const selectedPid = await select({
message: 'Select zombie process to kill:',
choices: processList.map(proc => ({
name: `Port ${proc.port} ──► PID ${proc.pid} (${proc.name})`,
value: proc.pid,
})),
});
return selectedPid;
}
3. High-Speed Concurrent Task Execution in Go
When scanning filesystem directories (e.g. cleaning node_modules), use Go goroutines with worker pools for maximum IOPS efficiency.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan string, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for path := range jobs {
// Simulate processing directory path
results <- fmt.Sprintf("Worker %d cleaned: %s", id, path)
}
}
func main() {
jobs := make(chan string, 100)
results := make(chan string, 100)
var wg sync.WaitGroup
// Spawn 4 parallel worker goroutines
for w := 1; w <= 4; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
paths := []string{"./app/node_modules", "./server/node_modules", "./web/node_modules"}
for _, p := range paths {
jobs <- p
}
close(jobs)
wg.Wait()
close(results)
for res := range results {
fmt.Println(res)
}
}
4. ANSI Terminal Color Formatting Without Overhead
Avoid heavy dependencies for basic colors. Use standard ANSI escape sequences or lightweight libraries like picocolors.
import pc from 'picocolors';
console.log(pc.green(pc.bold('✔ SUCCESS: Process killed on port 3000')));
console.log(pc.yellow('⚠ WARNING: Port 8080 is bound by system process'));
console.log(pc.red('✖ ERROR: Permission denied. Run with sudo.'));
5. Non-Blocking Non-Zero Exit Codes
Ensure your CLI returns proper POSIX exit codes so it can be chained inside CI/CD bash pipelines.
-
0: Successful execution -
1: General application error -
2: Invalid CLI command flags or arguments -
130: Terminated by user (SIGINT)
✍️ Authored by Lakshan Muruganandam
Lakshan Muruganandam is a software engineer and author of open-source CLI tools including port-sniper and node-reaper.
- GitHub: github.com/lakshanmuruganandam
- X / Twitter: @itsmeladdoo
- Official Tech Blog: lakshanmuruganandam.hashnode.dev
Top comments (0)