← 返回文章Repositories Adopting the Base in 2025
Conformance Tests by Contract Family
发现
Copy article
最后 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 CLI 对如何报告失败意见不合时,一个地方的修复在另一个地方也是修复。 没有什么可以向上游传递。 工作不会复利,到了第十个工具后,你不是在构建杠杆,而是在维护一系列近乎失败的组合。
我已经写过想要相反的情况。 整个论点在如何将 AI 收益转化为复利基础设施中是:当每个依赖项目继承收益时,收益变得持久。 一个共享的能力表面。 一个推广规则。 一个改进落地并扩散的地方。
我为 AI 能力、工作流、运营构建了那一层。 我并没有为我最常做的事情构建它。
所以我构建了一个基础,并让每个CLI项目将其改进向上游合并
规则很简单,也是我来执行的:当我资产中的一个CLI需要更好的东西——更干净的服务注册方式、更好的错误路径、让测试套件可读的测试助手——那项改进就不会留在项目中。 它进入基础,基础又传播到其他项目。
这就是整个设计。 基础是故意小的。 它对你的工具做什么没有任何偏见。 它对命令是什么有强烈的看法:它是一个接受参数、执行工作、报告结果并结束的东西。
文件夹创建于2025年7月6日,我的工具中有2依赖于版本1.0。0同一天的同一版本。 这就是说明:它不是先建后采纳。 它是从已经存在的工作中提取的,在复制相同脚手架到不同项目已不再合理的点。
它迅速传播,因为传播本身就是整个想法。 8 仓库在25天内就开始使用它。 10 在11周内。
Chart data
| repositories | |
|---|---|
| Jul 6 | 2 |
| Jul 8 | 4 |
| Jul 17 | 5 |
| Jul 23 | 7 |
| Jul 30 | 8 |
| Sep 20 | 10 |
Git 比所有这些都晚。 仓库于2025年11月12日初始化,已运行4个月,第二天发布——这就是为什么版本历史和实际历史不一致,也为什么我检查文件系统而不是信任提交日志来写这篇文章。
那些10工具做 Cloudflare 管理。 本地 DNS 与 nginx 管理。 对 Coolify 的部署。 浏览器自动化。 跨模型提供商的成本报告。 大多数是私有的,这就是我用它们的功能而不是名称来描述它们的原因。 公开的有 aia,它并行查询多个模型,和基础本身。 vssh,我的受保护远程执行工具,也公开发布,源自同样的本能——一次性、正确地构建操作员界面,然后停止重建。
股息是真实的且乏味的,这正是基础设施股息的正确形态。 在一个工具中的硬化在所有工具中都出现了。 当我发现一个命令可以打印红色错误信息并仍然以
0 退出——告诉人类它失败了,告诉 shell 它成功了——修复并未应用到该工具中出现问题的 16 个位置。 它被应用到基础,并且每个工具都继承了它。在 13 个月后,我想让它重新构建,而不是补丁
到 2026 年 8 月,基础已经可以工作,但我仍想把它删掉。
并不是因为它坏了。 而是因为它积累了。 因为我最自豪的退出码规则是被改装而不是从一开始就设计进去的。 因为它所写的世界已经在它下面发生了变化:我大多数 CLI 的调用现在不再由我手动输入。 它们是由代理发出的,读取 stdout、stderr 和
$? 作为它们唯一的感知方式。所以我没有补丁,而是重新设定了条款。 我给 Claude Fable 了一条单一指令,并且故意让它很大:
如果这将是人类建造的最后一个 CLI 框架——十年后仍在使用的那个 你现在有机会让它成为那样。
从那里开始设计。 我并不期待收到文件。 我期待的是一个计划。
Fable 带着一份条约回来,约束是承诺必须少
到来的不是功能列表。 它被结构化为一份条约,中间被一道硬墙分开。
一半是合同:每个 CLI 在此基础上构建的内容对每个观察者所保证的,以 RFC-2119 语言的编号条款形式书写 — 必须, 不得, 应该, 可能。它们有十二个家族。 退出码。 流纪律。 机器输出。 自我描述。 语法。 环境。 取消。 确定性。 性能预算。 兼容性。
另一半是作者表面,允许增长,只存在于使满足合同成为最小阻力路径。
下面的推理是我认为有说服力的部分。 旨在持续十年的设计不能依赖时尚,因为时尚是会过期的。 它也不能依赖聪明,因为聪明是你无法在第 8 年预测的。 它只能依赖自 1970 以来未变的接口:参数向量、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个测试并报告成功的测试文件,在途中杀死了跑者。 信号处理在 Ctrl-C 时退出
0 — 一个中断的命令报告它已成功。 一个颜色助手,一旦我们替换了它包装的依赖,悄悄地缩小了自己的类型签名,并破坏了未更改字符的代码。 那最后一个值得坐下来思考。它没有被我们任何人编写的测试捕获。 它在迁移期间在 消费者 的类型检查中出现,这是它唯一可能出现的地方。 它在迁移期间的 消费者的 类型检查中出现,这是它可能出现的唯一地方。
合同只有在构建失败时才算数,因为某条款没有测试
一个没有任何检查的承诺是注释。
因此,兼容性套件解析合同文件,找到每个包含单词 MUST 的条款,并在其中任何一个没有注册测试时导致构建失败。 你不能在同一次提交中添加承诺而不添加证明它的东西。
Chart 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,编写 fixture CLI,导入解压后的入口点,并在 Node、Bun 和 Deno 下启动它们——按 shell 看到的方式断言退出状态和字节。这种形状不是审美选择。 这个包曾经发布过一个 65 KB stub. 一个单独的
"sideEffects": false 标志让打包器对路由器和 exit-code 模块进行树摇,移除它们的代码,但它们的名字仍保留在导出列表中。 构建以 0 退出。 源套件在整个过程中保持绿色。 只有工件是证据,且没有人关注工件。迁移 7 工具发现 3 关卡,没人知道它们存在
我们在同一天迁移了 7 的 10 CLI,迁移是设计获得实际评分的地方。
股息立即到账且不花钱:因为旧版本中的命令已经返回值——框架仅使用它们来推导退出码,然后丢弃它们——这些返回值中的每一个在升级日都变成了 JSON 负载。 7 工具获得了机器可读输出,而没有重写任何命令。
我们没有预料到的是同样的缺陷出现在 3 不同的工具中,且它们彼此都不知道。 每个工具在路由器前都有一个门:一个手工维护的有效命令名列表,或一个启动步骤,在任何其他操作之前要求凭证。 在每种情况下,新的
manifest 命令——描述工具整个表面的单一调用,使代理可以在不阅读源代码的情况下学习——都回答“未知命令”或“缺少令牌”。其中一个保留了其命令列表的第二份副本和手写的帮助屏幕,两者都与工具实际执行的内容产生了漂移。 删除两者后,它的套件从 52 通过到 3 失败到 57 通过到 0. 集合中最大的工具有 364 测试,它们在升级前后通过,且未更改源代码。
该模式足够通用,以至于成为一项书面程序,直接包含在包内。 它是 9 步骤,耗时的 2 步骤是 2 没有人预料到。
零依赖是唯一不需要监控的数字
基础有 2 运行时依赖。 现在没有了。
这部分是审美,主要是算术。 2025年9月8日,一名攻击者使用伪造域名和一次性实时验证码钓鱼获取了 Josh Junon 的 npm 账户,Josh Junon 是 JavaScript 中最依赖的一些包的维护者。 18 个包发布了恶意版本,包括
chalk 和 debug——这些包每周总下载量约 2.6 亿。 载荷是一个 crypto-clipper. 维护者发现后大约在 2 小时内恢复,并且受损版本在该窗口内仍被下载约 2.6 百万次。Chalk 是我要求它们提供 CLI 时模型反复使用的 3 个库之一。
基础未受影响——它从未依赖 chalk——我想精确而非戏剧化地说明这一点,因为它是在事件发生后 2 个月创建的。 相关性不在于我们躲过了什么。 而在于事件恰好描述了风险类别:每个依赖都是他人发布决策的十年,你正在信任一个你不控制的账户。 替代一个依赖的颜色处理约占 60 行。 替代另一个的提示约占 120. 零是唯一不需要监控的数字。
基础现在返回的内容
已发布的版本为 85 KB,未压缩,没有运行时依赖,运行在 Node、Bun 和 Deno 上。 在其上构建的每个命令都会得到,且不需要每个命令的代码:
| 保证 | 实际意义 |
|---|---|
| 诚实的退出码 | 报告给人的错误会被报告给 shell |
--json 和 --ndjson | 你的命令返回的值,以机器可解析的形状 |
manifest | 整个工具在 1 确定性调用中描述,未加载任何内容 |
| 流纪律 | stdout 是有效载荷;每条日志行在 stderr 上 |
| 用法错误 | 对于“你调用我错误”,退出 2,与 “我尝试失败” 的 1 区分 |
| 提示安全 | 没有终端的提示会在毫秒内失败,而不是永远挂起 |
| 取消 | Ctrl-C 终止命令的信号,然后退出 130 |
我不断回到的不是列表中的任何单一项目。 之所以重要的是列表现在是可检查的. README 自己的示例作为对已发布 tarball 的测试运行,文中引用的数字与套件产生的数字保持一致——这一规则在编写后一分钟内就发现了第一个错误,页面显示 87 KB,而工件为 85.
基础代码在 github.com/light-merlin-dark/merlin-cli 开源,合约是仓库中的文件,而不是网站上的声明。
四年前的问题是每个模型对 CLI 的定义都不同。 解决方案从未是与这些意见争论。 而是拥有它们共同构建的基础,并将承诺写在某处,以便构建失败时可追溯。
#AI#developer-tools#cli#architecture#open-source#testing#supply-chain#experiential#insights