1

Como Criar uma CLI Profissional com Node.js e TypeScript

A maioria das CLIs internas de times de engenharia começa como um script solto com process.argv[2]. Funciona por duas semanas. Depois alguém passa uma flag errada, o script quebra silenciosamente, e ninguém sabe por quê.

Uma CLI profissional resolve isso: parsing tipado de argumentos, mensagens de erro claras, help gerado automaticamente, exit codes corretos e distribuição via npm. Este post monta uma do zero, com decisões justificadas em cada etapa.

A stack e por que essas escolhas

Existem três bibliotecas dominantes para parsing de argumentos em CLIs Node.js. A escolha depende da complexidade da sua ferramenta:

Critériocommanderyargscitty (unjs)
Subcomandos nativosSim, com API fluenteSim, mas API mais verbosaSim, leve
Tipagem TypeScriptTipos incluídos desde v9@types/yargs separadoTipos nativos
Tamanho (install size)~180 KB~800 KB~30 KB
Geração de helpAutomáticaAutomáticaAutomática
Ecossistema/adoçãonpm, Vite, Prisma CLIwebpack, mochaNuxi, giget
Composição com promptsManual (combine com inquirer)ManualManual

Se a CLI tem menos de 3 subcomandos e zero interatividade, citty resolve com menos dependências. Se tem 5+ subcomandos, validação complexa e precisa de ecossistema maduro, commander é a escolha pragmática. yargs funciona, mas o install size e a API menos fluente tornam difícil justificar quando commander existe.

Este post usa commander + chalk + ora (spinner) + zod (validação de input). A CLI de exemplo será um scaffold: ferramenta que cria estrutura de diretórios para projetos a partir de templates.

Setup do projeto com TypeScript e build para CJS

mkdir scaffold-cli && cd scaffold-cli
npm init -y
npm install commander chalk ora zod
npm install -D typescript @types/node tsx
npx tsc --init

O tsconfig.json precisa de atenção em dois pontos que causam dor de cabeça em CLIs:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "sourceMap": true,
    // skipLibCheck evita conflitos entre @types de dependências transitivas
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

No package.json, o campo bin é o que transforma um módulo npm em CLI executável:

{
  "name": "scaffold-cli",
  "version": "1.0.0",
  "bin": {
    "scaffold": "./dist/index.js"
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "prepublishOnly": "npm run build"
  }
}

O campo files garante que só o diretório dist vai para o registry. Sem ele, node_modules, src e tudo mais vão junto, inflando o pacote.

Entrypoint com shebang e programa principal

#!/usr/bin/env node
// src/index.ts
// O shebang acima é obrigatório: sem ele, sistemas Unix não sabem
// que este arquivo deve rodar com Node.js quando chamado diretamente.

import { Command } from "commander";
import { createCommand } from "./commands/create.js";
import { listCommand } from "./commands/list.js";

const program = new Command();

program
  .name("scaffold")
  .description("Cria estrutura de diretórios para projetos")
  .version("1.0.0");

// Cada subcomando vive em arquivo próprio para manter o entrypoint limpo
program.addCommand(createCommand);
program.addCommand(listCommand);

program.parse();

Dois detalhes que parecem cosméticos mas importam em produção: o .version() habilita --version automaticamente, e o program.parse() sem argumentos lê de process.argv por padrão.

Subcomando com validação via Zod

O subcomando create recebe nome do projeto e template. A validação acontece com Zod porque commander só valida presença de argumentos, não formato ou regras de negócio.

// src/commands/create.ts
import { Command } from 

---

Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/cli-profissional-nodejs-typescript-commander-chalk](https://www.vivodecodigo.com.br/backend/cli-profissional-nodejs-typescript-commander-chalk)
Carregando publicação patrocinada...