@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
package/cbb/lib/install/init.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 将AI 编码规范与工作流套件安装到目标项目中。
|
|
7
7
|
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)、
|
|
8
|
-
* WorkBuddy
|
|
8
|
+
* WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)、
|
|
9
|
+
* MiniMax Code (.minimax/rules/,规则经 AGENTS.md 托管导航加载,命令转 skill,hook 走用户级本地插件)
|
|
9
10
|
*
|
|
10
11
|
* 使用方式:
|
|
11
12
|
* npm install -g @jspg-ai/coding-bb # 首次使用
|
|
@@ -40,6 +41,7 @@ function execCmd(file, args, opts = {}) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
|
|
44
|
+
const setupDecision = require('./setup-decision');
|
|
43
45
|
|
|
44
46
|
// 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
|
|
45
47
|
// 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
|
|
@@ -55,10 +57,11 @@ const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
|
|
|
55
57
|
// 适配器(数组顺序 = 工具选择面板的显示顺序)
|
|
56
58
|
const adapters = [
|
|
57
59
|
require('./opencode'),
|
|
58
|
-
require('./
|
|
60
|
+
require('./workbuddy'),
|
|
59
61
|
require('./trae'),
|
|
60
62
|
require('./qoder'),
|
|
61
63
|
require('./claude-code'),
|
|
64
|
+
require('./minimax'),
|
|
62
65
|
];
|
|
63
66
|
|
|
64
67
|
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
|
|
@@ -75,12 +78,17 @@ const SUPERPOWERS_WHITELIST = [
|
|
|
75
78
|
// worktree 管理 skill 清单:init / close / push 三件套(SKILL.md + scripts)。
|
|
76
79
|
// 按 skill 形态部署到各工具的 skillsDir,使这三个能力可被 AI 在对话中自动触发
|
|
77
80
|
// (description 驱动),同时工具也会把它们暴露为斜杠命令供用户手动指定。
|
|
78
|
-
const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
|
|
81
|
+
const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push', 'cbb-local-mode'];
|
|
79
82
|
|
|
80
83
|
// 装到空间的 tools skills:当前不分发。
|
|
81
84
|
// cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
|
|
82
85
|
const TOOLS_SKILLS = [];
|
|
83
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
|
+
|
|
84
92
|
// 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
|
|
85
93
|
// onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
|
|
86
94
|
const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
@@ -88,20 +96,48 @@ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
|
88
96
|
// settings.json hook 工具
|
|
89
97
|
const settingsManager = require('../utils/settings');
|
|
90
98
|
|
|
99
|
+
// MiniMax 本地插件 hook(无 settings.json hooks 的适配器走插件形态)
|
|
100
|
+
const pluginHookManager = require('../utils/plugin-hook');
|
|
101
|
+
|
|
102
|
+
// AGENTS.md 托管导航块(agentsNav 适配器的项目级规则注入通道)
|
|
103
|
+
const agentsNav = require('./agents-nav');
|
|
104
|
+
|
|
105
|
+
// 命令 → skill 形态转换(commandsAsSkills 适配器,当前仅 MiniMax Code)
|
|
106
|
+
const commandSkill = require('./command-skill');
|
|
107
|
+
|
|
108
|
+
// 工具选择策略(退市工具不在面板提供;勾选范围即权威,ADR 0006)
|
|
109
|
+
const toolSelection = require('./tool-selection');
|
|
110
|
+
|
|
91
111
|
/** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
|
|
92
112
|
function hookOpts(adapter) {
|
|
93
113
|
return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
// 工具历史名迁移(codebuddy → workbuddy)与集合归一见 setup-decision.js
|
|
117
|
+
// (normalizeToolSet / LEGACY_TOOL_NAMES 单一来源)
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 迁移清理:移除本包写在旧用户级 settings 里的 hook(如 WorkBuddy 适配修正:
|
|
121
|
+
* ~/.codebuddy/settings.json → ~/.workbuddy/settings.json)。
|
|
122
|
+
* removeHooks 只删 _managedBy: cbb 标记的条目,文件清空则删除。返回 true 表示有改动。
|
|
123
|
+
*/
|
|
124
|
+
function cleanLegacySettings(adapter) {
|
|
125
|
+
if (!adapter.legacySettingsFile || !adapter.hookEvents) return false;
|
|
126
|
+
return settingsManager.removeHooks(adapter.legacySettingsFile, adapter.hookEvents, hookOpts(adapter));
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
// 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
|
|
97
130
|
const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
|
|
98
131
|
|
|
99
|
-
// 规则 frontmatter 改写(Trae/
|
|
132
|
+
// 规则 frontmatter 改写(Trae/WorkBuddy 需 alwaysApply: true)
|
|
100
133
|
const { applyRuleRewrites } = require('./rule-rewrite');
|
|
101
134
|
|
|
102
135
|
// .gitignore 条目决策(纯函数:检测/写入/卸载移除)
|
|
103
136
|
const gitignore = require('../utils/gitignore');
|
|
104
137
|
|
|
138
|
+
// git pre-commit hook:分支感知提交防线(ADR 0002,install/uninstall 调用)
|
|
139
|
+
const githook = require('../utils/githook');
|
|
140
|
+
|
|
105
141
|
// 业务空间配置(cbb setup 命令专用)
|
|
106
142
|
const workspaces = require('./workspaces');
|
|
107
143
|
|
|
@@ -117,6 +153,9 @@ const superpowers = require('../superpowers');
|
|
|
117
153
|
// OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
|
|
118
154
|
const openspec = require('../openspec');
|
|
119
155
|
|
|
156
|
+
// External 外部依赖管理(内置集成 + cbbm update --upstream-external)
|
|
157
|
+
const external = require('../external');
|
|
158
|
+
|
|
120
159
|
// ── 颜色与输出辅助 ──────────────────────────────────────
|
|
121
160
|
|
|
122
161
|
const { COLOR, COLOR_ENABLED, c, log, section, step, padVisual, progress, LOG_STYLES } = require('../utils/output');
|
|
@@ -127,13 +166,15 @@ function detectExistingInstall() {
|
|
|
127
166
|
}
|
|
128
167
|
|
|
129
168
|
/**
|
|
130
|
-
* 判定目标目录是否为业务空间根:workspace-config.json
|
|
131
|
-
*
|
|
169
|
+
* 判定目标目录是否为业务空间根:CBB-WORKSPACE.json(或旧名 workspace-config.json)存在且 .git 是目录。
|
|
170
|
+
* 需求工作树内也有该文件(tracked 副本),但其 .git 是文件(gitdir 指针),
|
|
132
171
|
* 必须以 .git 形态排除——避免「在 worktree 里误跑 cbb update」被当作空间根
|
|
133
172
|
* (误装 worktree 管理入口、在错误位置报旧导航提示)。
|
|
173
|
+
* 双名识别用于改名过渡期:存量空间(旧名)跑 update 时仍能识别并装上新版 hook。
|
|
134
174
|
*/
|
|
135
175
|
function detectWorkspaceRoot() {
|
|
136
|
-
if (!fs.existsSync(path.join(TARGET_DIR, '
|
|
176
|
+
if (!fs.existsSync(path.join(TARGET_DIR, 'CBB-WORKSPACE.json'))
|
|
177
|
+
&& !fs.existsSync(path.join(TARGET_DIR, 'workspace-config.json'))) return false;
|
|
137
178
|
const gitPath = path.join(TARGET_DIR, '.git');
|
|
138
179
|
return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
|
|
139
180
|
}
|
|
@@ -177,8 +218,13 @@ function writeManifest(paths, selectedAdapters) {
|
|
|
177
218
|
});
|
|
178
219
|
}
|
|
179
220
|
|
|
180
|
-
//
|
|
181
|
-
|
|
221
|
+
// 合并:选中的适配器写入新版本;未选中的(工具仍在包内)保持不变;
|
|
222
|
+
// 已从包中移除的工具(如 codebuddy)记录清理,避免清单里留下永久无效项
|
|
223
|
+
const knownNames = new Set(adapters.map(a => a.name));
|
|
224
|
+
const adapterVersions = {};
|
|
225
|
+
for (const [name, ver] of Object.entries(existingAdapterVersions)) {
|
|
226
|
+
if (knownNames.has(name)) adapterVersions[name] = ver;
|
|
227
|
+
}
|
|
182
228
|
selectedAdapters.forEach(adapter => {
|
|
183
229
|
adapterVersions[adapter.name] = pkg.version;
|
|
184
230
|
});
|
|
@@ -197,7 +243,7 @@ function readLastTools() {
|
|
|
197
243
|
if (!fs.existsSync(filePath)) return null;
|
|
198
244
|
try {
|
|
199
245
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
200
|
-
return new Set(data.tools || []);
|
|
246
|
+
return setupDecision.normalizeToolSet(new Set(data.tools || []));
|
|
201
247
|
} catch (_) {
|
|
202
248
|
return null;
|
|
203
249
|
}
|
|
@@ -298,19 +344,22 @@ function ask(question) {
|
|
|
298
344
|
async function selectTools(isUpgrade) {
|
|
299
345
|
const action = isUpgrade ? '升级' : '安装';
|
|
300
346
|
|
|
347
|
+
// 面板可选项 = 未退市适配器(Claude Code / MiniMax Code 已退市,见 tool-selection.js)
|
|
348
|
+
const selectable = toolSelection.selectableAdapters(adapters);
|
|
349
|
+
|
|
301
350
|
// 读取上次选择的工具:有记录则预选上次选择,首次安装则全部不选
|
|
302
351
|
const lastTools = readLastTools();
|
|
303
352
|
const preSelected = lastTools
|
|
304
|
-
?
|
|
353
|
+
? selectable.map((a, i) => lastTools.has(a.name) ? i : -1).filter(i => i >= 0)
|
|
305
354
|
: [];
|
|
306
355
|
|
|
307
356
|
// checkbox 多选:
|
|
308
357
|
// - 不切换 → 确认当前选择
|
|
309
358
|
// - 取消某项 → 只装剩下的
|
|
310
|
-
// - 全部不选 →
|
|
359
|
+
// - 全部不选 → 提示后回退全部可选项
|
|
311
360
|
const selectedIdx = await checkbox.promptCheckbox({
|
|
312
361
|
header: `🔧 选择要${action}的 AI 工具(空格切换,回车确认)`,
|
|
313
|
-
items:
|
|
362
|
+
items: selectable.map(a => ({
|
|
314
363
|
label: c('cyan', a.displayName),
|
|
315
364
|
})),
|
|
316
365
|
preSelected,
|
|
@@ -319,15 +368,15 @@ async function selectTools(isUpgrade) {
|
|
|
319
368
|
let result;
|
|
320
369
|
if (selectedIdx.size === 0) {
|
|
321
370
|
log(`未选择任何工具,默认全部${action}`, 'warn');
|
|
322
|
-
result =
|
|
371
|
+
result = selectable;
|
|
323
372
|
} else {
|
|
324
373
|
// 转成有序适配器数组(按原始顺序)
|
|
325
374
|
result = [...selectedIdx]
|
|
326
375
|
.sort((a, b) => a - b)
|
|
327
|
-
.map(i =>
|
|
376
|
+
.map(i => selectable[i]);
|
|
328
377
|
}
|
|
329
378
|
|
|
330
|
-
//
|
|
379
|
+
// 保存本次选择(勾选范围即权威,供下次运行时预选)
|
|
331
380
|
saveLastTools(result);
|
|
332
381
|
return result;
|
|
333
382
|
}
|
|
@@ -339,7 +388,7 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
339
388
|
const rulesDest = path.join(TARGET_DIR, adapter.rulesDir);
|
|
340
389
|
const action = isUpgrade ? '更新' : '安装';
|
|
341
390
|
|
|
342
|
-
const coreRules = ['cbb-coding-rule.md', 'cbb-priority.md', 'cbb-ai-behavior.md'];
|
|
391
|
+
const coreRules = ['cbb-coding-rule.md', 'cbb-priority.md', 'cbb-ai-behavior.md', 'cbb-workspace-discipline.md'];
|
|
343
392
|
|
|
344
393
|
// 确保目标目录存在
|
|
345
394
|
fs.mkdirSync(rulesDest, { recursive: true });
|
|
@@ -607,47 +656,13 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
607
656
|
const srcPath = path.join(srcDir, f);
|
|
608
657
|
const destPath = path.join(destDir, prefix + f);
|
|
609
658
|
removePath(destPath);
|
|
610
|
-
const content = rewriteCommandName(adapter, sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
659
|
+
const content = rewriteCommandName(adapter, commandSkill.sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
611
660
|
fs.writeFileSync(destPath, content);
|
|
612
661
|
});
|
|
613
662
|
|
|
614
663
|
return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
|
|
615
664
|
}
|
|
616
665
|
|
|
617
|
-
/**
|
|
618
|
-
* 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
|
|
619
|
-
* 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
|
|
620
|
-
* 会导致 opencode 拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述
|
|
621
|
-
* (超过 120 字截断),上游源文件不动(CLI 接管区)。
|
|
622
|
-
*/
|
|
623
|
-
function sanitizeCommandFrontmatter(content) {
|
|
624
|
-
const lines = content.split('\n');
|
|
625
|
-
if ((lines[0] || '').trim() !== '---') return content;
|
|
626
|
-
let end = -1;
|
|
627
|
-
for (let i = 1; i < lines.length; i++) {
|
|
628
|
-
if ((lines[i] || '').trim() === '---') { end = i; break; }
|
|
629
|
-
}
|
|
630
|
-
if (end === -1) return content;
|
|
631
|
-
let sentence = '';
|
|
632
|
-
for (let i = end + 1; i < lines.length; i++) {
|
|
633
|
-
const t = (lines[i] || '').trim();
|
|
634
|
-
if (t) { sentence = t; break; }
|
|
635
|
-
}
|
|
636
|
-
let changed = false;
|
|
637
|
-
for (let i = 1; i < end; i++) {
|
|
638
|
-
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
639
|
-
if (sentence) {
|
|
640
|
-
if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
|
|
641
|
-
lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
|
|
642
|
-
} else {
|
|
643
|
-
lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
|
|
644
|
-
}
|
|
645
|
-
changed = true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
666
|
/**
|
|
652
667
|
* opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
|
|
653
668
|
* 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
|
|
@@ -660,11 +675,49 @@ function rewriteCommandName(adapter, content) {
|
|
|
660
675
|
});
|
|
661
676
|
}
|
|
662
677
|
|
|
663
|
-
/**
|
|
678
|
+
/**
|
|
679
|
+
* MiniMax:无项目级 commands 目录,命令转 skill 形态(skill 即其斜杠命令),
|
|
680
|
+
* 每个命令落为 <skillsDir>/<prefix><name>/SKILL.md,目录名 = frontmatter name。
|
|
681
|
+
* @returns {{count:number, names:string[], destDir:string|null}} destDir 为清单登记用相对目录
|
|
682
|
+
*/
|
|
683
|
+
function deployCommandSkills(adapter, srcDir, filter) {
|
|
684
|
+
if (!fs.existsSync(srcDir)) return { count: 0, names: [], destDir: null };
|
|
685
|
+
const destDir = adapter.skillsDir;
|
|
686
|
+
const skillsRoot = path.join(TARGET_DIR, destDir);
|
|
687
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
688
|
+
|
|
689
|
+
// 与 skill 同构的扁平 .md 源文件(命令不是子目录)
|
|
690
|
+
const commandFiles = fs.readdirSync(srcDir)
|
|
691
|
+
.filter(f => f.endsWith('.md') && fs.statSync(path.join(srcDir, f)).isFile())
|
|
692
|
+
.filter(f => !filter || filter.includes(f));
|
|
693
|
+
|
|
694
|
+
const names = [];
|
|
695
|
+
commandFiles.forEach(f => {
|
|
696
|
+
const skillName = commandSkill.skillNameFor(f, adapter.commandPrefix || '');
|
|
697
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
698
|
+
removePath(skillDir);
|
|
699
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
700
|
+
const content = commandSkill.convertCommandToSkill(
|
|
701
|
+
applyContentRewrites(adapter, fs.readFileSync(path.join(srcDir, f), 'utf-8')),
|
|
702
|
+
skillName);
|
|
703
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
704
|
+
names.push(skillName);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return { count: commandFiles.length, names, destDir };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir;
|
|
712
|
+
* commandsAsSkills 适配器转 skill 形态 → adapter.skillsDir)
|
|
713
|
+
* @returns {{count:number, names:string[], destDir:string|null}}
|
|
714
|
+
*/
|
|
664
715
|
function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
665
|
-
if (
|
|
666
|
-
return
|
|
716
|
+
if (adapter.commandsAsSkills) return deployCommandSkills(adapter, srcDir, filter);
|
|
717
|
+
if (!adapter.commandsDir) return { count: 0, names: [], destDir: null };
|
|
718
|
+
const result = deployCommandFiles(
|
|
667
719
|
adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
|
|
720
|
+
return { ...result, destDir: adapter.commandsDir };
|
|
668
721
|
}
|
|
669
722
|
|
|
670
723
|
// ── 设置 OpenSpec 工作流 ──────────────────────────────────
|
|
@@ -984,22 +1037,29 @@ async function init(targetDir, options = {}) {
|
|
|
984
1037
|
// 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
|
|
985
1038
|
const isNonInteractive = yes || !process.stdin.isTTY;
|
|
986
1039
|
let selectedAdapters;
|
|
1040
|
+
let fromRecord = false; // 选择来自预设或 .last-tools 记录(需落盘归一化后的新工具名)
|
|
987
1041
|
if (presetAdapters && presetAdapters.length > 0) {
|
|
988
1042
|
selectedAdapters = presetAdapters;
|
|
1043
|
+
fromRecord = true;
|
|
989
1044
|
} else if (isNonInteractive) {
|
|
990
1045
|
const lastTools = readLastTools();
|
|
991
|
-
|
|
992
|
-
|
|
1046
|
+
// 旧记录只用于预选可选项——历史遗留的退市工具不并回(ADR 0006:勾选范围即权威)
|
|
1047
|
+
const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
|
|
1048
|
+
selectedAdapters = reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters);
|
|
1049
|
+
fromRecord = reused.length > 0;
|
|
993
1050
|
if (yes && process.stdin.isTTY) {
|
|
994
1051
|
log(reused.length > 0
|
|
995
1052
|
? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
|
|
996
|
-
: '--yes
|
|
1053
|
+
: '--yes 非交互模式,无上次选择记录,已默认选中全部可选工具', 'info');
|
|
997
1054
|
}
|
|
998
1055
|
} else {
|
|
999
1056
|
selectedAdapters = await selectTools(isUpgrade);
|
|
1000
1057
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1058
|
+
|
|
1059
|
+
// 退市工具不粘性保留(ADR 0006):选择集即权威,旧记录里的退市工具随清单差集清理
|
|
1060
|
+
|
|
1061
|
+
// 本次选择落盘(沿用旧记录时同样落盘:归一化历史工具名 + 剔除退市遗留记录)
|
|
1062
|
+
if (fromRecord) {
|
|
1003
1063
|
saveLastTools(selectedAdapters);
|
|
1004
1064
|
}
|
|
1005
1065
|
|
|
@@ -1018,13 +1078,14 @@ async function init(targetDir, options = {}) {
|
|
|
1018
1078
|
newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
|
|
1019
1079
|
newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
|
|
1020
1080
|
newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
|
|
1081
|
+
newPaths.add(`${adapter.rulesDir}/cbb-workspace-discipline.md`);
|
|
1021
1082
|
// opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
|
|
1022
1083
|
if (adapter.instructionsFile) {
|
|
1023
1084
|
injectInstructions(adapter, newPaths, managedPaths);
|
|
1024
1085
|
migrateLegacyInstructions(adapter);
|
|
1025
1086
|
}
|
|
1026
1087
|
});
|
|
1027
|
-
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (
|
|
1088
|
+
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (4 个文件)`, 'success');
|
|
1028
1089
|
|
|
1029
1090
|
// 安装 Skills(支持 skills 的适配器)
|
|
1030
1091
|
const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
|
|
@@ -1085,12 +1146,28 @@ async function init(targetDir, options = {}) {
|
|
|
1085
1146
|
log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
|
|
1086
1147
|
}
|
|
1087
1148
|
|
|
1149
|
+
// 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
|
|
1150
|
+
const externalSkillsSrc = external.EXTERNAL_DIR;
|
|
1151
|
+
let exCount = 0;
|
|
1152
|
+
if (fs.existsSync(externalSkillsSrc)) {
|
|
1153
|
+
skillsAdapters.forEach(adapter => {
|
|
1154
|
+
const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
|
|
1155
|
+
if (exResult.count > 0) {
|
|
1156
|
+
exCount = exResult.count;
|
|
1157
|
+
exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
|
|
1161
|
+
} else {
|
|
1162
|
+
log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1088
1165
|
// 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
|
|
1089
1166
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
1090
1167
|
}
|
|
1091
1168
|
|
|
1092
|
-
// 安装 Commands(官方 11 个 → /opsx:*,onboard
|
|
1093
|
-
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
|
|
1169
|
+
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;commandsAsSkills 适配器转 skill 形态)
|
|
1170
|
+
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir || a.commandsAsSkills);
|
|
1094
1171
|
if (commandsAdapters.length > 0) {
|
|
1095
1172
|
console.log(`\n ⚡ ${action} Commands...`);
|
|
1096
1173
|
const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
|
|
@@ -1098,7 +1175,7 @@ async function init(targetDir, options = {}) {
|
|
|
1098
1175
|
.filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
|
|
1099
1176
|
|
|
1100
1177
|
const cmdDests = [...new Set(commandsAdapters
|
|
1101
|
-
.map(a => a.commandsDir)
|
|
1178
|
+
.map(a => a.commandsDir || (a.commandsAsSkills ? `${a.skillsDir}(转 skill)` : null))
|
|
1102
1179
|
.filter(Boolean))];
|
|
1103
1180
|
let cmdCount = 0;
|
|
1104
1181
|
commandsAdapters.forEach(adapter => {
|
|
@@ -1107,7 +1184,7 @@ async function init(targetDir, options = {}) {
|
|
|
1107
1184
|
const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc, openspecCmdInclude);
|
|
1108
1185
|
if (baseResult.count > 0) {
|
|
1109
1186
|
adapterCmdCount += baseResult.count;
|
|
1110
|
-
baseResult.names.forEach(name => newPaths.add(`${
|
|
1187
|
+
baseResult.names.forEach(name => newPaths.add(`${baseResult.destDir}/${name}`));
|
|
1111
1188
|
}
|
|
1112
1189
|
cmdCount = Math.max(cmdCount, adapterCmdCount);
|
|
1113
1190
|
});
|
|
@@ -1128,18 +1205,27 @@ async function init(targetDir, options = {}) {
|
|
|
1128
1205
|
setupOpenSpec(isUpgrade, managedPaths);
|
|
1129
1206
|
newPaths.add('openspec/config.yaml');
|
|
1130
1207
|
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1208
|
+
// AGENTS.md 规则导航块(声明 agentsNav 的适配器;MiniMax 唯一项目级规则注入通道)。
|
|
1209
|
+
// 选中 → 注入/更新托管块(块外用户内容不动);未选中(本次 update 取消勾选)→ 精确摘除。
|
|
1210
|
+
let agentsNavState = null;
|
|
1211
|
+
let agentsNavFile = null;
|
|
1212
|
+
adapters.filter(a => a.agentsNav).forEach(adapter => {
|
|
1213
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
1214
|
+
if (selectedAdapters.some(sa => sa.name === adapter.name)) {
|
|
1215
|
+
const state = agentsNav.inject(target, adapter.name, adapter.agentsNav.text);
|
|
1216
|
+
if (state !== 'unchanged') { agentsNavState = state; agentsNavFile = adapter.agentsNav.file; }
|
|
1217
|
+
} else if (agentsNav.remove(target, adapter.name)) {
|
|
1218
|
+
agentsNavState = 'removed';
|
|
1137
1219
|
}
|
|
1220
|
+
});
|
|
1221
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') {
|
|
1222
|
+
log(`AGENTS.md 规则导航 → ${agentsNavFile}`, 'success');
|
|
1138
1223
|
}
|
|
1139
1224
|
|
|
1140
|
-
// 写入自动版本检查 hook
|
|
1225
|
+
// 写入自动版本检查 hook(settings.json 方言 + 本地插件方言)
|
|
1141
1226
|
const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
|
|
1142
|
-
|
|
1227
|
+
const pluginHookAdapters = selectedAdapters.filter(a => a.pluginHook);
|
|
1228
|
+
if (hookAdapters.length > 0 || pluginHookAdapters.length > 0) {
|
|
1143
1229
|
console.log(`\n 🔔 配置自动版本检查...`);
|
|
1144
1230
|
const command = settingsManager.getCheckUpdateCommand();
|
|
1145
1231
|
hookAdapters.forEach(adapter => {
|
|
@@ -1152,9 +1238,28 @@ async function init(targetDir, options = {}) {
|
|
|
1152
1238
|
if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
|
|
1153
1239
|
log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
|
|
1154
1240
|
}
|
|
1241
|
+
// 迁移:清理旧用户级 settings(如 ~/.codebuddy → ~/.workbuddy)
|
|
1242
|
+
if (cleanLegacySettings(adapter)) {
|
|
1243
|
+
log(`迁移清理旧用户级配置 ${adapter.legacySettingsFile}`, 'success');
|
|
1244
|
+
}
|
|
1155
1245
|
if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
|
|
1156
1246
|
});
|
|
1157
|
-
|
|
1247
|
+
if (hookAdapters.length > 0) {
|
|
1248
|
+
log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
|
|
1249
|
+
}
|
|
1250
|
+
// 本地插件方言(MiniMax:无 settings.json hooks)——用户级,不写入项目清单
|
|
1251
|
+
if (pluginHookAdapters.length > 0) {
|
|
1252
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1253
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1254
|
+
const info = pluginHookManager.install({
|
|
1255
|
+
dataDir: adapter.resolveDataDir(),
|
|
1256
|
+
...adapter.pluginHook,
|
|
1257
|
+
command,
|
|
1258
|
+
version: pkgVersion,
|
|
1259
|
+
});
|
|
1260
|
+
log(`${adapter.displayName} 版本检查插件 → ${info.dir}`, 'success');
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1158
1263
|
}
|
|
1159
1264
|
|
|
1160
1265
|
// 升级时清理旧路径(在旧清单中但不在新路径集中)。
|
|
@@ -1202,6 +1307,14 @@ async function init(targetDir, options = {}) {
|
|
|
1202
1307
|
// 更新 .gitignore
|
|
1203
1308
|
updateGitignore();
|
|
1204
1309
|
|
|
1310
|
+
// 安装分支感知提交防线(ADR 0002,仅业务空间根:空间根仓库 + .codespace/ 各应用)
|
|
1311
|
+
let hookSummary = null;
|
|
1312
|
+
if (isWorkspaceRoot) {
|
|
1313
|
+
hookSummary = githook.installGitHooksForWorkspace(TARGET_DIR);
|
|
1314
|
+
const appDone = hookSummary.apps.length;
|
|
1315
|
+
log(`提交防线 pre-commit hook → 空间根 + ${appDone} 个应用仓库(${hookSummary.root})`, 'success');
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1205
1318
|
// 完成摘要
|
|
1206
1319
|
console.log(`\n ✅ ${action}完成!\n`);
|
|
1207
1320
|
|
|
@@ -1230,13 +1343,14 @@ async function init(targetDir, options = {}) {
|
|
|
1230
1343
|
|
|
1231
1344
|
// 附加组件(单行)
|
|
1232
1345
|
const hasSkills = selectedAdapters.some(a => a.skillsDir);
|
|
1233
|
-
const hasCommands = selectedAdapters.some(a => a.commandsDir);
|
|
1234
|
-
const hasHooks = selectedAdapters.some(a => a.settingsFile && a.hookEvents);
|
|
1346
|
+
const hasCommands = selectedAdapters.some(a => a.commandsDir || a.commandsAsSkills);
|
|
1347
|
+
const hasHooks = selectedAdapters.some(a => (a.settingsFile && a.hookEvents) || a.pluginHook);
|
|
1235
1348
|
const extras = [];
|
|
1236
1349
|
if (hasSkills) extras.push('Skills');
|
|
1237
1350
|
if (hasCommands) extras.push('Commands');
|
|
1238
1351
|
if (hasHooks) extras.push('版本检查 hook');
|
|
1239
|
-
if (
|
|
1352
|
+
if (hookSummary) extras.push('提交防线 pre-commit hook');
|
|
1353
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') extras.push('AGENTS.md(规则导航)');
|
|
1240
1354
|
if (extras.length > 0) {
|
|
1241
1355
|
console.log(` ${extras.join(' / ')} 已配置`);
|
|
1242
1356
|
}
|
|
@@ -1246,38 +1360,51 @@ async function init(targetDir, options = {}) {
|
|
|
1246
1360
|
// ── cbb setup 命令 ──────────────────────────────────────
|
|
1247
1361
|
|
|
1248
1362
|
/**
|
|
1249
|
-
* 若 targetDir
|
|
1250
|
-
*
|
|
1251
|
-
* - 版本不同 →
|
|
1252
|
-
* -
|
|
1363
|
+
* 若 targetDir 已安装,比较「已装版本 vs 当前包版本」与「本次工具选择 vs .last-tools」
|
|
1364
|
+
* 决定是否继续安装(判定纯逻辑见 setup-decision.js)。
|
|
1365
|
+
* - 版本不同 → 'install'(setup 已整体确认过,直接升级到当前包版本,不再二次询问)
|
|
1366
|
+
* - 版本一致但本次选择含 .last-tools 之外的新工具(或无记录)→ 'install'
|
|
1367
|
+
* (beta.14 及以前的 bug:版本一致即整体跳过,新工具的空间级目录永不安装)
|
|
1368
|
+
* - 版本一致且所选 ⊆ .last-tools → 'skip'(不询问、不重复安装)
|
|
1253
1369
|
*
|
|
1254
1370
|
* 注:只比较「已装版本 vs 当前包版本」——安装动作只能落盘当前包的产物,
|
|
1255
1371
|
* registry 是否有更新由用户级版本检查 hook 提醒,不在此处联网查询。
|
|
1372
|
+
* @param {string} targetDir
|
|
1373
|
+
* @param {Array<{name:string}>} [selectedAdapters] 本次 setup 选中的工具
|
|
1256
1374
|
* @returns {'install'|'skip'}
|
|
1257
1375
|
*/
|
|
1258
|
-
function decideUpgradeIfInstalled(targetDir) {
|
|
1376
|
+
function decideUpgradeIfInstalled(targetDir, selectedAdapters) {
|
|
1259
1377
|
const installed = version.readInstalledVersion(targetDir);
|
|
1260
1378
|
if (!installed) return 'install'; // 未安装:不提示,直接继续
|
|
1261
1379
|
|
|
1262
1380
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
1381
|
+
const decision = setupDecision.decideSetupAction({
|
|
1382
|
+
sameVersion: version.compareVersions(installed, pkg.version) === 0,
|
|
1383
|
+
lastTools: setupDecision.normalizeToolSet(setupDecision.readLastToolsFrom(targetDir)),
|
|
1384
|
+
selectedNames: (selectedAdapters || []).map(a => a.name),
|
|
1385
|
+
});
|
|
1263
1386
|
|
|
1264
|
-
|
|
1265
|
-
if (version.compareVersions(installed, pkg.version) === 0) {
|
|
1387
|
+
if (decision.action === 'skip') {
|
|
1266
1388
|
step('✓', '版本', `v${installed}(已是最新,跳过)`);
|
|
1267
1389
|
return 'skip';
|
|
1268
1390
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1391
|
+
if (decision.reason === 'new-tools') {
|
|
1392
|
+
step('✓', '版本', `v${installed}(版本一致,新增工具 ${decision.newTools.join('、')} → 继续安装)`);
|
|
1393
|
+
} else if (decision.reason === 'no-tools-record') {
|
|
1394
|
+
step('✓', '版本', `v${installed}(无工具选择记录 → 继续安装)`);
|
|
1395
|
+
} else {
|
|
1396
|
+
step('✓', '版本', `v${installed} → v${pkg.version}`);
|
|
1397
|
+
}
|
|
1271
1398
|
return 'install';
|
|
1272
1399
|
}
|
|
1273
1400
|
|
|
1274
1401
|
/**
|
|
1275
|
-
* 检查业务空间根目录下是否有
|
|
1276
|
-
* 不存在时,将包内样例文件复制为
|
|
1402
|
+
* 检查业务空间根目录下是否有 CBB-WORKSPACE.json(init-worktree 技能依赖)。
|
|
1403
|
+
* 不存在时,将包内样例文件复制为 CBB-WORKSPACE.json。
|
|
1277
1404
|
* 返回 'exists' | 'created'(不打印,由调用方汇总输出)。
|
|
1278
1405
|
*/
|
|
1279
1406
|
function ensureWorkspaceConfig(targetDir) {
|
|
1280
|
-
const configPath = path.join(targetDir, '
|
|
1407
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1281
1408
|
if (fs.existsSync(configPath)) return 'exists';
|
|
1282
1409
|
|
|
1283
1410
|
const samplePath = path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG);
|
|
@@ -1286,12 +1413,12 @@ function ensureWorkspaceConfig(targetDir) {
|
|
|
1286
1413
|
}
|
|
1287
1414
|
|
|
1288
1415
|
/**
|
|
1289
|
-
* 读取
|
|
1416
|
+
* 读取 CBB-WORKSPACE.json 中的应用列表,将关联应用代码 clone/fetch 到 .codespace/ 目录。
|
|
1290
1417
|
* 首次 clone(耗时较长),后续 fetch --all(秒级更新)。
|
|
1291
1418
|
* 这是基准代码,init-worktree 技能后续只做轻量 fetch 检查。
|
|
1292
1419
|
*/
|
|
1293
1420
|
/**
|
|
1294
|
-
* 读取
|
|
1421
|
+
* 读取 CBB-WORKSPACE.json,将关联应用代码 clone/fetch 到 .codespace/ 目录。
|
|
1295
1422
|
* 不打印逐条日志,返回汇总对象由调用方渲染:
|
|
1296
1423
|
* { ok, fail, updated[], cloned[], fetchFailed[], sampleApps[], invalid[] }
|
|
1297
1424
|
* 长耗时(首次 clone),通过 onProgress 回调实时提示当前正在处理哪个应用。
|
|
@@ -1300,7 +1427,7 @@ function ensureWorkspaceConfig(targetDir) {
|
|
|
1300
1427
|
*/
|
|
1301
1428
|
async function syncAppRepos(targetDir, onProgress = () => {}) {
|
|
1302
1429
|
const summary = { ok: 0, fail: 0, updated: [], cloned: [], fetchFailed: [], sampleApps: [], invalid: [] };
|
|
1303
|
-
const configPath = path.join(targetDir, '
|
|
1430
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1304
1431
|
if (!fs.existsSync(configPath)) return summary;
|
|
1305
1432
|
|
|
1306
1433
|
let config;
|
|
@@ -1454,7 +1581,7 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
|
|
|
1454
1581
|
}
|
|
1455
1582
|
|
|
1456
1583
|
/**
|
|
1457
|
-
* 处理单个业务空间:计算子目录 → 检测/克隆 →
|
|
1584
|
+
* 处理单个业务空间:计算子目录 → 检测/克隆 → CBB-WORKSPACE.json → 拉取应用代码 → 升级询问 → 安装 AI 配置。
|
|
1458
1585
|
* 输出为紧凑步进行(✓ 标签 值),失败时不抛出也不退出,返回结果对象供调用方汇总。
|
|
1459
1586
|
* @param {object} [opts] - { selectedAdapters } cbb setup 上层选定的适配器(透传给空间级安装)
|
|
1460
1587
|
* @returns {Promise<{ok: boolean, error?: string, message?: string, subdir?: string, skipped?: boolean}>}
|
|
@@ -1516,28 +1643,28 @@ async function initSingleWorkspace(w, opts = {}) {
|
|
|
1516
1643
|
step('✓', '仓库', `已克隆 ${subdirName}/`);
|
|
1517
1644
|
}
|
|
1518
1645
|
|
|
1519
|
-
// 3.
|
|
1646
|
+
// 3. CBB-WORKSPACE.json:缺失则从样例生成,随后校验格式
|
|
1520
1647
|
const cfgState = ensureWorkspaceConfig(targetDir);
|
|
1521
|
-
const configPath = path.join(targetDir, '
|
|
1648
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1522
1649
|
const configValidation = workspaces.validateWorkspaceConfig(configPath);
|
|
1523
1650
|
if (!configValidation.valid) {
|
|
1524
|
-
step('✗', '配置', `
|
|
1651
|
+
step('✗', '配置', `CBB-WORKSPACE.json 校验失败:${configValidation.message}`,
|
|
1525
1652
|
{ iconColor: 'red', labelColor: 'red', valueColor: 'red' });
|
|
1526
|
-
// 撤销本次克隆,使工作空间回到初始状态,用户配好
|
|
1653
|
+
// 撤销本次克隆,使工作空间回到初始状态,用户配好 CBB-WORKSPACE.json 后可重新执行
|
|
1527
1654
|
if (wasCloned && fs.existsSync(targetDir)) {
|
|
1528
1655
|
removePath(targetDir);
|
|
1529
|
-
log(`已撤销本次克隆(${subdirName}/),请配好
|
|
1656
|
+
log(`已撤销本次克隆(${subdirName}/),请配好 CBB-WORKSPACE.json 后重新执行 cbb setup`, 'warn');
|
|
1530
1657
|
} else {
|
|
1531
|
-
log('请修正
|
|
1658
|
+
log('请修正 CBB-WORKSPACE.json 后重新执行 cbb setup', 'warn');
|
|
1532
1659
|
}
|
|
1533
1660
|
log(`参考格式:${path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG)}`, 'info');
|
|
1534
1661
|
return { ok: false, error: 'CONFIG_INVALID', message: configValidation.message };
|
|
1535
1662
|
}
|
|
1536
1663
|
if (cfgState === 'created') {
|
|
1537
|
-
step('!', '配置', '
|
|
1664
|
+
step('!', '配置', 'CBB-WORKSPACE.json 已从样例生成,请编辑 apps 后重跑 cbb setup',
|
|
1538
1665
|
{ iconColor: 'yellow', valueColor: 'yellow' });
|
|
1539
1666
|
} else {
|
|
1540
|
-
step('✓', '配置', `
|
|
1667
|
+
step('✓', '配置', `CBB-WORKSPACE.json(${configValidation.config.apps.length} 个应用)`);
|
|
1541
1668
|
}
|
|
1542
1669
|
|
|
1543
1670
|
// 4. 拉取关联应用代码到 .codespace/(基准代码,首次 clone 较慢,实时提示当前应用)
|
|
@@ -1555,11 +1682,11 @@ async function initSingleWorkspace(w, opts = {}) {
|
|
|
1555
1682
|
step(appWarn ? '!' : '✓', '应用', appParts.join(' · '), { iconColor: appWarn ? 'yellow' : 'green' });
|
|
1556
1683
|
if (apps.sampleApps.length > 0) {
|
|
1557
1684
|
const names = apps.sampleApps.map(a => a.name).join('、');
|
|
1558
|
-
console.log(` ${c('gray', `${names} → 编辑
|
|
1685
|
+
console.log(` ${c('gray', `${names} → 编辑 CBB-WORKSPACE.json 填入真实仓库地址`)}`);
|
|
1559
1686
|
}
|
|
1560
1687
|
|
|
1561
|
-
// 5.
|
|
1562
|
-
const decision = decideUpgradeIfInstalled(targetDir);
|
|
1688
|
+
// 5. 若已安装:版本一致且无新增工具才跳过;否则继续安装/升级(setup 已整体确认过,不再二次询问)
|
|
1689
|
+
const decision = decideUpgradeIfInstalled(targetDir, opts.selectedAdapters);
|
|
1563
1690
|
if (decision === 'skip') {
|
|
1564
1691
|
return { ok: true, subdir: subdirName, upToDate: true };
|
|
1565
1692
|
}
|
|
@@ -1581,7 +1708,7 @@ function describeBatchError(r) {
|
|
|
1581
1708
|
case 'DIR_MISMATCH': return '子目录已存在但不是该业务空间的仓库';
|
|
1582
1709
|
case 'CLONE_FAILED': return `git clone 失败:${r.message || ''}`.trim();
|
|
1583
1710
|
case 'PULL_FAILED': return `工作空间仓库同步失败:${r.message || ''}`.trim();
|
|
1584
|
-
case 'CONFIG_INVALID': return `
|
|
1711
|
+
case 'CONFIG_INVALID': return `CBB-WORKSPACE.json 不合法:${r.message || ''}`.trim();
|
|
1585
1712
|
case 'INIT_FAILED': return `初始化失败:${r.message || ''}`.trim();
|
|
1586
1713
|
default: return r.error || '未知错误';
|
|
1587
1714
|
}
|
|
@@ -1654,20 +1781,26 @@ async function setupWorkspace() {
|
|
|
1654
1781
|
.sort((a, b) => a - b)
|
|
1655
1782
|
.map(i => list[i]);
|
|
1656
1783
|
|
|
1657
|
-
// 2.5 选择 AI
|
|
1784
|
+
// 2.5 选择 AI 工具适配器(一次选择,作用于用户级组件与全部空间;非交互默认全选可选工具)
|
|
1785
|
+
const selectable = toolSelection.selectableAdapters(adapters);
|
|
1658
1786
|
let selectedAdapters;
|
|
1659
1787
|
if (!process.stdin.isTTY) {
|
|
1660
|
-
selectedAdapters =
|
|
1788
|
+
selectedAdapters = selectable;
|
|
1661
1789
|
} else {
|
|
1790
|
+
// 预选 = 所选空间 .cbb/.last-tools 记录的并集(目录规则与 initSingleWorkspace 一致:
|
|
1791
|
+
// repo 末段子目录;退市工具不并回;旧名归一;并集为空回退预选 opencode——首次引导行为)
|
|
1792
|
+
const records = selected.map(w => {
|
|
1793
|
+
const subdirName = workspaces.extractRepoBasename(w.repo);
|
|
1794
|
+
return subdirName ? setupDecision.readLastToolsFrom(path.join(parentDir, subdirName)) : null;
|
|
1795
|
+
});
|
|
1662
1796
|
const toolIdx = await checkbox.promptCheckbox({
|
|
1663
1797
|
header: '🔧 选择要安装的 AI 工具(空格切换,回车确认)',
|
|
1664
|
-
items:
|
|
1665
|
-
|
|
1666
|
-
preSelected: adapters.reduce((acc, a, i) => (a.name === 'opencode' ? acc.concat(i) : acc), []),
|
|
1798
|
+
items: selectable.map(a => ({ label: c('cyan', a.displayName) })),
|
|
1799
|
+
preSelected: setupDecision.preselectIndexes(records, selectable),
|
|
1667
1800
|
});
|
|
1668
1801
|
selectedAdapters = toolIdx.size === 0
|
|
1669
|
-
?
|
|
1670
|
-
: [...toolIdx].sort((a, b) => a - b).map(i =>
|
|
1802
|
+
? selectable
|
|
1803
|
+
: [...toolIdx].sort((a, b) => a - b).map(i => selectable[i]);
|
|
1671
1804
|
}
|
|
1672
1805
|
|
|
1673
1806
|
// 3. 配置摘要(业务空间 / 目标目录 / AI 工具)
|
|
@@ -1715,28 +1848,33 @@ function printSetupSummary(selected, selectedAdapters, parentDir) {
|
|
|
1715
1848
|
/**
|
|
1716
1849
|
* 安装用户级组件(与项目无关的内容,一次安装全局可用,幂等)。
|
|
1717
1850
|
*
|
|
1718
|
-
* - 清单:~/.cbb/.user-components
|
|
1851
|
+
* - 清单:~/.cbb/.user-components 只记录**本次实装**的用户级路径
|
|
1719
1852
|
* - 内容:check-update hook(settings 由 settingsManager 幂等管理)
|
|
1853
|
+
* - 差集清理(ADR 0006):不在本次目标集合内的适配器,其清单遗留路径、
|
|
1854
|
+
* 本包版本检查 hook、本地插件一并移除(仅删本包标记项,不动用户自装内容)
|
|
1720
1855
|
*/
|
|
1721
1856
|
const USER_MANIFEST_FILE = path.join(os.homedir(), '.cbb', '.user-components');
|
|
1722
1857
|
// 装到用户级的工具 skills(与具体项目无关,任何会话都可能用到)。
|
|
1723
1858
|
// 当前为空:cbb-wiki-ops 保留在源仓库(cbb/tools/)但不分发,有用户需要时再加入此清单。
|
|
1724
1859
|
const USER_LEVEL_SKILLS = [];
|
|
1725
1860
|
|
|
1726
|
-
async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
1861
|
+
async function setupUserLevelComponents(targetAdapters = toolSelection.selectableAdapters(adapters)) {
|
|
1727
1862
|
const home = os.homedir();
|
|
1728
1863
|
const srcDir = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
|
|
1729
1864
|
if (!fs.existsSync(srcDir)) return;
|
|
1730
1865
|
|
|
1731
|
-
|
|
1866
|
+
// 旧清单仅作差集清理基准,不再并入 managed(ADR 0006:勾选范围即权威,
|
|
1867
|
+
// 本次未实装的路径由下方差集移除,而非累积保留)
|
|
1868
|
+
let oldPaths = [];
|
|
1732
1869
|
const manifestDir = path.dirname(USER_MANIFEST_FILE);
|
|
1733
1870
|
fs.mkdirSync(manifestDir, { recursive: true });
|
|
1734
1871
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1735
1872
|
try {
|
|
1736
1873
|
const data = JSON.parse(fs.readFileSync(USER_MANIFEST_FILE, 'utf-8'));
|
|
1737
|
-
|
|
1874
|
+
oldPaths = data.paths || [];
|
|
1738
1875
|
} catch (_) { /* ignore parse errors */ }
|
|
1739
1876
|
}
|
|
1877
|
+
const managed = new Set();
|
|
1740
1878
|
|
|
1741
1879
|
const summaryParts = [];
|
|
1742
1880
|
|
|
@@ -1762,10 +1900,64 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1762
1900
|
hookAdapters.forEach(adapter => {
|
|
1763
1901
|
settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents, hookOpts(adapter));
|
|
1764
1902
|
settingsManager.removeLegacyHooks(adapter.settingsFile, hookOpts(adapter));
|
|
1903
|
+
cleanLegacySettings(adapter);
|
|
1765
1904
|
});
|
|
1766
1905
|
summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
|
|
1767
1906
|
}
|
|
1768
1907
|
|
|
1908
|
+
// 本地插件 hook(MiniMax:无 settings.json hooks,走 <dataDir>/plugins 本地插件,幂等覆盖)
|
|
1909
|
+
const pluginHookAdapters = targetAdapters.filter(a => a.pluginHook);
|
|
1910
|
+
if (pluginHookAdapters.length > 0) {
|
|
1911
|
+
const command = settingsManager.getCheckUpdateCommand();
|
|
1912
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1913
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1914
|
+
pluginHookManager.install({
|
|
1915
|
+
dataDir: adapter.resolveDataDir(),
|
|
1916
|
+
...adapter.pluginHook,
|
|
1917
|
+
command,
|
|
1918
|
+
version: pkgVersion,
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
summaryParts.push(`版本检查插件 → ${pluginHookAdapters.map(a => a.displayName).join('、')}`);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// ── 用户级差集清理(ADR 0006:不在本次勾选范围内的工具,用户级组件一并移除)──
|
|
1925
|
+
const targetNames = new Set(targetAdapters.map(a => a.name));
|
|
1926
|
+
|
|
1927
|
+
// 1) 清单路径差集:旧有新无 → 删除(仅本包管理路径,不碰用户自装内容)
|
|
1928
|
+
const removedPaths = [];
|
|
1929
|
+
oldPaths.forEach(relPath => {
|
|
1930
|
+
if (!managed.has(relPath)) {
|
|
1931
|
+
const target = path.join(home, relPath);
|
|
1932
|
+
if (fs.existsSync(target)) {
|
|
1933
|
+
removePath(target);
|
|
1934
|
+
removedPaths.push(relPath);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
if (removedPaths.length > 0) {
|
|
1939
|
+
summaryParts.push(`移除清单遗留 ×${removedPaths.length}`);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// 2) 非目标适配器:摘除本包版本检查 hook(仅删 _managedBy: cbb 标记项,保留用户配置)
|
|
1943
|
+
// + 本地插件目录
|
|
1944
|
+
const removedNames = [];
|
|
1945
|
+
adapters.filter(a => !targetNames.has(a.name)).forEach(adapter => {
|
|
1946
|
+
let touched = false;
|
|
1947
|
+
if (adapter.settingsFile && adapter.hookEvents) {
|
|
1948
|
+
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) touched = true;
|
|
1949
|
+
if (cleanLegacySettings(adapter)) touched = true;
|
|
1950
|
+
}
|
|
1951
|
+
if (adapter.pluginHook &&
|
|
1952
|
+
pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
|
|
1953
|
+
touched = true;
|
|
1954
|
+
}
|
|
1955
|
+
if (touched) removedNames.push(adapter.displayName);
|
|
1956
|
+
});
|
|
1957
|
+
if (removedNames.length > 0) {
|
|
1958
|
+
summaryParts.push(`清理 ${removedNames.join('、')} 的用户级组件`);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1769
1961
|
if (summaryParts.length > 0) step('✓', '用户级组件', summaryParts.join(' · '));
|
|
1770
1962
|
|
|
1771
1963
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
@@ -1778,14 +1970,15 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1778
1970
|
/**
|
|
1779
1971
|
* cbb update:就地升级当前目录内容 + 用户级组件。
|
|
1780
1972
|
*
|
|
1781
|
-
* 单层安装:统一安装器按空间根判定(
|
|
1782
|
-
* worktree
|
|
1973
|
+
* 单层安装:统一安装器按空间根判定(CBB-WORKSPACE.json + .git 目录)补齐
|
|
1974
|
+
* worktree 管理入口;旧子集清单升级时由清单差集机制自动补齐全量内容。
|
|
1783
1975
|
*/
|
|
1784
1976
|
async function updateInstalled() {
|
|
1785
|
-
// 用户级组件沿用本目录的工具选择(.last-tools
|
|
1977
|
+
// 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量。
|
|
1978
|
+
// 旧记录只预选可选项——历史遗留的退市工具不并回(ADR 0006)
|
|
1786
1979
|
const lastTools = readLastTools();
|
|
1787
|
-
const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
|
|
1788
|
-
await setupUserLevelComponents(reused.length > 0 ? reused : adapters);
|
|
1980
|
+
const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
|
|
1981
|
+
await setupUserLevelComponents(reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters));
|
|
1789
1982
|
|
|
1790
1983
|
const manifest = readManifest();
|
|
1791
1984
|
if (manifest.size === 0) {
|
|
@@ -1880,6 +2073,9 @@ async function uninstallUserLevelComponents() {
|
|
|
1880
2073
|
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) {
|
|
1881
2074
|
log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
|
|
1882
2075
|
}
|
|
2076
|
+
if (cleanLegacySettings(adapter)) {
|
|
2077
|
+
log(`${adapter.legacySettingsFile}: 移除版本检查 hook(旧路径),保留用户配置`, 'success');
|
|
2078
|
+
}
|
|
1883
2079
|
});
|
|
1884
2080
|
|
|
1885
2081
|
removePath(USER_MANIFEST_FILE);
|
|
@@ -1908,7 +2104,7 @@ async function uninstall() {
|
|
|
1908
2104
|
// 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
|
|
1909
2105
|
let removeUserLevel = false;
|
|
1910
2106
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1911
|
-
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.
|
|
2107
|
+
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook、MiniMax Code 本地插件)?(y/N): ');
|
|
1912
2108
|
removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
|
|
1913
2109
|
}
|
|
1914
2110
|
|
|
@@ -1937,6 +2133,25 @@ async function uninstall() {
|
|
|
1937
2133
|
skippedPaths.add(adapter.settingsFile);
|
|
1938
2134
|
});
|
|
1939
2135
|
|
|
2136
|
+
// 本地插件 hook(MiniMax,用户级组件):仅确认卸载用户级时删除插件目录
|
|
2137
|
+
if (removeUserLevel) {
|
|
2138
|
+
adapters.forEach(adapter => {
|
|
2139
|
+
if (!adapter.pluginHook) return;
|
|
2140
|
+
if (pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
|
|
2141
|
+
log(`移除 ${adapter.displayName} 版本检查插件(${adapter.resolveDataDir()})`, 'success');
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// AGENTS.md 规则导航块:本包托管块精确摘除(块外用户内容不动;文件归用户所有不进清单)
|
|
2147
|
+
adapters.forEach(adapter => {
|
|
2148
|
+
if (!adapter.agentsNav) return;
|
|
2149
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
2150
|
+
if (agentsNav.remove(target, adapter.name)) {
|
|
2151
|
+
log(`移除 ${adapter.agentsNav.file} 规则导航块`, 'success');
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
|
|
1940
2155
|
// opencode.json:移除本包登记的 instructions pattern(cbb 创建的整体文件由清单删除兜底)
|
|
1941
2156
|
removeInstructionsRegistrations();
|
|
1942
2157
|
|
|
@@ -1965,6 +2180,22 @@ async function uninstall() {
|
|
|
1965
2180
|
// 清理 .gitignore
|
|
1966
2181
|
cleanupGitignore();
|
|
1967
2182
|
|
|
2183
|
+
// 移除分支感知提交防线(ADR 0002):空间根 + .codespace/ 各应用(业务空间根专属)
|
|
2184
|
+
if (detectWorkspaceRoot()) {
|
|
2185
|
+
const results = [githook.uninstallGitHook(TARGET_DIR)];
|
|
2186
|
+
const codeDir = path.join(TARGET_DIR, '.codespace');
|
|
2187
|
+
if (fs.existsSync(codeDir)) {
|
|
2188
|
+
for (const name of fs.readdirSync(codeDir)) {
|
|
2189
|
+
const appDir = path.join(codeDir, name);
|
|
2190
|
+
if (fs.existsSync(path.join(appDir, '.git'))) {
|
|
2191
|
+
results.push(githook.uninstallGitHook(appDir));
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const removedHooks = results.filter(r => r !== 'noop').length;
|
|
2196
|
+
if (removedHooks > 0) log(`移除提交防线 pre-commit hook(${removedHooks} 个仓库)`, 'success');
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1968
2199
|
// 卸载用户级组件(如已确认)
|
|
1969
2200
|
if (removeUserLevel) {
|
|
1970
2201
|
console.log('');
|
|
@@ -1977,13 +2208,14 @@ async function uninstall() {
|
|
|
1977
2208
|
// ── 上游版本检查 ──────────────────────────────────────────────
|
|
1978
2209
|
|
|
1979
2210
|
/**
|
|
1980
|
-
* 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
|
|
2211
|
+
* 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
|
|
1981
2212
|
* 供 cbbm check-upstream 调用。
|
|
1982
2213
|
*/
|
|
1983
2214
|
async function checkUpstream() {
|
|
1984
2215
|
const results = await Promise.allSettled([
|
|
1985
2216
|
openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
|
|
1986
2217
|
superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
|
|
2218
|
+
external.checkUpstream().then(list => ({ name: 'external', list })),
|
|
1987
2219
|
]);
|
|
1988
2220
|
|
|
1989
2221
|
let outdated = false;
|
|
@@ -1992,6 +2224,22 @@ async function checkUpstream() {
|
|
|
1992
2224
|
log(`检查失败: ${r.reason.message}`, 'error');
|
|
1993
2225
|
continue;
|
|
1994
2226
|
}
|
|
2227
|
+
// external 是多依赖聚合结果,逐依赖渲染
|
|
2228
|
+
if (r.value.name === 'external') {
|
|
2229
|
+
for (const item of r.value.list) {
|
|
2230
|
+
if (item.error) {
|
|
2231
|
+
log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
|
|
2232
|
+
} else if (!item.latest) {
|
|
2233
|
+
log(`external/${item.name}: 无法获取上游版本`, 'warn');
|
|
2234
|
+
} else if (item.outdated) {
|
|
2235
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
|
|
2236
|
+
outdated = true;
|
|
2237
|
+
} else {
|
|
2238
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
1995
2243
|
const { name, current, latest, outdated: isOutdated } = r.value;
|
|
1996
2244
|
if (!latest) {
|
|
1997
2245
|
log(`${name}: 无法获取上游版本`, 'warn');
|
|
@@ -2009,7 +2257,7 @@ async function checkUpstream() {
|
|
|
2009
2257
|
}
|
|
2010
2258
|
|
|
2011
2259
|
if (outdated) {
|
|
2012
|
-
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
|
|
2260
|
+
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
|
|
2013
2261
|
}
|
|
2014
2262
|
}
|
|
2015
2263
|
|
|
@@ -2018,7 +2266,7 @@ async function checkUpstream() {
|
|
|
2018
2266
|
// 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
|
|
2019
2267
|
const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
|
|
2020
2268
|
const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
|
|
2021
|
-
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
|
|
2269
|
+
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
|
|
2022
2270
|
|
|
2023
2271
|
const args = process.argv.slice(2);
|
|
2024
2272
|
const command = args[0];
|
|
@@ -2026,11 +2274,11 @@ const restArgs = args.slice(1);
|
|
|
2026
2274
|
const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
|
|
2027
2275
|
|
|
2028
2276
|
if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
|
|
2029
|
-
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
|
|
2277
|
+
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
|
|
2030
2278
|
process.exit(1);
|
|
2031
2279
|
}
|
|
2032
2280
|
if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
|
|
2033
|
-
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
|
|
2281
|
+
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
|
|
2034
2282
|
log('升级已安装内容请使用 cbb update', 'warn');
|
|
2035
2283
|
process.exit(1);
|
|
2036
2284
|
}
|
|
@@ -2055,7 +2303,8 @@ switch (command) {
|
|
|
2055
2303
|
case 'update': {
|
|
2056
2304
|
const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
|
|
2057
2305
|
const upstreamOpenspec = restArgs.includes('--upstream-openspec');
|
|
2058
|
-
|
|
2306
|
+
const upstreamExternal = restArgs.includes('--upstream-external');
|
|
2307
|
+
if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
|
|
2059
2308
|
// 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
|
|
2060
2309
|
if (upstreamSuperpowers) {
|
|
2061
2310
|
superpowers.fetchLatestSkills().then(result => {
|
|
@@ -2088,6 +2337,26 @@ switch (command) {
|
|
|
2088
2337
|
process.exit(1);
|
|
2089
2338
|
});
|
|
2090
2339
|
}
|
|
2340
|
+
if (upstreamExternal) {
|
|
2341
|
+
// 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
|
|
2342
|
+
const extNames = [];
|
|
2343
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
2344
|
+
if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
|
|
2345
|
+
extNames.push(restArgs[i + 1]);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
external.fetchDeps(extNames).then(results => {
|
|
2349
|
+
const failed = results.filter(r => r.error);
|
|
2350
|
+
if (failed.length > 0) {
|
|
2351
|
+
failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
|
|
2352
|
+
process.exit(1);
|
|
2353
|
+
}
|
|
2354
|
+
log('external 外部依赖完整性校验通过', 'success');
|
|
2355
|
+
}).catch(err => {
|
|
2356
|
+
log(`更新失败: ${err.message}`, 'error');
|
|
2357
|
+
process.exit(1);
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2091
2360
|
} else {
|
|
2092
2361
|
updateInstalled().catch(err => {
|
|
2093
2362
|
log(err.message, 'error');
|
|
@@ -2142,12 +2411,13 @@ function showHelp() {
|
|
|
2142
2411
|
@jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
|
|
2143
2412
|
|
|
2144
2413
|
cbbm check-update 版本检查(用户级 hook 每日自动触发)
|
|
2145
|
-
cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
|
|
2414
|
+
cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
|
|
2146
2415
|
cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
|
|
2147
2416
|
|
|
2148
2417
|
维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
|
|
2149
2418
|
cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
|
|
2150
2419
|
cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
|
|
2420
|
+
cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
|
|
2151
2421
|
`);
|
|
2152
2422
|
return;
|
|
2153
2423
|
}
|