@jspg-ai/coding-bb 0.0.3-beta.33 → 0.0.3-beta.34
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 +1 -1
- package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
- package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +9 -5
- package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +3 -2
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -138
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +13 -10
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
- package/package.json +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
|
@@ -101,7 +101,7 @@ trigger: always_on
|
|
|
101
101
|
|
|
102
102
|
## 8. 业务空间开发模式(双模式,用户选择制)
|
|
103
103
|
|
|
104
|
-
**仅当工作目录是业务空间根(存在 `CBB-WORKSPACE.json` 且 `.git` 为目录)时生效。空间支持两种开发模式,能力对称(都能修改 workspace
|
|
104
|
+
**仅当工作目录是业务空间根(存在 `CBB-WORKSPACE.json` 且 `.git` 为目录)时生效。空间支持两种开发模式,能力对称(都能修改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),由用户显式选择,agent 只给推荐不代决。**
|
|
105
105
|
|
|
106
106
|
**模式一 · 本地模式(单线程快改,当前会话就地干):**
|
|
107
107
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 删除需求分支(共享脚本,真源:cbb/worktrees/_shared/scripts/)
|
|
5
|
+
*
|
|
6
|
+
* 供 cbb-worktree-close(Step 4.4 交付完成后自动删本地 + 远程分支)与
|
|
7
|
+
* cbb-local-mode(Step 5.4 收尾删远程分支)调用。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node delete-branches.js <ws_root> <branch> <mode> # mode: keep / local / local_remote
|
|
10
|
+
*
|
|
11
|
+
* 逐仓库执行:本地分支按合并判定选 -d / -D(squash 合并 -d 失败回退 -D);
|
|
12
|
+
* local_remote 模式追加 git push <remote> --delete。
|
|
13
|
+
* 清理阶段失败报告并继续其余仓库,不回滚(ADR 0007)。
|
|
14
|
+
*/
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const wsRoot = process.argv[2];
|
|
20
|
+
const branch = process.argv[3];
|
|
21
|
+
const mode = process.argv[4];
|
|
22
|
+
|
|
23
|
+
if (!wsRoot || !branch || !mode) {
|
|
24
|
+
console.error('Usage: node delete-branches.js <ws_root> <branch> <mode>');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (mode === 'keep') {
|
|
29
|
+
console.log('Keep branch, skip delete');
|
|
30
|
+
console.log('---');
|
|
31
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode: 'keep', ok: true, deleted: [] }));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode !== 'local' && mode !== 'local_remote') {
|
|
36
|
+
console.error('mode must be keep / local / local_remote');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd, cwd, env) {
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(env || {}) } }).trim() };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectMain(repoPath) {
|
|
49
|
+
const remote = run('git remote', repoPath);
|
|
50
|
+
if (!remote.ok || !remote.out) return 'main';
|
|
51
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
52
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
53
|
+
if (symbolic.ok && symbolic.out) {
|
|
54
|
+
return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
55
|
+
}
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergedTo(repoPath, from, to) {
|
|
60
|
+
// 0=merged, 1=unmerged, 2=to-ref-missing
|
|
61
|
+
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
62
|
+
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
63
|
+
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
64
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
65
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
66
|
+
if (!cherry.ok) return 1;
|
|
67
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergedToMain(repoPath, branchName, main) {
|
|
72
|
+
if (mergedTo(repoPath, branchName, main) === 0) return true;
|
|
73
|
+
if (mergedTo(repoPath, branchName, 'origin/' + main) === 0) return true;
|
|
74
|
+
// release-* transit
|
|
75
|
+
const releaseList = run('git for-each-ref --format="%(refname:short)" "refs/heads/release-*" "refs/remotes/origin/release-*"', repoPath);
|
|
76
|
+
if (releaseList.ok && releaseList.out) {
|
|
77
|
+
const releases = releaseList.out.split(/\r?\n/).filter(Boolean).sort();
|
|
78
|
+
for (const rel of releases) {
|
|
79
|
+
if (mergedTo(repoPath, branchName, rel) === 0) {
|
|
80
|
+
if (mergedTo(repoPath, rel, main) === 0) return true;
|
|
81
|
+
if (mergedTo(repoPath, rel, 'origin/' + main) === 0) return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deleteBranch(repoPath, name) {
|
|
89
|
+
const checkLocal = run('git branch --list "' + branch + '"', repoPath);
|
|
90
|
+
if (checkLocal.ok && checkLocal.out) {
|
|
91
|
+
const main = detectMain(repoPath);
|
|
92
|
+
const merged = main ? mergedToMain(repoPath, branch, main) : false;
|
|
93
|
+
let flag = merged ? '-d' : '-D';
|
|
94
|
+
const del = run('git branch ' + flag + ' "' + branch + '"', repoPath);
|
|
95
|
+
if (del.ok) {
|
|
96
|
+
console.log('OK ' + name + ' local branch deleted (' + flag + ', ' + (merged ? 'merged' : 'unmerged') + ')');
|
|
97
|
+
} else {
|
|
98
|
+
if (flag === '-d') {
|
|
99
|
+
const fallback = run('git branch -D "' + branch + '"', repoPath);
|
|
100
|
+
if (fallback.ok) {
|
|
101
|
+
console.log('OK ' + name + ' local branch deleted (squash merge, fallback -D)');
|
|
102
|
+
} else {
|
|
103
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Skip ' + name + ': no local branch ' + branch);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (mode === 'local_remote') {
|
|
116
|
+
const remote = run('git remote', repoPath);
|
|
117
|
+
if (remote.ok && remote.out) {
|
|
118
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
119
|
+
const pushDel = run('git push "' + r + '" --delete "' + branch + '"', repoPath, { GIT_TERMINAL_PROMPT: '0' });
|
|
120
|
+
if (pushDel.ok) {
|
|
121
|
+
console.log('OK ' + name + ' remote branch deleted');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('Skip ' + name + ': remote has no ' + branch);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
131
|
+
let config = {};
|
|
132
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
133
|
+
|
|
134
|
+
const apps = config.apps || [];
|
|
135
|
+
|
|
136
|
+
console.log('=== Delete Workspace Branch ===');
|
|
137
|
+
deleteBranch(wsRoot, 'workspace');
|
|
138
|
+
|
|
139
|
+
console.log('=== Delete App Branches ===');
|
|
140
|
+
for (const app of apps) {
|
|
141
|
+
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
142
|
+
deleteBranch(appPath, app.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('---');
|
|
146
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode, ok: true }));
|
|
147
|
+
process.exit(0);
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 目标需求名识别
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/find-target-worktree.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(close / push)。
|
|
7
7
|
*
|
|
8
8
|
* 用法:node find-target-worktree.js <ws_root>
|
|
9
9
|
*
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 工作空间根目录定位
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/find-workspace-root.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(close / push)。
|
|
7
7
|
*
|
|
8
8
|
* 优先:git worktree list 第一行(主仓库根目录,不受 worktree 内 CBB-WORKSPACE.json 副本干扰)
|
|
9
9
|
* 兜底:当前目录含 CBB-WORKSPACE.json
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 推送核心模块
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/push-core.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / push)。
|
|
7
7
|
*
|
|
8
8
|
* 公开 API:
|
|
9
9
|
* - tryExec(cmd, cwd, execImpl) 包装 execSync,统一返回 {ok, out|err}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// koffi is resolved from cbb global install (no local node_modules needed).
|
|
5
5
|
//
|
|
6
6
|
// 唯一真源:cbb/worktrees/_shared/scripts/silence-popup.js
|
|
7
|
-
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init /
|
|
7
|
+
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / close / push / local-mode)。
|
|
8
8
|
// skill 内同目录引用:脚本第一行 `require('./silence-popup')`。
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
10
|
try {
|
|
@@ -5,7 +5,7 @@ description: "业务空间本地模式开发:空间根与各涉及应用在 .c
|
|
|
5
5
|
|
|
6
6
|
# Local Mode(本地模式)
|
|
7
7
|
|
|
8
|
-
业务空间的**就地开发**模式(ADR 0002):需求涉及的所有仓库(空间根 + 各应用的标准本地检出 `.codespace/<app>/`)统一切到同名分支,当前会话在原地直接开发。与 worktree 模式**能力对称**(都能改 workspace
|
|
8
|
+
业务空间的**就地开发**模式(ADR 0002):需求涉及的所有仓库(空间根 + 各应用的标准本地检出 `.codespace/<app>/`)统一切到同名分支,当前会话在原地直接开发。与 worktree 模式**能力对称**(都能改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),差异仅是**无隔离目录、不可并行**——适合单线程快速推进的需求。
|
|
9
9
|
|
|
10
10
|
**启动时声明:** "我正在使用 cbb-local-mode 技能进行本地模式开发。"
|
|
11
11
|
|
|
@@ -42,7 +42,7 @@ node "<skill_dir>/scripts/check-local-state.js" "<ws_root>"
|
|
|
42
42
|
|
|
43
43
|
## Step 1:需求名与布局确认
|
|
44
44
|
|
|
45
|
-
1. 对话中询问需求名(kebab-case,同时作分支名;禁 `..` / 路径分隔符 /
|
|
45
|
+
1. 对话中询问需求名(kebab-case,同时作分支名;禁 `..` / 路径分隔符 / 空格):直接给名,或输入需求描述由 agent 推荐 3–5 个英文候选(`AskUserQuestion` 选定;不满意可直接回复自己的名字)
|
|
46
46
|
2. 从需求语境提取**涉及的应用子集**(改哪些 `.codespace/<app>`);纯 workspace 内容则仅根参与。展示布局向用户确认:
|
|
47
47
|
|
|
48
48
|
```
|
|
@@ -74,7 +74,7 @@ node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,
|
|
|
74
74
|
|
|
75
75
|
## Step 4:提交推送(转 cbb-worktree-push)
|
|
76
76
|
|
|
77
|
-
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → MR
|
|
77
|
+
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → Step 7 交付选择(提 MR 预填链接 / 直接合并 / 暂不处理,ADR 0008),全流程与 worktree 模式一致。
|
|
78
78
|
|
|
79
79
|
## Step 5:收尾(收尾合并门禁 · ADR 0007)
|
|
80
80
|
|
|
@@ -110,7 +110,11 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
|
|
|
110
110
|
git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" branch -d "<需求名>"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
-
|
|
113
|
+
- 各仓库本地分支删除后,远程分支删除由共享脚本完成(删除前输出将删清单):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
|
|
117
|
+
```
|
|
114
118
|
- `checkout` 被 hook 拦(说明 main 落后或本地 main 有分叉)→ 先 `pull --ff-only`,再删分支
|
|
115
119
|
- openspec change 归档在合并前执行(产物在分支上随交付合入)
|
|
116
120
|
- 全部仓库回 main 后向用户报告"本地模式已收尾,空间回到干净态"
|
|
@@ -126,7 +130,7 @@ git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" b
|
|
|
126
130
|
| 场景 | 处理方式 |
|
|
127
131
|
|------|----------|
|
|
128
132
|
| 任何触发 | 先跑 Step 0 状态自检(clean 零交互 / active 用户四选一) |
|
|
129
|
-
| 需求名 |
|
|
133
|
+
| 需求名 | 对话输入:直接给名,或给需求描述(agent 推荐 3–5 个英文候选供选);自由文本本身禁走 AskUserQuestion |
|
|
130
134
|
| 涉及应用 | 需求语境提取子集,`--apps` 传入;中途补齐重跑(幂等) |
|
|
131
135
|
| 切分支失败(dirty 冲突) | 报告详情,用户处理;禁止自动 stash |
|
|
132
136
|
| commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 删除需求分支(共享脚本,真源:cbb/worktrees/_shared/scripts/)
|
|
5
|
+
*
|
|
6
|
+
* 供 cbb-worktree-close(Step 4.4 交付完成后自动删本地 + 远程分支)与
|
|
7
|
+
* cbb-local-mode(Step 5.4 收尾删远程分支)调用。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node delete-branches.js <ws_root> <branch> <mode> # mode: keep / local / local_remote
|
|
10
|
+
*
|
|
11
|
+
* 逐仓库执行:本地分支按合并判定选 -d / -D(squash 合并 -d 失败回退 -D);
|
|
12
|
+
* local_remote 模式追加 git push <remote> --delete。
|
|
13
|
+
* 清理阶段失败报告并继续其余仓库,不回滚(ADR 0007)。
|
|
14
|
+
*/
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const wsRoot = process.argv[2];
|
|
20
|
+
const branch = process.argv[3];
|
|
21
|
+
const mode = process.argv[4];
|
|
22
|
+
|
|
23
|
+
if (!wsRoot || !branch || !mode) {
|
|
24
|
+
console.error('Usage: node delete-branches.js <ws_root> <branch> <mode>');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (mode === 'keep') {
|
|
29
|
+
console.log('Keep branch, skip delete');
|
|
30
|
+
console.log('---');
|
|
31
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode: 'keep', ok: true, deleted: [] }));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode !== 'local' && mode !== 'local_remote') {
|
|
36
|
+
console.error('mode must be keep / local / local_remote');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd, cwd, env) {
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(env || {}) } }).trim() };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectMain(repoPath) {
|
|
49
|
+
const remote = run('git remote', repoPath);
|
|
50
|
+
if (!remote.ok || !remote.out) return 'main';
|
|
51
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
52
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
53
|
+
if (symbolic.ok && symbolic.out) {
|
|
54
|
+
return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
55
|
+
}
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergedTo(repoPath, from, to) {
|
|
60
|
+
// 0=merged, 1=unmerged, 2=to-ref-missing
|
|
61
|
+
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
62
|
+
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
63
|
+
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
64
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
65
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
66
|
+
if (!cherry.ok) return 1;
|
|
67
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergedToMain(repoPath, branchName, main) {
|
|
72
|
+
if (mergedTo(repoPath, branchName, main) === 0) return true;
|
|
73
|
+
if (mergedTo(repoPath, branchName, 'origin/' + main) === 0) return true;
|
|
74
|
+
// release-* transit
|
|
75
|
+
const releaseList = run('git for-each-ref --format="%(refname:short)" "refs/heads/release-*" "refs/remotes/origin/release-*"', repoPath);
|
|
76
|
+
if (releaseList.ok && releaseList.out) {
|
|
77
|
+
const releases = releaseList.out.split(/\r?\n/).filter(Boolean).sort();
|
|
78
|
+
for (const rel of releases) {
|
|
79
|
+
if (mergedTo(repoPath, branchName, rel) === 0) {
|
|
80
|
+
if (mergedTo(repoPath, rel, main) === 0) return true;
|
|
81
|
+
if (mergedTo(repoPath, rel, 'origin/' + main) === 0) return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deleteBranch(repoPath, name) {
|
|
89
|
+
const checkLocal = run('git branch --list "' + branch + '"', repoPath);
|
|
90
|
+
if (checkLocal.ok && checkLocal.out) {
|
|
91
|
+
const main = detectMain(repoPath);
|
|
92
|
+
const merged = main ? mergedToMain(repoPath, branch, main) : false;
|
|
93
|
+
let flag = merged ? '-d' : '-D';
|
|
94
|
+
const del = run('git branch ' + flag + ' "' + branch + '"', repoPath);
|
|
95
|
+
if (del.ok) {
|
|
96
|
+
console.log('OK ' + name + ' local branch deleted (' + flag + ', ' + (merged ? 'merged' : 'unmerged') + ')');
|
|
97
|
+
} else {
|
|
98
|
+
if (flag === '-d') {
|
|
99
|
+
const fallback = run('git branch -D "' + branch + '"', repoPath);
|
|
100
|
+
if (fallback.ok) {
|
|
101
|
+
console.log('OK ' + name + ' local branch deleted (squash merge, fallback -D)');
|
|
102
|
+
} else {
|
|
103
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Skip ' + name + ': no local branch ' + branch);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (mode === 'local_remote') {
|
|
116
|
+
const remote = run('git remote', repoPath);
|
|
117
|
+
if (remote.ok && remote.out) {
|
|
118
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
119
|
+
const pushDel = run('git push "' + r + '" --delete "' + branch + '"', repoPath, { GIT_TERMINAL_PROMPT: '0' });
|
|
120
|
+
if (pushDel.ok) {
|
|
121
|
+
console.log('OK ' + name + ' remote branch deleted');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('Skip ' + name + ': remote has no ' + branch);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
131
|
+
let config = {};
|
|
132
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
133
|
+
|
|
134
|
+
const apps = config.apps || [];
|
|
135
|
+
|
|
136
|
+
console.log('=== Delete Workspace Branch ===');
|
|
137
|
+
deleteBranch(wsRoot, 'workspace');
|
|
138
|
+
|
|
139
|
+
console.log('=== Delete App Branches ===');
|
|
140
|
+
for (const app of apps) {
|
|
141
|
+
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
142
|
+
deleteBranch(appPath, app.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('---');
|
|
146
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode, ok: true }));
|
|
147
|
+
process.exit(0);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// koffi is resolved from cbb global install (no local node_modules needed).
|
|
5
5
|
//
|
|
6
6
|
// 唯一真源:cbb/worktrees/_shared/scripts/silence-popup.js
|
|
7
|
-
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init /
|
|
7
|
+
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / close / push / local-mode)。
|
|
8
8
|
// skill 内同目录引用:脚本第一行 `require('./silence-popup')`。
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
10
|
try {
|
|
@@ -287,6 +287,7 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<branch>" --apps "<app
|
|
|
287
287
|
- 退出码 0 → 交付完成,继续 Step 4
|
|
288
288
|
- `need-mr`(`branch-protected`:远端主干受保护)→ 远端保护是常态,**改用出口一**(不是故障)
|
|
289
289
|
- `failed`(含 `merge-failed` / `push-failed`)→ 输出失败明细,阻塞并询问用户(修复后重跑;重跑幂等)
|
|
290
|
+
- `failed` 且 `not-on-main`(空间根检出不是主干:被其他需求占用或异常)→ 先让该占用回到主干(收尾本地需求)或改用出口一;**不要用 `--allow-checkout`**(会切走占用者的工作区)
|
|
290
291
|
|
|
291
292
|
**出口三 · 确认废弃(`deprecate-branches.js`)**
|
|
292
293
|
|
|
@@ -493,9 +494,9 @@ git -C "<ws_root>" worktree list
|
|
|
493
494
|
| `find-workspace-root.js` | 定位工作空间根目录(共享脚本,源仓库真源在 `_shared/scripts/`) | 无 | 0/1 |
|
|
494
495
|
| `find-target-worktree.js` | 目标 worktree 识别(共享脚本,源仓库真源在 `_shared/scripts/`) | ws_root | 0/1/2 |
|
|
495
496
|
| `discover-apps.js` | 发现关联应用 worktree | ws_root, branch | 0/1/2 |
|
|
496
|
-
| `check-unarchived.js` | 检查未归档 change | ws_root | 0/1 |
|
|
497
|
+
| `check-unarchived.js` | 检查未归档 change(双扫空间根 + worktree) | ws_root, branch | 0/1 |
|
|
497
498
|
| `safety-check.js` | 安全评估(合并判定 + 未提交 + 远程) | ws_root, branch | 0/1 |
|
|
498
499
|
| `remove-worktrees.js` | 移除 worktree(三层降级) | ws_root, branch, force | 0/1 |
|
|
499
500
|
| `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
|
+
| `merge-branches.js` | 收尾合并门禁·直合并(预检 → merge --no-ff → push 主干,共享脚本,真源在 `_shared/scripts/`) | ws_root, branch, --apps / --target / --allow-checkout / --dry-run / --check | 0/1/2 |
|
|
501
502
|
| `deprecate-branches.js` | 收尾合并门禁·废弃(远程分支重命名为 `<分支>-deprecated`,共享脚本) | ws_root, branch, --apps | 0/1/2 |
|