@jspg-ai/coding-bb 0.0.3-beta.31 → 0.0.3-beta.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/cbb/dev-standards/rules/cbb-ai-behavior.md +5 -4
  2. package/cbb/lib/install/claude-code.js +3 -3
  3. package/cbb/lib/install/init.js +55 -23
  4. package/cbb/lib/install/minimax.js +2 -2
  5. package/cbb/lib/install/tool-selection.js +8 -30
  6. package/cbb/lib/utils/githook.js +6 -2
  7. package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
  8. package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
  9. package/cbb/worktrees/_shared/scripts/install-git-hook.js +1 -1
  10. package/cbb/worktrees/_shared/scripts/merge-branches.js +469 -0
  11. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +38 -6
  12. package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
  13. package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +1 -1
  14. package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +469 -0
  15. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +86 -51
  16. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +3 -1
  17. package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +4 -2
  18. package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
  19. package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +469 -0
  20. package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +4 -2
  21. package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +1 -1
  22. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +143 -20
  23. package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
  24. package/config/openspec/schemas/spec-driven/schema.yaml +1 -0
  25. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cbb-worktree-close
3
- description: "关闭/清理/删除需求工作空间的 worktree 和分支。触发:用户明确说关闭/清理/删除 worktree、结束隔离开发、合并完要清理。禁止在 cbb-worktree-init 执行后自动调用——只有用户主动要求关闭时才触发。"
3
+ description: "关闭/清理/删除需求工作空间的 worktree 和分支。内置收尾合并门禁(ADR 0007):未合并分支必须先经 MR 或直合并交付,或由用户确认废弃并重命名为 <分支>-deprecated,否则不允许关闭。触发:用户明确说关闭/清理/删除 worktree、结束隔离开发、合并完要清理。禁止在 cbb-worktree-init 执行后自动调用——只有用户主动要求关闭时才触发。"
4
4
  ---
5
5
 
6
6
  # Close Worktree(跨平台脚本化版本 v2.0)
@@ -17,7 +17,9 @@ description: "关闭/清理/删除需求工作空间的 worktree 和分支。触
17
17
  - 只操作 `.worktrees/worktree-<需求名>` 这一类精确路径,**绝不碰其它 worktree**(如 IDE/工具自动创建的 `~/.qoder/worktree/...` 会话 worktree)
18
18
  - 通过检查 `.worktrees/worktree-<需求名>/.codespace/<应用名>/` **是否实际存在**来确定关联应用——创建时用户可能只关联了 `CBB-WORKSPACE.json` 中的部分应用,不能假设全部应用都关联
19
19
  - **顺序硬约束:先移除所有 worktree,再删除分支。** 目标分支当前正检出于 worktree 中,先删分支会报 `error: Cannot delete branch '<分支>' checked out at '<worktree 路径>'`
20
- - **破坏性操作必须先报告、经用户确认才执行**:未提交修改、未合并/未推送的提交,绝不静默 `--force` 或 `-D` 丢弃
20
+ - **收尾合并门禁(ADR 0007)**:关闭前每个参与仓库必须为"已合并"(复用 `safety-check` 的 `merged` 判定,含 release 中转 / squash 兜底)。未合并时由用户在门禁处三选一——**提 MR**(人工浏览器合入)/ **直合并**(agent 执行 `merge-branches.js`,前置:已归档 + 已提交)/ **确认废弃**(`deprecate-branches.js` 把远程分支重命名为 `<分支>-deprecated`)。**硬门禁:未合并且非废弃一律不允许关闭**
21
+ - **合并成功后自动删分支(ADR 0007)**:交付完成(MR 已合并或直合并成功)后自动删各参与仓库的本地 + 远程 feature 分支(删除前输出将删清单);"废弃"出口则保留分支(仅远程重命名)
22
+ - **破坏性操作必须先报告、经用户确认才执行**:未提交修改、未确认废弃的分支,绝不静默 `--force` 或 `-D` 丢弃
21
23
  - 软性检查失败时报告详情,询问用户是否继续
22
24
  - 输入约定:凡是需要用户自由输入文本(如分支名、确认)的场景,**必须直接在对话中提问并等待用户回复**,禁止使用 `AskUserQuestion` 工具;`AskUserQuestion` 仅用于固定选项场景(选择归档哪些 change、是否丢弃未提交修改)
23
25
 
@@ -224,60 +226,81 @@ AskUserQuestion({
224
226
  - 若 delta specs 需要同步 → 对该 change 单独提示同步选项(同步 / 跳过同步直接归档)
225
227
  - 每个 change 的归档结果独立记录(✅ 已归档 / ⏭️ 跳过 / ❌ 失败)
226
228
 
227
- ### 3.3 分支处理方式(默认保留,不询问)
229
+ ### 3.3 未推送 commit 处理(仅当 2.4 任一 worktree `unpushed_count > 0` 时出现)
228
230
 
229
- 历史版本此步骤会询问"保留 / 删本地 / 删本地+远程",但绝大多数场景仅需移除 worktree、自动保留分支;频繁弹窗会打断 close-worktree 的批量清理节奏。改为 **默认 `MODE = keep`,不再触发 `AskUserQuestion`**。
231
+ **场景**:用户在 worktree 中做了 commit,但没有调 `cbb-worktree-push` 推送,本地有 ahead 提交。
230
232
 
231
- **默认行为**:
233
+ **报告**:
234
+ ```
235
+ ⚠️ 检测到未推送的提交(ahead of origin/<branch>):
236
+ - workspace: 3 个 commit
237
+ - ep-user-info-server: 1 个 commit
238
+
239
+ 请选择处理方式:
240
+ [先推送](推荐)调 cbb-worktree-push 推送所有 ahead 提交
241
+ [继续] 不单独推送,直接进入合并门禁(走「直合并」时会随合并一并推送到主干)
242
+ [中止] 终止流程,我先手动处理
243
+ ```
232
244
 
233
- - `MODE = keep` → 仅移除 worktree,**本地分支与远程分支都不动**
234
- - Step 2 安全评估结果(合并状态 / 远程存在性)仅用于内部决策展示,**不再弹出分支处理问卷**
245
+ - 用户选择「先推送」→ AI 引导执行 `cbb-worktree-push` skill(或 `/cbb-worktree-push`),完成后**重新跑** Step 2 safety-check 确认 `unpushed_count = 0` 再回到本步骤
246
+ - 用户选择「继续」→ 进入 Step 3.4;注意:走「提 MR」出口时会因 feature 未推送而失败,需回到本步骤先推送
247
+ - 用户选择「中止」→ 终止流程
235
248
 
236
- **逃生通道**(确需主动删分支时手动调用):
249
+ > ⓘ 走「直合并」出口时,ahead 提交会被合并进主干并随 `git push origin <主干>` 一并推送,无需单独推送 feature 分支。
237
250
 
238
- 如用户在 Step 1.1 选定要关闭的 worktree 时同步要求"删除分支",或运行后需要清理本地/远程分支,可手动调用脚本:
251
+ ### 3.4 合并门禁决策(核心 · ADR 0007)
239
252
 
240
- ```bash
241
- # 仅删本地分支(已合并自动 -d,未合并自动 -D;含 squash 合并 -D 兜底)
242
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local
253
+ 依据 Step 2 的 `merged` 判定(每个参与仓库 `merged !== null` 即已合并,含 `main` / `origin/main` / `main-via-release-*`):
243
254
 
244
- # 删本地 + 远程分支
245
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local_remote
246
- ```
255
+ **情况 A:全部参与仓库已合并** → 无交互,直接进入 Step 4(合并成功后自动删分支)。
247
256
 
248
- **调用前的强警告**(调用方需自评):
257
+ **情况 B:存在未合并仓库** → 硬门禁触发,`AskUserQuestion` 三选一(**不允许"未合并且非废弃"地关闭**):
249
258
 
250
- - **未合并分支**(Step 2.2 `merged: null`):删除会永久丢失未合并的提交,必须二次确认
251
- - **远程分支**:删除后无法直接恢复,需确认其他仓库/同事是否还在引用
252
- - **推荐做法**:默认 `keep` 关闭 worktree 即可,确有必要再单独执行脚本;这样误删风险被显式手动动作兜住
259
+ ```
260
+ 以下仓库的需求分支尚未交付(未合并到主干):
261
+ - workspace(merged: null)
262
+ - ep-foo-app(merged: null)
263
+
264
+ 请选择交付方式:
265
+ [提 MR](推荐) 生成预填合并请求链接,由人工在浏览器合入
266
+ [直合并] 由 agent 直接合并到主干并推送远端(需你确认)
267
+ [确认废弃] 该分支不再交付,远程重命名为 <分支>-deprecated
268
+ ```
253
269
 
254
- > 合并代码到主分支属于另一项工作(参见 `finishing-a-development-branch`),本技能不代为合并;若用户其实想合并,引导其先完成合并再回来关闭。
270
+ **出口一 · 提 MR(推荐)**
255
271
 
256
- ### 3.4 未推送 commit 处理(仅当 2.4 任一 worktree `unpushed_count > 0` 时出现)
272
+ 1. 先确保 feature 已推送(Step 3.3 未推送时先推送)
273
+ 2. **引导用户运行 `cbb-worktree-push`**(说"提交并推送"):其 Step 7 会列出目标分支候选(`git branch -r` 现场候选,**禁止 AI 代决**)并生成 MR 预填链接(可选自动开浏览器);本技能不自带 MR 链接脚本
274
+ 3. **中止 close**:提示"合并完成后重跑本技能"。**不执行任何移除 / 删除**
257
275
 
258
- **场景**:
259
- - 用户在 worktree 中做了 commit,但**没有调** `cbb-worktree-push` 推送
260
- - 本地有 ahead 提交未推送到 origin
261
- - 一旦 close worktree + 后续走 `local` / `local_remote` 删分支,ahead 提交可能永久丢失(取决于上游 `delete-branches.js` 行为)
276
+ **出口二 · 直合并(`merge-branches.js`)**
262
277
 
263
- **报告**:
278
+ - **前置(Q18)**:3.1 无未提交修改、3.2 已归档并提交——**合并前必须已归档 + 已提交**(两者先后不限)
279
+ - 执行(`--apps` 传 Step 1.2 发现的参与应用):
280
+
281
+ ```bash
282
+ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<branch>" --apps "<app1,app2>"
264
283
  ```
265
- ⚠️ 检测到未推送的提交(ahead of origin/<branch>):
266
- - workspace: 3 个 commit
267
- - ep-user-info-server: 1 个 commit
268
- - ep-baz-app: 2 个 commit
269
284
 
270
- 请选择处理方式:
271
- [先推送](推荐)调 cbb-worktree-push 推送所有 ahead 提交
272
- [接受丢失] 继续关闭,ahead 提交在删分支后永久丢失
273
- [中止] 终止流程,我先手动处理
285
+ - 两阶段:预检(主干可探测 / 主干检出干净且不领先 origin / 分支存在;全过才执行)→ 执行(checkout 主干 → pull --ff-only → merge --no-ff → push origin 主干)
286
+ - 解读末行 `MERGE_RESULT`:
287
+ - 退出码 0 → 交付完成,继续 Step 4
288
+ - `need-mr`(`branch-protected`:远端主干受保护)→ 远端保护是常态,**改用出口一**(不是故障)
289
+ - `failed`(含 `merge-failed` / `push-failed`)→ 输出失败明细,阻塞并询问用户(修复后重跑;重跑幂等)
290
+
291
+ **出口三 · 确认废弃(`deprecate-branches.js`)**
292
+
293
+ - **二次确认**(远程重命名不可逆):`AskUserQuestion` 固定选项"确认废弃 / 中止"
294
+ - 执行:
295
+
296
+ ```bash
297
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<branch>" --apps "<app1,app2>"
274
298
  ```
275
299
 
276
- - 用户选择「先推送」→ AI 引导用户执行 `cbb-worktree-push` skill(或 `/cbb-worktree-push`),完成后**重新跑** Step 2 safety-check 确认 `unpushed_count = 0` 再回到本步骤
277
- - 用户选择「接受丢失」→ 必须二次确认(ahead 提交真的会丢),确认后继续 Step 4
278
- - 用户选择「中止」→ 终止流程
300
+ - 效果:远程存在该分支的仓库,重命名 `<分支>` → `<分支>-deprecated`(**保留历史、不删提交**);**本地分支保持原名不动**
301
+ - 解读末行 `DEPRECATE_RESULT`:`failed` → 报告并终止
279
302
 
280
- > ⓘ **为什么必须二次确认**:close-worktree 的默认 MODE=keep 不会删分支,ahead 提交仍在本地;只有用户主动走 `local` / `local_remote` 删分支模式才会真正丢。AI 必须先在报告中讲清楚后果,再询问用户决策。
303
+ > ⓘ 门禁判定复用 `safety-check` 的 `merged`:他人经 MR 或 `release-*` 中转合入后,重跑本技能即自动放行,无需重复交付。
281
304
 
282
305
  ---
283
306
 
@@ -341,32 +364,34 @@ node "<skill_dir>/scripts/remove-worktrees.js "<ws_root>" "<branch>" "<force>"
341
364
  - **不会产生孤立窗口**——`--reuse-window` 是复用当前窗口切换目录,**不会打开新窗口**。即使 IDE 已开着指向 WS_ROOT 的其他窗口,本步骤也不会重复创建,仅是把当前指向已删除目录的窗口切走
342
365
  - **失败不致命**——`session_move` / `--reuse-window` 失败时仅警告,后续分支删除不受影响
343
366
 
344
- ### 4.4 分支处理(默认跳过,按需手动调用)
367
+ ### 4.4 分支处理(合并成功后自动删除 · ADR 0007)
345
368
 
346
- 默认 `MODE = keep`(见 Step 3.3),本步骤**不主动调用** `delete-branches.js`,本地分支与远程分支均保留。
369
+ **按 Step 3.4 的门禁出口决定**:
347
370
 
348
- 如用户在 Step 1.1 选定 worktree 时已要求"删除分支",或运行后需要单独清理,可在此步骤**手动调用**:
371
+ - **交付完成**(情况 A 已合并,或出口二直合并成功)→ **自动删除**各参与仓库的本地 + 远程 feature 分支:
349
372
 
350
373
  ```bash
351
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local
352
- # 或
353
374
  node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local_remote
354
375
  ```
355
376
 
377
+ - **出口三废弃** → **保留分支**(远程已重命名为 `<分支>-deprecated`),**不调用** `delete-branches.js`
378
+
379
+ **删除前必做**:输出"即将删除的本地 / 远程分支清单"(逐仓库),透明可见。
380
+
356
381
  脚本内部对每个仓库:
357
382
  - **本地分支**:检测是否已合并(复用 safety-check 的合并判定算法)
358
383
  - 已合并 → `git branch -d`(安全删除)
359
384
  - 未合并 → `git branch -D`(强制删除)
360
385
  - squash 合并可能让 `-d` 失败 → 自动回退到 `-D`(squash 合并同样安全)
361
- - **远程分支**(仅 `local_remote` 模式):`git push <remote> --delete <branch>`
386
+ - **远程分支**(`local_remote` 模式):`git push <remote> --delete <branch>`
362
387
  - 分支不存在时 push --delete 会报错,按"已删除/本就无"处理即可
363
388
  - 远程引用已在 Step 2.0 主动 fetch 刷新并持久化在 .git,此处直接读取无需重复 fetch
364
389
 
365
- **解读末行 `BRANCH_DELETE_RESULT`**:
366
- - `mode`:keep / local / local_remote
367
- - `ok`:是否全部成功
390
+ **解读末行 `BRANCH_DELETE_RESULT`**:`mode`(keep / local / local_remote)、`ok`。
391
+
392
+ **失败处理(ADR 0007)**:清理阶段(删除分支)失败**报告并继续其余仓库,不回滚**——交付已达成,清理不净不该阻塞(与合并阶段的"失败即停"不同)。
368
393
 
369
- > 默认跳过分支删除是安全红线的取舍:未合并分支误删会永久丢失提交、远程分支误删会冲击其他仓库/同事。保留为显式手动动作可让调用方在执行前再做一次审视。
394
+ **顺序硬约束**:必须在 Step 4.2(移除 worktree)**之后**执行——分支正检出于 worktree,先删会报 `Cannot delete branch ... checked out at ...`。
370
395
 
371
396
  > 关于 `.gitignore`:创建时把 `.worktrees/` 加入 `.gitignore` 并**提交到了 feature 分支**(非主分支)。
372
397
  > - 分支未合并、被 `-D` 删除 → 这些 commit 随分支一起消失,主分支的 `.gitignore` 不受影响
@@ -395,6 +420,7 @@ git -C "<ws_root>" worktree list
395
420
  ```
396
421
  ✅ 工作空间 worktree 已关闭
397
422
  当前目录:<工作空间根目录绝对路径>
423
+ 交付方式:已合并(MR 合入)/ 直合并(agent 合并并推送主干)/ 确认废弃
398
424
  已移除 worktree:
399
425
  - .worktrees/worktree-feature-xxx(workspace)
400
426
  - .worktrees/worktree-feature-xxx/.codespace/ep-user-info-server(嵌套,ep-user-info-server 仓库)
@@ -404,7 +430,9 @@ git -C "<ws_root>" worktree list
404
430
  ✅ ai-call-send-im-msg → 已归档到 archive/2026-07-08-ai-call-send-im-msg
405
431
  ⏭️ fix-payment-timeout → 用户选择跳过
406
432
  (无未归档 change 时不显示本节)
407
- 分支处理:feature-xxx 已保留(默认行为;如需删除分支,手动调用 scripts/delete-branches.js)
433
+ 分支处理:
434
+ - 交付完成 → 已删除本地 + 远程分支:feature-xxx(逐仓库列出)
435
+ - 已废弃 → 远程已重命名为 feature-xxx-deprecated(本地分支保留原名)
408
436
  剩余工作空间 worktree:<列出仍存在的,或 "无" >
409
437
  👉 仅当所用工具不支持自动切根、且你的窗口仍停在已删除的 worktree 时,请手动切回工作空间根:<ws_root 绝对路径>
410
438
  Workspace cleaned up.
@@ -425,7 +453,10 @@ git -C "<ws_root>" worktree list
425
453
  ### 绝不允许
426
454
 
427
455
  - 先删分支再删 worktree(分支正检出于 worktree,会报 `Cannot delete branch ... checked out at ...`)
428
- - 未经用户确认用 `--force` 丢弃未提交修改,或主动调用 `delete-branches.js` 对未合并分支执行 `-D` 强删
456
+ - **未合并且未确认为废弃时关闭 worktree**(违反 ADR 0007 硬门禁)
457
+ - 未经用户确认用 `--force` 丢弃未提交修改;或对未确认废弃的未合并分支执行 `-D` 强删
458
+ - 未经二次确认执行 `deprecate-branches.js`(远程重命名不可逆)
459
+ - 直合并前跳过"已归档 + 已提交"前置校验
429
460
  - 操作 `.worktrees/worktree-<需求名>` 以外的任何 worktree(如 `~/.qoder/worktree/...` 会话 worktree)
430
461
  - 移除/删除前跳过 Step 2 安全评估
431
462
  - 删除工作空间根目录的 AI 配置目录(`.claude/`、`.qoder/`、`.opencode/` 等)
@@ -437,6 +468,8 @@ git -C "<ws_root>" worktree list
437
468
 
438
469
  - 读 `CBB-WORKSPACE.json` 取应用名列表;以 `.worktrees/worktree-<需求名>/.codespace/<应用名>/` 实际存在性确定关联清单
439
470
  - 移除/删除前执行安全评估(未提交修改 / 是否合并 / 是否推远程)并报告
471
+ - **按 Step 3.4 门禁三选一交付**;未合并且未确认为废弃时**禁止关闭**
472
+ - 交付完成(自动删分支)前**先输出将删清单**;清理阶段失败报告并继续,不回滚
440
473
  - 严格按 Step 4.2(移除 worktree)→ Step 4.4(删分支)顺序
441
474
  - 移除 workspace worktree 前先把会话移出(`cd "<WS_ROOT>" &&` 或 `git -C` 绝对路径)
442
475
  - 收尾切回 WS 根目录、`pwd` 确认
@@ -463,4 +496,6 @@ git -C "<ws_root>" worktree list
463
496
  | `check-unarchived.js` | 检查未归档 change | ws_root | 0/1 |
464
497
  | `safety-check.js` | 安全评估(合并判定 + 未提交 + 远程) | ws_root, branch | 0/1 |
465
498
  | `remove-worktrees.js` | 移除 worktree(三层降级) | ws_root, branch, force | 0/1 |
466
- | `delete-branches.js` | 删除分支(按 mode) | ws_root, branch, mode | 0/1/2 |
499
+ | `delete-branches.js` | 删除分支(按 mode;交付完成后由 Step 4.4 自动以 local_remote 调用) | ws_root, branch, mode | 0/1/2 |
500
+ | `merge-branches.js` | 收尾合并门禁·直合并(预检 → merge --no-ff → push 主干,共享脚本,真源在 `_shared/scripts/`) | ws_root, branch, --apps / --allow-checkout / --dry-run | 0/1/2 |
501
+ | `deprecate-branches.js` | 收尾合并门禁·废弃(远程分支重命名为 `<分支>-deprecated`,共享脚本) | ws_root, branch, --apps | 0/1/2 |
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  require('./silence-popup');
3
3
  /**
4
- * cbb-worktree-close 未归档 change 检查
4
+ * 未归档 change 检查(共享脚本,真源:cbb/worktrees/_shared/scripts/)
5
+ *
6
+ * 供 cbb-worktree-close(收尾安全评估)与 cbb-worktree-push(Step 7 直合并前置,ADR 0008)调用。
5
7
  *
6
8
  * 用法:node check-unarchived.js <ws_root> [branch]
7
9
  *
@@ -52,8 +52,10 @@ function mergedTo(repoPath, from, to) {
52
52
  if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
53
53
  if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
54
54
  if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
55
- const cherry = run('git cherry "' + to + '" "' + from + '" | grep -q "^+" && echo N || echo Y', repoPath);
56
- if (cherry.out === 'N') return 1;
55
+ // 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
56
+ const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
57
+ if (!cherry.ok) return 1;
58
+ if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
57
59
  return 0;
58
60
  }
59
61
 
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ require('./silence-popup');
3
+ /**
4
+ * 收尾合并门禁 —— 废弃分支出口(ADR 0007)
5
+ *
6
+ * 用户在门禁处确认"分支废弃、不再交付"后,把各参与仓库远端的需求分支重命名为
7
+ * <branch>-deprecated(保留历史、去掉占用中的旧名),**不删除任何提交**。
8
+ * 本地分支保持原名不动;未推送的仓库(远端无该分支)跳过。
9
+ *
10
+ * 用法:
11
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ *
13
+ * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
+ * 末行:DEPRECATE_RESULT [...]
15
+ */
16
+
17
+ const { execSync } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ function run(cmd, cwd) {
22
+ try {
23
+ return {
24
+ ok: true,
25
+ out: execSync(cmd, {
26
+ cwd,
27
+ encoding: 'utf-8',
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ timeout: 120000,
30
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
31
+ }).trim(),
32
+ };
33
+ } catch (e) {
34
+ return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
35
+ }
36
+ }
37
+
38
+ function loadApps(wsRoot) {
39
+ try {
40
+ const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
41
+ return Array.isArray(config.apps) ? config.apps : [];
42
+ } catch (e) {
43
+ return [];
44
+ }
45
+ }
46
+
47
+ /** 对单个仓库执行废弃重命名 */
48
+ function deprecateRepo(name, repo, branch) {
49
+ const newName = branch + '-deprecated';
50
+ const rec = { name, status: 'skipped', detail: null };
51
+ if (!fs.existsSync(path.join(repo, '.git'))) {
52
+ rec.detail = 'no-repo';
53
+ return rec;
54
+ }
55
+ const remote = run('git remote', repo);
56
+ if (!remote.ok || !remote.out) {
57
+ rec.detail = 'no-remote';
58
+ return rec;
59
+ }
60
+ const r = remote.out.split(/\r?\n/)[0];
61
+ // 仅重命名远端确实存在的分支
62
+ const exists = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
63
+ if (!exists.ok || !exists.out) {
64
+ rec.detail = 'remote-has-no-branch';
65
+ return rec;
66
+ }
67
+ const pushNew = run('git push "' + r + '" "refs/heads/' + branch + ':refs/heads/' + newName + '"', repo);
68
+ if (!pushNew.ok) {
69
+ rec.status = 'failed';
70
+ rec.detail = 'push-new-failed: ' + (pushNew.err || '').split(/\r?\n/)[0];
71
+ return rec;
72
+ }
73
+ const delOld = run('git push "' + r + '" --delete "' + branch + '"', repo);
74
+ if (!delOld.ok) {
75
+ rec.status = 'failed';
76
+ rec.detail = 'delete-old-failed: ' + (delOld.err || '').split(/\r?\n/)[0];
77
+ return rec;
78
+ }
79
+ rec.status = 'deprecated';
80
+ rec.detail = 'remote ' + branch + ' → ' + newName;
81
+ return rec;
82
+ }
83
+
84
+ function main() {
85
+ const args = process.argv.slice(2);
86
+ const wsRoot = args[0];
87
+ const branch = args[1];
88
+
89
+ if (!wsRoot || !branch || branch.indexOf('--') === 0) {
90
+ console.error('Usage: node deprecate-branches.js <ws_root> <branch> [--apps a,b]');
91
+ process.exit(1);
92
+ }
93
+
94
+ const appsIdx = args.indexOf('--apps');
95
+ const appFilter = appsIdx >= 0
96
+ ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
+ : null;
98
+
99
+ const repos = [{ name: 'workspace', repo: wsRoot }];
100
+ for (const app of loadApps(wsRoot)) {
101
+ if (appFilter && !appFilter.has(app.name)) continue;
102
+ repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
+ }
104
+
105
+ const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
+
107
+ results.forEach(r => {
108
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : ''));
109
+ });
110
+
111
+ const ok = results.every(r => r.status !== 'failed');
112
+ console.log('---');
113
+ console.log('DEPRECATE_RESULT', JSON.stringify({ branch, new_name: branch + '-deprecated', results }));
114
+ process.exit(ok ? 0 : 2);
115
+ }
116
+
117
+ if (require.main === module) main();
118
+
119
+ module.exports = { run, loadApps, deprecateRepo };