@jspg-ai/coding-bb 0.0.3-beta.21 → 0.0.3-beta.22
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.
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +21 -14
- package/cbb/lib/install/init.js +29 -1
- package/cbb/lib/utils/githook.js +224 -0
- package/cbb/lib/utils/gitignore.js +5 -1
- package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +127 -0
- package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +158 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +9 -6
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +75 -48
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +85 -91
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +31 -6
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +23 -3
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +127 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +5 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +17 -5
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +5 -5
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +48 -12
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +45 -20
- package/config/workspace-agents.sample.md +50 -45
- package/package.json +2 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cbb-worktree-init
|
|
3
|
-
description: "为一个需求在 workspace
|
|
3
|
+
description: "为一个需求在 workspace 与各关联应用仓库同步创建同名物理 git worktree,直聚在 `.worktrees/worktree-<需求名>/` 下,并把 AI 配置以 junction 链入工作树(开发会话开进 worktree)。触发:用户明确要求创建需求工作空间/开发隔离环境/并行开发,或双模式选择时用户选 worktree 模式,或为已有需求补齐新增应用(重跑本技能即幂等)。单线程快改且不需要隔离时引导用 cbb-local-mode(本地模式)。禁止自动串联 cbb-worktree-close——除非用户明确说了要关闭。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Init Worktree(跨平台脚本化版本
|
|
6
|
+
# Init Worktree(跨平台脚本化版本 v3.0)
|
|
7
7
|
|
|
8
|
-
为一个需求在 workspace 与各关联应用仓库同步创建同名 git worktree,物理直聚在 `.worktrees/worktree-<需求名>/`
|
|
8
|
+
为一个需求在 workspace 与各关联应用仓库同步创建同名 git worktree,物理直聚在 `.worktrees/worktree-<需求名>/` 目录下。当你需要跨多个微服务仓库并行开发某个需求、或需要隔离环境时,本技能一次性完成工作空间和关联应用仓库的 worktree 创建(ADR 0002 worktree 模式)。
|
|
9
9
|
|
|
10
10
|
**启动时声明:** "我正在使用 cbb-worktree-init 技能来创建多仓库联动的隔离 worktree 环境。"
|
|
11
11
|
|
|
@@ -13,10 +13,13 @@ description: "为一个需求在 workspace 与各关联应用仓库(= workspac
|
|
|
13
13
|
|
|
14
14
|
- 本技能的所有原子操作封装为 `scripts/*.js` Node.js 脚本,**跨平台执行**(Windows PowerShell / macOS bash / Linux bash / Git Bash 均无需适配)
|
|
15
15
|
- 物理位置与 `init-worktree` 兼容:`.worktrees/worktree-<需求名>/<应用名>/`
|
|
16
|
-
- 所有仓库使用统一的分支名(不加前缀);worktree 目录名自动加 `worktree-`
|
|
16
|
+
- 所有仓库使用统一的分支名(不加前缀);worktree 目录名自动加 `worktree-` 前缀;**分支基线统一取 `origin/<main>`(ADR 0002,不取 HEAD)**
|
|
17
|
+
- **lazy push(ADR 0002)**:本技能**不向远端推送任何分支**——远端副作用收敛到 `cbb-worktree-push` 唯一出口;推送失败早暴露的职责由 push 技能的 dry-run 承担
|
|
18
|
+
- **AI 配置 junction(ADR 0002 防线 B)**:创建 worktree 后把根的 AI 配置**内容子目录**(rules / skills / commands)junction 进工作树——开发会话开在 `<wt_path>`(上游 `/opsx:*` 相对路径原样生效,零翻译);工具状态落在工作树自建空壳目录,不互相污染;`cbb update` 在根跑一次全 worktree 生效
|
|
17
19
|
- **本技能必须在业务工作空间根目录执行**(即由 `cbb setup` 初始化、包含 `workspace-config.json` 的目录)
|
|
18
20
|
- 输入约定:**自由文本**(需求名等)直接在对话中提问并等待回复,禁止使用 `AskUserQuestion`;`AskUserQuestion` 仅用于固定选项决策场景(如 push 失败处理、远程分支已存在时的二选一)
|
|
19
|
-
-
|
|
21
|
+
- 关联应用默认为 `workspace-config.json` 的**全量 apps**;支持按需求语境传入**应用子集**(见 Step 1),需要增减应用时编辑该文件并重跑本技能(幂等)
|
|
22
|
+
- **模式选择(ADR 0002)**:接到需求先判断用户要哪种模式——要**并行 / 隔离** → 本技能;**单线程快改、当前会话就地干** → 引导 `cbb-local-mode`。用户明确指定即遵从,agent 只给推荐不代决
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -140,8 +143,8 @@ worktree 需要知道要关联哪些应用仓库。
|
|
|
140
143
|
|
|
141
144
|
## Step 1:关联应用与代码同步
|
|
142
145
|
|
|
143
|
-
> ⓘ **关联应用 =
|
|
144
|
-
> 需要增减应用时,编辑 `workspace-config.json`
|
|
146
|
+
> ⓘ **关联应用 = 需求涉及的应用子集**:从需求语境提取("改订单导出" → 订单相关应用);纯 workspace 内容的需求 → 仅 workspace。子集判断错了零代价——重跑本技能幂等补齐(已存在的分支/worktree 自动复用,详见「幂等重跑」章节)。涉及全部应用 / 不确定时用全量。
|
|
147
|
+
> 需要增减应用时,编辑 `workspace-config.json` 后**重跑本技能**即可。
|
|
145
148
|
|
|
146
149
|
### 1.1 同步 `.codespace/` 目录代码
|
|
147
150
|
|
|
@@ -151,20 +154,20 @@ node "<skill_dir>/scripts/sync-repos.js" "<ws_root>"
|
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
**用法**:
|
|
154
|
-
- `<ws_root>`:工作空间根绝对路径(apps 由脚本内部从 `<ws_root>/workspace-config.json`
|
|
157
|
+
- `<ws_root>`:工作空间根绝对路径(apps 由脚本内部从 `<ws_root>/workspace-config.json` 读取,全量 fetch/clone)
|
|
155
158
|
|
|
156
159
|
**解读输出**:
|
|
157
160
|
- 退出码 0 + 末行 `SYNC_RESULT [...]` → 全部成功
|
|
158
161
|
- 退出码 2 + 末行 `SYNC_RESULT [...]`(含 `clone-failed` 项)→ 部分 clone 失败,**终止流程**
|
|
159
162
|
|
|
160
|
-
> ⓘ **`.codespace/`
|
|
163
|
+
> ⓘ **`.codespace/` 目录**:各应用的**标准本地检出**(基准 = `origin/main`,ADR 0002)。首次由 `cbb setup` clone,本脚本如已存在则 fetch 更新(秒级),不存在则 clone 兜底(并补装提交防线 hook)。该目录已 gitignore,不被 workspace 仓库跟踪。本地模式进行中本脚本仍可安全执行(fetch 不动工作区/分支)。
|
|
161
164
|
|
|
162
165
|
### 1.2 最终输出
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
列出本次关联的应用(按 Step 1 确定的子集)及本地路径(保留序号便于复核),向用户展示并确认:
|
|
165
168
|
|
|
166
169
|
```
|
|
167
|
-
|
|
170
|
+
确认关联应用(需求 <需求名> 涉及子集):
|
|
168
171
|
后端:
|
|
169
172
|
[1] ep-user-auth-app → .codespace/ep-user-auth-app
|
|
170
173
|
[2] ep-user-info-server → .codespace/ep-user-info-server
|
|
@@ -172,7 +175,7 @@ node "<skill_dir>/scripts/sync-repos.js" "<ws_root>"
|
|
|
172
175
|
[3] ep-user-web → .codespace/ep-user-web
|
|
173
176
|
```
|
|
174
177
|
|
|
175
|
-
> 后续所有步骤中的 `APP_REPOS` 均指向 `$WS_ROOT/.codespace
|
|
178
|
+
> 后续所有步骤中的 `APP_REPOS` 均指向 `$WS_ROOT/.codespace/<应用名>/`,不再依赖开发者手动配置本地路径。**子集应用名单由 AI 在对话上下文维护,经 `--apps` 参数传给 Step 3.3 / 3.5 脚本(逗号分隔);不传 = 全量。**
|
|
176
179
|
|
|
177
180
|
---
|
|
178
181
|
|
|
@@ -235,62 +238,45 @@ node "<skill_dir>/scripts/update-gitignore.js" "<ws_root>" "<branch>"
|
|
|
235
238
|
### 3.3 创建分支
|
|
236
239
|
|
|
237
240
|
```bash
|
|
238
|
-
node "<skill_dir>/scripts/create-branches.js" "<ws_root>" "<branch>"
|
|
241
|
+
node "<skill_dir>/scripts/create-branches.js" "<ws_root>" "<branch>" [--apps "app1,app2"]
|
|
239
242
|
```
|
|
240
243
|
|
|
244
|
+
- `--apps`:Step 1 确定的应用子集(逗号分隔);缺省 = 全量
|
|
245
|
+
|
|
241
246
|
**解读输出**:
|
|
242
247
|
- 退出码 0 + 末行 `BRANCH_RESULT [...]` → 全部创建或复用成功
|
|
243
248
|
- 退出码 2 + 末行 `BRANCH_RESULT [...]`(含 `failed` 项)→ 部分失败,**终止流程**
|
|
244
249
|
|
|
245
250
|
**报告示例**:
|
|
246
251
|
```
|
|
247
|
-
✅ workspace: 分支 feature-login
|
|
252
|
+
✅ workspace: 分支 feature-login 创建成功(from origin/main)
|
|
248
253
|
⏭️ ep-user-auth-app: 分支已存在,复用
|
|
249
254
|
✅ ep-user-info-server: 分支 feature-login 创建成功(从 origin/main)
|
|
250
255
|
```
|
|
251
256
|
|
|
252
|
-
> ⓘ
|
|
257
|
+
> ⓘ **统一从 `origin/main` 创建分支(ADR 0002)**:workspace 与 app 仓库的分支基线均为 `origin/<main>`(Step 2 已 fetch 刷新)——不取 HEAD,本地模式进行中重跑本技能不被根检出状态污染;与 app 仓库当前在哪个分支、是否有未提交修改完全解耦。
|
|
253
258
|
|
|
254
|
-
> ⚠️ 必须用 `git branch` 而非 `git checkout -b`:因为同一分支不能同时在主目录和 worktree 中检出,切换后 Step 3.
|
|
259
|
+
> ⚠️ 必须用 `git branch` 而非 `git checkout -b`:因为同一分支不能同时在主目录和 worktree 中检出,切换后 Step 3.5 的 `git worktree add` 会报 `fatal: '<branch>' is already checked out`。
|
|
255
260
|
|
|
256
261
|
**情况 B:任一仓库已存在该分支 → 复用**
|
|
257
262
|
- 脚本自动跳过已存在分支(`status: 'reused'`),不阻塞流程
|
|
258
263
|
- 不再提供「复用/修改/中断」选项(脚本自动处理)
|
|
264
|
+
- 本地模式遗留的分支同样直接复用(本地升级 worktree 场景:提交随身带走)
|
|
259
265
|
|
|
260
|
-
### 3.4
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
node "<skill_dir>/scripts/push-branches.js" "<ws_root>" "<branch>"
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
脚本内部自动完成:
|
|
267
|
-
1. **远程探测**:每个仓库先 `git remote` 确认存在远程名
|
|
268
|
-
2. **推送**:执行 `git push -u <remote> <branch>` 推送并建立 upstream
|
|
269
|
-
3. **错误归类**:stderr 含 `Permission denied (publickey)` / `Authentication failed` / `Could not resolve host` / `protected branch` / `already exists` / `non-fast-forward` 时归类为对应错误码
|
|
270
|
-
4. **远程分支已存在**:自动采集 `git rev-parse` + `git log -1` 元信息(hash/author/date/message),附加到 PUSH_RESULT
|
|
271
|
-
|
|
272
|
-
**解读输出:**
|
|
273
|
-
- 退出码 0 + 末行 `PUSH_RESULT [...]` → 全部推送成功,继续 Step 3.5
|
|
274
|
-
- 结果项 `skipped`(远程分支已存在且与本地一致)→ 幂等跳过,非失败
|
|
275
|
-
- 退出码 2 + 末行 `PUSH_RESULT [...]`(含 `failed` 或 `remote-exists` 项)→ **阻塞流程,AI agent 必须询问用户决策**(见下方"失败处理")
|
|
276
|
-
|
|
277
|
-
**失败处理(AI agent 行为):**
|
|
278
|
-
|
|
279
|
-
| 错误码 | 处理 |
|
|
280
|
-
|--------|------|
|
|
281
|
-
| `ssh-auth-failed` / `https-auth-failed` / `network-unreachable` / `branch-protected` / `no-remote` / `non-fast-forward` / `unknown` | 输出失败明细,按 error 码分组汇总。AskUserQuestion 提供三个选项:1) 修复后重试(推荐) 2) 跳过失败仓库继续 3) 完全终止 |
|
|
282
|
-
| `remote-exists` | 先在纯文本中输出远程 commit 元信息 + 本地 HEAD hash(**二次确认**),再 AskUserQuestion 提供两个选项:1) 关联到 origin/<branch>(执行 set-upstream-to) 2) 终止流程,关闭需求空间后重新创建 |
|
|
266
|
+
### 3.4 分支就位(lazy push,不推远端)
|
|
283
267
|
|
|
284
|
-
|
|
268
|
+
**本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → 用户确认 commit 信息 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
|
|
285
269
|
|
|
286
|
-
|
|
270
|
+
> 旧版在本步骤推送空分支建立 upstream(`push-branches.js`),自 0.0.3-beta.22 起移除:避免每个需求在远端留下空分支尸体;`push-worktrees.js` 首次推送时自动建立 upstream,行为等价。
|
|
287
271
|
|
|
288
272
|
### 3.5 创建 Worktree
|
|
289
273
|
|
|
290
274
|
```bash
|
|
291
|
-
node "<skill_dir>/scripts/create-worktrees.js" "<ws_root>" "<branch>"
|
|
275
|
+
node "<skill_dir>/scripts/create-worktrees.js" "<ws_root>" "<branch>" [--apps "app1,app2"]
|
|
292
276
|
```
|
|
293
277
|
|
|
278
|
+
- `--apps`:与 Step 3.3 相同的应用子集;缺省 = 全量
|
|
279
|
+
|
|
294
280
|
脚本内部自动完成:
|
|
295
281
|
1. **元数据自愈**:仅 `git worktree prune`(清理"目录已缺失/失效"的注册项);**绝不删除任何物理目录**(0.0.3-beta.11 起——旧版"清理无元数据残留目录"因 Windows 路径分隔符不匹配会误删活跃 worktree,已整体移除)
|
|
296
282
|
2. **workspace worktree 创建**:`git worktree add .worktrees/worktree-<branch> <branch>`
|
|
@@ -310,9 +296,23 @@ node "<skill_dir>/scripts/create-worktrees.js" "<ws_root>" "<branch>"
|
|
|
310
296
|
WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
311
297
|
```
|
|
312
298
|
|
|
313
|
-
> ⓘ **tracked 内容自动 checkout**:workspace 主分支 tracked 的所有内容(openspec/、cbb/ 等)在 workspace 工作树创建时**自动从 git 对象库 checkout**——**不需要 `cp`、`git clone`
|
|
299
|
+
> ⓘ **tracked 内容自动 checkout**:workspace 主分支 tracked 的所有内容(openspec/、cbb/ 等)在 workspace 工作树创建时**自动从 git 对象库 checkout**——**不需要 `cp`、`git clone` 或任何"带过来"动作**。各应用 worktree 同理。
|
|
314
300
|
|
|
315
|
-
|
|
301
|
+
### 3.6 链入 AI 配置(junction,ADR 0002 防线 B)
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
脚本把根的 AI 配置**内容子目录**(`.claude/`、`.qoder/`、`.opencode/`、`.workbuddy/`、`.trae/` 下的 rules / skills / commands)junction 进工作树;`.opencode/opencode.json` 为文件则复制。
|
|
308
|
+
|
|
309
|
+
**解读末行 `JUNCTION_RESULT`**:
|
|
310
|
+
- 退出码 0 → linked / copied / skipped(幂等)任一组合均成功
|
|
311
|
+
- 退出码 2 → junction 失败(权限/文件系统),报告后继续——**AI 配置缺失不阻塞流程**,开发会话仍可退回"根会话 + 显式路径"模式(hook 会拦住写错位置的 commit)
|
|
312
|
+
|
|
313
|
+
> ⓘ **为何只 junction 内容子目录**:工具向配置根写状态(如 `.claude/settings.local.json`)落在工作树自建空壳目录,按项目隔离不串扰(ADR 0001 否决整目录链接的污染点由此规避);内容唯一物理副本仍在根——`cbb update` 跑一次,全部 worktree 经 junction 即时生效。
|
|
314
|
+
|
|
315
|
+
> ⓘ **AI 配置目录形态**:worktree 内 `<wt_path>/.claude/rules` 等是链接(指向根),`.claude/` 本身是物理目录(装工具状态)。**勿在 worktree 内手动改 rules/skills/commands**——改的是根的同一份;规范变更应走 `cbb update`。
|
|
316
316
|
|
|
317
317
|
---
|
|
318
318
|
|
|
@@ -320,8 +320,8 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
320
320
|
|
|
321
321
|
不执行脚本。AI agent 向用户输出最终报告(格式见「最终报告」),并完成以下交接声明:
|
|
322
322
|
|
|
323
|
-
1.
|
|
324
|
-
2.
|
|
323
|
+
1. **开发会话开进 worktree**:新开 IDE 窗口 / agent 会话于 `<wt_path>`(AI 配置已 junction 就位,`/opsx:*` 等命令与技能在该会话可用);本(根)会话保留作控制面——后续创建/关闭 worktree、`cbb update` 等空间级操作在此执行
|
|
324
|
+
2. **目录纪律(worktree 会话内自然成立)**:会话 cwd == 开发目录,上游相对路径原样生效;**分支感知 hook 已覆盖全部仓库**——任何落 main 的载荷提交会被拦截(大声失败是防线,不是故障)
|
|
325
325
|
3. 不自动安装依赖:各仓库技术栈不一(Maven 依赖本机 `~/.m2` 全局共享、无需初始化;Node/Python 等由你进入 worktree 后执行正确的命令,如 `npm ci` / `pnpm`)
|
|
326
326
|
4. **输出下一步建议**:最终报告必须包含「下一步建议」段(格式见「最终报告」),告知用户后续可走的流程;仅提示,不自动串联执行任何后续技能
|
|
327
327
|
|
|
@@ -333,31 +333,34 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
333
333
|
|
|
334
334
|
```
|
|
335
335
|
✅ worktree 已创建:<wt_path>
|
|
336
|
-
关联应用:<应用名 → 嵌套路径>
|
|
337
|
-
|
|
336
|
+
关联应用:<应用名 → 嵌套路径> 列表(应用子集或全量)
|
|
337
|
+
🔗 AI 配置已 junction 就位:rules / skills / commands(cbb update 一处生效)
|
|
338
|
+
📌 开发会话请开在 <wt_path>(新开 IDE 窗口 / agent 会话);本根会话保留作控制面
|
|
338
339
|
👉 下一步建议:
|
|
339
|
-
1.
|
|
340
|
+
1. 开始需求开发(开发会话内,二选一):
|
|
340
341
|
- 走 OpenSpec 工作流:/opsx:propose → /opsx:apply → /opsx:verify → /opsx:archive
|
|
341
342
|
- 或直接对话开发:把需求告诉我,在 <wt_path> 内实现
|
|
342
|
-
2. 开发中提交推送:说"提交并推送"(cbb-worktree-push)
|
|
343
|
+
2. 开发中提交推送:说"提交并推送"(cbb-worktree-push;首次 push 自动建立 upstream)
|
|
343
344
|
3. 需求完成后:说"关闭工作空间"(cbb-worktree-close)
|
|
344
345
|
```
|
|
345
346
|
|
|
346
|
-
> 若 Step 3.5 为部分失败(退出码 3),报告必须列出已创建/失败详情与回滚清理命令。
|
|
347
|
+
> 若 Step 3.5 为部分失败(退出码 3),报告必须列出已创建/失败详情与回滚清理命令。
|
|
347
348
|
|
|
348
349
|
---
|
|
349
350
|
|
|
350
|
-
## 幂等重跑(增减应用 /
|
|
351
|
+
## 幂等重跑(增减应用 / 中断恢复 / 本地模式升级)
|
|
351
352
|
|
|
352
353
|
本技能可安全重跑:在业务工作空间根目录用**同一个需求名**再执行一次即可。
|
|
353
354
|
|
|
354
355
|
| 场景 | 重跑行为 |
|
|
355
356
|
|------|----------|
|
|
356
|
-
| 空间新增了应用(编辑 `workspace-config.json` 后) | Step 1 拉取新应用代码 → Step 3.3 为新应用建分支 → Step 3.
|
|
357
|
+
| 空间新增了应用(编辑 `workspace-config.json` 后) | Step 1 拉取新应用代码 → Step 3.3 为新应用建分支 → Step 3.5 创建缺失的嵌套 worktree → Step 3.6 补 junction |
|
|
357
358
|
| 上次 init 中途失败 | 已存在的分支 / worktree 自动复用,`.gitignore` 处理自动跳过,仅补齐缺失部分 |
|
|
358
359
|
| 应用从 config 移除 | 已创建的分支 / worktree **不会**自动删除;如需清理用 `cbb-worktree-close` skill 关闭后重建 |
|
|
360
|
+
| **本地模式升级 worktree**(ADR 0002) | Step 3.3 分支已存在 → reused(本地阶段提交随身带走);先 `git checkout main` 腾出各仓库本地检出,再重跑本技能 |
|
|
361
|
+
| 本地模式进行中重跑 | 安全:分支基线取 `origin/main`(非 HEAD),不受根/应用检出状态影响;fetch 不动工作区 |
|
|
359
362
|
|
|
360
|
-
> ⓘ
|
|
363
|
+
> ⓘ 本技能**零远端副作用**(lazy push):不存在"重跑推送开发中提交"的风险面——推送只发生在 `cbb-worktree-push`。
|
|
361
364
|
|
|
362
365
|
---
|
|
363
366
|
|
|
@@ -365,18 +368,18 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
365
368
|
|
|
366
369
|
| 场景 | 处理方式 |
|
|
367
370
|
|------|----------|
|
|
368
|
-
| `.worktrees/`
|
|
371
|
+
| `.worktrees/` / `.codespace/` 未在 `.gitignore` | `cbb setup` / `cbb update` 已写入(0.0.3-beta.22 起含 `.worktrees/`);存量空间由 Step 3.2 补写提交 |
|
|
369
372
|
| `git worktree add` 权限被拒 | 运行环境降级,中断并告知用户 |
|
|
370
373
|
| 状态检查任一失败 | 终止并报告所有问题 |
|
|
371
374
|
| 部分应用 worktree 创建失败 | 提示已创建的回滚清理命令 |
|
|
372
375
|
| workspace 主分支 tracked 内容(openspec/、cbb/ 等) | 自动 checkout 到工作树,无需额外动作(cp/clone 都是多余的) |
|
|
373
|
-
| AI 配置目录(.claude/、.qoder/、.opencode/ 等) |
|
|
376
|
+
| AI 配置目录(.claude/、.qoder/、.opencode/ 等) | Step 3.6 junction 内容子目录进工作树;唯一物理副本在空间根,`cbb update` 一处生效 |
|
|
374
377
|
| 需求名(同时作分支名和工作树目录名) | 用户在对话中纯文本输入,参考 Step 3.1 命名规则(kebab-case,禁 `..`、路径分隔符、空格) |
|
|
375
|
-
| 关联应用范围 |
|
|
378
|
+
| 关联应用范围 | 需求语境提取子集(`--apps` 传入,缺省全量);增减应用后重跑本技能(幂等) |
|
|
379
|
+
| 单线程快改 / 不需要隔离 | 引导 cbb-local-mode(本地模式,就地切分支) |
|
|
376
380
|
| 运行环境 node 不可用 | 脚本 fail-fast 并提示用户到系统终端重试 |
|
|
377
|
-
|
|
|
378
|
-
|
|
|
379
|
-
| 开发过程中推送所有 worktree | Step 3.4 仅覆盖创建时的空 commit;开发过程中产生的提交必须调 `cbb-worktree-push` skill 推送(详见该 skill) |
|
|
381
|
+
| 推送(认证/网络/保护分支/远程已存在) | 不在本技能——`cbb-worktree-push` 处理(lazy push,ADR 0002) |
|
|
382
|
+
| 开发过程中推送所有 worktree | 对 cbb-worktree-push 说"提交并推送";首次 push 自动建立 upstream |
|
|
380
383
|
|
|
381
384
|
---
|
|
382
385
|
|
|
@@ -422,22 +425,12 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
422
425
|
- **问题:** 需求名需要用户自由输入文本,却用了 `AskUserQuestion` 工具,被迫提供预设选项
|
|
423
426
|
- **修复:** 自由文本输入场景一律**直接在对话中提问**并等待用户文本回复;`AskUserQuestion` 仅用于固定选项的选择场景
|
|
424
427
|
|
|
425
|
-
###
|
|
426
|
-
|
|
427
|
-
- **问题:** 流程结束后仍停留在工作空间根目录,或跨多次 Bash 调用后丢失 cwd,后续命令作用在错误上下文
|
|
428
|
-
- **修复:** Step 5 起的所有命令用 `cd "<worktree 绝对路径>" && <cmd>` 单次 Bash 调用发起,首条命令后接 `pwd` 确认;最终报告首行输出 `当前目录`
|
|
428
|
+
### 完成后开发会话未开进 worktree
|
|
429
429
|
|
|
430
|
-
|
|
430
|
+
- **问题:** 流程结束后开发会话仍停留在工作空间根目录(junction 好的 AI 配置没人加载),agent 在根会话里靠路径翻译干活,回到第一代失败模式
|
|
431
|
+
- **修复:** Step 4 交接必须明确"开发会话开在 `<wt_path>`"(新 IDE 窗口 / agent 会话);根会话保留作控制面(worktree 生灭、`cbb update`)
|
|
431
432
|
|
|
432
|
-
|
|
433
|
-
- **修复:** push-branches.js 退出码 2 时必须阻塞流程并询问用户;不允许脚本或 AI 自动跳过失败仓库
|
|
434
|
-
|
|
435
|
-
### 远程分支已存在时自动 set-upstream
|
|
436
|
-
|
|
437
|
-
- **问题:** 看到 origin/<branch> 已存在就直接 `git branch --set-upstream-to`,可能关联到他人的同名分支
|
|
438
|
-
- **修复:** 与本地完全一致时脚本幂等跳过(`skipped`,非失败);不一致时输出 metadata 由用户决策(关联 vs 终止);AI 在 AskUserQuestion 之前必须先输出 commit 元信息作为二次确认
|
|
439
|
-
|
|
440
|
-
### 用 `git checkout -b` 在 push 后切分支
|
|
433
|
+
### 用 `git checkout -b` 在创建 worktree 前切分支
|
|
441
434
|
|
|
442
435
|
- **问题:** Step 3.5 创建 worktree 前先切到新分支,导致 worktree 创建时分支已检出冲突
|
|
443
436
|
- **修复:** Step 3.3 已统一用 `git branch`(不切换);Step 3.5 的 `git worktree add` 会自动处理 checkout
|
|
@@ -445,12 +438,13 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
445
438
|
### 把脚本调用与 Shell 变量混用
|
|
446
439
|
|
|
447
440
|
- **问题:** 试图在 Bash 调用中嵌套 Node 脚本与 shell 变量,导致变量解析失败
|
|
448
|
-
- **修复:** 所有脚本参数(`<ws_root>`、`<branch
|
|
441
|
+
- **修复:** 所有脚本参数(`<ws_root>`、`<branch>`、`--apps` 列表)由 AI agent 在对话上下文中维护,**作为字符串参数传入脚本**,不要在 Bash 命令中拼 shell 变量
|
|
449
442
|
|
|
450
|
-
###
|
|
443
|
+
### 误把 hook 拦截当故障
|
|
444
|
+
|
|
445
|
+
- **问题:** 开发会话在 main 上误打载荷 commit 被分支感知 hook 拦截,agent 以为是环境故障,尝试绕过或反复重试
|
|
446
|
+
- **修复:** 拦截是防线(ADR 0002)的正确行为——回需求分支重做;`--no-verify` 绕过仅限用户显式要求
|
|
451
447
|
|
|
452
|
-
- **问题:** worktree 创建成功后,会话仍在空间根运行 git commit / openspec / 构建命令,污染主分支或把变更产物生成在主分支上
|
|
453
|
-
- **修复:** Step 4 交接已声明需求工作目录 `<wt_path>`;所有写操作命令必须显式 `cd` 或用绝对路径指向 worktree
|
|
454
448
|
|
|
455
449
|
---
|
|
456
450
|
|
|
@@ -465,19 +459,17 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
465
459
|
- 用 `git checkout -b` 切分支后再建 worktree(分支检出冲突)
|
|
466
460
|
- 用 `AskUserQuestion` 收集自由文本(需求名 / 分支选项等)
|
|
467
461
|
- 运行环境 node 不可用时强行继续 → 改为 fail-fast 提示用户
|
|
468
|
-
-
|
|
469
|
-
-
|
|
470
|
-
- **AI 询问关联选项前不输出 commit 元信息** → 必须先在 AskUserQuestion 之前输出远程 hash/author/date/message + 本地 hash
|
|
471
|
-
- **在空间根执行 git/openspec/构建等写操作命令** → 必须显式指定需求工作树路径(`<wt_path>` 或其应用子目录)
|
|
462
|
+
- **在本技能内向远端推送任何分支**(lazy push,ADR 0002)——推送只发生在 `cbb-worktree-push`
|
|
463
|
+
- **hook 拦截 main 载荷提交后用 `--no-verify` 绕过**——仅限用户显式要求
|
|
472
464
|
|
|
473
465
|
**必须执行**
|
|
474
466
|
|
|
475
467
|
- workspace 仓库标准检查(当前在主分支等),app 仓库轻量检查(fetch + `origin/main` 可用即可)
|
|
476
|
-
-
|
|
477
|
-
- 严格 Step 3.3 → Step 3.
|
|
478
|
-
-
|
|
479
|
-
-
|
|
480
|
-
-
|
|
468
|
+
- 所有仓库分支统一从 `origin/<main>` 创建(`git branch <需求名> ${REMOTE}/${MAIN}`),不依赖本地检出状态(本地模式进行中重跑不受污染)
|
|
469
|
+
- 严格 Step 3.3 → Step 3.5 顺序(先统一创建分支,再统一创建 worktree;workspace 工作树先建,各应用按序补建)
|
|
470
|
+
- Step 3.6 junction AI 配置(失败不阻塞,但要报告)
|
|
471
|
+
- 所有脚本调用以 `node "<skill_dir>/scripts/<name>.js" "<绝对路径>" "<branch>"` 形式发起(apps 缺省由脚本内部从 workspace-config.json 读取,子集用 `--apps` 传入);**执行时 cwd 保持在工作空间根**(`check-env.js` / `parse-config.js` 按 cwd 定位)
|
|
472
|
+
- Step 4 交接必须声明"开发会话开在 `<wt_path>`、根会话保留作控制面"
|
|
481
473
|
|
|
482
474
|
---
|
|
483
475
|
|
|
@@ -487,10 +479,12 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
487
479
|
|------|------|------|--------|
|
|
488
480
|
| `check-env.js` | 环境探活 | 无 | 0/1/2 |
|
|
489
481
|
| `parse-config.js` | 解析 workspace-config.json | 无 | 0/1/2 |
|
|
490
|
-
| `sync-repos.js` | 同步 .codespace/
|
|
482
|
+
| `sync-repos.js` | 同步 .codespace/ 仓库(兜底 clone 时补装提交防线 hook) | ws_root | 0/1/2 |
|
|
491
483
|
| `check-repos.js` | 状态检查 | ws_root | 0/1/2 |
|
|
492
484
|
| `update-gitignore.js` | 安全更新 .gitignore | ws_root, branch | 0/1/2 |
|
|
493
|
-
| `create-branches.js` |
|
|
494
|
-
| `create-worktrees.js` | 创建 worktree(仅 prune 元数据,不删物理目录) | ws_root, branch | 0/1/2/3 |
|
|
495
|
-
| `
|
|
485
|
+
| `create-branches.js` | 创建分支(统一从 origin/<main> 建) | ws_root, branch, [--apps] | 0/1/2 |
|
|
486
|
+
| `create-worktrees.js` | 创建 worktree(仅 prune 元数据,不删物理目录) | ws_root, branch, [--apps] | 0/1/2/3 |
|
|
487
|
+
| `junction-ai-config.js` | AI 配置内容子目录 junction 进工作树(ADR 0002 防线 B) | ws_root, wt_path | 0/1/2 |
|
|
496
488
|
| `check-env-deep.js` | 环境健康自检(Node 版本 / 权限) | 无 | 0/2 |
|
|
489
|
+
|
|
490
|
+
> `push-branches.js` 自 0.0.3-beta.22 起从本技能流程移除(lazy push,ADR 0002);文件保留作历史参考,不再被任何步骤调用。
|
|
@@ -13,10 +13,17 @@ const wsRoot = process.argv[2];
|
|
|
13
13
|
const branch = process.argv[3];
|
|
14
14
|
|
|
15
15
|
if (!wsRoot || !branch) {
|
|
16
|
-
console.error('Usage: node create-branches.js <ws_root> <branch>');
|
|
16
|
+
console.error('Usage: node create-branches.js <ws_root> <branch> [--apps "app1,app2"]');
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// 可选 --apps 应用子集(逗号分隔;缺省 = 全量,ADR 0002 需求语境子集)
|
|
21
|
+
let appFilter = null;
|
|
22
|
+
const appsFlagIdx = process.argv.indexOf('--apps');
|
|
23
|
+
if (appsFlagIdx >= 0) {
|
|
24
|
+
appFilter = new Set((process.argv[appsFlagIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean));
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
function loadAppsFromConfig() {
|
|
21
28
|
const configPath = path.join(wsRoot, 'workspace-config.json');
|
|
22
29
|
if (!fs.existsSync(configPath)) return null;
|
|
@@ -26,12 +33,25 @@ function loadAppsFromConfig() {
|
|
|
26
33
|
} catch (e) { return null; }
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
36
|
+
const allApps = loadAppsFromConfig();
|
|
37
|
+
if (!allApps) {
|
|
31
38
|
console.error('workspace-config.json missing or apps empty');
|
|
32
39
|
process.exit(1);
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
const apps = appFilter
|
|
43
|
+
? allApps.filter(a => appFilter.has(a.name))
|
|
44
|
+
: allApps;
|
|
45
|
+
|
|
46
|
+
if (appFilter) {
|
|
47
|
+
const unknown = [...appFilter].filter(n => !allApps.some(a => a.name === n));
|
|
48
|
+
if (unknown.length > 0) {
|
|
49
|
+
console.error('❌ --apps 中的应用不在 workspace-config.json:' + unknown.join(', '));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
console.log('ℹ️ 应用子集(' + apps.length + '/' + allApps.length + '):' + apps.map(a => a.name).join(', '));
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
function tryExec(cmd, cwd) {
|
|
36
56
|
try {
|
|
37
57
|
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
@@ -53,15 +73,20 @@ function detectMainBranch(cwd) {
|
|
|
53
73
|
|
|
54
74
|
const results = [];
|
|
55
75
|
|
|
56
|
-
// workspace: create from
|
|
76
|
+
// workspace: create from <remote>/<main> (ADR 0002:基线统一取远端主干,
|
|
77
|
+
// 不依赖本地 HEAD——本地模式进行中根可能检出在 feature 分支,从 HEAD 建会污染新分支基线;
|
|
78
|
+
// 无 remote 时回退 HEAD)
|
|
79
|
+
const wsMain = detectMainBranch(wsRoot);
|
|
80
|
+
const wsRemote = tryExec('git remote', wsRoot);
|
|
81
|
+
const wsBase = (wsRemote.ok && wsRemote.out) ? wsRemote.out.split(/\r?\n/)[0] + '/' + wsMain : 'HEAD';
|
|
57
82
|
const wsCheck = tryExec('git branch --list "' + branch + '"', wsRoot);
|
|
58
83
|
if (wsCheck.ok && wsCheck.out) {
|
|
59
84
|
console.log('Skip workspace: branch exists, reuse');
|
|
60
85
|
results.push({ name: 'workspace', status: 'reused' });
|
|
61
86
|
} else {
|
|
62
|
-
const created = tryExec('git branch "' + branch + '"', wsRoot);
|
|
87
|
+
const created = tryExec('git branch "' + branch + '" "' + wsBase + '"', wsRoot);
|
|
63
88
|
if (created.ok) {
|
|
64
|
-
console.log('OK workspace: branch ' + branch + ' created (from
|
|
89
|
+
console.log('OK workspace: branch ' + branch + ' created (from ' + wsBase + ')');
|
|
65
90
|
results.push({ name: 'workspace', status: 'created' });
|
|
66
91
|
} else {
|
|
67
92
|
console.error('FAIL workspace: branch create failed');
|
|
@@ -29,10 +29,17 @@ const wsRoot = process.argv[2];
|
|
|
29
29
|
const branch = process.argv[3];
|
|
30
30
|
|
|
31
31
|
if (!wsRoot || !branch) {
|
|
32
|
-
console.error('❌ 用法:node create-worktrees.js <ws_root> <branch>');
|
|
32
|
+
console.error('❌ 用法:node create-worktrees.js <ws_root> <branch> [--apps "app1,app2"]');
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// 可选 --apps 应用子集(逗号分隔;缺省 = 全量,ADR 0002 需求语境子集)
|
|
37
|
+
let appFilter = null;
|
|
38
|
+
const appsFlagIdx = process.argv.indexOf('--apps');
|
|
39
|
+
if (appsFlagIdx >= 0) {
|
|
40
|
+
appFilter = new Set((process.argv[appsFlagIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
function loadAppsFromConfig() {
|
|
37
44
|
const configPath = path.join(wsRoot, 'workspace-config.json');
|
|
38
45
|
if (!fs.existsSync(configPath)) return null;
|
|
@@ -42,12 +49,25 @@ function loadAppsFromConfig() {
|
|
|
42
49
|
} catch (e) { return null; }
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
52
|
+
const allApps = loadAppsFromConfig();
|
|
53
|
+
if (!allApps) {
|
|
47
54
|
console.error('❌ workspace-config.json 缺失或 apps 为空');
|
|
48
55
|
process.exit(1);
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
const apps = appFilter
|
|
59
|
+
? allApps.filter(a => appFilter.has(a.name))
|
|
60
|
+
: allApps;
|
|
61
|
+
|
|
62
|
+
if (appFilter) {
|
|
63
|
+
const unknown = [...appFilter].filter(n => !allApps.some(a => a.name === n));
|
|
64
|
+
if (unknown.length > 0) {
|
|
65
|
+
console.error('❌ --apps 中的应用不在 workspace-config.json:' + unknown.join(', '));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.log('ℹ️ 应用子集(' + apps.length + '/' + allApps.length + '):' + apps.map(a => a.name).join(', '));
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
function run(cmd, cwd) {
|
|
52
72
|
try {
|
|
53
73
|
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 向指定 git 仓库安装分支感知 pre-commit hook(ADR 0002)。
|
|
4
|
+
*
|
|
5
|
+
* 真源:cbb/lib/utils/githook.js 的 PRE_COMMIT_SOURCE(本文件为部署到业务空间的自包含副本,
|
|
6
|
+
* 改动必须两处同步——本仓库内改真源后同步本文件,勿只改其一)。
|
|
7
|
+
*
|
|
8
|
+
* 用法(二选一):
|
|
9
|
+
* CLI:node install-git-hook.js <repoPath...> —— 对每个仓库安装,输出 HOOK_RESULT
|
|
10
|
+
* require:installTo(repoPath) → 'installed' | 'updated' | 'skipped-not-git'
|
|
11
|
+
*
|
|
12
|
+
* 供 cbb-worktree-init 的 sync-repos.js 兜底 clone 后调用(cbb setup 已装过,此处幂等补齐)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const HOOK_MARKER = 'managed-by-cbb';
|
|
19
|
+
const HOOK_FILE = 'pre-commit';
|
|
20
|
+
const BACKUP_SUFFIX = '.cbb-backup';
|
|
21
|
+
|
|
22
|
+
// 与 cbb/lib/utils/githook.js 的 buildPreCommitSource() 输出完全一致(构建时注入)
|
|
23
|
+
const PRE_COMMIT_SOURCE = "#!/usr/bin/env node\n// managed-by-cbb v1 —— 分支感知提交防线(ADR 0002)\n// main 上:workspace 仓库只放行控制面白名单,应用仓库全拦;非 main 一律放行。\n// 绕过逃生口仅限用户显式 git commit --no-verify。\nconst { execSync } = require('child_process');\nconst fs = require('fs');\nfunction run(cmd) {\n try { return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }\n catch (e) { return null; }\n}\nconst branch = run('git branch --show-current') || '';\n// 主干名:优先远端 HEAD 指向;探测失败按本地 main/master 存在性回退\nlet main = null;\nconst remotes = run('git remote');\nif (remotes) {\n const r = remotes.split(/\\r?\\n/)[0];\n const sym = run('git symbolic-ref \"refs/remotes/' + r + '/HEAD\"');\n if (sym) main = sym.replace('refs/remotes/' + r + '/', '');\n}\nif (!main) {\n if (run('git rev-parse --verify --quiet refs/heads/main')) main = 'main';\n else if (run('git rev-parse --verify --quiet refs/heads/master')) main = 'master';\n}\n// 非主干(含 detached / 探测失败)→ 放行\nif (!main || branch !== main) process.exit(0);\n\n// 主干上的提交:收集 staged 变更(空提交放行)\nconst staged = run('git diff --cached --name-only') || '';\nconst files = staged.split(/\\r?\\n/).filter(Boolean);\nif (files.length === 0) process.exit(0);\n\n// 仓库类型:workspace 仓库 = 检出含 workspace-config.json\nconst isWorkspace = fs.existsSync('workspace-config.json');\n\nfunction deny(bad) {\n console.error('[cbb-guard] 主干上拦截载荷提交(ADR 0002):');\n bad.forEach(f => console.error(' ' + f));\n console.error('需求载荷须在需求分支上提交:本地模式先 git checkout -b <需求名>;worktree 模式在 .worktrees/ 工作树内提交。');\n console.error('控制面白名单:workspace-config.json / .gitignore / AGENTS.md / openspec/config.yaml / openspec/schemas/** / .cbb/**');\n console.error('如确需在主干提交,须由用户显式 git commit --no-verify。');\n process.exit(1);\n}\n\nif (isWorkspace) {\n const EXACT = new Set(['workspace-config.json', '.gitignore', 'AGENTS.md', 'openspec/config.yaml']);\n const PREFIXES = ['openspec/schemas/', '.cbb/'];\n const bad = files.filter(f => !EXACT.has(f) && !PREFIXES.some(p => f.indexOf(p) === 0));\n if (bad.length) deny(bad);\n} else {\n deny(files);\n}\nprocess.exit(0);\n";
|
|
24
|
+
|
|
25
|
+
function isCbbHookContent(content) {
|
|
26
|
+
return typeof content === 'string' && content.includes(HOOK_MARKER);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveHooksDir(repoPath) {
|
|
30
|
+
const gitDir = path.join(repoPath, '.git');
|
|
31
|
+
if (!fs.existsSync(gitDir)) return null;
|
|
32
|
+
const st = fs.statSync(gitDir);
|
|
33
|
+
if (!st.isDirectory()) return null; // worktree 检出(.git 为文件):hook 由主仓库承担
|
|
34
|
+
return path.join(gitDir, 'hooks');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function installTo(repoPath) {
|
|
38
|
+
const dir = resolveHooksDir(repoPath);
|
|
39
|
+
if (!dir) return 'skipped-not-git';
|
|
40
|
+
|
|
41
|
+
const hookPath = path.join(dir, HOOK_FILE);
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(hookPath)) {
|
|
45
|
+
const prev = fs.readFileSync(hookPath, 'utf-8');
|
|
46
|
+
if (isCbbHookContent(prev)) {
|
|
47
|
+
if (prev !== PRE_COMMIT_SOURCE) fs.writeFileSync(hookPath, PRE_COMMIT_SOURCE);
|
|
48
|
+
return 'updated';
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(hookPath + BACKUP_SUFFIX, prev); // 用户自有 hook:备份后接管
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(hookPath, PRE_COMMIT_SOURCE);
|
|
53
|
+
try { fs.chmodSync(hookPath, 0o755); } catch (_) { /* Windows 无压力 */ }
|
|
54
|
+
return 'installed';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { installTo, isCbbHookContent, PRE_COMMIT_SOURCE };
|
|
58
|
+
|
|
59
|
+
// CLI 形式:node install-git-hook.js <repoPath...>
|
|
60
|
+
if (require.main === module) {
|
|
61
|
+
const targets = process.argv.slice(2);
|
|
62
|
+
if (targets.length === 0) {
|
|
63
|
+
console.error('Usage: node install-git-hook.js <repoPath...>');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const results = targets.map(p => ({ path: p, status: installTo(p) }));
|
|
67
|
+
results.forEach(r => console.log((r.status === 'skipped-not-git' ? '⚠️ ' : '✅ ') + r.path + ' → ' + r.status));
|
|
68
|
+
console.log('HOOK_RESULT', JSON.stringify(results));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|