@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,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);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 收尾合并门禁 —— 废弃分支出口(ADR 0007)
|
|
5
|
+
*
|
|
6
|
+
* 用户在门禁处确认"分支废弃、不再交付"后,把各参与仓库远端的需求分支重命名为
|
|
7
|
+
* <branch>-deprecated(保留历史、去掉占用中的旧名),**不删除任何提交**。
|
|
8
|
+
* 本地分支保持原名不动;未推送的仓库(远端无该分支)跳过。
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node deprecate-branches.js <ws_root> <branch> [--apps a,b]
|
|
12
|
+
*
|
|
13
|
+
* 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
|
|
14
|
+
* 末行:DEPRECATE_RESULT [...]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
function run(cmd, cwd) {
|
|
22
|
+
try {
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
out: execSync(cmd, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
+
timeout: 120000,
|
|
30
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
31
|
+
}).trim(),
|
|
32
|
+
};
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadApps(wsRoot) {
|
|
39
|
+
try {
|
|
40
|
+
const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
41
|
+
return Array.isArray(config.apps) ? config.apps : [];
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 对单个仓库执行废弃重命名 */
|
|
48
|
+
function deprecateRepo(name, repo, branch) {
|
|
49
|
+
const newName = branch + '-deprecated';
|
|
50
|
+
const rec = { name, status: 'skipped', detail: null };
|
|
51
|
+
if (!fs.existsSync(path.join(repo, '.git'))) {
|
|
52
|
+
rec.detail = 'no-repo';
|
|
53
|
+
return rec;
|
|
54
|
+
}
|
|
55
|
+
const remote = run('git remote', repo);
|
|
56
|
+
if (!remote.ok || !remote.out) {
|
|
57
|
+
rec.detail = 'no-remote';
|
|
58
|
+
return rec;
|
|
59
|
+
}
|
|
60
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
61
|
+
// 仅重命名远端确实存在的分支
|
|
62
|
+
const exists = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
|
|
63
|
+
if (!exists.ok || !exists.out) {
|
|
64
|
+
rec.detail = 'remote-has-no-branch';
|
|
65
|
+
return rec;
|
|
66
|
+
}
|
|
67
|
+
const pushNew = run('git push "' + r + '" "refs/heads/' + branch + ':refs/heads/' + newName + '"', repo);
|
|
68
|
+
if (!pushNew.ok) {
|
|
69
|
+
rec.status = 'failed';
|
|
70
|
+
rec.detail = 'push-new-failed: ' + (pushNew.err || '').split(/\r?\n/)[0];
|
|
71
|
+
return rec;
|
|
72
|
+
}
|
|
73
|
+
const delOld = run('git push "' + r + '" --delete "' + branch + '"', repo);
|
|
74
|
+
if (!delOld.ok) {
|
|
75
|
+
rec.status = 'failed';
|
|
76
|
+
rec.detail = 'delete-old-failed: ' + (delOld.err || '').split(/\r?\n/)[0];
|
|
77
|
+
return rec;
|
|
78
|
+
}
|
|
79
|
+
rec.status = 'deprecated';
|
|
80
|
+
rec.detail = 'remote ' + branch + ' → ' + newName;
|
|
81
|
+
return rec;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
const args = process.argv.slice(2);
|
|
86
|
+
const wsRoot = args[0];
|
|
87
|
+
const branch = args[1];
|
|
88
|
+
|
|
89
|
+
if (!wsRoot || !branch || branch.indexOf('--') === 0) {
|
|
90
|
+
console.error('Usage: node deprecate-branches.js <ws_root> <branch> [--apps a,b]');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const appsIdx = args.indexOf('--apps');
|
|
95
|
+
const appFilter = appsIdx >= 0
|
|
96
|
+
? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
100
|
+
for (const app of loadApps(wsRoot)) {
|
|
101
|
+
if (appFilter && !appFilter.has(app.name)) continue;
|
|
102
|
+
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
|
|
106
|
+
|
|
107
|
+
results.forEach(r => {
|
|
108
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : ''));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const ok = results.every(r => r.status !== 'failed');
|
|
112
|
+
console.log('---');
|
|
113
|
+
console.log('DEPRECATE_RESULT', JSON.stringify({ branch, new_name: branch + '-deprecated', results }));
|
|
114
|
+
process.exit(ok ? 0 : 2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (require.main === module) main();
|
|
118
|
+
|
|
119
|
+
module.exports = { run, loadApps, deprecateRepo };
|
|
@@ -1,96 +1,148 @@
|
|
|
1
|
-
require('./silence-popup');
|
|
2
|
-
/**
|
|
3
|
-
* openspec-shared 目标需求名识别
|
|
4
|
-
*
|
|
5
|
-
* 唯一真源:cbb/worktrees/_shared/scripts/find-target-worktree.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(
|
|
7
|
-
*
|
|
8
|
-
* 用法:node find-target-worktree.js <ws_root>
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 1. cwd 在 .worktrees/worktree-xxx/ 内 → 自动从路径反推
|
|
12
|
-
* 2.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.
|
|
68
|
-
console.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
require('./silence-popup');
|
|
2
|
+
/**
|
|
3
|
+
* openspec-shared 目标需求名识别
|
|
4
|
+
*
|
|
5
|
+
* 唯一真源:cbb/worktrees/_shared/scripts/find-target-worktree.js
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(close / push)。
|
|
7
|
+
*
|
|
8
|
+
* 用法:node find-target-worktree.js <ws_root>
|
|
9
|
+
*
|
|
10
|
+
* 优先级(ADR 0002 双布局):
|
|
11
|
+
* 1. cwd 在 .worktrees/worktree-xxx/ 内 → 自动从路径反推
|
|
12
|
+
* 2. 收集候选:worktree 目录(.worktrees/worktree-xxx)
|
|
13
|
+
* + 本地布局(根 / .codespace/<app>/ 检出在非主干分支,本地模式进行中)
|
|
14
|
+
* 同一需求名两种布局共存时合并为一项(标 both)
|
|
15
|
+
* 3. 唯一候选 → 自动选用;多个 → 按 mtime 倒序列出(本地布局用分支检出时间)
|
|
16
|
+
*
|
|
17
|
+
* 输出:
|
|
18
|
+
* 末行输出需求名(用于 AI agent 解析)
|
|
19
|
+
* 多个时按 [1] xxx ⭐ / [2] yyy 列出
|
|
20
|
+
*
|
|
21
|
+
* 退出码:
|
|
22
|
+
* 0 - 唯一识别(自动或唯一)
|
|
23
|
+
* 1 - 多个候选,需用户输入
|
|
24
|
+
* 2 - 无候选(无 worktree 且无本地布局检出)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { execSync } = require('child_process');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const wsRoot = process.argv[2];
|
|
32
|
+
|
|
33
|
+
if (!wsRoot) {
|
|
34
|
+
console.error('❌ 用法:node find-target-worktree.js <ws_root>');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(cmd, cwd) {
|
|
39
|
+
try {
|
|
40
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
41
|
+
} catch (e) { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function currentBranch(cwd) {
|
|
45
|
+
return run('git branch --show-current', cwd) || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 主干名(探测失败返回 null → 该仓库任何非空检出都视为候选) */
|
|
49
|
+
function detectMain(cwd) {
|
|
50
|
+
const remotes = run('git remote', cwd);
|
|
51
|
+
if (remotes) {
|
|
52
|
+
const r = remotes.split(/\r?\n/)[0];
|
|
53
|
+
const sym = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
|
|
54
|
+
if (sym) return sym.replace('refs/remotes/' + r + '/', '');
|
|
55
|
+
}
|
|
56
|
+
if (run('git rev-parse --verify --quiet refs/heads/main', cwd)) return 'main';
|
|
57
|
+
if (run('git rev-parse --verify --quiet refs/heads/master', cwd)) return 'master';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 优先级 1:cwd 反推
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const cwdMatch = cwd.match(/\.worktrees[\\\/]worktree-([^\\\/]+)/);
|
|
64
|
+
if (cwdMatch) {
|
|
65
|
+
const branch = cwdMatch[1];
|
|
66
|
+
console.log('✅ 从 cwd 反推需求名:' + branch);
|
|
67
|
+
console.log('---');
|
|
68
|
+
console.log(branch);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 优先级 2a:worktree 目录候选
|
|
73
|
+
const candidates = new Map(); // name → { layouts: Set, mtime }
|
|
74
|
+
|
|
75
|
+
const worktreesDir = path.join(wsRoot, '.worktrees');
|
|
76
|
+
if (fs.existsSync(worktreesDir)) {
|
|
77
|
+
const entries = fs.readdirSync(worktreesDir, { withFileTypes: true })
|
|
78
|
+
.filter(e => e.isDirectory() && e.name.startsWith('worktree-'));
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
const name = e.name.substring('worktree-'.length);
|
|
81
|
+
const full = path.join(worktreesDir, e.name);
|
|
82
|
+
const item = candidates.get(name) || { layouts: new Set(), mtime: 0 };
|
|
83
|
+
item.layouts.add('worktree');
|
|
84
|
+
item.mtime = Math.max(item.mtime, fs.statSync(full).mtime.getTime());
|
|
85
|
+
candidates.set(name, item);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 优先级 2b:本地布局候选(根 + .codespace/<app>/ 检出在非主干分支)
|
|
90
|
+
function addLocalCandidate(name, cwdPath) {
|
|
91
|
+
if (!name) return;
|
|
92
|
+
const item = candidates.get(name) || { layouts: new Set(), mtime: 0 };
|
|
93
|
+
item.layouts.add('local');
|
|
94
|
+
try {
|
|
95
|
+
item.mtime = Math.max(item.mtime, fs.statSync(path.join(cwdPath, '.git')).mtime.getTime());
|
|
96
|
+
} catch (_) { /* .git 状态时间不可读则保持 */ }
|
|
97
|
+
candidates.set(name, item);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rootMain = detectMain(wsRoot);
|
|
101
|
+
const rootBranch = currentBranch(wsRoot);
|
|
102
|
+
if (rootBranch && (!rootMain || rootBranch !== rootMain)) {
|
|
103
|
+
addLocalCandidate(rootBranch, wsRoot);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
107
|
+
if (fs.existsSync(codeDir)) {
|
|
108
|
+
for (const app of fs.readdirSync(codeDir).sort()) {
|
|
109
|
+
const appDir = path.join(codeDir, app);
|
|
110
|
+
if (!fs.existsSync(path.join(appDir, '.git'))) continue;
|
|
111
|
+
const appMain = detectMain(appDir);
|
|
112
|
+
const appBranch = currentBranch(appDir);
|
|
113
|
+
if (appBranch && (!appMain || appBranch !== appMain)) {
|
|
114
|
+
addLocalCandidate(appBranch, appDir);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (candidates.size === 0) {
|
|
120
|
+
console.error('❌ 未找到任何进行中的需求(无 worktree,且无仓库检出在非主干分支)');
|
|
121
|
+
console.error(' worktree 模式请先执行 cbb-worktree-init;本地模式请先执行 cbb-local-mode');
|
|
122
|
+
console.log('---');
|
|
123
|
+
console.log('NO_WORKTREE');
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 唯一候选 → 自动选用
|
|
128
|
+
if (candidates.size === 1) {
|
|
129
|
+
const [name] = [...candidates.keys()];
|
|
130
|
+
const layouts = [...candidates.get(name).layouts].join('+');
|
|
131
|
+
console.log('✅ 自动选用唯一进行中需求:' + name + '(' + layouts + ')');
|
|
132
|
+
console.log('---');
|
|
133
|
+
console.log(name);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 多候选 → 按 mtime 倒序列出
|
|
138
|
+
const sorted = [...candidates.entries()].sort((a, b) => b[1].mtime - a[1].mtime);
|
|
139
|
+
console.log('⚠️ 检测到 ' + sorted.length + ' 个进行中的需求(按最近活动排序):');
|
|
140
|
+
sorted.forEach(([name, item], i) => {
|
|
141
|
+
const layouts = [...item.layouts].join('+');
|
|
142
|
+
const tag = i === 0 ? ' ⭐ (推荐:最近活动)' : '';
|
|
143
|
+
console.log(' [' + (i + 1) + '] ' + name + '(' + layouts + ')' + tag);
|
|
144
|
+
});
|
|
145
|
+
console.log('请在对话中输入序号(1/2/3)或完整分支名:');
|
|
146
|
+
console.log('---');
|
|
147
|
+
console.log('MULTIPLE_WORKTREEES');
|
|
148
|
+
process.exit(1);
|
|
@@ -3,10 +3,10 @@ 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
|
-
* 优先:git worktree list 第一行(主仓库根目录,不受 worktree 内
|
|
9
|
-
* 兜底:当前目录含
|
|
8
|
+
* 优先:git worktree list 第一行(主仓库根目录,不受 worktree 内 CBB-WORKSPACE.json 副本干扰)
|
|
9
|
+
* 兜底:当前目录含 CBB-WORKSPACE.json
|
|
10
10
|
*
|
|
11
11
|
* 退出码:
|
|
12
12
|
* 0 - 定位成功,输出 WS_ROOT 绝对路径
|
|
@@ -44,19 +44,19 @@ if (worktreeList.ok) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// 兜底:当前目录含
|
|
47
|
+
// 兜底:当前目录含 CBB-WORKSPACE.json
|
|
48
48
|
if (!wsRoot) {
|
|
49
49
|
const cwd = process.cwd();
|
|
50
|
-
if (fs.existsSync(path.join(cwd, '
|
|
50
|
+
if (fs.existsSync(path.join(cwd, 'CBB-WORKSPACE.json'))) {
|
|
51
51
|
wsRoot = cwd;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// 校验
|
|
56
|
-
if (!wsRoot || !fs.existsSync(path.join(wsRoot, '
|
|
56
|
+
if (!wsRoot || !fs.existsSync(path.join(wsRoot, 'CBB-WORKSPACE.json'))) {
|
|
57
57
|
console.error('❌ 未找到工作空间根目录');
|
|
58
|
-
console.error(' 原因:git worktree list 失败且当前目录无
|
|
59
|
-
console.error(' 请在业务工作空间根目录(由 cbb setup 初始化、包含
|
|
58
|
+
console.error(' 原因:git worktree list 失败且当前目录无 CBB-WORKSPACE.json');
|
|
59
|
+
console.error(' 请在业务工作空间根目录(由 cbb setup 初始化、包含 CBB-WORKSPACE.json)下执行本技能');
|
|
60
60
|
process.exit(1);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -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 只放行控制面白名单,应用全拦(merge 提交除外);非 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// merge 进行中(MERGE_HEAD 存在)→ 显式人工合并提交,放行(ADR 0007 合并门禁)\nif (run('git rev-parse --verify --quiet MERGE_HEAD')) 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 仓库 = 检出含 CBB-WORKSPACE.json(或旧名,改名过渡期识别)\nconst isWorkspace = fs.existsSync('CBB-WORKSPACE.json') || 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('控制面白名单:CBB-WORKSPACE.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 // 旧名仅放行删除条目:更名迁移提交(git mv)会产生旧名删除 + 新名新增两条\n const EXACT = new Set(['CBB-WORKSPACE.json', '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
|
+
}
|