feat: botmux update 支持本地 checkout 走 git pull + 构建 + 重启(CLI 与 Dashboard) - #930
feat: botmux update 支持本地 checkout 走 git pull + 构建 + 重启(CLI 与 Dashboard)#930deepcoldy wants to merge 6 commits into
Conversation
复审修复已推(commit 072d5f5)按复审意见收敛 4 处边界:
已知语义(本 PR 不扩范围):restart intent 仍按 version 判 update/manual,同 tag 不同 SHA 会落 manual intent,只影响 owner「已更新到 vX」DM 的措辞,不影响更新/build/lease/实际重启。 测试: |
原来 `botmux update` 只认 npm/pnpm/bun 全局安装,跑在本地 checkout(有 .git/src) 时会报「无法安全识别安装方式」退出。现给 cmdUpgrade 补一条本地开发分支: - 定位运行目录:优先解析全局瘦 wrapper(~/.botmux/bin/botmux)里指向的 dist/cli.js,往上两级得 checkout 根;解析不到回退到本进程安装根,再校验确是 git 工作树。逻辑抽成纯函数 src/utils/local-dev-update.ts 便于单测。 - 更新四步(在该目录内):git status --porcelain 有未提交改动即 fail closed 中止并提示(不自动 stash);git pull --ff-only(分叉/冲突直接报错停下,不 自动 merge);pnpm build(dist/ 被 gitignore,只 pull 不 build 重启后仍跑旧 代码,故不可省);node <dir>/dist/cli.js restart 从本 checkout 重启。 影响面:纯 CLI 侧改动,只动 cmdUpgrade 的本地分支 + 新增一个 utils 纯函数; 不动 npm/pnpm/bun 线上升级路径,也不动 maintenance 定时器与任何 worker/后端 /PTY 共用代码。跨平台:git/pnpm 调用在 win32 走 shell:true 解析 .cmd。 测试: - 新增 test/local-dev-update.test.ts(7 例,wrapper 文本解析 + 目录推导) - 更新 test/cli-update-alias.test.ts:断言改为新的本地分支行为(横幅 + update==upgrade 别名契约不变) - 实测:脏工作区正确中止不 stash;隔离仓库验证 ff-only 快进;wrapper 定位 到正确 checkout - pnpm build 通过;相关单测全绿(其余环境类失败在 stash 后基线上同样存在,与本 改动无关)
延续 CLI cmdUpgrade 的本地开发更新能力,把同一套逻辑接到 Dashboard「版本与 更新」卡片:本地 checkout(git 工作树)现在也能一键 git pull --ff-only + 构建 + 重启,不再只是置灰提示「请手动 git pull」。 共享层(src/utils/local-dev-update.ts): - 把 checkout 定位(解析 ~/.botmux/bin/botmux wrapper → dist/cli.js → 上两级)、 git 干净检查、HEAD 读取、更新步骤定义抽成共享纯/薄函数,CLI 与 dashboard 共用,避免两边逻辑漂移。CLI cmdUpgradeLocalDev 改为复用这些函数。 Dashboard 后端(dashboard.ts): - POST /api/update/run:local-dev 分支改为在 install 锁内执行 git 干净检查 (脏工作区 fail closed,返回 dirty_worktree + 文件列表,不 stash)→ git pull --ff-only(分叉/冲突报错停下)→ pnpm build(300s 超时、捕获输出); 用 HEAD 前后 sha 判断 changed;版本走 resolveCurrentVersion(git describe)。 复用既有 updateInFlight / 跨进程锁 / restart-lease 门禁。 - GET /api/update/status:新增 localDevUpdatable(local-dev 且定位到 git 工作树)。 - POST /api/update/restart:local-dev 从 resolveLocalDevCheckoutDir 重启, 与 CLI 一致(不再只用 dashboard 进程自身 cli.js)。 - rollback 对 local-dev 仍拒绝(本地开发无回滚语义)。 Dashboard 前端(settings-page.tsx / i18n.ts): - localDevUpdatable 时启用按钮,文案「本地更新(git pull + 构建 + 重启)」, 走既有 run→restart 两步流;dirty_worktree 错误在 UI 回显改动文件列表。 - 新增 zh/en 文案:本地可更新提示、确认框、进行中提示、脏工作区提示等。 影响面:改动集中在版本更新链路(CLI cmdUpgrade + dashboard /api/update/* + 更新卡片 UI)。不动 npm/pnpm/bun 线上升级路径与 maintenance 定时器(后者对 local-dev 仍 skip);不涉及 worker/后端/PTY/IM 共用代码。跨平台:git/pnpm 子进程在 win32 走 shell:true 解析 .cmd。 测试: - test/local-dev-update.test.ts:纯函数 + 真实 git 仓库验证 isGitWorktree / gitPorcelainStatus(干净/脏)/ gitHeadSha(跨 commit 变化)/ 更新步骤定义,共 11 例 - test/cli-update-alias.test.ts:按新本地分支行为更新断言(update==upgrade 别名契约不变) - test/dashboard-update-action.test.ts 现有 8 例仍通过(响应字段形状未变) - pnpm build + tsc --noEmit 通过;新 i18n key 与后端路由字符串确认已进 dist bundle - CLI 本地分支实测:脏工作区正确中止、ff-only 快进(隔离仓库)、wrapper 定位正确
回应对本地 checkout 更新路径的复审,修 4 个边界问题: 1. wrapper 解析收紧,避免更新错仓库:parseWrapperCliEntry 只在 `exec … node …` 行上匹配以 `dist/cli.js`(`[\\/]` 兼容 Windows 分隔符)结尾的目标,注释里的 其它 `.js`(如 `# preload "…/hook.js"`)不再被当成 CLI。resolveLocalDevCheckoutDir 进一步用 isBotmuxCheckout 做 repo 身份校验(`.git` + package.json.name==="botmux"), 非 botmux 仓库回退到 running root。**不**要求 dist 已存在——use:here 允许先有 checkout 后 build,本地更新正是要产出那个 dist。 2. build 成功后始终重启:local-dev 只 build 不动 HEAD(checkout 已被手动 pull 过) 时,旧逻辑因 changed=false 跳过重启,导致新 dist 不生效、与按钮承诺不符。/api/update/run local-dev 分支恒返回 restartRequired:true;前端改用 updateResponseNeedsRestart (restartRequired || changed)决定是否重启,changed 仅用于展示。 3. restart 前校验目标 cli.js 存在:/api/update/restart 对 local-dev 用 checkout 目标前,先校验 botmuxCliEntryAt(target) 存在;否则回退 running root。避免 wrapper 指向尚未 build 的 checkout 时,重启驱动 spawn 出 pid 又立刻因模块缺失退出、 restart lease 得不到清理(UI 空等 90s 且后续重启被挡)。 4. 版本按实际更新的 checkout 读:新增 resolveCurrentVersionAt(dir),runLocalDevUpdate 返回该 checkout 的 old/new version,不再混用 running root 的版本(wrapper→B、 dashboard 跑 A 时会显示错版本)。 已知语义(本 PR 不扩范围):restart intent 仍按 version 判 update/manual,同 tag 不同 SHA 会落 manual intent,只影响 owner「已更新到 vX」DM 的措辞,不影响更新/ build/lease/实际重启。 测试: - local-dev-update.test.ts 补:注释假路径负测、非 dist/cli.js 目标负测、Windows 分隔符正测、isBotmuxCheckout(.git + name 双条件、无 .git 拒绝) - dashboard-update-action.test.ts 补 updateResponseNeedsRestart 三例,锁住 changed=false && restartRequired=true 仍重启的核心回归 - pnpm build + tsc --noEmit 通过;相关单测 47 例全过;CLI 本地分支实测仍正确 (定位到真实 checkout、脏工作区中止)
二轮复审指出上一版 parseWrapperCliEntry 只判断行内出现 exec/node,未锚定为该行 实际执行的命令:`# old: exec node ".../dist/cli.js" "$@"` 这类「保留旧命令作注释」 写法,以及 `echo exec node "..."`,仍会被选中——若旧路径也是 botmux checkout, 后续 repo 身份校验照样通过,最终更新/重启错 checkout。 改为按本仓库生成 wrapper 的固定形状精确匹配:行首(仅允许前导空白)`exec` → `node`/绝对 node 路径 → 引号包裹的以 `dist/cli.js`(`[\\/]` 兼容 Windows)结尾的 路径 → 结尾 `"$@"`。锚定行首 + 要求尾部 `"$@"` 直接排除注释行与 echo 行。 测试:补「注释掉的旧 exec 行在真 exec 前 → 返回真路径」「echo exec 行 → null」 「缺尾部 $@ → null」负测;原有正例(标准/绝对 node/Windows 分隔符)保留。 build + tsc 通过;解析相关 30 例全过;对线上实际 wrapper 解析定位仍正确。
上一版正则允许捕获相对路径(如 `exec node "nested/botmux/dist/cli.js" "$@"`)。 相对路径在 shell exec 时相对调用者 cwd 解析,而 Dashboard/CLI resolver 在这里对它 做 dirname/repo 身份校验时相对的是读取方进程的 cwd——两者 cwd 不同就会解释成不同 checkout,Dashboard cwd 下恰有同名 botmux repo 时仍可能更新错目标。函数注释与前序 commit 也都承诺只接受绝对路径。 parseWrapperCliEntry 捕获后加 `posix.isAbsolute(entry) || win32.isAbsolute(entry)` 校验,不绝对则跳过该行。跨平台:POSIX `/…` 与 Windows drive(`C:\…`)/UNC 前缀都算绝对。 测试:补相对路径(`nested/…` 与 `./dist/cli.js`)→ null 负测;原有绝对路径正例 (POSIX/绝对 node 路径/Windows 分隔符)保留。build + tsc 通过;解析相关单测全过。
514bb3c to
dec2745
Compare
|
建议合并前再收口一处本地 checkout 的事务边界:
建议把成功构建的绝对 checkout(最好连同 build 后 HEAD)作为 server-side update plan 固定下来, 另一个相关边界是 这是自动评审的初步意见,最终以维护者审阅为准。 |
回应评审对多-checkout 并发的两处收口: A. run→restart 之间 wrapper 漂移(TOCTOU):/api/update/run 更新 checkout B 后, /api/update/restart 原来会重新解析 wrapper——若期间另一个 worktree `use:here` 把 wrapper 改指 C,可能「更新 B 却重启 C 或回退 A」。现 run 成功后把实际构建的 checkout 绝对路径 + 构建后 HEAD 固定为 server 端 pending plan;restart 复用并 复验:目标 cli.js 不存在→update_target_unavailable,HEAD 漂移→update_target_drifted, 均在 claim restart lease 前 fail closed(不留悬空 lease)。无 pending plan 的 纯手动重启仍按 wrapper 实时解析(cli.js 不存在则回退 running root)。决策逻辑抽成 纯函数 resolveLocalDevRestartTarget,注入探针便于单测。前端对两个新错误码回显 可操作文案并刷新状态。 B. CLI 本地更新未进共享锁:cmdUpgradeLocalDev 的 git 干净检查 + pull + build 现 包进 withFileLockSync(globalInstallUpdateLockTarget())——与 dashboard 的 /api/update/run 同一把跨进程锁,避免 CLI 与 dashboard 并发对同一 checkout 交错 build 互相清理/覆盖 dist。restart 不在锁内(有自己的 restart lease)。锁文件父目录 先 mkdir,避免 daemon 从未起过的机器上 ENOENT 盖掉真实错误。 测试:resolveLocalDevRestartTarget 5 例(pinned 正常/目标丢失/HEAD 漂移/无 plan 命中/无 plan 回退,覆盖 wrapper B→C 回归);build + tsc 通过;相关单测 55 例全过; CLI 本地分支实测脏工作区仍正确中止。 已知语义(不扩范围):restart intent 仍按 version 判 update/manual,同 tag 不同 SHA 落 manual intent,只影响通知措辞。
已收口两处并发边界(commit 9c2a21b)A. run→restart 之间 wrapper 漂移(TOCTOU)
无 pending plan 的纯手动重启仍按 wrapper 实时解析(cli.js 不存在则回退 running root)。决策逻辑抽成纯函数 B. CLI 本地更新未进共享锁 测试: 已知语义(不扩范围):restart intent 仍按 version 判 update/manual,同 tag 不同 SHA 落 manual intent,只影响通知措辞。 |
背景 / 为什么
原来
botmux update(=upgrade别名)只认 npm/pnpm/bun 全局安装。跑在本地checkout(有
.git/src)时:pull + 构建后重启」。
本 PR 给「本地开发从源码运行」这种部署补上一键更新:定位 checkout → git pull
--ff-only → 构建 → 重启,CLI 与 Dashboard 都支持。
改了什么
共享层
src/utils/local-dev-update.ts把 checkout 定位、git 干净检查、HEAD 读取、更新步骤定义抽成共享纯/薄函数,CLI 与
Dashboard 共用,避免逻辑漂移:
~/.botmux/bin/botmux里指向的dist/cli.js→ 往上两级得 checkout 根(那才是用户实际敲
botmux跑的目录),拿不到回退到本进程安装根,再校验是 git 工作树;
localDevUpdateSteps():git pull --ff-only→pnpm build(重启单独由各调用方施加)。CLI(
src/cli.ts)cmdUpgrade检测到本地 checkout 走新分支cmdUpgradeLocalDev:git status --porcelain有未提交改动 → fail closed 中止并列出改动(不自动 stash);git pull --ff-only(分叉/冲突直接报错停下,不自动 merge);pnpm build(关键:dist/被 gitignore,只 pull 不 build 重启后跑的还是旧代码);node <checkout>/dist/cli.js restart从本 checkout 重启(不走 PATH,避免被更靠前的全局 botmux 抢先)。Dashboard 后端(
src/dashboard.ts)POST /api/update/run:local-dev 分支不再拒绝,改为在既有 install 锁内执行git 干净检查(脏工作区返回
dirty_worktree+ 文件列表,不 stash)→git pull --ff-only→pnpm build(300s 超时、捕获输出);用 HEAD 前后 sha 判断changed;版本走
resolveCurrentVersion(git describe)。复用既有updateInFlight/ 跨进程锁 / restart-lease 门禁;GET /api/update/status:新增localDevUpdatable(本地开发且定位到 git 工作树才为 true);POST /api/update/restart:local-dev 从resolveLocalDevCheckoutDir()重启,与 CLI 一致;rollback对 local-dev 仍拒绝(本地开发无回滚语义)。Dashboard 前端(
settings-page.tsx/i18n.ts)localDevUpdatable时按钮启用,文案「本地更新(git pull + 构建 + 重启)」,走既有run→restart 两步流;
dirty_worktree错误在 UI 回显改动文件列表;影响面
改动集中在版本更新链路:CLI
cmdUpgrade+ Dashboard/api/update/*+ 更新卡片 UI。git/pnpm子进程在 win32 走shell:true解析.cmdshim;daemon 实际跑在 Linux,路径/进程调用两边都已考虑。
测试
pnpm build+pnpm exec tsc --noEmit通过;test/local-dev-update.test.ts(11 例):wrapper 文本解析 + 目录推导 + 更新步骤定义,并用真实 git 仓库验证
isGitWorktree/gitPorcelainStatus(干净/脏)/gitHeadSha(跨 commit 变化);test/cli-update-alias.test.ts:按新本地分支行为更新断言(update==upgrade别名契约不变);test/dashboard-update-action.test.ts现有 8 例仍通过(响应字段形状未变);distbundle;git pull --ff-only快进(隔离仓库验证)、wrapper 定位到正确 checkout。
待 live 验证
Dashboard「本地更新」按钮的端到端点击需要一个「从本 checkout 跑起来的 dashboard」,
部署会重启 daemon,留到 review 后 live 验证并补 UI 截图。