@jspg-ai/coding-bb 0.0.3-beta.8 → 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 +136 -123
- 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 +37 -9
- 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
|
@@ -7,7 +7,9 @@ require('./silence-popup');
|
|
|
7
7
|
* node commit-worktrees.js <wt_path> --message "msg" [options]
|
|
8
8
|
*
|
|
9
9
|
* 行为:
|
|
10
|
-
* 1.
|
|
10
|
+
* 1. 按需求名发现参与仓库(ADR 0002 双布局):
|
|
11
|
+
* - worktree 布局:.worktrees/worktree-<branch>/ 的 workspace + 其 .codespace/<app>/ 容器内各 app worktree(镜像主布局,ADR 0003)
|
|
12
|
+
* - 本地布局:根 / .codespace/<app>/ 检出恰在 <branch> 分支上
|
|
11
13
|
* 2. 对每个仓库:
|
|
12
14
|
* a. git status --porcelain 检测 dirty
|
|
13
15
|
* b. 若 clean 或已有 ahead commit → 跳过
|
|
@@ -95,23 +97,79 @@ function isGitRepo(p) {
|
|
|
95
97
|
return fs.existsSync(path.join(p, '.git'));
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/** 读取 CBB-WORKSPACE.json 的 app 名列表(旧布局兼容判定用;缺失返回 []) */
|
|
101
|
+
function loadAppNames(wsRoot) {
|
|
102
|
+
try {
|
|
103
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
|
|
104
|
+
return Array.isArray(cfg.apps) ? cfg.apps.map(a => a.name).filter(Boolean) : [];
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function currentBranch(p) {
|
|
111
|
+
try {
|
|
112
|
+
return execSync('git branch --show-current', { cwd: p, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
113
|
+
} catch (e) { return ''; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 按需求名发现参与仓库(ADR 0002 双布局):
|
|
118
|
+
* 布局一 worktree:.worktrees/worktree-<branch>/ 的 workspace + 其 .codespace/<app>/ 容器内 app worktree(ADR 0003 镜像主布局;≤beta.23 旧平铺顶层布局按配置 app 名兼容)
|
|
119
|
+
* 布局二 本地模式:根 / .codespace/<app>/ 的检出恰在 <branch> 分支上(同名仓库以 worktree 布局优先)
|
|
120
|
+
* 两者皆空 → error。
|
|
121
|
+
*/
|
|
98
122
|
function scanWorktreeRepos(wsRoot, branch) {
|
|
99
123
|
const wtRoot = path.join(wsRoot, '.worktrees', 'worktree-' + branch);
|
|
100
124
|
const repos = [];
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
|
|
126
|
+
// 布局一:worktree
|
|
127
|
+
if (fs.existsSync(wtRoot)) {
|
|
128
|
+
if (isGitRepo(wtRoot)) {
|
|
129
|
+
repos.push({ name: 'workspace', wt: wtRoot, repo: wsRoot, layout: 'worktree' });
|
|
130
|
+
}
|
|
131
|
+
// app worktree 收拢在 .codespace/ 容器内(镜像主布局,ADR 0003)
|
|
132
|
+
const appContainer = path.join(wtRoot, '.codespace');
|
|
133
|
+
if (fs.existsSync(appContainer)) {
|
|
134
|
+
for (const e of fs.readdirSync(appContainer, { withFileTypes: true })) {
|
|
135
|
+
if (!e.isDirectory()) continue;
|
|
136
|
+
const appWt = path.join(appContainer, e.name);
|
|
137
|
+
if (!isGitRepo(appWt)) continue;
|
|
138
|
+
repos.push({ name: e.name, wt: appWt, repo: path.join(wsRoot, '.codespace', e.name), layout: 'worktree' });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 兼容 ≤beta.23 旧平铺布局:按配置 app 名精确匹配(避免把根仓库自带的嵌套 git 目录误判为 app)
|
|
142
|
+
for (const name of loadAppNames(wsRoot)) {
|
|
143
|
+
if (repos.some(r => r.name === name)) continue;
|
|
144
|
+
const legacyWt = path.join(wtRoot, name);
|
|
145
|
+
if (fs.existsSync(path.join(legacyWt, '.git'))) {
|
|
146
|
+
console.error('⚠️ ' + name + ' 仍是旧平铺布局(' + legacyWt + '),建议重跑 cbb-worktree-init 迁移到 .codespace/ 容器');
|
|
147
|
+
repos.push({ name, wt: legacyWt, repo: path.join(wsRoot, '.codespace', name), layout: 'worktree' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
103
150
|
}
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
|
|
152
|
+
// 布局二:本地模式(检出在需求分支上)
|
|
153
|
+
const names = new Set(repos.map(r => r.name));
|
|
154
|
+
if (!names.has('workspace') && isGitRepo(wsRoot) && currentBranch(wsRoot) === branch) {
|
|
155
|
+
repos.push({ name: 'workspace', wt: wsRoot, repo: wsRoot, layout: 'local' });
|
|
106
156
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
157
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
158
|
+
if (fs.existsSync(codeDir)) {
|
|
159
|
+
for (const name of fs.readdirSync(codeDir).sort()) {
|
|
160
|
+
if (names.has(name)) continue;
|
|
161
|
+
const appDir = path.join(codeDir, name);
|
|
162
|
+
if (!isGitRepo(appDir)) continue;
|
|
163
|
+
if (currentBranch(appDir) === branch) {
|
|
164
|
+
repos.push({ name, wt: appDir, repo: appDir, layout: 'local' });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
113
167
|
}
|
|
114
|
-
|
|
168
|
+
|
|
169
|
+
const error = repos.length === 0
|
|
170
|
+
? ('未发现需求 ' + branch + ' 的参与仓库(worktree 不存在,且无仓库检出在 ' + branch + ' 分支上)')
|
|
171
|
+
: null;
|
|
172
|
+
return { wtRoot, repos, error };
|
|
115
173
|
}
|
|
116
174
|
|
|
117
175
|
function run(cmd, cwd) {
|
|
@@ -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,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* cbb-worktree-push MR 链接生成(预填 URL + 可选开浏览器)
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* node make-mr-urls.js <ws_root> <branch> <target> [--platform gitlab|github|gitee] [--open]
|
|
8
|
+
* node make-mr-urls.js <wt_path> <target> [...] # wt_path 形态同 push-worktrees
|
|
9
|
+
*
|
|
10
|
+
* 行为(对 workspace worktree + 各 app worktree 逐仓库):
|
|
11
|
+
* 1. git ls-remote 校验 feature 分支已在远端(push 过)、目标分支存在
|
|
12
|
+
* 2. git rev-list 判断与目标分支有无差异(无差异 → no-diff 跳过)
|
|
13
|
+
* 3. 解析 origin URL 判定平台(github.com / gitee.com,其余默认 GitLab;--platform 强制)
|
|
14
|
+
* 4. 生成预填 MR/PR 创建 URL;--open 时逐个开浏览器
|
|
15
|
+
*
|
|
16
|
+
* 安全红线:只生成链接,绝不代为合并、不触碰任何凭据(提交动作由人在平台页面完成)
|
|
17
|
+
*
|
|
18
|
+
* 退出码:
|
|
19
|
+
* 0 - 正常(含全部跳过)
|
|
20
|
+
* 1 - 参数错误
|
|
21
|
+
* 2 - 致命(worktree 不存在 / 无仓库)
|
|
22
|
+
*
|
|
23
|
+
* 末行 MR_RESULT [{name,status,url|reason}]
|
|
24
|
+
* status: ok | no-diff | not-pushed | no-target | manual | no-remote
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { execSync, spawn } = require('child_process');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { scanWorktreeRepos } = require('./push-worktrees');
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const flags = {};
|
|
33
|
+
const pos = [];
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
if (a === '--open') flags.open = true;
|
|
37
|
+
else if (a === '--platform') { flags.platform = argv[++i]; }
|
|
38
|
+
else if (a.startsWith('--platform=')) { flags.platform = a.split('=')[1]; }
|
|
39
|
+
else pos.push(a);
|
|
40
|
+
}
|
|
41
|
+
let inputs;
|
|
42
|
+
if (pos.length === 2) {
|
|
43
|
+
const wtPath = path.resolve(pos[0]);
|
|
44
|
+
const m = wtPath.match(/^(.+)[\\\/]\.worktrees[\\\/]worktree-(.+)$/);
|
|
45
|
+
if (!m) return { error: '两参数形式时 argv[1] 必须是 <ws_root>/.worktrees/worktree-<branch> 形态' };
|
|
46
|
+
inputs = { wsRoot: m[1], branch: m[2], target: pos[1] };
|
|
47
|
+
} else if (pos.length === 3) {
|
|
48
|
+
inputs = { wsRoot: path.resolve(pos[0]), branch: pos[1], target: pos[2] };
|
|
49
|
+
} else {
|
|
50
|
+
return { error: '用法:node make-mr-urls.js <ws_root> <branch> <target> [--platform X] [--open] | node make-mr-urls.js <wt_path> <target> [--open]' };
|
|
51
|
+
}
|
|
52
|
+
if (flags.platform && !['gitlab', 'github', 'gitee'].includes(flags.platform)) {
|
|
53
|
+
return { error: '--platform 仅支持 gitlab / github / gitee' };
|
|
54
|
+
}
|
|
55
|
+
return { ...inputs, ...flags };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 解析 git remote URL → { webBase }(浏览器可点的仓库主页,无 .git 后缀) */
|
|
59
|
+
function parseRemoteUrl(url) {
|
|
60
|
+
const u = (url || '').trim().replace(/\.git$/, '');
|
|
61
|
+
if (!u) return null;
|
|
62
|
+
// scp 式 ssh:git@host:group/proj
|
|
63
|
+
let m = u.match(/^[^@/\s]+@([^:]+):(.+)$/);
|
|
64
|
+
if (m) return { host: m[1], repoPath: m[2], webBase: 'https://' + m[1] + '/' + m[2] };
|
|
65
|
+
// 显式 ssh:// 或 http(s)://
|
|
66
|
+
m = u.match(/^(ssh|https?):\/\/(?:[^@/\s]+@)?([^/]+)\/(.+)$/);
|
|
67
|
+
if (m) {
|
|
68
|
+
const scheme = m[1] === 'ssh' ? 'https' : m[1];
|
|
69
|
+
return { host: m[2], repoPath: m[3], webBase: scheme + '://' + m[2] + '/' + m[3] };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectPlatform(remoteUrlStr, override) {
|
|
75
|
+
if (override) return override;
|
|
76
|
+
const parsed = parseRemoteUrl(remoteUrlStr);
|
|
77
|
+
if (!parsed) return null;
|
|
78
|
+
const host = parsed.host.toLowerCase();
|
|
79
|
+
if (host === 'github.com' || host.endsWith('.github.com')) return 'github';
|
|
80
|
+
if (host === 'gitee.com') return 'gitee';
|
|
81
|
+
// 企业自建以 GitLab 为绝对主流;误判时 --platform 强制覆盖
|
|
82
|
+
return 'gitlab';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 生成预填 MR/PR 创建 URL(branch/target 做 URL 编码) */
|
|
86
|
+
function buildMrUrl(platform, webBase, branch, target) {
|
|
87
|
+
const b = encodeURIComponent(branch);
|
|
88
|
+
const t = encodeURIComponent(target);
|
|
89
|
+
if (platform === 'gitlab') {
|
|
90
|
+
return webBase + '/-/merge_requests/new?merge_request%5Bsource_branch%5D=' + b + '&merge_request%5Btarget_branch%5D=' + t;
|
|
91
|
+
}
|
|
92
|
+
if (platform === 'github') {
|
|
93
|
+
return webBase + '/compare/' + t + '...' + b + '?expand=1';
|
|
94
|
+
}
|
|
95
|
+
if (platform === 'gitee') {
|
|
96
|
+
return webBase + '/compare/' + t + '...' + b;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultRun(cmd, cwd) {
|
|
102
|
+
try {
|
|
103
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 单仓库判定:ls-remote 拿 source/target 的远端 sha → rev-list 数差异
|
|
111
|
+
* @returns {{status:'ok'|'no-diff'|'not-pushed'|'no-target'|'no-remote', remote?:string}}
|
|
112
|
+
*/
|
|
113
|
+
function inspectRepo(repoDir, branch, target, execImpl) {
|
|
114
|
+
const run = execImpl || defaultRun;
|
|
115
|
+
const remote = run('git remote get-url origin', repoDir);
|
|
116
|
+
if (!remote.ok) return { status: 'no-remote' };
|
|
117
|
+
const ls = run('git ls-remote --heads origin "' + branch + '" "' + target + '"', repoDir);
|
|
118
|
+
if (!ls.ok) return { status: 'no-remote' };
|
|
119
|
+
const refs = {};
|
|
120
|
+
ls.out.split('\n').forEach(line => {
|
|
121
|
+
const m = line.match(/^([0-9a-f]+)\s+refs\/heads\/(.+)$/);
|
|
122
|
+
if (m) refs[m[2]] = m[1];
|
|
123
|
+
});
|
|
124
|
+
if (!refs[branch]) return { status: 'not-pushed', remote: remote.out };
|
|
125
|
+
if (!refs[target]) return { status: 'no-target', remote: remote.out };
|
|
126
|
+
const ahead = run('git rev-list --count "' + refs[target] + '..' + refs[branch] + '"', repoDir);
|
|
127
|
+
if (ahead.ok && ahead.out === '0') return { status: 'no-diff', remote: remote.out };
|
|
128
|
+
// rev-list 失败(目标 sha 本地不可达等)→ 不阻塞,照常给链接
|
|
129
|
+
return { status: 'ok', remote: remote.out };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function openBrowser(url, platform) {
|
|
133
|
+
const os = process.platform;
|
|
134
|
+
try {
|
|
135
|
+
if (os === 'win32') spawn('cmd', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true }).unref();
|
|
136
|
+
else if (os === 'darwin') spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
137
|
+
else spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
138
|
+
} catch (e) { /* 打不开不阻塞:URL 清单已输出 */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 编排:扫描 worktree 仓库 + 逐仓库判定与生成
|
|
143
|
+
* @param {{wsRoot:string, branch:string, target:string, platform?:string, execImpl?:Function}} opts
|
|
144
|
+
*/
|
|
145
|
+
function makeMrUrls(opts) {
|
|
146
|
+
const { wtRoot, repos, error } = scanWorktreeRepos(opts.wsRoot, opts.branch);
|
|
147
|
+
if (error) return { repos: [], results: [], error };
|
|
148
|
+
const results = [];
|
|
149
|
+
for (const r of repos) {
|
|
150
|
+
const info = inspectRepo(r.wt, opts.branch, opts.target, opts.execImpl);
|
|
151
|
+
const item = { name: r.name, status: info.status };
|
|
152
|
+
if (info.status === 'ok') {
|
|
153
|
+
const platform = detectPlatform(info.remote, opts.platform);
|
|
154
|
+
const web = parseRemoteUrl(info.remote);
|
|
155
|
+
const url = platform && web ? buildMrUrl(platform, web.webBase, opts.branch, opts.target) : null;
|
|
156
|
+
if (url) { item.url = url; item.platform = platform; }
|
|
157
|
+
else { item.status = 'manual'; item.reason = 'origin 无法解析为已知平台仓库地址'; }
|
|
158
|
+
} else {
|
|
159
|
+
item.reason = {
|
|
160
|
+
'no-diff': '与目标分支无差异,无需 MR',
|
|
161
|
+
'not-pushed': 'feature 分支未推送到远端(先完成 push)',
|
|
162
|
+
'no-target': '远端不存在目标分支',
|
|
163
|
+
'no-remote': '仓库无 origin 远端',
|
|
164
|
+
}[info.status];
|
|
165
|
+
}
|
|
166
|
+
results.push(item);
|
|
167
|
+
}
|
|
168
|
+
return { wtRoot, repos, results, error: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { parseArgs, parseRemoteUrl, detectPlatform, buildMrUrl, inspectRepo, makeMrUrls };
|
|
172
|
+
|
|
173
|
+
function main() {
|
|
174
|
+
const args = parseArgs(process.argv.slice(2));
|
|
175
|
+
if (args.error) {
|
|
176
|
+
console.error('❌ ' + args.error);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const { wsRoot, branch, target } = args;
|
|
180
|
+
console.log('=== 生成 MR 预填链接 ===');
|
|
181
|
+
console.log('需求分支:' + branch + ' → 目标分支:' + target);
|
|
182
|
+
|
|
183
|
+
const { results, error } = makeMrUrls({ wsRoot, branch, target, platform: args.platform });
|
|
184
|
+
if (error) {
|
|
185
|
+
console.error('❌ ' + error);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
if (results.length === 0) {
|
|
189
|
+
console.error('❌ worktree 下未发现任何 git 仓库');
|
|
190
|
+
process.exit(2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let opened = 0;
|
|
194
|
+
for (const r of results) {
|
|
195
|
+
if (r.status === 'ok') {
|
|
196
|
+
console.log('✅ ' + r.name + ' [' + r.platform + ']: ' + r.url);
|
|
197
|
+
if (args.open) { openBrowser(r.url); opened++; }
|
|
198
|
+
} else {
|
|
199
|
+
console.log('⏭️ ' + r.name + ': ' + r.reason + '(' + r.status + ')');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (args.open && opened > 0) console.log('🌐 已在浏览器打开 ' + opened + ' 个 MR 创建页(人工确认标题/评审人后提交)');
|
|
203
|
+
|
|
204
|
+
console.log('---');
|
|
205
|
+
console.log('MR_RESULT', JSON.stringify(results));
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (require.main === module) {
|
|
210
|
+
main();
|
|
211
|
+
}
|