@jspg-ai/coding-bb 0.0.3-beta.30 → 0.0.3-beta.31
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/lib/external/index.js +228 -0
- package/cbb/lib/install/init.js +70 -7
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +32 -40
- 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/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* External 小型外部依赖管理模块
|
|
5
|
+
*
|
|
6
|
+
* 管理顶层 external/ 目录下的零散第三方依赖(当前为 frontend-design skill,
|
|
7
|
+
* 来源 anthropics/skills)。与 superpowers / openspec 的整仓 pin 不同,
|
|
8
|
+
* 这里是「一依赖一目录、pin 文件放依赖自己的子目录内」的轻量形态:
|
|
9
|
+
*
|
|
10
|
+
* external/<dep-name>/
|
|
11
|
+
* ├── .version # pin:{ repo, ref(commit sha), path(tarball 内子路径), updatedAt }
|
|
12
|
+
* └── ... # 上游 path 子目录的内容(如 SKILL.md + LICENSE.txt)
|
|
13
|
+
*
|
|
14
|
+
* 新增外部依赖 = 在 external/ 下新建子目录并写入 .version,无需改本模块。
|
|
15
|
+
*
|
|
16
|
+
* 网络:GitHub 直连可能被墙 / 被本机 DNS 劫持,走 config/upstream-mirrors.json
|
|
17
|
+
* 的 'external' 镜像前缀列表(按顺序尝试,首个成功即用;空字符串 = 直连)。
|
|
18
|
+
*
|
|
19
|
+
* 升级方式:cbbm update --upstream-external [name](按 .version 的 ref 幂等重拉,省略 name = 全部)
|
|
20
|
+
* 版本检查:cbbm check-upstream(对比 pinned sha 与上游 main 该 path 的最新提交)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
|
|
27
|
+
const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
|
|
28
|
+
|
|
29
|
+
// External 依赖根目录
|
|
30
|
+
const EXTERNAL_DIR = path.join(SHARED_STANDARDS_DIR, 'external');
|
|
31
|
+
const VERSION_FILE_NAME = '.version';
|
|
32
|
+
// skill 型依赖的完整性标记(上游 path 内容含 SKILL.md 时以其校验完整性)
|
|
33
|
+
const SKILL_MARKER = 'SKILL.md';
|
|
34
|
+
|
|
35
|
+
const { log } = require('../utils/output');
|
|
36
|
+
const { extractTarGz } = require('../utils/tar');
|
|
37
|
+
const { loadMirrors, fetchWithMirrors } = require('../utils/upstream');
|
|
38
|
+
|
|
39
|
+
// ── 依赖发现与 pin 读取 ──────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 读取依赖目录内的 .version pin。字段不全 / JSON 损坏 / 文件缺失返回 null。
|
|
43
|
+
* @param {string} dir
|
|
44
|
+
* @returns {{repo: string, ref: string, path: string, updatedAt?: string}|null}
|
|
45
|
+
*/
|
|
46
|
+
function readPin(dir) {
|
|
47
|
+
const file = path.join(dir, VERSION_FILE_NAME);
|
|
48
|
+
if (!fs.existsSync(file)) return null;
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
51
|
+
if (!data || typeof data.repo !== 'string' || !data.repo.includes('/') ||
|
|
52
|
+
typeof data.ref !== 'string' || !data.ref ||
|
|
53
|
+
typeof data.path !== 'string' || !data.path) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 扫描 external/ 下各依赖子目录,返回全部依赖(pin 无效的目录跳过并告警)。
|
|
64
|
+
* @returns {Array<{name: string, dir: string, pin: object}>}
|
|
65
|
+
*/
|
|
66
|
+
function listDeps() {
|
|
67
|
+
if (!fs.existsSync(EXTERNAL_DIR)) return [];
|
|
68
|
+
const deps = [];
|
|
69
|
+
for (const entry of fs.readdirSync(EXTERNAL_DIR)) {
|
|
70
|
+
const dir = path.join(EXTERNAL_DIR, entry);
|
|
71
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
72
|
+
const pin = readPin(dir);
|
|
73
|
+
if (!pin) {
|
|
74
|
+
log(`external/${entry}: 缺少有效 .version pin({ repo, ref, path }),跳过`, 'warn');
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
deps.push({ name: entry, dir, pin });
|
|
78
|
+
}
|
|
79
|
+
return deps;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 依赖内容完整性:skill 型(上游含 SKILL.md)要求本地 SKILL.md 存在;
|
|
84
|
+
* 其余类型要求除 .version 外至少有一个条目。
|
|
85
|
+
* @param {string} dir
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function validateDepDir(dir) {
|
|
89
|
+
if (!fs.existsSync(dir)) return false;
|
|
90
|
+
if (fs.existsSync(path.join(dir, SKILL_MARKER))) return true;
|
|
91
|
+
return fs.readdirSync(dir).some(f => f !== VERSION_FILE_NAME);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 上游拉取 ──────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/** tarball 下载地址(按镜像前缀拼接;ref = commit sha,上游无 skill 粒度 tag) */
|
|
97
|
+
function tarballUrl(mirror, pin) {
|
|
98
|
+
return `${mirror}https://api.github.com/repos/${pin.repo}/tarball/${pin.ref}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 按 .version 的 ref 下载 tarball,解压出 pin.path 子目录,内容覆盖到 external/<name>/。
|
|
103
|
+
* 幂等:pin 一致且内容完整 → 跳过重拉。.version 由本模块改写,不被上游内容覆盖。
|
|
104
|
+
* @param {string[]} [names] - 指定依赖名;省略 = 全部
|
|
105
|
+
* @returns {Promise<Array<{name: string, ref: string, skipped: boolean, error?: string}>>}
|
|
106
|
+
*/
|
|
107
|
+
async function fetchDeps(names) {
|
|
108
|
+
const all = listDeps();
|
|
109
|
+
let deps = all;
|
|
110
|
+
if (names && names.length > 0) {
|
|
111
|
+
const known = new Set(all.map(d => d.name));
|
|
112
|
+
const unknown = names.filter(n => !known.has(n));
|
|
113
|
+
if (unknown.length > 0) {
|
|
114
|
+
throw new Error(`未知的 external 依赖: ${unknown.join(', ')}(现有: ${[...known].join(', ') || '无'})`);
|
|
115
|
+
}
|
|
116
|
+
deps = all.filter(d => names.includes(d.name));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const dep of deps) {
|
|
121
|
+
try {
|
|
122
|
+
results.push(await fetchDep(dep));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
results.push({ name: dep.name, ref: dep.pin.ref, skipped: false, error: err.message });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 拉取单个依赖。
|
|
132
|
+
* @param {{name: string, dir: string, pin: object}} dep
|
|
133
|
+
* @returns {Promise<{name: string, ref: string, skipped: boolean}>}
|
|
134
|
+
*/
|
|
135
|
+
async function fetchDep(dep) {
|
|
136
|
+
const { name, dir, pin } = dep;
|
|
137
|
+
|
|
138
|
+
// pin 一致且内容完整 → 幂等跳过
|
|
139
|
+
if (validateDepDir(dir)) {
|
|
140
|
+
log(`external/${name} 已按 pin ${pin.ref.slice(0, 12)} 就位,跳过重拉`, 'success');
|
|
141
|
+
return { name, ref: pin.ref, skipped: true };
|
|
142
|
+
}
|
|
143
|
+
log(`external/${name} 内容不完整,按 pin ${pin.ref.slice(0, 12)} 拉取...`, 'info');
|
|
144
|
+
|
|
145
|
+
const mirrors = loadMirrors('external');
|
|
146
|
+
log(`正在获取 ${pin.repo}@${pin.ref.slice(0, 12)}(${mirrors.map(m => m || '直连').join(' → ')})...`, 'info');
|
|
147
|
+
const { buffer, mirror } = await fetchWithMirrors(mirrors, m => tarballUrl(m, pin));
|
|
148
|
+
log(`已从 ${mirror || '(直连)'} 下载 ${(buffer.length / 1024).toFixed(0)} KB`, 'info');
|
|
149
|
+
|
|
150
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-ext-'));
|
|
151
|
+
try {
|
|
152
|
+
extractTarGz(buffer, tmpDir);
|
|
153
|
+
|
|
154
|
+
// GitHub tarball 顶层目录名格式:<owner>-<repo>-<short-sha>
|
|
155
|
+
const topPrefix = `${pin.repo.replace('/', '-')}-`;
|
|
156
|
+
const tops = fs.readdirSync(tmpDir).filter(f => {
|
|
157
|
+
const p = path.join(tmpDir, f);
|
|
158
|
+
return fs.statSync(p).isDirectory() && f.startsWith(topPrefix);
|
|
159
|
+
});
|
|
160
|
+
if (tops.length === 0) throw new Error(`解压后未找到 ${topPrefix}* 目录`);
|
|
161
|
+
|
|
162
|
+
// pin.path 是 tarball 内相对仓库根的子路径(如 skills/frontend-design)
|
|
163
|
+
const source = path.join(tmpDir, tops[0], pin.path);
|
|
164
|
+
if (!fs.existsSync(source)) {
|
|
165
|
+
throw new Error(`上游 ${pin.repo}@${pin.ref.slice(0, 12)} 中不存在路径 ${pin.path}(ref 或 path 配置有误)`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 覆盖内容到 external/<name>/(cpSync 合并,.version 保留)
|
|
169
|
+
fs.cpSync(source, dir, { recursive: true });
|
|
170
|
+
if (!validateDepDir(dir)) throw new Error('拉取后完整性校验未通过');
|
|
171
|
+
|
|
172
|
+
// 刷新 pin 时间戳(ref 不变)
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(dir, VERSION_FILE_NAME),
|
|
175
|
+
JSON.stringify({ ...pin, updatedAt: new Date().toISOString() }, null, 2) + '\n'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
log(`external/${name} 已更新: ${pin.repo}@${pin.ref.slice(0, 12)}`, 'success');
|
|
179
|
+
return { name, ref: pin.ref, skipped: false };
|
|
180
|
+
} finally {
|
|
181
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 检查全部依赖上游是否有新提交(只读,不改动任何文件)。
|
|
187
|
+
* 对比 pinned sha 与上游 main 分支 pin.path 的最新提交 sha
|
|
188
|
+
* (commits?path= 只统计该子路径的变更,仓库其他目录的提交不告警)。
|
|
189
|
+
* 单依赖失败不阻塞其他依赖(返回 {name, error} 条目)。
|
|
190
|
+
* @returns {Promise<Array<{name: string, current: string, latest: string, outdated: boolean}|{name: string, error: string}>>}
|
|
191
|
+
*/
|
|
192
|
+
async function checkUpstream() {
|
|
193
|
+
const deps = listDeps();
|
|
194
|
+
const results = await Promise.allSettled(deps.map(async dep => {
|
|
195
|
+
const mirrors = loadMirrors('external');
|
|
196
|
+
const { buffer } = await fetchWithMirrors(mirrors, m =>
|
|
197
|
+
`${m}https://api.github.com/repos/${dep.pin.repo}/commits?path=${encodeURIComponent(dep.pin.path)}&sha=main&per_page=1`
|
|
198
|
+
);
|
|
199
|
+
const data = JSON.parse(buffer.toString('utf-8'));
|
|
200
|
+
const latest = Array.isArray(data) && data[0] ? data[0].sha : null;
|
|
201
|
+
return {
|
|
202
|
+
name: dep.name,
|
|
203
|
+
current: dep.pin.ref,
|
|
204
|
+
latest,
|
|
205
|
+
outdated: Boolean(latest && latest !== dep.pin.ref),
|
|
206
|
+
};
|
|
207
|
+
}));
|
|
208
|
+
return results.map((r, i) => r.status === 'fulfilled'
|
|
209
|
+
? r.value
|
|
210
|
+
: { name: deps[i].name, error: r.reason.message });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 导出 ──────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
// 常量
|
|
217
|
+
EXTERNAL_DIR,
|
|
218
|
+
VERSION_FILE_NAME,
|
|
219
|
+
SKILL_MARKER,
|
|
220
|
+
|
|
221
|
+
// 核心函数
|
|
222
|
+
listDeps,
|
|
223
|
+
readPin,
|
|
224
|
+
validateDepDir,
|
|
225
|
+
fetchDeps,
|
|
226
|
+
checkUpstream,
|
|
227
|
+
tarballUrl,
|
|
228
|
+
};
|
package/cbb/lib/install/init.js
CHANGED
|
@@ -84,6 +84,11 @@ const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktre
|
|
|
84
84
|
// cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
|
|
85
85
|
const TOOLS_SKILLS = [];
|
|
86
86
|
|
|
87
|
+
// external 外部依赖 skills 白名单:第三方上游 pin vendor 的 skill(源在 external/<name>/,
|
|
88
|
+
// 各子目录自带 .version pin,供 cbbm update --upstream-external 维护)。
|
|
89
|
+
// 非 skill 类外部依赖不走部署通道,不会进本清单。
|
|
90
|
+
const EXTERNAL_SKILLS = ['frontend-design'];
|
|
91
|
+
|
|
87
92
|
// 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
|
|
88
93
|
// onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
|
|
89
94
|
const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
@@ -156,6 +161,9 @@ const superpowers = require('../superpowers');
|
|
|
156
161
|
// OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
|
|
157
162
|
const openspec = require('../openspec');
|
|
158
163
|
|
|
164
|
+
// External 外部依赖管理(内置集成 + cbbm update --upstream-external)
|
|
165
|
+
const external = require('../external');
|
|
166
|
+
|
|
159
167
|
// ── 颜色与输出辅助 ──────────────────────────────────────
|
|
160
168
|
|
|
161
169
|
const { COLOR, COLOR_ENABLED, c, log, section, step, padVisual, progress, LOG_STYLES } = require('../utils/output');
|
|
@@ -1156,6 +1164,22 @@ async function init(targetDir, options = {}) {
|
|
|
1156
1164
|
log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
|
|
1157
1165
|
}
|
|
1158
1166
|
|
|
1167
|
+
// 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
|
|
1168
|
+
const externalSkillsSrc = external.EXTERNAL_DIR;
|
|
1169
|
+
let exCount = 0;
|
|
1170
|
+
if (fs.existsSync(externalSkillsSrc)) {
|
|
1171
|
+
skillsAdapters.forEach(adapter => {
|
|
1172
|
+
const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
|
|
1173
|
+
if (exResult.count > 0) {
|
|
1174
|
+
exCount = exResult.count;
|
|
1175
|
+
exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
|
|
1179
|
+
} else {
|
|
1180
|
+
log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1159
1183
|
// 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
|
|
1160
1184
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
1161
1185
|
}
|
|
@@ -2164,13 +2188,14 @@ async function uninstall() {
|
|
|
2164
2188
|
// ── 上游版本检查 ──────────────────────────────────────────────
|
|
2165
2189
|
|
|
2166
2190
|
/**
|
|
2167
|
-
* 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
|
|
2191
|
+
* 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
|
|
2168
2192
|
* 供 cbbm check-upstream 调用。
|
|
2169
2193
|
*/
|
|
2170
2194
|
async function checkUpstream() {
|
|
2171
2195
|
const results = await Promise.allSettled([
|
|
2172
2196
|
openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
|
|
2173
2197
|
superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
|
|
2198
|
+
external.checkUpstream().then(list => ({ name: 'external', list })),
|
|
2174
2199
|
]);
|
|
2175
2200
|
|
|
2176
2201
|
let outdated = false;
|
|
@@ -2179,6 +2204,22 @@ async function checkUpstream() {
|
|
|
2179
2204
|
log(`检查失败: ${r.reason.message}`, 'error');
|
|
2180
2205
|
continue;
|
|
2181
2206
|
}
|
|
2207
|
+
// external 是多依赖聚合结果,逐依赖渲染
|
|
2208
|
+
if (r.value.name === 'external') {
|
|
2209
|
+
for (const item of r.value.list) {
|
|
2210
|
+
if (item.error) {
|
|
2211
|
+
log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
|
|
2212
|
+
} else if (!item.latest) {
|
|
2213
|
+
log(`external/${item.name}: 无法获取上游版本`, 'warn');
|
|
2214
|
+
} else if (item.outdated) {
|
|
2215
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
|
|
2216
|
+
outdated = true;
|
|
2217
|
+
} else {
|
|
2218
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2182
2223
|
const { name, current, latest, outdated: isOutdated } = r.value;
|
|
2183
2224
|
if (!latest) {
|
|
2184
2225
|
log(`${name}: 无法获取上游版本`, 'warn');
|
|
@@ -2196,7 +2237,7 @@ async function checkUpstream() {
|
|
|
2196
2237
|
}
|
|
2197
2238
|
|
|
2198
2239
|
if (outdated) {
|
|
2199
|
-
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
|
|
2240
|
+
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
|
|
2200
2241
|
}
|
|
2201
2242
|
}
|
|
2202
2243
|
|
|
@@ -2205,7 +2246,7 @@ async function checkUpstream() {
|
|
|
2205
2246
|
// 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
|
|
2206
2247
|
const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
|
|
2207
2248
|
const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
|
|
2208
|
-
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
|
|
2249
|
+
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
|
|
2209
2250
|
|
|
2210
2251
|
const args = process.argv.slice(2);
|
|
2211
2252
|
const command = args[0];
|
|
@@ -2213,11 +2254,11 @@ const restArgs = args.slice(1);
|
|
|
2213
2254
|
const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
|
|
2214
2255
|
|
|
2215
2256
|
if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
|
|
2216
|
-
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
|
|
2257
|
+
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
|
|
2217
2258
|
process.exit(1);
|
|
2218
2259
|
}
|
|
2219
2260
|
if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
|
|
2220
|
-
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
|
|
2261
|
+
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
|
|
2221
2262
|
log('升级已安装内容请使用 cbb update', 'warn');
|
|
2222
2263
|
process.exit(1);
|
|
2223
2264
|
}
|
|
@@ -2242,7 +2283,8 @@ switch (command) {
|
|
|
2242
2283
|
case 'update': {
|
|
2243
2284
|
const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
|
|
2244
2285
|
const upstreamOpenspec = restArgs.includes('--upstream-openspec');
|
|
2245
|
-
|
|
2286
|
+
const upstreamExternal = restArgs.includes('--upstream-external');
|
|
2287
|
+
if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
|
|
2246
2288
|
// 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
|
|
2247
2289
|
if (upstreamSuperpowers) {
|
|
2248
2290
|
superpowers.fetchLatestSkills().then(result => {
|
|
@@ -2275,6 +2317,26 @@ switch (command) {
|
|
|
2275
2317
|
process.exit(1);
|
|
2276
2318
|
});
|
|
2277
2319
|
}
|
|
2320
|
+
if (upstreamExternal) {
|
|
2321
|
+
// 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
|
|
2322
|
+
const extNames = [];
|
|
2323
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
2324
|
+
if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
|
|
2325
|
+
extNames.push(restArgs[i + 1]);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
external.fetchDeps(extNames).then(results => {
|
|
2329
|
+
const failed = results.filter(r => r.error);
|
|
2330
|
+
if (failed.length > 0) {
|
|
2331
|
+
failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
|
|
2332
|
+
process.exit(1);
|
|
2333
|
+
}
|
|
2334
|
+
log('external 外部依赖完整性校验通过', 'success');
|
|
2335
|
+
}).catch(err => {
|
|
2336
|
+
log(`更新失败: ${err.message}`, 'error');
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2278
2340
|
} else {
|
|
2279
2341
|
updateInstalled().catch(err => {
|
|
2280
2342
|
log(err.message, 'error');
|
|
@@ -2329,12 +2391,13 @@ function showHelp() {
|
|
|
2329
2391
|
@jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
|
|
2330
2392
|
|
|
2331
2393
|
cbbm check-update 版本检查(用户级 hook 每日自动触发)
|
|
2332
|
-
cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
|
|
2394
|
+
cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
|
|
2333
2395
|
cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
|
|
2334
2396
|
|
|
2335
2397
|
维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
|
|
2336
2398
|
cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
|
|
2337
2399
|
cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
|
|
2400
|
+
cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
|
|
2338
2401
|
`);
|
|
2339
2402
|
return;
|
|
2340
2403
|
}
|
|
@@ -73,7 +73,7 @@ node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,
|
|
|
73
73
|
|
|
74
74
|
## Step 4:提交推送(转 cbb-worktree-push)
|
|
75
75
|
|
|
76
|
-
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run
|
|
76
|
+
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → MR 预填链接,全流程与 worktree 模式一致。
|
|
77
77
|
|
|
78
78
|
## Step 5:收尾(MR 合并后)
|
|
79
79
|
|
|
@@ -265,7 +265,7 @@ node "<skill_dir>/scripts/create-branches.js" "<ws_root>" "<branch>" [--apps "ap
|
|
|
265
265
|
|
|
266
266
|
### 3.4 分支就位(lazy push,不推远端)
|
|
267
267
|
|
|
268
|
-
**本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 →
|
|
268
|
+
**本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → agent 生成 commit 信息并提交 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
|
|
269
269
|
|
|
270
270
|
> 旧版在本步骤推送空分支建立 upstream(`push-branches.js`),自 0.0.3-beta.22 起移除:避免每个需求在远端留下空分支尸体;`push-worktrees.js` 首次推送时自动建立 upstream,行为等价。
|
|
271
271
|
|
|
@@ -21,7 +21,7 @@ description: "提交并推送当前需求的所有关联仓库(跨 worktree
|
|
|
21
21
|
- **本技能支持两层执行位置**(与 `cbb-worktree-close` 对称):
|
|
22
22
|
- 工作空间根目录(`WS_ROOT/`)
|
|
23
23
|
- worktree 内部(`WS_ROOT/.worktrees/worktree-<需求名>/` 或其子目录)
|
|
24
|
-
-
|
|
24
|
+
- 输入约定:**自由文本**(需求名等)直接在对话中提问并等待回复;**固定选项多选**使用 `AskUserQuestion`。**例外:commit 信息由 agent 自行生成,不提问、不等待确认**
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -56,7 +56,7 @@ node "<skill_dir>/scripts/<name>.js" <参数>
|
|
|
56
56
|
|------|------|------|
|
|
57
57
|
| **A. cbb 工作空间根** | cwd(或其 git 顶层)含 `CBB-WORKSPACE.json`;`find-workspace-root.js` 退出码 0 | 走 **Step 1 → 6** 的跨 worktree 批量流程 |
|
|
58
58
|
| **B. worktree 内** | cwd 命中 `<ws_root>/.worktrees/worktree-<需求名>/` | 用 cwd 反推 `<需求名>`,走 **Step 1 → 6** 批量流程 |
|
|
59
|
-
| **C. 普通 git 仓库** | 在 git 仓库内,但无 `CBB-WORKSPACE.json`(`find-workspace-root.js` 退出码 1) | **降级为单仓库提交推送**:不调 worktree 脚本,按常规 `git add -u` →
|
|
59
|
+
| **C. 普通 git 仓库** | 在 git 仓库内,但无 `CBB-WORKSPACE.json`(`find-workspace-root.js` 退出码 1) | **降级为单仓库提交推送**:不调 worktree 脚本,按常规 `git add -u` → agent 生成 commit 信息 → `git commit` → `git push`(信息生成规则与 A/B 场景一致:精炼最简、5–50 字符) |
|
|
60
60
|
| **D. 非 git 目录** | `git rev-parse --is-inside-work-tree` 失败 | 说明"当前目录不是 git 仓库",终止,不做任何操作 |
|
|
61
61
|
|
|
62
62
|
**判场景与定位(A/B 场景):**
|
|
@@ -74,9 +74,9 @@ node "<skill_dir>/scripts/find-workspace-root.js"
|
|
|
74
74
|
**场景 C 降级流程**(普通仓库误触):
|
|
75
75
|
|
|
76
76
|
1. `git status` 报告改动现状
|
|
77
|
-
2.
|
|
77
|
+
2. agent 基于改动内容生成 commit 信息(精炼最简,5–50 字符;规则见 Step 3)
|
|
78
78
|
3. `git add -u`(默认不含未追踪文件;用户明确要求时才 `git add -A`)
|
|
79
|
-
4. `git commit -m "
|
|
79
|
+
4. `git commit -m "<agent 生成的信息>"`
|
|
80
80
|
5. `git push`(无 upstream 时先询问用户是否 `-u`)
|
|
81
81
|
6. 汇报结果;**不要**提及 worktree 批量流程
|
|
82
82
|
|
|
@@ -111,36 +111,27 @@ node "<skill_dir>/scripts/commit-worktrees.js" "<ws_root>" "<branch>" --message
|
|
|
111
111
|
- `skipped (skip-repo)`:用户指定跳过
|
|
112
112
|
- `failed`:错误
|
|
113
113
|
|
|
114
|
-
> ⓘ **dry-run
|
|
114
|
+
> ⓘ **dry-run 必跑**:把将要 commit 的文件清单与 agent 生成的 commit 信息一并展示给用户(透明可见,**不停顿等待回复**),避免误纳 `.idea/` 等被 git 忽略的文件(虽然 `git add -u` 已天然排除,但 dry-run 仍是最后兜底)。
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
|
-
## Step 3
|
|
118
|
+
## Step 3:生成 commit 信息(agent 自主)
|
|
119
119
|
|
|
120
|
-
AI agent
|
|
120
|
+
AI agent 在执行 commit 前**自行生成** commit 信息,**不向用户提问、不等待确认**。
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
**生成规则(硬约束)**:
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
回复示例(统一):
|
|
135
|
-
feat(<branch>): 实现客户运费逻辑
|
|
136
|
-
|
|
137
|
-
回复示例(分别):
|
|
138
|
-
workspace: chore(<branch>): 同步 openspec 标准
|
|
139
|
-
ep-foo-app: feat(<branch>): 实现 saveClientExpectFreight
|
|
140
|
-
ep-bar-app: refactor(<branch>): 拆分 PlanService
|
|
141
|
-
```
|
|
124
|
+
| 规则 | 要求 |
|
|
125
|
+
|------|------|
|
|
126
|
+
| 长度 | **5–50 字符**(按字符计:1 个中文字 = 1,1 个英文字母 / 空格 / 标点 = 1);超出范围不得提交 |
|
|
127
|
+
| 风格 | 精炼、最简——一句话说清"改了什么",不写缘由、不换行、不堆砌 |
|
|
128
|
+
| 依据 | 以 Step 2 dry-run 的实际改动 + `openspec/changes/<branch>/tasks.md` 已完成 task 为准;禁止凭空编造 |
|
|
129
|
+
| 格式 | 沿用 Conventional Commits 前缀(`feat` / `fix` / `refactor` / `chore` / `docs` 等)+ 需求名,如 `feat(<branch>): 实现客户运费逻辑` |
|
|
130
|
+
| 多仓库 | 各仓库改动语义不同时**逐仓库分别生成**(经 `--per-repo`),禁止硬塞同一条 |
|
|
131
|
+
|
|
132
|
+
**展示时机**:生成的每条信息在 Step 2 dry-run 预览中随待提交文件一并输出(透明可见),但**不停顿等待回复**,直接进入 Step 4。
|
|
142
133
|
|
|
143
|
-
|
|
134
|
+
> ⓘ 用户若在对话中显式给出 commit 信息,直接采用(不覆盖用户意图);未给出时按上述规则自行生成。
|
|
144
135
|
|
|
145
136
|
---
|
|
146
137
|
|
|
@@ -163,7 +154,7 @@ node "<skill_dir>/scripts/commit-worktrees.js" "<ws_root>/.worktrees/worktree-<b
|
|
|
163
154
|
**选项**:
|
|
164
155
|
| 选项 | 说明 |
|
|
165
156
|
|------|------|
|
|
166
|
-
| `--message / -m` | 必填(除非 --per-repo 完全覆盖)。commit
|
|
157
|
+
| `--message / -m` | 必填(除非 --per-repo 完全覆盖)。commit 信息(agent 生成,5–50 字符) |
|
|
167
158
|
| `--per-repo "<name>:msg;<name>:msg"` | 覆盖特定仓库的 commit 信息 |
|
|
168
159
|
| `--untracked` | 同时 stage 未追踪文件(默认 `git add -u`,加此项时 `git add -A`) |
|
|
169
160
|
| `--allow-empty` | 允许空提交 |
|
|
@@ -283,11 +274,11 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
|
|
|
283
274
|
|------|----------|
|
|
284
275
|
| **任何触发** | **先做 Step 0 场景识别**,再决定策略 |
|
|
285
276
|
| 场景 A:cwd 在 cbb 工作空间根 | find-workspace-root 成功 → 多个 worktree 时让用户选 |
|
|
286
|
-
| 场景 B:cwd 在 worktree 内 | 自动用 cwd
|
|
277
|
+
| 场景 B:cwd 在 worktree 内 | 自动用 cwd 反推分支,扫描后 agent 生成 commit 信息 |
|
|
287
278
|
| 场景 C:cwd 在普通 git 仓库(无 CBB-WORKSPACE.json) | **降级为单仓库提交推送**,不调用 worktree 脚本 |
|
|
288
279
|
| 场景 D:cwd 非 git 目录 | 说明情况并终止,不做任何操作 |
|
|
289
|
-
| commit
|
|
290
|
-
|
|
|
280
|
+
| commit 信息 | agent 自行生成(5–50 字符,精炼最简),不询问用户;dry-run 预览时一并展示 |
|
|
281
|
+
| 多仓库改动语义不同 | agent 逐仓库分别生成,用 `--per-repo "ep-foo:msg;ep-bar:msg"` |
|
|
291
282
|
| 仓库干净 | commit-worktrees 自动 skipped,只 push 不 commit |
|
|
292
283
|
| 仓库已有 ahead commit | commit-worktrees 自动 skipped(不重复 commit),只 push |
|
|
293
284
|
| 未追踪文件 | 默认不纳入(避免误纳 .idea);用户明确要求时加 `--untracked` |
|
|
@@ -308,10 +299,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
|
|
|
308
299
|
- **问题:** 用户在普通仓库说"提交代码",技能被 description 匹配触发,直接跑 `find-workspace-root.js` 失败并报错终止
|
|
309
300
|
- **修复:** Step 0 先判场景;场景 C(普通 git 仓库)降级为单仓库提交推送,不调用 worktree 脚本、不提及批量流程
|
|
310
301
|
|
|
311
|
-
###
|
|
302
|
+
### commit 信息超长或空洞
|
|
312
303
|
|
|
313
|
-
- **问题:** agent
|
|
314
|
-
- **修复:** Step 3
|
|
304
|
+
- **问题:** agent 生成的 commit 信息过长(罗列全部改动、写缘由)或过于笼统("更新代码"),与"5–50 字符、精炼最简"不符
|
|
305
|
+
- **修复:** Step 3 按规则生成——以实际 diff / 已完成 task 为依据,一句话说清改了什么,长度落在 5–50 字符
|
|
315
306
|
|
|
316
307
|
### 误纳未追踪文件
|
|
317
308
|
|
|
@@ -328,10 +319,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
|
|
|
328
319
|
- **问题:** apply 实现完 → 用户关掉 IDE → 调 close-worktree → ahead 提交永久丢失
|
|
329
320
|
- **修复:** 强烈建议 apply 完成后立即调本 skill 推送;或 close-worktree Step 3.4 兜底
|
|
330
321
|
|
|
331
|
-
###
|
|
322
|
+
### 多仓库硬塞同一条 commit 信息
|
|
332
323
|
|
|
333
324
|
- **问题:** workspace 改了 CBB-WORKSPACE.json,ep-foo-app 改了 Java 代码,硬塞同一条 "feat: 实现 XX" 信息不准确
|
|
334
|
-
- **修复:**
|
|
325
|
+
- **修复:** agent 逐仓库生成各自的 commit 信息,用 `--per-repo` 传入
|
|
335
326
|
|
|
336
327
|
### push 失败时跳过失败仓库直接继续
|
|
337
328
|
|
|
@@ -349,7 +340,7 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
|
|
|
349
340
|
|
|
350
341
|
**绝不允许**
|
|
351
342
|
|
|
352
|
-
-
|
|
343
|
+
- commit 信息超出 5–50 字符范围 / 空洞无意义 / 与改动不符
|
|
353
344
|
- 默认 `git add -A` 误纳未追踪文件
|
|
354
345
|
- push 失败后跳过失败仓库直接继续
|
|
355
346
|
- 远程分支已存在时脚本自动 set-upstream-to
|
|
@@ -363,9 +354,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
|
|
|
363
354
|
- 严格按 Step 0 → 1 → 2 → 3 → 4 → 5 → 6 顺序执行(场景 C/D 除外,按 Step 0 降级策略)
|
|
364
355
|
- 所有脚本调用以 `node "<skill_dir>/scripts/<name>.js"` 形式发起(`<skill_dir>` = 本 SKILL.md 所在目录)
|
|
365
356
|
- 执行脚本时 cwd 保持在工作空间根;场景 C 降级时按普通 git 流程
|
|
366
|
-
- Step 2 dry-run
|
|
367
|
-
- Step 3
|
|
368
|
-
- commit
|
|
357
|
+
- Step 2 dry-run 必跑,把待提交文件清单与 agent 生成的 commit 信息一并展示给用户
|
|
358
|
+
- Step 3 由 agent 自行生成 commit 信息(5–50 字符、精炼最简),不向用户提问、不等待确认;生成结果在 dry-run 预览中展示
|
|
359
|
+
- commit 信息必须依据实际 diff / 已完成 task 生成,沿用 Conventional Commits 前缀(feature/refactor/fix 等语义)
|
|
360
|
+
- 多个仓库改动语义不同时必须逐仓库分别生成(`--per-repo`),禁止硬塞同一条
|
|
369
361
|
- commit-worktrees / push-worktrees 退出码 2 时必须阻塞流程
|
|
370
362
|
- 远程分支已存在时 AI 必须先输出 commit 元信息再询问
|
|
371
363
|
- push 成功后必须走 Step 7 MR 提案(AskUserQuestion 固定选项;用户选"暂不提"则正常结束)
|