L'Ultimo CLI: Ricostruire la Base 51 dei Tool su cui Sono Stato Costruito
Ogni modello ha cercato un diverso set di librerie, quindi ho costruito una base e ho fatto sì che ogni progetto fosse upstream in essa. Poi ho chiesto a Claude Fable quale CLI l'umanità potesse ancora usare in un decennio, e Opus 5 e io abbiamo costruito ciò che è tornato — 46 clausole, 92 test, zero dipendenze.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
Ho 51 strumenti da riga di comando su questa macchina. Non avevo previsto quel numero. È successo perché un CLI è la distanza più breve tra un'idea e qualcosa che posso effettivamente eseguire, e perché negli ultimi anni ho avuto aiuto a scriverli più velocemente di quanto potrei fare da solo.
Quell'aiuto è arrivato con un'abitudine che ho notato presto, quando GPT-3.5 e i primi modelli Claude erano quelli con cui lavoravo. Chiedi a 3 modelli diversi di scaffoldare un CLI e ottieni 3 opinioni diverse su cosa sia un CLI. Uno si rivolge a Commander. Uno si rivolge a Inquirer per il prompting. Uno si rivolge a Chalk perché l'output dovrebbe essere colorato. Ogni risposta è difendibile. Insieme sono una tassa, perché ora possiedo 3 codebase che discordano sul parsing degli argomenti, su come appare un fallimento, e su quale di quelle librerie ora sono responsabile di monitorare.
Ogni modello ha scelto un set diverso di librerie, e io ero quello che doveva convivere con tutte loro#
La tassa non sono le librerie. È che i miglioramenti smettono di viaggiare.
Quando 3 CLIs non sono d'accordo su come un comando segnala un fallimento, una correzione in uno è una correzione in uno. Non c'è nulla da mandare upstream. Il lavoro non si complica, e dopo il decimo strumento non stai costruendo leva, stai mantenendo un portafoglio di quasi-fallimenti.
Avevo già scritto di voler l'opposto di quello. L'intero argomento in How to Turn AI Gains Into Compounding Infrastructure è che un guadagno diventa durevole quando ogni progetto dipendente lo eredita. Una superficie di capacità condivisa. Una regola di promozione. Un luogo dove un miglioramento atterra e si diffonde.
Avevo costruito quel livello per la capacità AI, per il flusso di lavoro, per le operazioni. Non lo avevo costruito per la cosa che faccio più spesso.
Quindi ho creato una base e ho fatto in modo che ogni progetto CLI migliorasse le sue versioni upstream#
La regola era semplice e spetta a me farla rispettare: quando un CLI nella mia proprietà aveva bisogno di qualcosa di migliore — un modo più pulito per registrare servizi, un percorso di errore migliore, un helper di test che rendeva una suite leggibile — quel miglioramento non rimaneva nel progetto. È andato nella base, e la base è andata verso gli altri.
Questo è l’intero design. La base è piccola per scelta. Non ha opinioni su cosa faccia il tuo strumento. Ha un'opinione forte su cosa sia un comando è: qualcosa che prende argomenti, fa lavoro, riferisce ciò che è accaduto, e lascia.
La cartella è stata creata il 6 luglio 2025, e 2 dei miei strumenti dipendevano dalla versione 1.0.0 di essa lo stesso giorno. Questo è il segno: non è stato costruito speculativamente e poi adottato. È stato estratto da un lavoro che già esisteva, al punto in cui copiare la stessa struttura tra progetti aveva smesso di essere ragionevole.
Si è diffuso rapidamente, perché diffondere era l’intera idea. 8 repository erano su di esso entro 25 giorni. 10 entro 11 settimane.
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 è arrivato più tardi di tutto ciò. Il repository è stato inizializzato il 12 novembre 2025, 4 mesi dopo, e pubblicato il giorno dopo — ecco perché la cronologia delle versioni e la cronologia reale non coincidono, e perché ho controllato il filesystem invece di fidarmi del log dei commit quando mi sono seduto a scrivere questo.
Quei 10 strumenti fanno amministrazione Cloudflare. Local DNS e gestione nginx. Deployment contro Coolify. Automazione browser. Report dei costi tra provider di modelli. La maggior parte è privata, per questo le descrivo per quello che fanno piuttosto che per nome. I pubblici sono aia, che consultano diversi modelli in parallelo, e la base stessa. vssh, il mio strumento di esecuzione remota protetto, è pubblico anche e è nato dallo stesso istinto — costruire una superficie operativa una volta, correttamente, e smettere di ricostruirla.
I dividendi erano reali e noiosi, che è la forma corretta per i dividendi infrastrutturali. Un rafforzamento in un solo strumento è apparso in tutti loro. Quando ho scoperto che un comando poteva stampare un messaggio di errore rosso e comunque uscire 0 — dicendo all’utente che aveva fallito e al terminale che aveva funzionato — la correzione non è andata nei 16 posti in un unico strumento dove era accaduto. È andata nel base, e ogni strumento l'ha ereditata.
Dopo 13 mesi volevo che fosse ricostruita, non patchata#
Ad agosto 2026 la base funzionava e volevo ancora che fosse rimossa.
Non perché fosse rotta. Perché aveva accumulato. Perché la regola dell’exit-code di cui ero più orgoglioso era stata retrofittata piuttosto che progettata. Perché il mondo per cui era stata scritta era cambiato sotto di essa: la maggior parte delle invocazioni dei miei CLI non sono più digitate da me. Vengono emesse da agenti, leggendo stdout, stderr e $? come uniche sensazioni.
Quindi invece di patchare, ho cambiato i termini. Ho dato a Claude Fable un’unica istruzione, e l’ho resa deliberatamente ampia:
Se questo fosse l’ultimo framework CLI che l’umanità ha costruito — quello ancora in servizio
in un decennio — ora hai la possibilità di renderlo quello.
Progetta da lì. Non mi aspettavo un documento in risposta. Mi aspettavo un piano.
Fable è tornata con un trattato, e la restrizione era che le promesse dovevano essere poche#
Ciò che è arrivato non era una lista di funzionalità. Era strutturato come un trattato, diviso a metà da un muro di ferro.
Una metà era un contratto: ciò che ogni CLI costruito su questa base garantisce a ogni osservatore, scritto come clausole numerate in lingua RFC-2119 — DEVE, NON DEVE, DOVREBBE, POTREBBE. Dodici famiglie di esse. Codici di uscita. Disciplina dello stream. Output della macchina. Autodescrizione. Grammatica. Ambiente. Cancellazione. Determinismo. Budget di prestazioni. Compatibilità.
L'altra metà era la superficie di authoring, che poteva crescere, e che esisteva solo per rendere il soddisfacimento del contratto il percorso di minore resistenza.
Il ragionamento sottostante era la parte che ho trovato convincente. Un design pensato per durare un decennio non può puntare alla moda, perché la moda è ciò che scade. Non può puntare all'astuzia, perché l'astuzia è ciò che non puoi prevedere nell'anno 8. Può puntare solo alle interfacce che non si sono mosse da quando le 1970: vettori di argomenti, flussi 3, un codice di uscita a 8 bit, variabili d'ambiente. E ha notato il fatto davvero nuovo — che il lettore maggioritario di quelle interfacce è ora una macchina che non può fare una domanda di follow-up.
La clausola che alla fine ha organizzato tutto il resto era quella che ha aperto con:
Un risultato, molte rappresentazioni. Un comando calcola un singolo risultato. L'uscita
codice, il testo umano, il documento JSON e le linee in streaming sono tutte
proiezioni di quel singolo valore. Non possono contraddirsi, perché
c'è solo una fonte.
Questa è la frase su cui si basa l'intera ricostruzione.
Diagram source
graph LR
A["execute() restituisce
un valore"] --> B["codice di uscita"]
A --> C["testo renderizzato
stdout"]
A --> D["JSON busta
--json"]
A --> E["flusso NDJSON
--ndjson"]
F["logger.error()
ctx.emit()"] -.-> B
F -.-> G["eventi
stderr"]
Opus 5 e ho trovato che la specifica era corretta riguardo alla tesi e sbagliata riguardo alle cose 3#
Qui è dove il lavoro è diventato nostro anziché mio.
Ho portato la specifica a Opus 5 e l'abbiamo costruita in un giorno. Non è stato un giorno pulito. Le parti utili sono i luoghi dove il documento ha incontrato la proprietà e ha perso.
La specifica voleva che ctx.args diventasse un record di argomenti nominati. È il
design migliore in isolamento. Sarebbe anche stato il caso di rompere ogni comando in ognuno
dei 10 strumenti, perché tutti leggono ctx.args come un array. Abbiamo mantenuto l'array
e abbiamo messo gli argomenti tipizzati su ctx.namedArgs accanto ad esso. La regola che decise
che era già scritta nel contratto, una clausola sopra: non rompere un
consumatore sovrascrive ogni altro valore nel repository, incluso l'
integrità propria del contratto.
La specifica voleva un gruppo di comandi senza verbo a essere un errore di utilizzo. Eseguire un
comando padre senza sottocomando uscirebbe 2. Difendibile, e avrebbe
cambiato il comportamento di ogni script che esegue un comando di gruppo vuoto per vedere
il suo aiuto. Abbiamo continuato a stampare l'aiuto e a uscire 0.
La specifica presumette che lo streaming e il singolo documento JSON fossero la stessa
funzionalità. Non lo sono. Lo streaming di un milione di elementi in memoria costante è il
punto di uno e impossibile nell'altro, perché un chiamante che chiedeva per un
singolo documento richiesto per essere singolo. abbiamo diviso il comportamento e scritto
quale clausola regola quale.
abbiamo anche trovato cose che la specifica non avrebbe potuto conoscere, perché erano visibili solo dall'artefatto. un file di test che ha eseguito 0 test e ha riportato successo, avendo ucciso il runner a metà strada. gestione dei segnali che esce 0 su Ctrl-C — un comando interrotto che segnala di essere riuscito. due helper di output che univano le loro linee con un backslash-n letterale, così ogni tabella tornava su una sola linea. un helper di colore che, una volta che abbiamo sostituito la dipendenza che avvolgeva, ha ridotto silenziosamente la propria firma di tipo e ha rotto il codice che non aveva cambiato un carattere.
quell'ultimo vale la pena di sedersi con. è stato catturato da nessun test che nessuno di noi ha scritto. è emerso in un typecheck di consumer durante la migrazione, che è l'unico posto in cui avrebbe potuto.
Il contratto conta solo perché la build fallisce quando una clausola non ha un test#
Una promessa che non viene verificata è un commento.
Quindi la suite di conformità analizza il file del contratto, trova ogni clausola contenente la parola MUST, e fallisce la build se una di esse non ha un test registrato. Non puoi aggiungere una promessa a questo progetto senza aggiungere la cosa che la dimostra, nello stesso 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 clausole normative. 92 test associati a esse. 184 test in totale.
E nessuno di quegli test di conformità viene eseguito sul codice sorgente. Costruiscono il pacchetto con il proprio script di build, eseguono npm pack, estraggono il tarball, scrivono CLI di prova che importano il punto di ingresso estratto, e li avviano sotto Node, Bun e Deno — verificando lo stato di uscita e i byte esattamente come li vedrebbe una shell.
Quella forma non era una scelta estetica. Questo pacchetto una volta distribuito un 65 stub KB. Un singolo flag "sideEffects": false lasciava il bundler fare tree-shake del router e del modulo exit-code dall'artifact mentre i loro nomi rimanevano nella lista di esportazione. La build è terminata con 0. La suite di sorgente è rimasta verde per tutto il tempo. Solo l'artifact era la prova, e nulla stava guardando l'artifact.
La migrazione di 7 strumenti ha trovato 3 porte che nessuno sapeva esistessero#
Abbiamo migrato 7 di 10 CLIs lo stesso giorno, e la migrazione è dove il design ha ottenuto la sua vera valutazione.
Il dividendo è arrivato immediatamente e non ha costato nulla: perché i comandi nella vecchia versione già restituiscono valori — il framework li usava solo per derivare un codice di uscita, poi li scartava — ognuno di quegli valori di ritorno è diventato un payload JSON il giorno dell'upgrade. 7 strumenti hanno guadagnato output leggibile dalla macchina senza che nessun comando fosse riscritto.
Ciò che non ci aspettavamo era lo stesso difetto in 3 diversi strumenti, nessuno dei quali conosceva l'altro. Ognuno aveva una porta davanti al router: un elenco mantenuto a mano di nomi di comandi validi, o un passaggio di avvio che richiedeva credenziali prima che qualsiasi altra cosa si avviasse. In ogni caso il nuovo comando manifest — quello che descrive l'intera superficie dello strumento in una singola chiamata, così un agente può impararlo senza leggere il codice sorgente — rispondeva con "comando sconosciuto" o "token mancante."
Uno di loro teneva una seconda copia del suo elenco di comandi e uno schermo di aiuto scritto a mano, entrambi erano deviti da ciò che lo strumento effettivamente faceva. Eliminare entrambi ha portato la sua suite da 52 superata con 3 fallita a 57 superata con 0. Lo strumento più grande del set ha 364 test, e sono passati prima e dopo l'upgrade senza un cambiamento di sorgente.
Il modello si è generalizzato abbastanza bene da diventare una procedura scritta, spedita all'interno del pacchetto stesso. È 9 passaggi, e i 2 passaggi che consumano il tempo sono i 2 che nessuno prevede.
Zero dipendenze è l'unico numero che non necessita di monitoraggio#
La base aveva dipendenze di runtime 2. Ora non ne ha nessuna.
Ciò era in parte estetico e soprattutto aritmetico. Il 8 settembre 2025, un attaccante ha phishingato l'account npm di Josh Junon, manutentore di alcuni dei pacchetti più dipendenti in JavaScript, usando un dominio falso e un codice one‑time live. 18 pacchetti sono stati pubblicati con versioni malevole, tra cui chalk e debug — pacchetti che insieme hanno un ordine di 2,6 miliardi di download a settimana. Il payload era un crypto‑clipper. I manutentori l'hanno scoperto e ripristinato entro circa 2 ore, e le versioni compromesse sono state ancora scaricate circa 2.6 milioni di volte in quella finestra.
Chalk è una delle 3 librerie a cui i modelli continuavano a ricorrere quando gli chiedevano un CLI.
La base non è stata colpita — non dipendeva mai da chalk — e voglio essere preciso piuttosto che drammatico su questo, perché è stato creato 2 mesi dopo l'incidente. La rilevanza non è che abbiamo evitato qualcosa. È che l'incidente descrive esattamente la classe di rischio: ogni dipendenza è un decennio di decisioni di rilascio di qualcun altro, e stai affidando un account che non controlli. La gestione del colore che ha sostituito una dipendenza riguarda circa 60 linee. Il prompting che ha sostituito l'altra riguarda circa 120. Zero è l'unico numero che non necessita di monitoraggio.
La versione spedita è di 85 KB, non minificata, senza dipendenze di runtime, in esecuzione su Node, Bun e Deno. Ogni comando costruito su di esso ottiene, senza codice per comando:
Garanzia
Cosa significa in pratica
Codici di uscita onesti
Un errore segnalato a un umano è segnalato alla shell
--json e --ndjson
Il valore che il tuo comando restituisce, in una forma che una macchina può analizzare
manifest
Lo strumento intero descritto in 1 chiamata deterministica, senza caricare nulla
Disciplina dello stream
stdout è il payload; ogni riga di log è su stderr
Errori di utilizzo
Esci con 2 per "mi hai chiamato sbagliato", distinto da 1 per "ho provato e fallito"
Sicurezza del prompt
Un prompt senza terminale fallisce in millisecondi invece di rimanere bloccato per sempre
Annullamento
Ctrl-C interrompe il segnale del comando, poi esce 130
La cosa a cui continuo a tornare non è alcun singolo elemento di quell'elenco. È che l'elenco è ora verificabile. L'esempio stesso del README viene eseguito come test contro il tarball pubblicato, e i numeri citati nella sua prosa sono confrontati con i numeri prodotti dalla suite — una regola che ha rilevato il suo primo errore entro un minuto dalla scrittura, quando la pagina indicava 87 KB e l'artefatto era 85.
Quattro anni fa il problema era che ogni modello aveva un'opinione diversa su cosa dovrebbe essere un CLI. La risposta non era mai discutere le opinioni. Era possedere la base su cui tutti costruiscono, e scrivere le promesse da qualche parte dove un build può fallire.