@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.
@@ -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:仅 cbb-design-to-wiki(openspec 设计阶段动作)。
76
- // cbb-wiki-ops 当前不分发(保留在源仓库,有用户需要时再加入 USER_LEVEL_SKILLS);Java 工具不装。
77
- const TOOLS_SKILLS = ['cbb-design-to-wiki'];
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/rules/*.md glob 登记进项目根
315
- * opencode.json 的 instructions(官方支持的加载方式)。
318
+ * opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json
319
+ * instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
320
+ * instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
321
+ * 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
316
322
  * - 文件不存在:创建(记入清单,卸载时整体删除)
317
- * - 文件存在:仅追加 pattern(幂等);若清单显示该文件本由本包创建,则续登
318
- * newPaths 防止升级清理误删;JSONC(含注释)解析失败则只提示不改写
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, 'opencode.json');
322
- const pattern = adapter.instructionsPattern;
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
- const cfg = { $schema: 'https://opencode.ai/config.json', instructions: [pattern] };
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('opencode.json');
327
- log(`生成 opencode.json 并登记 rules 加载(instructions: ${pattern})`, 'success');
328
- return;
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(`opencode.json 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${pattern}"`, 'warn');
379
+ log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
335
380
  return;
336
381
  }
337
- const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
338
- if (!list.includes(pattern)) {
339
- cfg.instructions = [...list, pattern];
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(`已在 opencode.json instructions 中登记 ${pattern}`, 'success');
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 pattern(用户创建的 opencode.json 只删 pattern 不删文件) */
390
+ /** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
348
391
  function removeInstructionsRegistrations() {
349
392
  adapters.forEach(adapter => {
350
- if (!adapter.instructionsPattern) return;
351
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
352
- if (!fs.existsSync(cfgPath)) return;
353
- let cfg;
354
- try {
355
- cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
356
- } catch (_) {
357
- return; // 不可解析的文件不动
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
- if (Array.isArray(cfg.instructions) && cfg.instructions.includes(adapter.instructionsPattern)) {
360
- cfg.instructions = cfg.instructions.filter(p => p !== adapter.instructionsPattern);
361
- if (cfg.instructions.length === 0) delete cfg.instructions;
362
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
363
- log('opencode.json: 移除本包登记的 instructions,保留用户配置', 'success');
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 登记进项目根 opencode.json instructions
871
- if (adapter.instructionsPattern) {
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:仅 cbb-design-to-wiki(设计文档发布属 openspec 设计阶段动作)
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(官方 12 个 → /opsx:*;worktree 管理 → /cbb:worktree-*,init/close/push 三件套)
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 注册进项目根 opencode.json 的 instructions 字段(官方支持的加载方式)
9
- * - skills:.opencode/skills/<name>/SKILL.md,结构与 Claude 同构
10
- * - commands:命令名 = 文件名(嵌套目录无命名空间,且 init.md 等会撞内置 /init),
11
- * 因此扁平化为 opsx-<name>.md(/opsx-propose);worktree 管理命令同理扁平化为
12
- * cbb-<name>.md(/cbb-worktree-init),部署副本中的 /opsx: 与 /cbb: 引用同步改写
13
- * - hooks:opencode UserPromptSubmit 等价事件,版本检查 hook 不安装(settingsFile 留空即跳过)
14
- */
15
-
16
- module.exports = {
17
- name: 'opencode',
18
- displayName: 'opencode',
19
- rulesDir: '.opencode/rules',
20
- skillsDir: '.opencode/skills',
21
- commandsDir: '.opencode/commands',
22
- // 部署时给命令文件名加此前缀(规避内置命令名冲突)
23
- commandPrefix: 'opsx-',
24
- // worktree 管理命令(非 openspec 上游)落在同一扁平目录,用 cbb- 前缀区分
25
- worktreeCommandsDir: '.opencode/commands',
26
- worktreeCommandPrefix: 'cbb-',
27
- // 部署副本中将该引用串改写为 opencode 的命令风格(配合 commandPrefix)
28
- contentRewrites: [['/opsx:', '/opsx-'], ['/cbb:', '/cbb-']],
29
- // rules 加载登记:注入项目根 opencode.json 的 instructions(glob,相对项目根)
30
- instructionsPattern: '.opencode/rules/*.md',
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspg-ai/coding-bb",
3
- "version": "0.0.3-beta.1",
3
+ "version": "0.0.3-beta.3",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {