@hunterzheng/kld-sdd 2.4.19

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 (52) hide show
  1. package/README.md +275 -0
  2. package/bin/kld-sdd-init.js +24 -0
  3. package/index.js +13 -0
  4. package/lib/init.js +1124 -0
  5. package/lib/skills-bundle.js +30 -0
  6. package/package.json +48 -0
  7. package/skywalk-sdd/apply-worktree-finish.cjs +551 -0
  8. package/skywalk-sdd/index.cjs +2991 -0
  9. package/templates/ci/github-actions-sdd.yml +67 -0
  10. package/templates/ci/gitlab-ci-sdd.yml +44 -0
  11. package/templates/git-hooks/pre-commit-sdd-check.cjs +155 -0
  12. package/templates/git-hooks/pre-push-sdd-check.cjs +56 -0
  13. package/templates/hooks/claude/hooks/sdd-apply-gate.cjs +173 -0
  14. package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +315 -0
  15. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +146 -0
  16. package/templates/hooks/claude/hooks/sdd-pre-tool.cjs +41 -0
  17. package/templates/hooks/claude/hooks/sdd-prompt.cjs +88 -0
  18. package/templates/hooks/claude/hooks/sdd-skill-apply-gate.cjs +268 -0
  19. package/templates/hooks/claude/hooks/sdd-stop.cjs +108 -0
  20. package/templates/hooks/claude/settings.json +72 -0
  21. package/templates/openspec/design.md +290 -0
  22. package/templates/openspec/overview.md +143 -0
  23. package/templates/openspec/proposal.md +108 -0
  24. package/templates/openspec/spec.md +185 -0
  25. package/templates/openspec/tasks.md +287 -0
  26. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +251 -0
  27. package/templates/skills/kld-sdd/opsx-apply/checklist.md +94 -0
  28. package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +129 -0
  29. package/templates/skills/kld-sdd/opsx-apply/reference.md +335 -0
  30. package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +104 -0
  31. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +162 -0
  32. package/templates/skills/kld-sdd/opsx-archive/checklist.md +33 -0
  33. package/templates/skills/kld-sdd/opsx-check/SKILL.md +197 -0
  34. package/templates/skills/kld-sdd/opsx-check/checklist.md +35 -0
  35. package/templates/skills/kld-sdd/opsx-design/SKILL.md +166 -0
  36. package/templates/skills/kld-sdd/opsx-design/checklist.md +46 -0
  37. package/templates/skills/kld-sdd/opsx-design/reference.md +44 -0
  38. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +104 -0
  39. package/templates/skills/kld-sdd/opsx-knowledge/SKILL.md +130 -0
  40. package/templates/skills/kld-sdd/opsx-knowledge/references/modules.md +26 -0
  41. package/templates/skills/kld-sdd/opsx-knowledge/scripts/config.json +39 -0
  42. package/templates/skills/kld-sdd/opsx-knowledge/scripts/retrieve.cjs +199 -0
  43. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +201 -0
  44. package/templates/skills/kld-sdd/opsx-propose/checklist.md +44 -0
  45. package/templates/skills/kld-sdd/opsx-propose/reference.md +94 -0
  46. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +168 -0
  47. package/templates/skills/kld-sdd/opsx-spec/checklist.md +46 -0
  48. package/templates/skills/kld-sdd/opsx-spec/reference.md +49 -0
  49. package/templates/skills/kld-sdd/opsx-task/SKILL.md +199 -0
  50. package/templates/skills/kld-sdd/opsx-task/checklist.md +46 -0
  51. package/templates/skills/kld-sdd/opsx-task/reference.md +40 -0
  52. package/templates/skills/kld-sdd/opsx-test/SKILL.md +143 -0
package/lib/init.js ADDED
@@ -0,0 +1,1124 @@
1
+ /**
2
+ * KLD SDD 初始化核心逻辑
3
+ *
4
+ * 功能:
5
+ * 1. 检测并自动安装 openspec
6
+ * 2. 执行 openspec init 生成基础结构
7
+ * 3. 部署 opsx skills
8
+ * 4. 复制标准文档模版到项目
9
+ * 5. 更新 .gitignore
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+ const readline = require('readline');
16
+ const os = require('os');
17
+ const {
18
+ SKILLS_BUNDLE_NAME,
19
+ OPSX_SKILL_DIRS,
20
+ renderTemplate
21
+ } = require('./skills-bundle');
22
+
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ });
27
+
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
+ };
97
+
98
+ /**
99
+ * 询问用户选择
100
+ */
101
+ function ask(question) {
102
+ return new Promise(resolve => {
103
+ rl.question(question, answer => resolve(answer.trim()));
104
+ });
105
+ }
106
+
107
+ /**
108
+ * 从命令行参数读取指定编辑器
109
+ * 支持:--tool codex、--tool=codex、--tool cursor,codex、--tool all
110
+ */
111
+ function parseSelectedTools(args) {
112
+ const toolArgIndex = args.findIndex(arg => arg === '--tool' || arg.startsWith('--tool='));
113
+ if (toolArgIndex === -1) return null;
114
+
115
+ const rawValue = args[toolArgIndex] === '--tool'
116
+ ? args[toolArgIndex + 1]
117
+ : args[toolArgIndex].slice('--tool='.length);
118
+
119
+ if (!rawValue) {
120
+ throw new Error('缺少 --tool 参数值,请指定编辑器名称');
121
+ }
122
+
123
+ const normalized = rawValue.trim().toLowerCase();
124
+ if (normalized === 'all' || normalized === '全部') {
125
+ return Object.keys(TOOL_CONFIGS);
126
+ }
127
+
128
+ const selected = normalized.split(',').map(item => item.trim()).filter(Boolean);
129
+ const invalid = selected.filter(tool => !TOOL_CONFIGS[tool]);
130
+ if (invalid.length > 0) {
131
+ throw new Error(`不支持的编辑器: ${invalid.join(', ')}。可用值: ${Object.keys(TOOL_CONFIGS).join(', ')}, all`);
132
+ }
133
+
134
+ return selected;
135
+ }
136
+
137
+ /**
138
+ * 让用户选择编辑器
139
+ * 无效输入时重新询问,不允许默认
140
+ */
141
+ async function selectEditor() {
142
+ const mapping = {
143
+ '1': ['cursor'],
144
+ '2': ['claude'],
145
+ '3': ['codebuddy'],
146
+ '4': ['qoder'],
147
+ '5': ['opencode'],
148
+ '6': ['kunlunzhima'],
149
+ '7': ['workbuddy'],
150
+ '8': ['codex'],
151
+ '9': ['cursor', 'claude', 'codebuddy', 'qoder', 'opencode', 'kunlunzhima', 'workbuddy', 'codex']
152
+ };
153
+
154
+ while (true) {
155
+ console.log('\n请选择您使用的 AI 编辑器:');
156
+ console.log(' 1. Cursor');
157
+ console.log(' 2. Claude Code');
158
+ console.log(' 3. CodeBuddy');
159
+ console.log(' 4. Qoder');
160
+ console.log(' 5. OpenCode');
161
+ console.log(' 6. KunlunZhima');
162
+ console.log(' 7. WorkBuddy');
163
+ console.log(' 8. Codex');
164
+ console.log(' 9. 全部(为所有编辑器生成 skills)');
165
+
166
+ const answer = await ask('请输入选项 (1-9): ');
167
+ const selected = mapping[answer];
168
+
169
+ if (!selected) {
170
+ console.log(`❌ 无效选项 "${answer}",请输入 1-9 之间的数字\n`);
171
+ continue; // 重新循环询问
172
+ }
173
+
174
+ const names = selected.map(k => TOOL_CONFIGS[k].name).join(', ');
175
+ console.log(`✓ 已选择: ${names}\n`);
176
+ return selected;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 检查命令是否可用
182
+ */
183
+ function commandExists(command) {
184
+ try {
185
+ if (os.platform() === 'win32') {
186
+ execSync(`where ${command}`, { stdio: 'ignore' });
187
+ } else {
188
+ execSync(`which ${command}`, { stdio: 'ignore' });
189
+ }
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 获取包安装路径
198
+ */
199
+ function getPackagePath() {
200
+ // 尝试多种方式找到包的路径
201
+ try {
202
+ // 方式1:通过 require.resolve 找到包位置
203
+ const mainPath = require.resolve('../package.json');
204
+ return path.dirname(mainPath);
205
+ } catch {
206
+ // 方式2:使用当前文件位置
207
+ return path.resolve(__dirname, '..');
208
+ }
209
+ }
210
+
211
+ /**
212
+ * 获取模版路径
213
+ */
214
+ function getTemplatePath() {
215
+ const pkgPath = getPackagePath();
216
+ return path.join(pkgPath, 'templates', 'openspec');
217
+ }
218
+
219
+ /**
220
+ * 自动安装 openspec
221
+ */
222
+ async function installOpenspec() {
223
+ console.log('📦 openspec 未安装,正在自动安装...');
224
+
225
+ const npmCmd = os.platform() === 'win32' ? 'npm.cmd' : 'npm';
226
+ const openspecPkg = '@fission-ai/openspec@latest';
227
+
228
+ try {
229
+ // 尝试全局安装
230
+ console.log(` 正在执行: npm install -g ${openspecPkg}`);
231
+ execSync(`${npmCmd} install -g ${openspecPkg}`, {
232
+ stdio: 'inherit',
233
+ timeout: 180000
234
+ });
235
+ console.log('✅ openspec 安装完成');
236
+ return true;
237
+ } catch (error) {
238
+ console.log('❌ 全局安装失败,尝试本地安装...');
239
+
240
+ try {
241
+ // 本地安装到当前项目
242
+ const cwd = process.cwd();
243
+ execSync(`${npmCmd} install ${openspecPkg}`, {
244
+ cwd,
245
+ stdio: 'inherit',
246
+ timeout: 180000
247
+ });
248
+ console.log('✅ openspec 本地安装完成');
249
+ return true;
250
+ } catch (localError) {
251
+ console.log(`❌ 安装失败,请手动安装: npm install -g ${openspecPkg}`);
252
+ return false;
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 执行 openspec init
259
+ * @param {string[]} selectedTools - 用户选择的编辑器列表
260
+ */
261
+ async function runOpenspecInit(selectedTools = []) {
262
+ console.log('🚀 正在运行 openspec init...');
263
+
264
+ // 检查 openspec 是否安装
265
+ if (!commandExists('openspec')) {
266
+ const shouldInstall = await ask('openspec 未安装,是否自动安装? (y/n): ');
267
+ if (shouldInstall === 'y' || shouldInstall === 'Y') {
268
+ const installed = await installOpenspec();
269
+ if (!installed) {
270
+ console.log('⚠️ 跳过 openspec 初始化,继续应用模版...');
271
+ return false;
272
+ }
273
+ } else {
274
+ console.log('⚠️ 跳过 openspec 安装,继续应用模版...');
275
+ return false;
276
+ }
277
+ }
278
+
279
+ try {
280
+ // openspec CLI 支持的工具列表(kunlunzhima 等自定义工具不在其中)
281
+ const openspecSupportedTools = ['cursor', 'claude', 'codebuddy', 'qoder', 'opencode'];
282
+
283
+ // 过滤出 openspec 支持的工具
284
+ const supportedTools = selectedTools.filter(t => openspecSupportedTools.includes(t));
285
+ const unsupportedTools = selectedTools.filter(t => !openspecSupportedTools.includes(t));
286
+
287
+ if (unsupportedTools.length > 0) {
288
+ const names = unsupportedTools.map(k => TOOL_CONFIGS[k]?.name || k).join(', ');
289
+ console.log(` ℹ️ ${names} 不在 openspec 支持列表中,将单独创建目录结构`);
290
+ }
291
+
292
+ // 构建工具参数:只传递 openspec 支持的工具
293
+ // 当没有支持的工具时,使用 --tools none 创建基础目录结构
294
+ const toolsArg = supportedTools.length > 0
295
+ ? `--tools ${supportedTools.join(',')}`
296
+ : '--tools none';
297
+
298
+ // 执行 openspec init,传入支持的工具(或 none)
299
+ const cmd = `openspec init ${toolsArg} --force`.trim();
300
+ console.log(` 执行: ${cmd}`);
301
+ execSync(cmd, { stdio: 'inherit' });
302
+
303
+ console.log('✅ openspec 初始化完成');
304
+ return true;
305
+ } catch (error) {
306
+ console.log('⚠️ openspec init 执行失败,继续应用模版...');
307
+ return false;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * 递归复制目录
313
+ */
314
+ function copyDir(source, target) {
315
+ if (!fs.existsSync(target)) {
316
+ fs.mkdirSync(target, { recursive: true });
317
+ }
318
+
319
+ const files = fs.readdirSync(source);
320
+ for (const file of files) {
321
+ const srcPath = path.join(source, file);
322
+ const tgtPath = path.join(target, file);
323
+
324
+ const stat = fs.statSync(srcPath);
325
+ if (stat.isDirectory()) {
326
+ copyDir(srcPath, tgtPath);
327
+ } else {
328
+ fs.copyFileSync(srcPath, tgtPath);
329
+ }
330
+ }
331
+ }
332
+
333
+ function copyDirRendered(source, target, config, toolKey) {
334
+ if (!fs.existsSync(target)) {
335
+ fs.mkdirSync(target, { recursive: true });
336
+ }
337
+
338
+ const files = fs.readdirSync(source);
339
+ for (const file of files) {
340
+ const srcPath = path.join(source, file);
341
+ const tgtPath = path.join(target, file);
342
+
343
+ const stat = fs.statSync(srcPath);
344
+ if (stat.isDirectory()) {
345
+ copyDirRendered(srcPath, tgtPath, config, toolKey);
346
+ } else {
347
+ const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey);
348
+ fs.writeFileSync(tgtPath, rendered, 'utf8');
349
+ }
350
+ }
351
+ }
352
+
353
+ /** OpenSpec init 生成的 4 个官方 slash command(KLD-SDD 使用 skills,不保留 command) */
354
+ const NATIVE_OPSX_CMDS_HYPHEN = [
355
+ 'opsx-propose.md',
356
+ 'opsx-apply.md',
357
+ 'opsx-archive.md',
358
+ 'opsx-explore.md'
359
+ ];
360
+ const NATIVE_OPSX_CMDS_SLASH = ['propose.md', 'apply.md', 'archive.md', 'explore.md'];
361
+
362
+ /**
363
+ * SDD 自定义 command 标记(init 不部署;用户手动维护时保留)
364
+ */
365
+ function isSddCustomCommand(content) {
366
+ return content.includes('# SDD') || content.includes('skywalk-sdd/log.cjs');
367
+ }
368
+
369
+ /**
370
+ * 清理原生 openspec 命令,所有编辑器与 Cursor 行为一致:初始化时删除官方 command。
371
+ * 同时清理 openspec 遗留的 JSON 文件。
372
+ *
373
+ * 覆盖 TOOL_CONFIGS 中的全部编辑器(cursor/claude/codebuddy/qoder/opencode/kunlunzhima/workbuddy/codex),
374
+ * 与 Cursor 行为一致:初始化时删除 OpenSpec 官方 command,仅保留 skills。
375
+ *
376
+ * openspec 对不同工具生成的目录结构:
377
+ * - Cursor: commands/opsx-*.md(根目录,连字符)
378
+ * - Claude/CodeBuddy/Qoder 等: commands/opsx/*.md(opsx 子目录)
379
+ * - OpenCode: command/opsx-*.md(command 单数目录,整目录删除)
380
+ *
381
+ * @param {string[]} selectedTools - 用户选择的编辑器列表
382
+ */
383
+ function cleanupNativeOpenspecCommands(selectedTools = Object.keys(TOOL_CONFIGS)) {
384
+ console.log('🗑 正在清理原生 openspec 命令(所有编辑器与 Cursor 一致,仅保留 skills)...');
385
+
386
+ const cwd = process.cwd();
387
+
388
+ for (const toolKey of selectedTools) {
389
+ const config = TOOL_CONFIGS[toolKey];
390
+ if (!config) continue;
391
+
392
+ const configDir = path.join(cwd, config.configDir);
393
+ if (!fs.existsSync(configDir)) continue;
394
+
395
+ // 处理 commands/ 目录(复数,标准目录)
396
+ const commandsDir = path.join(configDir, 'commands');
397
+ if (fs.existsSync(commandsDir)) {
398
+ // 删除 JSON 文件(openspec 安装的遗留)
399
+ const allFiles = fs.readdirSync(commandsDir);
400
+ let deletedJson = 0;
401
+ for (const file of allFiles) {
402
+ const filePath = path.join(commandsDir, file);
403
+ if (file.endsWith('.json') && fs.statSync(filePath).isFile()) {
404
+ fs.unlinkSync(filePath);
405
+ deletedJson++;
406
+ }
407
+ }
408
+ if (deletedJson > 0) {
409
+ console.log(` 🗑 ${config.name}: 删除 ${deletedJson} 个 JSON 命令文件`);
410
+ }
411
+
412
+ // 删除 commands/ 根目录下的原生 opsx-* 命令(Cursor 等,与 Claude 行为一致)
413
+ let deletedCmds = 0;
414
+ for (const file of fs.readdirSync(commandsDir)) {
415
+ const isNativeHyphen = NATIVE_OPSX_CMDS_HYPHEN.includes(file) || /^opsx-.*\.md$/i.test(file);
416
+ if (!isNativeHyphen) continue;
417
+ const filePath = path.join(commandsDir, file);
418
+ if (!fs.statSync(filePath).isFile()) continue;
419
+ const content = fs.readFileSync(filePath, 'utf-8');
420
+ if (isSddCustomCommand(content)) continue;
421
+ fs.unlinkSync(filePath);
422
+ deletedCmds++;
423
+ }
424
+ if (deletedCmds > 0) {
425
+ console.log(` ✓ ${config.name}: 删除 ${deletedCmds} 个原生 opsx-* 命令(commands/)`);
426
+ }
427
+
428
+ // 删除 commands/opsx/ 子目录下的官方命令(Claude/CodeBuddy/Qoder 等,与 Cursor 一致无条件删除)
429
+ const opsxDir = path.join(commandsDir, 'opsx');
430
+ if (fs.existsSync(opsxDir)) {
431
+ let deletedOpsxCmds = 0;
432
+ for (const file of NATIVE_OPSX_CMDS_SLASH) {
433
+ const filePath = path.join(opsxDir, file);
434
+ if (!fs.existsSync(filePath)) continue;
435
+ const content = fs.readFileSync(filePath, 'utf-8');
436
+ if (isSddCustomCommand(content)) continue;
437
+ fs.unlinkSync(filePath);
438
+ deletedOpsxCmds++;
439
+ }
440
+ if (deletedOpsxCmds > 0) {
441
+ console.log(` ✓ ${config.name}: 删除 ${deletedOpsxCmds} 个原生命令(commands/opsx/)`);
442
+ }
443
+ // 子目录为空则移除
444
+ if (fs.readdirSync(opsxDir).length === 0) {
445
+ fs.rmdirSync(opsxDir);
446
+ }
447
+ }
448
+ }
449
+
450
+ // 处理 command/ 目录(单数,opencode 特有)
451
+ if (config.nativeCommandDir) {
452
+ const nativeDir = path.join(cwd, config.nativeCommandDir);
453
+ if (fs.existsSync(nativeDir)) {
454
+ // 删除整个 command/ 目录(全是原生命令)
455
+ fs.rmSync(nativeDir, { recursive: true, force: true });
456
+ console.log(` 🗑 ${config.name}: 删除原生 command/ 目录`);
457
+ }
458
+ }
459
+ }
460
+
461
+ console.log('✅ 原生 openspec 命令清理完成');
462
+ return true;
463
+ }
464
+
465
+ /**
466
+ * 清理旧版嵌套 bundle(skills/kld-sdd/opsx-*)
467
+ * Claude Code 只识别 skills/<skill-name>/SKILL.md 一层,嵌套 bundle 无法被 / 菜单发现
468
+ * @param {string[]} selectedTools - 用户选择的编辑器列表
469
+ */
470
+ function cleanupLegacyBundledOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
471
+ console.log('🧹 正在清理旧版嵌套 skills/kld-sdd/ bundle...');
472
+
473
+ const cwd = process.cwd();
474
+
475
+ for (const toolKey of selectedTools) {
476
+ const config = TOOL_CONFIGS[toolKey];
477
+ if (!config || !config.skillsDir) continue;
478
+
479
+ const bundledDir = path.join(cwd, config.skillsDir, SKILLS_BUNDLE_NAME);
480
+ if (fs.existsSync(bundledDir)) {
481
+ fs.rmSync(bundledDir, { recursive: true, force: true });
482
+ console.log(` 🗑 ${config.name}: 删除嵌套 bundle ${SKILLS_BUNDLE_NAME}/`);
483
+ }
484
+
485
+ const legacySdd = path.join(cwd, config.skillsDir, 'opsx-sdd');
486
+ if (fs.existsSync(legacySdd)) {
487
+ fs.rmSync(legacySdd, { recursive: true, force: true });
488
+ console.log(` 🗑 ${config.name}: 删除旧版 opsx-sdd/`);
489
+ }
490
+ }
491
+
492
+ console.log('✅ 旧版嵌套 bundle 清理完成');
493
+ return true;
494
+ }
495
+
496
+ /**
497
+ * 部署 SDD opsx skills
498
+ * 模板源:templates/skills/kld-sdd/(源码组织)
499
+ * 部署目标:.claude/skills/opsx-<name>/SKILL.md(扁平一层,Claude Code 可识别)
500
+ * @param {string[]} selectedTools - 用户选择的编辑器列表
501
+ */
502
+ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
503
+ console.log('🎯 正在部署 opsx skills...');
504
+
505
+ const pkgPath = getPackagePath();
506
+ const cwd = process.cwd();
507
+
508
+ const skillsTemplatePath = path.join(pkgPath, 'templates', 'skills', SKILLS_BUNDLE_NAME);
509
+
510
+ if (!fs.existsSync(skillsTemplatePath)) {
511
+ console.log(`⚠️ SDD skills 模板目录不存在: ${skillsTemplatePath}`);
512
+ return false;
513
+ }
514
+
515
+ // 获取 bundle 内所有 skill 目录
516
+ const skillDirs = fs.readdirSync(skillsTemplatePath).filter(d => {
517
+ return fs.statSync(path.join(skillsTemplatePath, d)).isDirectory();
518
+ });
519
+
520
+ if (skillDirs.length === 0) {
521
+ console.log('⚠️ 没有找到任何 skill 模板');
522
+ return false;
523
+ }
524
+
525
+ for (const toolKey of selectedTools) {
526
+ const config = TOOL_CONFIGS[toolKey];
527
+ if (!config || !config.skillsDir) continue;
528
+
529
+ const targetSkillsDir = path.join(cwd, config.skillsDir);
530
+
531
+ // 确保 skills 目录存在
532
+ if (!fs.existsSync(targetSkillsDir)) {
533
+ fs.mkdirSync(targetSkillsDir, { recursive: true });
534
+ }
535
+
536
+ console.log(` 部署 ${config.name} 的 opsx skills(${skillDirs.length} 个,扁平到 ${config.skillsDir}/)...`);
537
+
538
+ for (const skillDir of skillDirs) {
539
+ const sourceDir = path.join(skillsTemplatePath, skillDir);
540
+ const targetDir = path.join(targetSkillsDir, skillDir);
541
+
542
+ if (!fs.existsSync(targetDir)) {
543
+ fs.mkdirSync(targetDir, { recursive: true });
544
+ }
545
+
546
+ if (fs.existsSync(sourceDir)) {
547
+ copyDirRendered(sourceDir, targetDir, config, toolKey);
548
+ console.log(` ✓ ${skillDir}/`);
549
+ }
550
+ }
551
+ }
552
+
553
+ console.log('✅ opsx skills 部署完成');
554
+ return true;
555
+ }
556
+
557
+ /**
558
+ * 部署 Claude Code Hook Pack(可选增强)
559
+ * Hook 只调用 skywalk-sdd/log.cjs,不写私有日志,不替代 OPSX 模板采集。
560
+ */
561
+ function hookCommandText(hook) {
562
+ const args = Array.isArray(hook.args) ? hook.args : [];
563
+ return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
564
+ }
565
+
566
+ function isSddClaudeHook(hook) {
567
+ const text = hookCommandText(hook);
568
+ return /(?:^|\s|["'])\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate|apply-test-gate|skill-apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i.test(text) ||
569
+ /(?:^|\s|["'])\$\{CLAUDE_PROJECT_DIR\}[\\/]+\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate|apply-test-gate|skill-apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i.test(text);
570
+ }
571
+
572
+ function sameHookCommand(left, right) {
573
+ return hookCommandText(left) === hookCommandText(right) &&
574
+ String(left.type || '') === String(right.type || '');
575
+ }
576
+
577
+ function removeManagedSddHooks(entries) {
578
+ return entries
579
+ .map(entry => {
580
+ if (!Array.isArray(entry.hooks)) {
581
+ return entry;
582
+ }
583
+ return {
584
+ ...entry,
585
+ hooks: entry.hooks.filter(hook => !isSddClaudeHook(hook)),
586
+ };
587
+ })
588
+ .filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
589
+ }
590
+
591
+ function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
592
+ if (!selectedTools.includes('claude')) {
593
+ return true;
594
+ }
595
+
596
+ console.log('🪝 正在部署 Claude Code SDD Hook Pack...');
597
+
598
+ const pkgPath = getPackagePath();
599
+ const cwd = process.cwd();
600
+ const hookTemplateDir = path.join(pkgPath, 'templates', 'hooks', 'claude');
601
+ const hookSourceDir = path.join(hookTemplateDir, 'hooks');
602
+ const settingsTemplatePath = path.join(hookTemplateDir, 'settings.json');
603
+
604
+ if (!fs.existsSync(hookSourceDir) || !fs.existsSync(settingsTemplatePath)) {
605
+ console.log(` ⚠️ Claude Hook 模板缺失: ${hookTemplateDir}`);
606
+ return false;
607
+ }
608
+
609
+ const claudeDir = path.join(cwd, '.claude');
610
+ const targetHookDir = path.join(claudeDir, 'hooks');
611
+ if (!fs.existsSync(targetHookDir)) {
612
+ fs.mkdirSync(targetHookDir, { recursive: true });
613
+ }
614
+
615
+ copyDir(hookSourceDir, targetHookDir);
616
+ console.log(' ✓ 部署 .claude/hooks/sdd-*.cjs');
617
+
618
+ const targetSettingsPath = path.join(claudeDir, 'settings.json');
619
+ const templateSettings = JSON.parse(fs.readFileSync(settingsTemplatePath, 'utf8'));
620
+ let existingSettings = {};
621
+ if (fs.existsSync(targetSettingsPath)) {
622
+ try {
623
+ existingSettings = JSON.parse(fs.readFileSync(targetSettingsPath, 'utf8'));
624
+ } catch {
625
+ const backupPath = `${targetSettingsPath}.backup`;
626
+ fs.copyFileSync(targetSettingsPath, backupPath);
627
+ console.log(` ℹ️ settings.json 不是合法 JSON,已备份到 ${backupPath}`);
628
+ existingSettings = {};
629
+ }
630
+ }
631
+
632
+ existingSettings.hooks = existingSettings.hooks || {};
633
+ for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
634
+ existingSettings.hooks[eventName] = removeManagedSddHooks(existingSettings.hooks[eventName] || []);
635
+ for (const entry of entries) {
636
+ const templateHooks = entry.hooks || [];
637
+ const exists = existingSettings.hooks[eventName].some(existingEntry => {
638
+ const existingHooks = existingEntry.hooks || [];
639
+ return templateHooks.every(templateHook => {
640
+ return existingHooks.some(existingHook => sameHookCommand(existingHook, templateHook));
641
+ });
642
+ });
643
+ if (!exists) {
644
+ existingSettings.hooks[eventName].push(entry);
645
+ }
646
+ }
647
+ }
648
+
649
+ fs.writeFileSync(targetSettingsPath, JSON.stringify(existingSettings, null, 2) + '\n', 'utf8');
650
+ console.log(' ✓ 合并 .claude/settings.json hooks 配置');
651
+ console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX skills 完成)');
652
+ return true;
653
+ }
654
+
655
+ /**
656
+ * 清理原生 openspec-* skills(防止残留)
657
+ * 仅负责删除 openspec init 产生的原生 skills。
658
+ *
659
+ * @param {string[]} selectedTools - 用户选择的编辑器列表
660
+ */
661
+ function cleanupNativeOpenspecSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
662
+ console.log('🧹 正在清理原生 openspec-* skills...');
663
+
664
+ const cwd = process.cwd();
665
+
666
+ // 原生 openspec-* skills(需要删除)
667
+ const nativeSkillsToRemove = [
668
+ 'openspec-propose',
669
+ 'openspec-apply-change',
670
+ 'openspec-archive-change',
671
+ 'openspec-explore'
672
+ ];
673
+
674
+ for (const toolKey of selectedTools) {
675
+ const config = TOOL_CONFIGS[toolKey];
676
+ if (!config || !config.skillsDir) continue;
677
+
678
+ const skillsDir = path.join(cwd, config.skillsDir);
679
+ if (!fs.existsSync(skillsDir)) continue;
680
+
681
+ let removed = 0;
682
+ for (const nativeSkill of nativeSkillsToRemove) {
683
+ const nativeSkillDir = path.join(skillsDir, nativeSkill);
684
+ if (fs.existsSync(nativeSkillDir)) {
685
+ fs.rmSync(nativeSkillDir, { recursive: true, force: true });
686
+ removed++;
687
+ }
688
+ }
689
+ if (removed > 0) {
690
+ console.log(` 🗑 ${config.name}: 删除 ${removed} 个原生 openspec-* skills`);
691
+ }
692
+ }
693
+
694
+ console.log('✅ 原生 skills 清理完成');
695
+ return true;
696
+ }
697
+
698
+ /**
699
+ * 复制标准文档模版到项目
700
+ */
701
+ function copyTemplatesToProject() {
702
+ console.log('📄 正在复制标准文档模版到项目...');
703
+
704
+ const pkgPath = getPackagePath();
705
+ const templatePath = path.join(pkgPath, 'templates', 'openspec');
706
+ const cwd = process.cwd();
707
+ const targetDir = path.join(cwd, 'openspec-templates');
708
+
709
+ if (!fs.existsSync(templatePath)) {
710
+ console.log(`⚠️ 模版源目录不存在: ${templatePath}`);
711
+ return false;
712
+ }
713
+
714
+ // 创建目标目录
715
+ if (!fs.existsSync(targetDir)) {
716
+ fs.mkdirSync(targetDir, { recursive: true });
717
+ }
718
+
719
+ // 复制模版文件
720
+ copyDir(templatePath, targetDir);
721
+
722
+ console.log(`✅ 标准文档模版已复制到: ${targetDir}`);
723
+ console.log(' 包含文件:');
724
+ const files = fs.readdirSync(targetDir);
725
+ files.forEach(file => {
726
+ console.log(` - ${file}`);
727
+ });
728
+
729
+ return true;
730
+ }
731
+
732
+ /**
733
+ * 初始化全局 overview.md 到 openspec/specs/ 目录
734
+ * overview.md 是全局架构约束,所有 Capability 的设计都必须遵守
735
+ */
736
+ function initGlobalOverview() {
737
+ console.log('🌐 正在初始化全局架构约束 (overview.md)...');
738
+
739
+ const pkgPath = getPackagePath();
740
+ const overviewSource = path.join(pkgPath, 'templates', 'openspec', 'overview.md');
741
+ const cwd = process.cwd();
742
+ const targetDir = path.join(cwd, 'openspec', 'specs');
743
+ const targetFile = path.join(targetDir, 'overview.md');
744
+
745
+ // 检查源文件是否存在
746
+ if (!fs.existsSync(overviewSource)) {
747
+ console.log(`⚠️ overview.md 源文件不存在: ${overviewSource}`);
748
+ return false;
749
+ }
750
+
751
+ // 创建目标目录
752
+ if (!fs.existsSync(targetDir)) {
753
+ fs.mkdirSync(targetDir, { recursive: true });
754
+ }
755
+
756
+ // 检查目标文件是否已存在
757
+ if (fs.existsSync(targetFile)) {
758
+ // 读取现有文件,检查是否需要更新
759
+ const existingContent = fs.readFileSync(targetFile, 'utf-8');
760
+ const newContent = fs.readFileSync(overviewSource, 'utf-8');
761
+
762
+ if (existingContent === newContent) {
763
+ console.log(' ✓ overview.md 已是最新版本,跳过');
764
+ return true;
765
+ }
766
+
767
+ // 备份现有文件
768
+ const backupFile = path.join(targetDir, 'overview.md.backup');
769
+ fs.copyFileSync(targetFile, backupFile);
770
+ console.log(` ℹ️ 已备份现有 overview.md 到 ${backupFile}`);
771
+ }
772
+
773
+ // 复制 overview.md
774
+ fs.copyFileSync(overviewSource, targetFile);
775
+
776
+ console.log(`✅ 全局架构约束已初始化: ${targetFile}`);
777
+ console.log(' ℹ️ 此文件定义了全局数据字典、接口规范、共享实体等约束');
778
+ console.log(' ℹ️ 执行 opsx-design 或 opsx-task 时会自动读取此文件');
779
+
780
+ return true;
781
+ }
782
+
783
+ /**
784
+ * 部署 SDD Telemetry 数据目录
785
+ * 创建本地数据目录用于存储事件和活跃状态(最终报告落 openspec/changes/<change>/reports/,不再写 skywalk-sdd/reports/)
786
+ */
787
+ function deployTelemetryDataDir() {
788
+ console.log('📊 正在初始化 SDD Telemetry 数据目录...');
789
+
790
+ const pkgPath = getPackagePath();
791
+ const cwd = process.cwd();
792
+ const dataDir = path.join(cwd, 'skywalk-sdd');
793
+
794
+ if (!fs.existsSync(dataDir)) {
795
+ fs.mkdirSync(dataDir, { recursive: true });
796
+ }
797
+
798
+ // 创建本地数据目录(事件和活跃状态存储;报告落 change 子目录,不在此创建)
799
+ const eventsDir = path.join(dataDir, 'events');
800
+ const stateDir = path.join(dataDir, 'state');
801
+ if (!fs.existsSync(eventsDir)) {
802
+ fs.mkdirSync(eventsDir, { recursive: true });
803
+ console.log(' ✓ 创建 skywalk-sdd/events/ 数据目录');
804
+ }
805
+ if (!fs.existsSync(stateDir)) {
806
+ fs.mkdirSync(stateDir, { recursive: true });
807
+ console.log(' ✓ 创建 skywalk-sdd/state/ 活跃阶段状态目录');
808
+ }
809
+
810
+ // 部署项目内 Telemetry CLI(不依赖 npm 发布)
811
+ const telemetrySrc = path.join(pkgPath, 'skywalk-sdd', 'index.cjs');
812
+ const telemetryDst = path.join(dataDir, 'log.cjs');
813
+ if (fs.existsSync(telemetrySrc)) {
814
+ fs.copyFileSync(telemetrySrc, telemetryDst);
815
+ console.log(' ✓ 部署 skywalk-sdd/log.cjs(本地 Telemetry CLI)');
816
+ } else {
817
+ console.log(` ⚠️ Telemetry CLI 源文件缺失: ${telemetrySrc}`);
818
+ }
819
+
820
+ const worktreeFinishSrc = path.join(pkgPath, 'skywalk-sdd', 'apply-worktree-finish.cjs');
821
+ const worktreeFinishDst = path.join(dataDir, 'apply-worktree-finish.cjs');
822
+ if (fs.existsSync(worktreeFinishSrc)) {
823
+ fs.copyFileSync(worktreeFinishSrc, worktreeFinishDst);
824
+ console.log(' ✓ 部署 skywalk-sdd/apply-worktree-finish.cjs(Apply 收尾脚本)');
825
+ }
826
+
827
+ console.log(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics');
828
+ console.log(' ✓ Apply worktree: record-base 在 §1.5;收尾 apply-worktree-finish.cjs --change=<name>');
829
+ console.log('✅ Telemetry 已就绪(数据存储在 skywalk-sdd/,无需配置 MCP)');
830
+ return true;
831
+ }
832
+
833
+ function installGitHookShim(cwd, hookName, scriptPath) {
834
+ const hooksDir = path.join(cwd, '.git', 'hooks');
835
+ if (!fs.existsSync(hooksDir)) {
836
+ return;
837
+ }
838
+
839
+ const hookPath = path.join(hooksDir, hookName);
840
+ const marker = 'KLD SDD quality gate';
841
+ const shim = [
842
+ '#!/bin/sh',
843
+ `# ${marker}`,
844
+ `if [ -f "${scriptPath}" ]; then`,
845
+ ` node "${scriptPath}" "$@"`,
846
+ 'fi',
847
+ '',
848
+ ].join('\n');
849
+
850
+ if (fs.existsSync(hookPath)) {
851
+ const existing = fs.readFileSync(hookPath, 'utf8');
852
+ if (existing.includes(marker)) {
853
+ console.log(` ✓ .git/hooks/${hookName} 已包含 SDD 质量门禁`);
854
+ } else {
855
+ console.log(` ℹ️ .git/hooks/${hookName} 已存在,已保留不覆盖;可手动调用 ${scriptPath}`);
856
+ }
857
+ return;
858
+ }
859
+
860
+ fs.writeFileSync(hookPath, shim, 'utf8');
861
+ try {
862
+ fs.chmodSync(hookPath, 0o755);
863
+ } catch {
864
+ // Windows 上 chmod 可能没有实际效果,不影响 Git Bash 执行文本 hook。
865
+ }
866
+ console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁`);
867
+ }
868
+
869
+ /**
870
+ * 部署 Git hooks / CI 兜底模板
871
+ * 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
872
+ */
873
+ function deployQualityGateTemplates() {
874
+ console.log('🧰 正在部署 SDD Git hooks / CI 兜底模板...');
875
+
876
+ const pkgPath = getPackagePath();
877
+ const cwd = process.cwd();
878
+ const dataDir = path.join(cwd, 'skywalk-sdd');
879
+ const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
880
+ const ciTemplateDir = path.join(pkgPath, 'templates', 'ci');
881
+ const targetGitHooksDir = path.join(dataDir, 'git-hooks');
882
+ const targetCiDir = path.join(dataDir, 'ci');
883
+
884
+ if (fs.existsSync(gitHooksTemplateDir)) {
885
+ copyDir(gitHooksTemplateDir, targetGitHooksDir);
886
+ console.log(' ✓ 部署 skywalk-sdd/git-hooks/');
887
+ } else {
888
+ console.log(` ⚠️ Git hooks 模板缺失: ${gitHooksTemplateDir}`);
889
+ }
890
+
891
+ if (fs.existsSync(ciTemplateDir)) {
892
+ copyDir(ciTemplateDir, targetCiDir);
893
+ console.log(' ✓ 部署 skywalk-sdd/ci/');
894
+ } else {
895
+ console.log(` ⚠️ CI 模板缺失: ${ciTemplateDir}`);
896
+ }
897
+
898
+ installGitHookShim(cwd, 'pre-commit', 'skywalk-sdd/git-hooks/pre-commit-sdd-check.cjs');
899
+ installGitHookShim(cwd, 'pre-push', 'skywalk-sdd/git-hooks/pre-push-sdd-check.cjs');
900
+
901
+ console.log('✅ Git hooks / CI 兜底模板已就绪');
902
+ return true;
903
+ }
904
+
905
+ /**
906
+ * 检测当前项目已初始化的 AI 编辑器
907
+ */
908
+ function detectTools() {
909
+ const detected = [];
910
+ const cwd = process.cwd();
911
+ for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
912
+ if (fs.existsSync(path.join(cwd, config.configDir))) {
913
+ detected.push(key);
914
+ }
915
+ }
916
+ return detected;
917
+ }
918
+
919
+ /**
920
+ * 创建 / 更新 .gitignore
921
+ */
922
+ function updateGitignore() {
923
+ const cwd = process.cwd();
924
+ const gitignorePath = path.join(cwd, '.gitignore');
925
+ const requiredLines = [
926
+ '.cursor/commands/personal-*',
927
+ '.claude/commands/personal-*',
928
+ '.codebuddy/commands/personal-*',
929
+ '.qoder/commands/personal-*',
930
+ '.kunlunzhima/commands/personal-*',
931
+ '.workbuddy/commands/personal-*',
932
+ '.agents/commands/personal-*',
933
+ 'skywalk-sdd/events/',
934
+ 'skywalk-sdd/state/',
935
+ '.worktrees/'
936
+ ];
937
+
938
+ const sddConfig = `
939
+ # KLD SDD AI 编辑器个人配置(请勿提交)
940
+ .cursor/commands/personal-*
941
+ .claude/commands/personal-*
942
+ .codebuddy/commands/personal-*
943
+ .qoder/commands/personal-*
944
+ .kunlunzhima/commands/personal-*
945
+ .workbuddy/commands/personal-*
946
+ .agents/commands/personal-*
947
+
948
+ # SDD Telemetry 数据(本地度量,可选提交;最终报告落 openspec/changes/<change>/reports/ 随 change 提交)
949
+ skywalk-sdd/events/
950
+ skywalk-sdd/state/
951
+
952
+ # SDD Apply 隔离 worktree(本地临时目录)
953
+ .worktrees/
954
+ `;
955
+
956
+ if (fs.existsSync(gitignorePath)) {
957
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
958
+ if (!content.includes('KLD SDD')) {
959
+ fs.appendFileSync(gitignorePath, sddConfig);
960
+ console.log('✅ 已更新 .gitignore');
961
+ } else {
962
+ const missingLines = requiredLines.filter(line => !content.includes(line));
963
+ if (missingLines.length > 0) {
964
+ fs.appendFileSync(gitignorePath, `\n# KLD SDD 补充忽略规则\n${missingLines.join('\n')}\n`);
965
+ console.log('✅ 已补充 .gitignore');
966
+ }
967
+ }
968
+ } else {
969
+ fs.writeFileSync(gitignorePath, sddConfig.trim());
970
+ console.log('✅ 已创建 .gitignore');
971
+ }
972
+ }
973
+
974
+ /**
975
+ * 显示帮助信息
976
+ */
977
+ function showHelp() {
978
+ console.log(`
979
+ KLD SDD 项目初始化工具
980
+
981
+ 用法:
982
+ kld-sdd-init [选项]
983
+ npx kld-sdd [选项]
984
+
985
+ 选项:
986
+ -h, --help 显示帮助信息
987
+ -v, --version 显示版本号
988
+ --skip-openspec 跳过 openspec init 步骤
989
+ --skip-template 跳过复制内置模版
990
+ --tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
991
+
992
+ 示例:
993
+ kld-sdd-init # 完整初始化流程
994
+ kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
995
+ kld-sdd-init --tool codex # 仅部署 Codex 配置
996
+ `);
997
+ }
998
+
999
+ /**
1000
+ * 主函数
1001
+ */
1002
+ async function main() {
1003
+ const args = process.argv.slice(2);
1004
+
1005
+ // 解析参数
1006
+ if (args.includes('-h') || args.includes('--help')) {
1007
+ showHelp();
1008
+ rl.close();
1009
+ return;
1010
+ }
1011
+
1012
+ if (args.includes('-v') || args.includes('--version')) {
1013
+ const pkg = require('../package.json');
1014
+ console.log(pkg.version);
1015
+ rl.close();
1016
+ return;
1017
+ }
1018
+
1019
+ const skipOpenspec = args.includes('--skip-openspec');
1020
+ const skipTemplate = args.includes('--skip-template');
1021
+
1022
+ console.log('╔════════════════════════════════════════╗');
1023
+ console.log('║ KLD SDD 项目初始化工具 ║');
1024
+ console.log('║ 内置 openSpec 标准模版 ║');
1025
+ console.log('╚════════════════════════════════════════╝');
1026
+ console.log();
1027
+
1028
+ // 0. 选择编辑器
1029
+ let selectedTools;
1030
+ try {
1031
+ selectedTools = parseSelectedTools(args) || await selectEditor();
1032
+ } catch (error) {
1033
+ console.error(`❌ ${error.message}`);
1034
+ rl.close();
1035
+ return;
1036
+ }
1037
+
1038
+ // 1. 运行 openspec init(传入用户选择的工具)
1039
+ if (!skipOpenspec) {
1040
+ await runOpenspecInit(selectedTools);
1041
+ }
1042
+
1043
+ if (!skipTemplate) {
1044
+ // 2. 清理原生 openspec 命令(删除 opsx-*.md,避免命名混淆)
1045
+ cleanupNativeOpenspecCommands(selectedTools);
1046
+
1047
+ // 3. 清理旧版嵌套 skills/kld-sdd/ bundle(Claude Code 无法识别二层目录)
1048
+ cleanupLegacyBundledOpsxSkills(selectedTools);
1049
+
1050
+ // 3.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
1051
+ deployOpsxSkills(selectedTools);
1052
+
1053
+ // 3.2 清理原生 openspec-* skills(防止残留)
1054
+ cleanupNativeOpenspecSkills(selectedTools);
1055
+
1056
+ // 3.3 部署 Claude Code Hook Pack(可选增强)
1057
+ deployClaudeHookPack(selectedTools);
1058
+
1059
+ // 5. 部署 SDD Telemetry 数据目录
1060
+ deployTelemetryDataDir();
1061
+
1062
+ // 5.1 部署 Git hooks / CI 兜底模板
1063
+ deployQualityGateTemplates();
1064
+
1065
+ // 6. 复制标准文档模版到项目(供参考)
1066
+ copyTemplatesToProject();
1067
+
1068
+ // 7. 初始化全局架构约束 (overview.md)
1069
+ initGlobalOverview();
1070
+ }
1071
+
1072
+ // 7. 更新 .gitignore
1073
+ updateGitignore();
1074
+
1075
+ const selectedNames = selectedTools.map(t => TOOL_CONFIGS[t].name).join(', ');
1076
+
1077
+ console.log();
1078
+ console.log('╔════════════════════════════════════════╗');
1079
+ console.log('║ ✅ 初始化完成! ║');
1080
+ console.log('╚════════════════════════════════════════╝');
1081
+ console.log();
1082
+ console.log(`已为以下编辑器生成配置: ${selectedNames}`);
1083
+ console.log();
1084
+ console.log('已生成/覆盖:');
1085
+ console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
1086
+ console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
1087
+ console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
1088
+ if (selectedTools.includes('claude')) {
1089
+ console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1090
+ }
1091
+ console.log(' 📊 skywalk-sdd/ # SDD Telemetry 数据目录');
1092
+ console.log(' 🧰 skywalk-sdd/git-hooks/ # Git hooks 质量门禁模板');
1093
+ console.log(' 🧪 skywalk-sdd/ci/ # CI 兜底采集模板');
1094
+ console.log();
1095
+
1096
+ // 统一的 skill 格式说明
1097
+ console.log('可用 skills(opsx-* 系列,10 个 SDD skills):');
1098
+ console.log(' opsx-propose - 创建业务意图文档(Why)');
1099
+ console.log(' opsx-spec - 创建技术契约文档(What)');
1100
+ console.log(' opsx-design - 创建技术实现方案(How)');
1101
+ console.log(' opsx-task - 拆解AI编码任务(Do)');
1102
+ console.log(' opsx-check - 质量门禁检查(Verify)');
1103
+ console.log(' opsx-apply - 申请变更实施(Apply)');
1104
+ console.log(' opsx-test - 执行单元测试(Test)');
1105
+ console.log(' opsx-archive - 归档变更(Archive)');
1106
+ console.log(' opsx-explore - 浏览变更状态(Explore)');
1107
+ console.log(' opsx-knowledge - 业务知识库检索(辅助)');
1108
+
1109
+ console.log();
1110
+ console.log('后续步骤:');
1111
+ console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
1112
+ console.log(' 2. 按顺序执行 propose → spec → design → task');
1113
+ console.log(' 3. 激活 opsx-check 验证文档质量');
1114
+ console.log(' 4. 激活 opsx-apply 申请实施,opsx-archive 归档完成');
1115
+ console.log();
1116
+ console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1117
+ console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1118
+ console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=. 查看四维度指标');
1119
+ console.log();
1120
+
1121
+ rl.close();
1122
+ }
1123
+
1124
+ module.exports = { main, detectTools, getTemplatePath, selectEditor };