kld-sdd 2.5.0 → 2.5.1
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/kld-sdd-guide.html +1109 -0
- package/lib/command-bridge.js +156 -0
- package/lib/deploy-codebuddy-hooks.js +99 -0
- package/lib/hook-gate-core.js +333 -0
- package/lib/init.js +127 -81
- package/lib/settings-merge.js +85 -0
- package/lib/skills-bundle.js +142 -5
- package/lib/tool-profiles.js +270 -0
- package/package.json +3 -2
- package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
- package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
- package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
- package/templates/hooks/codebuddy/settings.json +72 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +5 -5
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
- package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
- package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
package/lib/init.js
CHANGED
|
@@ -17,83 +17,25 @@ const os = require('os');
|
|
|
17
17
|
const {
|
|
18
18
|
SKILLS_BUNDLE_NAME,
|
|
19
19
|
OPSX_SKILL_DIRS,
|
|
20
|
-
|
|
20
|
+
OPSX_SKILL_METADATA,
|
|
21
|
+
renderTemplate,
|
|
22
|
+
validateRenderedContent,
|
|
21
23
|
} = require('./skills-bundle');
|
|
24
|
+
const { getToolProfile, buildToolConfigs } = require('./tool-profiles');
|
|
25
|
+
const {
|
|
26
|
+
deployKunlunCommandBridges,
|
|
27
|
+
cleanupManagedKunlunBridges,
|
|
28
|
+
BRIDGE_MARKER,
|
|
29
|
+
} = require('./command-bridge');
|
|
30
|
+
const { deployCodebuddyHookPack } = require('./deploy-codebuddy-hooks');
|
|
22
31
|
|
|
23
32
|
const rl = readline.createInterface({
|
|
24
33
|
input: process.stdin,
|
|
25
34
|
output: process.stdout
|
|
26
35
|
});
|
|
27
36
|
|
|
28
|
-
//
|
|
29
|
-
const TOOL_CONFIGS =
|
|
30
|
-
cursor: {
|
|
31
|
-
name: 'Cursor',
|
|
32
|
-
configDir: '.cursor',
|
|
33
|
-
commandsDir: '.cursor/commands',
|
|
34
|
-
opsxDir: '.cursor/commands/opsx',
|
|
35
|
-
skillsDir: '.cursor/skills',
|
|
36
|
-
agentType: 'cursor'
|
|
37
|
-
},
|
|
38
|
-
claude: {
|
|
39
|
-
name: 'Claude Code',
|
|
40
|
-
configDir: '.claude',
|
|
41
|
-
commandsDir: '.claude/commands',
|
|
42
|
-
opsxDir: '.claude/commands/opsx',
|
|
43
|
-
skillsDir: '.claude/skills',
|
|
44
|
-
agentType: 'claude-code'
|
|
45
|
-
},
|
|
46
|
-
codebuddy: {
|
|
47
|
-
name: 'CodeBuddy',
|
|
48
|
-
configDir: '.codebuddy',
|
|
49
|
-
commandsDir: '.codebuddy/commands',
|
|
50
|
-
opsxDir: '.codebuddy/commands/opsx',
|
|
51
|
-
skillsDir: '.codebuddy/skills',
|
|
52
|
-
agentType: 'codebuddy'
|
|
53
|
-
},
|
|
54
|
-
qoder: {
|
|
55
|
-
name: 'Qoder',
|
|
56
|
-
configDir: '.qoder',
|
|
57
|
-
commandsDir: '.qoder/commands',
|
|
58
|
-
opsxDir: '.qoder/commands/opsx',
|
|
59
|
-
skillsDir: '.qoder/skills',
|
|
60
|
-
agentType: 'qoder'
|
|
61
|
-
},
|
|
62
|
-
opencode: {
|
|
63
|
-
name: 'OpenCode',
|
|
64
|
-
configDir: '.opencode',
|
|
65
|
-
commandsDir: '.opencode/commands',
|
|
66
|
-
opsxDir: '.opencode/commands/opsx',
|
|
67
|
-
skillsDir: '.opencode/skills',
|
|
68
|
-
agentType: 'opencode',
|
|
69
|
-
// opencode 原生命令使用 command/(单数),需要额外清理
|
|
70
|
-
nativeCommandDir: '.opencode/command'
|
|
71
|
-
},
|
|
72
|
-
kunlunzhima: {
|
|
73
|
-
name: 'KunlunZhima',
|
|
74
|
-
configDir: '.kunlunzhima',
|
|
75
|
-
commandsDir: '.kunlunzhima/commands',
|
|
76
|
-
opsxDir: '.kunlunzhima/commands/opsx',
|
|
77
|
-
skillsDir: '.kunlunzhima/skills',
|
|
78
|
-
agentType: 'kunlunzhima'
|
|
79
|
-
},
|
|
80
|
-
workbuddy: {
|
|
81
|
-
name: 'WorkBuddy',
|
|
82
|
-
configDir: '.workbuddy',
|
|
83
|
-
commandsDir: '.workbuddy/commands',
|
|
84
|
-
opsxDir: '.workbuddy/commands/opsx',
|
|
85
|
-
skillsDir: '.workbuddy/skills',
|
|
86
|
-
agentType: 'workbuddy'
|
|
87
|
-
},
|
|
88
|
-
codex: {
|
|
89
|
-
name: 'Codex',
|
|
90
|
-
configDir: '.agents',
|
|
91
|
-
commandsDir: '.agents/commands',
|
|
92
|
-
opsxDir: '.agents/commands/opsx',
|
|
93
|
-
skillsDir: '.agents/skills',
|
|
94
|
-
agentType: 'codex'
|
|
95
|
-
}
|
|
96
|
-
};
|
|
37
|
+
// 工具类型配置(路径字段从 TOOL_PROFILES 派生,避免双份漂移)
|
|
38
|
+
const TOOL_CONFIGS = buildToolConfigs();
|
|
97
39
|
|
|
98
40
|
/**
|
|
99
41
|
* 询问用户选择
|
|
@@ -331,6 +273,8 @@ function copyDir(source, target) {
|
|
|
331
273
|
}
|
|
332
274
|
|
|
333
275
|
function copyDirRendered(source, target, config, toolKey) {
|
|
276
|
+
const profile = getToolProfile(toolKey);
|
|
277
|
+
|
|
334
278
|
if (!fs.existsSync(target)) {
|
|
335
279
|
fs.mkdirSync(target, { recursive: true });
|
|
336
280
|
}
|
|
@@ -344,7 +288,8 @@ function copyDirRendered(source, target, config, toolKey) {
|
|
|
344
288
|
if (stat.isDirectory()) {
|
|
345
289
|
copyDirRendered(srcPath, tgtPath, config, toolKey);
|
|
346
290
|
} else {
|
|
347
|
-
const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey);
|
|
291
|
+
const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey, profile);
|
|
292
|
+
validateRenderedContent(rendered, profile, tgtPath);
|
|
348
293
|
fs.writeFileSync(tgtPath, rendered, 'utf8');
|
|
349
294
|
}
|
|
350
295
|
}
|
|
@@ -363,7 +308,9 @@ const NATIVE_OPSX_CMDS_SLASH = ['propose.md', 'apply.md', 'archive.md', 'explore
|
|
|
363
308
|
* SDD 自定义 command 标记(init 不部署;用户手动维护时保留)
|
|
364
309
|
*/
|
|
365
310
|
function isSddCustomCommand(content) {
|
|
366
|
-
return content.includes(
|
|
311
|
+
return content.includes(BRIDGE_MARKER)
|
|
312
|
+
|| content.includes('skywalk-sdd/log.cjs')
|
|
313
|
+
|| /# SDD\b/.test(content);
|
|
367
314
|
}
|
|
368
315
|
|
|
369
316
|
/**
|
|
@@ -376,7 +323,8 @@ function isSddCustomCommand(content) {
|
|
|
376
323
|
* openspec 对不同工具生成的目录结构:
|
|
377
324
|
* - Cursor: commands/opsx-*.md(根目录,连字符)
|
|
378
325
|
* - Claude/CodeBuddy/Qoder 等: commands/opsx/*.md(opsx 子目录)
|
|
379
|
-
* - OpenCode:
|
|
326
|
+
* - OpenSpec OpenCode: commands/opsx-*.md(复数扁平,通用 commands/ 循环清理)
|
|
327
|
+
* - Legacy OpenCode: command/(单数目录,nativeCommandDir 整目录删除)
|
|
380
328
|
*
|
|
381
329
|
* @param {string[]} selectedTools - 用户选择的编辑器列表
|
|
382
330
|
*/
|
|
@@ -524,16 +472,17 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
524
472
|
|
|
525
473
|
for (const toolKey of selectedTools) {
|
|
526
474
|
const config = TOOL_CONFIGS[toolKey];
|
|
527
|
-
|
|
475
|
+
const profile = getToolProfile(toolKey);
|
|
476
|
+
if (!config || !profile.skillsDir) continue;
|
|
528
477
|
|
|
529
|
-
const targetSkillsDir = path.join(cwd,
|
|
478
|
+
const targetSkillsDir = path.join(cwd, profile.skillsDir);
|
|
530
479
|
|
|
531
480
|
// 确保 skills 目录存在
|
|
532
481
|
if (!fs.existsSync(targetSkillsDir)) {
|
|
533
482
|
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
534
483
|
}
|
|
535
484
|
|
|
536
|
-
console.log(` 部署 ${config.name} 的 opsx skills(${skillDirs.length} 个,扁平到 ${
|
|
485
|
+
console.log(` 部署 ${config.name} 的 opsx skills(${skillDirs.length} 个,扁平到 ${profile.skillsDir}/)...`);
|
|
537
486
|
|
|
538
487
|
for (const skillDir of skillDirs) {
|
|
539
488
|
const sourceDir = path.join(skillsTemplatePath, skillDir);
|
|
@@ -554,6 +503,46 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
554
503
|
return true;
|
|
555
504
|
}
|
|
556
505
|
|
|
506
|
+
/**
|
|
507
|
+
* 按 profile 部署 Agent 专属产物(Kunlun bridge / CodeBuddy hooks / Claude hooks)
|
|
508
|
+
* 仅处理 selectedTools 中的 profile,不触碰未选 Agent 目录。
|
|
509
|
+
*/
|
|
510
|
+
function deployProfileArtifacts(selectedTools = Object.keys(TOOL_CONFIGS), cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
511
|
+
console.log('🧩 正在部署 Agent 专属产物...');
|
|
512
|
+
|
|
513
|
+
for (const toolKey of selectedTools) {
|
|
514
|
+
const profile = getToolProfile(toolKey);
|
|
515
|
+
const config = TOOL_CONFIGS[toolKey];
|
|
516
|
+
if (!config) continue;
|
|
517
|
+
|
|
518
|
+
if (profile.requiresCommandBridge) {
|
|
519
|
+
cleanupManagedKunlunBridges(profile, cwd);
|
|
520
|
+
const bridgeResult = deployKunlunCommandBridges({
|
|
521
|
+
profile,
|
|
522
|
+
projectRoot: cwd,
|
|
523
|
+
pkgPath,
|
|
524
|
+
skillMetadata: OPSX_SKILL_METADATA,
|
|
525
|
+
});
|
|
526
|
+
console.log(` ✓ ${config.name}: 部署 ${bridgeResult.deployed.length} 个 Kunlun command bridge`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (profile.hookProvider === 'codebuddy') {
|
|
530
|
+
const hookResult = deployCodebuddyHookPack(profile, cwd, pkgPath);
|
|
531
|
+
if (!hookResult.ok) {
|
|
532
|
+
throw new Error(hookResult.error || `CodeBuddy Hook 部署失败: ${profile.configDir}/settings.json`);
|
|
533
|
+
}
|
|
534
|
+
console.log(` ✓ ${config.name}: 部署 Hook Pack(运行 /hooks 审核后生效)`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (profile.hookProvider === 'claude') {
|
|
538
|
+
deployClaudeHookPack([toolKey]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log('✅ Agent 专属产物部署完成');
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
|
|
557
546
|
/**
|
|
558
547
|
* 部署 Claude Code Hook Pack(可选增强)
|
|
559
548
|
* Hook 只调用 skywalk-sdd/log.cjs,不写私有日志,不替代 OPSX 模板采集。
|
|
@@ -780,6 +769,30 @@ function initGlobalOverview() {
|
|
|
780
769
|
return true;
|
|
781
770
|
}
|
|
782
771
|
|
|
772
|
+
/**
|
|
773
|
+
* 部署 SDD 操作手册(HTML)到 openspec 目录
|
|
774
|
+
*/
|
|
775
|
+
function deploySddGuideManual(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
776
|
+
console.log('📖 正在部署 SDD 操作手册...');
|
|
777
|
+
|
|
778
|
+
const source = path.join(pkgPath, 'kld-sdd-guide.html');
|
|
779
|
+
const targetDir = path.join(cwd, 'openspec');
|
|
780
|
+
const targetFile = path.join(targetDir, 'kld-sdd操作手册.html');
|
|
781
|
+
|
|
782
|
+
if (!fs.existsSync(source)) {
|
|
783
|
+
console.log('⚠️ 操作手册源文件不存在,跳过: kld-sdd-guide.html');
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!fs.existsSync(targetDir)) {
|
|
788
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
fs.copyFileSync(source, targetFile);
|
|
792
|
+
console.log(`✅ SDD 操作手册已部署: openspec/kld-sdd操作手册.html`);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
|
|
783
796
|
/**
|
|
784
797
|
* 部署 SDD Telemetry 数据目录
|
|
785
798
|
* 创建本地数据目录用于存储事件和活跃状态(最终报告落 openspec/changes/<change>/reports/,不再写 skywalk-sdd/reports/)
|
|
@@ -1036,7 +1049,7 @@ async function main() {
|
|
|
1036
1049
|
} catch (error) {
|
|
1037
1050
|
console.error(`❌ ${error.message}`);
|
|
1038
1051
|
rl.close();
|
|
1039
|
-
|
|
1052
|
+
process.exit(1);
|
|
1040
1053
|
}
|
|
1041
1054
|
|
|
1042
1055
|
// 1. 运行 openspec init(传入用户选择的工具)
|
|
@@ -1057,8 +1070,8 @@ async function main() {
|
|
|
1057
1070
|
// 3.2 清理原生 openspec-* skills(防止残留)
|
|
1058
1071
|
cleanupNativeOpenspecSkills(selectedTools);
|
|
1059
1072
|
|
|
1060
|
-
// 3.3 部署
|
|
1061
|
-
|
|
1073
|
+
// 3.3 部署 Agent 专属产物(Kunlun bridge / CodeBuddy hooks / Claude hooks)
|
|
1074
|
+
deployProfileArtifacts(selectedTools, process.cwd(), getPackagePath());
|
|
1062
1075
|
|
|
1063
1076
|
// 5. 部署 SDD Telemetry 数据目录
|
|
1064
1077
|
deployTelemetryDataDir();
|
|
@@ -1071,6 +1084,9 @@ async function main() {
|
|
|
1071
1084
|
|
|
1072
1085
|
// 7. 初始化全局架构约束 (overview.md)
|
|
1073
1086
|
initGlobalOverview();
|
|
1087
|
+
|
|
1088
|
+
// 7.1 部署 SDD 操作手册(HTML)
|
|
1089
|
+
deploySddGuideManual();
|
|
1074
1090
|
}
|
|
1075
1091
|
|
|
1076
1092
|
// 7. 更新 .gitignore
|
|
@@ -1087,8 +1103,16 @@ async function main() {
|
|
|
1087
1103
|
console.log();
|
|
1088
1104
|
console.log('已生成/覆盖:');
|
|
1089
1105
|
console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
|
|
1106
|
+
console.log(' 📖 openspec/kld-sdd操作手册.html # SDD 操作手册(HTML)');
|
|
1090
1107
|
console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
|
|
1091
1108
|
console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
|
|
1109
|
+
if (selectedTools.includes('kunlunzhima')) {
|
|
1110
|
+
console.log(' 📎 .kunlunzhima/commands/opsx/ # KunlunZhima OPSX command bridge(11 个)');
|
|
1111
|
+
console.log(' ℹ️ KunlunZhima 通过 commands/skills 入口使用 SDD,未启用自动 Hook');
|
|
1112
|
+
}
|
|
1113
|
+
if (selectedTools.includes('codebuddy')) {
|
|
1114
|
+
console.log(' 🪝 .codebuddy/hooks/ # CodeBuddy SDD Hook Pack(运行 /hooks 审核后生效)');
|
|
1115
|
+
}
|
|
1092
1116
|
if (selectedTools.includes('claude')) {
|
|
1093
1117
|
console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
|
|
1094
1118
|
}
|
|
@@ -1098,7 +1122,7 @@ async function main() {
|
|
|
1098
1122
|
console.log();
|
|
1099
1123
|
|
|
1100
1124
|
// 统一的 skill 格式说明
|
|
1101
|
-
console.log('可用 skills(opsx-* 系列,
|
|
1125
|
+
console.log('可用 skills(opsx-* 系列,11 个 SDD skills):');
|
|
1102
1126
|
console.log(' opsx-propose - 创建业务意图文档(Why)');
|
|
1103
1127
|
console.log(' opsx-spec - 创建技术契约文档(What)');
|
|
1104
1128
|
console.log(' opsx-design - 创建技术实现方案(How)');
|
|
@@ -1109,6 +1133,7 @@ async function main() {
|
|
|
1109
1133
|
console.log(' opsx-archive - 归档变更(Archive)');
|
|
1110
1134
|
console.log(' opsx-explore - 浏览变更状态(Explore)');
|
|
1111
1135
|
console.log(' opsx-knowledge - 业务知识库检索(辅助)');
|
|
1136
|
+
console.log(' opsx-rules - 生成/更新 Agent 规则(辅助)');
|
|
1112
1137
|
|
|
1113
1138
|
console.log();
|
|
1114
1139
|
console.log('后续步骤:');
|
|
@@ -1116,6 +1141,15 @@ async function main() {
|
|
|
1116
1141
|
console.log(' 2. 按顺序执行 propose → spec → design → task');
|
|
1117
1142
|
console.log(' 3. 激活 opsx-check 验证文档质量');
|
|
1118
1143
|
console.log(' 4. 激活 opsx-apply 申请实施,opsx-archive 归档完成');
|
|
1144
|
+
if (selectedTools.includes('codebuddy')) {
|
|
1145
|
+
console.log(' 5. CodeBuddy 用户:重启会话后运行 /hooks 审核 SDD Hook,审批后门禁生效');
|
|
1146
|
+
}
|
|
1147
|
+
if (selectedTools.includes('kunlunzhima')) {
|
|
1148
|
+
console.log(' 5. KunlunZhima 用户:在 Craft 模式使用 /opsx:propose 等 OPSX 命令(.kunlunzhima/commands/opsx/)');
|
|
1149
|
+
}
|
|
1150
|
+
if (selectedTools.includes('opencode')) {
|
|
1151
|
+
console.log(' 5. OpenCode 用户:用 /opsx-propose 或 /skills 启动 SDD(.opencode/skills/opsx-*/,skill 即斜杠命令)');
|
|
1152
|
+
}
|
|
1119
1153
|
console.log();
|
|
1120
1154
|
console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
|
|
1121
1155
|
console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
|
|
@@ -1125,4 +1159,16 @@ async function main() {
|
|
|
1125
1159
|
rl.close();
|
|
1126
1160
|
}
|
|
1127
1161
|
|
|
1128
|
-
module.exports = {
|
|
1162
|
+
module.exports = {
|
|
1163
|
+
main,
|
|
1164
|
+
detectTools,
|
|
1165
|
+
getTemplatePath,
|
|
1166
|
+
getPackagePath,
|
|
1167
|
+
selectEditor,
|
|
1168
|
+
deployOpsxSkills,
|
|
1169
|
+
deployProfileArtifacts,
|
|
1170
|
+
deploySddGuideManual,
|
|
1171
|
+
copyDirRendered,
|
|
1172
|
+
parseSelectedTools,
|
|
1173
|
+
TOOL_CONFIGS,
|
|
1174
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative merge for CodeBuddy .codebuddy/settings.json hooks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const SDD_HOOK_COMMAND_RE = /(?:^|\s|["'])\$?CODEBUDDY_PROJECT_DIR[\\/]+\.codebuddy[\\/]+hooks[\\/]+sdd-[\w-]+\.cjs(?=$|\s|["'])/i;
|
|
9
|
+
|
|
10
|
+
function hookCommandText(hook) {
|
|
11
|
+
const args = Array.isArray(hook.args) ? hook.args : [];
|
|
12
|
+
return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isSddCodebuddyHook(hook) {
|
|
16
|
+
return SDD_HOOK_COMMAND_RE.test(hookCommandText(hook));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sameHookCommand(left, right) {
|
|
20
|
+
return hookCommandText(left) === hookCommandText(right) &&
|
|
21
|
+
String(left.type || '') === String(right.type || '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removeManagedSddHooks(entries) {
|
|
25
|
+
return entries
|
|
26
|
+
.map(entry => {
|
|
27
|
+
if (!Array.isArray(entry.hooks)) {
|
|
28
|
+
return entry;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...entry,
|
|
32
|
+
hooks: entry.hooks.filter(hook => !isSddCodebuddyHook(hook)),
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mergeCodebuddySettings(settingsPath, templateSettings) {
|
|
39
|
+
const displayPath = path.basename(settingsPath);
|
|
40
|
+
let originalBytes = '';
|
|
41
|
+
if (fs.existsSync(settingsPath)) {
|
|
42
|
+
originalBytes = fs.readFileSync(settingsPath, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let existingSettings = {};
|
|
46
|
+
if (originalBytes) {
|
|
47
|
+
try {
|
|
48
|
+
existingSettings = JSON.parse(originalBytes);
|
|
49
|
+
} catch {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${settingsPath})`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const merged = { ...existingSettings };
|
|
58
|
+
merged.hooks = merged.hooks || {};
|
|
59
|
+
|
|
60
|
+
for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
|
|
61
|
+
merged.hooks[eventName] = removeManagedSddHooks(merged.hooks[eventName] || []);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const templateHooks = entry.hooks || [];
|
|
64
|
+
const exists = merged.hooks[eventName].some(existingEntry => {
|
|
65
|
+
const existingHooks = existingEntry.hooks || [];
|
|
66
|
+
return templateHooks.every(templateHook =>
|
|
67
|
+
existingHooks.some(existingHook => sameHookCommand(existingHook, templateHook))
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
if (!exists) {
|
|
71
|
+
merged.hooks[eventName].push(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
mergeCodebuddySettings,
|
|
82
|
+
isSddCodebuddyHook,
|
|
83
|
+
hookCommandText,
|
|
84
|
+
removeManagedSddHooks,
|
|
85
|
+
};
|
package/lib/skills-bundle.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* SDD skills bundle 共享配置与模板渲染
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
5
8
|
const SKILLS_BUNDLE_NAME = 'kld-sdd';
|
|
6
9
|
|
|
7
10
|
const OPSX_SKILL_DIRS = [
|
|
@@ -14,17 +17,151 @@ const OPSX_SKILL_DIRS = [
|
|
|
14
17
|
'opsx-test',
|
|
15
18
|
'opsx-archive',
|
|
16
19
|
'opsx-explore',
|
|
17
|
-
'opsx-knowledge'
|
|
20
|
+
'opsx-knowledge',
|
|
21
|
+
'opsx-rules',
|
|
18
22
|
];
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
|
|
26
|
+
function commandNameFromSkillDir(skillDir) {
|
|
27
|
+
return skillDir.replace(/^opsx-/, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function displayNameFromCommandName(commandName) {
|
|
31
|
+
return `OPSX: ${commandName.charAt(0).toUpperCase() + commandName.slice(1)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSkillFrontmatter(content) {
|
|
35
|
+
const stripped = content.replace(/^\uFEFF/, '');
|
|
36
|
+
if (!stripped.startsWith('---')) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const end = stripped.indexOf('---', 3);
|
|
41
|
+
if (end < 0) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const block = stripped.slice(3, end).trim();
|
|
46
|
+
const result = {};
|
|
47
|
+
|
|
48
|
+
for (const rawLine of block.split('\n')) {
|
|
49
|
+
const line = rawLine.replace(/\r$/, '');
|
|
50
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
51
|
+
if (!match) continue;
|
|
52
|
+
const key = match[1];
|
|
53
|
+
let value = match[2].trim();
|
|
54
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
55
|
+
value = value.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
result[key] = value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildOpsxSkillMetadata(pkgPath = PKG_ROOT) {
|
|
64
|
+
const skillsTemplateDir = path.join(pkgPath, 'templates', 'skills', SKILLS_BUNDLE_NAME);
|
|
65
|
+
|
|
66
|
+
return OPSX_SKILL_DIRS.map((skillDir) => {
|
|
67
|
+
const commandName = commandNameFromSkillDir(skillDir);
|
|
68
|
+
const skillMdPath = path.join(skillsTemplateDir, skillDir, 'SKILL.md');
|
|
69
|
+
let description = '';
|
|
70
|
+
let argumentHint = '';
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(skillMdPath)) {
|
|
73
|
+
const frontmatter = parseSkillFrontmatter(fs.readFileSync(skillMdPath, 'utf8'));
|
|
74
|
+
description = frontmatter.description || '';
|
|
75
|
+
argumentHint = frontmatter['argument-hint'] || '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
skillDir,
|
|
80
|
+
commandName,
|
|
81
|
+
displayName: displayNameFromCommandName(commandName),
|
|
82
|
+
description,
|
|
83
|
+
argumentHint,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const OPSX_SKILL_METADATA = buildOpsxSkillMetadata(PKG_ROOT);
|
|
89
|
+
|
|
90
|
+
const KUNLUN_SLASH_COMMANDS = [
|
|
91
|
+
'propose',
|
|
92
|
+
'spec',
|
|
93
|
+
'design',
|
|
94
|
+
'task',
|
|
95
|
+
'check',
|
|
96
|
+
'apply',
|
|
97
|
+
'test',
|
|
98
|
+
'archive',
|
|
99
|
+
'explore',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function transformKunlunSlashCommands(content) {
|
|
103
|
+
if (typeof content !== 'string') {
|
|
104
|
+
return content;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let result = content;
|
|
108
|
+
for (const cmd of KUNLUN_SLASH_COMMANDS) {
|
|
109
|
+
result = result.replace(new RegExp(`/opsx-${cmd}(?![a-z-])`, 'g'), `/opsx:${cmd}`);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function replaceProfileTokens(content, profile) {
|
|
115
|
+
if (!profile) {
|
|
116
|
+
return content;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const vars = profile.templateVariables || {};
|
|
21
120
|
return content
|
|
22
|
-
.replace(
|
|
23
|
-
.replace(
|
|
121
|
+
.replace(/\$\{SKILL_RUNTIME_DIR\}/g, vars.SKILL_RUNTIME_DIR || profile.skillsDir || '')
|
|
122
|
+
.replace(/\$\{HOOK_GATE_DESCRIPTION\}/g, vars.HOOK_GATE_DESCRIPTION || '')
|
|
123
|
+
.replace(/\$\{SHELL_GUIDANCE\}/g, vars.SHELL_GUIDANCE || '')
|
|
124
|
+
.replace(/\$\{AGENT_SKILL_DIR\}/g, profile.skillsDir || '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderTemplate(content, config, toolKey, profile) {
|
|
128
|
+
let rendered = replaceProfileTokens(
|
|
129
|
+
content
|
|
130
|
+
.replace(/^\uFEFF/, '')
|
|
131
|
+
.replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey),
|
|
132
|
+
profile
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (toolKey === 'kunlunzhima') {
|
|
136
|
+
rendered = transformKunlunSlashCommands(rendered);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return rendered;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateRenderedContent(content, profile, context = '') {
|
|
143
|
+
const unresolved = (content.match(/\$\{[A-Z_]+\}/g) || [])
|
|
144
|
+
// Claude Code 运行时注入,部署时保留字面量
|
|
145
|
+
.filter((token) => token !== '${CLAUDE_SKILL_DIR}');
|
|
146
|
+
if (unresolved.length > 0) {
|
|
147
|
+
const unique = [...new Set(unresolved)];
|
|
148
|
+
const prefix = context ? `${context}: ` : '';
|
|
149
|
+
throw new Error(`${prefix}未解析的模板 token: ${unique.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
24
153
|
}
|
|
25
154
|
|
|
26
155
|
module.exports = {
|
|
27
156
|
SKILLS_BUNDLE_NAME,
|
|
28
157
|
OPSX_SKILL_DIRS,
|
|
29
|
-
|
|
158
|
+
OPSX_SKILL_METADATA,
|
|
159
|
+
buildOpsxSkillMetadata,
|
|
160
|
+
commandNameFromSkillDir,
|
|
161
|
+
displayNameFromCommandName,
|
|
162
|
+
parseSkillFrontmatter,
|
|
163
|
+
renderTemplate,
|
|
164
|
+
replaceProfileTokens,
|
|
165
|
+
transformKunlunSlashCommands,
|
|
166
|
+
validateRenderedContent,
|
|
30
167
|
};
|