O Último CLI: Reconstruindo a Base 51 dos Ferramentas que Foram Construídas Contra
Cada modelo buscou um conjunto diferente de bibliotecas, então eu construí uma base e transformei todos os projetos em upstream nela. Então eu perguntei ao Claude Fable qual CLI humanidade ainda poderia estar usando em uma década, e Opus 5 e eu construímos o que voltou — 46 cláusulas, 92 testes, zero dependências.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
Eu tenho 51 ferramentas de linha de comando nesta máquina. Eu não planejei esse número. Isso aconteceu porque um CLI é a menor distância entre uma ideia e algo que eu realmente posso executar, e porque nos últimos anos eu tive ajuda para escrevê-los mais rápido do que eu poderia sozinho.
Essa ajuda veio com um hábito que notei cedo, quando o GPT-3.5 e os primeiros modelos Claude eram os que eu trabalhava. Pergunte a 3 modelos diferentes para criar um CLI e você obtém 3 opiniões diferentes sobre o que é um CLI. Um busca por Commander. Um busca por Inquirer para a solicitação. Um busca por Chalk porque a saída deve ser colorida. Cada resposta é defensável. Juntos eles são um imposto, porque agora eu possuo 3 bases de código que discordam sobre análise de argumentos, sobre como uma falha parece, e sobre qual dessas bibliotecas eu agora sou responsável por monitorar.
Cada modelo buscou um conjunto diferente de bibliotecas, e eu fui quem teve que viver com todas elas#
O problema não são as bibliotecas. É que as melhorias param de viajar.
Quando 3 CLIs discordam sobre como um comando relata falha, uma correção em um é uma correção em um. Não há nada para enviar para cima. O trabalho não se acumula, e depois da décima ferramenta você não está construindo alavancagem, você está mantendo um portfólio de quase falhas.
Eu já havia escrito sobre querer o oposto disso. Todo o argumento em How to Turn AI Gains Into Compounding Infrastructure é que um ganho se torna durável quando todo projeto dependente o herda. Uma superfície de capacidade compartilhada. Uma regra de promoção. Um lugar onde uma melhoria cai e se espalha.
Eu construí essa camada para capacidade de IA, para fluxo de trabalho, para operações. Eu não a construí para a coisa que eu realmente faço mais frequentemente.
Então eu criei uma base e fiz cada CLI projeto enviar suas melhorias para ela#
A regra era simples e era minha impor: quando um CLI na minha propriedade precisava de algo melhor — uma forma mais limpa de registrar serviços, um caminho de erro melhor, um auxiliar de teste que tornasse um conjunto legível — essa melhoria não ficava no projeto. Ela entrou na base, e a base saiu para os outros.
Esse é todo o design. A base é pequena por intenção. Ela não tem opinião sobre o que sua ferramenta faz. Ela tem uma opinião forte sobre o que um comando é: algo que recebe argumentos, faz trabalho, relata o que aconteceu, e deixa.
A pasta foi criada em 6 de julho de 2025, e 2 das minhas ferramentas dependiam da versão 1.0.0 dela no mesmo dia. Isso é o sinal: não foi construído especulativamente e depois adotado. Foi extraído de trabalho que já existia, no ponto em que copiar a mesma estrutura entre projetos deixou de ser razoável.
Se espalhou rapidamente, porque espalhar era toda a ideia. 8 repositórios estavam sobre isso em 25 dias. 10 em 11 semanas.
Repositories Adopting the Base in 2025Chart data
repositories
Jul 6
2
Jul 8
4
Jul 17
5
Jul 23
7
Jul 30
8
Sep 20
10
Git veio depois de tudo isso. O repositório foi inicializado em 12 de novembro de 2025, 4 meses depois, e publicado no dia seguinte — por isso o histórico de versões e o histórico real discordam, e por isso eu verifiquei o sistema de arquivos em vez de confiar no log de commits quando me sentei para escrever isso.
Esses 10 ferramentas fazem administração do Cloudflare. Local DNS e gerenciamento do nginx. Implantação contra Coolify. Automação de navegador. Relatório de custos entre provedores de modelo. A maioria são privadas, por isso as descrevo pelo que fazem em vez de pelo nome. As públicas são aia, que consultam vários modelos em paralelo, e a base em si. vssh, minha ferramenta de execução remota protegida, também é pública e surgiu do mesmo instinto — construir a superfície do operador uma vez, corretamente, e parar de reconstruí-la.
Os dividendos foram reais e foram chatos, que é a forma correta para dividendos de infraestrutura. Um reforço em uma ferramenta apareceu em todas elas. Quando descobri que um comando poderia imprimir uma mensagem de erro vermelha e ainda sair 0 — informando ao humano que falhou e ao shell que funcionou — a correção não entrou nos 16 lugares em uma ferramenta onde aconteceu. Ela entrou na base, e todas as ferramentas a herdaram.
Depois de 13 meses eu queria que fosse reconstruída, não apenas consertada#
Em agosto de 2026 a base estava funcionando e eu ainda queria que fosse removida.
Não porque estava quebrada. Porque havia acumulado. Porque a regra de exit-code da qual eu mais me orgulhei havia sido retrofitted em vez de projetada desde o início. Porque o mundo para o qual ela foi escrita mudou debaixo dela: a maioria das invocações dos meus CLIs não são mais digitadas por mim. Elas são emitidas por agentes, lendo stdout, stderr, e $? como seus únicos sentidos.
Então, em vez de consertar, eu defini os termos de forma diferente. Eu dei ao Claude Fable uma única instrução, e a tornei deliberadamente grande:
Se este fosse o último framework CLI que a humanidade construiu — o que ainda está em serviço
em uma década — você agora tem a chance de torná-lo assim.
Projete-o a partir daí. Eu não esperava um documento de volta. Eu esperava um plano.
Fable voltou com um tratado, e a restrição era que as promessas tivessem que ser poucas#
O que chegou não foi uma lista de recursos. Foi estruturado como um tratado, dividido ao meio por uma parede dura.
Uma metade era um contrato: o que todo CLI construído sobre esta base garante a todo observador, escrito como cláusulas numeradas na linguagem RFC-2119 — DEVE, NÃO DEVE, DEVERIA, PODE. Doze famílias deles. Códigos de saída. Disciplina de fluxo. Saída da máquina. Auto-descrição. Gramática. Ambiente. Cancelamento. Determinismo. Orçamentos de desempenho. Compatibilidade.
A outra metade era a superfície de autoria, que podia crescer, e que existia apenas para tornar o cumprimento do contrato o caminho de menor resistência.
O raciocínio abaixo dele foi a parte que achei convincente. Um design pensado para durar uma década não pode apostar na moda, porque a moda é o que expira. Ele não pode apostar na astúcia, porque a astúcia é o que você não pode prever no ano 8. Ele só pode apostar nas interfaces que não mudaram desde os 1970s: vetores de argumentos, fluxos 3, um código de saída de 8 bits, variáveis de ambiente. E ele observou o fato genuinamente novo — que o leitor maioritário dessas interfaces agora é uma máquina que não pode fazer uma pergunta de acompanhamento.
A cláusula que acabou organizando tudo o mais foi a que ele abriu com:
Um resultado, muitas renderizações. Um comando calcula um único resultado. O código de saída
código, o texto humano, o documento JSON e as linhas de streaming são todas
projeções desse único valor. Eles não podem se contradizer, porque
há apenas uma fonte.
Essa é a frase que toda a reconstrução gira em torno.
Diagram source
graph LR
A["execute() retorna
um valor"] --> B["código de saída"]
A --> C["texto renderizado
stdout"]
A --> D["envelope JSON
--json"]
A --> E["fluxo NDJSON
--ndjson"]
F["logger.error()
ctx.emit()"] -.-> B
F -.-> G["eventos
stderr"]
Opus 5 e eu encontrei a especificação correta sobre a tese e errada sobre 3 coisas#
É aqui que o trabalho se tornou nosso em vez de meu.
Levei a especificação para Opus 5 e construímos em um dia. Não foi um dia limpo. As partes úteis são os lugares onde o documento encontrou a propriedade e perdeu.
A especificação queria que ctx.args se tornasse um registro de argumentos nomeados. É o
melhor design em isolamento. Também teria quebrado todo comando em cada um
dos 10 ferramentas, porque todos lêem ctx.args como um array. Mantivemos o array
e colocamos os argumentos tipados em ctx.namedArgs ao lado dele. A regra que decidiu
já estava escrito no contrato, uma cláusula acima: nunca quebre um
consumidor outranqueia todo outro valor no repositório, incluindo a própria
completude do contrato.
A especificação queria um grupo de comandos sem verbo ser um erro de uso. Executar um
comando pai sem subcomando sairia 2. Defensável, e teria
alterado o comportamento de todo script que executa um comando de agrupamento puro para ver
seu help. Continuamos imprimindo help e saindo 0.
A especificação assumiu streaming e o único JSON documento como a mesma
funcionalidade. Eles não são. Streamar um milhão de itens em memória constante é o
ponto de um e impossível no outro, porque um chamador que pediu um
único documento pediu que fosse único. Separamos o comportamento e anotamos
qual cláusula governa qual.
Também encontramos coisas que a especificação não poderia saber, porque eram visíveis apenas a partir do artefato. Um arquivo de teste que executou testes 0 e relatou sucesso, tendo matado o runner parcialmente. Tratamento de sinal que saiu 0 ao Ctrl-C — um comando interrompido relatando que havia sido bem-sucedido. Dois auxiliares de saída que juntaram suas linhas com uma barra invertida-n literal, então toda tabela retornou em uma linha. Um auxiliar de cor que, uma vez que substituímos a dependência que ele havia embrulhado, reduziu silenciosamente sua própria assinatura de tipo e quebrou código que não havia mudado um caractere.
Esse último vale a pena ficar sentado com. Foi capturado por nenhum teste que nenhum de nós escreveu. Surgiu em uma verificação de tipo de consumer durante a migração, que é o único lugar onde poderia ter.
O contrato só conta porque a construção falha quando uma cláusula não tem teste#
Uma promessa que nada verifica é um comentário.
Então o conjunto de conformidade analisa o arquivo de contrato, encontra cada cláusula contendo a palavra MUST, e falha a construção se uma delas não tiver teste registrado. Você não pode adicionar uma promessa a este projeto sem adicionar a coisa que a prova, no mesmo commit.
Conformance Tests by Contract FamilyChart data
Value
Grammar
20
Exit codes (truth)
12
Machine output
11
Self-description
10
Environment
8
Prompt safety
6
Streams
5
Cancellation
5
Determinism
5
46 cláusulas normativas. 92 testes mapeados a elas. 184 testes no total.
E nenhum desses testes de conformidade roda contra a fonte. Eles constroem o pacote com seu próprio script de construção, executam npm pack, descompactam o tarball, escrevem CLIs de fixture que importam o ponto de entrada descompactado, e os iniciam sob Node, Bun e Deno — afirmando sobre o status de saída e os bytes exatamente como um shell os veria.
Essa forma não foi uma escolha estética. Este pacote, uma vez enviado, tinha um stub de KB 65. Um único sinalizador "sideEffects": false deixou o bundler tree-shake o roteador e o módulo de código de saída do artefato enquanto seus nomes permaneciam na lista de exportação. A construção saiu 0. O conjunto de origem permaneceu verde o tempo todo. Somente o artefato era evidência, e ninguém estava olhando para o artefato.
Migrando 7 ferramentas encontrou 3 portões que ninguém sabia que existiam#
Migramos 7 de 10 CLIs no mesmo dia, e a migração é onde o design obteve sua nota real.
O dividendo caiu imediatamente e não custou nada: porque os comandos na versão antiga já retornavam valores — o framework os usava apenas para derivar um código de saída, depois descartava — cada um desses valores de retorno tornou-se um JSON payload no dia da atualização. 7 ferramentas ganharam saída legível por máquina sem que nenhum comando fosse reescrito.
O que não esperávamos era o mesmo defeito em 3 ferramentas diferentes, nenhuma das quais sabia da outra. Cada uma tinha um portão na frente do roteador: uma lista mantida à mão de nomes de comandos válidos, ou uma etapa de inicialização que exigia credenciais antes que qualquer coisa rodasse. Em todos os casos o novo comando manifest — o que descreve toda a superfície da ferramenta em uma única chamada, para que um agente possa aprender sem ler o código fonte — respondia com "comando desconhecido" ou "token ausente."
Um deles mantinha uma segunda cópia de sua lista de comandos e uma tela de ajuda escrita à mão, ambas haviam divergido do que a ferramenta realmente fazia. Excluir ambos tirou sua suíte de 52 passando com 3 falhando para 57 passando com 0. A maior ferramenta do conjunto tem 364 testes, e eles passaram antes e depois da atualização sem alteração de código fonte.
O padrão se generalizou bem o suficiente para se tornar um procedimento escrito, enviado dentro do próprio pacote. É 9 passos, e os 2 passos que consomem o tempo são os 2 que ninguém antecipa.
Zero dependências é o único número que não precisa de monitoramento#
A base tinha 2 dependências de tempo de execução. Agora não tem nenhuma.
Isso foi parcialmente estético e principalmente aritmético. Em 8 de setembro de 2025, um atacante phishingou a conta npm de Josh Junon, mantenedor de alguns dos pacotes mais dependentes em JavaScript, usando um domínio falso e um código de uso único ao vivo. 18 pacotes foram publicados com versões maliciosas, incluindo chalk e debug — pacotes que juntos têm algo em torno de 2,6 bilhões de downloads por semana. O payload era um crypto-clipper. Os mantenedores perceberam e revertiram em aproximadamente 2 horas, e as versões comprometidas ainda foram baixadas cerca de 2.6 milhões de vezes nesse intervalo.
Chalk é uma das 3 bibliotecas que os modelos continuaram a buscar quando eu lhes pedi um CLI.
A base não foi afetada — nunca dependia de chalk — e quero ser preciso em vez de dramático sobre isso, porque foi criada 2 meses após o incidente. A relevância não é que evitamos algo. É que o incidente descreve exatamente a classe de risco: cada dependência é uma década de decisões de lançamento de outra pessoa, e você está confiando em uma conta que não controla. O tratamento de cores que substituiu uma dependência é sobre 60 linhas. O prompting que substituiu a outra é sobre 120. Zero é o único número que não precisa de monitoramento.
A versão que foi lançada tem 85 KB, não minificada, sem dependências de tempo de execução, rodando em Node, Bun e Deno. Todo comando construído sobre ele obtém, sem código por comando:
Garantia
O que isso significa na prática
Códigos de saída honestos
Um erro reportado a um humano é reportado ao shell
--json e --ndjson
O valor que seu comando retorna, em uma forma que a máquina pode analisar
manifest
A ferramenta inteira descrita em 1 chamada determinística, carregando nada
Disciplina de fluxo
stdout é carga útil; cada linha de log está em stderr
Erros de uso
Saia com 2 para "você me chamou errado", distinto de 1 para "eu tentei e falhei"
Segurança de prompt
Um prompt sem terminal falha em milissegundos em vez de ficar preso para sempre
Cancelamento
Ctrl-C interrompe o sinal do comando, então sai 130
A coisa que continuo voltando é não qualquer item único nessa lista. É que a lista agora é verificável. O próprio exemplo do README roda como um teste contra o tarball publicado, e os números citados em sua prosa são mantidos pelos números que o conjunto produz — uma regra que pegou seu primeiro erro dentro de um minuto de ser escrito, onde a página dizia 87 KB e o artefato era 85.
Quatro anos atrás o problema era que cada modelo tinha uma opinião diferente sobre o que um CLI deveria ser. A resposta nunca foi argumentar com as opiniões. Era possuir a base que todos construíam, e escrever as promessas em algum lugar onde uma construção possa falhar.