@jspg-ai/coding-bb 0.0.3-beta.9 → 0.0.5-beta.1
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/README.md +1 -0
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -15
- package/cbb/dev-standards/rules/cbb-workspace-discipline.md +38 -0
- package/cbb/lib/external/index.js +228 -0
- package/cbb/lib/install/agents-nav.js +92 -0
- package/cbb/lib/install/claude-code.js +8 -1
- package/cbb/lib/install/command-skill.js +105 -0
- package/cbb/lib/install/init.js +399 -129
- package/cbb/lib/install/minimax.js +60 -0
- package/cbb/lib/install/rule-rewrite.js +1 -1
- package/cbb/lib/install/setup-decision.js +92 -0
- package/cbb/lib/install/tool-selection.js +23 -0
- package/cbb/lib/install/trae.js +18 -8
- package/cbb/lib/install/workbuddy.js +36 -0
- package/cbb/lib/install/workspaces.js +15 -61
- package/cbb/lib/utils/check-update.js +13 -5
- package/cbb/lib/utils/githook.js +230 -0
- package/cbb/lib/utils/gitignore.js +17 -4
- package/cbb/lib/utils/plugin-hook.js +92 -0
- package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/_shared/scripts/merge-branches.js +580 -0
- package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
- package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +194 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +580 -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 +134 -81
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +77 -48
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -136
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +580 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +44 -9
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +15 -4
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +131 -120
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
- 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 +192 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
- 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-init/scripts/sync-repos.js +11 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +17 -5
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +213 -56
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +580 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
- package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
- package/config/openspec/schemas/spec-driven/schema.yaml +20 -0
- package/config/upstream-mirrors.json +5 -0
- package/external/frontend-design/.version +6 -0
- package/external/frontend-design/LICENSE.txt +177 -0
- package/external/frontend-design/SKILL.md +71 -0
- package/openspec/.version +2 -2
- package/openspec/commands/archive.md +15 -8
- package/openspec/commands/bulk-archive.md +13 -6
- package/openspec/commands/continue.md +0 -1
- package/openspec/commands/explore.md +1 -1
- package/openspec/commands/ff.md +1 -1
- package/openspec/commands/onboard.md +9 -4
- package/openspec/commands/update.md +11 -5
- package/openspec/commands/verify.md +57 -26
- package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
- package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
- package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
- package/openspec/skills/openspec-explore/SKILL.md +2 -2
- package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
- package/openspec/skills/openspec-new-change/SKILL.md +1 -1
- package/openspec/skills/openspec-onboard/SKILL.md +10 -5
- package/openspec/skills/openspec-propose/SKILL.md +1 -1
- package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
- package/openspec/skills/openspec-update-change/SKILL.md +12 -6
- package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
- package/package.json +3 -2
- package/superpowers/.version +3 -3
- package/superpowers/skills/writing-plans/SKILL.md +32 -20
- package/cbb/lib/install/codebuddy.js +0 -30
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
- package/config/workspace-agents.sample.md +0 -41
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 本地模式开工(ADR 0002):根 + 指定应用就地切需求分支。
|
|
4
|
+
*
|
|
5
|
+
* 用法:node start-local.js <ws_root> <branch> [--apps app1,app2] [--all-apps]
|
|
6
|
+
*
|
|
7
|
+
* 行为:
|
|
8
|
+
* 1. 前置自检:根与所有 .codespace/<app>/ 若任一检出在非主干分支 → 遗留态,
|
|
9
|
+
* 退出码 2(agent 应升级为用户决策:继续 / 收尾 / 升级 / 丢弃,不在本脚本内处理)
|
|
10
|
+
* 2. 参与仓库 = workspace + 应用子集:
|
|
11
|
+
* - --apps a,b → 指定应用(本地模式推荐:从需求语境提取涉及应用)
|
|
12
|
+
* - --all-apps → CBB-WORKSPACE.json 全量应用
|
|
13
|
+
* - 缺省 → 仅 workspace(后续重跑幂等补齐,与 worktree-init 增补语义一致)
|
|
14
|
+
* 3. 每个仓库:
|
|
15
|
+
* - 当前已在 <branch> → reused
|
|
16
|
+
* - 分支存在(未检出)→ git checkout
|
|
17
|
+
* - 否则 → git checkout -b <branch> <remote>/<main>(基线不取 HEAD,ADR 0002;
|
|
18
|
+
* remote 不可用回退本地 main/master,再回退 HEAD)
|
|
19
|
+
* - dirty 仓库切换失败 → 报错退出(不 stash 兜底,交给用户决策)
|
|
20
|
+
* 4. 参与的应用仓库幂等补装提交防线 hook(cbb setup 已装,此处兜底)
|
|
21
|
+
*
|
|
22
|
+
* 幂等:重跑同一 branch 安全(reused / checkout 跳过)。
|
|
23
|
+
*
|
|
24
|
+
* 退出码:
|
|
25
|
+
* 0 - 全部切换成功
|
|
26
|
+
* 1 - 参数错误
|
|
27
|
+
* 2 - 遗留态 / 部分切换失败
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { execSync } = require('child_process');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { installTo: installGitHook } = require('./install-git-hook');
|
|
34
|
+
|
|
35
|
+
const wsRoot = process.argv[2];
|
|
36
|
+
const branch = process.argv[3];
|
|
37
|
+
|
|
38
|
+
if (!wsRoot || !branch) {
|
|
39
|
+
console.error('Usage: node start-local.js <ws_root> <branch> [--apps a,b] [--all-apps]');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function run(cmd, cwd) {
|
|
44
|
+
try {
|
|
45
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim() };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { ok: false, err: (e.stderr ? e.stderr.toString() : e.message || String(e)).trim() };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 主干名:symbolic-ref 远端 HEAD → 本地 main/master 回退 */
|
|
52
|
+
function detectMain(cwd) {
|
|
53
|
+
const remotes = run('git remote', cwd);
|
|
54
|
+
if (remotes.ok && remotes.out) {
|
|
55
|
+
const r = remotes.out.split(/\r?\n/)[0];
|
|
56
|
+
const sym = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
|
|
57
|
+
if (sym.ok && sym.out) return { remote: r, main: sym.out.replace('refs/remotes/' + r + '/', '') };
|
|
58
|
+
}
|
|
59
|
+
if (run('git rev-parse --verify --quiet refs/heads/main', cwd).ok) return { remote: null, main: 'main' };
|
|
60
|
+
if (run('git rev-parse --verify --quiet refs/heads/master', cwd).ok) return { remote: null, main: 'master' };
|
|
61
|
+
return { remote: null, main: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function currentBranch(cwd) {
|
|
65
|
+
const r = run('git branch --show-current', cwd);
|
|
66
|
+
return r.ok ? r.out : '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── 1. 前置自检:遗留态检测 ───────────────────────────────
|
|
70
|
+
// 「非主干」且「不是本次 branch」才是遗留(别的需求没收尾);
|
|
71
|
+
// 已检出在本次 branch 上 = 幂等补齐场景,放行(reused)。
|
|
72
|
+
|
|
73
|
+
function isOnMain(cwd) {
|
|
74
|
+
const info = detectMain(cwd);
|
|
75
|
+
return info.main ? currentBranch(cwd) === info.main : false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isOnTarget(cwd) {
|
|
79
|
+
return currentBranch(cwd) === branch || isOnMain(cwd);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const leftovers = [];
|
|
83
|
+
if (!isOnTarget(wsRoot)) leftovers.push({ name: 'workspace', branch: currentBranch(wsRoot) });
|
|
84
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
85
|
+
if (fs.existsSync(codeDir)) {
|
|
86
|
+
for (const name of fs.readdirSync(codeDir).sort()) {
|
|
87
|
+
const appDir = path.join(codeDir, name);
|
|
88
|
+
if (fs.existsSync(path.join(appDir, '.git')) && !isOnTarget(appDir)) {
|
|
89
|
+
leftovers.push({ name, branch: currentBranch(appDir) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (leftovers.length > 0) {
|
|
94
|
+
console.error('❌ 遗留态:以下仓库检出在非主干分支,先由用户决策处理(继续/收尾/升级/丢弃):');
|
|
95
|
+
leftovers.forEach(l => console.error(' - ' + l.name + ': ' + l.branch));
|
|
96
|
+
console.log('---');
|
|
97
|
+
console.log('LOCAL_START_FAILED', JSON.stringify({ reason: 'leftover', leftovers }));
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 2. 参与仓库集合 ───────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
let appNames = [];
|
|
104
|
+
const argApps = process.argv.indexOf('--apps');
|
|
105
|
+
const argAll = process.argv.includes('--all-apps');
|
|
106
|
+
if (argApps >= 0) {
|
|
107
|
+
const raw = process.argv[argApps + 1] || '';
|
|
108
|
+
appNames = raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
109
|
+
} else if (argAll) {
|
|
110
|
+
try {
|
|
111
|
+
const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
112
|
+
appNames = (config.apps || []).map(a => a.name);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error('❌ 读取 CBB-WORKSPACE.json 失败:' + e.message);
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const targets = [{ name: 'workspace', dir: wsRoot }];
|
|
120
|
+
for (const name of appNames) {
|
|
121
|
+
const appDir = path.join(wsRoot, '.codespace', name);
|
|
122
|
+
if (!fs.existsSync(path.join(appDir, '.git'))) {
|
|
123
|
+
console.error('❌ 应用仓库不存在:.codespace/' + name + '(请检查 CBB-WORKSPACE.json 或先跑 cbb setup)');
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
targets.push({ name, dir: appDir });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 3. 逐仓库切换 ─────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const results = [];
|
|
132
|
+
let hasFailure = false;
|
|
133
|
+
|
|
134
|
+
for (const t of targets) {
|
|
135
|
+
const cur = currentBranch(t.dir);
|
|
136
|
+
if (cur === branch) {
|
|
137
|
+
console.log('⏭️ ' + t.name + ': 已在 ' + branch + '(reused)');
|
|
138
|
+
results.push({ name: t.name, status: 'reused' });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const exists = run('git rev-parse --verify --quiet refs/heads/' + branch, t.dir).ok;
|
|
143
|
+
const info = detectMain(t.dir);
|
|
144
|
+
let cmd;
|
|
145
|
+
let base = 'HEAD';
|
|
146
|
+
if (exists) {
|
|
147
|
+
cmd = 'git checkout "' + branch + '"';
|
|
148
|
+
base = '(existing)';
|
|
149
|
+
} else {
|
|
150
|
+
if (info.remote && info.main) base = info.remote + '/' + info.main;
|
|
151
|
+
else if (info.main) base = info.main;
|
|
152
|
+
cmd = 'git checkout -b "' + branch + '" "' + base + '"';
|
|
153
|
+
}
|
|
154
|
+
const r = run(cmd, t.dir);
|
|
155
|
+
if (r.ok) {
|
|
156
|
+
console.log('✅ ' + t.name + ': ' + (exists ? 'checkout' : 'created from ' + base) + ' → ' + branch);
|
|
157
|
+
results.push({ name: t.name, status: exists ? 'switched' : 'created', base });
|
|
158
|
+
} else {
|
|
159
|
+
console.error('❌ ' + t.name + ': 切换失败 — ' + r.err);
|
|
160
|
+
results.push({ name: t.name, status: 'failed', error: r.err });
|
|
161
|
+
hasFailure = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── 4. 参与应用补装提交防线 hook(幂等)────────────────────
|
|
166
|
+
|
|
167
|
+
const hookResults = [];
|
|
168
|
+
for (const t of targets) {
|
|
169
|
+
if (t.name === 'workspace') continue; // 根仓库 hook 由 cbb setup 安装
|
|
170
|
+
hookResults.push({ name: t.name, status: installGitHook(t.dir) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log('---');
|
|
174
|
+
console.log('LOCAL_START_RESULT', JSON.stringify({
|
|
175
|
+
branch,
|
|
176
|
+
results,
|
|
177
|
+
hooks: hookResults,
|
|
178
|
+
worktreeHint: '如需升级隔离:重跑 cbb-worktree-init(分支已存在自动复用,worktree 从分支创建,提交随身带走)',
|
|
179
|
+
}));
|
|
180
|
+
process.exit(hasFailure ? 2 : 0);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbb-local-mode
|
|
3
|
+
description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃后回 main + 删分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Local Mode(本地模式)
|
|
7
|
+
|
|
8
|
+
业务空间的**就地开发**模式(ADR 0002):需求涉及的所有仓库(空间根 + 各应用的标准本地检出 `.codespace/<app>/`)统一切到同名分支,当前会话在原地直接开发。与 worktree 模式**能力对称**(都能改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),差异仅是**无隔离目录、不可并行**——适合单线程快速推进的需求。
|
|
9
|
+
|
|
10
|
+
**启动时声明:** "我正在使用 cbb-local-mode 技能进行本地模式开发。"
|
|
11
|
+
|
|
12
|
+
## 核心原则
|
|
13
|
+
|
|
14
|
+
- **两模式由用户选择,agent 只给推荐**:需要并行 / 隔离 → cbb-worktree-init;单线程快改 → 本技能。涉及应用代码不再是 worktree 独占——本地模式同样支持(`.codespace/<app>/` 就地切分支)
|
|
15
|
+
- **基准 = `origin/main`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**:本地模式暂态占用视图合法;main 检出是基准快照,**严禁直接在 main 检出上提交**(pre-commit hook 会拦)
|
|
16
|
+
- **一次只承载一个需求**:本地模式独占空间根与各 app 的本地检出;期间并行新需求只能走 worktree 模式(两模式互不干扰,worktree 分支从 `origin/main` 建,不依赖本地检出状态)
|
|
17
|
+
- **提交防线在 hook**:分支感知 pre-commit hook 拦截一切 main 上的载荷提交(workspace 白名单外 / 应用全拦;merge 提交放行);被拦 = 大声失败,按指引回分支,**禁止自动 `--no-verify` 绕过**
|
|
18
|
+
- **收尾合并门禁(ADR 0007,与 worktree 模式对称)**:收尾前每个参与仓库必须为"已合并",未合并时三选一——**提 MR** / **直合并**(`merge-branches.js --allow-checkout`)/ **确认废弃**(远程重命名 `<需求名>-deprecated`)。**硬门禁:未合并且非废弃一律不允许收尾**
|
|
19
|
+
- 所有原子操作封装为 `scripts/*.js`(跨平台 Node.js),调用方式与 worktree 三件套一致:`node "<skill_dir>/scripts/<name>.js" <参数>`,cwd 保持空间根
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Step 0:状态自检(必跑,零交互常态)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node "<skill_dir>/scripts/check-local-state.js" "<ws_root>"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**解读末行 `LOCAL_STATE`**:
|
|
30
|
+
- `state: "clean"` → 全部仓库在主干 → **直接进 Step 1**(零交互)
|
|
31
|
+
- `state: "active"` → 存在遗留(非主干检出,`branches` 为遗留需求名)→ **必须在对话中让用户四选一,不得自行决定**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
检测到本地需求 <分支名> 进行中(参与仓库:…):
|
|
35
|
+
[1] 继续该需求(推荐若同名)—— 重建上下文接着干
|
|
36
|
+
[2] 先收尾 —— 走收尾流程(见 Step 5)后重新开工
|
|
37
|
+
[3] 升级 worktree —— 转跑 cbb-worktree-init 腾出本地检出
|
|
38
|
+
[4] 丢弃 —— 确认无未推送/未合并后清理分支(需二次确认)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> 自检同时是**会话恢复**机制:新会话"继续刚才的需求"靠它还原布局(同名分支的仓库集合 = 参与仓库)。
|
|
42
|
+
|
|
43
|
+
## Step 1:需求名与布局确认
|
|
44
|
+
|
|
45
|
+
1. 对话中询问需求名(kebab-case,同时作分支名;禁 `..` / 路径分隔符 / 空格):直接给名,或输入需求描述由 agent 推荐 3–5 个英文候选(`AskUserQuestion` 选定;不满意可直接回复自己的名字)
|
|
46
|
+
2. 从需求语境提取**涉及的应用子集**(改哪些 `.codespace/<app>`);纯 workspace 内容则仅根参与。展示布局向用户确认:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
本地模式布局(需求 <需求名>):
|
|
50
|
+
workspace → 空间根 checkout -b <需求名>
|
|
51
|
+
ep-foo-app → .codespace/ep-foo-app checkout -b <需求名>
|
|
52
|
+
ep-bar-app → 不参与(本需求不涉及)
|
|
53
|
+
确认后开工;中途涉及新应用可重跑本技能幂等补齐。
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> 布局确认属于方案确认(先批准后动工);应用子集判断错了没有代价——重跑一次即补。
|
|
57
|
+
|
|
58
|
+
## Step 2:切分支(开工)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,<app2>"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `--apps`:指定涉及应用(逗号分隔);`--all-apps`:全量;缺省仅 workspace
|
|
65
|
+
- **解读末行 `LOCAL_START_RESULT`**:逐仓库 created / switched / reused + hook 补装结果
|
|
66
|
+
- 退出码 2(`LOCAL_START_FAILED leftover`)→ 回 Step 0 用户决策;切换失败(dirty 冲突)→ 报告详情让用户先处理,**不自动 stash**
|
|
67
|
+
|
|
68
|
+
## Step 3:开发(会话 cwd == 开发目录)
|
|
69
|
+
|
|
70
|
+
- 当前会话**原地**开发:workspace 内容用根相对路径,应用代码用 `.codespace/<app>/...` 相对路径——上游 `/opsx:propose → apply → verify` 的相对路径指令**原样生效**,零翻译(worktree 模式自 ADR 0003 起同布局,两模式路径通用)
|
|
71
|
+
- 需求产物落 `openspec/changes/<需求名>/`(在分支上,hook 已保证不会误进 main)
|
|
72
|
+
- **读基准代码约定**:本地模式进行中工作区是开发态——要读"main 上是什么"先看 `git -C .codespace/<app> branch --show-current`,或直接 `git show main:<path>`,不要默认工作区即基准
|
|
73
|
+
- **中途插单**:用户插入非本需求的改动 → 先确认归属(并入当前需求 / 收尾后处理 / 转 worktree),不得静默混入
|
|
74
|
+
|
|
75
|
+
## Step 4:提交推送(转 cbb-worktree-push)
|
|
76
|
+
|
|
77
|
+
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → Step 7 交付选择(提 MR 预填链接 / 直接合并 / 暂不处理,ADR 0008),全流程与 worktree 模式一致。
|
|
78
|
+
|
|
79
|
+
## Step 5:收尾(收尾合并门禁 · ADR 0007)
|
|
80
|
+
|
|
81
|
+
MR 合并、验收通过后回到本会话收尾。**与 worktree 模式同门禁**:每个参与仓库(空间根 + 各参与 `.codespace/<app>/`)必须为"已合并"。
|
|
82
|
+
|
|
83
|
+
**5.1 交付前置**:未提交改动 → 先提交;未归档 change → 先归档并提交(**合并前必须已归档 + 已提交**)。
|
|
84
|
+
|
|
85
|
+
**5.2 判定是否已合并**(复用共享判定,算法与 close 的 `safety-check` 同源):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- 解读末行 `MERGE_CHECK_RESULT`:每仓库 `merged`(含 `origin/main` / `main-via-release-*`)/ `unmerged` / `no-branch`
|
|
92
|
+
- 全部 `merged` / `no-branch`(退出码 0)→ 直接进 5.4
|
|
93
|
+
- 存在 `unmerged`(退出码 2)→ 进 5.3
|
|
94
|
+
|
|
95
|
+
**5.3 门禁三选一**(`AskUserQuestion`,同 close Step 3.4):
|
|
96
|
+
|
|
97
|
+
- **提 MR** → **中止收尾**,引导用户运行 `cbb-worktree-push`(说"提交并推送")生成 MR 预填链接(其 Step 7:目标分支候选交互 + `make-mr-urls.js`)→ 合并完重跑本步
|
|
98
|
+
- **直合并** → `merge-branches.js`(本地检出被需求分支占用,**必须加 `--allow-checkout`** 切回主干):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --allow-checkout
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑;`AUTOSTASH_PENDING`(`MERGED` 但退出码 2)→ 交付已完成,仅暂存恢复遇冲突——**本地模式的改动会被恢复到已合入需求的 trunk 上(基线已变,冲突概率高于 worktree 模式)**,列冲突点问用户二选一(agent 解决 / 手动解决,协议同 close 出口二),处理前不得 drop stash
|
|
105
|
+
- **确认废弃** → `deprecate-branches.js`(远程重命名 `<需求名>-deprecated`;本地分支保留原名)
|
|
106
|
+
|
|
107
|
+
**5.4 交付完成 → 各仓库回主干并删分支**:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" branch -d "<需求名>"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- 各仓库本地分支删除后,远程分支删除由共享脚本完成(删除前输出将删清单):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
|
|
117
|
+
```
|
|
118
|
+
- `checkout` 被 hook 拦(说明 main 落后或本地 main 有分叉)→ 先 `pull --ff-only`,再删分支
|
|
119
|
+
- openspec change 归档在合并前执行(产物在分支上随交付合入)
|
|
120
|
+
- 全部仓库回 main 后向用户报告"本地模式已收尾,空间回到干净态"
|
|
121
|
+
|
|
122
|
+
## 升级 worktree(中途要并行 / 要隔离)
|
|
123
|
+
|
|
124
|
+
**直接重跑 `cbb-worktree-init`(幂等)**:分支已存在自动复用(reused),worktree 从分支创建——本地阶段的提交**随身带走**,零成本升级。升级前提:本地检出先回 main(`git checkout main`,未提交改动先 commit 或用户决策),否则 worktree 检出与本地检出冲突。
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 快速参考
|
|
129
|
+
|
|
130
|
+
| 场景 | 处理方式 |
|
|
131
|
+
|------|----------|
|
|
132
|
+
| 任何触发 | 先跑 Step 0 状态自检(clean 零交互 / active 用户四选一) |
|
|
133
|
+
| 需求名 | 对话输入:直接给名,或给需求描述(agent 推荐 3–5 个英文候选供选);自由文本本身禁走 AskUserQuestion |
|
|
134
|
+
| 涉及应用 | 需求语境提取子集,`--apps` 传入;中途补齐重跑(幂等) |
|
|
135
|
+
| 切分支失败(dirty 冲突) | 报告详情,用户处理;禁止自动 stash |
|
|
136
|
+
| commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
|
|
137
|
+
| 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
|
|
138
|
+
| 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
|
|
139
|
+
| 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则各仓库 checkout main + pull + 删分支;否则三选一(MR / 直合并 `--allow-checkout` / 确认废弃) |
|
|
140
|
+
| 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
|
|
141
|
+
| 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回 main 腾检出 |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 常见错误
|
|
146
|
+
|
|
147
|
+
### 跳过 Step 0 直接开工
|
|
148
|
+
|
|
149
|
+
- **问题:** 上一个本地需求没收尾,新需求改动混入旧分支
|
|
150
|
+
- **修复:** 状态自检必跑;active 态必须用户决策后才能动
|
|
151
|
+
|
|
152
|
+
### 把工作区当基准代码读
|
|
153
|
+
|
|
154
|
+
- **问题:** 本地模式进行中 `.codespace/<app>/` 是 feature 分支,agent 读它当"main 现状",得出错误结论
|
|
155
|
+
- **修复:** 读基准前确认检出分支,或 `git show main:<path>`;`git diff main` 看开发态差异
|
|
156
|
+
|
|
157
|
+
### 同时开两个本地需求
|
|
158
|
+
|
|
159
|
+
- **问题:** 单检出单分支,第二个需求的 checkout 会顶掉第一个
|
|
160
|
+
- **修复:** 一次一个本地需求;并行需求走 worktree 模式(`cbb-worktree-init` 从 `origin/main` 建分支,不受本地占用影响)
|
|
161
|
+
|
|
162
|
+
### 收尾忘记 pull
|
|
163
|
+
|
|
164
|
+
- **问题:** `checkout main` 后直接删分支,本地 main 陈旧,下次 `start-local` 从旧基线建分支
|
|
165
|
+
- **修复:** 收尾固定三连:checkout main && pull && branch -d
|
|
166
|
+
|
|
167
|
+
### 中途插单静默混入
|
|
168
|
+
|
|
169
|
+
- **问题:** 用户插的"顺手改一下"与本需求改动混在同一 commit,评审困难
|
|
170
|
+
- **修复:** 插单必问归属:并入 / 收尾后处理 / 转 worktree
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 红线规则
|
|
175
|
+
|
|
176
|
+
**绝不允许**
|
|
177
|
+
|
|
178
|
+
- 跳过 Step 0 状态自检(active 态未决策就开工)
|
|
179
|
+
- 直接在 main 检出上提交载荷(hook 会拦;被拦后 --no-verify 绕过仅限用户显式要求)
|
|
180
|
+
- 自动 stash 兜底切换失败(用户决策)
|
|
181
|
+
- 同时承载第二个本地需求(并行走 worktree)
|
|
182
|
+
- 把 `.codespace/<app>/` 的 main 检出当开发区(基准快照,本地模式只在分支上开发)
|
|
183
|
+
- **未合并且未确认为废弃时收尾**(违反 ADR 0007 硬门禁)
|
|
184
|
+
- 未经二次确认执行废弃重命名(远程重命名不可逆)
|
|
185
|
+
|
|
186
|
+
**必须执行**
|
|
187
|
+
|
|
188
|
+
- Step 0 自检 → clean 才开工
|
|
189
|
+
- 布局展示 + 用户确认后才切分支
|
|
190
|
+
- 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
|
|
191
|
+
- 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
|
|
192
|
+
- 直合并走 `merge-branches.js --allow-checkout`(本地检出需从需求分支切回主干)
|
|
193
|
+
- 收尾三连(checkout main / pull / 删分支)完成后报告干净态
|
|
194
|
+
- 所有写操作前的分支确认(当前检出 = 需求分支)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 本地模式状态自检(ADR 0002,改动清单 #12)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:node check-local-state.js <ws_root>
|
|
6
|
+
*
|
|
7
|
+
* 扫描空间根 + 所有 .codespace/<app>/ 的 { 当前分支, dirty },
|
|
8
|
+
* 判定「干净态」(全部在主干且 clean)或「遗留态」(任一仓库检出在非主干分支)。
|
|
9
|
+
* 干净态 → 零交互直接进入模式选择;遗留态 → agent 升级为用户决策
|
|
10
|
+
* (继续 / 收尾 / 升级 worktree / 丢弃)。同名分支集合 = 遗留需求的仓库布局。
|
|
11
|
+
*
|
|
12
|
+
* 输出:
|
|
13
|
+
* 人类可读报告(逐仓库状态 + 态判定 + 推断的需求名)
|
|
14
|
+
* 末行 LOCAL_STATE { state, branches[], repos[{name,branch,dirty}] }
|
|
15
|
+
* - state: 'clean' | 'active'(active = 存在非主干检出)
|
|
16
|
+
* - branches: 非主干分支名集合(通常为 1 个 = 遗留需求名;多个不同名 → 多头遗留,需用户逐个处理)
|
|
17
|
+
*
|
|
18
|
+
* 退出码:
|
|
19
|
+
* 0 - 扫描完成(无论何种状态;态判定交给 agent 解读)
|
|
20
|
+
* 1 - 参数错误
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const wsRoot = process.argv[2];
|
|
28
|
+
|
|
29
|
+
if (!wsRoot) {
|
|
30
|
+
console.error('Usage: node check-local-state.js <ws_root>');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function run(cmd, cwd) {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 主干名:symbolic-ref 远端 HEAD → 本地 main/master 存在性回退;探测失败 null */
|
|
43
|
+
function detectMain(cwd) {
|
|
44
|
+
const remotes = run('git remote', cwd);
|
|
45
|
+
if (remotes) {
|
|
46
|
+
const r = remotes.split(/\r?\n/)[0];
|
|
47
|
+
const sym = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
|
|
48
|
+
if (sym) return sym.replace('refs/remotes/' + r + '/', '');
|
|
49
|
+
}
|
|
50
|
+
if (run('git rev-parse --verify --quiet refs/heads/main', cwd)) return 'main';
|
|
51
|
+
if (run('git rev-parse --verify --quiet refs/heads/master', cwd)) return 'master';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scanRepo(cwd, name) {
|
|
56
|
+
const branch = run('git branch --show-current', cwd) || '(detached)';
|
|
57
|
+
const status = run('git status --porcelain', cwd);
|
|
58
|
+
const dirty = !!(status && status.length);
|
|
59
|
+
const main = detectMain(cwd);
|
|
60
|
+
const repo = { name, branch, dirty };
|
|
61
|
+
if (main) repo.main = main;
|
|
62
|
+
return repo;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 扫描空间根 ─────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(path.join(wsRoot, '.git'))) {
|
|
68
|
+
console.error('❌ ' + wsRoot + ' 不是 git 仓库');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const repos = [scanRepo(wsRoot, 'workspace')];
|
|
73
|
+
|
|
74
|
+
// ── 扫描 .codespace/ 各应用 ───────────────────────────────
|
|
75
|
+
|
|
76
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
77
|
+
if (fs.existsSync(codeDir)) {
|
|
78
|
+
for (const name of fs.readdirSync(codeDir).sort()) {
|
|
79
|
+
const appDir = path.join(codeDir, name);
|
|
80
|
+
if (fs.existsSync(path.join(appDir, '.git'))) {
|
|
81
|
+
repos.push(scanRepo(appDir, name));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 态判定 ─────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
for (const r of repos) {
|
|
89
|
+
const icon = (r.main && r.branch === r.main) ? '📄' : '🌿';
|
|
90
|
+
const dirtyTag = r.dirty ? '(有未提交修改)' : '';
|
|
91
|
+
const baseTag = (r.main && r.branch !== r.main) ? ' ← 主干 ' + r.main : '';
|
|
92
|
+
console.log(icon + ' ' + r.name + ': ' + r.branch + dirtyTag + baseTag);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const active = repos.filter(r => !(r.main && r.branch === r.main));
|
|
96
|
+
const branches = [...new Set(active.map(r => r.branch))];
|
|
97
|
+
|
|
98
|
+
let state;
|
|
99
|
+
if (active.length === 0) {
|
|
100
|
+
state = 'clean';
|
|
101
|
+
console.log('✅ 干净态:全部仓库在主干且可开工(模式选择不受限)');
|
|
102
|
+
} else {
|
|
103
|
+
state = 'active';
|
|
104
|
+
console.log('⚠️ 遗留态:检测到本地需求进行中(分支:' + branches.join(', ') + ')');
|
|
105
|
+
console.log(' 参与仓库:' + active.map(r => r.name).join(', '));
|
|
106
|
+
if (branches.length > 1) {
|
|
107
|
+
console.log(' ⚠️ 存在多个不同分支名——多头遗留,需用户逐个处理');
|
|
108
|
+
}
|
|
109
|
+
console.log(' 处理选项:继续该需求 / 先收尾 / 升级 worktree(git worktree add)/ 丢弃');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('---');
|
|
113
|
+
console.log('LOCAL_STATE', JSON.stringify({ state, branches, repos }));
|
|
114
|
+
process.exit(0);
|
|
@@ -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);
|