@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.
- package/cbb/lib/install/init.js +120 -34
- package/cbb/lib/install/opencode.js +37 -31
- package/package.json +1 -1
package/cbb/lib/install/init.js
CHANGED
|
@@ -315,56 +315,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
315
315
|
// ── opencode instructions 登记 ────────────────────────────────
|
|
316
316
|
|
|
317
317
|
/**
|
|
318
|
-
* opencode 无原生 rules 目录:把 .opencode/
|
|
319
|
-
* opencode.
|
|
318
|
+
* opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json 的
|
|
319
|
+
* instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
|
|
320
|
+
* instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
|
|
321
|
+
* 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
|
|
320
322
|
* - 文件不存在:创建(记入清单,卸载时整体删除)
|
|
321
|
-
* -
|
|
322
|
-
*
|
|
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,
|
|
326
|
-
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
|
+
});
|
|
327
349
|
if (!fs.existsSync(cfgPath)) {
|
|
328
|
-
|
|
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(
|
|
331
|
-
log(`生成
|
|
332
|
-
|
|
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(
|
|
379
|
+
log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
|
|
339
380
|
return;
|
|
340
381
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
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;
|
|
344
385
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
345
|
-
log(
|
|
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
|
|
390
|
+
/** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
|
|
352
391
|
function removeInstructionsRegistrations() {
|
|
353
392
|
adapters.forEach(adapter => {
|
|
354
|
-
if (!adapter.
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
cfg
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
875
|
-
if (adapter.
|
|
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
|
|
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
|
+
};
|