Criando uma CLI com Gluegun
Aprenda como criar um app de linha de comando com o Gluegun, e entenda como usei a ferramenta para criar o Replicante, meu mais novo projeto open-source.
Quando comecei a implementar o Replicante (o meu primeiro projeto que considero verdadeiramente open source), precisei de uma ferramenta que me permitisse implementar uma CLI (command line interface) apresentável, fácil de usar, e fácil de desenvolver. Encontrei tudo isso no Gluegun.
#Gluegun?
Gluegun é um toolkit para desenvolvimento de apps de linha de comando escritos em TypeScript. O nome é bem condizente com a sua construção, pois ele cola com maestria uma série de excelentes bibliotecas para resolver os principais problemas de um app de linha de comando.
Ele inclui ferramentas para lidar com todos essas classes de problemas:
- parameters - para trabalhar com parâmetros de linhas de comando (argumentos e opções), usando o yargs-parser ou enquirer
- templates - para gerar arquivos a parter de templates, usando ejs
- patching - para aplicar patches em arquivos, manipulando o conteúdo
- filesystem - para mover arquivos e pastas por aí, usando o fs-jetpack
- system -para executar outros scripts de linha de comando, usando o execa ou cross-spawn
- http - para interagir com APIs, usando axios e apisauce
- prompt - para gerar prompts com opção de auto-completar
- print - para imprimir textos e tabelas coloridos, usando colors, ora ou cli-table3
- semver - para trabalhar com versionamento semântico, usando o semver
- strings - para manipular strings e dados de templates, com várias funções auxiliares do pluralize
- packageManager - para instalar pacotes NPM com Yarn ou NPM
Outra funcionalidade bacana do Gluegun é sua extensibilidade: ele nos permite escrever nossos próprios plugins e extensões de forma bem fácil.
#Quem usa o Gluegun?
Existem alguns apps de peso que foram implementados com o Gluegun. Dentre eles, vale citar o AWS Amplify CLI, para desenvolvimento de aplicações serverless, e o Ignite CLI, um starter kit e CLI para React Native
#Criando nosso primeiro app
Alguns dos exemplos que vou apresentar no resto do tutorial foram adaptados e simplificados dos códigos do Replicante.
Mas primeiro precisamos instalar o Gluegun, e essa é uma tarefa fácil. Basta instalar via Yarn assim:
yarn global add gluegun
E agora estamos prontos para criar nosso primeiro app com o comando gluegun new <nomeprojeto>
. Para o Replicante, usei o comando:
gluegun new replicante
O Gluegun vai perguntar se queremos criar o app em Javascript Moderno (Node 8.2+) ou TypeScript. Particularmente, creio que essa é a melhor opção, pois já traz uma pipeline de build funcionando.

Agora você pode navegar para a pasta do seu novo app, no meu caso, replicante, usando 'cd replicante', e criar um link simbólico para o executável usando o comando yarn link
.
A partir daí, você já pode rodar o seu app usando o próprio nome da ferramenta. No meu caso, se eu rodar o comando replicante
, vou ver algo assim:

#A estrutura de um projeto com Gluegun
O projeto criado para nós pela ferramenta segue uma estrutura bem fácil de acompanhar:

Na pasta commands, o Gluegun já traz dois comandos de exemplo para nos ajudar. O primeiro deles é o generate
, um exemplo de geração de arquivos baseados em templates EJS, que pode ser muito útil numa variedade de cenários de desenvolvimento, como por exemplo, a criação de componentes React.

Já o segundo, é o próprio comando replicante
, que executamos há pouco, e imprime a mensagem "Welcome to your CLI".

Uma peça importante, onde a mágica começa, é o arquivo cli.ts
. Ele é responsável por instanciar o runtime que vai rodar a aplicação. Nessa etapa, podemos configurar os plugins, os locais dos fontes e habilitar comandos padrão como help e version:
const { build } = require('gluegun')
/**
* Create the cli and kick it off
*/
async function run(argv) {
// create a CLI runtime
const cli = build()
.brand('replicante')
.src(__dirname)
.plugins('./node_modules', { matching: 'replicante-*', hidden: true })
.help() // provides default for help, h, --help, -h
.version() // provides default for version, v, --version, -v
.create()
// enable the following method if you'd like to skip loading one of these core extensions
// this can improve performance if they're not necessary for your project:
// .exclude(['meta', 'strings', 'print', 'filesystem', 'semver', 'system', 'prompt', 'http', 'template', 'patching', 'package-manager'])
const toolbox = await cli.run(argv)
// send it back (for testing, mostly)
return toolbox
}
module.exports = { run }
Além disso, ele já gera alguns cenários de teste básicos com Jest, no arquivo __tests__/cli-integration.test.ts
, contemplando a ajuda, a versão e a geração de arquivos com base em templates. Uma mão na roda para começar:
const { system, filesystem } = require('gluegun')
const src = filesystem.path(__dirname, '..')
const cli = async cmd =>
system.run('node ' + filesystem.path(src, 'bin', 'replicante') + ` ${cmd}`)
test('outputs version', async () => {
const output = await cli('--version')
expect(output).toContain('0.0.1')
})
test('outputs help', async () => {
const output = await cli('--help')
expect(output).toContain('0.0.1')
})
test('generates file', async () => {
const output = await cli('generate foo')
expect(output).toContain('Generated file at models/foo-model.ts')
const foomodel = filesystem.read('models/foo-model.ts')
expect(foomodel).toContain(`module.exports = {`)
expect(foomodel).toContain(`name: 'foo'`)
// cleanup artifact
filesystem.remove('models')
})
#Implementando o Replicante
Nesta seção, vou falar um pouco sobre como usei o Gluegun para tornar o Replicante uma realidade.
Resumidamente, o Replicante é um super copiador de projetos. Você diz pra ele em qual dos seus projetos quer se basear, e quais transformações aplicar (geralmente, substituir termos específicos e namespaces), e ele gera um novo com as modificações aplicadas. Muito útil para freelancers que querem reaproveitar projetos anteriores nos seus novos projetos.
Acho interessante começar pelo cli.ts
, e explorar como ele ficou:
const { build } = require('gluegun')
async function run(argv) {
// create a CLI runtime
const cli = build()
.brand('replicante')
.src(__dirname)
.plugins('./node_modules', { matching: 'replicante-*', hidden: true })
.help() // provides default for help, h, --help, -h
.version() // provides default for version, v, --version, -v
.defaultCommand()
.exclude(['semver', 'prompt', 'http', 'package-manager'])
.create()
// run it
const toolbox = await cli.run(argv)
// send it back (for testing, mostly)
return toolbox
}
module.exports = { run }
O código de inicialização acima possui algumas diferenças em relação ao primeiro exemplo que mostrei anteriormente.
A primeira mudança foi a adição da função defaultCommand()
. Ela foi adicionada para permitir remover a implementação do script de comando replicante.ts
, criado junto com o app. No lugar dele, o Gluegun cuida de imprimir as boas vidas, e instruir o usuário sobre o comando help
:

Em seguida, foi adicionada a função exclude
, para a qual eu passei uma lista de módulos do toolkit do Gluegun que a CLI do Replicante não usa, e que não devem ser carregados na inicialização do app. Assim, ele carrega mais rápido e a experiência do usuário melhora significativamente.
A lista completa de módulos que podem ser excluídos vem já na forma de comentário quando o cli.ts
é criado, e serve para consulta, mas por questão de simplicidade vou replicá-la aqui:
.exclude(['meta', 'strings', 'print', 'filesystem', 'semver',
'system', 'prompt', 'http', 'template', 'patching', 'package-manager'])
Criei então um novo comando, chamado create.ts
na pasta commands
. Na declaração do comando, defini o nome e a descrição do mesmo.
import { GluegunCommand, filesystem, strings } from 'gluegun'
const command: GluegunCommand = {
name: 'create',
description:'Create a REPLICANT by applying the RECIPE instructions to the SAMPLE',
run: async toolbox => {
//...
}
}
#Parâmetros
O comando create
precisa de três parâmetros para funcionar:
- O diretório do projeto template
- O arquivo de receita (JSON) com as instruções de transformação
- O diretório de destino (opcional)
Para capturar esses parâmetros, usei as funcionalidades do Gluegun:
run: async toolbox => {
const { parameters, print } = toolbox
// Captura os parâmetros da linha de comando
const templatePath = parameters.first
const recipePath = parameters.second
const targetPath = parameters.options.target || './output'
// Validação básica
if (!templatePath || !recipePath) {
print.error('Usage: replicante create <template-path> <recipe-path> [--target=<output-path>]')
return
}
// Verifica se os arquivos existem
if (!filesystem.exists(templatePath)) {
print.error(`Template path not found: ${templatePath}`)
return
}
if (!filesystem.exists(recipePath)) {
print.error(`Recipe file not found: ${recipePath}`)
return
}
// Continua com a lógica de replicação...
}
#Processamento da receita
O arquivo de receita é um JSON que contém as instruções de transformação. O Gluegun facilita muito a leitura e manipulação desses arquivos:
// Lê o arquivo de receita
const recipe = filesystem.read(recipePath, 'json')
if (!recipe) {
print.error('Failed to read recipe file')
return
}
// Valida a estrutura da receita
if (!recipe.replicantName || !recipe.templateName) {
print.error('Invalid recipe: missing required fields')
return
}
print.info(`Creating replicant '${recipe.replicantName}' from template '${recipe.templateName}'`)
#Copiando e transformando arquivos
A parte mais interessante é a transformação dos arquivos. O Gluegun oferece excelentes ferramentas para manipulação de arquivos:
// Copia todos os arquivos do template para o destino
await filesystem.copyAsync(templatePath, targetPath, {
matching: '**/*',
ignoring: recipe.ignoreArtifacts || []
})
// Aplica as transformações de nome de arquivo
for (const replacement of recipe.fileNameReplacements || []) {
const files = filesystem.find(targetPath, {
matching: `**/*${replacement.from}*`,
files: true,
directories: true
})
for (const file of files) {
const newName = file.replace(replacement.from, replacement.to)
await filesystem.moveAsync(file, newName)
}
}
// Aplica as transformações de conteúdo
for (const replacement of recipe.sourceCodeReplacements || []) {
const files = filesystem.find(targetPath, {
matching: '**/*',
files: true,
ignoring: ['**/*.{png,jpg,jpeg,gif,ico,svg,woff,woff2,ttf,eot}']
})
for (const file of files) {
const content = filesystem.read(file)
if (content && content.includes(replacement.from)) {
const newContent = content.replace(new RegExp(replacement.from, 'g'), replacement.to)
filesystem.write(file, newContent)
}
}
}
#Feedback visual
O Gluegun oferece excelentes ferramentas para feedback visual, incluindo spinners e mensagens coloridas:
const { print } = toolbox
// Inicia um spinner
const spinner = print.spin('Processing template...')
try {
// Faz o processamento
await processTemplate()
// Sucesso
spinner.succeed('Template processed successfully!')
print.success(`Replicant '${recipe.replicantName}' created at ${targetPath}`)
} catch (error) {
// Erro
spinner.fail('Failed to process template')
print.error(error.message)
}
#Conclusão
O Gluegun se mostrou uma ferramenta excelente para criar CLIs em Node.js/TypeScript. Ele abstrai muito da complexidade envolvida na criação de aplicações de linha de comando, permitindo focar na lógica de negócio.
Algumas vantagens que encontrei:
- Facilidade de uso: API simples e intuitiva
- Extensibilidade: Sistema de plugins flexível
- Ferramentas integradas: Tudo que você precisa já vem incluído
- TypeScript: Suporte nativo com tipagem completa
- Testes: Framework de testes integrado
Se você está pensando em criar uma CLI, definitivamente recomendo dar uma olhada no Gluegun. Ele vai economizar muito tempo e esforço, permitindo que você se concentre no que realmente importa: resolver o problema do usuário.