Skip to content

feat: botmux update 支持本地 checkout 走 git pull + 构建 + 重启(CLI 与 Dashboard) - #930

Open
deepcoldy wants to merge 5 commits into
masterfrom
feat/local-dev-git-update
Open

feat: botmux update 支持本地 checkout 走 git pull + 构建 + 重启(CLI 与 Dashboard)#930
deepcoldy wants to merge 5 commits into
masterfrom
feat/local-dev-git-update

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

背景 / 为什么

原来 botmux update(= upgrade 别名)只认 npm/pnpm/bun 全局安装。跑在本地
checkout(有 .git/src)时:

  • CLI 会报「无法安全识别当前安装方式」直接退出;
  • Dashboard「版本与更新」卡片把「更新到最新版」按钮置灰,只提示「请手动 git
    pull + 构建后重启」。

本 PR 给「本地开发从源码运行」这种部署补上一键更新:定位 checkout → git pull
--ff-only → 构建 → 重启,CLI 与 Dashboard 都支持。

改了什么

共享层 src/utils/local-dev-update.ts

把 checkout 定位、git 干净检查、HEAD 读取、更新步骤定义抽成共享纯/薄函数,CLI 与
Dashboard 共用,避免逻辑漂移:

  • checkout 定位:解析全局瘦 wrapper ~/.botmux/bin/botmux 里指向的 dist/cli.js
    → 往上两级得 checkout 根(那才是用户实际敲 botmux 跑的目录),拿不到回退到本
    进程安装根,再校验是 git 工作树;
  • localDevUpdateSteps()git pull --ff-onlypnpm build(重启单独由各调用方施加)。

CLI(src/cli.ts

cmdUpgrade 检测到本地 checkout 走新分支 cmdUpgradeLocalDev

  1. git status --porcelain 有未提交改动 → fail closed 中止并列出改动(不自动 stash);
  2. git pull --ff-only(分叉/冲突直接报错停下,不自动 merge);
  3. pnpm build关键dist/ 被 gitignore,只 pull 不 build 重启后跑的还是旧代码);
  4. 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-onlypnpm 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。

  • 不动 npm/pnpm/bun 线上升级路径;不动 maintenance 定时器(对 local-dev 仍 skip);
  • 不涉及 worker/后端/PTY/IM 等多 CLI × 多后端共用代码路径;
  • 跨平台:git/pnpm 子进程在 win32 走 shell:true 解析 .cmd shim;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 例仍通过(响应字段形状未变);
  • 新增 i18n key 与后端路由字符串确认已进 dist bundle;
  • CLI 本地分支实测:脏工作区正确中止不 stash、git pull --ff-only 快进(隔离仓库验证)、
    wrapper 定位到正确 checkout。

待 live 验证

Dashboard「本地更新」按钮的端到端点击需要一个「从本 checkout 跑起来的 dashboard」,
部署会重启 daemon,留到 review 后 live 验证并补 UI 截图。

@deepcoldy

Copy link
Copy Markdown
Owner Author

复审修复已推(commit 072d5f5

按复审意见收敛 4 处边界:

  1. wrapper 解析收紧parseWrapperCliEntry 只在 exec … node … 行匹配以 dist/cli.js[\\/] 兼容 Windows)结尾的目标,注释里的其它 .js 不再抢先;resolveLocalDevCheckoutDir 叠加 isBotmuxCheckout.git + package.json.name==="botmux")repo 身份校验,非 botmux 回退 running root。要求 dist 已存在(use:here 允许先 checkout 后 build)。
  2. build 成功后始终重启/api/update/run local-dev 恒返回 restartRequired:true;前端改用 updateResponseNeedsRestartrestartRequired || changed)决定重启,changed 仅展示。修复「已 pull 未 build → 点更新只 build、HEAD 不变、UI 报已最新跳过重启、新 dist 不生效」。
  3. restart 前校验目标 cli.js/api/update/restart 对 local-dev 用 checkout 前校验 botmuxCliEntryAt(target) 存在,否则回退 running root,避免 spawn 出 pid 又立刻因模块缺失退出、lease 得不到清理。
  4. 版本按实际 checkout 读:新增 resolveCurrentVersionAt(dir)runLocalDevUpdate 返回该 checkout 的 old/new version,不再混用 running root。

已知语义(本 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 双条件;dashboard-update-action.test.tsupdateResponseNeedsRestart 三例(锁 changed=false && restartRequired=true 仍重启)。pnpm build + tsc --noEmit 通过;相关单测 47 例全过;CLI 本地分支实测仍正确。

原来 `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 通过;解析相关单测全过。
@deepcoldy
deepcoldy force-pushed the feat/local-dev-git-update branch from 514bb3c to dec2745 Compare August 18, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant