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 = 一个仓库管理所有包,兼具单体的集中性和多仓的模块化。

monorepo vs polyrepo 对比

opencode 选择的路线是一条清晰的"三层契约":

  • Bun 作为包管理器(取代 pnpm/yarn/npm)
  • catalog 统一版本锁定(根 package.json 一言九鼎)
  • workspace:* 协议跨包引用(编译期保证版本一致)

【设计】29 个 packages 的分层架构

全景数据

运行仓库附带的 demo/monorepo-stats.sh,可以直接看到全貌:

终端输出:monorepo 统计

关键数据:

  • 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 层,每层只能依赖下层:

29 packages 六层架构图

  层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→L6

L1 基础设施 → L2 扩展/SDK → L3 CLI/服务 → L4 UI → L5 平台 → L6 发布

L1 只依赖第三方库,不依赖任何 workspace 包。L3 可以依赖 L1/L2,但不能依赖 L4。跨层依赖(如 L3 依赖 L4 的 tui)在功能紧密耦合时可以破例,但不鼓励。

真实的依赖关系

从核心包 opencode(L3)的 package.json 中可以看到它的 workspace 依赖:

packages/opencode 的 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"
    }
  }
}

root package.json workspaces 配置

文件路径: 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:" 协议:

catalog 协议 vs 直接版本号

// 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:选择的本质

决策树

monorepo 选型决策树

opencode 选 monorepo 的核心原因是:

29 个包中大部分随 CLI 一起发布,共同演化频率极高。

场景 适合模式 理由
包独立迭代,互不依赖 Polyrepo 灵活发布,独立版本号,互不影响
2–5 个包偶尔同步 Monorepo 轻量 pnpm workspaces + 手动版本管理即可
20+ 个包频繁同步发布 Monorepo 重度 Bun + catalog + workspace:* 三件套
需外部开发者独立消费 独立仓库 + SDK 发布 对外提供稳定 API,不暴露内部包

版本管理对比

Polyrepo vs Monorepo 版本管理对比

Monorepo 的做法:

catalog 一个版本锚点控制所有 29 个包。升级 effect 版本时,只需要改 root package.jsoncatalog.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 linkyalc,改 core → npm publish → opencode npm 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拆代码的曹操」