Lição 28 · Fusão v3 · apps/cli ← índice
Alembic × Hermes — O Curso de Fusão v3 · Fechamento (26–30)

apps/cli: a superfície única

Tudo que a fusão construiu — funil, employees, memória, marketing, RAG — sai por UMA porta: o único app entre os 28 workspaces. São 33 comandos top-level e 47 formas documentadas, todos nascidos do mesmo protocolo de seis passos, todos obedecendo à mesma doutrina (dry-run por default) e, desde o digest #1, com os próprios números policiados por script — a lei do derive-counts.

33
comandos top-level (commandSchema, args.ts:18–52)
47
formas documentadas no USAGE (index.ts:68–196)
1
app na workspace — a porta única do motor
0/1/2
exit codes: sucesso · falha de execução · uso inválido
1

O protocolo: seis passos, três arquivos

Um comando novo NUNCA é improvisado. A receita canônica (documentada no CLAUDE.md do repo, "How to add or change a CLI command") tem seis passos: schema Zod em args.ts (<comando>ArgsSchema + entrada no PARSE_OPTIONS + mapeamento no toRecord); implementação em commands.ts como run<Comando> devolvendo Result<T, Error> — nunca lançando; dispatch em index.ts; atualizar o USAGE; testes em commands.test.ts/e2e.test.ts; documentar em CLAUDE.md, AGENTS.md e README. Pular o ④ ou o ⑥ cria o drift que o derive-counts denuncia (seção 4).

No caminho de execução, o argv percorre: parseCliArgs (args.ts:1248) tokeniza, toRecord (args.ts:993) casa posicionais + flags num record único — inclusive lendo ambiente: --offline OU ALEMBIC_OFFLINE ligam o modo hermético —, o parsedArgsSchema.safeParse valida TUDO na borda, e só então dispatch chama o run<Comando>. Uso inválido nem chega ao comando: index.ts:434–438 imprime a mensagem + o USAGE no stderr e devolve EXIT_USAGE = 2 (index.ts:225).

argv alembic … tokenise + toRecord args.ts:993 (+ALEMBIC_OFFLINE) Zod safeParse parseCliArgs · args.ts:1248 run<Comando> Result, nunca lança exit 0 exit 1 uso inválido → stderr: mensagem + USAGE → exit 2 index.ts:434–438 · EXIT_USAGE (index.ts:225) — o comando NUNCA roda
A mesma esteira para os 33 comandos: parse e validação na borda, Result no meio, exit code na saída.

Pense como… a recepção de um hospital: ninguém entra direto na sala de cirurgia — triagem (Zod), prontuário tipado (Args) e só então o especialista (run<Comando>). Onde quebra: a recepção humana improvisa com paciente incompleto; parseCliArgs recusa QUALQUER coisa fora do schema, com o caminho exato do campo errado na mensagem.

harness · 0004-saida-estruturada-ferramentas · s1
2

CommandDeps: a injeção que faz o CLI testável

Nenhum run<Comando> escreve no stdout por conta própria. Todos recebem CommandDeps (commands.ts:289–300): um write (Writer), um logger (CliLogger) e o seam opcional imagegen — a costura de geração de imagem do course --images, populada na BORDA (index.tsALEMBIC_CLIPROXY_TOKEN/CLIPROXYAPI_API_KEY do ambiente; a library nunca toca process.env). É o mesmo idioma de ports do motor inteiro, agora na casca: os testes injetam writer/logger/imagegen falsos e exercitam comandos INTEIROS sem terminal, sem rede e sem custo — é assim que commands.test.ts e e2e.test.ts (2 dos 8 arquivos de teste do app; ls apps/cli/src/*.test.ts) cobrem a superfície dentro dos 200 arquivos · 1.834 testes verdes da suíte.

apps/cli/src/commands.ts:289–300 (trecho real)
/** Inputs every command shares: where to write output and where to log. */
export interface CommandDeps {
  readonly write: Writer;
  readonly logger: CliLogger;
  /** Optional image-generation seam for `course --images` (item 4.3). The CLI
   *  boundary populates it with the env-sourced cliproxy creds; tests inject fake
   *  runner/fetch/fileIo so the opt-in hero path is exercised WITHOUT any real call. */
  readonly imagegen?: ImageGenDeps;
}
Superfície de teste: 8 arquivos *.test.ts em apps/cli/src/ (prove: ls apps/cli/src/*.test.ts) — de commands.test.ts, que injeta CommandDeps fake por comando, ao e2e.test.ts, que percorre parse→dispatch→exit code de ponta a ponta sem terminal real.
regra vivaEsta página pratica a própria lei: os "33/47" acima vieram do spec DERIVADO da fonte — se a sua contagem manual discordar amanhã, rode o derive-counts antes de confiar no seu dedo (ou no nosso).
Por que isso importa para a fusão: o CLI é FINO de propósito. runDistill, runEmployee, runImport… são adaptadores de borda: parse aqui, decisão nos packages. Quando a lição 30 falar de "superfície única", é isto — um lugar onde TODO caminho do motor tem forma de comando, com as mesmas garantias.
3

Doutrina: dry-run por default, efeito é opt-in

Regra da casa, sem exceção na superfície: comando digitado por curiosidade custa $0 e não muda nada. Cada tipo de efeito tem sua trava EXPLÍCITA — e elas não se confundem:

EfeitoDefaultOpt-in explícitoExemplo
Rede/modelooffline, $0--onlineemployee run imprime o prompt e PARA; import --distill sem --online falha fechado
Escrita em discopreview--writeimport mostra o que gravaria; --write appenda idempotente (dedupe por id)
Gasto realpreview de custo--approve E --yesmarketing video/ad: uma flag sozinha NUNCA gasta — dupla trava
Funil--offline força $0distill <corpus> --offline: hermético, byte-estável
produto · 0006-marketing-factory · s1
TRILHO DEFAULT — $0: preview, dry-run, offline determinístico é onde TODO comando cai sem flags — errar aqui não custa nem muda nada 🔒 rede/modelo --online (preflight; sem gateway ⇒ falha fechado, sem gasto) 🔒 escrita --write (append idempotente, dedupe por id determinístico) 🔒🔒 gasto: --approve E --yes em SÉRIE — decisão e confirmação são atos distintos; 1 chave = preview
Cada efeito tem sua fechadura; sem TODAS as chaves daquele efeito, o comando desce para o trilho $0 — nunca para o erro.

Detalhe de desenho que os quizzes adoram: a dupla trava é permissiva no parse e rígida no efeito. marketing video --approve sem --yes NÃO é erro de uso — o comando roda e entrega o preview de custo, que é o resultado útil e seguro do caminho incompleto. Erro de uso (exit 2) é para argv malformado; caminho incompleto é dry-run.

4

derive-counts: a lei da casa contra o drift

De onde saem "33 comandos / 47 formas"? De script, não de dedo. O USAGE exportado (index.ts:68–196) é a superfície DECLARADA — o contrato legível por humanos e por scripts. A história que virou lei: o digest #1 do failure-historian elegeu o drift de contagem como pior ofensor recorrente → propôs "derivar contagens da fonte" → nasceu scripts/derive-counts.mjs (PR #158) → e a PRIMEIRA execução pegou drift vivo: docs diziam 32 comandos; a fonte dizia 33/47. Desde então, número de superfície se DERIVA. (Este curso obedece: todo número aqui vem do spec derivado, nunca de contagem manual — está no rodapé.)

Você roda alembic marketing video shots.json --approve, sem --yes. O que acontece?
Correto: a dupla trava não é validação de argv — é gate de EFEITO. O comando executa e devolve o preview de custo; a geração paga exige a conjunção --approve E --yes. Uma flag passada por engano num script jamais queima orçamento.
Um PR adiciona o comando novo mas esquece o passo ④ (USAGE). Qual é a consequência concreta?
Correto: o USAGE é texto exportado (index.ts:68–196), não gerado — por isso ele PODE mentir, e por isso existe a lei: derive-counts compara a superfície declarada com os docs e denuncia o drift (foi exatamente o 32→33/47 da primeira execução, PR #158). A receita de 6 passos existe para o drift nunca nascer.
Camada técnica — comandos copy-paste

Explore a superfície inteira sem gastar um centavo (doutrina: tudo abaixo é $0):

# a superfície declarada (os 33/47 saem daqui)
alembic help

# o enum canônico + a esteira de parse
sed -n '18,52p' apps/cli/src/args.ts
sed -n '993,1010p' apps/cli/src/args.ts
sed -n '1248,1270p' apps/cli/src/args.ts

# o contrato de deps injetadas + o caminho do uso inválido
sed -n '289,300p' apps/cli/src/commands.ts
sed -n '430,444p' apps/cli/src/index.ts

# a superfície de teste do app (8 arquivos; e2e cobre parse→dispatch→exit)
ls apps/cli/src/*.test.ts

# dry-runs de doutrina: rodar "de verdade" sem efeito nenhum
alembic import ~/.claude/projects --format claude        # preview; nada gravado
alembic employee run <id> --goal "teste"                 # imprime o prompt e para
alembic marketing campaign request.json --out plan.json  # pipeline inteiro em $0

# o baseline de saúde (a forma canônica, com safe-test)
pnpm -r typecheck && pnpm -r build && node scripts/safe-test.mjs pnpm -w test
O que levar desta lição
Pergunta de acompanhamento sugerida: "o que acontece quando o operador ERRA mesmo assim — símlink que some, empate de custo, teste que passa sem rodar?" — é exatamente a próxima lição: todas as gotchas que custaram caro, cada uma com o erro e o fix diagramados.