@jspg-ai/coding-bb 0.0.3-beta.1 → 0.0.3-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cbb/lib/install/init.js +133 -41
- package/cbb/lib/install/opencode.js +37 -31
- package/package.json +1 -1
package/cbb/lib/install/init.js
CHANGED
|
@@ -72,9 +72,13 @@ const SUPERPOWERS_WHITELIST = [
|
|
|
72
72
|
// (单层安装:业务空间根是唯一安装点,worktree 管理入口 init 同样部署在根)
|
|
73
73
|
const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
|
|
74
74
|
|
|
75
|
-
// 装到空间的 tools skills
|
|
76
|
-
// cbb-wiki-ops
|
|
77
|
-
const TOOLS_SKILLS = [
|
|
75
|
+
// 装到空间的 tools skills:当前不分发。
|
|
76
|
+
// cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
|
|
77
|
+
const TOOLS_SKILLS = [];
|
|
78
|
+
|
|
79
|
+
// 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
|
|
80
|
+
// onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
|
|
81
|
+
const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
78
82
|
|
|
79
83
|
// settings.json hook 工具
|
|
80
84
|
const settingsManager = require('../utils/settings');
|
|
@@ -311,56 +315,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
311
315
|
// ── opencode instructions 登记 ────────────────────────────────
|
|
312
316
|
|
|
313
317
|
/**
|
|
314
|
-
* opencode 无原生 rules 目录:把 .opencode/
|
|
315
|
-
* opencode.
|
|
318
|
+
* opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json 的
|
|
319
|
+
* instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
|
|
320
|
+
* instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
|
|
321
|
+
* 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
|
|
316
322
|
* - 文件不存在:创建(记入清单,卸载时整体删除)
|
|
317
|
-
* -
|
|
318
|
-
*
|
|
323
|
+
* - 文件存在:幂等补齐缺失的 glob(保留用户已有 instructions)
|
|
324
|
+
* - JSONC(含注释)解析失败则只提示不改写
|
|
325
|
+
* 旧版登记在项目根 opencode.json,由 migrateLegacyInstructions 负责迁移清理。
|
|
319
326
|
*/
|
|
320
327
|
function injectInstructions(adapter, newPaths, managedPaths) {
|
|
321
|
-
const cfgPath = path.join(TARGET_DIR,
|
|
322
|
-
const
|
|
328
|
+
const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
|
|
329
|
+
const patterns = adapter.instructionsPatterns;
|
|
330
|
+
let cfg = {};
|
|
331
|
+
if (fs.existsSync(cfgPath)) {
|
|
332
|
+
try {
|
|
333
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
334
|
+
} catch (_) {
|
|
335
|
+
log(`${adapter.instructionsFile} 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${patterns.join('", "')}"`, 'warn');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!cfg || typeof cfg !== 'object') cfg = {};
|
|
339
|
+
}
|
|
340
|
+
const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
341
|
+
const merged = [...list];
|
|
342
|
+
let added = false;
|
|
343
|
+
patterns.forEach(p => {
|
|
344
|
+
if (!merged.includes(p)) {
|
|
345
|
+
merged.push(p);
|
|
346
|
+
added = true;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
323
349
|
if (!fs.existsSync(cfgPath)) {
|
|
324
|
-
|
|
350
|
+
cfg.instructions = merged;
|
|
351
|
+
if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json';
|
|
352
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
325
353
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
326
|
-
newPaths.add(
|
|
327
|
-
log(`生成
|
|
328
|
-
|
|
354
|
+
newPaths.add(adapter.instructionsFile);
|
|
355
|
+
log(`生成 ${adapter.instructionsFile} 并登记 rules 加载(instructions: ${patterns.join(' | ')})`, 'success');
|
|
356
|
+
} else if (added) {
|
|
357
|
+
cfg.instructions = merged;
|
|
358
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
359
|
+
log(`已在 ${adapter.instructionsFile} instructions 中登记 rules glob`, 'success');
|
|
329
360
|
}
|
|
361
|
+
// 本包此前创建的文件:续登清单,避免升级清理误删
|
|
362
|
+
if (managedPaths.has(adapter.instructionsFile)) newPaths.add(adapter.instructionsFile);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 旧版(≤0.0.3-beta.2)把 rules glob 登记在项目根 opencode.json;迁移为
|
|
367
|
+
* .opencode/opencode.json 后需把旧 pattern 从根文件摘除(用户其余配置保留)。
|
|
368
|
+
* 根文件若由本包创建(在清单中),升级时由清单差集自动整体删除,此处只摘 pattern。
|
|
369
|
+
*/
|
|
370
|
+
function migrateLegacyInstructions(adapter) {
|
|
371
|
+
const legacyPattern = adapter.legacyInstructionsPattern;
|
|
372
|
+
if (!legacyPattern) return;
|
|
373
|
+
const cfgPath = path.join(TARGET_DIR, 'opencode.json');
|
|
374
|
+
if (!fs.existsSync(cfgPath)) return;
|
|
330
375
|
let cfg;
|
|
331
376
|
try {
|
|
332
377
|
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
333
378
|
} catch (_) {
|
|
334
|
-
log(
|
|
379
|
+
log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
|
|
335
380
|
return;
|
|
336
381
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
cfg.instructions
|
|
382
|
+
if (Array.isArray(cfg.instructions) && cfg.instructions.includes(legacyPattern)) {
|
|
383
|
+
cfg.instructions = cfg.instructions.filter(p => p !== legacyPattern);
|
|
384
|
+
if (cfg.instructions.length === 0) delete cfg.instructions;
|
|
340
385
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
341
|
-
log(
|
|
386
|
+
log('已从根 opencode.json 移除旧版 rules 登记(迁移至 .opencode/opencode.json)', 'success');
|
|
342
387
|
}
|
|
343
|
-
// 本包此前创建的文件:续登清单,避免升级清理误删
|
|
344
|
-
if (managedPaths.has('opencode.json')) newPaths.add('opencode.json');
|
|
345
388
|
}
|
|
346
389
|
|
|
347
|
-
/** 卸载时移除本包登记的 instructions
|
|
390
|
+
/** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
|
|
348
391
|
function removeInstructionsRegistrations() {
|
|
349
392
|
adapters.forEach(adapter => {
|
|
350
|
-
if (!adapter.
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
cfg
|
|
356
|
-
|
|
357
|
-
|
|
393
|
+
if (!adapter.instructionsFile) return;
|
|
394
|
+
const patterns = adapter.instructionsPatterns;
|
|
395
|
+
// 新位置:.opencode/opencode.json(cbb 创建的整体文件由清单删除兜底)
|
|
396
|
+
const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
|
|
397
|
+
if (fs.existsSync(cfgPath)) {
|
|
398
|
+
let cfg;
|
|
399
|
+
try {
|
|
400
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
401
|
+
} catch (_) {
|
|
402
|
+
cfg = null; // 不可解析的文件不动
|
|
403
|
+
}
|
|
404
|
+
if (cfg && Array.isArray(cfg.instructions)) {
|
|
405
|
+
const filtered = cfg.instructions.filter(p => !patterns.includes(p));
|
|
406
|
+
if (filtered.length !== cfg.instructions.length) {
|
|
407
|
+
if (filtered.length === 0) delete cfg.instructions;
|
|
408
|
+
else cfg.instructions = filtered;
|
|
409
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
410
|
+
log(`${adapter.instructionsFile}: 移除本包登记的 instructions,保留用户配置`, 'success');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
358
413
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
414
|
+
// 旧位置:项目根 opencode.json(未升级就卸载的老空间,或迁移残留)
|
|
415
|
+
const legacyPath = path.join(TARGET_DIR, 'opencode.json');
|
|
416
|
+
if (adapter.legacyInstructionsPattern && fs.existsSync(legacyPath)) {
|
|
417
|
+
let legacyCfg;
|
|
418
|
+
try {
|
|
419
|
+
legacyCfg = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
|
|
420
|
+
} catch (_) {
|
|
421
|
+
return; // 不可解析的文件不动
|
|
422
|
+
}
|
|
423
|
+
if (Array.isArray(legacyCfg.instructions) && legacyCfg.instructions.includes(adapter.legacyInstructionsPattern)) {
|
|
424
|
+
legacyCfg.instructions = legacyCfg.instructions.filter(p => p !== adapter.legacyInstructionsPattern);
|
|
425
|
+
if (legacyCfg.instructions.length === 0) delete legacyCfg.instructions;
|
|
426
|
+
fs.writeFileSync(legacyPath, JSON.stringify(legacyCfg, null, 2) + '\n');
|
|
427
|
+
log('opencode.json: 移除本包登记的旧版 instructions,保留用户配置', 'success');
|
|
428
|
+
}
|
|
364
429
|
}
|
|
365
430
|
});
|
|
366
431
|
}
|
|
@@ -495,13 +560,37 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
495
560
|
const srcPath = path.join(srcDir, f);
|
|
496
561
|
const destPath = path.join(destDir, prefix + f);
|
|
497
562
|
removePath(destPath);
|
|
498
|
-
const content = applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
|
|
563
|
+
const content = sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8')));
|
|
499
564
|
fs.writeFileSync(destPath, content);
|
|
500
565
|
});
|
|
501
566
|
|
|
502
567
|
return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
|
|
503
568
|
}
|
|
504
569
|
|
|
570
|
+
/**
|
|
571
|
+
* 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
|
|
572
|
+
* 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
|
|
573
|
+
* 会导致 opencode 拒绝加载整个项目配置。此处仅在部署副本的 frontmatter 块内
|
|
574
|
+
* 移除空的 description 行(undefined 合法),上游源文件不动(CLI 接管区)。
|
|
575
|
+
*/
|
|
576
|
+
function sanitizeCommandFrontmatter(content) {
|
|
577
|
+
const lines = content.split('\n');
|
|
578
|
+
if ((lines[0] || '').trim() !== '---') return content;
|
|
579
|
+
let end = -1;
|
|
580
|
+
for (let i = 1; i < lines.length; i++) {
|
|
581
|
+
if ((lines[i] || '').trim() === '---') { end = i; break; }
|
|
582
|
+
}
|
|
583
|
+
if (end === -1) return content;
|
|
584
|
+
let changed = false;
|
|
585
|
+
for (let i = 1; i < end; i++) {
|
|
586
|
+
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
587
|
+
lines[i] = null;
|
|
588
|
+
changed = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
592
|
+
}
|
|
593
|
+
|
|
505
594
|
/** 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir) */
|
|
506
595
|
function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
507
596
|
if (!adapter.commandsDir) return { count: 0, names: [] };
|
|
@@ -867,9 +956,10 @@ async function init(targetDir, options = {}) {
|
|
|
867
956
|
newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
|
|
868
957
|
newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
|
|
869
958
|
newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
|
|
870
|
-
// opencode:rules glob
|
|
871
|
-
if (adapter.
|
|
959
|
+
// opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
|
|
960
|
+
if (adapter.instructionsFile) {
|
|
872
961
|
injectInstructions(adapter, newPaths, managedPaths);
|
|
962
|
+
migrateLegacyInstructions(adapter);
|
|
873
963
|
}
|
|
874
964
|
});
|
|
875
965
|
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
|
|
@@ -899,7 +989,7 @@ async function init(targetDir, options = {}) {
|
|
|
899
989
|
osCount = osExtResult.count;
|
|
900
990
|
osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
901
991
|
}
|
|
902
|
-
// 工具 Skills
|
|
992
|
+
// 工具 Skills:当前不分发(恢复方式见 TOOLS_SKILLS 注释)
|
|
903
993
|
const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
|
|
904
994
|
if (extResult.count > 0) {
|
|
905
995
|
extCount = extResult.count;
|
|
@@ -931,13 +1021,15 @@ async function init(targetDir, options = {}) {
|
|
|
931
1021
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
932
1022
|
}
|
|
933
1023
|
|
|
934
|
-
// 安装 Commands(官方
|
|
1024
|
+
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;worktree 管理 → /cbb:worktree-*,init/close/push 三件套)
|
|
935
1025
|
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
|
|
936
1026
|
if (commandsAdapters.length > 0) {
|
|
937
1027
|
console.log(`\n ⚡ ${action} Commands...`);
|
|
938
1028
|
const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
|
|
939
1029
|
const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
|
|
940
1030
|
const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
|
|
1031
|
+
const openspecCmdInclude = fs.readdirSync(commandsBaseSrc)
|
|
1032
|
+
.filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
|
|
941
1033
|
|
|
942
1034
|
const cmdDests = [...new Set(commandsAdapters
|
|
943
1035
|
.flatMap(a => [a.commandsDir, a.worktreeCommandsDir])
|
|
@@ -945,8 +1037,8 @@ async function init(targetDir, options = {}) {
|
|
|
945
1037
|
let cmdCount = 0;
|
|
946
1038
|
commandsAdapters.forEach(adapter => {
|
|
947
1039
|
let adapterCmdCount = 0;
|
|
948
|
-
// 官方 Commands(openspec/commands/)—
|
|
949
|
-
const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc);
|
|
1040
|
+
// 官方 Commands(openspec/commands/)— 部署除不分发清单外的全部
|
|
1041
|
+
const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc, openspecCmdInclude);
|
|
950
1042
|
if (baseResult.count > 0) {
|
|
951
1043
|
adapterCmdCount += baseResult.count;
|
|
952
1044
|
baseResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* opencode 适配器
|
|
3
|
-
*
|
|
4
|
-
* 将编码规范分发到 .opencode/ 目录(opencode 项目级配置域)。
|
|
5
|
-
*
|
|
6
|
-
* 与 Claude Code / Qoder 的映射差异:
|
|
7
|
-
* - rules:opencode 无原生 rules 目录,部署到 .opencode/rules/ 并由安装流程
|
|
8
|
-
* 将 glob
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
/**
|
|
2
|
+
* opencode 适配器
|
|
3
|
+
*
|
|
4
|
+
* 将编码规范分发到 .opencode/ 目录(opencode 项目级配置域)。
|
|
5
|
+
*
|
|
6
|
+
* 与 Claude Code / Qoder 的映射差异:
|
|
7
|
+
* - rules:opencode 无原生 rules 目录,部署到 .opencode/rules/ 并由安装流程
|
|
8
|
+
* 将 glob 登记进 .opencode/opencode.json 的 instructions 字段(opencode V2
|
|
9
|
+
* 官方支持的配置位置:全局目录 / 项目根 / .opencode 目录内均会被发现)
|
|
10
|
+
* - skills:.opencode/skills/<name>/SKILL.md,结构与 Claude 同构
|
|
11
|
+
* - commands:命令名 = 文件名(嵌套目录无命名空间,且 init.md 等会撞内置 /init),
|
|
12
|
+
* 因此扁平化为 opsx-<name>.md(/opsx-propose);worktree 管理命令同理扁平化为
|
|
13
|
+
* cbb-<name>.md(/cbb-worktree-init),部署副本中的 /opsx: 与 /cbb: 引用同步改写
|
|
14
|
+
* - hooks:opencode 无 UserPromptSubmit 等价事件,版本检查 hook 不安装(settingsFile 留空即跳过)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
name: 'opencode',
|
|
19
|
+
displayName: 'opencode',
|
|
20
|
+
rulesDir: '.opencode/rules',
|
|
21
|
+
skillsDir: '.opencode/skills',
|
|
22
|
+
commandsDir: '.opencode/commands',
|
|
23
|
+
// 部署时给命令文件名加此前缀(规避内置命令名冲突)
|
|
24
|
+
commandPrefix: 'opsx-',
|
|
25
|
+
// worktree 管理命令(非 openspec 上游)落在同一扁平目录,用 cbb- 前缀区分
|
|
26
|
+
worktreeCommandsDir: '.opencode/commands',
|
|
27
|
+
worktreeCommandPrefix: 'cbb-',
|
|
28
|
+
// 部署副本中将该引用串改写为 opencode 的命令风格(配合 commandPrefix)
|
|
29
|
+
contentRewrites: [['/opsx:', '/opsx-'], ['/cbb:', '/cbb-']],
|
|
30
|
+
// rules 加载登记:写入 .opencode/opencode.json 的 instructions。
|
|
31
|
+
// instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
|
|
32
|
+
// 登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
|
|
33
|
+
instructionsFile: '.opencode/opencode.json',
|
|
34
|
+
instructionsPatterns: ['rules/*.md', '.opencode/rules/*.md'],
|
|
35
|
+
// 旧版(≤0.0.3-beta.2)曾登记在项目根 opencode.json 的 pattern,迁移/卸载时识别清理用
|
|
36
|
+
legacyInstructionsPattern: '.opencode/rules/*.md',
|
|
37
|
+
};
|