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.
Files changed (29) hide show
  1. package/kld-sdd-guide.html +1109 -0
  2. package/lib/command-bridge.js +156 -0
  3. package/lib/deploy-codebuddy-hooks.js +99 -0
  4. package/lib/hook-gate-core.js +333 -0
  5. package/lib/init.js +127 -81
  6. package/lib/settings-merge.js +85 -0
  7. package/lib/skills-bundle.js +142 -5
  8. package/lib/tool-profiles.js +270 -0
  9. package/package.json +3 -2
  10. package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
  11. package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
  12. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
  13. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
  14. package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
  15. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
  16. package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
  17. package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
  18. package/templates/hooks/codebuddy/settings.json +72 -0
  19. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +5 -5
  20. package/templates/skills/kld-sdd/opsx-check/SKILL.md +1 -1
  21. package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
  22. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
  23. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
  24. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
  25. package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
  26. package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
  27. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
  28. package/templates/skills/kld-sdd/opsx-task/SKILL.md +1 -1
  29. 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
- renderTemplate
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('# SDD') || content.includes('skywalk-sdd/log.cjs');
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: command/opsx-*.md(command 单数目录,整目录删除)
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
- if (!config || !config.skillsDir) continue;
475
+ const profile = getToolProfile(toolKey);
476
+ if (!config || !profile.skillsDir) continue;
528
477
 
529
- const targetSkillsDir = path.join(cwd, config.skillsDir);
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} 个,扁平到 ${config.skillsDir}/)...`);
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
- return;
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 部署 Claude Code Hook Pack(可选增强)
1061
- deployClaudeHookPack(selectedTools);
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-* 系列,10 个 SDD skills):');
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 = { main, detectTools, getTemplatePath, selectEditor };
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
+ };
@@ -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
- function renderTemplate(content, config, toolKey) {
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(/^\uFEFF/, '')
23
- .replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey);
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
- renderTemplate
158
+ OPSX_SKILL_METADATA,
159
+ buildOpsxSkillMetadata,
160
+ commandNameFromSkillDir,
161
+ displayNameFromCommandName,
162
+ parseSkillFrontmatter,
163
+ renderTemplate,
164
+ replaceProfileTokens,
165
+ transformKunlunSlashCommands,
166
+ validateRenderedContent,
30
167
  };