Como construir uma CLI em TypeScript com Commander ou oclif
1. Introdução às CLIs em TypeScript e escolha do framework
Ferramentas de linha de comando (CLI) são essenciais para automatizar tarefas no desenvolvimento de software. TypeScript eleva a criação de CLIs com tipagem segura, autocomplete inteligente e manutenibilidade superior. Ao escolher um framework, duas opções se destacam:
Commander.js é uma biblioteca leve e flexível, ideal para projetos pequenos e médios. Sua API minimalista permite definir comandos, opções e argumentos com poucas linhas de código.
oclif (Open CLI Framework) da Heroku é uma solução opinativa e escalável, projetada para CLIs complexas com suporte nativo a plugins, múltiplos comandos e scaffolding automático.
A escolha depende do escopo: Commander para simplicidade, oclif para crescimento.
2. Configuração inicial do projeto
Inicializando o projeto TypeScript
mkdir minha-cli
cd minha-cli
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
Configure o tsconfig.json com:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
Configuração com Commander
npm install commander
Estrutura básica do arquivo src/index.ts:
#!/usr/bin/env node
import { Command } from 'commander';
const program = new Command();
program
.name('minha-cli')
.description('CLI de exemplo com Commander')
.version('1.0.0');
program.parse(process.argv);
Configuração com oclif
npx oclif generate minha-cli
cd minha-cli
npm install
O gerador cria automaticamente a estrutura de diretórios, comandos de exemplo e configuração completa.
3. Estrutura de comandos e argumentos
Commander: comandos principais e subcomandos
// src/index.ts
import { Command } from 'commander';
const program = new Command();
program
.command('criar <nome>')
.description('Cria um novo recurso')
.option('-t, --tipo <tipo>', 'Tipo do recurso', 'padrao')
.action((nome, options) => {
console.log(`Recurso "${nome}" criado como tipo "${options.tipo}"`);
});
program
.command('listar')
.description('Lista recursos disponíveis')
.action(() => {
console.log('Listando recursos...');
});
program.parse(process.argv);
oclif: comandos aninhados e namespaces
// src/commands/criar.ts
import { Command, Flags } from '@oclif/core';
export default class Criar extends Command {
static description = 'Cria um novo recurso';
static flags = {
tipo: Flags.string({ char: 't', description: 'Tipo do recurso', default: 'padrao' }),
};
static args = [{ name: 'nome', required: true }];
async run(): Promise<void> {
const { args, flags } = await this.parse(Criar);
this.log(`Recurso "${args.nome}" criado como tipo "${flags.tipo}"`);
}
}
Para subcomandos, crie diretórios aninhados: src/commands/recurso/criar.ts.
Manipulação de argumentos e flags
Commander:
.option('-d, --debug', 'Modo debug', false)
.option('-n, --numero <valor>', 'Número inteiro', parseInt)
.argument('<arquivo>', 'Arquivo de entrada')
.argument('[diretorio]', 'Diretório de saída', '.')
oclif:
static flags = {
debug: Flags.boolean({ char: 'd', default: false }),
numero: Flags.integer({ char: 'n', description: 'Número inteiro' }),
};
static args = {
arquivo: Args.string({ required: true, description: 'Arquivo de entrada' }),
diretorio: Args.string({ default: '.', description: 'Diretório de saída' }),
};
4. Validação de entrada e tratamento de erros
Validadores customizados (Commander)
function validarEmail(valor: string): string {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor)) {
throw new Error('Email inválido');
}
return valor;
}
program
.command('enviar-email <email>')
.argument('<email>', 'Email do destinatário', validarEmail)
.action((email) => {
console.log(`Email enviado para ${email}`);
});
Mensagens de erro amigáveis
Commander:
program.exitOverride();
program.configureOutput({
outputError: (str, write) => write(`❌ Erro: ${str}`)
});
oclif:
async run(): Promise<void> {
try {
// lógica principal
} catch (error) {
this.error('Falha ao executar comando', { exit: 1 });
}
}
Exibição de help automático
Ambos frameworks geram help automaticamente com --help ou -h.
5. Interação com o usuário e saída formatada
Leitura de input com enquirer
npm install enquirer
import { Input, Select } from 'enquirer';
const resposta = await new Input({
name: 'nome',
message: 'Qual seu nome?'
}).run();
const cor = await new Select({
name: 'cor',
message: 'Escolha uma cor',
choices: ['Vermelho', 'Azul', 'Verde']
}).run();
Formatação com chalk e cli-table3
npm install chalk cli-table3
import chalk from 'chalk';
import Table from 'cli-table3';
const tabela = new Table({
head: ['Nome', 'Idade', 'Cargo'],
colWidths: [20, 10, 15]
});
tabela.push(
['João Silva', 30, 'Desenvolvedor'],
['Maria Souza', 28, 'Designer']
);
console.log(chalk.green.bold('Funcionários:'));
console.log(tabela.toString());
console.log(chalk.yellow(`Total: 2 registros`));
Barras de progresso com ora e cli-progress
npm install ora cli-progress
import ora from 'ora';
import cliProgress from 'cli-progress';
const spinner = ora('Processando...').start();
// simula trabalho
setTimeout(() => {
spinner.succeed('Concluído!');
}, 2000);
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(100, 0);
for (let i = 0; i <= 100; i++) {
bar.update(i);
// simula progresso
}
bar.stop();
6. Testes e depuração da CLI
Testes unitários com Jest
npm install jest ts-jest @types/jest --save-dev
// src/__tests__/comandos.test.ts
import { Command } from 'commander';
test('comando criar deve aceitar nome', () => {
const program = new Command();
program
.command('criar <nome>')
.action((nome) => {
expect(nome).toBe('meu-recurso');
});
program.parse(['node', 'test', 'criar', 'meu-recurso']);
});
Testes de integração com execa
npm install execa --save-dev
import { execa } from 'execa';
test('CLI deve executar comando listar', async () => {
const { stdout } = await execa('node', ['./dist/index.js', 'listar']);
expect(stdout).toContain('Listando recursos');
});
Depuração com Node.js inspect
node --inspect-brk dist/index.js meu-comando
Use Chrome DevTools para depuração visual, breakpoints e inspeção de variáveis.
7. Empacotamento e distribuição
Configuração do bin no package.json
{
"bin": {
"minha-cli": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
Publicação no npm
npm login
npm publish
npm install -g minha-cli
Executáveis standalone com pkg
npm install -g pkg
pkg dist/index.js --targets node18-linux-x64,node18-win-x64,node18-macos-x64
Isso gera binários independentes que não requerem Node.js instalado.
8. Comparação prática: Commander vs oclif em projetos reais
Exemplo: CLI de gerenciamento de tarefas
Commander (simplicidade):
// tarefas-cli/src/index.ts
const program = new Command();
program
.command('adicionar <tarefa>')
.option('-p, --prioridade <nivel>', 'Prioridade (alta, media, baixa)', 'media')
.action((tarefa, options) => {
console.log(`Tarefa "${tarefa}" adicionada com prioridade ${options.prioridade}`);
});
program
.command('listar')
.option('--prioridade <nivel>', 'Filtrar por prioridade')
.action((options) => {
console.log(`Listando tarefas${options.prioridade ? ` (prioridade: ${options.prioridade})` : ''}`);
});
program.parse();
oclif (escalabilidade):
// tarefas-cli/src/commands/adicionar.ts
export default class Adicionar extends Command {
static description = 'Adiciona nova tarefa';
static args = { tarefa: Args.string({ required: true }) };
static flags = {
prioridade: Flags.string({ options: ['alta', 'media', 'baixa'], default: 'media' })
};
async run() {
const { args, flags } = await this.parse(Adicionar);
this.log(`Tarefa "${args.tarefa}" adicionada com prioridade ${flags.prioridade}`);
}
}
Quando usar cada um
| Característica | Commander | oclif |
|---|---|---|
| Complexidade | Baixa | Alta |
| Curva de aprendizado | Rápida | Moderada |
| Plugins | Manual | Nativo |
| Scaffolding | Manual | Automático |
| Ideal para | Scripts rápidos, CLIs pequenas | CLIs empresariais, multi-comando |
Commander é perfeito para ferramentas internas, scripts de automação e projetos onde a simplicidade é prioridade.
oclif brilha em CLIs que crescerão com o tempo, necessitam de plugins, hooks e comandos aninhados complexos.
Referências
- Documentação oficial do Commander.js — Repositório oficial com guia completo de API, exemplos e migração
- Documentação oficial do oclif — Guia completo do framework oclif, incluindo scaffolding, plugins e hooks
- TypeScript Handbook: CLI Tools — Documentação oficial do TypeScript sobre ferramentas de linha de comando
- Tutorial: Building a CLI with Node.js and Commander — Tutorial prático da DigitalOcean sobre criação de CLIs com Commander
- How to Build a CLI with oclif and TypeScript — Artigo da Heroku no Dev.to sobre construção de CLIs escaláveis com oclif
- npm Documentation: Creating Node.js modules — Guia oficial do npm para empacotamento e publicação de módulos Node.js
- pkg: Package your Node.js project into an executable — Ferramenta para criar executáveis standalone a partir de projetos Node.js