🔭 如果让你设计一个几十个包组成的 CLI 工具,你会怎么组织代码仓库?
一个直觉是把所有代码写在一个大包里——简单粗暴,但随着功能增加,构建越来越慢,依赖越来越乱,谁都不敢改公共模块。
另一个直觉是每个功能独立仓库——解耦彻底,但版本同步变成噩梦:A 包改了接口,B 包没升级,CI 一片红。
opencode 的作者选了第三条路——monorepo,但不是一个简单的 monorepo,而是一套精心设计的 29 个 packages 的分层体系,用 Bun 的 workspace 协议把每个包的版本牢牢锁在一起。
这篇文章带你从头看到尾,理解这套设计背后的权衡。
【问题】为什么需要 monorepo?
单体应用的困境
一个生产级 AI Coding Agent 涉及的能力很广:
CLI 入口、HTTP 服务、TUI 界面 LLM 调用、工具执行、会话管理 权限控制、配置管理、插件系统 桌面应用、Web 前端、VS Code 扩展 SaaS 控制台、企业功能、Slack 集成
如果所有代码塞进一个包里,随着功能增长必然面临:
❌ 构建慢:改一行代码要等全量编译❌ 依赖乱:全局 package.json 膨胀到 200+ 依赖❌ 不敢重构:公共模块改了,不知道影响了谁❌ 发布粒度粗:改个小功能也要发整个版本多仓库的困境
拆成独立仓库能解决单体的局部性问题,但引入了新的系统性风险:
❌ 版本同步:A 包 v2.1 需要 B 包 v1.8,手工维护版本矩阵❌ 原子发布难:改一个跨包 feature,要依次发布 N 个包❌ 开发体验差:改一个包要切 repo、装依赖、跑测试、提 PR❌ CI 重复:每个 repo 都要配置一模一样的 CI,出问题 N 倍排查成本monorepo 的解
monorepo = 一个仓库管理所有包,兼具单体的集中性和多仓的模块化。

opencode 选择的路线是一条清晰的"三层契约":
Bun 作为包管理器(取代 pnpm/yarn/npm) catalog 统一版本锁定(根 package.json 一言九鼎) workspace:*协议跨包引用(编译期保证版本一致)
【设计】29 个 packages 的分层架构
全景数据
运行仓库附带的 demo/monorepo-stats.sh,可以直接看到全貌:

关键数据:
29 个 workspace packages(不含 VS Code 扩展,其中 25 个来自顶层目录、4 个来自 console/stats 嵌套子包) 包管理器: Bun 1.3.14 版本锚点: effect 4.0.0-beta.74、typescript 5.8.2、zod 4.1.8、ai 6.0.168 依赖锁定: 所有子包通过 "catalog:"协议引用同一个版本
六层架构
这 29 个包按职责分为 6 层,每层只能依赖下层:

层6: 发布与文档 containers/*, web, storybook ↑ 层5: 平台 SaaS console-*, enterprise, slack, stats-* ↑ 层4: UI 与应用 app, ui, desktop, tui ↑ 层3: CLI 与服务 opencode, cli, server ↑ 层2: 扩展与 SDK sdk, plugin, script, vscode ↑ 层1: 基础设施 core, llm, function, effect-drizzle-sqlite, http-recorder核心包一览
@opencode-ai/core | |||
@opencode-ai/llm | |||
@opencode-ai/effect-drizzle-sqlite | |||
@opencode-ai/sdk | |||
@opencode-ai/plugin | |||
opencode | |||
@opencode-ai/cli | |||
@opencode-ai/server | |||
@opencode-ai/app | |||
@opencode-ai/ui | |||
@opencode-ai/tui | |||
@opencode-ai/console-* | |||
@opencode-ai/enterprise | |||
@opencode-ai/slack | |||
packages/web |
依赖方向
设计上强制了 单向依赖:

L1 基础设施 → L2 扩展/SDK → L3 CLI/服务 → L4 UI → L5 平台 → L6 发布L1 只依赖第三方库,不依赖任何 workspace 包。L3 可以依赖 L1/L2,但不能依赖 L4。跨层依赖(如 L3 依赖 L4 的 tui)在功能紧密耦合时可以破例,但不鼓励。
真实的依赖关系
从核心包 opencode(L3)的 package.json 中可以看到它的 workspace 依赖:

opencode 依赖于: @opencode-ai/llm ← L1:LLM 调用 @opencode-ai/plugin ← L2:插件 API @opencode-ai/script ← L2:脚本工具 @opencode-ai/sdk ← L2:SDK 客户端 @opencode-ai/server ← L3:HTTP 服务(同层) @opencode-ai/tui ← L4:终端界面(⚠️ 跨层例外)注意到 tui 是 L4 层,但 opencode(L3)直接依赖它——说明单向依赖不是死规定,当功能紧密耦合时可以合理破例。设计原则在真实工程中允许例外。
下图展示了跨包之间的核心依赖链路:

分层收益
基础层稳定:core、llm 等底层包的变更不会波及所有上层 并行开发:UI 团队改 app,CLI 团队改 opencode,互不阻塞 可测试性:任意层都可以 mock 下层进行独立单元测试
【源码】root package.json — monorepo 的指挥中心
workspaces 声明
monorepo 的核心配置在文件 package.json(仓库根目录)的 workspaces 字段中:
{"workspaces":{"packages":["packages/*","packages/console/*","packages/stats/*","packages/sdk/js","packages/slack"],"catalog":{"effect":"4.0.0-beta.74","typescript":"5.8.2","zod":"4.1.8","ai":"6.0.168","solid-js":"1.9.10","drizzle-orm":"1.0.0-rc.2","hono":"4.10.7","shiki":"3.20.0"}}}
文件路径:package.json — 第 24–92 行
机制一:workspaces.packages — 包发现
packages/* 匹配顶层所有目录,packages/console/* 深入嵌套的 console 子包,packages/sdk/js 精确指定单个包。组合使用让目录结构兼顾规则性和灵活性。
Bun 在 bun install 时会扫描这些目录,自动建立内部链接——不需要 npm link、不需要 yalc、不需要手工配置。
机制二:catalog — 版本锚点
Bun 的 catalog 机制(类似 pnpm 的 pnpm-workspace.yaml)将关键依赖的版本统一在根 package.json 中定义。每个子包引用时用 "catalog:" 协议:

// packages/opencode/package.json{"dependencies":{"effect":"catalog:","typescript":"catalog:"}}当一个版本定义、全仓库生效——避免了 A 包用 zod 4.1.0、B 包用 zod 4.1.8 的版本碎片化。
catalog vs 直接写版本号的区别:
bun update 逐步推进 |
机制三:workspace:* — 跨包依赖
包之间引用使用 workspace:* 协议:
// packages/opencode/package.json{"dependencies":{"@opencode-ai/llm":"workspace:*","@opencode-ai/plugin":"workspace:*","@opencode-ai/script":"workspace:*","@opencode-ai/sdk":"workspace:*","@opencode-ai/server":"workspace:*","@opencode-ai/tui":"workspace:*"}}* 表示"当前 workspace 的最新版本",Bun 在安装时自动链接成本地文件系统链接。发布时,bun publish 自动将 workspace:* 替换为实际版本号——开发者无需手动维护版本映射表。
【权衡】Monorepo vs Polyrepo:选择的本质
决策树

opencode 选 monorepo 的核心原因是:
29 个包中大部分随 CLI 一起发布,共同演化频率极高。
| 20+ 个包频繁同步发布 | Monorepo 重度 | Bun + catalog + workspace: 三件套 |
版本管理对比

Monorepo 的做法:
catalog 一个版本锚点控制所有 29 个包。升级 effect 版本时,只需要改 root package.json 的 catalog.effect,所有子包自动继承。
# 一行命令升级全仓库的 typescriptsed -i 's/"typescript": "5.8.2"/"typescript": "5.9.0"/' package.jsonbun installPolyrepo 的做法:
# 每个 repo 都要改,然后依次发布cd core && npm publishcd llm && npm update @opencode-ai/core && npm publishcd opencode && npm update @opencode-ai/llm && npm publish# ... 10 个仓库重复同样操作构建编排对比:
Monorepo:Bun 原生 workspaces + Turbo 增量构建,只编译变更的包 Polyrepo:每个 repo 独立 CI/CD,配置重复但互不干扰
开发体验对比:
Monorepo: bun run --cwd packages/opencode dev,同一仓库内改 core → opencode 即时生效Polyrepo:每个包本地 npm link或yalc,改 core →npm publish→ opencodenpm update
差异的本质
共同演化的频率决定选型。
如果 29 个包每个月才同步一次,polyrepo 更合适——各发各的版本,偶尔发个升级 PR 更新依赖。
但 opencode 的 29 个包中有 20+ 个随 CLI 一起发布,每次 Release 都要同步。monorepo 的 catalog + workspace 协议让这种高频同步从"手工操作"变成了"Bun 自动处理"。
【锚点】
Monorepo 的核心不是代码共享,而是保证共同演化的包始终同步。

本篇总结
workspace:* 双协议 | |
下篇预告
这篇文章的背景知识是后续所有章节的基础。接下来深入每个 package:
packages/cli | ||
packages/opencode | ||
packages/opencode | /review、/commit、/diff | |
packages/opencode | ||
packages/opencode | ||
packages/opencode |
夜雨聆风