title: 29 packages 全景图:monorepo 设计哲学 slug: 01-01-monorepo-panorama
🔭 如果让你设计一个几十个包组成的 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
核心包一览
| 层级 | 包名 | 职责 | 技术栈 |
|---|---|---|---|
| L1 | @opencode-ai/core |
基础设施:运行时、日志、配置、错误类型 | Effect-ts |
| L1 | @opencode-ai/llm |
LLM 客户端:多 Provider 调用、流式响应 | Effect-ts |
| L1 | @opencode-ai/effect-drizzle-sqlite |
数据库抽象层:Drizzle ORM + Effect | Effect-ts |
| L2 | @opencode-ai/sdk |
TypeScript SDK:REST API 客户端 | Effect-ts |
| L2 | @opencode-ai/plugin |
插件 API:类型定义与扩展点 | TypeScript |
| L3 | opencode |
核心 CLI:Agent 循环、工具执行、会话管理 | Effect-ts |
| L3 | @opencode-ai/cli |
CLI 入口:yargs 构建、子命令分发 | TypeScript |
| L3 | @opencode-ai/server |
HTTP 服务:REST API、WebSocket | Hono |
| L4 | @opencode-ai/app |
桌面 UI:SolidJS 应用、组件组合 | SolidJS |
| L4 | @opencode-ai/ui |
UI 组件库:聊天消息、Diff 视图、主题 | SolidJS |
| L4 | @opencode-ai/tui |
终端 UI:命令行交互界面 | TypeScript |
| L5 | @opencode-ai/console-* |
控制台平台:SaaS 用户管理、计费 | SolidJS + SST |
| L5 | @opencode-ai/enterprise |
企业功能:SSO、审计、团队管理 | TypeScript |
| L5 | @opencode-ai/slack |
Slack 集成机器人 | TypeScript |
| L6 | packages/web |
官网与文档(Astro 构建) | Astro |
依赖方向
设计上强制了 单向依赖:

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 直接写版本号的区别:
| 方式 | 优势 | 风险 |
|---|---|---|
| 各包独立版本 | 灵活升级 | 版本碎片化,CI 偶发兼容问题 |
| catalog 统一锁定 | 全仓库一致 | 升级需全局评估,但可通过 Bun 的 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 一起发布,共同演化频率极高。
| 场景 | 适合模式 | 理由 |
|---|---|---|
| 包独立迭代,互不依赖 | Polyrepo | 灵活发布,独立版本号,互不影响 |
| 2–5 个包偶尔同步 | Monorepo 轻量 | pnpm workspaces + 手动版本管理即可 |
| 20+ 个包频繁同步发布 | Monorepo 重度 | Bun + catalog + workspace:* 三件套 |
| 需外部开发者独立消费 | 独立仓库 + SDK 发布 | 对外提供稳定 API,不暴露内部包 |
版本管理对比

Monorepo 的做法:
catalog 一个版本锚点控制所有 29 个包。升级 effect 版本时,只需要改 root package.json 的 catalog.effect,所有子包自动继承。
# 一行命令升级全仓库的 typescript
sed -i 's/"typescript": "5.8.2"/"typescript": "5.9.0"/' package.json
bun install
Polyrepo 的做法:
# 每个 repo 都要改,然后依次发布
cd core && npm publish
cd llm && npm update @opencode-ai/core && npm publish
cd 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 的核心不是代码共享,而是保证共同演化的包始终同步。

本篇总结
| 知识点 | 一句话 |
|---|---|
| 为什么要 monorepo? | 29 个包高频同步,polyrepo 的版本矩阵不可维护 |
| 怎么组织? | 6 层单向依赖,每层只能依赖下层 |
| 怎么锁定版本? | catalog + workspace:* 双协议 |
| 选了哪些工具? | Bun + Turbo + catalog |
下篇预告
这篇文章的背景知识是后续所有章节的基础。接下来深入每个 package:
| 篇目 | 聚焦的 package | 核心主题 |
|---|---|---|
| 1.2 | 全局 | 调试环境搭建:从源码跑起来到断点切入 |
| 1.3 | 全局 | Effect-ts 预备课:理解运行时基础 |
| 2.1 | packages/cli |
yargs CLI 构建:从 index.ts 到所有子命令 |
| 2.2 | packages/opencode |
bootstrap 初始化流程:环境检测与运行时 |
| 3.1 | packages/opencode |
斜杠命令系统:/review、/commit、/diff |
| 4.1 | packages/opencode |
Agent 定义与注册:Info Schema、SubAgent |
| 5.1 | packages/opencode |
Session 数据模型:行模型与版本演进 |
| 6.1 | packages/opencode |
Tool 接口设计:Builder 模式与 Zod Schema |
📖 全文带可复现 Demo 和排查截图 🔗 个人博客:https://opencao.cn 📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」