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

11 KiB
Raw Blame History

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/
│       └── ...
├── pi-mono-reference/       # 保留在 root(现状)
└── docs/

pnpm-workspace.yaml:

packages:
  - "packages/*"

Root package.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/
  • 不动 pi-mono-reference/:那是第三方 reference 副本,保留在 root
  • 不改 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/
├── pi-mono-reference/                # 保留根(第三方 reference)
├── 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 脚本改造示意

// 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 触发(示意)

# .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,反之亦然。