惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
量子位
GbyAI
GbyAI
腾讯CDC
T
Tailwind CSS Blog
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
D
Docker
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
Jina AI
Jina AI
IT之家
IT之家
Y
Y Combinator Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a CLI Tool with Node.js: From Zero to npm
Alex Chen · 2026-05-16 · via DEV Community

Alex Chen

Building a CLI Tool with Node.js: From Zero to npm

CLI tools are the best way to automate repetitive tasks. Here's how to build and publish one.

Why Build a CLI?

  • Automate your daily workflows
  • Share tools with your team (or the world)
  • Learn Node.js streams, file system, process management
  • Publish to npm — instant credibility + potential users

Step 1: Project Setup

mkdir my-cli && cd my-cli
npm init -y

# Install dependencies
npm install commander inquirer chalk ora

Enter fullscreen mode Exit fullscreen mode

package.json essentials:

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "My awesome CLI tool",
  "bin": {
    "my-cli": "./bin/cli.js"
  },
  "type": "module",          // Use ES modules
  "engines": {
    "node": ">=18.0.0"
  },
  "files": [                  # What gets published to npm
    "bin/",
    "src/"
  ],
  "keywords": ["cli", "tool"],
  "license": "MIT"
}

Enter fullscreen mode Exit fullscreen mode

Step 2: The Entry Point

#!/usr/bin/env node
// bin/cli.js — Shebang line makes it executable

import { program } from 'commander';
import chalk from 'chalk';
import { init } from '../src/commands/init.js';

program
  .name('my-cli')
  .description('My awesome CLI tool')
  .version('1.0.0');

// Register commands
program
  .command('init')
  .description('Initialize a new project')
  .option('-n, --name <name>', 'Project name', 'my-app')
  .option('-t, --template <template>', 'Template to use', 'basic')
  .action(init);

// Default action if no command given
program.action(() => {
  program.help();
});

program.parse();

Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x bin/cli.js

Enter fullscreen mode Exit fullscreen mode

Step 3: Your First Command

// src/commands/init.js
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
import ora from 'ora';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export async function init(options) {
  const spinner = ora('Creating project...').start();

  const projectName = options.name;
  const targetDir = path.resolve(process.cwd(), projectName);

  try {
    // Create directory
    await fs.mkdir(targetDir, { recursive: true });

    // Create package.json
    const pkgJson = {
      name: projectName,
      version: '1.0.0',
      type: 'module',
      scripts: { start: 'node index.js' }
    };
    await fs.writeFile(
      path.join(targetDir, 'package.json'),
      JSON.stringify(pkgJson, null, 2)
    );

    // Create main file
    await fs.writeFile(
      path.join(targetDir, 'index.js'),
      `console.log("Hello from ${projectName}!");\n`
    );

    // Create README
    await fs.writeFile(
      path.join(targetDir, 'README.md'),
      `# ${projectName}\n\nGetting started:\n\n\`\`\`bash\nnpm install\nnpm start\n\`\`\`\n`
    );

    spinner.succeed(chalk.green('Project created!'));

    console.log('\nNext steps:');
    console.log(`  cd ${projectName}`);
    console.log('  npm install');
    console.log('  npm start');

  } catch (err) {
    spinner.fail(chalk.red('Failed to create project'));
    console.error(err.message);
    process.exit(1);
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Interactive Prompts

// src/commands/create.js
import inquirer from 'inquirer';
import chalk from 'chalk';

export async function create() {
  console.log(chalk.blue.bold('\n🚀 Create a new component\n'));

  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'name',
      message: 'Component name:',
      validate: (input) => input.length > 0 || 'Name is required'
    },
    {
      type: 'list',
      name: 'type',
      message: 'Component type:',
      choices: ['Functional', 'Class-based', 'Hook'],
      default: 'Functional'
    },
    {
      type: 'checkbox',
      name: 'features',
      message: 'Features:',
      choices: [
        { name: 'TypeScript support', value: 'ts' },
        { name: 'CSS Modules', value: 'css' },
        { name: 'Storybook story', value: 'storybook' },
        { name: 'Unit tests', value: 'test' }
      ]
    },
    {
      type: 'confirm',
      name: 'confirm',
      message: 'Confirm creation?',
      default: true
    }
  ]);

  if (!answers.confirm) {
    console.log(chalk.yellow('Cancelled.'));
    return;
  }

  console.log(chalk.green('\n✅ Creating component...'));
  console.log(JSON.stringify(answers, null, 2));
}

Enter fullscreen mode Exit fullscreen mode

Step 5: Output Formatting

// src/utils/display.js
import chalk from 'chalk';
import Table from 'cli-table3'; // npm install cli-table3

export function showTable(data) {
  const table = new Table({
    head: ['Name', 'Status', 'Size'].map(h => chalk.cyan(h)),
    style: { head: [], border: [] }
  });

  data.forEach(row => {
    const status = row.status === 'ok' 
      ? chalk.green('') 
      : chalk.red('');
    table.push([row.name, status, row.size]);
  });

  console.log(table.toString());
}

export function success(message) {
  console.log(chalk.green(`✔ ${message}`));
}

export function error(message) {
  console.log(chalk.red(`✖ ${message}`));
}

export function warning(message) {
  console.log(chalk.yellow(`⚠ ${message}`));
}

export function info(message) {
  console.log(chalk.blue(`ℹ ${message}`));
}

Enter fullscreen mode Exit fullscreen mode

Step 6: File Operations

// src/utils/files.js
import fs from 'fs/promises';
import path from 'path';

export async function ensureDir(dirPath) {
  await fs.mkdir(dirPath, { recursive: true });
}

export async function copyTemplate(templateDir, targetDir, replacements = {}) {
  const files = await fs.readdir(templateDir);

  for (const file of files) {
    const src = path.join(templateDir, file);
    const dest = path.join(targetDir, file);

    const stat = await fs.stat(src);
    if (stat.isDirectory()) {
      await copyTemplate(src, dest, replacements);
    } else {
      let content = await fs.readFile(src, 'utf8');

      // Replace template variables
      for (const [key, value] of Object.entries(replacements)) {
        content = content.replaceAll(`{{${key}}}`, value);
      }

      await fs.writeFile(dest, content);
    }
  }
}

export async function readConfig(dirPath) {
  const configPath = path.join(dirPath, '.config.json');
  try {
    return JSON.parse(await fs.readFile(configPath, 'utf8'));
  } catch {
    return null;
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 7: Error Handling

// src/utils/errors.js
import chalk from 'chalk';

class CLIError extends Error {
  constructor(message, code = 1) {
    super(message);
    this.name = 'CLIError';
    this.code = code;
  }
}

function handleError(err) {
  if (err instanceof CLIError) {
    console.error(chalk.red(`Error: ${err.message}`));
    process.exit(err.code);
  }

  if (err.code === 'ENOENT') {
    console.error(chalk.red(`File not found: ${err.path}`));
    process.exit(1);
  }

  if (err.code === 'EACCES') {
    console.error(chalk.red('Permission denied. Try with sudo?'));
    process.exit(1);
  }

  // Unexpected error
  console.error(chalk.red('Something went wrong:'));
  console.error(err.stack || err.message);
  process.exit(1);
}

process.on('uncaughtException', handleError);
process.on('unhandledRejection', handleError);

export { CLIError, handleError };

Enter fullscreen mode Exit fullscreen mode

Step 8: Testing Locally

# Link globally for testing
npm link

# Now you can run it anywhere:
my-cli --help
my-cli init --name test-app
my-cli create

# Unlink when done:
npm unlink -g my-cli

Enter fullscreen mode Exit fullscreen mode

Step 9: Publishing to npm

# 1. Login to npm
npm login

# 2. Check if name is available
npm view my-cli 2>/dev/null && echo "TAKEN" || echo "AVAILABLE"

# 3. Dry run (check what will be published)
npm pack --dry-run

# 4. Publish!
npm publish --access public

# After publishing:
# Users can install: npm install -g my-cli

Enter fullscreen mode Exit fullscreen mode

Step 10: CI/CD for CLI Tools

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - run: npm install
      - run: npm test

      - run: npm link
      - run: my-cli --help
      - run: my-cli init --name test-project

Enter fullscreen mode Exit fullscreen mode

Pro Tips

Make It Fast

// Lazy-load heavy modules
async function loadHeavyModule() {
  const { somethingHeavy } = await import('./heavy-module.js');
  return somethingHeavy;
}

Enter fullscreen mode Exit fullscreen mode

Add Shell Autocomplete

// Commander supports autocomplete
program.configureHelp({
  commandDescription: (cmd) => `${cmd.description()} [options]`
});

Enter fullscreen mode Exit fullscreen mode

Update Notification

import pkg from '../package.json' assert { type: 'json' };
import { checkForUpdate } from './utils/update.js';

// On every run, silently check for updates
checkForUpdate(pkg.name, pkg.version).catch(() => {});

Enter fullscreen mode Exit fullscreen mode


What CLI tool would make YOUR life easier? Maybe you should build it.

Follow @armorbreak for more Node.js tutorials.