@jspg-ai/coding-bb 0.0.5-beta.2 → 0.0.5-beta.3
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-workspace-discipline.md +1 -1
- package/cbb/worktrees/_shared/scripts/return-to-trunk.js +184 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +27 -17
- package/cbb/worktrees/skills/cbb-local-mode/scripts/return-to-trunk.js +184 -0
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +1 -1
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@ trigger: always_on
|
|
|
10
10
|
|
|
11
11
|
- 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
|
|
12
12
|
- 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
|
|
13
|
-
- 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR /
|
|
13
|
+
- 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并(临时 worktree 零接触,ADR 0008) / 确认废弃)后方可收尾;交付完成后各参与仓库回主干并删分支(`return-to-trunk.js` + `delete-branches.js`,主干名经远端 HEAD 探测,兼容 main / master)
|
|
14
14
|
|
|
15
15
|
**模式二 · worktree 模式(并行 / 隔离):**
|
|
16
16
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 收尾回主干 —— 交付完成后把各参与仓库的本地检出切回主干并追平(ADR 0007 / 0008)
|
|
5
|
+
*
|
|
6
|
+
* 背景:merge-branches.js 统一零接触通道后,合并在临时 worktree 内完成、
|
|
7
|
+
* 不触碰本地检出(本地主干检出不随交付前移)。本地模式收尾需把各仓库
|
|
8
|
+
* 本地检出从需求分支切回主干——本脚本替代曾经写死 `git checkout main`
|
|
9
|
+
* 的裸命令:主干名逐仓库经远端 HEAD 探测(与 check-local-state.js 同源,
|
|
10
|
+
* 兼容 master 等),不再假设主干叫 main。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]
|
|
14
|
+
*
|
|
15
|
+
* 行为(逐仓库,相互独立、失败不停链):
|
|
16
|
+
* 1. 目录无 .git → SKIP no-repo
|
|
17
|
+
* 2. git fetch --quiet origin(失败仅降级,后续判定退回本地主干)
|
|
18
|
+
* 3. 主干名 detectMain:远端 HEAD → 本地 main → 本地 master → origin/main → origin/master
|
|
19
|
+
* → FAIL cannot-detect-main
|
|
20
|
+
* 4. 当前检出 ≠ 主干:
|
|
21
|
+
* - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
|
|
22
|
+
* - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
|
|
23
|
+
* 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
|
|
24
|
+
* (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
|
|
25
|
+
* 6. 已在主干 → 跳过切回,仍追平(幂等)
|
|
26
|
+
*
|
|
27
|
+
* 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
|
|
28
|
+
* 末行:TRUNK_RESULT [...]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { execSync } = require('child_process');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
35
|
+
function run(cmd, cwd) {
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
out: execSync(cmd, {
|
|
40
|
+
cwd,
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
timeout: 120000,
|
|
44
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
45
|
+
}).trim(),
|
|
46
|
+
};
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasRef(repoPath, ref) {
|
|
53
|
+
return run('git rev-parse --verify --quiet "' + ref + '"', repoPath).ok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadApps(wsRoot) {
|
|
57
|
+
try {
|
|
58
|
+
const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
59
|
+
return Array.isArray(config.apps) ? config.apps : [];
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 主干名:symbolic-ref 远端 HEAD → 本地 main/master → 远端跟踪 origin/main|master → 空串(与 check-local-state.js 同源,回退链更强) */
|
|
66
|
+
function detectMain(repoPath) {
|
|
67
|
+
const remote = run('git remote', repoPath);
|
|
68
|
+
if (remote.ok && remote.out) {
|
|
69
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
70
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
71
|
+
if (symbolic.ok && symbolic.out) return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
72
|
+
}
|
|
73
|
+
if (hasRef(repoPath, 'refs/heads/main')) return 'main';
|
|
74
|
+
if (hasRef(repoPath, 'refs/heads/master')) return 'master';
|
|
75
|
+
if (hasRef(repoPath, 'refs/remotes/origin/main')) return 'main';
|
|
76
|
+
if (hasRef(repoPath, 'refs/remotes/origin/master')) return 'master';
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 检出未提交项数(含未跟踪,不含 ignored);-1 = 状态不可读 */
|
|
81
|
+
function countUncommitted(repoPath) {
|
|
82
|
+
const s = run('git status --porcelain', repoPath);
|
|
83
|
+
if (!s.ok) return -1;
|
|
84
|
+
return s.out ? s.out.split(/\r?\n/).filter(Boolean).length : 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main() {
|
|
88
|
+
const args = process.argv.slice(2);
|
|
89
|
+
const wsRoot = args[0];
|
|
90
|
+
if (!wsRoot || wsRoot.indexOf('--') === 0) {
|
|
91
|
+
console.error('Usage: node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]');
|
|
92
|
+
process.exit(1);
|
|
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
|
+
const dryRun = args.includes('--dry-run');
|
|
99
|
+
|
|
100
|
+
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
101
|
+
for (const app of loadApps(wsRoot)) {
|
|
102
|
+
if (appFilter && !appFilter.has(app.name)) continue;
|
|
103
|
+
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const results = [];
|
|
107
|
+
let failed = false;
|
|
108
|
+
|
|
109
|
+
for (const rp of repos) {
|
|
110
|
+
const rec = { name: rp.name, status: 'ok', main: null, switched: false, uncommitted: 0, reason: null, detail: null };
|
|
111
|
+
if (!fs.existsSync(path.join(rp.repo, '.git'))) {
|
|
112
|
+
rec.status = 'no-repo';
|
|
113
|
+
results.push(rec);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
run('git fetch --quiet origin', rp.repo);
|
|
117
|
+
const main = detectMain(rp.repo);
|
|
118
|
+
if (!main) {
|
|
119
|
+
rec.status = 'failed';
|
|
120
|
+
rec.reason = 'cannot-detect-main';
|
|
121
|
+
results.push(rec);
|
|
122
|
+
failed = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
rec.main = main;
|
|
126
|
+
rec.uncommitted = countUncommitted(rp.repo);
|
|
127
|
+
const current = run('git branch --show-current', rp.repo);
|
|
128
|
+
|
|
129
|
+
if (current.ok && current.out !== main) {
|
|
130
|
+
// detached(out 为空)也需切回——「不在主干」含 detached
|
|
131
|
+
if (dryRun) {
|
|
132
|
+
rec.reason = 'would-switch (from ' + current.out + ')';
|
|
133
|
+
results.push(rec);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (rec.uncommitted > 0) {
|
|
137
|
+
rec.status = 'failed';
|
|
138
|
+
rec.reason = 'dirty-worktree';
|
|
139
|
+
rec.detail = '检出 ' + current.out + ' 上有 ' + rec.uncommitted + ' 项未提交改动,不自动 stash——先处理再重跑';
|
|
140
|
+
results.push(rec);
|
|
141
|
+
failed = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const co = hasRef(rp.repo, 'refs/heads/' + main)
|
|
145
|
+
? run('git checkout --quiet "' + main + '"', rp.repo)
|
|
146
|
+
: run('git checkout -b "' + main + '" --track origin/' + main, rp.repo);
|
|
147
|
+
if (!co.ok) {
|
|
148
|
+
rec.status = 'failed';
|
|
149
|
+
rec.reason = 'checkout-failed';
|
|
150
|
+
rec.detail = (co.err || '').split(/\r?\n/)[0];
|
|
151
|
+
results.push(rec);
|
|
152
|
+
failed = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
rec.switched = true;
|
|
156
|
+
} else if (dryRun) {
|
|
157
|
+
rec.reason = 'already-on-trunk';
|
|
158
|
+
results.push(rec);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
|
|
163
|
+
const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
|
|
164
|
+
if (!pull.ok) {
|
|
165
|
+
rec.status = 'failed';
|
|
166
|
+
rec.reason = 'pull-failed';
|
|
167
|
+
rec.detail = (pull.err || '').split(/\r?\n/)[0];
|
|
168
|
+
failed = true;
|
|
169
|
+
}
|
|
170
|
+
} else if (!rec.reason) {
|
|
171
|
+
rec.reason = 'no-remote-trunk';
|
|
172
|
+
}
|
|
173
|
+
results.push(rec);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
results.forEach(r => {
|
|
177
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.main ? ' → ' + r.main : '') + (r.reason ? ': ' + r.reason : '') + (r.detail ? ' — ' + r.detail : ''));
|
|
178
|
+
});
|
|
179
|
+
console.log('---');
|
|
180
|
+
console.log('TRUNK_RESULT', JSON.stringify({ results }));
|
|
181
|
+
process.exit(failed ? 2 : 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (require.main === module) main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cbb-local-mode
|
|
3
|
-
description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 /
|
|
3
|
+
description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃后回主干 + 删分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Local Mode(本地模式)
|
|
@@ -88,14 +88,15 @@ MR 合并、验收通过后回到本会话收尾。**与 worktree 模式同门
|
|
|
88
88
|
node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
- 解读末行 `MERGE_CHECK_RESULT`:每仓库 `merged`(含 `origin/main` / `main-via-release-*`)/ `unmerged` / `no-branch`
|
|
92
|
-
- 全部 `merged` / `no-branch`(退出码 0)→ 直接进 5.4
|
|
93
|
-
- 存在 `unmerged`(退出码 2
|
|
91
|
+
- 解读末行 `MERGE_CHECK_RESULT`:每仓库 `merged`(含 `origin/main` / `main-via-release-*`)/ `unmerged` / `no-branch`(该仓库无此本地分支,不参与放行)/ `no-repo`(目录缺失,不参与放行)/ `failed`(主干探测或 fetch 失败)
|
|
92
|
+
- 全部 `merged` / `no-branch` / `no-repo`(退出码 0)→ 直接进 5.4
|
|
93
|
+
- 存在 `unmerged`(退出码 2 且状态为 `unmerged`)→ 进 5.3
|
|
94
|
+
- 存在 `failed`(退出码 2,**与 `unmerged` 撞码,先看状态再行动**)→ 这不是"未合并":报告失败仓库与原因(`cannot-detect-main` / `fetch-failed`),修复环境(远端可达性 / 远端默认分支设置)后重跑 5.2;**禁止按未合并走 5.3 门禁**
|
|
94
95
|
|
|
95
96
|
**5.3 门禁三选一**(`AskUserQuestion`,同 close Step 3.4):
|
|
96
97
|
|
|
97
98
|
- **提 MR** → **中止收尾**,引导用户运行 `cbb-worktree-push`(说"提交并推送")生成 MR 预填链接(其 Step 7:目标分支候选交互 + `make-mr-urls.js`)→ 合并完重跑本步
|
|
98
|
-
- **直合并** → `merge-branches.js`(临时 worktree 合并,**不碰本地检出**——被需求分支占用也无需处理;交付后本地主干检出不随之前移,由 5.4 的
|
|
99
|
+
- **直合并** → `merge-branches.js`(临时 worktree 合并,**不碰本地检出**——被需求分支占用也无需处理;交付后本地主干检出不随之前移,由 5.4 的 `return-to-trunk.js` 追平):
|
|
99
100
|
|
|
100
101
|
```bash
|
|
101
102
|
node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
|
|
@@ -104,24 +105,33 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
|
|
|
104
105
|
`need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑
|
|
105
106
|
- **确认废弃** → `deprecate-branches.js`(远程重命名 `<需求名>-deprecated`;本地分支保留原名)
|
|
106
107
|
|
|
107
|
-
**5.4 交付完成 →
|
|
108
|
+
**5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master):
|
|
109
|
+
|
|
110
|
+
1. 各仓库切回主干并追平远端(替代原裸 git 三连;不自动 stash,失败即报告):
|
|
108
111
|
|
|
109
112
|
```bash
|
|
110
|
-
|
|
113
|
+
node "<skill_dir>/scripts/return-to-trunk.js" "<ws_root>" --apps "<app1,app2>"
|
|
111
114
|
```
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
**解读末行 `TRUNK_RESULT`**:
|
|
117
|
+
- 退出码 0 → 全部已回主干并追平
|
|
118
|
+
- 某仓库 `dirty-worktree` → 检出上有未提交改动(回归 5.1 前置,先处理再重跑);**不自动 stash**
|
|
119
|
+
- 某仓库 `pull-failed`(本地主干与 origin 分叉)→ 输出详情,交用户决策
|
|
120
|
+
- 某仓库 `cannot-detect-main` → 检查该仓库远端与主干分支(`fetch-failed` 同理)
|
|
121
|
+
- 幂等:已在主干的仓库仅追平远端,重复执行无害
|
|
122
|
+
|
|
123
|
+
2. 本地 + 远程需求分支删除(本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
|
|
114
124
|
|
|
115
125
|
```bash
|
|
116
126
|
node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
|
|
117
127
|
```
|
|
118
|
-
|
|
128
|
+
|
|
119
129
|
- openspec change 归档在合并前执行(产物在分支上随交付合入)
|
|
120
|
-
-
|
|
130
|
+
- 全部仓库回主干且分支删除后,向用户报告"本地模式已收尾,空间回到干净态"
|
|
121
131
|
|
|
122
132
|
## 升级 worktree(中途要并行 / 要隔离)
|
|
123
133
|
|
|
124
|
-
**直接重跑 `cbb-worktree-init`(幂等)**:分支已存在自动复用(reused),worktree
|
|
134
|
+
**直接重跑 `cbb-worktree-init`(幂等)**:分支已存在自动复用(reused),worktree 从分支创建——本地阶段的提交**随身带走**,零成本升级。升级前提:本地检出先回主干(跑 5.4 的 `return-to-trunk.js`,主干名以远端 HEAD 为准;未提交改动先 commit 或用户决策),否则 worktree 检出与本地检出冲突。
|
|
125
135
|
|
|
126
136
|
---
|
|
127
137
|
|
|
@@ -136,9 +146,9 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
136
146
|
| commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
|
|
137
147
|
| 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
|
|
138
148
|
| 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
|
|
139
|
-
| 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 →
|
|
149
|
+
| 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃) |
|
|
140
150
|
| 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
|
|
141
|
-
| 升级隔离 | 重跑 cbb-worktree-init
|
|
151
|
+
| 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回主干腾检出(return-to-trunk.js) |
|
|
142
152
|
|
|
143
153
|
---
|
|
144
154
|
|
|
@@ -161,8 +171,8 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
161
171
|
|
|
162
172
|
### 收尾忘记 pull
|
|
163
173
|
|
|
164
|
-
- **问题:**
|
|
165
|
-
- **修复:**
|
|
174
|
+
- **问题:** 回主干后未追平远端,本地主干陈旧,下次 `start-local` 从旧基线建分支
|
|
175
|
+
- **修复:** 收尾固定两步脚本:`return-to-trunk.js`(切回主干 + `pull --ff-only` 追平)→ `delete-branches.js` 删分支
|
|
166
176
|
|
|
167
177
|
### 中途插单静默混入
|
|
168
178
|
|
|
@@ -189,6 +199,6 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
189
199
|
- 布局展示 + 用户确认后才切分支
|
|
190
200
|
- 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
|
|
191
201
|
- 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
|
|
192
|
-
- 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4
|
|
193
|
-
-
|
|
202
|
+
- 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 `return-to-trunk.js` 切回主干)
|
|
203
|
+
- 收尾脚本化(`return-to-trunk.js` 回主干 + `delete-branches.js` 删分支)完成后报告干净态
|
|
194
204
|
- 所有写操作前的分支确认(当前检出 = 需求分支)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 收尾回主干 —— 交付完成后把各参与仓库的本地检出切回主干并追平(ADR 0007 / 0008)
|
|
5
|
+
*
|
|
6
|
+
* 背景:merge-branches.js 统一零接触通道后,合并在临时 worktree 内完成、
|
|
7
|
+
* 不触碰本地检出(本地主干检出不随交付前移)。本地模式收尾需把各仓库
|
|
8
|
+
* 本地检出从需求分支切回主干——本脚本替代曾经写死 `git checkout main`
|
|
9
|
+
* 的裸命令:主干名逐仓库经远端 HEAD 探测(与 check-local-state.js 同源,
|
|
10
|
+
* 兼容 master 等),不再假设主干叫 main。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]
|
|
14
|
+
*
|
|
15
|
+
* 行为(逐仓库,相互独立、失败不停链):
|
|
16
|
+
* 1. 目录无 .git → SKIP no-repo
|
|
17
|
+
* 2. git fetch --quiet origin(失败仅降级,后续判定退回本地主干)
|
|
18
|
+
* 3. 主干名 detectMain:远端 HEAD → 本地 main → 本地 master → origin/main → origin/master
|
|
19
|
+
* → FAIL cannot-detect-main
|
|
20
|
+
* 4. 当前检出 ≠ 主干:
|
|
21
|
+
* - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
|
|
22
|
+
* - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
|
|
23
|
+
* 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
|
|
24
|
+
* (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
|
|
25
|
+
* 6. 已在主干 → 跳过切回,仍追平(幂等)
|
|
26
|
+
*
|
|
27
|
+
* 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
|
|
28
|
+
* 末行:TRUNK_RESULT [...]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { execSync } = require('child_process');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
35
|
+
function run(cmd, cwd) {
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
out: execSync(cmd, {
|
|
40
|
+
cwd,
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
timeout: 120000,
|
|
44
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
45
|
+
}).trim(),
|
|
46
|
+
};
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasRef(repoPath, ref) {
|
|
53
|
+
return run('git rev-parse --verify --quiet "' + ref + '"', repoPath).ok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadApps(wsRoot) {
|
|
57
|
+
try {
|
|
58
|
+
const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
59
|
+
return Array.isArray(config.apps) ? config.apps : [];
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 主干名:symbolic-ref 远端 HEAD → 本地 main/master → 远端跟踪 origin/main|master → 空串(与 check-local-state.js 同源,回退链更强) */
|
|
66
|
+
function detectMain(repoPath) {
|
|
67
|
+
const remote = run('git remote', repoPath);
|
|
68
|
+
if (remote.ok && remote.out) {
|
|
69
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
70
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
71
|
+
if (symbolic.ok && symbolic.out) return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
72
|
+
}
|
|
73
|
+
if (hasRef(repoPath, 'refs/heads/main')) return 'main';
|
|
74
|
+
if (hasRef(repoPath, 'refs/heads/master')) return 'master';
|
|
75
|
+
if (hasRef(repoPath, 'refs/remotes/origin/main')) return 'main';
|
|
76
|
+
if (hasRef(repoPath, 'refs/remotes/origin/master')) return 'master';
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 检出未提交项数(含未跟踪,不含 ignored);-1 = 状态不可读 */
|
|
81
|
+
function countUncommitted(repoPath) {
|
|
82
|
+
const s = run('git status --porcelain', repoPath);
|
|
83
|
+
if (!s.ok) return -1;
|
|
84
|
+
return s.out ? s.out.split(/\r?\n/).filter(Boolean).length : 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main() {
|
|
88
|
+
const args = process.argv.slice(2);
|
|
89
|
+
const wsRoot = args[0];
|
|
90
|
+
if (!wsRoot || wsRoot.indexOf('--') === 0) {
|
|
91
|
+
console.error('Usage: node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]');
|
|
92
|
+
process.exit(1);
|
|
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
|
+
const dryRun = args.includes('--dry-run');
|
|
99
|
+
|
|
100
|
+
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
101
|
+
for (const app of loadApps(wsRoot)) {
|
|
102
|
+
if (appFilter && !appFilter.has(app.name)) continue;
|
|
103
|
+
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const results = [];
|
|
107
|
+
let failed = false;
|
|
108
|
+
|
|
109
|
+
for (const rp of repos) {
|
|
110
|
+
const rec = { name: rp.name, status: 'ok', main: null, switched: false, uncommitted: 0, reason: null, detail: null };
|
|
111
|
+
if (!fs.existsSync(path.join(rp.repo, '.git'))) {
|
|
112
|
+
rec.status = 'no-repo';
|
|
113
|
+
results.push(rec);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
run('git fetch --quiet origin', rp.repo);
|
|
117
|
+
const main = detectMain(rp.repo);
|
|
118
|
+
if (!main) {
|
|
119
|
+
rec.status = 'failed';
|
|
120
|
+
rec.reason = 'cannot-detect-main';
|
|
121
|
+
results.push(rec);
|
|
122
|
+
failed = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
rec.main = main;
|
|
126
|
+
rec.uncommitted = countUncommitted(rp.repo);
|
|
127
|
+
const current = run('git branch --show-current', rp.repo);
|
|
128
|
+
|
|
129
|
+
if (current.ok && current.out !== main) {
|
|
130
|
+
// detached(out 为空)也需切回——「不在主干」含 detached
|
|
131
|
+
if (dryRun) {
|
|
132
|
+
rec.reason = 'would-switch (from ' + current.out + ')';
|
|
133
|
+
results.push(rec);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (rec.uncommitted > 0) {
|
|
137
|
+
rec.status = 'failed';
|
|
138
|
+
rec.reason = 'dirty-worktree';
|
|
139
|
+
rec.detail = '检出 ' + current.out + ' 上有 ' + rec.uncommitted + ' 项未提交改动,不自动 stash——先处理再重跑';
|
|
140
|
+
results.push(rec);
|
|
141
|
+
failed = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const co = hasRef(rp.repo, 'refs/heads/' + main)
|
|
145
|
+
? run('git checkout --quiet "' + main + '"', rp.repo)
|
|
146
|
+
: run('git checkout -b "' + main + '" --track origin/' + main, rp.repo);
|
|
147
|
+
if (!co.ok) {
|
|
148
|
+
rec.status = 'failed';
|
|
149
|
+
rec.reason = 'checkout-failed';
|
|
150
|
+
rec.detail = (co.err || '').split(/\r?\n/)[0];
|
|
151
|
+
results.push(rec);
|
|
152
|
+
failed = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
rec.switched = true;
|
|
156
|
+
} else if (dryRun) {
|
|
157
|
+
rec.reason = 'already-on-trunk';
|
|
158
|
+
results.push(rec);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
|
|
163
|
+
const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
|
|
164
|
+
if (!pull.ok) {
|
|
165
|
+
rec.status = 'failed';
|
|
166
|
+
rec.reason = 'pull-failed';
|
|
167
|
+
rec.detail = (pull.err || '').split(/\r?\n/)[0];
|
|
168
|
+
failed = true;
|
|
169
|
+
}
|
|
170
|
+
} else if (!rec.reason) {
|
|
171
|
+
rec.reason = 'no-remote-trunk';
|
|
172
|
+
}
|
|
173
|
+
results.push(rec);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
results.forEach(r => {
|
|
177
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.main ? ' → ' + r.main : '') + (r.reason ? ': ' + r.reason : '') + (r.detail ? ' — ' + r.detail : ''));
|
|
178
|
+
});
|
|
179
|
+
console.log('---');
|
|
180
|
+
console.log('TRUNK_RESULT', JSON.stringify({ results }));
|
|
181
|
+
process.exit(failed ? 2 : 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (require.main === module) main();
|
|
@@ -367,7 +367,7 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
|
367
367
|
| 空间新增了应用(编辑 `CBB-WORKSPACE.json` 后) | Step 1 拉取新应用代码 → Step 3.3 为新应用建分支 → Step 3.5 在 `.codespace/` 容器内创建缺失的 app worktree → Step 3.6 补 junction |
|
|
368
368
|
| 上次 init 中途失败 | 已存在的分支 / worktree 自动复用,`.gitignore` 处理自动跳过,仅补齐缺失部分 |
|
|
369
369
|
| 应用从 config 移除 | 已创建的分支 / worktree **不会**自动删除;如需清理用 `cbb-worktree-close` skill 关闭后重建 |
|
|
370
|
-
| **本地模式升级 worktree**(ADR 0002) | Step 3.3 分支已存在 → reused
|
|
370
|
+
| **本地模式升级 worktree**(ADR 0002) | Step 3.3 分支已存在 → reused(本地阶段提交随身带走);先把各仓库本地检出切回主干(主干名以远端 HEAD 为准,勿写死 main),再重跑本技能 |
|
|
371
371
|
| 本地模式进行中重跑 | 安全:分支基线取 `origin/main`(非 HEAD),不受根/应用检出状态影响;fetch 不动工作区 |
|
|
372
372
|
|
|
373
373
|
> ⓘ 本技能**零远端副作用**(lazy push):不存在"重跑推送开发中提交"的风险面——推送只发生在 `cbb-worktree-push`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jspg-ai/coding-bb",
|
|
3
|
-
"version": "0.0.5-beta.
|
|
3
|
+
"version": "0.0.5-beta.3",
|
|
4
4
|
"description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
|
|
5
5
|
"main": "cbb/lib/install/init.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"sync:shared": "node scripts/sync-shared.js",
|
|
22
22
|
"pretest": "node scripts/sync-shared.js --check",
|
|
23
|
-
"test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/install/tool-selection.test.js && node test/lib/install/agents-nav.test.js && node test/lib/install/command-skill.test.js && node test/lib/utils/plugin-hook.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/lib/external/external.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js && node test/worktrees/merge-branches.test.js && node test/worktrees/deprecate-branches.test.js && node test/worktrees/delete-branches.test.js && node test/worktrees/skill-refs.test.js"
|
|
23
|
+
"test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/install/tool-selection.test.js && node test/lib/install/agents-nav.test.js && node test/lib/install/command-skill.test.js && node test/lib/utils/plugin-hook.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/lib/external/external.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js && node test/worktrees/merge-branches.test.js && node test/worktrees/deprecate-branches.test.js && node test/worktrees/delete-branches.test.js && node test/worktrees/return-to-trunk.test.js && node test/worktrees/skill-refs.test.js"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cbb",
|