@jspg-ai/coding-bb 0.0.3-beta.9 → 0.0.4
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 +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 +469 -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 +469 -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 +133 -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 +469 -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 +212 -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 +469 -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
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
require('./silence-popup');
|
|
3
3
|
/**
|
|
4
|
-
* cbb-worktree-init 创建 Worktree
|
|
4
|
+
* cbb-worktree-init 创建 Worktree(自愈清理仅限元数据 prune)
|
|
5
5
|
*
|
|
6
6
|
* 用法:node create-worktrees.js <ws_root> <branch>
|
|
7
|
-
* (apps 由脚本内部从 <ws_root>/
|
|
7
|
+
* (apps 由脚本内部从 <ws_root>/CBB-WORKSPACE.json 读取)
|
|
8
8
|
*
|
|
9
9
|
* 行为:
|
|
10
|
-
* 1.
|
|
10
|
+
* 1. 自愈清理:仅 git worktree prune(元数据),**绝不删除任何物理目录**
|
|
11
|
+
* (0.0.3-beta.11 事故修复:旧版按"不在 worktree list"负向推断递归删残留目录,
|
|
12
|
+
* Windows 下 git 输出正斜杠路径与 realpathSync 反斜杠永不匹配,误删活跃 worktree)
|
|
11
13
|
* 2. workspace worktree 创建(.worktrees/worktree-<branch>)
|
|
12
14
|
* 3. 各 app worktree 创建(物理嵌套在 workspace worktree 内)
|
|
13
15
|
* 4. 失败时输出回滚提示
|
|
@@ -15,7 +17,7 @@ require('./silence-popup');
|
|
|
15
17
|
* 退出码:
|
|
16
18
|
* 0 - 全部成功
|
|
17
19
|
* 1 - 参数错误
|
|
18
|
-
* 2 - workspace worktree
|
|
20
|
+
* 2 - workspace worktree 创建失败(含目标目录存在但非有效 worktree 的残留场景)
|
|
19
21
|
* 3 - 部分 app worktree 创建失败
|
|
20
22
|
*/
|
|
21
23
|
|
|
@@ -27,12 +29,19 @@ const wsRoot = process.argv[2];
|
|
|
27
29
|
const branch = process.argv[3];
|
|
28
30
|
|
|
29
31
|
if (!wsRoot || !branch) {
|
|
30
|
-
console.error('❌ 用法:node create-worktrees.js <ws_root> <branch>');
|
|
32
|
+
console.error('❌ 用法:node create-worktrees.js <ws_root> <branch> [--apps "app1,app2"]');
|
|
31
33
|
process.exit(1);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// 可选 --apps 应用子集(逗号分隔;缺省 = 全量,ADR 0002 需求语境子集)
|
|
37
|
+
let appFilter = null;
|
|
38
|
+
const appsFlagIdx = process.argv.indexOf('--apps');
|
|
39
|
+
if (appsFlagIdx >= 0) {
|
|
40
|
+
appFilter = new Set((process.argv[appsFlagIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function loadAppsFromConfig() {
|
|
35
|
-
const configPath = path.join(wsRoot, '
|
|
44
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
36
45
|
if (!fs.existsSync(configPath)) return null;
|
|
37
46
|
try {
|
|
38
47
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -40,12 +49,25 @@ function loadAppsFromConfig() {
|
|
|
40
49
|
} catch (e) { return null; }
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
console.error('❌
|
|
52
|
+
const allApps = loadAppsFromConfig();
|
|
53
|
+
if (!allApps) {
|
|
54
|
+
console.error('❌ CBB-WORKSPACE.json 缺失或 apps 为空');
|
|
46
55
|
process.exit(1);
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
const apps = appFilter
|
|
59
|
+
? allApps.filter(a => appFilter.has(a.name))
|
|
60
|
+
: allApps;
|
|
61
|
+
|
|
62
|
+
if (appFilter) {
|
|
63
|
+
const unknown = [...appFilter].filter(n => !allApps.some(a => a.name === n));
|
|
64
|
+
if (unknown.length > 0) {
|
|
65
|
+
console.error('❌ --apps 中的应用不在 CBB-WORKSPACE.json:' + unknown.join(', '));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.log('ℹ️ 应用子集(' + apps.length + '/' + allApps.length + '):' + apps.map(a => a.name).join(', '));
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
function run(cmd, cwd) {
|
|
50
72
|
try {
|
|
51
73
|
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
@@ -54,52 +76,28 @@ function run(cmd, cwd) {
|
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
// 1.
|
|
58
|
-
console.log('===
|
|
79
|
+
// 1. 自愈清理(仅元数据:prune 只清理"目录已缺失/失效"的 worktree 注册项,不碰物理目录)
|
|
80
|
+
console.log('=== 自愈清理(仅 prune 元数据,不删物理目录)===');
|
|
59
81
|
|
|
60
|
-
// 1.1 prune 所有仓库
|
|
61
82
|
const allRepos = [wsRoot, ...apps.map(a => path.join(wsRoot, '.codespace', a.name))];
|
|
62
83
|
for (const repo of allRepos) {
|
|
63
84
|
if (fs.existsSync(repo)) {
|
|
64
85
|
run('git worktree prune', repo);
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
|
-
console.log('✅ 已 prune 所有仓库的 worktree
|
|
68
|
-
|
|
69
|
-
// 1.2 清理 .worktrees/ 下无元数据的物理目录
|
|
70
|
-
const worktreesDir = path.join(wsRoot, '.worktrees');
|
|
71
|
-
if (fs.existsSync(worktreesDir)) {
|
|
72
|
-
const validWts = run('git worktree list --porcelain', wsRoot);
|
|
73
|
-
const validPaths = new Set();
|
|
74
|
-
if (validWts.ok) {
|
|
75
|
-
validWts.out.split('\n').forEach(line => {
|
|
76
|
-
if (line.startsWith('worktree ')) {
|
|
77
|
-
validPaths.add(line.substring(9).trim());
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const entries = fs.readdirSync(worktreesDir, { withFileTypes: true });
|
|
83
|
-
for (const entry of entries) {
|
|
84
|
-
if (!entry.isDirectory()) continue;
|
|
85
|
-
const dir = path.join(worktreesDir, entry.name);
|
|
86
|
-
let absDir;
|
|
87
|
-
try { absDir = fs.realpathSync(dir); } catch { continue; }
|
|
88
|
-
if (!validPaths.has(absDir)) {
|
|
89
|
-
try {
|
|
90
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
-
console.log('🧹 已清理残留目录:' + entry.name);
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.log('⏳ 残留目录仍被占用,跳过:' + entry.name);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
88
|
+
console.log('✅ 已 prune 所有仓库的 worktree 元数据(物理目录一律保留)');
|
|
98
89
|
|
|
99
90
|
// 2. workspace worktree 创建
|
|
100
91
|
console.log('=== 创建 Workspace Worktree ===');
|
|
101
92
|
const wtDir = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
102
93
|
if (fs.existsSync(wtDir)) {
|
|
94
|
+
if (!fs.existsSync(path.join(wtDir, '.git'))) {
|
|
95
|
+
console.error('❌ 目标目录已存在但不是有效 worktree(缺 .git 指针):' + wtDir);
|
|
96
|
+
console.error(' 可能是上次 close / init 中断的残留。本脚本不会自动删除任何目录,请二选一后重跑:');
|
|
97
|
+
console.error(' 1) 执行 cbb-worktree-close 技能清理该需求');
|
|
98
|
+
console.error(' 2) 手动删除上述目录后执行:git -C "' + wsRoot + '" worktree prune');
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
103
101
|
console.log('⏭️ workspace worktree 已存在,复用:' + wtDir);
|
|
104
102
|
} else {
|
|
105
103
|
const created = run('git worktree add ".worktrees/worktree-' + branch + '" "' + branch + '"', wsRoot);
|
|
@@ -111,12 +109,13 @@ if (fs.existsSync(wtDir)) {
|
|
|
111
109
|
console.log('✅ workspace worktree 已创建:' + wtDir);
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
// 3. apps worktree
|
|
112
|
+
// 3. apps worktree 创建(物理嵌套于 workspace worktree 的 .codespace/ 容器内,镜像主布局,ADR 0003)
|
|
115
113
|
console.log('=== 创建 App Worktrees ===');
|
|
116
114
|
const results = [];
|
|
117
115
|
for (const app of apps) {
|
|
118
116
|
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
119
|
-
const nestedWt = path.join(wtDir, app.name);
|
|
117
|
+
const nestedWt = path.join(wtDir, '.codespace', app.name);
|
|
118
|
+
const legacyWt = path.join(wtDir, app.name);
|
|
120
119
|
|
|
121
120
|
if (fs.existsSync(nestedWt) && fs.existsSync(path.join(nestedWt, '.git'))) {
|
|
122
121
|
console.log('⏭️ ' + app.name + ': 嵌套 worktree 已存在,复用');
|
|
@@ -124,8 +123,23 @@ for (const app of apps) {
|
|
|
124
123
|
continue;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
//
|
|
126
|
+
// 防御性兜底:容器目录不存在则创建
|
|
128
127
|
fs.mkdirSync(path.dirname(nestedWt), { recursive: true });
|
|
128
|
+
|
|
129
|
+
// 存量迁移:≤beta.23 旧平铺布局 → .codespace/ 容器(worktree 平移,分支与提交随身带走)
|
|
130
|
+
if (fs.existsSync(legacyWt) && fs.existsSync(path.join(legacyWt, '.git'))) {
|
|
131
|
+
const moved = run('git worktree move "' + legacyWt + '" "' + nestedWt + '"', appPath);
|
|
132
|
+
if (moved.ok) {
|
|
133
|
+
console.log('🔁 ' + app.name + ': 旧平铺 worktree 已迁移到 .codespace/ 容器');
|
|
134
|
+
results.push({ name: app.name, status: 'migrated' });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
console.error('❌ ' + app.name + ': 旧平铺 worktree 迁移失败(常见原因:目录被 IDE 等进程占用):' + moved.err);
|
|
138
|
+
console.error(' 请关闭占用 ' + legacyWt + ' 的程序后重跑本技能;或先执行 cbb-worktree-close 再重建');
|
|
139
|
+
results.push({ name: app.name, status: 'failed', error: moved.err });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
const created = run('git worktree add "' + nestedWt + '" "' + branch + '"', appPath);
|
|
130
144
|
if (created.ok) {
|
|
131
145
|
console.log('✅ ' + app.name + ': 嵌套 worktree 创建成功');
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* worktree 模式 AI 配置 junction(ADR 0002,防线 B)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:node junction-ai-config.js <ws_root> <wt_path> [--remove]
|
|
6
|
+
*
|
|
7
|
+
* 目的:让开发会话能开进 worktree(工作树内加载 AI 配置),同时保持
|
|
8
|
+
* 「单层安装」实质——配置唯一物理副本在空间根,cbb update 一处生效。
|
|
9
|
+
*
|
|
10
|
+
* 策略(规避 ADR 0001 否决时的状态污染):
|
|
11
|
+
* - 只 junction **只读内容子目录**(rules / skills / commands):
|
|
12
|
+
* 工具向配置根写状态(如 settings.local.json)落在 worktree 自建的
|
|
13
|
+
* 物理(空壳)配置目录里,按项目隔离,不互相覆盖;
|
|
14
|
+
* - 配置根目录本身在 worktree 侧是物理目录(不是链接);
|
|
15
|
+
* - .opencode/opencode.json 为文件(junction 不支持文件):直接复制
|
|
16
|
+
* (cbb update 后需重跑本脚本同步;worktree 为瞬态,影响可控)。
|
|
17
|
+
*
|
|
18
|
+
* 幂等:linkPath 已指向同一 target → skip;指向不同 → 重建;
|
|
19
|
+
* 存在同名**物理目录**(非链接)→ warn 跳过(不覆盖用户内容)。
|
|
20
|
+
*
|
|
21
|
+
* --remove(cbb-worktree-close 调用):
|
|
22
|
+
* - 拆除 worktree 内已建 junction 与复制件,**源文件零接触**:
|
|
23
|
+
* 只解除链接本身(fs.rmSync 对 reparse point 仅删链接、不穿透目标,
|
|
24
|
+
* 已由 Windows 实测验证),复制件为普通文件直接删除;
|
|
25
|
+
* - 拆除前校验链接目标位于空间根内——目标在空间根外 / readlink 失败
|
|
26
|
+
* → 跳过并警告(防误删外部目录);同名物理目录/文件 → 跳过不碰。
|
|
27
|
+
*
|
|
28
|
+
* 退出码:0 成功(含全部 skip);1 参数错误;2 执行失败。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const wsRoot = process.argv[2];
|
|
35
|
+
const wtPath = process.argv[3];
|
|
36
|
+
const removeMode = process.argv[4] === '--remove';
|
|
37
|
+
|
|
38
|
+
if (!wsRoot || !wtPath) {
|
|
39
|
+
console.error('Usage: node junction-ai-config.js <ws_root> <wt_path> [--remove]');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 各工具的项目级配置根(存在才处理)与本包部署的内容子目录
|
|
44
|
+
const CONFIG_ROOTS = ['.claude', '.qoder', '.opencode', '.workbuddy', '.trae', '.minimax'];
|
|
45
|
+
const CONTENT_SUBDIRS = ['rules', 'skills', 'commands'];
|
|
46
|
+
// 文件级内容(复制而非 junction):opencode 的 instructions 登记
|
|
47
|
+
const CONTENT_FILES = { '.opencode': ['opencode.json'] };
|
|
48
|
+
|
|
49
|
+
const linked = [];
|
|
50
|
+
const copied = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
const warned = [];
|
|
53
|
+
let hasFailure = false;
|
|
54
|
+
|
|
55
|
+
/** 归一化 junction 目标路径(Windows readlink 返回 \\?\ 前缀) */
|
|
56
|
+
function normLinkTarget(p) {
|
|
57
|
+
return path.resolve(String(p).replace(/^\\\\\?\\/, ''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 目标是否位于空间根内(防误删空间根外目录的守卫) */
|
|
61
|
+
function isWithin(p, root) {
|
|
62
|
+
const r = path.resolve(root) + path.sep;
|
|
63
|
+
return path.resolve(p).startsWith(r);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── remove 模式:显式拆除 junction 与复制件(源文件零接触)──
|
|
67
|
+
if (removeMode) {
|
|
68
|
+
const removed = [];
|
|
69
|
+
const guarded = [];
|
|
70
|
+
|
|
71
|
+
for (const root of CONFIG_ROOTS) {
|
|
72
|
+
for (const sub of CONTENT_SUBDIRS) {
|
|
73
|
+
const linkPath = path.join(wtPath, root, sub);
|
|
74
|
+
const label = root + '/' + sub;
|
|
75
|
+
let st;
|
|
76
|
+
try { st = fs.lstatSync(linkPath); } catch (e) { continue; } // 不存在 → 跳过(dangling 链接 lstat 仍成功,可拆)
|
|
77
|
+
if (!st.isSymbolicLink()) continue; // 物理目录/文件(工具状态空壳或用户内容)→ 不碰
|
|
78
|
+
|
|
79
|
+
const target = normLinkTarget(fs.readlinkSync(linkPath));
|
|
80
|
+
if (!isWithin(target, wsRoot)) {
|
|
81
|
+
guarded.push(label + ' → ' + target + '(目标不在空间根内,跳过)');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// 只解除链接:Node 对 reparse point 仅删除链接本身,不递归、不触达目标内容
|
|
87
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
88
|
+
removed.push(label);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('❌ 拆除失败 ' + label + ' — ' + e.message);
|
|
91
|
+
hasFailure = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const file of (CONTENT_FILES[root] || [])) {
|
|
96
|
+
const linkPath = path.join(wtPath, root, file);
|
|
97
|
+
if (!fs.existsSync(linkPath)) continue;
|
|
98
|
+
try {
|
|
99
|
+
fs.unlinkSync(linkPath); // 复制件为普通文件,删除不影响源
|
|
100
|
+
removed.push(root + '/' + file);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('❌ 删除失败 ' + root + '/' + file + ' — ' + e.message);
|
|
103
|
+
hasFailure = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (removed.length) console.log('✅ 已拆除 junction/复制件:' + removed.join(', '));
|
|
109
|
+
if (guarded.length) console.log('⚠️ ' + guarded.join(';'));
|
|
110
|
+
if (removed.length === 0 && guarded.length === 0) console.log('ℹ️ 无待拆除项(幂等)');
|
|
111
|
+
|
|
112
|
+
console.log('---');
|
|
113
|
+
console.log('JUNCTION_REMOVE_RESULT', JSON.stringify({ removed, guarded, wt: wtPath }));
|
|
114
|
+
process.exit(hasFailure ? 2 : 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function linkDir(target, linkPath, label) {
|
|
118
|
+
if (fs.existsSync(linkPath)) {
|
|
119
|
+
const st = fs.lstatSync(linkPath);
|
|
120
|
+
if (st.isSymbolicLink()) {
|
|
121
|
+
const existing = normLinkTarget(fs.readlinkSync(linkPath));
|
|
122
|
+
if (existing === path.resolve(target)) {
|
|
123
|
+
skipped.push(label);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
fs.unlinkSync(linkPath); // 指向不同 → 重建
|
|
127
|
+
} else if (st.isDirectory()) {
|
|
128
|
+
warned.push(label + '(存在同名物理目录,未覆盖)');
|
|
129
|
+
return;
|
|
130
|
+
} else {
|
|
131
|
+
fs.unlinkSync(linkPath); // 同名文件,罕见,重建
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
135
|
+
try {
|
|
136
|
+
if (process.platform === 'win32') {
|
|
137
|
+
fs.symlinkSync(path.resolve(target), linkPath, 'junction');
|
|
138
|
+
} else {
|
|
139
|
+
fs.symlinkSync(path.resolve(target), linkPath, 'dir');
|
|
140
|
+
}
|
|
141
|
+
linked.push(label);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error('❌ junction 失败 ' + label + ' — ' + e.message);
|
|
144
|
+
hasFailure = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function copyFile(target, dest, label) {
|
|
149
|
+
if (fs.existsSync(dest) && fs.existsSync(target) &&
|
|
150
|
+
fs.readFileSync(dest, 'utf-8') === fs.readFileSync(target, 'utf-8')) {
|
|
151
|
+
skipped.push(label);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
156
|
+
fs.copyFileSync(target, dest);
|
|
157
|
+
copied.push(label);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error('❌ 复制失败 ' + label + ' — ' + e.message);
|
|
160
|
+
hasFailure = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const root of CONFIG_ROOTS) {
|
|
165
|
+
const srcRoot = path.join(wsRoot, root);
|
|
166
|
+
if (!fs.existsSync(srcRoot) || !fs.statSync(srcRoot).isDirectory()) continue;
|
|
167
|
+
|
|
168
|
+
for (const sub of CONTENT_SUBDIRS) {
|
|
169
|
+
const src = path.join(srcRoot, sub);
|
|
170
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
|
|
171
|
+
linkDir(src, path.join(wtPath, root, sub), root + '/' + sub);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const file of (CONTENT_FILES[root] || [])) {
|
|
175
|
+
const src = path.join(srcRoot, file);
|
|
176
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) continue;
|
|
177
|
+
copyFile(src, path.join(wtPath, root, file), root + '/' + file);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (linked.length === 0 && copied.length === 0 && skipped.length === 0 && warned.length === 0) {
|
|
182
|
+
console.log('ℹ️ 空间根无 AI 配置目录,跳过(cbb setup 选择工具后再装)');
|
|
183
|
+
} else {
|
|
184
|
+
if (linked.length) console.log('✅ junction:' + linked.join(', '));
|
|
185
|
+
if (copied.length) console.log('✅ 复制:' + copied.join(', '));
|
|
186
|
+
if (skipped.length) console.log('⏭️ 已就绪:' + skipped.join(', '));
|
|
187
|
+
if (warned.length) console.log('⚠️ ' + warned.join(';'));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('---');
|
|
191
|
+
console.log('JUNCTION_RESULT', JSON.stringify({ linked, copied, skipped, warned, wt: wtPath }));
|
|
192
|
+
process.exit(hasFailure ? 2 : 0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
require('./silence-popup');
|
|
3
3
|
/**
|
|
4
|
-
* cbb-worktree-init 读取并校验
|
|
4
|
+
* cbb-worktree-init 读取并校验 CBB-WORKSPACE.json
|
|
5
5
|
*
|
|
6
6
|
* 退出码:
|
|
7
7
|
* 0 - 成功,输出应用列表 JSON
|
|
@@ -15,10 +15,10 @@ require('./silence-popup');
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
|
-
const configPath = path.join(process.cwd(), '
|
|
18
|
+
const configPath = path.join(process.cwd(), 'CBB-WORKSPACE.json');
|
|
19
19
|
|
|
20
20
|
if (!fs.existsSync(configPath)) {
|
|
21
|
-
console.error('❌
|
|
21
|
+
console.error('❌ CBB-WORKSPACE.json 不存在');
|
|
22
22
|
console.error(' 请先在 cbb setup 阶段创建,或手动配置');
|
|
23
23
|
console.error('---');
|
|
24
24
|
console.error('CONFIG_MISSING');
|
|
@@ -29,7 +29,7 @@ let config;
|
|
|
29
29
|
try {
|
|
30
30
|
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
31
31
|
} catch (e) {
|
|
32
|
-
console.error('❌
|
|
32
|
+
console.error('❌ CBB-WORKSPACE.json 解析失败:', e.message);
|
|
33
33
|
console.error(' 请检查 JSON 格式');
|
|
34
34
|
console.error('---');
|
|
35
35
|
console.error('CONFIG_INVALID');
|
|
@@ -38,8 +38,8 @@ try {
|
|
|
38
38
|
|
|
39
39
|
const apps = config.apps || [];
|
|
40
40
|
if (apps.length === 0) {
|
|
41
|
-
console.error('❌
|
|
42
|
-
console.error(' 请编辑
|
|
41
|
+
console.error('❌ CBB-WORKSPACE.json 中 apps 为空');
|
|
42
|
+
console.error(' 请编辑 CBB-WORKSPACE.json 配置关联应用');
|
|
43
43
|
console.error('---');
|
|
44
44
|
console.error('CONFIG_EMPTY');
|
|
45
45
|
process.exit(2);
|
|
@@ -61,7 +61,7 @@ for (const app of apps) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
console.log('✅
|
|
64
|
+
console.log('✅ CBB-WORKSPACE.json 有效(' + apps.length + ' 个应用)');
|
|
65
65
|
console.log('---');
|
|
66
66
|
console.log('CONFIG_OK', JSON.stringify(apps));
|
|
67
67
|
process.exit(0);
|
|
@@ -20,7 +20,7 @@ const fs = require('fs');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
22
|
function loadAppsFromConfig(wsRoot) {
|
|
23
|
-
const configPath = path.join(wsRoot, '
|
|
23
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
24
24
|
if (!fs.existsSync(configPath)) return null;
|
|
25
25
|
try {
|
|
26
26
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -40,7 +40,7 @@ function executePush(opts) {
|
|
|
40
40
|
const branch = opts.branch;
|
|
41
41
|
const apps = opts.apps != null ? opts.apps : loadAppsFromConfig(wsRoot);
|
|
42
42
|
if (apps == null) {
|
|
43
|
-
throw new Error('
|
|
43
|
+
throw new Error('CBB-WORKSPACE.json missing or apps empty');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const results = [];
|
|
@@ -68,7 +68,7 @@ function main() {
|
|
|
68
68
|
|
|
69
69
|
const apps = loadAppsFromConfig(wsRoot);
|
|
70
70
|
if (apps == null) {
|
|
71
|
-
console.error('❌
|
|
71
|
+
console.error('❌ CBB-WORKSPACE.json 缺失或 apps 为空');
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -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 {
|
|
@@ -4,7 +4,7 @@ require('./silence-popup');
|
|
|
4
4
|
* cbb-worktree-init 同步 .codespace/ 仓库
|
|
5
5
|
*
|
|
6
6
|
* 用法:node sync-repos.js <ws_root>
|
|
7
|
-
* - ws_root: 工作空间根绝对路径(apps 从 <ws_root>/
|
|
7
|
+
* - ws_root: 工作空间根绝对路径(apps 从 <ws_root>/CBB-WORKSPACE.json 读取)
|
|
8
8
|
*
|
|
9
9
|
* 对每个 app:
|
|
10
10
|
* - .codespace/<name>/.git 存在 → fetch --all
|
|
@@ -19,6 +19,7 @@ require('./silence-popup');
|
|
|
19
19
|
const { execSync } = require('child_process');
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
|
+
const { installTo: installGitHook } = require('./install-git-hook');
|
|
22
23
|
|
|
23
24
|
const wsRoot = process.argv[2];
|
|
24
25
|
|
|
@@ -28,23 +29,23 @@ if (!wsRoot) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* 从
|
|
32
|
+
* 从 CBB-WORKSPACE.json 读取 apps(避免 shell 字符串传递中的转义问题)
|
|
32
33
|
*/
|
|
33
34
|
function loadAppsFromConfig() {
|
|
34
|
-
const configPath = path.join(wsRoot, '
|
|
35
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
35
36
|
if (!fs.existsSync(configPath)) {
|
|
36
|
-
console.error('❌
|
|
37
|
+
console.error('❌ CBB-WORKSPACE.json 不存在:' + configPath);
|
|
37
38
|
return null;
|
|
38
39
|
}
|
|
39
40
|
try {
|
|
40
41
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
41
42
|
if (!config.apps || !Array.isArray(config.apps) || config.apps.length === 0) {
|
|
42
|
-
console.error('❌
|
|
43
|
+
console.error('❌ CBB-WORKSPACE.json 中 apps 为空');
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
46
|
return config.apps;
|
|
46
47
|
} catch (e) {
|
|
47
|
-
console.error('❌
|
|
48
|
+
console.error('❌ CBB-WORKSPACE.json 解析失败:' + e.message);
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -77,7 +78,10 @@ for (const app of apps) {
|
|
|
77
78
|
// 不存在:clone
|
|
78
79
|
try {
|
|
79
80
|
execSync('git clone -q "' + app.repo + '" "' + target + '"', { stdio: 'pipe' });
|
|
80
|
-
|
|
81
|
+
// 兜底 clone 出的应用同步装分支感知 pre-commit hook(ADR 0002;
|
|
82
|
+
// cbb setup 首次 clone 已装,此处幂等补齐)
|
|
83
|
+
installGitHook(target);
|
|
84
|
+
console.log('✅ ' + app.name + ': 代码已拉取(clone + 提交防线 hook)');
|
|
81
85
|
results.push({ name: app.name, status: 'cloned' });
|
|
82
86
|
} catch (e) {
|
|
83
87
|
console.error('❌ ' + app.name + ': clone 失败 — ' + app.repo);
|
|
@@ -7,8 +7,8 @@ require('./silence-popup');
|
|
|
7
7
|
*
|
|
8
8
|
* 行为:
|
|
9
9
|
* 1. stash 当前未提交修改(仅 untracked 时不产生 stash)
|
|
10
|
-
* 2. 目标分支不存在则从
|
|
11
|
-
* 顺序无关自愈;create-branches.js 后续会报 reused)
|
|
10
|
+
* 2. 目标分支不存在则从 <remote>/<main> 创建(首跑时分支尚未由 create-branches.js 建立,
|
|
11
|
+
* 顺序无关自愈;create-branches.js 后续会报 reused;基线不取 HEAD,见 ADR 0002)
|
|
12
12
|
* 3. 切换到目标分支
|
|
13
13
|
* 4. 检查 .worktrees/ 和 .codespace/ 是否在 .gitignore,缺失则添加并提交
|
|
14
14
|
* 5. 切回原分支
|
|
@@ -81,16 +81,28 @@ if (status.ok && status.out) {
|
|
|
81
81
|
console.log('ℹ️ 无未提交修改,跳过 stash');
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// 2.1
|
|
84
|
+
// 2.1 目标分支不存在则创建(Step 3.2 先于 Step 3.3 执行时分支尚未建立)
|
|
85
|
+
// 基线取 <remote>/<main>(ADR 0002,与 create-branches.js 一致):本地模式进行中根可能检出在
|
|
86
|
+
// feature 分支,从 HEAD 建会把未合并的开发态带进新分支;无 remote 时回退 HEAD
|
|
85
87
|
const branchList = run('git branch --list "' + branch + '"', wsRoot);
|
|
86
88
|
if (branchList.ok && !branchList.out) {
|
|
87
|
-
const
|
|
89
|
+
const remoteOut = run('git remote', wsRoot);
|
|
90
|
+
let base = 'HEAD';
|
|
91
|
+
if (remoteOut.ok && remoteOut.out) {
|
|
92
|
+
const r = remoteOut.out.split(/\r?\n/)[0];
|
|
93
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', wsRoot);
|
|
94
|
+
const main = (symbolic.ok && symbolic.out)
|
|
95
|
+
? symbolic.out.replace('refs/remotes/' + r + '/', '')
|
|
96
|
+
: 'main';
|
|
97
|
+
base = r + '/' + main;
|
|
98
|
+
}
|
|
99
|
+
const create = run('git branch "' + branch + '" "' + base + '"', wsRoot);
|
|
88
100
|
if (!create.ok) {
|
|
89
101
|
console.error('❌ 创建分支 ' + branch + ' 失败:' + create.err);
|
|
90
102
|
if (stashed) run('git stash pop', wsRoot);
|
|
91
103
|
process.exit(2);
|
|
92
104
|
}
|
|
93
|
-
console.log('✅ 分支 ' + branch + ' 不存在,已从
|
|
105
|
+
console.log('✅ 分支 ' + branch + ' 不存在,已从 ' + base + ' 创建');
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
// 3. 切换到目标分支
|