@jspg-ai/coding-bb 0.0.3-beta.30 → 0.0.3-beta.32
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/claude-code.js +3 -3
- package/cbb/lib/install/init.js +125 -30
- package/cbb/lib/install/minimax.js +2 -2
- package/cbb/lib/install/tool-selection.js +8 -30
- 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
|
+
};
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 将编码规范分发到 .claude/rules/ 目录
|
|
5
5
|
*
|
|
6
6
|
* 退市(selectable: false):团队当前不使用 Claude Code,不在安装面板提供。
|
|
7
|
-
* 适配器代码与全部能力保留(含用户级 settings hook
|
|
8
|
-
*
|
|
7
|
+
* 适配器代码与全部能力保留(含用户级 settings hook);旧记录中的遗留内容
|
|
8
|
+
* 不在勾选范围时随清单差集与用户级差集清理移除(ADR 0006)。
|
|
9
9
|
* 恢复入口:删掉 selectable 即可重新出现在面板(决策:不留参数开关)。
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -22,6 +22,6 @@ module.exports = {
|
|
|
22
22
|
settingsFile: path.join(os.homedir(), '.claude', 'settings.json'),
|
|
23
23
|
settingsFileIsUserLevel: true,
|
|
24
24
|
hookEvents: ['UserPromptSubmit'],
|
|
25
|
-
//
|
|
25
|
+
// 退市:不在安装面板提供(代码保留;旧记录遗留随清单/用户级差集清理,ADR 0006)
|
|
26
26
|
selectable: false,
|
|
27
27
|
};
|
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'];
|
|
@@ -100,7 +105,7 @@ const agentsNav = require('./agents-nav');
|
|
|
100
105
|
// 命令 → skill 形态转换(commandsAsSkills 适配器,当前仅 MiniMax Code)
|
|
101
106
|
const commandSkill = require('./command-skill');
|
|
102
107
|
|
|
103
|
-
//
|
|
108
|
+
// 工具选择策略(退市工具不在面板提供;勾选范围即权威,ADR 0006)
|
|
104
109
|
const toolSelection = require('./tool-selection');
|
|
105
110
|
|
|
106
111
|
/** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
|
|
@@ -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');
|
|
@@ -376,10 +384,7 @@ async function selectTools(isUpgrade) {
|
|
|
376
384
|
.map(i => selectable[i]);
|
|
377
385
|
}
|
|
378
386
|
|
|
379
|
-
//
|
|
380
|
-
result = toolSelection.carryOverRetired(adapters, result, lastTools);
|
|
381
|
-
|
|
382
|
-
// 保存本次选择(含粘性保留项,避免记录丢失导致下次被清理),供下次运行时预选
|
|
387
|
+
// 保存本次选择(勾选范围即权威,供下次运行时预选)
|
|
383
388
|
saveLastTools(result);
|
|
384
389
|
return result;
|
|
385
390
|
}
|
|
@@ -1046,7 +1051,8 @@ async function init(targetDir, options = {}) {
|
|
|
1046
1051
|
fromRecord = true;
|
|
1047
1052
|
} else if (isNonInteractive) {
|
|
1048
1053
|
const lastTools = readLastTools();
|
|
1049
|
-
|
|
1054
|
+
// 旧记录只用于预选可选项——历史遗留的退市工具不并回(ADR 0006:勾选范围即权威)
|
|
1055
|
+
const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
|
|
1050
1056
|
selectedAdapters = reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters);
|
|
1051
1057
|
fromRecord = reused.length > 0;
|
|
1052
1058
|
if (yes && process.stdin.isTTY) {
|
|
@@ -1058,19 +1064,10 @@ async function init(targetDir, options = {}) {
|
|
|
1058
1064
|
selectedAdapters = await selectTools(isUpgrade);
|
|
1059
1065
|
}
|
|
1060
1066
|
|
|
1061
|
-
//
|
|
1062
|
-
// 覆盖面板外入口:setup 预设(presetAdapters)与非交互记录沿用;交互面板已在 selectTools 内并过。
|
|
1063
|
-
const lastToolsForDir = readLastTools();
|
|
1064
|
-
const withRetired = toolSelection.carryOverRetired(adapters, selectedAdapters, lastToolsForDir);
|
|
1065
|
-
const retiredKept = toolSelection.retiredNames(withRetired);
|
|
1066
|
-
if (retiredKept.length > 0) {
|
|
1067
|
-
log(`保留上次选择的退市工具:${retiredKept.join('、')}(当前不在安装面板提供,内容继续维护)`, 'dim');
|
|
1068
|
-
}
|
|
1069
|
-
selectedAdapters = withRetired;
|
|
1067
|
+
// 退市工具不粘性保留(ADR 0006):选择集即权威,旧记录里的退市工具随清单差集清理
|
|
1070
1068
|
|
|
1071
|
-
//
|
|
1072
|
-
|
|
1073
|
-
if (isWorkspaceRoot && fromRecord) {
|
|
1069
|
+
// 本次选择落盘(沿用旧记录时同样落盘:归一化历史工具名 + 剔除退市遗留记录)
|
|
1070
|
+
if (fromRecord) {
|
|
1074
1071
|
saveLastTools(selectedAdapters);
|
|
1075
1072
|
}
|
|
1076
1073
|
|
|
@@ -1156,6 +1153,22 @@ async function init(targetDir, options = {}) {
|
|
|
1156
1153
|
log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
|
|
1157
1154
|
}
|
|
1158
1155
|
|
|
1156
|
+
// 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
|
|
1157
|
+
const externalSkillsSrc = external.EXTERNAL_DIR;
|
|
1158
|
+
let exCount = 0;
|
|
1159
|
+
if (fs.existsSync(externalSkillsSrc)) {
|
|
1160
|
+
skillsAdapters.forEach(adapter => {
|
|
1161
|
+
const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
|
|
1162
|
+
if (exResult.count > 0) {
|
|
1163
|
+
exCount = exResult.count;
|
|
1164
|
+
exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
|
|
1168
|
+
} else {
|
|
1169
|
+
log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1159
1172
|
// 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
|
|
1160
1173
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
1161
1174
|
}
|
|
@@ -1847,8 +1860,10 @@ function printSetupSummary(selected, selectedAdapters, parentDir) {
|
|
|
1847
1860
|
/**
|
|
1848
1861
|
* 安装用户级组件(与项目无关的内容,一次安装全局可用,幂等)。
|
|
1849
1862
|
*
|
|
1850
|
-
* - 清单:~/.cbb/.user-components
|
|
1863
|
+
* - 清单:~/.cbb/.user-components 只记录**本次实装**的用户级路径
|
|
1851
1864
|
* - 内容:check-update hook(settings 由 settingsManager 幂等管理)
|
|
1865
|
+
* - 差集清理(ADR 0006):不在本次目标集合内的适配器,其清单遗留路径、
|
|
1866
|
+
* 本包版本检查 hook、本地插件一并移除(仅删本包标记项,不动用户自装内容)
|
|
1852
1867
|
*/
|
|
1853
1868
|
const USER_MANIFEST_FILE = path.join(os.homedir(), '.cbb', '.user-components');
|
|
1854
1869
|
// 装到用户级的工具 skills(与具体项目无关,任何会话都可能用到)。
|
|
@@ -1860,15 +1875,18 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
|
|
|
1860
1875
|
const srcDir = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
|
|
1861
1876
|
if (!fs.existsSync(srcDir)) return;
|
|
1862
1877
|
|
|
1863
|
-
|
|
1878
|
+
// 旧清单仅作差集清理基准,不再并入 managed(ADR 0006:勾选范围即权威,
|
|
1879
|
+
// 本次未实装的路径由下方差集移除,而非累积保留)
|
|
1880
|
+
let oldPaths = [];
|
|
1864
1881
|
const manifestDir = path.dirname(USER_MANIFEST_FILE);
|
|
1865
1882
|
fs.mkdirSync(manifestDir, { recursive: true });
|
|
1866
1883
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1867
1884
|
try {
|
|
1868
1885
|
const data = JSON.parse(fs.readFileSync(USER_MANIFEST_FILE, 'utf-8'));
|
|
1869
|
-
|
|
1886
|
+
oldPaths = data.paths || [];
|
|
1870
1887
|
} catch (_) { /* ignore parse errors */ }
|
|
1871
1888
|
}
|
|
1889
|
+
const managed = new Set();
|
|
1872
1890
|
|
|
1873
1891
|
const summaryParts = [];
|
|
1874
1892
|
|
|
@@ -1915,6 +1933,43 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
|
|
|
1915
1933
|
summaryParts.push(`版本检查插件 → ${pluginHookAdapters.map(a => a.displayName).join('、')}`);
|
|
1916
1934
|
}
|
|
1917
1935
|
|
|
1936
|
+
// ── 用户级差集清理(ADR 0006:不在本次勾选范围内的工具,用户级组件一并移除)──
|
|
1937
|
+
const targetNames = new Set(targetAdapters.map(a => a.name));
|
|
1938
|
+
|
|
1939
|
+
// 1) 清单路径差集:旧有新无 → 删除(仅本包管理路径,不碰用户自装内容)
|
|
1940
|
+
const removedPaths = [];
|
|
1941
|
+
oldPaths.forEach(relPath => {
|
|
1942
|
+
if (!managed.has(relPath)) {
|
|
1943
|
+
const target = path.join(home, relPath);
|
|
1944
|
+
if (fs.existsSync(target)) {
|
|
1945
|
+
removePath(target);
|
|
1946
|
+
removedPaths.push(relPath);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
if (removedPaths.length > 0) {
|
|
1951
|
+
summaryParts.push(`移除清单遗留 ×${removedPaths.length}`);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// 2) 非目标适配器:摘除本包版本检查 hook(仅删 _managedBy: cbb 标记项,保留用户配置)
|
|
1955
|
+
// + 本地插件目录
|
|
1956
|
+
const removedNames = [];
|
|
1957
|
+
adapters.filter(a => !targetNames.has(a.name)).forEach(adapter => {
|
|
1958
|
+
let touched = false;
|
|
1959
|
+
if (adapter.settingsFile && adapter.hookEvents) {
|
|
1960
|
+
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) touched = true;
|
|
1961
|
+
if (cleanLegacySettings(adapter)) touched = true;
|
|
1962
|
+
}
|
|
1963
|
+
if (adapter.pluginHook &&
|
|
1964
|
+
pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
|
|
1965
|
+
touched = true;
|
|
1966
|
+
}
|
|
1967
|
+
if (touched) removedNames.push(adapter.displayName);
|
|
1968
|
+
});
|
|
1969
|
+
if (removedNames.length > 0) {
|
|
1970
|
+
summaryParts.push(`清理 ${removedNames.join('、')} 的用户级组件`);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1918
1973
|
if (summaryParts.length > 0) step('✓', '用户级组件', summaryParts.join(' · '));
|
|
1919
1974
|
|
|
1920
1975
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
@@ -1931,9 +1986,10 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
|
|
|
1931
1986
|
* worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
|
|
1932
1987
|
*/
|
|
1933
1988
|
async function updateInstalled() {
|
|
1934
|
-
// 用户级组件沿用本目录的工具选择(.last-tools
|
|
1989
|
+
// 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量。
|
|
1990
|
+
// 旧记录只预选可选项——历史遗留的退市工具不并回(ADR 0006)
|
|
1935
1991
|
const lastTools = readLastTools();
|
|
1936
|
-
const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
|
|
1992
|
+
const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
|
|
1937
1993
|
await setupUserLevelComponents(reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters));
|
|
1938
1994
|
|
|
1939
1995
|
const manifest = readManifest();
|
|
@@ -2164,13 +2220,14 @@ async function uninstall() {
|
|
|
2164
2220
|
// ── 上游版本检查 ──────────────────────────────────────────────
|
|
2165
2221
|
|
|
2166
2222
|
/**
|
|
2167
|
-
* 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
|
|
2223
|
+
* 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
|
|
2168
2224
|
* 供 cbbm check-upstream 调用。
|
|
2169
2225
|
*/
|
|
2170
2226
|
async function checkUpstream() {
|
|
2171
2227
|
const results = await Promise.allSettled([
|
|
2172
2228
|
openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
|
|
2173
2229
|
superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
|
|
2230
|
+
external.checkUpstream().then(list => ({ name: 'external', list })),
|
|
2174
2231
|
]);
|
|
2175
2232
|
|
|
2176
2233
|
let outdated = false;
|
|
@@ -2179,6 +2236,22 @@ async function checkUpstream() {
|
|
|
2179
2236
|
log(`检查失败: ${r.reason.message}`, 'error');
|
|
2180
2237
|
continue;
|
|
2181
2238
|
}
|
|
2239
|
+
// external 是多依赖聚合结果,逐依赖渲染
|
|
2240
|
+
if (r.value.name === 'external') {
|
|
2241
|
+
for (const item of r.value.list) {
|
|
2242
|
+
if (item.error) {
|
|
2243
|
+
log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
|
|
2244
|
+
} else if (!item.latest) {
|
|
2245
|
+
log(`external/${item.name}: 无法获取上游版本`, 'warn');
|
|
2246
|
+
} else if (item.outdated) {
|
|
2247
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
|
|
2248
|
+
outdated = true;
|
|
2249
|
+
} else {
|
|
2250
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2182
2255
|
const { name, current, latest, outdated: isOutdated } = r.value;
|
|
2183
2256
|
if (!latest) {
|
|
2184
2257
|
log(`${name}: 无法获取上游版本`, 'warn');
|
|
@@ -2196,7 +2269,7 @@ async function checkUpstream() {
|
|
|
2196
2269
|
}
|
|
2197
2270
|
|
|
2198
2271
|
if (outdated) {
|
|
2199
|
-
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
|
|
2272
|
+
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
|
|
2200
2273
|
}
|
|
2201
2274
|
}
|
|
2202
2275
|
|
|
@@ -2205,7 +2278,7 @@ async function checkUpstream() {
|
|
|
2205
2278
|
// 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
|
|
2206
2279
|
const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
|
|
2207
2280
|
const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
|
|
2208
|
-
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
|
|
2281
|
+
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
|
|
2209
2282
|
|
|
2210
2283
|
const args = process.argv.slice(2);
|
|
2211
2284
|
const command = args[0];
|
|
@@ -2213,11 +2286,11 @@ const restArgs = args.slice(1);
|
|
|
2213
2286
|
const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
|
|
2214
2287
|
|
|
2215
2288
|
if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
|
|
2216
|
-
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
|
|
2289
|
+
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
|
|
2217
2290
|
process.exit(1);
|
|
2218
2291
|
}
|
|
2219
2292
|
if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
|
|
2220
|
-
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
|
|
2293
|
+
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
|
|
2221
2294
|
log('升级已安装内容请使用 cbb update', 'warn');
|
|
2222
2295
|
process.exit(1);
|
|
2223
2296
|
}
|
|
@@ -2242,7 +2315,8 @@ switch (command) {
|
|
|
2242
2315
|
case 'update': {
|
|
2243
2316
|
const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
|
|
2244
2317
|
const upstreamOpenspec = restArgs.includes('--upstream-openspec');
|
|
2245
|
-
|
|
2318
|
+
const upstreamExternal = restArgs.includes('--upstream-external');
|
|
2319
|
+
if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
|
|
2246
2320
|
// 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
|
|
2247
2321
|
if (upstreamSuperpowers) {
|
|
2248
2322
|
superpowers.fetchLatestSkills().then(result => {
|
|
@@ -2275,6 +2349,26 @@ switch (command) {
|
|
|
2275
2349
|
process.exit(1);
|
|
2276
2350
|
});
|
|
2277
2351
|
}
|
|
2352
|
+
if (upstreamExternal) {
|
|
2353
|
+
// 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
|
|
2354
|
+
const extNames = [];
|
|
2355
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
2356
|
+
if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
|
|
2357
|
+
extNames.push(restArgs[i + 1]);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
external.fetchDeps(extNames).then(results => {
|
|
2361
|
+
const failed = results.filter(r => r.error);
|
|
2362
|
+
if (failed.length > 0) {
|
|
2363
|
+
failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
|
|
2364
|
+
process.exit(1);
|
|
2365
|
+
}
|
|
2366
|
+
log('external 外部依赖完整性校验通过', 'success');
|
|
2367
|
+
}).catch(err => {
|
|
2368
|
+
log(`更新失败: ${err.message}`, 'error');
|
|
2369
|
+
process.exit(1);
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2278
2372
|
} else {
|
|
2279
2373
|
updateInstalled().catch(err => {
|
|
2280
2374
|
log(err.message, 'error');
|
|
@@ -2329,12 +2423,13 @@ function showHelp() {
|
|
|
2329
2423
|
@jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
|
|
2330
2424
|
|
|
2331
2425
|
cbbm check-update 版本检查(用户级 hook 每日自动触发)
|
|
2332
|
-
cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
|
|
2426
|
+
cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
|
|
2333
2427
|
cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
|
|
2334
2428
|
|
|
2335
2429
|
维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
|
|
2336
2430
|
cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
|
|
2337
2431
|
cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
|
|
2432
|
+
cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
|
|
2338
2433
|
`);
|
|
2339
2434
|
return;
|
|
2340
2435
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* 退市(selectable: false):实测 MiniMax Code 不支持目录级 skill 与 command
|
|
20
20
|
* (无项目级 commands 目录,skill 亦需插件形态承载),当前分发形态不可用。
|
|
21
21
|
* 适配器代码与全部能力保留(备将来改走插件形态),但不在安装面板提供;
|
|
22
|
-
*
|
|
22
|
+
* 旧记录中的遗留内容不在勾选范围时随清单差集与用户级差集清理移除(ADR 0006)。
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
const path = require('path');
|
|
@@ -55,6 +55,6 @@ module.exports = {
|
|
|
55
55
|
timeout: 10,
|
|
56
56
|
},
|
|
57
57
|
resolveDataDir,
|
|
58
|
-
//
|
|
58
|
+
// 退市:不在安装面板提供(代码保留;旧记录遗留随清单/用户级差集清理,ADR 0006)
|
|
59
59
|
selectable: false,
|
|
60
60
|
};
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 工具选择策略:安装面板的可见集合(未退市适配器)。
|
|
5
5
|
*
|
|
6
6
|
* 背景(2026-09-28 决策):MiniMax Code 实测不支持目录级 skill 与 command、
|
|
7
7
|
* Claude Code 团队当前未用,两者**退市**——适配器与全部能力保留(见各适配器
|
|
8
8
|
* 文件头),仅不在 `cbb setup` / `cbb update` 的安装面板提供。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 恢复入口:删掉适配器的 `selectable: false` 即回到面板(决策:不留参数开关)。
|
|
10
|
+
* 退市工具的取舍(ADR 0006,反转 beta.30 的粘性保留决策):
|
|
11
|
+
* - 退市工具**不进**选择集:面板、非交互沿用、setup 预设、用户级组件,
|
|
12
|
+
* 一律只用未退市适配器(`selectable !== false`);
|
|
13
|
+
* - 勾选范围即权威:`.last-tools` 只如实记录本次实际选择,不在范围内的
|
|
14
|
+
* 工具内容(含用户级 hook / 插件)由清单差集与用户级差集清理移除;
|
|
15
|
+
* - 恢复入口:删掉适配器的 `selectable: false` 即回到面板。
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
/** 面板/默认可选适配器(未标 selectable: false 的全部) */
|
|
@@ -22,24 +20,4 @@ function selectableAdapters(adapters) {
|
|
|
22
20
|
return adapters.filter(a => a.selectable !== false);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
* 把「本空间旧记录中的退市工具」并入选择结果(按注册表顺序追加,去重)。
|
|
27
|
-
* @param {Array} adapters 注册表全量
|
|
28
|
-
* @param {Array} selected 本次选择(面板 / 预设 / 记录沿用)
|
|
29
|
-
* @param {Set<string>|null} lastTools 该空间的旧工具记录(.last-tools)
|
|
30
|
-
* @returns {Array} 并入后的有序数组(无退市工具需保留时原样返回)
|
|
31
|
-
*/
|
|
32
|
-
function carryOverRetired(adapters, selected, lastTools) {
|
|
33
|
-
if (!lastTools || lastTools.size === 0) return selected;
|
|
34
|
-
const selectedNames = new Set(selected.map(a => a.name));
|
|
35
|
-
const carried = adapters.filter(a =>
|
|
36
|
-
a.selectable === false && lastTools.has(a.name) && !selectedNames.has(a.name));
|
|
37
|
-
return carried.length > 0 ? selected.concat(carried) : selected;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 结果集中退市工具的显示名(供安装输出提示,如「保留上次选择的 Claude Code」) */
|
|
41
|
-
function retiredNames(selected) {
|
|
42
|
-
return selected.filter(a => a.selectable === false).map(a => a.displayName);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = { selectableAdapters, carryOverRetired, retiredNames };
|
|
23
|
+
module.exports = { selectableAdapters };
|
|
@@ -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
|
|