epicora-genesis-cli
CLI que cria os projetos da Epicora a partir dos boilerplates genesis. Você roda um comando, escolhe num menu quais apps o projeto vai ter (api, web, mobile) e quais módulos opcionais quer usar — e recebe os projetos limpos: sem pastas sobrando, sem imports quebrados, sem dependências que você não vai usar, cada um compilando e com o git já iniciado.
Como usar
npx epicora-genesis-cli meu-projeto
O que esse comando faz?
npx Xbaixa e executa o pacoteXdo npm sem você precisar instalar nada antes. Os repos dos boilerplates continuam privados: a CLI clona eles usando as suas credenciais do git (as mesmas que você usa nogit clonedo dia a dia).
Aí aparecem os menus:
◆ Quais apps esse projeto vai ter?
◼ API (NestJS + Mongoose)
◼ Web (Vite + React)
◻ Mobile (Expo + NativeWind)
◆ Quais módulos opcionais esse projeto vai usar?
◼ Push Notifications (Expo) [api, mobile]
E o resultado são pastas lado a lado, uma por app marcado:
meu-projeto-api/ ← compilando, git iniciado, primeiro commit feito
meu-projeto-web/
meu-projeto-mobile/ (se tivesse sido marcado)
Para cada app, a CLI faz sozinha, nesta ordem:
- Clona o boilerplate completo e apaga a pasta
.git(o histórico é do boilerplate, não do seu projeto novo); - Lê o manifesto de módulos opcionais de dentro do próprio clone;
- Remove do código tudo que pertence aos módulos que você NÃO marcou;
- Troca o nome do projeto no
package.json; - Roda os comandos de verificação do boilerplate (ex.:
pnpm installepnpm build) — se algo falhar, a geração para na hora e mostra o erro (você nunca recebe um projeto quebrado); - Roda
git inite faz o primeiro commit.
Flags (opcionais)
| Flag | Para que serve |
|---|---|
--apps <ids> |
Pula o menu de apps. Ex.: --apps api,mobile |
--features <ids> |
Pula o menu de módulos. Ex.: --features push-notifications ou none |
--repo <app=src> |
Origem alternativa de um app: URL git ou pasta local. Pode repetir |
--skip-install |
Não roda install nem verificação (você roda depois na mão) |
A flag --repo com pasta local é a forma de testar mudanças num boilerplate
sem precisar commitar: a CLI copia a pasta do seu disco em vez de clonar.
node index.mjs teste --apps api --repo api=C:\repos\genesis-api --features none
Como funciona por dentro
A ideia central: remover, não montar
Existem duas formas de construir um gerador de projetos:
- Montar (aditiva): manter uma pasta de templates e copiar/colar os pedaços selecionados para dentro de um projeto vazio.
- Remover (subtrativa): clonar o projeto completo e apagar o que não foi selecionado. ← é o que fazemos aqui.
Por que remover é melhor no nosso caso? Porque cada boilerplate é um app de verdade, que compila e roda todo dia. Se os módulos virassem templates soltos, ninguém mais compilaria aquele código no dia a dia — e ele quebraria sem ninguém perceber. Removendo, o código testado é exatamente o código entregue.
A divisão de responsabilidades: CLI burra, manifesto esperto
Esta CLI não sabe nada sobre os módulos dos boilerplates. Tudo que é
específico de cada um (quais módulos existem, quais pastas são de quem, quais
dependências são de quem) vive num arquivo genesis.json na raiz do PRÓPRIO
boilerplate — o manifesto.
Isso tem duas consequências boas:
- Adicionar um módulo novo (Stripe, multi-tenant...) não mexe na CLI. É só editar o manifesto no repo do boilerplate. Publicar versão nova deste pacote é raro de propósito;
- A versão do manifesto sempre casa com a versão do código, porque a CLI lê o manifesto de dentro do clone que acabou de fazer.
Os 3 mecanismos de remoção
1. Apagar pastas. Cada módulo declara suas pastas no campo paths do
manifesto. Não foi selecionado? As pastas somem. Simples, e funciona igual em
qualquer stack.
2. Marcadores de feature (para código no meio de arquivos que ficam). Às
vezes um módulo opcional aparece DENTRO de um arquivo do core. Exemplo real da
api: o create-movie.handler.ts (que fica) envia um push pro diretor do filme
(código do módulo de notificações, que pode sair). Esses trechos ficam entre
comentários especiais:
// <feature:push-notifications>
await this.notificationsService.sendToUser(...);
// </feature:push-notifications>
Na hora de gerar o projeto:
- módulo não selecionado → a CLI apaga o bloco inteiro, incluindo os comentários;
- módulo selecionado → a CLI apaga só as linhas de comentário e mantém o código.
Ou seja: o projeto gerado nunca tem esses comentários — eles só existem nos
boilerplates. E como marcador é só um comentário, funciona em qualquer arquivo
texto: .ts, .tsx, .env.example, app.json, README...
3. Transforms de stack (AST). Algumas stacks têm um "registro central"
que precisa de cirurgia mais fina que apagar linhas. Na NestJS, apagar a pasta
src/notifications não basta: o app.module.ts ainda teria
import { NotificationsModule } ... e NotificationsModule dentro do array
imports — e isso não compila.
Para isso existe o transform nest-modules (ativado quando o manifesto diz
"stack": "nestjs"). Ele usa a biblioteca ts-morph, que lê o arquivo
TypeScript do mesmo jeito que o compilador lê (isso é a "AST": o código
entendido como estrutura, não como texto). Aí ela consegue dizer "remova o
item NotificationsModule do array imports" sem risco de quebrar vírgula,
chave ou formatação — coisa que um find-and-replace de texto quebraria fácil.
O passe roda em TODOS os src/**/*.module.ts, então também conserta módulos
do core que importavam um módulo opcional.
E o web e o mobile? Não precisam de transform nenhum. Os pontos de
registro deles (rotas, telas de navegação, providers) são resolvidos com
marcadores — que vocês já teriam que usar de qualquer forma. Se um dia um
ponto de registro virar dor repetitiva, dá para escrever um transform novo em
lib/transforms/ e ativar pela stack do manifesto.
Rede de segurança: depois de tudo isso, a CLI ainda remove imports que
ficaram sem uso nos arquivos .ts/.tsx alterados (cobre o caso clássico de
alguém marcar o uso e esquecer o import) e roda os comandos de verify do
manifesto. Se sobrou qualquer referência a código apagado, o build acusa e a
geração falha com o erro na tela — nunca silenciosamente.
E o package.json?
Cada módulo declara no manifesto quais dependências npm são só dele (ex.:
expo-server-sdk é só do push). A CLI apaga essas entradas do package.json
ANTES de rodar o install — então elas nem chegam a ser baixadas.
Módulos compartilhados entre apps (o superpoder do multi-app)
Se o mesmo id de módulo existe no manifesto de mais de um app — ex.:
push-notifications na api (módulos Devices/Notifications + SDK do Expo) e no
mobile (telas e handlers de push) — ele aparece uma vez só no menu, com a
indicação [api, mobile], e a escolha vale para todos os apps de uma vez.
Sem isso, seria fácil gerar uma api sem push junto com um mobile cheio de telas de push apontando para endpoints que não existem. Com isso, é impossível: a decisão é uma só.
O manifesto (genesis.json na raiz de cada boilerplate)
{
"schemaVersion": 1,
"stack": "nestjs",
"verify": ["pnpm install", "pnpm build"],
"features": [
{
"id": "push-notifications",
"label": "Push Notifications (Expo)",
"hint": "Devices + Notifications + expo-server-sdk",
"default": true,
"dependsOn": [],
"paths": ["src/devices", "src/notifications"],
"nestModules": ["DevicesModule", "NotificationsModule"],
"npmDependencies": ["expo-server-sdk"],
"npmDevDependencies": [],
"packageScripts": [],
"envVars": []
}
]
}
Campo a campo:
stack— qual conjunto de transforms ativar. Hoje só existe"nestjs". Web e mobile podem simplesmente omitir (paths + marcadores resolvem tudo);verify— os comandos rodados no projeto gerado, em ordem. O primeiro deve ser o install; os seguintes são a verificação (pnpm build,tsc --noEmit...). É a garantia de que o projeto gerado funciona;id— o nome usado nos marcadores (<feature:stripe>), na flag--featurese no compartilhamento entre apps (mesmo id = mesma escolha em todos os apps);label/hint— o texto que aparece no menu;default— se já vem marcado no menu;dependsOn— ids de outros módulos que este precisa. Se alguém marcar Stripe e Stripe depender demulti-tenant, a CLI inclui os dois sozinha;paths— pastas/arquivos a apagar quando o módulo não for selecionado;nestModules— (só na stack nestjs) os nomes das classes de módulo que o transform de AST remove dos arraysimports;npmDependencies/npmDevDependencies/packageScripts— o que apagar dopackage.json;envVars— documentação de quais variáveis de ambiente são do módulo (a remoção delas no.env.exampleé feita pelos marcadores).
Compatibilidade: a CLI também lê o formato antigo da genesis-api (
cli/modules.json), assumindo stack nestjs e verify install+build. E repos SEM manifesto nenhum funcionam normalmente — são clonados, renomeados e verificados, só não oferecem módulos opcionais. Isso permite adotar a CLI primeiro e instrumentar os boilerplates com calma.
Como instrumentar um boilerplate (passo a passo com Stripe)
Suponha que vocês vão criar o módulo de pagamentos na api:
1. Implemente o módulo normalmente (src/payments/...), no boilerplate,
funcionando de verdade. O boilerplate continua sendo um app completo — essa é
a regra de ouro de tudo aqui.
2. Marque o que vazar para fora da pasta do módulo. Tudo que o Stripe tocar em arquivos do core deve ficar entre marcadores:
// user.schema.ts
// <feature:stripe>
@Prop()
stripeCustomerId?: string;
// </feature:stripe>
# .env.example
# <feature:stripe>
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# </feature:stripe>
Não precisa marcar nada em app.module.ts nem em outros *.module.ts — o
transform de AST resolve esses sozinho.
3. Declare o módulo no genesis.json do repo da api (ver formato acima).
Se o Stripe também tiver telas no web, declare um feature com o MESMO id
stripe no genesis.json do genesis-web — vira uma escolha única no menu.
4. Teste os dois caminhos antes de commitar, apontando para a sua pasta local:
node index.mjs teste1 --apps api --repo api=..\genesis-api --features none
node index.mjs teste2 --apps api --repo api=..\genesis-api --features stripe
Os dois projetos gerados precisam passar no verify. Se o "sem" não passar,
quase sempre é um trecho de código que ficou sem marcador.
Quando a CLI em si precisa mudar?
Quase nunca — e é proposital. As únicas situações:
- Boilerplate novo (um 4º app): adicionar uma entrada em
lib/apps.mjs; - Transform novo para uma stack: criar em
lib/transforms/e registrar emSTACK_TRANSFORMSnolib/prune.mjs; - Mudança no pipeline em si (raro).
Nesses casos, publica-se uma versão nova do pacote.
Estrutura do código
genesis-cli/
├── index.mjs # ponto de entrada: menus + loop por app
└── lib/
├── apps.mjs # registro dos boilerplates (id, label, URL)
├── manifest.mjs # lê/valida genesis.json (e o formato legado)
├── prune.mjs # a remoção: paths, marcadores, transforms, órfãos
├── package-json.mjs # renomeia o projeto e limpa deps/scripts
├── run.mjs # executa comandos (com quoting correto no Windows)
└── transforms/
└── nest-modules.mjs # o passe de AST dos *.module.ts (stack nestjs)
Publicação
O pacote se chama epicora-genesis-cli e roda direto com npx epicora-genesis-cli meu-projeto (sem instalar nada antes). A CLI em si não
contém nada secreto (os boilerplates continuam privados, clonados com as
credenciais de quem roda), então pode ir para o npm público — ou para o GitHub
Packages da org, se preferirem auth fechada (com o custo de cada dev precisar
configurar o registry).