@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,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,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);
|