DEV Community

Alex Chen
Alex Chen

Posted on

Building a CLI Tool with Node.js (The Complete Guide)

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

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

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

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

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

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

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

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

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

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

Have you built a CLI tool? What does it do?

Follow @armorbreak for more Node.js content.

Top comments (0)