Files
AgentCompany/docs/proposals/M5-monorepo-split.md
T

280 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Proposal M5: landing/ 与 desktop app 拆分(pnpm workspace 或独立仓)
> 来源:问题.md M5(行 248-252)、附录 F 第 8 条(行 541-542)
> 状态:proposed
> 提出:Roundtable 2026-04-20(Linus + Karpathy + Steve Jobs)
> Owner: TBD
> 估计工时:1 周(workspace 方案)或 2 周(独立仓方案)
> Blocking dependencies:无
## 问题陈述
`landing/` 目录是独立的 marketing site(Next.js + Cloudflare Functions),自带:
- 独立 `package.json`
- 独立 `node_modules`
- 独立 build 流程
- 独立 deploy(Cloudflare Pages)
- 独立的 SQL migration(`landing/migrations/`)
和 desktop app(Electron + React)混在同一个 git repo 里。`scripts/generate-landing-release-links.mjs` 做跨项目协调,暗示两个产品要**手动同步**某些 artifact。
问题表现:
1. **贡献者 onboarding 费劲**:clone 后不知道要在根目录还是 landing/ 跑 pnpm install;CI 要跑哪个
2. **release 耦合**:desktop app 发版和 landing 发版必须同时推到同一个 commit——其中一个有问题就拖另一个
3. **依赖污染**:根 `node_modules` 和 `landing/node_modules` 可能安装同一个包的不同版本
4. **CI 资源浪费**:改一个 landing 文件触发整个 desktop 的 typecheck/test
5. **安全面混淆**:landing 的 Cloudflare token、数据库 URL 和 desktop 的签名证书放在同一个仓库,权限边界模糊
## 根因分析
Linus 视角:**一个 repo 两个产品 = release 耦合**。
Monorepo 本身不是错——Google / Facebook 用 monorepo。但 monorepo 需要**工具层面的约束**:
- 统一 task runner(nx / turborepo / bazel)
- 明确的 workspace 划分
- 共享依赖 hoisting 策略
- CI 按 changed-files 触发
当前项目有 pnpm 但没 workspace 配置,landing 实际上就是"目录塞在这里的另一个项目",没有 monorepo 的任何好处——**只有成本**。
Karpathy 视角补充:AI coding agent 在这种结构下很容易在 desktop 任务里改到 landing 文件,或反之。隔离不清楚的 repo 让 agent 的 blast radius 变大。
## 设计方案
### Option A:pnpm workspace(推荐,低成本)
**目标结构**:
```
AgentCompany/
├── package.json # root (workspace root, 无 runtime 代码)
├── pnpm-workspace.yaml # 新增
├── packages/
│ ├── desktop/ # 当前的根内容移到这里
│ │ ├── package.json
│ │ ├── src/
│ │ ├── tests/
│ │ └── ...
│ └── landing/ # 当前的 landing/ 移到这里
│ ├── package.json
│ ├── app/
│ └── ...
└── docs/
```
`pnpm-workspace.yaml`:
```yaml
packages:
- "packages/*"
```
**Root package.json**(变简):
```json
{
"name": "agentcompany-root",
"private": true,
"scripts": {
"desktop:dev": "pnpm --filter desktop dev",
"desktop:build": "pnpm --filter desktop build",
"landing:dev": "pnpm --filter landing dev",
"landing:build": "pnpm --filter landing build",
"typecheck": "pnpm -r typecheck",
"lint": "pnpm -r lint"
}
}
```
**CI 按 package 分跑**:desktop PR 只跑 desktop typecheck/test,landing PR 只跑 landing。
**好处**:
- 两个项目独立 `package.json`、独立版本
- 根目录干净
- 共享开发工具(eslint / tsconfig base)可提到 root
- pnpm 自动 hoist 共享依赖,磁盘占用下降
**成本**:
- git history 移动(git mv 保留 blame)
- CI 脚本改写(`scripts/` 要调整路径引用)
- 发布流程(electron-builder / cloudflare deploy)适配新路径
### Option B:独立仓(彻底隔离)
把 landing/ 推到独立 repo(例如 `github.com/badlogic/agentcompany-landing`),现 repo 只保留 desktop。
**好处**:
- 完全隔离 CI / secrets / 权限
- landing 团队可独立迭代不看 desktop 代码
- desktop 的 code review 范围缩小
**成本**:
- 跨 repo 协调(release link 生成脚本要走 GitHub API)
- Onboarding 要 clone 两个 repo
- git history 完整迁移工作量
### 推荐 A
- 当前团队规模不需要完全隔离
- landing 改动相对少,一个 repo 能保持 cross-cutting 改动(例如品牌一致性)的原子性
- workspace 成本最低,收益立刻兑现
## Migration 计划(Option A)
1. **Phase 1(1 天)**:新建 `packages/desktop/` 空目录 + `packages/landing/`;`pnpm-workspace.yaml`
2. **Phase 2(1 天)**:`git mv` 把当前根代码移到 `packages/desktop/`,landing/ 移到 `packages/landing/`
- 根 `package.json` 拆成 root + desktop
- 根配置(`tsconfig.base.json` / `eslint.config.mjs`)保留 root
3. **Phase 3(1 天)**:修 `scripts/*` 里的硬编码路径(`scripts/release-*.mjs`、`scripts/build-*.mjs`)
4. **Phase 4(1 天)**:CI 配置分包跑
5. **Phase 5(1 天)**:文档同步——README 更新;`CLAUDE.md` / `AGENTS.md` 说明 workspace 结构
6. **Phase 6(持续)**:观察 2 周,处理零星问题
## 风险与回滚
| 风险 | 触发条件 | 回滚方法 |
|------|---------|---------|
| git history 丢失 | git mv 不规范 | 用 `git mv` 保留 rename detection;必要时 `git log --follow` 验证 |
| electron-builder 找不到 artifact | 打包路径变化 | Phase 3 脚本改完后跑一次完整 build 验证 |
| Cloudflare deploy 路径改 | landing build output 位置变 | wrangler.toml 更新;deploy 前本地 dry-run |
| renderer import path 断 | 代码中的硬路径 | codemod 扫 `require("../../landing")` 类的跨 package 引用(理论上没有) |
**回滚路径**:phase 2 是单个 commit(git mv),任何后续阶段出问题直接 revert。
## 验证标准
1. **独立 build 成功**:`pnpm --filter desktop build` 和 `pnpm --filter landing build` 分别跑通
2. **依赖完整**:`pnpm install` 根目录跑完 `pnpm -r typecheck` 全绿
3. **CI 提速**:改 landing 文件的 PR 不触发 desktop test
4. **Release 脚本**:`pnpm run release:local` / `pnpm release:boss` / `pnpm release:cloudflare:public` 都能跑
5. **Git blame 保留**:`git log --follow packages/desktop/src/main/service-graph.ts` 能追到原始 commit
## 不在范围
- **不引入 turborepo / nx**:pnpm workspace 已足够,加 task runner 是过度工程
- **不改代码模块边界**:desktop 内部代码结构本 proposal 不动
- **不做跨 package 共享代码**:landing 目前不需要 desktop 的类型;如果未来需要再加 `packages/shared/`
- **第三方 runtime reference snapshot 边界**:reference snapshot 已迁出本仓库,monorepo split 保持这个边界
- **不改 git branching strategy**:单仓单分支 vs 多分支不是本 proposal 问题
## 参考文件清单
- `landing/package.json`(独立 package 定义)
- `landing/migrations/`(独立 SQL migration)
- `scripts/generate-landing-release-links.mjs`(跨项目脚本)
- `scripts/release-public-cloudflare.mjs`(landing 发布)
- `scripts/release-boss.mjs`(desktop 发布)
- 根 `package.json:1-86`(会被拆分)
- 根 `pnpm-lock.yaml`(workspace 化后会更长但更清晰)
## 附:为什么现在修
**延迟成本**:landing 每加一个 dependency 就让根 `node_modules` 更大;desktop 的 CI 时间持续上升。现在拆 1 天,拖到 landing 翻倍大小就是 3 天+。
对 AI coding agent 来说,workspace 化后"这个改动属于哪个 package"一目了然——agent 不再会在 desktop 任务里 accidentally 修 landing 文件。这是隔离 blast radius 的最低成本方式。
次要触发:M5 和 M4(scripts 目录混乱)相关——scripts 里的 `release-public-cloudflare.mjs` / `release-boss.mjs` / `release-local.mjs` 混在一起部分就是因为 desktop/landing 不分。拆了 workspace,scripts 也可以按 package 分。
## 附:gating matrix
| Gate | 条件 | 放行判断人 |
|------|------|----------|
| G1 | `pnpm-workspace.yaml` 存在,`pnpm install` 在根目录一次跑通 | Owner |
| G2 | `pnpm --filter desktop build` 产出可签名的 `.app` bundle | Owner |
| G3 | `pnpm --filter landing build` 产出可部署的 Cloudflare Pages 资源 | Owner |
| G4 | CI 按 changed files 触发相应 package 测试 | Owner |
| G5 | git log --follow 在两个 package 内都能追到原始 commit | Linus |
## 附:迁移后的目录结构草图
```
AgentCompany/
├── package.json # workspace root
├── pnpm-workspace.yaml
├── pnpm-lock.yaml # hoisted
├── tsconfig.base.json # shared
├── eslint.config.mjs # shared
├── .gitignore
├── CLAUDE.md / AGENTS.md
├── docs/
├── scripts/
│ ├── desktop/ # electron-builder / signing / release
│ └── landing/ # cloudflare deploy / release-links
└── packages/
├── desktop/
│ ├── package.json
│ ├── src/
│ │ ├── main/
│ │ ├── preload/
│ │ ├── renderer/
│ │ └── shared/
│ ├── tests/
│ ├── builtin-skills/
│ ├── electron-vite.config.ts
│ └── tsconfig.json # extends ../../tsconfig.base.json
└── landing/
├── package.json
├── app/
├── migrations/
├── next.config.js
└── tsconfig.json
```
## 附:为什么不选其他方案
**方案 A(当前 proposal)**:pnpm workspace,一个仓两个 package。
**方案 B(独立仓)**:对当前团队规模不经济;跨仓协调成本高。
**方案 C(保持现状 + 更多 gitignore 隔离)**:治标不治本;CI 仍跑全量。
方案 A 成本 1 周,收益持续。
## 附:release 脚本改造示意
```json
// packages/desktop/package.json scripts 片段
{
"scripts": {
"build": "electron-vite build && node ../../scripts/desktop/build-desktop.mjs",
"release:local": "node ../../scripts/desktop/release-local.mjs",
"release:boss": "node ../../scripts/desktop/release-boss.mjs"
}
}
// packages/landing/package.json scripts 片段
{
"scripts": {
"build": "next build",
"release": "node ../../scripts/landing/release-cloudflare.mjs"
}
}
// root package.json
{
"scripts": {
"release:all": "pnpm --filter desktop release:boss && pnpm --filter landing release"
}
}
```
## 附:CI 按 changed files 触发(示意)
```yaml
# .github/workflows/desktop-ci.yml
on:
pull_request:
paths:
- "packages/desktop/**"
- "pnpm-lock.yaml"
- "tsconfig.base.json"
# .github/workflows/landing-ci.yml
on:
pull_request:
paths:
- "packages/landing/**"
```
desktop PR 不触发 landing CI,反之亦然。