kaiden

/0x424/writing
looking for work

building lightweight CLI tools

02. jul '26

Small CLI tools benefit from a tight dependency list. You can find a package for argument parsing, colored output, spinners, prompts, config loading, file handling. Node gives you most of what you need without them.

The tradeoff is real. Fewer dependencies mean faster installs, smaller bundles, less maintenance, fewer hidden vulnerabilities, and code that doesn’t require forensics when you return to it six months later.

This is what I focused on when I built kosei, a tool now used at Pleo to switch between environments (staging | dev) when developing locally.

Start with the platform

A good lightweight CLI usually only needs a few built-in tools:

  • process.argv for arguments
  • fs/promises for reading and writing files
  • path for file paths
  • child_process for running shell commands
  • readline if you need simple interactive input

For example, this is enough to build a tiny command parser:

#!/usr/bin/env node

const args = process.argv.slice(2)
const command = args

if (!command || command === "--help") {
  console.log(`
kosei <command>

Commands:
  init       Create a new config
  check      Validate project setup
`)
  process.exit(0)
}

if (command === "init") {
  console.log("Scaffolding project...")
} else if (command === "check") {
  console.log("Running checks...")
} else {
  console.error(`Unknown command: ${command}`)
  process.exit(1)
}

This is enough for a personal tool or internal workflow. It reads cleanly. It ships fast.

Add dependencies only when the pain is real

Treat dependencies as a cost. This doesn’t forbid packages. It demands a reason before you add one.

A few questions I ask first:

  • Can I solve this cleanly, with minimal effort using a standard library?
  • Is this feature core to the tool, or just a nice to have?
  • Will this dependency reduce code, or just hide simple logic?
  • Am I adding one package and accidentally pulling in fifty more?

A library that delivers measurable UX, safer parsing, or genuine maintenance wins is worth adding. Five convenience packages that hide simple logic are not.

Keep the surface area small

Single-purpose tools stay maintainable. Structure around a small core:

src/
  cli.ts
  commands/
    init.ts
    check.ts
  utils/
    fs.ts
    logger.ts

This keeps the entrypoint thin, makes each command testable in isolation, and lets you change implementation details without burning the whole tool.

Optimize for startup and clarity

CLI tools should start instantly and behave predictably. Minimal dependencies help. So does resisting the urge to abstract. If a command reads a file, transforms it, and writes output, keep that logic direct. Don’t build a framework to hide it.

A good CLI doesn’t need to be clever. Fast, readable, trustworthy. That’s the full list.

Final thought

Write the simplest version that works. Skip dependencies where possible. Add them only when they reduce cognitive load or solve a genuine problem faster.

This isn't ideology. Minimal tools ship faster. They're easier to maintain. When you open one a year later, you understand it immediately instead of navigating someone else's abstraction.

back to writing