Introdução ao Commander e Inquirer
Construir interfaces de linha de comando (CLI) profissionais é uma habilidade essencial para desenvolvedores Node.js modernos. As bibliotecas commander e inquirer são o padrão da indústria para este propósito. O commander gerencia argumentos e subcomandos com elegância, enquanto o inquirer torna a interação com usuários intuitiva através de prompts interativos. Juntas, essas ferramentas permitem criar aplicações CLI robustas, mantendo código limpo e escalável.
Neste artigo, você aprenderá a arquitetar uma CLI profissional desde o zero, entendendo não apenas a sintaxe, mas os padrões que grandes projetos como o Angular CLI e Create React App utilizam. Vamos ao trabalho.
Configuração e Estrutura Base
Setup inicial do projeto
Comece criando um novo projeto Node.js e instalando as dependências necessárias:
mkdir meu-cli
cd meu-cli
npm init -y
npm install commander inquirer chalk
O pacote chalk será útil para colorir a saída do terminal, melhorando a experiência do usuário.
Crie um arquivo bin/cli.js como ponto de entrada. Esta é a convenção padrão em projetos profissionais:
#!/usr/bin/env node
const { program } = require('commander');
const { createProject } = require('../lib/commands');
const { version } = require('../package.json');
program
.version(version)
.description('CLI para gerenciar projetos');
program
.command('create <name>')
.description('Criar um novo projeto')
.action((name) => {
createProject(name);
});
program.parse(process.argv);
No package.json, adicione o ponto de entrada do seu CLI:
{
"name": "meu-cli",
"version": "1.0.0",
"bin": {
"meu-cli": "./bin/cli.js"
}
}
Instale localmente com npm link para testar: meu-cli --version funcionará em qualquer lugar do seu terminal.
Integrando Commander para Subcomandos
Estrutura avançada com subcomandos
O commander brilha ao gerenciar múltiplos comandos. Crie uma estrutura modular em lib/commands/index.js:
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs').promises;
const path = require('path');
async function createProject(name) {
try {
const answers = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: 'Escolha um template:',
choices: ['React', 'Vue', 'Express']
},
{
type: 'confirm',
name: 'git',
message: 'Inicializar repositório Git?',
default: true
}
]);
const projectPath = path.join(process.cwd(), name);
await fs.mkdir(projectPath, { recursive: true });
console.log(chalk.green(`✓ Projeto "${name}" criado com sucesso!`));
console.log(chalk.blue(`Template: ${answers.template}`));
if (answers.git) {
console.log(chalk.green('✓ Git iniciado'));
}
} catch (error) {
console.error(chalk.red('✗ Erro ao criar projeto:'), error.message);
process.exit(1);
}
}
module.exports = { createProject };
Agora, no bin/cli.js, estenda com mais comandos:
#!/usr/bin/env node
const { program } = require('commander');
const { createProject, listProjects, deleteProject } = require('../lib/commands');
const { version } = require('../package.json');
program
.version(version)
.description('Gerenciador profissional de projetos');
program
.command('create <name>')
.description('Criar novo projeto')
.option('-t, --template <type>', 'especificar template', 'React')
.action((name, options) => createProject(name, options));
program
.command('list')
.alias('ls')
.description('Listar projetos')
.action(() => listProjects());
program
.command('delete <name>')
.description('Deletar projeto')
.option('-f, --force', 'deletar sem confirmação')
.action((name, options) => deleteProject(name, options));
program
.command('config')
.description('Configurar CLI')
.action(() => {
console.log(chalk.cyan('Abrindo configurações...'));
});
program.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}
Prompts Interativos com Inquirer
Padrões profissionais de interação
O inquirer oferece diversos tipos de prompts. Aqui está um exemplo prático que demonstra a variedade:
async function configurarProjeto(nome) {
const answers = await inquirer.prompt([
{
type: 'checkbox',
name: 'features',
message: 'Quais dependências deseja instalar?',
choices: [
{ name: 'ESLint', checked: true },
{ name: 'Prettier', checked: true },
{ name: 'Jest', value: 'jest' },
{ name: 'Husky', value: 'husky' }
]
},
{
type: 'password',
name: 'apiKey',
message: 'Insira sua chave de API:',
mask: '*'
},
{
type: 'input',
name: 'author',
message: 'Nome do autor:',
default: 'Seu Nome',
validate: (input) => input.length > 0 || 'Campo obrigatório'
},
{
type: 'number',
name: 'port',
message: 'Porta do servidor:',
default: 3000
}
]);
return answers;
}
Combine isso com validação e tratamento de erros robusto:
async function setupAndValidate() {
try {
const config = await configurarProjeto('novo-app');
console.log(chalk.green('\n✓ Configuração concluída!'));
console.log(chalk.gray(JSON.stringify(config, null, 2)));
return config;
} catch (error) {
if (error.isTtyError) {
console.error(chalk.red('CLI requer ambiente TTY interativo'));
} else {
console.error(chalk.red('Erro na configuração:'), error.message);
}
process.exit(1);
}
}
Padrões Profissionais e Boas Práticas
Escalabilidade e manutenibilidade
Um CLI profissional deve ser modular. Separe responsabilidades em diferentes arquivos:
projeto/
├── bin/
│ └── cli.js // Ponto de entrada
├── lib/
│ ├── commands/
│ │ ├── index.js
│ │ ├── create.js
│ │ ├── delete.js
│ │ └── list.js
│ ├── prompts.js // Perguntas reutilizáveis
│ ├── utils.js // Funções auxiliares
│ └── config.js // Gerenciamento de config
└── package.json
Crie um arquivo lib/prompts.js para centralizar questões:
const questions = {
projectName: {
type: 'input',
name: 'name',
message: 'Nome do projeto:',
validate: (input) => /^[a-z0-9-]+$/.test(input) || 'Use apenas letras minúsculas, números e hífens'
},
selectTemplate: {
type: 'list',
name: 'template',
message: 'Escolha um template:',
choices: ['React', 'Vue', 'Express', 'Next.js']
},
confirmInstall: {
type: 'confirm',
name: 'install',
message: 'Instalar dependências agora?',
default: true
}
};
module.exports = { questions };
Depois, reutilize em seus comandos:
const { questions } = require('./prompts');
async function createProject(name, options) {
const answers = await inquirer.prompt([
{ ...questions.selectTemplate },
{ ...questions.confirmInstall }
]);
// Sua lógica aqui
}
Adicione tratamento de erros global e feedback visual consistente usando chalk:
function logSuccess(message) {
console.log(chalk.green(`✓ ${message}`));
}
function logError(message) {
console.error(chalk.red(`✗ ${message}`));
}
function logInfo(message) {
console.log(chalk.blue(`ℹ ${message}`));
}
module.exports = { logSuccess, logError, logInfo };
Conclusão
Você aprendeu como construir uma CLI profissional usando commander para gerenciar comandos de forma elegante e inquirer para criar interfaces interativas ricas. Os três conceitos-chave são: (1) Modularizar sua arquitetura separando comandos, prompts e utilitários em arquivos específicos; (2) Usar padrões do ecossistema, como o diretório bin/ para entrada e respeitar convenções do npm; (3) Investir em experiência do usuário com feedback visual claro e validação robusta de entrada.
Com essa base sólida, você está pronto para construir ferramentas que competem com as melhores CLI do mercado. Pratique criando pequenos projetos e explore as opções avançadas da documentação oficial.