jdd-sprint-kit 0.3.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +253 -0
  3. package/bin/cli.js +48 -0
  4. package/compat/baseline.json +6 -0
  5. package/package.json +40 -0
  6. package/src/commands/compat-check.js +178 -0
  7. package/src/commands/init.js +267 -0
  8. package/src/commands/update.js +132 -0
  9. package/src/lib/adapters/codex.js +68 -0
  10. package/src/lib/copy.js +131 -0
  11. package/src/lib/detect.js +81 -0
  12. package/src/lib/fingerprint.js +152 -0
  13. package/src/lib/manifest.js +51 -0
  14. package/src/lib/merge.js +109 -0
  15. package/src/lib/prompts.js +196 -0
  16. package/templates/.claude/agents/auto-sprint.md +882 -0
  17. package/templates/.claude/agents/brownfield-scanner.md +259 -0
  18. package/templates/.claude/agents/deliverable-generator.md +429 -0
  19. package/templates/.claude/agents/judge-business.md +91 -0
  20. package/templates/.claude/agents/judge-quality.md +82 -0
  21. package/templates/.claude/agents/judge-security.md +80 -0
  22. package/templates/.claude/agents/scope-gate.md +219 -0
  23. package/templates/.claude/agents/worker.md +82 -0
  24. package/templates/.claude/commands/circuit-breaker.md +106 -0
  25. package/templates/.claude/commands/parallel.md +110 -0
  26. package/templates/.claude/commands/preview.md +85 -0
  27. package/templates/.claude/commands/specs.md +206 -0
  28. package/templates/.claude/commands/sprint.md +552 -0
  29. package/templates/.claude/commands/summarize-prd.md +290 -0
  30. package/templates/.claude/commands/validate.md +143 -0
  31. package/templates/.claude/hooks/desktop-notify.sh +9 -0
  32. package/templates/.claude/hooks/protect-readonly-paths.sh +42 -0
  33. package/templates/.claude/hooks/sprint-pre-compact.sh +42 -0
  34. package/templates/.claude/hooks/sprint-session-recovery.sh +33 -0
  35. package/templates/.claude/rules/bmad-mcp-search.md +97 -0
  36. package/templates/.claude/rules/bmad-sprint-guide.md +176 -0
  37. package/templates/.claude/rules/bmad-sprint-protocol.md +178 -0
  38. package/templates/.claude/settings.json +50 -0
  39. package/templates/.mcp.json.example +16 -0
  40. package/templates/_bmad/docs/architecture-to-epics-checklist.md +59 -0
  41. package/templates/_bmad/docs/blueprint-format-guide.md +387 -0
  42. package/templates/_bmad/docs/brownfield-context-format.md +167 -0
  43. package/templates/_bmad/docs/prd-format-guide.md +538 -0
  44. package/templates/_bmad/docs/sprint-input-format.md +496 -0
  45. package/templates/preview-template/.redocly.yaml +10 -0
  46. package/templates/preview-template/api/.gitkeep +0 -0
  47. package/templates/preview-template/index.html +12 -0
  48. package/templates/preview-template/package-lock.json +5547 -0
  49. package/templates/preview-template/package.json +33 -0
  50. package/templates/preview-template/public/mockServiceWorker.js +307 -0
  51. package/templates/preview-template/src/App.tsx +9 -0
  52. package/templates/preview-template/src/api/client.ts +32 -0
  53. package/templates/preview-template/src/components/.gitkeep +0 -0
  54. package/templates/preview-template/src/components/DevPanel.tsx +76 -0
  55. package/templates/preview-template/src/main.tsx +26 -0
  56. package/templates/preview-template/src/mocks/browser.ts +4 -0
  57. package/templates/preview-template/src/mocks/handlers.ts +3 -0
  58. package/templates/preview-template/src/pages/.gitkeep +0 -0
  59. package/templates/preview-template/src/vite-env.d.ts +1 -0
  60. package/templates/preview-template/tsconfig.json +21 -0
  61. package/templates/preview-template/vite.config.ts +6 -0
@@ -0,0 +1,267 @@
1
+ import * as p from '@clack/prompts';
2
+ import { existsSync } from 'node:fs';
3
+ import fsExtra from 'fs-extra';
4
+ const { ensureDirSync } = fsExtra;
5
+ import { join, relative } from 'node:path';
6
+ import { detect, checkBmadCompat } from '../lib/detect.js';
7
+ import { SPRINT_KIT_VERSION, SPRINT_KIT_FILES, BMAD_COMPAT } from '../lib/manifest.js';
8
+ import { copySprintKitFiles } from '../lib/copy.js';
9
+ import { mergeSettings, getSprintKitSettings, describeHookChanges } from '../lib/merge.js';
10
+ import {
11
+ showIntro, showEnvStatus, promptIdeSelection,
12
+ showBmadRequired, showAlreadyInstalled, showOutro,
13
+ confirmOverwrite, confirmSettingsMerge,
14
+ showVerification,
15
+ } from '../lib/prompts.js';
16
+ import { convertToCodex } from '../lib/adapters/codex.js';
17
+
18
+ export async function runInit(options = {}) {
19
+ const { yes = false, ide = 'claude-code', dryRun = false } = options;
20
+ const projectDir = process.cwd();
21
+
22
+ // --- Step 1: Intro ---
23
+ if (!yes) {
24
+ showIntro();
25
+ }
26
+
27
+ // --- Step 2: Environment detection ---
28
+ const env = detect(projectDir);
29
+ const nodeVersionOk = env.nodeVersion && parseInt(env.nodeVersion.split('.')[0], 10) >= 18;
30
+ const bmadCompat = checkBmadCompat(env.bmadVersion);
31
+
32
+ const envInfo = {
33
+ ...env,
34
+ projectDir,
35
+ nodeVersionOk,
36
+ bmadCompat,
37
+ };
38
+
39
+ if (!yes) {
40
+ showEnvStatus(envInfo);
41
+ }
42
+
43
+ // Node version check
44
+ if (!nodeVersionOk) {
45
+ if (yes) {
46
+ console.error('Error: Node.js >= 18 is required.');
47
+ process.exit(1);
48
+ }
49
+ p.cancel('Node.js 18 이상이 필요합니다. Node.js를 업그레이드해주세요.');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Git warning (non-blocking)
54
+ if (!env.isGitRepo && !yes) {
55
+ p.log.warn('Git 레포지토리가 아닙니다. 계속 진행합니다.');
56
+ }
57
+
58
+ // --- Step 3/4: BMad check ---
59
+ if (!env.hasBmad) {
60
+ if (yes) {
61
+ console.error('Error: BMad Method is not installed. Run: npx bmad-method install');
62
+ process.exit(1);
63
+ }
64
+ showBmadRequired(null, 'unknown');
65
+ process.exit(1);
66
+ }
67
+
68
+ if (bmadCompat === 'below_minimum') {
69
+ if (yes) {
70
+ console.error(`Error: BMad Method ${env.bmadVersion} is below minimum ${BMAD_COMPAT.minimum}`);
71
+ process.exit(1);
72
+ }
73
+ showBmadRequired(env.bmadVersion, bmadCompat);
74
+ process.exit(1);
75
+ }
76
+
77
+ if (bmadCompat === 'above_verified' && !yes) {
78
+ p.log.warn(`BMad Method ${env.bmadVersion}은 검증된 버전(${BMAD_COMPAT.verified})보다 높습니다. 계속 진행합니다.`);
79
+ }
80
+
81
+ // Already installed notice
82
+ if (env.hasSprintKit && !yes) {
83
+ showAlreadyInstalled(env.sprintKitVersion);
84
+ }
85
+
86
+ // --- Step 3: IDE selection ---
87
+ let ideSelection = ide;
88
+ if (!yes) {
89
+ ideSelection = await promptIdeSelection();
90
+ }
91
+ const includeCodex = ideSelection.includes('codex');
92
+
93
+ // --- Step 5: Install Sprint Kit files ---
94
+ if (dryRun) {
95
+ const results = copySprintKitFiles(projectDir, { dryRun: true });
96
+ console.log('\n[Dry Run] Files that would be installed:');
97
+ for (const f of [...results.copied, ...results.overwritten]) {
98
+ const rel = relative(projectDir, f);
99
+ console.log(` ${rel}`);
100
+ }
101
+ if (includeCodex) {
102
+ console.log('\n[Dry Run] Codex files that would be generated:');
103
+ console.log(' AGENTS.md');
104
+ for (const cmd of SPRINT_KIT_FILES.commands) {
105
+ console.log(` .codex/skills/${cmd.replace('.md', '')}/SKILL.md`);
106
+ }
107
+ for (const rule of SPRINT_KIT_FILES.rules) {
108
+ console.log(` .codex/rules/${rule}`);
109
+ }
110
+ }
111
+ return;
112
+ }
113
+
114
+ // Check for files that will be overwritten
115
+ if (!yes) {
116
+ const willOverwrite = [];
117
+ const dirs = [
118
+ { files: SPRINT_KIT_FILES.agents, dir: join('.claude', 'agents') },
119
+ { files: SPRINT_KIT_FILES.commands, dir: join('.claude', 'commands') },
120
+ { files: SPRINT_KIT_FILES.rules, dir: join('.claude', 'rules') },
121
+ { files: SPRINT_KIT_FILES.hooks, dir: join('.claude', 'hooks') },
122
+ { files: SPRINT_KIT_FILES.docs, dir: join('_bmad', 'docs') },
123
+ ];
124
+ for (const { files, dir } of dirs) {
125
+ for (const file of files) {
126
+ const fullPath = join(projectDir, dir, file);
127
+ if (existsSync(fullPath)) {
128
+ willOverwrite.push(join(dir, file));
129
+ }
130
+ }
131
+ }
132
+ if (willOverwrite.length > 0) {
133
+ const proceed = await confirmOverwrite(willOverwrite);
134
+ if (!proceed) {
135
+ p.cancel('설치가 취소되었습니다.');
136
+ process.exit(0);
137
+ }
138
+ }
139
+ }
140
+
141
+ const s = p.spinner();
142
+ if (!yes) s.start('Sprint Kit 파일 설치 중...');
143
+
144
+ const copyResults = copySprintKitFiles(projectDir);
145
+
146
+ if (!yes) {
147
+ s.stop('Sprint Kit 파일 설치 완료');
148
+ showCopyResults(copyResults);
149
+ }
150
+
151
+ // Codex conversion
152
+ if (includeCodex) {
153
+ if (!yes) {
154
+ const cs = p.spinner();
155
+ cs.start('Codex 추가 설치 중...');
156
+ const templateDir = join(projectDir); // Use project files as source after copy
157
+ const codexResults = convertToCodex(projectDir, templateDir);
158
+ cs.stop('Codex 추가 설치 완료');
159
+ p.note(
160
+ codexResults.converted.map(f => ` ${f} ✓`).join('\n'),
161
+ 'Codex 파일'
162
+ );
163
+ } else {
164
+ convertToCodex(projectDir, projectDir);
165
+ }
166
+ }
167
+
168
+ // --- Step 6: settings.json hook setup ---
169
+ const settingsPath = join(projectDir, '.claude', 'settings.json');
170
+ const backupPath = join(projectDir, '.claude', 'settings.json.bak');
171
+
172
+ if (env.hasSettings && !yes) {
173
+ const proceed = await confirmSettingsMerge();
174
+ if (proceed) {
175
+ const result = mergeSettings(settingsPath, backupPath);
176
+ p.log.success(`settings.json ${result.action === 'merged' ? '머지' : '생성'} 완료`);
177
+ } else {
178
+ p.log.info('settings.json 훅 설정을 건너뛰었습니다. 나중에 설정하려면: npx jdd-sprint-kit init');
179
+ }
180
+ } else {
181
+ mergeSettings(settingsPath, backupPath);
182
+ if (!yes) {
183
+ p.log.success(env.hasSettings ? 'settings.json 머지 완료' : 'settings.json 생성 완료');
184
+ }
185
+ }
186
+
187
+ // --- Step 7: Verification + Outro ---
188
+ const checks = verify(projectDir);
189
+
190
+ if (!yes) {
191
+ showVerification(checks);
192
+ showOutro(ideSelection);
193
+ } else {
194
+ const failed = checks.filter(c => !c.ok);
195
+ if (failed.length > 0) {
196
+ console.error('Verification failed:');
197
+ for (const c of failed) {
198
+ console.error(` ✗ ${c.path} — ${c.label}`);
199
+ }
200
+ process.exit(1);
201
+ }
202
+ console.log(`Sprint Kit v${SPRINT_KIT_VERSION} installed successfully.`);
203
+ }
204
+ }
205
+
206
+ function showCopyResults(results) {
207
+ const lines = [];
208
+ const categoryCounts = {};
209
+
210
+ const categories = [
211
+ { key: 'agents', label: '에이전트', desc: 'Sprint의 각 단계를 자동 실행합니다', dir: '.claude/agents/' },
212
+ { key: 'commands', label: '커맨드', desc: '/sprint 등 슬래시 커맨드', dir: '.claude/commands/' },
213
+ { key: 'rules', label: '규칙', desc: 'Sprint 프로토콜과 검색 가이드', dir: '.claude/rules/' },
214
+ { key: 'hooks', label: '훅', desc: '세션 자동 저장/복구', dir: '.claude/hooks/' },
215
+ { key: 'docs', label: '문서', desc: 'Sprint Input/PRD 포맷 가이드', dir: '_bmad/docs/' },
216
+ ];
217
+
218
+ for (const cat of categories) {
219
+ const count = SPRINT_KIT_FILES[cat.key].length;
220
+ categoryCounts[cat.key] = count;
221
+ lines.push(`${cat.label} — ${cat.desc}:`);
222
+ lines.push(` ${cat.dir} ${count} files ✓`);
223
+ lines.push('');
224
+ }
225
+
226
+ lines.push('프로토타입 스캐폴드:');
227
+ lines.push(' preview-template/ copied ✓');
228
+ lines.push('');
229
+ lines.push('MCP 설정 템플릿:');
230
+ lines.push(' .mcp.json.example copied ✓');
231
+
232
+ p.note(lines.join('\n'), 'Sprint Kit 파일 설치 완료');
233
+ }
234
+
235
+ function verify(projectDir) {
236
+ return [
237
+ {
238
+ path: '_bmad/bmm/',
239
+ label: 'BMad core',
240
+ ok: existsSync(join(projectDir, '_bmad', 'bmm')),
241
+ },
242
+ {
243
+ path: `.claude/agents/ (${SPRINT_KIT_FILES.agents.length})`,
244
+ label: 'Sprint agents',
245
+ ok: SPRINT_KIT_FILES.agents.every(f =>
246
+ existsSync(join(projectDir, '.claude', 'agents', f))
247
+ ),
248
+ },
249
+ {
250
+ path: `.claude/commands/ (${SPRINT_KIT_FILES.commands.length})`,
251
+ label: 'Sprint commands',
252
+ ok: SPRINT_KIT_FILES.commands.every(f =>
253
+ existsSync(join(projectDir, '.claude', 'commands', f))
254
+ ),
255
+ },
256
+ {
257
+ path: '.claude/settings.json',
258
+ label: 'hooks registered',
259
+ ok: existsSync(join(projectDir, '.claude', 'settings.json')),
260
+ },
261
+ {
262
+ path: 'preview-template/',
263
+ label: 'prototype scaffold',
264
+ ok: existsSync(join(projectDir, 'preview-template')),
265
+ },
266
+ ];
267
+ }
@@ -0,0 +1,132 @@
1
+ import * as p from '@clack/prompts';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ import { detect, checkBmadCompat } from '../lib/detect.js';
5
+ import { SPRINT_KIT_VERSION, SPRINT_KIT_FILES } from '../lib/manifest.js';
6
+ import { updateSprintKitFiles } from '../lib/copy.js';
7
+ import { mergeSettings } from '../lib/merge.js';
8
+
9
+ export async function runUpdate(options = {}) {
10
+ const { yes = false, dryRun = false } = options;
11
+ const projectDir = process.cwd();
12
+
13
+ if (!yes) {
14
+ p.intro(`JDD Sprint Kit Update v${SPRINT_KIT_VERSION}`);
15
+ }
16
+
17
+ // Environment check
18
+ const env = detect(projectDir);
19
+
20
+ if (!env.hasSprintKit) {
21
+ if (yes) {
22
+ console.error('Error: Sprint Kit is not installed. Run: npx jdd-sprint-kit init');
23
+ process.exit(1);
24
+ }
25
+ p.cancel('Sprint Kit이 설치되어 있지 않습니다. 먼저 init을 실행해주세요: npx jdd-sprint-kit init');
26
+ process.exit(1);
27
+ }
28
+
29
+ if (!env.hasBmad) {
30
+ if (yes) {
31
+ console.error('Error: BMad Method is not installed.');
32
+ process.exit(1);
33
+ }
34
+ p.cancel('BMad Method가 설치되어 있지 않습니다.');
35
+ process.exit(1);
36
+ }
37
+
38
+ const bmadCompat = checkBmadCompat(env.bmadVersion);
39
+ if (bmadCompat === 'below_minimum') {
40
+ if (yes) {
41
+ console.error(`Error: BMad Method ${env.bmadVersion} is below minimum.`);
42
+ process.exit(1);
43
+ }
44
+ p.cancel(`BMad Method ${env.bmadVersion}은 최소 버전 미만입니다.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Version comparison
49
+ if (!yes) {
50
+ p.note(
51
+ [
52
+ `현재 설치: v${env.sprintKitVersion}`,
53
+ `업데이트: v${SPRINT_KIT_VERSION}`,
54
+ ].join('\n'),
55
+ '버전 정보'
56
+ );
57
+ }
58
+
59
+ if (dryRun) {
60
+ const results = updateSprintKitFiles(projectDir, { dryRun: true });
61
+ console.log('\n[Dry Run] Files that would be updated:');
62
+ for (const f of [...results.copied, ...results.overwritten]) {
63
+ const rel = relative(projectDir, f);
64
+ console.log(` ${rel}`);
65
+ }
66
+ return;
67
+ }
68
+
69
+ // Show overwrite policy summary
70
+ if (!yes) {
71
+ p.note(
72
+ [
73
+ '덮어쓰기 대상 (Sprint Kit 소유):',
74
+ ' .claude/agents/*.md',
75
+ ' .claude/commands/{sprint,...}.md',
76
+ ' .claude/rules/bmad-*.md',
77
+ ' .claude/hooks/*.sh',
78
+ ' _bmad/docs/*.md',
79
+ ' preview-template/',
80
+ ' .mcp.json.example',
81
+ '',
82
+ '머지 대상:',
83
+ ' .claude/settings.json (사용자 훅 보존)',
84
+ '',
85
+ '스킵 대상 (사용자/BMad 소유):',
86
+ ' .claude/commands/bmad/',
87
+ ' _bmad/bmm/, _bmad/core/',
88
+ ' .mcp.json, CLAUDE.md, specs/',
89
+ ].join('\n'),
90
+ '업데이트 정책'
91
+ );
92
+
93
+ const proceed = await p.confirm({
94
+ message: '업데이트를 진행하시겠습니까?',
95
+ });
96
+ if (p.isCancel(proceed) || !proceed) {
97
+ p.cancel('업데이트가 취소되었습니다.');
98
+ process.exit(0);
99
+ }
100
+ }
101
+
102
+ // Execute update
103
+ const s = yes ? null : p.spinner();
104
+ if (s) s.start('Sprint Kit 파일 업데이트 중...');
105
+
106
+ const results = updateSprintKitFiles(projectDir);
107
+
108
+ if (s) s.stop('Sprint Kit 파일 업데이트 완료');
109
+
110
+ // Merge settings.json
111
+ const settingsPath = join(projectDir, '.claude', 'settings.json');
112
+ const backupPath = join(projectDir, '.claude', 'settings.json.bak');
113
+ mergeSettings(settingsPath, backupPath);
114
+
115
+ // Summary
116
+ const newCount = results.copied.length;
117
+ const updatedCount = results.overwritten.length;
118
+
119
+ if (!yes) {
120
+ p.note(
121
+ [
122
+ `새로 추가: ${newCount} files`,
123
+ `업데이트: ${updatedCount} files`,
124
+ `settings.json: 머지 완료`,
125
+ ].join('\n'),
126
+ '업데이트 결과'
127
+ );
128
+ p.outro(`Sprint Kit v${SPRINT_KIT_VERSION} 업데이트 완료!`);
129
+ } else {
130
+ console.log(`Sprint Kit v${SPRINT_KIT_VERSION} updated. (${newCount} new, ${updatedCount} updated)`);
131
+ }
132
+ }
@@ -0,0 +1,68 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import fsExtra from 'fs-extra';
3
+ const { ensureDirSync } = fsExtra;
4
+ import { join, basename } from 'node:path';
5
+ import { SPRINT_KIT_FILES } from '../manifest.js';
6
+
7
+ export function convertToCodex(projectDir, templateDir) {
8
+ const results = { converted: [], skipped: [] };
9
+
10
+ convertAgentsToCodex(projectDir, templateDir, results);
11
+ convertCommandsToSkills(projectDir, templateDir, results);
12
+ convertRulesToCodex(projectDir, templateDir, results);
13
+ // Hooks are not supported in Codex — skip
14
+
15
+ return results;
16
+ }
17
+
18
+ function convertAgentsToCodex(projectDir, templateDir, results) {
19
+ // .claude/agents/*.md → AGENTS.md (concatenated)
20
+ const agentsDir = join(templateDir, '.claude', 'agents');
21
+ const lines = ['# Sprint Kit Agents\n'];
22
+
23
+ for (const file of SPRINT_KIT_FILES.agents) {
24
+ const src = join(agentsDir, file);
25
+ if (!existsSync(src)) continue;
26
+ const content = readFileSync(src, 'utf-8');
27
+ const name = basename(file, '.md');
28
+ lines.push(`## ${name}\n`);
29
+ lines.push(content);
30
+ lines.push('\n---\n');
31
+ }
32
+
33
+ const dest = join(projectDir, 'AGENTS.md');
34
+ writeFileSync(dest, lines.join('\n'));
35
+ results.converted.push('AGENTS.md');
36
+ }
37
+
38
+ function convertCommandsToSkills(projectDir, templateDir, results) {
39
+ // .claude/commands/*.md → .codex/skills/{name}/SKILL.md
40
+ const skillsDir = join(projectDir, '.codex', 'skills');
41
+ ensureDirSync(skillsDir);
42
+
43
+ for (const file of SPRINT_KIT_FILES.commands) {
44
+ const src = join(templateDir, '.claude', 'commands', file);
45
+ if (!existsSync(src)) continue;
46
+ const name = basename(file, '.md');
47
+ const skillDir = join(skillsDir, name);
48
+ ensureDirSync(skillDir);
49
+ const content = readFileSync(src, 'utf-8');
50
+ writeFileSync(join(skillDir, 'SKILL.md'), content);
51
+ results.converted.push(`.codex/skills/${name}/SKILL.md`);
52
+ }
53
+ }
54
+
55
+ function convertRulesToCodex(projectDir, templateDir, results) {
56
+ // .claude/rules/*.md → .codex/rules/*.md
57
+ const rulesDir = join(projectDir, '.codex', 'rules');
58
+ ensureDirSync(rulesDir);
59
+
60
+ for (const file of SPRINT_KIT_FILES.rules) {
61
+ const src = join(templateDir, '.claude', 'rules', file);
62
+ if (!existsSync(src)) continue;
63
+ const content = readFileSync(src, 'utf-8');
64
+ const dest = join(rulesDir, file);
65
+ writeFileSync(dest, content);
66
+ results.converted.push(`.codex/rules/${file}`);
67
+ }
68
+ }
@@ -0,0 +1,131 @@
1
+ import fsExtra from 'fs-extra';
2
+ const { copySync, ensureDirSync } = fsExtra;
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { SPRINT_KIT_FILES, SPRINT_KIT_VERSION } from './manifest.js';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
10
+
11
+ // Overwrite policy per category
12
+ const OVERWRITE_POLICY = {
13
+ agents: 'overwrite', // Sprint Kit owned
14
+ commands: 'overwrite', // Sprint Kit owned
15
+ rules: 'overwrite', // Sprint Kit owned
16
+ hooks: 'overwrite', // Sprint Kit owned
17
+ docs: 'overwrite', // Sprint Kit owned
18
+ };
19
+
20
+ export function copySprintKitFiles(projectDir, options = {}) {
21
+ const { dryRun = false, force = false } = options;
22
+ const results = { copied: [], skipped: [], overwritten: [] };
23
+
24
+ // Agents
25
+ for (const file of SPRINT_KIT_FILES.agents) {
26
+ copyFile(
27
+ join(TEMPLATES_DIR, '.claude', 'agents', file),
28
+ join(projectDir, '.claude', 'agents', file),
29
+ results, { dryRun, force }
30
+ );
31
+ }
32
+
33
+ // Commands (Sprint Kit only — bmad/ is BMad-owned)
34
+ for (const file of SPRINT_KIT_FILES.commands) {
35
+ copyFile(
36
+ join(TEMPLATES_DIR, '.claude', 'commands', file),
37
+ join(projectDir, '.claude', 'commands', file),
38
+ results, { dryRun, force }
39
+ );
40
+ }
41
+
42
+ // Rules
43
+ for (const file of SPRINT_KIT_FILES.rules) {
44
+ copyFile(
45
+ join(TEMPLATES_DIR, '.claude', 'rules', file),
46
+ join(projectDir, '.claude', 'rules', file),
47
+ results, { dryRun, force }
48
+ );
49
+ }
50
+
51
+ // Hooks
52
+ for (const file of SPRINT_KIT_FILES.hooks) {
53
+ copyFile(
54
+ join(TEMPLATES_DIR, '.claude', 'hooks', file),
55
+ join(projectDir, '.claude', 'hooks', file),
56
+ results, { dryRun, force }
57
+ );
58
+ }
59
+
60
+ // Docs
61
+ for (const file of SPRINT_KIT_FILES.docs) {
62
+ copyFile(
63
+ join(TEMPLATES_DIR, '_bmad', 'docs', file),
64
+ join(projectDir, '_bmad', 'docs', file),
65
+ results, { dryRun, force }
66
+ );
67
+ }
68
+
69
+ // Preview template
70
+ const previewSrc = join(TEMPLATES_DIR, 'preview-template');
71
+ const previewDest = join(projectDir, 'preview-template');
72
+ if (existsSync(previewSrc)) {
73
+ if (dryRun) {
74
+ results.copied.push('preview-template/');
75
+ } else {
76
+ ensureDirSync(previewDest);
77
+ copySync(previewSrc, previewDest, { overwrite: true });
78
+ results.overwritten.push('preview-template/');
79
+ }
80
+ }
81
+
82
+ // .mcp.json.example
83
+ const mcpSrc = join(TEMPLATES_DIR, '.mcp.json.example');
84
+ const mcpDest = join(projectDir, '.mcp.json.example');
85
+ if (existsSync(mcpSrc)) {
86
+ copyFile(mcpSrc, mcpDest, results, { dryRun, force });
87
+ }
88
+
89
+ // Write version marker
90
+ if (!dryRun) {
91
+ const versionPath = join(projectDir, '.claude', '.sprint-kit-version');
92
+ ensureDirSync(dirname(versionPath));
93
+ writeFileSync(versionPath, SPRINT_KIT_VERSION + '\n');
94
+ }
95
+
96
+ // Ensure specs/ directory exists
97
+ if (!dryRun) {
98
+ ensureDirSync(join(projectDir, 'specs'));
99
+ }
100
+
101
+ return results;
102
+ }
103
+
104
+ function copyFile(src, dest, results, { dryRun, force }) {
105
+ if (!existsSync(src)) {
106
+ results.skipped.push(dest);
107
+ return;
108
+ }
109
+
110
+ const destExists = existsSync(dest);
111
+
112
+ if (dryRun) {
113
+ results[destExists ? 'overwritten' : 'copied'].push(dest);
114
+ return;
115
+ }
116
+
117
+ ensureDirSync(dirname(dest));
118
+ copySync(src, dest, { overwrite: true });
119
+
120
+ if (destExists) {
121
+ results.overwritten.push(dest);
122
+ } else {
123
+ results.copied.push(dest);
124
+ }
125
+ }
126
+
127
+ // For update: apply overwrite policy
128
+ export function updateSprintKitFiles(projectDir, options = {}) {
129
+ // Same as copy but respects SKIP rules
130
+ return copySprintKitFiles(projectDir, { ...options, force: true });
131
+ }
@@ -0,0 +1,81 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { BMAD_COMPAT } from './manifest.js';
4
+
5
+ export function detect(projectDir) {
6
+ return {
7
+ nodeVersion: getNodeVersion(),
8
+ isGitRepo: existsSync(join(projectDir, '.git')),
9
+ hasBmad: existsSync(join(projectDir, '_bmad', 'bmm')),
10
+ bmadVersion: readBmadVersion(projectDir),
11
+ hasSprintKit: existsSync(join(projectDir, '.claude', '.sprint-kit-version')),
12
+ sprintKitVersion: readSprintKitVersion(projectDir),
13
+ hasClaude: existsSync(join(projectDir, '.claude')),
14
+ hasSettings: existsSync(join(projectDir, '.claude', 'settings.json')),
15
+ hasMcpConfig: existsSync(join(projectDir, '.mcp.json')),
16
+ };
17
+ }
18
+
19
+ function getNodeVersion() {
20
+ try {
21
+ return process.version.replace(/^v/, '');
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function readBmadVersion(projectDir) {
28
+ // Try reading from _bmad/_config/manifest.yaml
29
+ const manifestPath = join(projectDir, '_bmad', '_config', 'manifest.yaml');
30
+ if (!existsSync(manifestPath)) return null;
31
+ try {
32
+ const content = readFileSync(manifestPath, 'utf-8');
33
+ const match = content.match(/version:\s*['"]?([^'"\n]+)/);
34
+ return match ? match[1].trim() : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function readSprintKitVersion(projectDir) {
41
+ const versionPath = join(projectDir, '.claude', '.sprint-kit-version');
42
+ if (!existsSync(versionPath)) return null;
43
+ try {
44
+ return readFileSync(versionPath, 'utf-8').trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ // Parse semver-ish version strings including pre-release tags like "6.0.0-Beta.8"
51
+ export function parseVersion(version) {
52
+ if (!version) return null;
53
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
54
+ if (!match) return null;
55
+ return {
56
+ major: parseInt(match[1], 10),
57
+ minor: parseInt(match[2], 10),
58
+ patch: parseInt(match[3], 10),
59
+ prerelease: match[4] || null,
60
+ raw: version,
61
+ };
62
+ }
63
+
64
+ // Returns: 'compatible', 'above_verified', 'below_minimum', 'unknown'
65
+ export function checkBmadCompat(bmadVersion) {
66
+ if (!bmadVersion) return 'unknown';
67
+ const current = parseVersion(bmadVersion);
68
+ const minimum = parseVersion(BMAD_COMPAT.minimum);
69
+ const verified = parseVersion(BMAD_COMPAT.verified);
70
+ if (!current || !minimum || !verified) return 'unknown';
71
+
72
+ if (compareMajorMinor(current, minimum) < 0) return 'below_minimum';
73
+ if (compareMajorMinor(current, verified) > 0) return 'above_verified';
74
+ return 'compatible';
75
+ }
76
+
77
+ function compareMajorMinor(a, b) {
78
+ if (a.major !== b.major) return a.major - b.major;
79
+ if (a.minor !== b.minor) return a.minor - b.minor;
80
+ return 0; // Patch differences are ignored per policy
81
+ }