Skip to main content
TutorialsTools & Productivity

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

Create a professional CLI tool with argument parsing, colorful output, progress indicators, and publish it to NPM for the world to use.

Mbeah Essilfie

Mbeah Essilfie

May 30, 2026 at 06:03 AM

9 min read 2146 views
Building a CLI Tool with Node.js: From Zero to NPM

Why Build a CLI?

CLIs are the most underrated way to boost productivity. Instead of remembering complex commands, wrap them in a tool tailored to your workflow.

Project Setup

mkdir my-cli && cd my-cli
pnpm init
pnpm add commander chalk ora
pnpm add -D typescript @types/node tsx
// package.json
{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "mycli": "./dist/index.js"
  },
  "type": "module"
}

Basic Command Structure

#!/usr/bin/env node
// src/index.ts
import { Command } from 'commander'
import chalk from 'chalk'

const program = new Command()

program
  .name('mycli')
  .description('My awesome CLI tool')
  .version('1.0.0')

program
  .command('init')
  .description('Initialize a new project')
  .argument('[name]', 'Project name', 'my-project')
  .option('-t, --template <template>', 'Template to use', 'default')
  .action(async (name, options) => {
    console.log(chalk.blue(`Creating project: ${name}`))
    console.log(chalk.gray(`Template: ${options.template}`))
    await createProject(name, options.template)
    console.log(chalk.green('✓ Done!'))
  })

program.parse()

Progress Indicators

import ora from 'ora'

async function createProject(name: string, template: string) {
  const spinner = ora('Creating project structure...').start()

  await mkdir(name)
  spinner.text = 'Installing dependencies...'

  await installDeps(name)
  spinner.text = 'Configuring...'

  await configure(name, template)
  spinner.succeed('Project created successfully!')
}
Good CLI UX means: progress feedback, clear error messages, and --help that actually helps. Treat your CLI like a UI.

Interactive Prompts

import { input, select, confirm } from '@inquirer/prompts'

async function interactiveInit() {
  const name = await input({
    message: 'Project name:',
    default: 'my-app',
  })

  const template = await select({
    message: 'Choose a template:',
    choices: [
      { name: 'Minimal', value: 'minimal' },
      { name: 'Full-stack (Nuxt)', value: 'nuxt' },
      { name: 'API only', value: 'api' },
    ],
  })

  const useDocker = await confirm({
    message: 'Add Docker setup?',
    default: true,
  })

  return { name, template, useDocker }
}

Error Handling

program
  .command('deploy')
  .action(async () => {
    try {
      await deploy()
    } catch (error) {
      console.error(chalk.red('✗ Deploy failed:'))
      console.error(chalk.gray((error as Error).message))
      process.exit(1)
    }
  })

Publishing to NPM

# Build
pnpm tsc

# Test locally
npm link
mycli --help

# Publish
npm login
npm publish

Tips

  • Always provide --help and --version
  • Use exit codes (0 = success, 1 = error)
  • Support piping (stdin/stdout)
  • Add shell completions for power users
  • Write a good README with examples

A well-crafted CLI is a gift to your future self and your team.

TypeScriptNode.js
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
982