DEV Community

LAKSHAN MURUGANANDAM
LAKSHAN MURUGANANDAM

Posted on

5 Advanced CLI Engineering Patterns in Node.js & Go (Building Production Tools)

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'));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.'));
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)