Последний CLI: Восстановление Базы 51, На Которой Были Построены Мои Инструменты
Каждый модель стремилась к разному набору библиотек, поэтому я создал одну базу и сделал каждый проект надстройкой над ней. Затем я спросил Claude Fable, какой CLI человечество всё ещё может использовать через десятилетие, и Opus 5 и я создали то, что вернулось — 46 пунктов, 92 теста, нулевые зависимости.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
У меня есть 51 командные инструменты на этой машине. Я не планировал это число. Это произошло, потому что CLI — это кратчайшее расстояние между идеей и тем, что я могу действительно запустить, и потому что в последние несколько лет у меня была помощь в их написании быстрее, чем я мог бы сделать в одиночку.
Эта помощь пришла с привычкой, которую я заметил рано, когда GPT-3.5 и первые модели Claude были теми, с которыми я работал. Попросите 3 разных модели построить CLI и вы получите 3 разных мнения о том, что такое CLI. Один выбирает Commander. Один выбирает Inquirer для подсказок. Один выбирает Chalk, потому что вывод должен быть окрашен. Каждый ответ обоснован. Вместе они образуют налог, потому что теперь я владею 3 кодовыми базами, которые не согласуются по разбору аргументов, по тому, как выглядит ошибка, и по тому, за какие из этих библиотек я теперь отвечаю.
Каждая модель выбрала другой набор библиотек, и я был тем, кто должен был жить со всеми ними#
Налог не в библиотеках. Это то, что улучшения перестают путешествовать.
Когда 3 CLIs расходятся в том, как команда сообщает об ошибке, исправление в одном — это исправление в одном. Нет ничего, куда можно было бы отправить это наверх. Работа не накапливается, и после десятой утилиты вы не создаете рычаг, вы поддерживаете портфель почти неудач.
Я уже писал о желании противоположного того. Весь аргумент в Как превратить выгоды ИИ в сложную инфраструктуру состоит в том, что выгода становится устойчивой, когда каждый зависимый проект наследует её. Общая поверхность возможностей. Правило продвижения. Одно место, где улучшение размещается и распространяется.
Я построил этот слой для возможностей ИИ, для рабочего процесса, для операций. Я не строил его для того, что я действительно делаю чаще всего.
Поэтому я создал одну базу и сделал каждый CLI проект, чтобы направлять его улучшения в неё#
Правило было простым, и я был обязан его применять: когда CLI в моём имуществе нуждался в чем-то лучшем — более чистом способе регистрации сервисов, лучшем пути ошибок, вспомогательном тестировании, которое делало набор читаемым — это улучшение не оставалось в проекте. Оно попало в базу, и база вышла к другим.
Это вся конструкция. База маленькая по назначению. У неё нет мнения о том, что делает ваш инструмент. У неё сильное мнение о том, что команда это: что-то, что принимает аргументы, выполняет работу, сообщает, что произошло, и выходит.
Папка была создана 6 июля 2025, и 2 моих инструментов зависели от версии 1.0.0 той же самой день. Это показатель: она не была построена спекулятивно, а затем принята. Она была извлечена из работы, которая уже существовала, в момент, когда копирование той же структуры между проектами перестало быть разумным.
Она быстро распространилась, потому что распространение было всей идеей. 8 репозитории были на ней в течение 25 дней. 10 в течение 11 недель.
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 пришёл позже всего этого. Репозиторий был инициализирован 12 ноября 2025, 4 месяцев спустя, и опубликован на следующий день — вот почему история версий и фактическая история расходятся, и почему я проверил файловую систему, а не доверял журналу коммитов, когда сел писать это.
Эти 10 инструменты делают администрирование Cloudflare. Локальное управление DNS и nginx. Развертывание против Coolify. Автоматизация браузера. Отчётность затрат по поставщикам моделей. Большинство из них приватные, поэтому я описываю их по тому, что делают, а не по имени. Публичные — aia, которые консультируют несколько моделей параллельно, и сам базовый слой. vssh, мой защищённый инструмент удалённого выполнения, тоже публичный и возник из того же инстинкта — создать поверхность оператора один раз, надёжно, и прекратить её перестраивать.
Дивиденды были реальными и скучными, что является правильной формой дивидендов инфраструктуры. Укрепление в одном инструменте проявилось во всех них. Когда я обнаружил, что команда может вывести красное сообщение об ошибке и всё равно выйти 0 — сообщая человеку об ошибке и оболочке о работе — исправление не попало в 16 мест в одном инструменте, где это происходило. Оно попало в базу, и каждый инструмент унаследовал его.
Через 13 месяцев я захотел, чтобы её пересобрали, а не патчили#
К августу 2026 года база работала, и я всё ещё хотел, чтобы она исчезла.
Не потому, что она сломана. Потому что она накопила. Потому что правило exit‑code, которым я был самым гордым, было переоборудовано, а не спроектировано с самого начала. Потому что мир, для которого она была написана, изменился под ней: большинство вызовов моих CLI больше не вводятся мной. Они выдаются агентами, читающими stdout, stderr и $? как их единственные чувства.
Поэтому вместо патча я изменил условия. Я дал Claude Fable одно указание, и сделал его намеренно большим:
Если бы это был последний CLI фреймворк, который человечество построило — тот, который всё ещё в эксплуатации
через десятилетие — у вас теперь есть шанс сделать его таким.
Спроектируйте его оттуда. Я не ожидал получить документ обратно. Я ожидал план.
Fable вернулся с договором, и ограничением было то, что обещания должны быть скудными#
Что пришло, не был список функций. Это было структурировано как договор, разделённый посередине твердой стеной.
Одна половина была контрактом: что каждый CLI построенный на этой основе гарантирует каждому наблюдателю, записано как нумерованные пункты в языке RFC-2119 — ДОЛЖНО, НЕ ДОЛЖНО, ДОЛЖНО, МОЖЕТ. двенадцать семейств. Коды завершения. Дисциплина потока. Вывод машины. Self‑description. Grammar. Environment. Cancellation. Determinism. Бюджеты производительности. Compatibility.
Оставшаяся половина была поверхностью авторства, которая могла расти, и существовала только для того, чтобы удовлетворение контракта было путем с наименьшим сопротивлением.
Подобное рассуждение было тем, что я нашёл убедительным. Дизайн, рассчитанный на десятилетие, не может полагаться на моду, потому что мода — это то, что истекает. Он не может полагаться на сообразительность, потому что сообразительность — это то, что нельзя предсказать в год 8. Он может полагаться только на интерфейсы, которые не менялись с тех пор, как 1970s: аргументные векторы, 3 потоки, код выхода из 8 бит, переменные окружения. И он отметил одну действительно новую факт — что большинство читателей этих интерфейсов теперь машина, которая не может задать последующий вопрос.
Клауза, которая в итоге организовала всё остальное, была той, которую он открыл:
Один результат, множество отображений. Команда вычисляет один результат. Код выхода
код, человеческий текст, документ JSON и стриминговые строки — все
проекции того одного значения. Они не могут противоречить друг другу, потому что
есть только один источник.
Это предложение, на которое опирается вся перестройка.
Diagram source
graph LR
A["execute() возвращает
одно значение"] --> B["код выхода"]
A --> C["отрендеренный текст
stdout"]
A --> D["JSON конверт
--json"]
A --> E["NDJSON поток
--ndjson"]
F["logger.error()
ctx.emit()"] -.-> B
F -.-> G["события
stderr"]
Opus 5 и я обнаружил, что спецификация была верна в отношении тезиса и неверна в отношении 3 вещей#
Здесь работа стала нашей, а не моей.
Я привел спецификацию к Opus 5 и мы построили её за день. Не был чистый день. Полезные части — это места, где документ встретился с имуществом и потерял.
Спецификация хотела, чтобы ctx.args стал записью именованных аргументов. Это
Это также сломало бы каждую команду во всех десяти инструментах, потому что они все читают ctx.args как массив. Мы сохранили массив
из 10 инструментов, потому что они все читают ctx.args как массив. Правило, которое решило
и поместите введённые аргументы рядом с ctx.namedArgs. Правило, которое определило
это уже было написано в контракте, один пункт выше: никогда не нарушать
потребитель превосходит все остальные значения в репозитории, включая собственную полноту контракта
собственную полноту.
Спецификация требовала, чтобы группа команд без глагола считалась ошибкой использования. Запуск
родительской команды без подкоманды завершал работу 2. Защищаемо, и это
изменило бы поведение каждого скрипта, который запускает простую команду группировки, чтобы увидеть
её справку. Мы продолжали печатать справку и выходили 0.
Спецификация предполагала, что поток и один JSON документ — это одна и та же
функция. Они не так. Поток миллиона элементов в постоянной памяти — это
точка одного и невозможность в другом, потому что вызывающий, который попросил
Мы разделили поведение и записали. Мы разделили поведение и записали
Мы также нашли вещи, о которых спецификация не могла знать, потому что они были видны только из артефакта.
Мы также обнаружили вещи, о которых спецификация не могла знать, потому что они были видны только из артефакта. Тестовый файл, который выполнил 0 тестов и сообщил об успехе, убив runner в середине пути. Обработка сигналов, которая завершилась 0 при Ctrl-C — прерванная команда, сообщающая, что она завершилась успешно. Вспомогательный цвет, который, как только мы заменили зависимость, которую он обертывал, тихо сузил свою собственную сигнатуру типа и сломал код, который не изменил ни одного символа. Последний из них стоит того, чтобы с ним посидеть.
Он не был пойман ни одним тестом, который мы написали. Он проявился в проверке типов потребителя во время миграции, что было единственным местом, где он мог появиться. Он появился в потребителя проверке типов во время миграции, что является единственным местом, где он мог.
Контракт считается только потому, что сборка не проходит, когда в пункте нет теста#
Обещание, которое ничего не проверяет, является комментарием.
Поэтому набор совместимости парсит файл контракта, находит каждый пункт, содержащий слово MUST, и неудачно завершает сборку, если у одного из них нет зарегистрированного теста. Вы не можете добавить обещание в этот проект, не добавив вещь, которая его подтверждает, в том же коммите.
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 нормативные пункты. 92 тесты, сопоставленные с ними. 184 тесты в общей сложности.
И ни один из этих тестов совместимости не запускается против исходного кода. Они собирают пакет со своим собственным скриптом сборки, запускают npm pack, распаковывают tarball, пишут фикстурные CLI, которые импортируют распакованную точку входа, и запускают их под Node, Bun и Deno — проверяя статус выхода и байты точно так, как это увидит shell.
Эта форма не была эстетическим выбором. Этот пакет когда‑то поставил 65 KB stub. Один флаг "sideEffects": false позволил сборщику tree‑shake‑ить роутер и модуль exit‑code из артефакта, пока их имена оставались в списке экспортов. Сборка завершилась 0. Исходный набор оставался зеленым всё время. Только артефакт был доказательством, и никто не смотрел на артефакт.
Перенос 7 инструментов обнаружил 3 ворота, о которых никто не знал#
Мы перенесли 7 из 10 CLI в тот же день, и миграция стала тем местом, где дизайн получил свой реальный балл.
Дивиденд сразу же появился и не стоил ничего: потому что команды в старой версии уже возвращали значения — фреймворк использовал их только для вывода кода выхода, а затем отбрасывал их — каждое из этих возвращаемых значений стало JSON полезной нагрузкой в день обновления. 7 инструменты получили машинно-читабельный вывод без переписывания ни одной команды.
Что мы не ожидали, так это тот же дефект в 3 разных инструментах, ни один из которых не знал о друге. У каждого был ворота перед маршрутизатором: вручную поддерживаемый список допустимых названий команд или шаг запуска, требующий учетные данные до того, как что-либо другое запустится. В каждом случае новая manifest команда — та, которая описывает всю поверхность инструмента в одном вызове, чтобы агент мог изучить её без чтения исходного кода — отвечала «неизвестная команда» или «отсутствует токен».
Одна из них хранила вторую копию своего списка команд и вручную написанный экран справки, оба из которых отстали от того, что инструмент действительно делал. Удаление обоих привело к тому, что его набор перешёл от 52 прохождения с 3 неудачей к 57 прохождению с 0. Самый большой инструмент в наборе имеет 364 тесты, и они прошли до и после обновления без изменения исходного кода.
Паттерн обобщился достаточно хорошо, чтобы стать письменной процедурой, поставляемой внутри самого пакета. Это 9 шаги, и шаги 2, которые потребляют время, являются 2 тем, чего никто не предвидит.
Ноль зависимостей — это единственное число, которое не требует мониторинга#
База имела 2 зависимости времени выполнения. Теперь у неё их нет.
Это было частично эстетическим и в основном арифметическим. 8 сентября 2025 года злоумышленник фишил npm‑аккаунт Джоша Джунона, поддерживающего некоторые из самых зависимых пакетов в JavaScript, используя поддельный домен и живой одноразовый код. 18 пакетов были опубликованы с вредоносными версиями, включая chalk и debug — пакеты, совокупно скачиваемые примерно 2,6 миллиарда раз в неделю. Полезная нагрузка была крипто‑клиппером. Поддерживающие обнаружили это и откатили в течение примерно 2 часов, а компрометированные версии всё ещё скачивались около 2.6 миллионов раз в этом окне.
Chalk — одна из 3 библиотек, к которым модели продолжали обращаться, когда я просил их о CLI.
База не пострадала — она никогда не зависела от chalk — и я хочу быть точным, а не драматичным в этом, потому что она была создана 2 месяцев после инцидента. Важность не в том, что мы избежали чего‑то. Это то, что инцидент описывает класс риска точно: каждая зависимость — это десятилетие чужих решений о выпуске, и вы доверяете аккаунту, которым не управляете. Обработка цвета, которая заменила одну зависимость, составляет примерно 60 строк. Подсказка, которая заменила другую, составляет примерно 120. Ноль — это единственное число, которое не требует мониторинга.
Версия, поставленная в комплекте, имеет размер 85 КБ, не минифицирована, без зависимостей во время выполнения, работает на Node, Bun и Deno. Каждая команда, построенная на ней, получает, без кода для каждой команды:
Гарантия
Что это означает на практике
Честные коды выхода
Ошибка, сообщаемая человеку, передаётся в оболочку
--json и --ndjson
Значение, которое возвращает ваша команда, в форме, которую машина может разобрать
manifest
Весь инструмент описан в 1 детерминированном вызове, не загружая ничего
Дисциплина потока
stdout — полезная нагрузка; каждая строка журнала выводится в stderr
Ошибки использования
Выход 2 для «вы вызвали меня неправильно», отличающийся от 1 для «я попытался и не смог»
Безопасность подсказки
Подсказка без терминала завершается за миллисекунды, а не висит бесконечно
Отмена
Ctrl-C прерывает сигнал команды, затем выходит 130
То, к чему я постоянно возвращаюсь, не является ни одним пунктом из этого списка. Это то, что список теперь проверяемый. Пример из README выполняется как тест против опубликованного tarball, и числа, упомянутые в его тексте, соответствуют числам, которые генерирует набор — правило, которое выявило первую ошибку в течение минуты после написания, когда страница указывала 87 KB, а артефакт был 85.
Четыре года назад проблема заключалась в том, что каждая модель имела своё мнение о том, что должно быть CLI. Ответ никогда не был в споре с мнениями. Это было владение базой, на которой они все строятся, и запись обещаний где‑то, чтобы сборка могла не пройти.