Building CLI Tools with Node.js: From Zero to npm Publish
Introduction
Command-line tools are one of the most satisfying things to build. They solve specific problems elegantly, they're fast to write, and they compound in value over time as you and your team use them daily. I've built a handful of internal CLI tools over the years — for scaffolding projects, automating repetitive workflows, and replacing multi-step manual processes with a single command.
Node.js is an excellent runtime for CLI tools because the npm ecosystem is enormous, distribution via npm is straightforward, and most of us already know JavaScript. This guide covers everything from the initial setup to publishing a polished, type-safe CLI to npm.
Core Concepts
The Anatomy of a Node.js CLI
- Shebang line — tells the OS to use Node.js to execute the file
- Executable permission —
chmod +xon Unix systems binfield inpackage.json— maps command names to script files- Argument parsing — reading and validating
process.argv
// package.json
{
"name": "my-cli-tool",
"version": "1.0.0",
"bin": {
"mytool": "./dist/index.js"
}
}When someone installs your package globally (npm install -g my-cli-tool), npm creates a symlink so mytool resolves to your script.
Project Setup
File Structure
my-cli/
├── src/
│ ├── index.ts ← Entry point
│ ├── commands/
│ │ ├── init.ts
│ │ └── generate.ts
│ ├── utils/
│ │ ├── files.ts
│ │ └── logger.ts
│ └── types.ts
├── dist/ ← Compiled output (gitignored)
├── package.json
├── tsconfig.json
└── README.mdpackage.json Configuration
{
"name": "shavrka-cli",
"version": "0.1.0",
"description": "A developer CLI toolkit",
"main": "dist/index.js",
"bin": {
"shavrka": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"start": "node dist/index.js",
"lint": "eslint src/**/*.ts"
},
"keywords": ["cli", "developer-tools"],
"license": "MIT",
"dependencies": {
"commander": "^11.0.0",
"chalk": "^5.3.0",
"ora": "^7.0.1",- Commander.js
- Inquirer.js
- Yargs
More information: Node.js CLI Docs