@jspg-ai/coding-bb 0.0.3-beta.2 → 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.
@@ -315,56 +315,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
315
315
  // ── opencode instructions 登记 ────────────────────────────────
316
316
 
317
317
  /**
318
- * opencode 无原生 rules 目录:把 .opencode/rules/*.md glob 登记进项目根
319
- * opencode.json 的 instructions(官方支持的加载方式)。
318
+ * opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json
319
+ * instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
320
+ * instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
321
+ * 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
320
322
  * - 文件不存在:创建(记入清单,卸载时整体删除)
321
- * - 文件存在:仅追加 pattern(幂等);若清单显示该文件本由本包创建,则续登
322
- * newPaths 防止升级清理误删;JSONC(含注释)解析失败则只提示不改写
323
+ * - 文件存在:幂等补齐缺失的 glob(保留用户已有 instructions)
324
+ * - JSONC(含注释)解析失败则只提示不改写
325
+ * 旧版登记在项目根 opencode.json,由 migrateLegacyInstructions 负责迁移清理。
323
326
  */
324
327
  function injectInstructions(adapter, newPaths, managedPaths) {
325
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
326
- 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
+ });
327
349
  if (!fs.existsSync(cfgPath)) {
328
- 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 });
329
353
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
330
- newPaths.add('opencode.json');
331
- log(`生成 opencode.json 并登记 rules 加载(instructions: ${pattern})`, 'success');
332
- 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');
333
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;
334
375
  let cfg;
335
376
  try {
336
377
  cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
337
378
  } catch (_) {
338
- log(`opencode.json 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${pattern}"`, 'warn');
379
+ log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
339
380
  return;
340
381
  }
341
- const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
342
- if (!list.includes(pattern)) {
343
- 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;
344
385
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
345
- log(`已在 opencode.json instructions 中登记 ${pattern}`, 'success');
386
+ log('已从根 opencode.json 移除旧版 rules 登记(迁移至 .opencode/opencode.json)', 'success');
346
387
  }
347
- // 本包此前创建的文件:续登清单,避免升级清理误删
348
- if (managedPaths.has('opencode.json')) newPaths.add('opencode.json');
349
388
  }
350
389
 
351
- /** 卸载时移除本包登记的 instructions pattern(用户创建的 opencode.json 只删 pattern 不删文件) */
390
+ /** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
352
391
  function removeInstructionsRegistrations() {
353
392
  adapters.forEach(adapter => {
354
- if (!adapter.instructionsPattern) return;
355
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
356
- if (!fs.existsSync(cfgPath)) return;
357
- let cfg;
358
- try {
359
- cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
360
- } catch (_) {
361
- 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
+ }
362
413
  }
363
- if (Array.isArray(cfg.instructions) && cfg.instructions.includes(adapter.instructionsPattern)) {
364
- cfg.instructions = cfg.instructions.filter(p => p !== adapter.instructionsPattern);
365
- if (cfg.instructions.length === 0) delete cfg.instructions;
366
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
367
- 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
+ }
368
429
  }
369
430
  });
370
431
  }
@@ -499,13 +560,37 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
499
560
  const srcPath = path.join(srcDir, f);
500
561
  const destPath = path.join(destDir, prefix + f);
501
562
  removePath(destPath);
502
- const content = applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
563
+ const content = sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8')));
503
564
  fs.writeFileSync(destPath, content);
504
565
  });
505
566
 
506
567
  return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
507
568
  }
508
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
+
509
594
  /** 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir) */
510
595
  function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
511
596
  if (!adapter.commandsDir) return { count: 0, names: [] };
@@ -871,9 +956,10 @@ async function init(targetDir, options = {}) {
871
956
  newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
872
957
  newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
873
958
  newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
874
- // opencode:rules glob 登记进项目根 opencode.json instructions
875
- if (adapter.instructionsPattern) {
959
+ // opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
960
+ if (adapter.instructionsFile) {
876
961
  injectInstructions(adapter, newPaths, managedPaths);
962
+ migrateLegacyInstructions(adapter);
877
963
  }
878
964
  });
879
965
  log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
@@ -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.2",
3
+ "version": "0.0.3-beta.3",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {