@jspg-ai/coding-bb 0.0.3-beta.2 → 0.0.3-beta.21
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 +128 -116
- package/cbb/lib/install/claude-code.js +1 -3
- package/cbb/lib/install/init.js +344 -127
- package/cbb/lib/install/opencode.js +35 -31
- package/cbb/lib/install/qoder.js +1 -3
- package/cbb/lib/install/rule-rewrite.js +27 -0
- package/cbb/lib/install/setup-decision.js +48 -0
- package/cbb/lib/install/trae.js +43 -0
- package/cbb/lib/install/workbuddy.js +36 -0
- package/cbb/lib/openspec/index.js +337 -554
- package/cbb/lib/superpowers/index.js +246 -265
- package/cbb/lib/utils/check-update.js +13 -2
- package/cbb/lib/utils/gitignore.js +9 -2
- package/cbb/lib/utils/settings.js +17 -4
- package/cbb/lib/utils/tar.js +92 -0
- package/cbb/lib/utils/upstream.js +90 -0
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +32 -23
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +36 -28
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +15 -37
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +25 -9
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +108 -31
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
- package/config/openspec/schemas/spec-driven/schema.yaml +40 -30
- package/config/openspec/schemas/spec-driven/templates/design.md +0 -18
- package/config/openspec/schemas/spec-driven/templates/proposal.md +4 -3
- package/config/upstream-mirrors.json +12 -0
- package/config/workspace-agents.sample.md +45 -39
- package/openspec/.version +2 -3
- package/openspec/commands/apply.md +189 -175
- package/openspec/commands/archive.md +237 -216
- package/openspec/commands/bulk-archive.md +355 -327
- package/openspec/commands/continue.md +116 -105
- package/openspec/commands/explore.md +230 -199
- package/openspec/commands/ff.md +115 -104
- package/openspec/commands/new.md +74 -63
- package/openspec/commands/onboard.md +557 -548
- package/openspec/commands/propose.md +161 -150
- package/openspec/commands/sync.md +277 -249
- package/openspec/commands/update.md +92 -80
- package/openspec/commands/verify.md +175 -162
- package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
- package/openspec/skills/openspec-archive-change/SKILL.md +30 -8
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +36 -6
- package/openspec/skills/openspec-continue-change/SKILL.md +14 -2
- package/openspec/skills/openspec-explore/SKILL.md +21 -9
- package/openspec/skills/openspec-ff-change/SKILL.md +14 -2
- package/openspec/skills/openspec-new-change/SKILL.md +13 -1
- package/openspec/skills/openspec-onboard/SKILL.md +49 -39
- package/openspec/skills/openspec-propose/SKILL.md +15 -3
- package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
- package/openspec/skills/openspec-update-change/SKILL.md +27 -14
- package/openspec/skills/openspec-verify-change/SKILL.md +17 -3
- package/package.json +2 -2
- package/superpowers/.version +4 -4
- package/superpowers/skills/brainstorming/SKILL.md +47 -12
- package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
- package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
- package/superpowers/skills/brainstorming/visual-companion.md +6 -6
- package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
- package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
- package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
- package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
- package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
- package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
- package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
- package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
- package/superpowers/skills/executing-plans/SKILL.md +350 -41
- package/superpowers/skills/executing-plans/scripts/task-done +52 -0
- package/superpowers/skills/executing-plans/scripts/task-start +28 -0
- package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
- package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
- package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
- package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
- package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
- package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
- package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
- package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
- package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/superpowers/skills/test-driven-development/SKILL.md +10 -0
- package/superpowers/skills/using-superpowers/SKILL.md +2 -0
- package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
- package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
- package/superpowers/skills/writing-plans/SKILL.md +30 -9
- package/superpowers/skills/writing-skills/SKILL.md +4 -2
- package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
- package/cbb/worktrees/commands/worktree-close.md +0 -64
- package/cbb/worktrees/commands/worktree-init.md +0 -51
- package/cbb/worktrees/commands/worktree-push.md +0 -42
package/cbb/lib/install/init.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* OpenSpec Shared Standards 安装脚本
|
|
5
5
|
*
|
|
6
6
|
* 将AI 编码规范与工作流套件安装到目标项目中。
|
|
7
|
-
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)
|
|
7
|
+
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)、
|
|
8
|
+
* WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)
|
|
8
9
|
*
|
|
9
10
|
* 使用方式:
|
|
10
11
|
* npm install -g @jspg-ai/coding-bb # 首次使用
|
|
@@ -39,6 +40,7 @@ function execCmd(file, args, opts = {}) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
|
|
43
|
+
const setupDecision = require('./setup-decision');
|
|
42
44
|
|
|
43
45
|
// 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
|
|
44
46
|
// 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
|
|
@@ -51,14 +53,17 @@ const MANIFEST_FILE = path.join(MANIFEST_DIR, '.managed-by-cbb');
|
|
|
51
53
|
const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
|
|
52
54
|
|
|
53
55
|
// 适配器
|
|
56
|
+
// 适配器(数组顺序 = 工具选择面板的显示顺序)
|
|
54
57
|
const adapters = [
|
|
55
|
-
require('./claude-code'),
|
|
56
|
-
require('./qoder'),
|
|
57
58
|
require('./opencode'),
|
|
59
|
+
require('./workbuddy'),
|
|
60
|
+
require('./trae'),
|
|
61
|
+
require('./qoder'),
|
|
62
|
+
require('./claude-code'),
|
|
58
63
|
];
|
|
59
64
|
|
|
60
|
-
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量
|
|
61
|
-
// 供
|
|
65
|
+
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
|
|
66
|
+
// 供 cbbm update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
|
|
62
67
|
const SUPERPOWERS_WHITELIST = [
|
|
63
68
|
'writing-plans', // openspec apply:生成 plan.md(schema.yaml 引用)
|
|
64
69
|
'subagent-driven-development', // openspec apply:执行 plan.md(schema.yaml 引用)
|
|
@@ -68,8 +73,9 @@ const SUPERPOWERS_WHITELIST = [
|
|
|
68
73
|
'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
|
|
69
74
|
];
|
|
70
75
|
|
|
71
|
-
// worktree
|
|
72
|
-
//
|
|
76
|
+
// worktree 管理 skill 清单:init / close / push 三件套(SKILL.md + scripts)。
|
|
77
|
+
// 按 skill 形态部署到各工具的 skillsDir,使这三个能力可被 AI 在对话中自动触发
|
|
78
|
+
// (description 驱动),同时工具也会把它们暴露为斜杠命令供用户手动指定。
|
|
73
79
|
const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
|
|
74
80
|
|
|
75
81
|
// 装到空间的 tools skills:当前不分发。
|
|
@@ -83,9 +89,38 @@ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
|
83
89
|
// settings.json hook 工具
|
|
84
90
|
const settingsManager = require('../utils/settings');
|
|
85
91
|
|
|
92
|
+
/** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
|
|
93
|
+
function hookOpts(adapter) {
|
|
94
|
+
return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 工具历史名迁移:beta.16 及以前为 'codebuddy'(CodeBuddy 目录模型),
|
|
98
|
+
// beta.17 起统一为 'workbuddy'。存量 .last-tools / 清单里的旧名读取时归一,
|
|
99
|
+
// 下次安装由 saveLastTools 以新名落盘。
|
|
100
|
+
const LEGACY_TOOL_NAMES = { codebuddy: 'workbuddy' };
|
|
101
|
+
|
|
102
|
+
/** 工具名集合归一(旧名 → 新名);null 透传 */
|
|
103
|
+
function normalizeToolSet(set) {
|
|
104
|
+
if (!set) return set;
|
|
105
|
+
return new Set([...set].map(t => LEGACY_TOOL_NAMES[t] || t));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 迁移清理:移除本包写在旧用户级 settings 里的 hook(如 WorkBuddy 适配修正:
|
|
110
|
+
* ~/.codebuddy/settings.json → ~/.workbuddy/settings.json)。
|
|
111
|
+
* removeHooks 只删 _managedBy: cbb 标记的条目,文件清空则删除。返回 true 表示有改动。
|
|
112
|
+
*/
|
|
113
|
+
function cleanLegacySettings(adapter) {
|
|
114
|
+
if (!adapter.legacySettingsFile || !adapter.hookEvents) return false;
|
|
115
|
+
return settingsManager.removeHooks(adapter.legacySettingsFile, adapter.hookEvents, hookOpts(adapter));
|
|
116
|
+
}
|
|
117
|
+
|
|
86
118
|
// 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
|
|
87
119
|
const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
|
|
88
120
|
|
|
121
|
+
// 规则 frontmatter 改写(Trae/WorkBuddy 需 alwaysApply: true)
|
|
122
|
+
const { applyRuleRewrites } = require('./rule-rewrite');
|
|
123
|
+
|
|
89
124
|
// .gitignore 条目决策(纯函数:检测/写入/卸载移除)
|
|
90
125
|
const gitignore = require('../utils/gitignore');
|
|
91
126
|
|
|
@@ -184,7 +219,7 @@ function readLastTools() {
|
|
|
184
219
|
if (!fs.existsSync(filePath)) return null;
|
|
185
220
|
try {
|
|
186
221
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
187
|
-
return new Set(data.tools || []);
|
|
222
|
+
return normalizeToolSet(new Set(data.tools || []));
|
|
188
223
|
} catch (_) {
|
|
189
224
|
return null;
|
|
190
225
|
}
|
|
@@ -209,6 +244,39 @@ function removePath(targetPath) {
|
|
|
209
244
|
}
|
|
210
245
|
}
|
|
211
246
|
|
|
247
|
+
/**
|
|
248
|
+
* 清理因删除文件/目录而变空的祖先目录(从深到浅,止于 TARGET_DIR)。
|
|
249
|
+
*
|
|
250
|
+
* 删除清单路径只删到该路径本身,其父目录会留空(如改名后残留的空 `.cbb/skills/`)。
|
|
251
|
+
* 此处统一收尾,升级与卸载共用。仅删除「存在且为空」的目录,非空目录(含用户文件)
|
|
252
|
+
* 一律保留。
|
|
253
|
+
*
|
|
254
|
+
* @param {Iterable<string>} relativePaths 已删除的相对路径(正斜杠)
|
|
255
|
+
* @returns {string[]} 实际删除的空目录相对路径
|
|
256
|
+
*/
|
|
257
|
+
function cleanupEmptyParentDirs(relativePaths) {
|
|
258
|
+
const dirsToCheck = new Set();
|
|
259
|
+
for (const relativePath of relativePaths) {
|
|
260
|
+
let dir = path.dirname(relativePath);
|
|
261
|
+
while (dir && dir !== '.') {
|
|
262
|
+
dirsToCheck.add(dir);
|
|
263
|
+
dir = path.dirname(dir);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const removed = [];
|
|
268
|
+
[...dirsToCheck]
|
|
269
|
+
.sort((a, b) => b.split('/').length - a.split('/').length)
|
|
270
|
+
.forEach(dir => {
|
|
271
|
+
const fullPath = path.join(TARGET_DIR, dir);
|
|
272
|
+
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
|
|
273
|
+
removePath(fullPath);
|
|
274
|
+
removed.push(dir);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return removed;
|
|
278
|
+
}
|
|
279
|
+
|
|
212
280
|
/**
|
|
213
281
|
* 安全删除目标路径(文件或目录)
|
|
214
282
|
* 在清单中 → 直接删除(本包旧版本)
|
|
@@ -306,7 +374,8 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
306
374
|
// 同名文件存在时直接覆盖
|
|
307
375
|
removePath(destPath);
|
|
308
376
|
|
|
309
|
-
|
|
377
|
+
const content = applyRuleRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
|
|
378
|
+
fs.writeFileSync(destPath, content);
|
|
310
379
|
}
|
|
311
380
|
});
|
|
312
381
|
|
|
@@ -315,56 +384,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
315
384
|
// ── opencode instructions 登记 ────────────────────────────────
|
|
316
385
|
|
|
317
386
|
/**
|
|
318
|
-
* opencode 无原生 rules 目录:把 .opencode/
|
|
319
|
-
* opencode.
|
|
387
|
+
* opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json 的
|
|
388
|
+
* instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
|
|
389
|
+
* instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
|
|
390
|
+
* 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
|
|
320
391
|
* - 文件不存在:创建(记入清单,卸载时整体删除)
|
|
321
|
-
* -
|
|
322
|
-
*
|
|
392
|
+
* - 文件存在:幂等补齐缺失的 glob(保留用户已有 instructions)
|
|
393
|
+
* - JSONC(含注释)解析失败则只提示不改写
|
|
394
|
+
* 旧版登记在项目根 opencode.json,由 migrateLegacyInstructions 负责迁移清理。
|
|
323
395
|
*/
|
|
324
396
|
function injectInstructions(adapter, newPaths, managedPaths) {
|
|
325
|
-
const cfgPath = path.join(TARGET_DIR,
|
|
326
|
-
const
|
|
397
|
+
const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
|
|
398
|
+
const patterns = adapter.instructionsPatterns;
|
|
399
|
+
let cfg = {};
|
|
400
|
+
if (fs.existsSync(cfgPath)) {
|
|
401
|
+
try {
|
|
402
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
403
|
+
} catch (_) {
|
|
404
|
+
log(`${adapter.instructionsFile} 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${patterns.join('", "')}"`, 'warn');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (!cfg || typeof cfg !== 'object') cfg = {};
|
|
408
|
+
}
|
|
409
|
+
const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
410
|
+
const merged = [...list];
|
|
411
|
+
let added = false;
|
|
412
|
+
patterns.forEach(p => {
|
|
413
|
+
if (!merged.includes(p)) {
|
|
414
|
+
merged.push(p);
|
|
415
|
+
added = true;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
327
418
|
if (!fs.existsSync(cfgPath)) {
|
|
328
|
-
|
|
419
|
+
cfg.instructions = merged;
|
|
420
|
+
if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json';
|
|
421
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
329
422
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
330
|
-
newPaths.add(
|
|
331
|
-
log(`生成
|
|
332
|
-
|
|
423
|
+
newPaths.add(adapter.instructionsFile);
|
|
424
|
+
log(`生成 ${adapter.instructionsFile} 并登记 rules 加载(instructions: ${patterns.join(' | ')})`, 'success');
|
|
425
|
+
} else if (added) {
|
|
426
|
+
cfg.instructions = merged;
|
|
427
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
428
|
+
log(`已在 ${adapter.instructionsFile} instructions 中登记 rules glob`, 'success');
|
|
333
429
|
}
|
|
430
|
+
// 本包此前创建的文件:续登清单,避免升级清理误删
|
|
431
|
+
if (managedPaths.has(adapter.instructionsFile)) newPaths.add(adapter.instructionsFile);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 旧版(≤0.0.3-beta.2)把 rules glob 登记在项目根 opencode.json;迁移为
|
|
436
|
+
* .opencode/opencode.json 后需把旧 pattern 从根文件摘除(用户其余配置保留)。
|
|
437
|
+
* 根文件若由本包创建(在清单中),升级时由清单差集自动整体删除,此处只摘 pattern。
|
|
438
|
+
*/
|
|
439
|
+
function migrateLegacyInstructions(adapter) {
|
|
440
|
+
const legacyPattern = adapter.legacyInstructionsPattern;
|
|
441
|
+
if (!legacyPattern) return;
|
|
442
|
+
const cfgPath = path.join(TARGET_DIR, 'opencode.json');
|
|
443
|
+
if (!fs.existsSync(cfgPath)) return;
|
|
334
444
|
let cfg;
|
|
335
445
|
try {
|
|
336
446
|
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
337
447
|
} catch (_) {
|
|
338
|
-
log(
|
|
448
|
+
log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
|
|
339
449
|
return;
|
|
340
450
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
cfg.instructions
|
|
451
|
+
if (Array.isArray(cfg.instructions) && cfg.instructions.includes(legacyPattern)) {
|
|
452
|
+
cfg.instructions = cfg.instructions.filter(p => p !== legacyPattern);
|
|
453
|
+
if (cfg.instructions.length === 0) delete cfg.instructions;
|
|
344
454
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
345
|
-
log(
|
|
455
|
+
log('已从根 opencode.json 移除旧版 rules 登记(迁移至 .opencode/opencode.json)', 'success');
|
|
346
456
|
}
|
|
347
|
-
// 本包此前创建的文件:续登清单,避免升级清理误删
|
|
348
|
-
if (managedPaths.has('opencode.json')) newPaths.add('opencode.json');
|
|
349
457
|
}
|
|
350
458
|
|
|
351
|
-
/** 卸载时移除本包登记的 instructions
|
|
459
|
+
/** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
|
|
352
460
|
function removeInstructionsRegistrations() {
|
|
353
461
|
adapters.forEach(adapter => {
|
|
354
|
-
if (!adapter.
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
cfg
|
|
360
|
-
|
|
361
|
-
|
|
462
|
+
if (!adapter.instructionsFile) return;
|
|
463
|
+
const patterns = adapter.instructionsPatterns;
|
|
464
|
+
// 新位置:.opencode/opencode.json(cbb 创建的整体文件由清单删除兜底)
|
|
465
|
+
const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
|
|
466
|
+
if (fs.existsSync(cfgPath)) {
|
|
467
|
+
let cfg;
|
|
468
|
+
try {
|
|
469
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
470
|
+
} catch (_) {
|
|
471
|
+
cfg = null; // 不可解析的文件不动
|
|
472
|
+
}
|
|
473
|
+
if (cfg && Array.isArray(cfg.instructions)) {
|
|
474
|
+
const filtered = cfg.instructions.filter(p => !patterns.includes(p));
|
|
475
|
+
if (filtered.length !== cfg.instructions.length) {
|
|
476
|
+
if (filtered.length === 0) delete cfg.instructions;
|
|
477
|
+
else cfg.instructions = filtered;
|
|
478
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
479
|
+
log(`${adapter.instructionsFile}: 移除本包登记的 instructions,保留用户配置`, 'success');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
362
482
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
483
|
+
// 旧位置:项目根 opencode.json(未升级就卸载的老空间,或迁移残留)
|
|
484
|
+
const legacyPath = path.join(TARGET_DIR, 'opencode.json');
|
|
485
|
+
if (adapter.legacyInstructionsPattern && fs.existsSync(legacyPath)) {
|
|
486
|
+
let legacyCfg;
|
|
487
|
+
try {
|
|
488
|
+
legacyCfg = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
|
|
489
|
+
} catch (_) {
|
|
490
|
+
return; // 不可解析的文件不动
|
|
491
|
+
}
|
|
492
|
+
if (Array.isArray(legacyCfg.instructions) && legacyCfg.instructions.includes(adapter.legacyInstructionsPattern)) {
|
|
493
|
+
legacyCfg.instructions = legacyCfg.instructions.filter(p => p !== adapter.legacyInstructionsPattern);
|
|
494
|
+
if (legacyCfg.instructions.length === 0) delete legacyCfg.instructions;
|
|
495
|
+
fs.writeFileSync(legacyPath, JSON.stringify(legacyCfg, null, 2) + '\n');
|
|
496
|
+
log('opencode.json: 移除本包登记的旧版 instructions,保留用户配置', 'success');
|
|
497
|
+
}
|
|
368
498
|
}
|
|
369
499
|
});
|
|
370
500
|
}
|
|
@@ -499,13 +629,59 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
499
629
|
const srcPath = path.join(srcDir, f);
|
|
500
630
|
const destPath = path.join(destDir, prefix + f);
|
|
501
631
|
removePath(destPath);
|
|
502
|
-
const content = applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
|
|
632
|
+
const content = rewriteCommandName(adapter, sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
503
633
|
fs.writeFileSync(destPath, content);
|
|
504
634
|
});
|
|
505
635
|
|
|
506
636
|
return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
|
|
507
637
|
}
|
|
508
638
|
|
|
639
|
+
/**
|
|
640
|
+
* 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
|
|
641
|
+
* 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
|
|
642
|
+
* 会导致 opencode 拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述
|
|
643
|
+
* (超过 120 字截断),上游源文件不动(CLI 接管区)。
|
|
644
|
+
*/
|
|
645
|
+
function sanitizeCommandFrontmatter(content) {
|
|
646
|
+
const lines = content.split('\n');
|
|
647
|
+
if ((lines[0] || '').trim() !== '---') return content;
|
|
648
|
+
let end = -1;
|
|
649
|
+
for (let i = 1; i < lines.length; i++) {
|
|
650
|
+
if ((lines[i] || '').trim() === '---') { end = i; break; }
|
|
651
|
+
}
|
|
652
|
+
if (end === -1) return content;
|
|
653
|
+
let sentence = '';
|
|
654
|
+
for (let i = end + 1; i < lines.length; i++) {
|
|
655
|
+
const t = (lines[i] || '').trim();
|
|
656
|
+
if (t) { sentence = t; break; }
|
|
657
|
+
}
|
|
658
|
+
let changed = false;
|
|
659
|
+
for (let i = 1; i < end; i++) {
|
|
660
|
+
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
661
|
+
if (sentence) {
|
|
662
|
+
if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
|
|
663
|
+
lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
|
|
664
|
+
} else {
|
|
665
|
+
lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
|
|
666
|
+
}
|
|
667
|
+
changed = true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
|
|
675
|
+
* 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
|
|
676
|
+
*/
|
|
677
|
+
function rewriteCommandName(adapter, content) {
|
|
678
|
+
if (!adapter.commandNameKebab) return content;
|
|
679
|
+
return content.replace(/^name:\s*"?([A-Za-z]+):\s*([^"\r\n]+?)"?\s*\r?$/m, (m0, ns, rest) => {
|
|
680
|
+
const kebab = (ns + '-' + rest).toLowerCase().replace(/\s+/g, '-');
|
|
681
|
+
return `name: "${kebab}"`;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
509
685
|
/** 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir) */
|
|
510
686
|
function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
511
687
|
if (!adapter.commandsDir) return { count: 0, names: [] };
|
|
@@ -513,13 +689,6 @@ function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
|
513
689
|
adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
|
|
514
690
|
}
|
|
515
691
|
|
|
516
|
-
/** worktree 管理命令(cbb/worktrees/commands/ → adapter.worktreeCommandsDir) */
|
|
517
|
-
function deployWorktreeCommands(adapter, srcDir, filter) {
|
|
518
|
-
if (!adapter.worktreeCommandsDir) return { count: 0, names: [] };
|
|
519
|
-
return deployCommandFiles(
|
|
520
|
-
adapter, srcDir, path.join(TARGET_DIR, adapter.worktreeCommandsDir), adapter.worktreeCommandPrefix || '', filter);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
692
|
// ── 设置 OpenSpec 工作流 ──────────────────────────────────
|
|
524
693
|
|
|
525
694
|
/**
|
|
@@ -837,12 +1006,15 @@ async function init(targetDir, options = {}) {
|
|
|
837
1006
|
// 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
|
|
838
1007
|
const isNonInteractive = yes || !process.stdin.isTTY;
|
|
839
1008
|
let selectedAdapters;
|
|
1009
|
+
let fromRecord = false; // 选择来自预设或 .last-tools 记录(需落盘归一化后的新工具名)
|
|
840
1010
|
if (presetAdapters && presetAdapters.length > 0) {
|
|
841
1011
|
selectedAdapters = presetAdapters;
|
|
1012
|
+
fromRecord = true;
|
|
842
1013
|
} else if (isNonInteractive) {
|
|
843
1014
|
const lastTools = readLastTools();
|
|
844
1015
|
const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
|
|
845
1016
|
selectedAdapters = reused.length > 0 ? reused : adapters;
|
|
1017
|
+
fromRecord = reused.length > 0;
|
|
846
1018
|
if (yes && process.stdin.isTTY) {
|
|
847
1019
|
log(reused.length > 0
|
|
848
1020
|
? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
|
|
@@ -851,8 +1023,9 @@ async function init(targetDir, options = {}) {
|
|
|
851
1023
|
} else {
|
|
852
1024
|
selectedAdapters = await selectTools(isUpgrade);
|
|
853
1025
|
}
|
|
854
|
-
// 业务空间根记录本次选择,供 cbb update
|
|
855
|
-
|
|
1026
|
+
// 业务空间根记录本次选择,供 cbb update 等非交互场景沿用;
|
|
1027
|
+
// 沿用旧记录时同样落盘,使历史工具名(codebuddy → workbuddy)归一化自愈
|
|
1028
|
+
if (isWorkspaceRoot && fromRecord) {
|
|
856
1029
|
saveLastTools(selectedAdapters);
|
|
857
1030
|
}
|
|
858
1031
|
|
|
@@ -871,9 +1044,10 @@ async function init(targetDir, options = {}) {
|
|
|
871
1044
|
newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
|
|
872
1045
|
newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
|
|
873
1046
|
newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
|
|
874
|
-
// opencode:rules glob
|
|
875
|
-
if (adapter.
|
|
1047
|
+
// opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
|
|
1048
|
+
if (adapter.instructionsFile) {
|
|
876
1049
|
injectInstructions(adapter, newPaths, managedPaths);
|
|
1050
|
+
migrateLegacyInstructions(adapter);
|
|
877
1051
|
}
|
|
878
1052
|
});
|
|
879
1053
|
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
|
|
@@ -883,11 +1057,10 @@ async function init(targetDir, options = {}) {
|
|
|
883
1057
|
if (skillsAdapters.length > 0) {
|
|
884
1058
|
console.log(`\n 🔧 ${action}按需加载 Skills...`);
|
|
885
1059
|
const standardsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'skills');
|
|
886
|
-
const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
|
|
887
1060
|
const extendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
|
|
888
1061
|
|
|
889
1062
|
const skillsDests = skillsAdapters.map(a => a.skillsDir);
|
|
890
|
-
let stdCount = 0,
|
|
1063
|
+
let stdCount = 0, extCount = 0;
|
|
891
1064
|
skillsAdapters.forEach(adapter => {
|
|
892
1065
|
// 通用编码规范 Skills(cbb/dev-standards/skills)
|
|
893
1066
|
const stdResult = deploySkills(adapter, isUpgrade, managedPaths, standardsSkillsSrc);
|
|
@@ -897,12 +1070,6 @@ async function init(targetDir, options = {}) {
|
|
|
897
1070
|
}
|
|
898
1071
|
// 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
|
|
899
1072
|
//(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
|
|
900
|
-
// worktree 扩展 Skills:init / close / push 三件套(单层安装,空间根是唯一安装点)
|
|
901
|
-
const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, WORKTREE_SKILLS);
|
|
902
|
-
if (osExtResult.count > 0) {
|
|
903
|
-
osCount = osExtResult.count;
|
|
904
|
-
osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
905
|
-
}
|
|
906
1073
|
// 工具 Skills:当前不分发(恢复方式见 TOOLS_SKILLS 注释)
|
|
907
1074
|
const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
|
|
908
1075
|
if (extResult.count > 0) {
|
|
@@ -911,9 +1078,22 @@ async function init(targetDir, options = {}) {
|
|
|
911
1078
|
}
|
|
912
1079
|
});
|
|
913
1080
|
if (stdCount > 0) log(`规范 Skills → ${skillsDests.join(', ')} (${stdCount} 个)`, 'success');
|
|
914
|
-
if (osCount > 0) log(`worktree Skills → ${skillsDests.join(', ')} (${osCount} 个)`, 'success');
|
|
915
1081
|
if (extCount > 0) log(`扩展 Skills → ${skillsDests.join(', ')} (${extCount} 个)`, 'success');
|
|
916
1082
|
|
|
1083
|
+
// 部署 worktree 管理 Skills(init / close / push;skill 形态,可被对话自动触发)
|
|
1084
|
+
const worktreeSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
|
|
1085
|
+
let wtCount = 0;
|
|
1086
|
+
if (fs.existsSync(worktreeSkillsSrc)) {
|
|
1087
|
+
skillsAdapters.forEach(adapter => {
|
|
1088
|
+
const wtResult = deploySkills(adapter, isUpgrade, managedPaths, worktreeSkillsSrc, WORKTREE_SKILLS);
|
|
1089
|
+
if (wtResult.count > 0) {
|
|
1090
|
+
wtCount = wtResult.count;
|
|
1091
|
+
wtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
if (wtCount > 0) log(`Worktree Skills → ${skillsDests.join(', ')} (${wtCount} 个)`, 'success');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
917
1097
|
// 部署 Superpowers Skills(白名单:openspec 工作流依赖 + 方法论基底)
|
|
918
1098
|
const superpowersSkillsSrc = superpowers.SUPERPOWERS_SKILLS_DIR;
|
|
919
1099
|
let spCount = 0;
|
|
@@ -935,18 +1115,16 @@ async function init(targetDir, options = {}) {
|
|
|
935
1115
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
936
1116
|
}
|
|
937
1117
|
|
|
938
|
-
// 安装 Commands(官方 11 个 → /opsx:*,onboard
|
|
1118
|
+
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发)
|
|
939
1119
|
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
|
|
940
1120
|
if (commandsAdapters.length > 0) {
|
|
941
1121
|
console.log(`\n ⚡ ${action} Commands...`);
|
|
942
1122
|
const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
|
|
943
|
-
const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
|
|
944
|
-
const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
|
|
945
1123
|
const openspecCmdInclude = fs.readdirSync(commandsBaseSrc)
|
|
946
1124
|
.filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
|
|
947
1125
|
|
|
948
1126
|
const cmdDests = [...new Set(commandsAdapters
|
|
949
|
-
.
|
|
1127
|
+
.map(a => a.commandsDir)
|
|
950
1128
|
.filter(Boolean))];
|
|
951
1129
|
let cmdCount = 0;
|
|
952
1130
|
commandsAdapters.forEach(adapter => {
|
|
@@ -957,12 +1135,6 @@ async function init(targetDir, options = {}) {
|
|
|
957
1135
|
adapterCmdCount += baseResult.count;
|
|
958
1136
|
baseResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
|
|
959
1137
|
}
|
|
960
|
-
// worktree 管理 Commands(cbb/worktrees/commands/)
|
|
961
|
-
const extResult = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
|
|
962
|
-
if (extResult.count > 0) {
|
|
963
|
-
adapterCmdCount += extResult.count;
|
|
964
|
-
extResult.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
|
|
965
|
-
}
|
|
966
1138
|
cmdCount = Math.max(cmdCount, adapterCmdCount);
|
|
967
1139
|
});
|
|
968
1140
|
if (cmdCount > 0) log(`Commands → ${cmdDests.join(', ')} (${cmdCount} 个)`, 'success');
|
|
@@ -1001,11 +1173,15 @@ async function init(targetDir, options = {}) {
|
|
|
1001
1173
|
const settingsPath = adapter.settingsFileIsUserLevel
|
|
1002
1174
|
? adapter.settingsFile
|
|
1003
1175
|
: path.join(TARGET_DIR, adapter.settingsFile);
|
|
1004
|
-
settingsManager.addHooks(settingsPath, command, adapter.hookEvents);
|
|
1176
|
+
settingsManager.addHooks(settingsPath, command, adapter.hookEvents, hookOpts(adapter));
|
|
1005
1177
|
// 清理旧版本遗留的 SessionStart hook(现已仅安装 UserPromptSubmit)
|
|
1006
|
-
if (settingsManager.removeLegacyHooks(settingsPath)) {
|
|
1178
|
+
if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
|
|
1007
1179
|
log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
|
|
1008
1180
|
}
|
|
1181
|
+
// 迁移:清理旧用户级 settings(如 ~/.codebuddy → ~/.workbuddy)
|
|
1182
|
+
if (cleanLegacySettings(adapter)) {
|
|
1183
|
+
log(`迁移清理旧用户级配置 ${adapter.legacySettingsFile}`, 'success');
|
|
1184
|
+
}
|
|
1009
1185
|
if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
|
|
1010
1186
|
});
|
|
1011
1187
|
log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
|
|
@@ -1028,6 +1204,8 @@ async function init(targetDir, options = {}) {
|
|
|
1028
1204
|
if (oldPaths.length > MAX_DISPLAY) {
|
|
1029
1205
|
log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
|
|
1030
1206
|
}
|
|
1207
|
+
// 收尾:清掉因此变空的祖先目录(如改名后残留的空 .cbb/skills/)
|
|
1208
|
+
cleanupEmptyParentDirs(oldPaths).forEach(dir => log(` 清理空目录 ${dir}/`, 'dim'));
|
|
1031
1209
|
}
|
|
1032
1210
|
|
|
1033
1211
|
const kept = protectedKeptPaths(managedPaths, newPaths);
|
|
@@ -1098,28 +1276,41 @@ async function init(targetDir, options = {}) {
|
|
|
1098
1276
|
// ── cbb setup 命令 ──────────────────────────────────────
|
|
1099
1277
|
|
|
1100
1278
|
/**
|
|
1101
|
-
* 若 targetDir
|
|
1102
|
-
*
|
|
1103
|
-
* - 版本不同 →
|
|
1104
|
-
* -
|
|
1279
|
+
* 若 targetDir 已安装,比较「已装版本 vs 当前包版本」与「本次工具选择 vs .last-tools」
|
|
1280
|
+
* 决定是否继续安装(判定纯逻辑见 setup-decision.js)。
|
|
1281
|
+
* - 版本不同 → 'install'(setup 已整体确认过,直接升级到当前包版本,不再二次询问)
|
|
1282
|
+
* - 版本一致但本次选择含 .last-tools 之外的新工具(或无记录)→ 'install'
|
|
1283
|
+
* (beta.14 及以前的 bug:版本一致即整体跳过,新工具的空间级目录永不安装)
|
|
1284
|
+
* - 版本一致且所选 ⊆ .last-tools → 'skip'(不询问、不重复安装)
|
|
1105
1285
|
*
|
|
1106
1286
|
* 注:只比较「已装版本 vs 当前包版本」——安装动作只能落盘当前包的产物,
|
|
1107
1287
|
* registry 是否有更新由用户级版本检查 hook 提醒,不在此处联网查询。
|
|
1288
|
+
* @param {string} targetDir
|
|
1289
|
+
* @param {Array<{name:string}>} [selectedAdapters] 本次 setup 选中的工具
|
|
1108
1290
|
* @returns {'install'|'skip'}
|
|
1109
1291
|
*/
|
|
1110
|
-
function decideUpgradeIfInstalled(targetDir) {
|
|
1292
|
+
function decideUpgradeIfInstalled(targetDir, selectedAdapters) {
|
|
1111
1293
|
const installed = version.readInstalledVersion(targetDir);
|
|
1112
1294
|
if (!installed) return 'install'; // 未安装:不提示,直接继续
|
|
1113
1295
|
|
|
1114
1296
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
1297
|
+
const decision = setupDecision.decideSetupAction({
|
|
1298
|
+
sameVersion: version.compareVersions(installed, pkg.version) === 0,
|
|
1299
|
+
lastTools: normalizeToolSet(setupDecision.readLastToolsFrom(targetDir)),
|
|
1300
|
+
selectedNames: (selectedAdapters || []).map(a => a.name),
|
|
1301
|
+
});
|
|
1115
1302
|
|
|
1116
|
-
|
|
1117
|
-
if (version.compareVersions(installed, pkg.version) === 0) {
|
|
1303
|
+
if (decision.action === 'skip') {
|
|
1118
1304
|
step('✓', '版本', `v${installed}(已是最新,跳过)`);
|
|
1119
1305
|
return 'skip';
|
|
1120
1306
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1307
|
+
if (decision.reason === 'new-tools') {
|
|
1308
|
+
step('✓', '版本', `v${installed}(版本一致,新增工具 ${decision.newTools.join('、')} → 继续安装)`);
|
|
1309
|
+
} else if (decision.reason === 'no-tools-record') {
|
|
1310
|
+
step('✓', '版本', `v${installed}(无工具选择记录 → 继续安装)`);
|
|
1311
|
+
} else {
|
|
1312
|
+
step('✓', '版本', `v${installed} → v${pkg.version}`);
|
|
1313
|
+
}
|
|
1123
1314
|
return 'install';
|
|
1124
1315
|
}
|
|
1125
1316
|
|
|
@@ -1410,8 +1601,8 @@ async function initSingleWorkspace(w, opts = {}) {
|
|
|
1410
1601
|
console.log(` ${c('gray', `${names} → 编辑 workspace-config.json 填入真实仓库地址`)}`);
|
|
1411
1602
|
}
|
|
1412
1603
|
|
|
1413
|
-
// 5.
|
|
1414
|
-
const decision = decideUpgradeIfInstalled(targetDir);
|
|
1604
|
+
// 5. 若已安装:版本一致且无新增工具才跳过;否则继续安装/升级(setup 已整体确认过,不再二次询问)
|
|
1605
|
+
const decision = decideUpgradeIfInstalled(targetDir, opts.selectedAdapters);
|
|
1415
1606
|
if (decision === 'skip') {
|
|
1416
1607
|
return { ok: true, subdir: subdirName, upToDate: true };
|
|
1417
1608
|
}
|
|
@@ -1612,8 +1803,9 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1612
1803
|
if (hookAdapters.length > 0) {
|
|
1613
1804
|
const command = settingsManager.getCheckUpdateCommand();
|
|
1614
1805
|
hookAdapters.forEach(adapter => {
|
|
1615
|
-
settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents);
|
|
1616
|
-
settingsManager.removeLegacyHooks(adapter.settingsFile);
|
|
1806
|
+
settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents, hookOpts(adapter));
|
|
1807
|
+
settingsManager.removeLegacyHooks(adapter.settingsFile, hookOpts(adapter));
|
|
1808
|
+
cleanLegacySettings(adapter);
|
|
1617
1809
|
});
|
|
1618
1810
|
summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
|
|
1619
1811
|
}
|
|
@@ -1729,9 +1921,12 @@ async function uninstallUserLevelComponents() {
|
|
|
1729
1921
|
// 移除版本检查 hook(保留用户其余配置)
|
|
1730
1922
|
adapters.forEach(adapter => {
|
|
1731
1923
|
if (!adapter.settingsFile || !adapter.hookEvents) return;
|
|
1732
|
-
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents)) {
|
|
1924
|
+
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) {
|
|
1733
1925
|
log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
|
|
1734
1926
|
}
|
|
1927
|
+
if (cleanLegacySettings(adapter)) {
|
|
1928
|
+
log(`${adapter.legacySettingsFile}: 移除版本检查 hook(旧路径),保留用户配置`, 'success');
|
|
1929
|
+
}
|
|
1735
1930
|
});
|
|
1736
1931
|
|
|
1737
1932
|
removePath(USER_MANIFEST_FILE);
|
|
@@ -1760,7 +1955,7 @@ async function uninstall() {
|
|
|
1760
1955
|
// 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
|
|
1761
1956
|
let removeUserLevel = false;
|
|
1762
1957
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1763
|
-
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder 的版本检查 hook)?(y/N): ');
|
|
1958
|
+
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook)?(y/N): ');
|
|
1764
1959
|
removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
|
|
1765
1960
|
}
|
|
1766
1961
|
|
|
@@ -1779,11 +1974,11 @@ async function uninstall() {
|
|
|
1779
1974
|
: path.join(TARGET_DIR, adapter.settingsFile);
|
|
1780
1975
|
// 项目级:需在清单中才处理
|
|
1781
1976
|
if (!adapter.settingsFileIsUserLevel && !managedPaths.has(adapter.settingsFile)) return;
|
|
1782
|
-
if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents)) {
|
|
1977
|
+
if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents, hookOpts(adapter))) {
|
|
1783
1978
|
log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
|
|
1784
1979
|
}
|
|
1785
1980
|
// 清理旧版本遗留的 SessionStart hook(无版本差异,无条件清理)
|
|
1786
|
-
if (settingsManager.removeLegacyHooks(settingsPath)) {
|
|
1981
|
+
if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
|
|
1787
1982
|
log(`${adapter.settingsFile}: 清理遗留 SessionStart hook`, 'success');
|
|
1788
1983
|
}
|
|
1789
1984
|
skippedPaths.add(adapter.settingsFile);
|
|
@@ -1808,22 +2003,7 @@ async function uninstall() {
|
|
|
1808
2003
|
});
|
|
1809
2004
|
|
|
1810
2005
|
// 清理可能变空的父目录(从深到浅)
|
|
1811
|
-
|
|
1812
|
-
managedPaths.forEach(relativePath => {
|
|
1813
|
-
let dir = path.dirname(relativePath);
|
|
1814
|
-
while (dir && dir !== '.') {
|
|
1815
|
-
dirsToCheck.add(dir);
|
|
1816
|
-
dir = path.dirname(dir);
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
// 按深度排序,深层先处理
|
|
1820
|
-
[...dirsToCheck].sort((a, b) => b.split('/').length - a.split('/').length).forEach(dir => {
|
|
1821
|
-
const fullPath = path.join(TARGET_DIR, dir);
|
|
1822
|
-
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
|
|
1823
|
-
removePath(fullPath);
|
|
1824
|
-
log(`清理空目录 ${dir}/`, 'success');
|
|
1825
|
-
}
|
|
1826
|
-
});
|
|
2006
|
+
cleanupEmptyParentDirs(managedPaths).forEach(dir => log(`清理空目录 ${dir}/`, 'success'));
|
|
1827
2007
|
|
|
1828
2008
|
// 删除清单文件和 .va 目录
|
|
1829
2009
|
removePath(manifestDir);
|
|
@@ -1841,11 +2021,50 @@ async function uninstall() {
|
|
|
1841
2021
|
console.log(`\n✅ 卸载完成!共移除 ${removed} 个文件/目录\n`);
|
|
1842
2022
|
}
|
|
1843
2023
|
|
|
2024
|
+
// ── 上游版本检查 ──────────────────────────────────────────────
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
|
|
2028
|
+
* 供 cbbm check-upstream 调用。
|
|
2029
|
+
*/
|
|
2030
|
+
async function checkUpstream() {
|
|
2031
|
+
const results = await Promise.allSettled([
|
|
2032
|
+
openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
|
|
2033
|
+
superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
|
|
2034
|
+
]);
|
|
2035
|
+
|
|
2036
|
+
let outdated = false;
|
|
2037
|
+
for (const r of results) {
|
|
2038
|
+
if (r.status === 'rejected') {
|
|
2039
|
+
log(`检查失败: ${r.reason.message}`, 'error');
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
const { name, current, latest, outdated: isOutdated } = r.value;
|
|
2043
|
+
if (!latest) {
|
|
2044
|
+
log(`${name}: 无法获取上游版本`, 'warn');
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (!current) {
|
|
2048
|
+
log(`${name}: 本地无 pin(上游最新 ${latest})`, 'warn');
|
|
2049
|
+
outdated = true;
|
|
2050
|
+
} else if (isOutdated) {
|
|
2051
|
+
log(`${name}: ${current} → ${latest}(有新版本)`, 'warn');
|
|
2052
|
+
outdated = true;
|
|
2053
|
+
} else {
|
|
2054
|
+
log(`${name}: ${current} 已是最新`, 'success');
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
if (outdated) {
|
|
2059
|
+
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1844
2063
|
// ── CLI 入口 ──────────────────────────────────────────────
|
|
1845
2064
|
|
|
1846
2065
|
// 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
|
|
1847
2066
|
const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
|
|
1848
|
-
const INTERNAL_COMMANDS = ['check-update', 'wiki', 'wp'];
|
|
2067
|
+
const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
|
|
1849
2068
|
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
|
|
1850
2069
|
|
|
1851
2070
|
const args = process.argv.slice(2);
|
|
@@ -1884,11 +2103,11 @@ switch (command) {
|
|
|
1884
2103
|
const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
|
|
1885
2104
|
const upstreamOpenspec = restArgs.includes('--upstream-openspec');
|
|
1886
2105
|
if (upstreamSuperpowers || upstreamOpenspec) {
|
|
1887
|
-
//
|
|
2106
|
+
// 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
|
|
1888
2107
|
if (upstreamSuperpowers) {
|
|
1889
2108
|
superpowers.fetchLatestSkills().then(result => {
|
|
1890
2109
|
if (result.skipped) {
|
|
1891
|
-
log(`superpowers skills 已是最新 (${result.
|
|
2110
|
+
log(`superpowers skills 已是最新 (${result.tag})`, 'success');
|
|
1892
2111
|
}
|
|
1893
2112
|
const validation = superpowers.validateSkills();
|
|
1894
2113
|
if (!validation.valid) {
|
|
@@ -1904,22 +2123,13 @@ switch (command) {
|
|
|
1904
2123
|
}
|
|
1905
2124
|
if (upstreamOpenspec) {
|
|
1906
2125
|
openspec.fetchLatestOpenSpec().then(result => {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
1913
|
-
log(`openspec 工作流完整性校验通过`, 'success');
|
|
1914
|
-
} else {
|
|
1915
|
-
const validation = openspec.validateDeployed();
|
|
1916
|
-
if (!validation.valid) {
|
|
1917
|
-
log(`本地 openspec 工作流不完整,请重新运行 cbb update --upstream-openspec:`, 'error');
|
|
1918
|
-
for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
|
|
1919
|
-
process.exit(1);
|
|
1920
|
-
}
|
|
1921
|
-
log(`openspec 工作流完整性校验通过`, 'success');
|
|
2126
|
+
const validation = openspec.validateDeployed();
|
|
2127
|
+
if (!validation.valid) {
|
|
2128
|
+
log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
|
|
2129
|
+
for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
|
|
2130
|
+
process.exit(1);
|
|
1922
2131
|
}
|
|
2132
|
+
log(`openspec 工作流完整性校验通过`, 'success');
|
|
1923
2133
|
}).catch(err => {
|
|
1924
2134
|
log(`更新失败: ${err.message}`, 'error');
|
|
1925
2135
|
process.exit(1);
|
|
@@ -1942,6 +2152,12 @@ switch (command) {
|
|
|
1942
2152
|
case 'check-update':
|
|
1943
2153
|
require('../utils/check-update.js');
|
|
1944
2154
|
break;
|
|
2155
|
+
case 'check-upstream':
|
|
2156
|
+
checkUpstream().catch(err => {
|
|
2157
|
+
log(err.message, 'error');
|
|
2158
|
+
process.exit(1);
|
|
2159
|
+
});
|
|
2160
|
+
break;
|
|
1945
2161
|
case 'wp':
|
|
1946
2162
|
require('../wiki/index.js').publish(restArgs);
|
|
1947
2163
|
break;
|
|
@@ -1973,11 +2189,12 @@ function showHelp() {
|
|
|
1973
2189
|
@jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
|
|
1974
2190
|
|
|
1975
2191
|
cbbm check-update 版本检查(用户级 hook 每日自动触发)
|
|
2192
|
+
cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
|
|
1976
2193
|
cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
|
|
1977
2194
|
|
|
1978
|
-
|
|
1979
|
-
cbbm update --upstream-superpowers
|
|
1980
|
-
cbbm update --upstream-openspec
|
|
2195
|
+
维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
|
|
2196
|
+
cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
|
|
2197
|
+
cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
|
|
1981
2198
|
`);
|
|
1982
2199
|
return;
|
|
1983
2200
|
}
|