Building a CLI Tool with Node.js (The Complete Guide)
CLI tools are underrated. They solve real problems, are easy to distribute, and look great on your resume.
Why Build a CLI?
Benefits:
- Easy to build (Node.js + one dependency)
- Easy to distribute (npm install -g)
- No UI needed (faster development)
- Automate repetitive tasks
- Look like a pro (CLI = serious developer energy)
- Portfolio piece that stands out
Setup
mkdir my-cli
cd my-cli
npm init -y
# The only dependency you really need:
npm install commander
# For interactive prompts:
npm install inquirer
# For colors and styling:
npm install chalk
package.json Configuration
{
"name": "my-cli",
"version": "1.0.0",
"description": "A CLI tool that does X",
"main": "index.js",
"bin": {
"mycli": "./index.js"
},
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
}
}
The Shebang (Critical!)
// #!/usr/bin/env node
// ↑ This line tells your OS to run with Node.js
// Without it: ./index.js → "permission denied"
// With it: ./index.js → runs with node
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
Define Commands
program
.name('mycli')
.description('CLI tool that does X')
.version('1.0.0');
// Simple command: mycli greet <name>
program
.command('greet <name>')
.description('Greet someone')
.option('-u, --uppercase', 'Greet in uppercase')
.option('-t, --times <number>', 'Number of times to greet', '1')
.action((name, options) => {
let message = `Hello, ${name}!`;
if (options.uppercase) message = message.toUpperCase();
for (let i = 0; i < parseInt(options.times); i++) {
console.log(chalk.green(message));
}
});
// Command with prompts: mycli init
program
.command('init')
.description('Initialize a new project')
.action(async () => {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Project name:',
default: 'my-project',
},
{
type: 'list',
name: 'framework',
message: 'Choose a framework:',
choices: ['React', 'Vue', 'Svelte', 'Vanilla'],
},
{
type: 'confirm',
name: 'typescript',
message: 'Use TypeScript?',
default: true,
},
{
type: 'checkbox',
name: 'features',
message: 'Select features:',
choices: ['Router', 'State Management', 'Testing', 'Linting'],
},
]);
console.log(chalk.blue('\nCreating project:'));
console.log(chalk.white(` Name: ${answers.name}`));
console.log(chalk.white(` Framework: ${answers.framework}`));
console.log(chalk.white(` TypeScript: ${answers.typescript ? 'Yes' : 'No'}`));
console.log(chalk.white(` Features: ${answers.features.join(', ')}`));
// Create project files
const dir = path.join(process.cwd(), answers.name);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
name: answers.name,
version: '0.1.0',
scripts: { dev: 'vite', build: 'vite build' },
}, null, 2));
console.log(chalk.green('\n✓ Project created successfully!'));
console.log(chalk.gray(` cd ${answers.name} && npm install`));
});
// File processing: mycli count <file>
program
.command('count <file>')
.description('Count lines, words, and characters in a file')
.action((file) => {
try {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n').length;
const words = content.split(/\s+/).filter(Boolean).length;
const chars = content.length;
console.log(chalk.bold('\n📊 File Statistics:'));
console.log(` File: ${chalk.cyan(file)}`);
console.log(` Lines: ${chalk.yellow(lines)}`);
console.log(` Words: ${chalk.yellow(words)}`);
console.log(` Chars: ${chalk.yellow(chars)}`);
} catch (err) {
console.error(chalk.red(`Error: ${err.message}`));
process.exit(1);
}
});
program.parse();
Interactive Menus
import { select, input, confirm, checkbox } from '@inquirer/prompts';
async function mainMenu() {
const action = await select({
message: 'What would you like to do?',
choices: [
{ name: '📄 Create new file', value: 'create' },
{ name: '📋 List files', value: 'list' },
{ name: '🔍 Search files', value: 'search' },
{ name: '🗑️ Delete file', value: 'delete' },
{ name: '🚪 Exit', value: 'exit' },
],
});
switch (action) {
case 'create':
const filename = await input({ message: 'File name:' });
fs.writeFileSync(filename, '');
console.log(chalk.green(`Created: ${filename}`));
break;
case 'list':
const files = fs.readdirSync('.');
files.forEach(f => console.log(` ${f}`));
break;
// ...
}
}
Loading Spinners
import ora from 'ora';
const spinner = ora('Fetching data...').start();
await new Promise(resolve => setTimeout(resolve, 2000));
spinner.succeed('Data fetched!');
// spinner.fail('Failed!');
// spinner.warn('Warning!');
// spinner.info('Info message');
Progress Bars
import cliProgress from 'cli-progress';
const bar = new cliProgress.SingleBar({
format: 'Progress |' + '{bar}' + '| {percentage}% | {value}/{total}',
barCompleteChar: '█',
barIncompleteChar: '░',
});
const total = 100;
bar.start(total, 0);
for (let i = 0; i < total; i++) {
await new Promise(resolve => setTimeout(resolve, 20));
bar.update(i + 1);
}
bar.stop();
Distribute via npm
# Make executable locally
chmod +x index.js
# Test locally
npm link
mycli greet "World"
# Publish to npm
npm publish --access public
# Now anyone can install:
npm install -g mycli
mycli --help
Example CLI Ideas
Useful CLIs you could build:
- Project scaffolder (like create-react-app for your stack)
- Git workflow tool (branch naming, commit message validation)
- API testing client (like curl but better)
- Log file analyzer (parse, filter, summarize)
- Environment variable manager (.env file CRUD)
- Database migration tool
- Image optimizer (batch resize/compress)
- Markdown to HTML converter
- Code snippet manager (save/search/insert snippets)
- Deployment helper (build, test, deploy in one command)
Have you built a CLI tool? What does it do?
Follow @armorbreak for more Node.js content.
Top comments (0)