spec-runner 1.0.0-alpha.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 (53) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +193 -0
  3. package/bin/spec-runner.js +715 -0
  4. package/install.sh +80 -0
  5. package/package.json +45 -0
  6. package/templates/base/.github/PULL_REQUEST_TEMPLATE.md +43 -0
  7. package/templates/base/.github/workflows/phase-gate-check.yml +216 -0
  8. package/templates/base/docs/adr/TEMPLATE.md +46 -0
  9. package/templates/base/docs/glossary.md +51 -0
  10. package/templates/base/docs/review/debt.md +8 -0
  11. package/templates/base/scripts/spec-runner.sh +1079 -0
  12. package/templates/base/templates/requirement/template.md +40 -0
  13. package/templates/claude/.claude/commands/sr-complete.md +9 -0
  14. package/templates/claude/.claude/commands/sr-configure.md +11 -0
  15. package/templates/claude/.claude/commands/sr-design-detail.md +9 -0
  16. package/templates/claude/.claude/commands/sr-design-high.md +9 -0
  17. package/templates/claude/.claude/commands/sr-fix.md +9 -0
  18. package/templates/claude/.claude/commands/sr-hotfix.md +9 -0
  19. package/templates/claude/.claude/commands/sr-implement.md +9 -0
  20. package/templates/claude/.claude/commands/sr-init.md +10 -0
  21. package/templates/claude/.claude/commands/sr-review.md +9 -0
  22. package/templates/claude/.claude/commands/sr-set-gate.md +9 -0
  23. package/templates/claude/.claude/commands/sr-status.md +9 -0
  24. package/templates/claude/.claude/commands/sr-test-design.md +9 -0
  25. package/templates/claude/.claude/hooks/pre-tool-use.sh +79 -0
  26. package/templates/claude/.claude/settings.json +29 -0
  27. package/templates/claude/CLAUDE.md +141 -0
  28. package/templates/copilot/.github/copilot-instructions.md +25 -0
  29. package/templates/copilot/.github/prompts/sr-complete.prompt.md +13 -0
  30. package/templates/copilot/.github/prompts/sr-configure.prompt.md +13 -0
  31. package/templates/copilot/.github/prompts/sr-design-detail.prompt.md +14 -0
  32. package/templates/copilot/.github/prompts/sr-design-high.prompt.md +13 -0
  33. package/templates/copilot/.github/prompts/sr-fix.prompt.md +14 -0
  34. package/templates/copilot/.github/prompts/sr-hotfix.prompt.md +14 -0
  35. package/templates/copilot/.github/prompts/sr-implement.prompt.md +13 -0
  36. package/templates/copilot/.github/prompts/sr-init.prompt.md +15 -0
  37. package/templates/copilot/.github/prompts/sr-review.prompt.md +14 -0
  38. package/templates/copilot/.github/prompts/sr-set-gate.prompt.md +14 -0
  39. package/templates/copilot/.github/prompts/sr-status.prompt.md +13 -0
  40. package/templates/copilot/.github/prompts/sr-test-design.prompt.md +13 -0
  41. package/templates/cursor/.cursor/commands/sr-complete.md +9 -0
  42. package/templates/cursor/.cursor/commands/sr-configure.md +11 -0
  43. package/templates/cursor/.cursor/commands/sr-design-detail.md +9 -0
  44. package/templates/cursor/.cursor/commands/sr-design-high.md +9 -0
  45. package/templates/cursor/.cursor/commands/sr-fix.md +9 -0
  46. package/templates/cursor/.cursor/commands/sr-hotfix.md +9 -0
  47. package/templates/cursor/.cursor/commands/sr-implement.md +9 -0
  48. package/templates/cursor/.cursor/commands/sr-init.md +10 -0
  49. package/templates/cursor/.cursor/commands/sr-review.md +9 -0
  50. package/templates/cursor/.cursor/commands/sr-set-gate.md +9 -0
  51. package/templates/cursor/.cursor/commands/sr-status.md +9 -0
  52. package/templates/cursor/.cursor/commands/sr-test-design.md +9 -0
  53. package/templates/cursor/.cursorrules +25 -0
@@ -0,0 +1,715 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // spec-runner — AI-driven DDD phase gate system installer
4
+ // =============================================================================
5
+ // npx spec-runner → 開発環境のみ選択してファイル展開
6
+ // npx spec-runner --configure → パス・TDD等の詳細設定(対話)。init からも呼ばれる
7
+ // npx spec-runner --update → 既存プロジェクトを最新版に更新
8
+ // =============================================================================
9
+
10
+ 'use strict';
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const { execSync, spawnSync } = require('child_process');
15
+
16
+ // ── 依存チェック(chalk/enquirer/ora が入っていない場合のフォールバック) ──
17
+ let chalk, ora;
18
+ try {
19
+ chalk = require('chalk');
20
+ } catch {
21
+ // chalk がない場合のフォールバック(bold.green 等の連鎖にも対応)
22
+ const id = s => s;
23
+ const proxy = () => new Proxy(id, { get: () => proxy() });
24
+ chalk = new Proxy(id, { get: () => proxy() });
25
+ }
26
+ try {
27
+ ora = require('ora');
28
+ } catch {
29
+ ora = (text) => ({ start: () => ({ succeed: (t) => console.log('✓', t || text), fail: (t) => console.error('✗', t || text) }) });
30
+ }
31
+
32
+ // ── 定数 ──────────────────────────────────────────────────────────────────────
33
+ const PKG_DIR = path.resolve(__dirname, '..');
34
+ const CWD = process.cwd();
35
+ const CONFIG_DIR = path.join(CWD, '.spec-runner');
36
+ const CONFIG_SH = path.join(CONFIG_DIR, 'config.sh');
37
+ const ARGS = process.argv.slice(2);
38
+
39
+ const isUpdate = ARGS.includes('--update');
40
+ const isConfigure = ARGS.includes('--configure');
41
+ const isSkipQuestion = ARGS.includes('--skip-questions');
42
+ const isDryRun = ARGS.includes('--dry-run');
43
+
44
+ // ── ユーティリティ ────────────────────────────────────────────────────────────
45
+ const log = (...a) => console.log(...a);
46
+ const ok = (msg) => log(chalk.green('✓'), msg);
47
+ const warn = (msg) => log(chalk.yellow('⚠'), msg);
48
+ const info = (msg) => log(chalk.cyan('ℹ'), msg);
49
+ const err = (msg) => { console.error(chalk.red('ERROR:'), msg); process.exit(1); };
50
+
51
+ function checkCommand(cmd) {
52
+ const r = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
53
+ return r.status === 0;
54
+ }
55
+
56
+ function copyFile(src, dest, vars = {}) {
57
+ if (!fs.existsSync(src)) return;
58
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
59
+ let content = fs.readFileSync(src, 'utf8');
60
+ // テンプレート変数を置換 {{VAR}}
61
+ for (const [k, v] of Object.entries(vars)) {
62
+ content = content.replaceAll(`{{${k}}}`, v);
63
+ }
64
+ if (!isDryRun) fs.writeFileSync(dest, content);
65
+ ok(`${path.relative(CWD, dest)}`);
66
+ }
67
+
68
+ function copyDir(srcDir, destDir, vars = {}) {
69
+ if (!fs.existsSync(srcDir)) return;
70
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
71
+ const srcPath = path.join(srcDir, entry.name);
72
+ const destPath = path.join(destDir, entry.name);
73
+ if (entry.isDirectory()) {
74
+ copyDir(srcPath, destPath, vars);
75
+ } else {
76
+ if (!fs.existsSync(destPath)) {
77
+ copyFile(srcPath, destPath, vars);
78
+ } else {
79
+ warn(`スキップ(既存): ${path.relative(CWD, destPath)}`);
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ // ── 対話型プロンプト(enquirer がない場合は readline フォールバック) ────────
86
+ async function prompt(questions) {
87
+ try {
88
+ const { prompt: enquirerPrompt } = require('enquirer');
89
+ return await enquirerPrompt(questions);
90
+ } catch {
91
+ // readline フォールバック
92
+ const readline = require('readline');
93
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
94
+ const answers = {};
95
+ for (const q of questions) {
96
+ if (q.type === 'select') {
97
+ const choices = q.choices.map((c, i) => ` ${i + 1}) ${c}`).join('\n');
98
+ const answer = await new Promise(resolve => {
99
+ rl.question(`${q.message}\n${choices}\n番号を入力: `, resolve);
100
+ });
101
+ const idx = parseInt(answer, 10) - 1;
102
+ answers[q.name] = q.choices[idx] || q.choices[0];
103
+ } else if (q.type === 'multiselect') {
104
+ const choiceList = q.choices.map((c, i) => ` ${i + 1}) ${c.name || c}`).join('\n');
105
+ const answer = await new Promise(resolve => {
106
+ rl.question(`${q.message}\n${choiceList}\n番号をカンマ区切りで入力(例: 1,2,3)。すべて選ぶ場合は Enter: `, resolve);
107
+ });
108
+ const selected = answer.trim() || q.choices.map((_, i) => i).join(',');
109
+ const indices = selected.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < q.choices.length);
110
+ answers[q.name] = indices.map(i => (q.choices[i] && q.choices[i].value) || q.choices[i]);
111
+ } else if (q.type === 'confirm') {
112
+ const answer = await new Promise(resolve => {
113
+ rl.question(`${q.message} [y/N]: `, resolve);
114
+ });
115
+ answers[q.name] = /^[yY]/.test(answer);
116
+ } else if (q.type === 'input') {
117
+ const initial = (q.initial !== undefined && q.initial !== null) ? String(q.initial) : '';
118
+ const answer = await new Promise(resolve => {
119
+ rl.question(initial ? `${q.message} [${initial}]: ` : `${q.message}: `, resolve);
120
+ });
121
+ answers[q.name] = (answer && answer.trim()) || initial;
122
+ } else {
123
+ const answer = await new Promise(resolve => {
124
+ rl.question(`${q.message}: `, resolve);
125
+ });
126
+ answers[q.name] = answer || (q.initial || '');
127
+ }
128
+ }
129
+ rl.close();
130
+ return answers;
131
+ }
132
+ }
133
+
134
+ // ── 既定の構造(AI と相談した結果で上書きできる)────────────────────────────
135
+ const DEFAULT_STRUCTURE = {
136
+ domainPath: 'src/domain',
137
+ usecasePath: 'src/useCase',
138
+ infraPath: 'src/infrastructure',
139
+ migrationDir: '',
140
+ sourceExtensions: 'ts tsx js jsx',
141
+ domainForbiddenGrepPattern: '',
142
+ testExtensions: 'test.ts test.tsx spec.ts spec.tsx',
143
+ appSourceDir: 'src',
144
+ testDir: 'tests',
145
+ buildCmd: 'echo "build"',
146
+ testCmd: 'echo "test"',
147
+ lintCmd: 'echo "lint"',
148
+ };
149
+
150
+ // ── メイン処理 ────────────────────────────────────────────────────────────────
151
+ async function main() {
152
+ log('');
153
+ log(chalk.bold('╔════════════════════════════════════════╗'));
154
+ log(chalk.bold('║ spec-runner v1.0.0-alpha ║'));
155
+ log(chalk.bold('║ AI-driven DDD Phase Gate System ║'));
156
+ log(chalk.bold('╚════════════════════════════════════════╝'));
157
+ log('');
158
+
159
+ // ── 依存チェック ──────────────────────────────────────────────────────────
160
+ if (!checkCommand('jq')) {
161
+ err('jq がインストールされていません。\n macOS: brew install jq\n Ubuntu: sudo apt install jq');
162
+ }
163
+ if (!checkCommand('git')) {
164
+ err('git がインストールされていません。');
165
+ }
166
+ ok('依存チェック完了 (jq, git)');
167
+ log('');
168
+
169
+ // ── 更新モード ──────────────────────────────────────────────────────────────
170
+ if (isUpdate) {
171
+ await runUpdate();
172
+ return;
173
+ }
174
+
175
+ // ── 設定モード(init から呼ばれる。詳細対話で config を更新)────────────────
176
+ if (isConfigure) {
177
+ await runConfigure();
178
+ return;
179
+ }
180
+
181
+ // ── 既存の spec-runner チェック ─────────────────────────────────────────────
182
+ if (fs.existsSync(CONFIG_SH)) {
183
+ warn('既に spec-runner が導入されています。');
184
+ info('更新するには: npx spec-runner --update');
185
+ info('再設定するには: ./scripts/spec-runner.sh init(対話でパス等を設定)');
186
+ process.exit(0);
187
+ }
188
+
189
+ // ── 初回セットアップ:開発環境のみ選択 ─────────────────────────────────────
190
+ const d = DEFAULT_STRUCTURE;
191
+ let answers;
192
+ if (isSkipQuestion) {
193
+ answers = {
194
+ tools: ['claude', 'cursor', 'copilot'],
195
+ ci: 'github-actions',
196
+ language: 'ja',
197
+ };
198
+ } else {
199
+ log('');
200
+ info(chalk.bold('どの開発環境で使いますか?'));
201
+ log('');
202
+ answers = await prompt([
203
+ {
204
+ type: 'multiselect',
205
+ name: 'tools',
206
+ message: 'どの AI ツール用の設定をインストールしますか?',
207
+ choices: [
208
+ { name: 'Claude Code', value: 'claude' },
209
+ { name: 'Cursor', value: 'cursor' },
210
+ { name: 'GitHub Copilot', value: 'copilot' },
211
+ ],
212
+ },
213
+ {
214
+ type: 'select',
215
+ name: 'ci',
216
+ message: 'CI/CD プラットフォームを選択してください',
217
+ choices: ['github-actions', 'gitlab-ci', 'none'],
218
+ },
219
+ {
220
+ type: 'select',
221
+ name: 'language',
222
+ message: 'ドキュメントの言語を選択してください',
223
+ choices: ['ja', 'en'],
224
+ },
225
+ ]);
226
+ }
227
+ // 詳細設定は init 時の対話に回す。ここでは既定値を使用
228
+ answers = {
229
+ ...answers,
230
+ runtimeMemo: '',
231
+ projectNote: '',
232
+ domainPath: d.domainPath,
233
+ usecasePath: d.usecasePath,
234
+ infraPath: d.infraPath,
235
+ migrationDir: d.migrationDir,
236
+ sourceExtensions: d.sourceExtensions,
237
+ domainForbiddenGrepPattern: d.domainForbiddenGrepPattern,
238
+ testDir: d.testDir,
239
+ ddd: true,
240
+ tdd: true,
241
+ };
242
+
243
+ if (!answers.tools || answers.tools.length === 0) {
244
+ answers.tools = ['claude', 'cursor', 'copilot'];
245
+ }
246
+
247
+ const toolLabels = { claude: 'Claude Code', cursor: 'Cursor', copilot: 'GitHub Copilot' };
248
+ log('');
249
+ info(`構造: ${chalk.cyan(answers.domainPath)} → ${chalk.cyan(answers.usecasePath)} → ${chalk.cyan(answers.infraPath)}`);
250
+ info(`ツール: ${chalk.cyan((answers.tools || []).map(t => toolLabels[t] || t).join(', '))}`);
251
+ info(`DDD: ${chalk.cyan(answers.ddd ? 'あり' : 'なし')} / TDD: ${chalk.cyan(answers.tdd !== false ? '必須' : 'オプション')} / CI: ${chalk.cyan(answers.ci)} / 言語: ${chalk.cyan(answers.language)}`);
252
+ log('');
253
+
254
+ // ── ファイル展開 ──────────────────────────────────────────────────────────
255
+ const spinner = ora('ファイルを展開中...').start();
256
+ try {
257
+ await deployFiles(answers);
258
+ spinner.succeed('ファイルの展開完了');
259
+ } catch (e) {
260
+ spinner.fail('展開中にエラーが発生しました');
261
+ err(e.message);
262
+ }
263
+
264
+ // ── 完了メッセージ ────────────────────────────────────────────────────────
265
+ const tools = answers.tools || ['claude'];
266
+ log('');
267
+ log(chalk.bold.green('✅ spec-runner のセットアップが完了しました!'));
268
+ log('');
269
+ log(chalk.bold('次のステップ:'));
270
+ log(' • パス・TDD 等の設定と最初のユースケース開始:');
271
+ log(chalk.cyan(' ./scripts/spec-runner.sh init "ユースケース名" "集約名"'));
272
+ log('');
273
+ log(' init を実行すると対話で Domain/UseCase のパスやテスト設定を聞かれます。');
274
+ log(' 引数なしで init だけ実行すると設定対話のみ行えます。');
275
+ log('');
276
+ if (tools.includes('claude')) {
277
+ log(' • Claude Code: CLAUDE.md をプロジェクトルートに置いたまま開く');
278
+ }
279
+ if (tools.includes('cursor')) {
280
+ log(' • Cursor: .cursorrules が読み込まれます。Rules for AI で確認してください');
281
+ }
282
+ if (tools.includes('copilot')) {
283
+ log(' • GitHub Copilot: .github/copilot-instructions.md が参照されます');
284
+ }
285
+ log('');
286
+ log(chalk.bold('コマンド一覧:'));
287
+ log(chalk.cyan(' ./scripts/spec-runner.sh help'));
288
+ log('');
289
+ log(chalk.gray('💡 構造を変えたいときは .spec-runner/config.sh を編集してください'));
290
+ }
291
+
292
+ // ── ファイル展開処理 ──────────────────────────────────────────────────────────
293
+ function dddLayerPatternFromPaths(domainPath, usecasePath, infraPath) {
294
+ const last = (p) => (p || '').split('/').filter(Boolean).pop() || '';
295
+ return [last(domainPath), last(usecasePath), last(infraPath)].filter(Boolean).join('|') || 'domain|useCase|infrastructure';
296
+ }
297
+
298
+ async function deployFiles(answers) {
299
+ const d = DEFAULT_STRUCTURE;
300
+ const domainPath = (answers.domainPath || d.domainPath).trim();
301
+ const usecasePath = (answers.usecasePath || d.usecasePath).trim();
302
+ const infraPath = (answers.infraPath || d.infraPath).trim();
303
+ const migrationDir = (answers.migrationDir || d.migrationDir).trim();
304
+ const sourceExt = (answers.sourceExtensions || d.sourceExtensions).trim();
305
+ const forbiddenPat = (answers.domainForbiddenGrepPattern || d.domainForbiddenGrepPattern).trim();
306
+ const testDir = (answers.testDir || d.testDir).trim() || 'tests';
307
+
308
+ const toolsList = (answers.tools || ['claude']).join(',');
309
+ const vars = {
310
+ FRAMEWORK: 'custom',
311
+ SPEC_RUNNER_TOOLS: toolsList,
312
+ RUNTIME_MEMO: (answers.runtimeMemo || '').trim().replace(/\s+/g, ' '),
313
+ LANGUAGE: answers.language || 'ja',
314
+ DDD_ENABLED: answers.ddd ? 'true' : 'false',
315
+ TDD_ENABLED: answers.tdd !== false ? 'true' : 'false',
316
+ SOURCE_EXTENSIONS: sourceExt,
317
+ TEST_EXTENSIONS: d.testExtensions,
318
+ APP_SOURCE_DIR: d.appSourceDir,
319
+ TEST_DIR: testDir,
320
+ MIGRATION_DIR: migrationDir,
321
+ BUILD_CMD: d.buildCmd,
322
+ TEST_CMD: d.testCmd,
323
+ LINT_CMD: d.lintCmd,
324
+ DOMAIN_PATH: domainPath,
325
+ USECASE_PATH: usecasePath,
326
+ INFRA_PATH: infraPath,
327
+ DDD_LAYER_PATTERN: dddLayerPatternFromPaths(domainPath, usecasePath, infraPath),
328
+ DOMAIN_FORBIDDEN_GREP_PATTERN: forbiddenPat,
329
+ CI_PLATFORM: answers.ci || 'github-actions',
330
+ DOC_LANGUAGE: answers.language || 'ja',
331
+ CONFIGURED: answers.configured !== undefined ? (answers.configured ? 'true' : 'false') : 'false',
332
+ };
333
+
334
+ const templatesDir = path.join(PKG_DIR, 'templates');
335
+ const baseDir = path.join(templatesDir, 'base');
336
+ const claudeDir = path.join(templatesDir, 'claude');
337
+ const cursorDir = path.join(templatesDir, 'cursor');
338
+ const copilotDir = path.join(templatesDir, 'copilot');
339
+ const tools = answers.tools || ['claude'];
340
+
341
+ function copyPathFrom(srcDir, relPath, destDir) {
342
+ const src = path.join(srcDir, relPath);
343
+ const dest = path.join(destDir || CWD, relPath);
344
+ if (!fs.existsSync(src)) return;
345
+ if (fs.statSync(src).isDirectory()) {
346
+ copyDir(src, dest, vars);
347
+ } else {
348
+ copyFile(src, dest, vars);
349
+ }
350
+ }
351
+
352
+ log('');
353
+ log(chalk.bold('展開するファイル:'));
354
+
355
+ // 1. 共通(base): scripts, docs, templates, .github/workflows, .github/PULL_REQUEST_TEMPLATE
356
+ copyPathFrom(baseDir, 'scripts');
357
+ copyPathFrom(baseDir, 'docs');
358
+ copyPathFrom(baseDir, 'templates');
359
+ copyPathFrom(baseDir, '.github/workflows');
360
+ copyPathFrom(baseDir, '.github/PULL_REQUEST_TEMPLATE.md');
361
+
362
+ // 2. Claude Code 選択時: templates/claude/ をそのままコピー
363
+ if (tools.includes('claude')) {
364
+ copyDir(claudeDir, CWD, vars);
365
+ }
366
+
367
+ // 3. Cursor 選択時: templates/cursor/ をそのままコピー
368
+ if (tools.includes('cursor')) {
369
+ copyDir(cursorDir, CWD, vars);
370
+ }
371
+
372
+ // 4. Copilot 選択時: templates/copilot/.github/ を CWD/.github/ にマージ
373
+ if (tools.includes('copilot')) {
374
+ copyDir(path.join(copilotDir, '.github'), path.join(CWD, '.github'), vars);
375
+ }
376
+
377
+ // 5. .spec-runner/config.sh を生成
378
+ generateConfigSh(vars);
379
+
380
+ // 6. .gitignore に .spec-runner/state.json を追加
381
+ appendGitignore();
382
+
383
+ // 7. scripts/spec-runner.sh に実行権限を付与
384
+ const devShPath = path.join(CWD, 'scripts', 'spec-runner.sh');
385
+ if (fs.existsSync(devShPath) && !isDryRun) {
386
+ fs.chmodSync(devShPath, '755');
387
+ }
388
+ if (tools.includes('claude')) {
389
+ const hookPath = path.join(CWD, '.claude', 'hooks', 'pre-tool-use.sh');
390
+ if (fs.existsSync(hookPath) && !isDryRun) {
391
+ fs.chmodSync(hookPath, '755');
392
+ }
393
+ }
394
+ }
395
+
396
+ // ── .spec-runner/config.sh 生成 ─────────────────────────────────────────────────
397
+ function generateConfigSh(vars) {
398
+ const content = `# =============================================================================
399
+ # .spec-runner/config.sh — spec-runner 設定
400
+ # このファイルは scripts/spec-runner.sh が読み込む設定ファイルです
401
+ # npx spec-runner / init 時の対話で生成・更新されます
402
+ # =============================================================================
403
+ # 詳細設定済みか(init で対話すると true になる)
404
+ export CONFIGURED="${vars.CONFIGURED || 'false'}"
405
+
406
+ # 使用フレームワーク・言語(相談で決定): ${vars.RUNTIME_MEMO || '(未記入)'}
407
+
408
+ # フレームワーク情報
409
+ export SPEC_RUNNER_FRAMEWORK="${vars.FRAMEWORK}"
410
+ export SPEC_RUNNER_TOOLS="${vars.SPEC_RUNNER_TOOLS || 'claude,cursor,copilot'}"
411
+ export SPEC_RUNNER_LANGUAGE="${vars.LANGUAGE}"
412
+ export SPEC_RUNNER_DDD_ENABLED="${vars.DDD_ENABLED}"
413
+ export SPEC_RUNNER_DOC_LANGUAGE="${vars.DOC_LANGUAGE}"
414
+
415
+ # TDD(テスト駆動): true のとき実装前にテスト設計+テストコード必須。false でオプションに
416
+ export TDD_ENABLED="${vars.TDD_ENABLED}"
417
+
418
+ # ─────────────────────────────────────────────────────────────────────────────
419
+ # 拡張子設定
420
+ # フェーズゲートフックがブロック対象にする拡張子(スペース区切り)
421
+ # ─────────────────────────────────────────────────────────────────────────────
422
+ export SOURCE_EXTENSIONS="${vars.SOURCE_EXTENSIONS}"
423
+ export TEST_EXTENSIONS="${vars.TEST_EXTENSIONS}"
424
+
425
+ # ─────────────────────────────────────────────────────────────────────────────
426
+ # ディレクトリ構成
427
+ # ─────────────────────────────────────────────────────────────────────────────
428
+ export APP_SOURCE_DIR="${vars.APP_SOURCE_DIR}"
429
+ export TEST_DIR="${vars.TEST_DIR}"
430
+ export MIGRATION_DIR="${vars.MIGRATION_DIR}"
431
+
432
+ # ─────────────────────────────────────────────────────────────────────────────
433
+ # DDD レイヤー設定
434
+ # CI の DDD 依存方向チェックで使用。空ならスキップ
435
+ # ─────────────────────────────────────────────────────────────────────────────
436
+ export DOMAIN_PATH="${vars.DOMAIN_PATH}"
437
+ export USECASE_PATH="${vars.USECASE_PATH}"
438
+ export INFRA_PATH="${vars.INFRA_PATH}"
439
+ export DDD_LAYER_PATTERN="${vars.DDD_LAYER_PATTERN}"
440
+ export DOMAIN_FORBIDDEN_GREP_PATTERN="${vars.DOMAIN_FORBIDDEN_GREP_PATTERN}"
441
+
442
+ # ─────────────────────────────────────────────────────────────────────────────
443
+ # ビルド / テスト コマンド
444
+ # ─────────────────────────────────────────────────────────────────────────────
445
+ export BUILD_CMD="${vars.BUILD_CMD}"
446
+ export TEST_CMD="${vars.TEST_CMD}"
447
+ export LINT_CMD="${vars.LINT_CMD}"
448
+
449
+ # ─────────────────────────────────────────────────────────────────────────────
450
+ # CI/CD 設定
451
+ # ─────────────────────────────────────────────────────────────────────────────
452
+ export CI_PLATFORM="${vars.CI_PLATFORM}"
453
+ `;
454
+
455
+ const dest = CONFIG_SH;
456
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
457
+ if (!isDryRun) fs.writeFileSync(dest, content);
458
+ ok(`.spec-runner/config.sh`);
459
+ }
460
+
461
+ // ── .gitignore 更新 ───────────────────────────────────────────────────────────
462
+ function appendGitignore() {
463
+ const gitignorePath = path.join(CWD, '.gitignore');
464
+ const additions = [
465
+ '',
466
+ '# spec-runner state (作業中の状態ファイル)',
467
+ '.spec-runner/state.json',
468
+ ].join('\n');
469
+
470
+ if (fs.existsSync(gitignorePath)) {
471
+ const content = fs.readFileSync(gitignorePath, 'utf8');
472
+ if (!content.includes('.spec-runner/state.json')) {
473
+ if (!isDryRun) fs.appendFileSync(gitignorePath, additions);
474
+ ok('.gitignore に .spec-runner/state.json を追加');
475
+ }
476
+ } else {
477
+ if (!isDryRun) fs.writeFileSync(gitignorePath, additions.trimStart());
478
+ ok('.gitignore を作成');
479
+ }
480
+ }
481
+
482
+ // ── config.sh から変数をパース ─────────────────────────────────────────────────
483
+ function parseConfigSh(content) {
484
+ const vars = {};
485
+ const re = /^export\s+([A-Z_]+)="([^"]*)"\s*$/gm;
486
+ let m;
487
+ while ((m = re.exec(content)) !== null) {
488
+ vars[m[1]] = m[2];
489
+ }
490
+ return vars;
491
+ }
492
+
493
+ // ── 設定モード(init から呼ばれる。詳細対話で config を更新)────────────────────
494
+ async function runConfigure() {
495
+ if (!fs.existsSync(CONFIG_SH)) {
496
+ err('.spec-runner/config.sh が見つかりません。まず npx spec-runner を実行してください。');
497
+ }
498
+
499
+ const d = DEFAULT_STRUCTURE;
500
+ const configContent = fs.readFileSync(CONFIG_SH, 'utf8');
501
+ const cfg = parseConfigSh(configContent);
502
+
503
+ log('');
504
+ log(chalk.bold('╔════════════════════════════════════════╗'));
505
+ log(chalk.bold('║ spec-runner 詳細設定(パス・TDD 等) ║'));
506
+ log(chalk.bold('╚════════════════════════════════════════╝'));
507
+ log('');
508
+ info('AI と相談して決めた構造を入力してください。既定値でよければ Enter。');
509
+ log('');
510
+
511
+ const answers = await prompt([
512
+ {
513
+ type: 'input',
514
+ name: 'runtimeMemo',
515
+ message: '使用フレームワーク・言語(AI と相談した名前。任意)',
516
+ initial: cfg.RUNTIME_MEMO || '',
517
+ },
518
+ {
519
+ type: 'input',
520
+ name: 'projectNote',
521
+ message: 'やりたいこと・プロジェクトの種類(メモ用・任意)',
522
+ initial: '',
523
+ },
524
+ {
525
+ type: 'input',
526
+ name: 'domainPath',
527
+ message: 'Domain 層のパス',
528
+ initial: cfg.DOMAIN_PATH || d.domainPath,
529
+ },
530
+ {
531
+ type: 'input',
532
+ name: 'usecasePath',
533
+ message: 'UseCase 層のパス',
534
+ initial: cfg.USECASE_PATH || d.usecasePath,
535
+ },
536
+ {
537
+ type: 'input',
538
+ name: 'infraPath',
539
+ message: 'Infrastructure 層のパス',
540
+ initial: cfg.INFRA_PATH || d.infraPath,
541
+ },
542
+ {
543
+ type: 'input',
544
+ name: 'migrationDir',
545
+ message: 'マイグレーション等のディレクトリ(無ければ空で Enter)',
546
+ initial: cfg.MIGRATION_DIR || d.migrationDir,
547
+ },
548
+ {
549
+ type: 'input',
550
+ name: 'sourceExtensions',
551
+ message: 'ソースの拡張子(スペース区切り)',
552
+ initial: cfg.SOURCE_EXTENSIONS || d.sourceExtensions,
553
+ },
554
+ {
555
+ type: 'input',
556
+ name: 'domainForbiddenGrepPattern',
557
+ message: 'Domain が import してはいけないパターン(正規表現、空でスキップ)',
558
+ initial: cfg.DOMAIN_FORBIDDEN_GREP_PATTERN || d.domainForbiddenGrepPattern,
559
+ },
560
+ {
561
+ type: 'input',
562
+ name: 'testDir',
563
+ message: 'テストディレクトリのパス(TDD で未コミット検出に使用)',
564
+ initial: cfg.TEST_DIR || d.testDir,
565
+ },
566
+ {
567
+ type: 'confirm',
568
+ name: 'ddd',
569
+ message: 'DDD(Domain-Driven Design)を使いますか?',
570
+ initial: (cfg.SPEC_RUNNER_DDD_ENABLED || 'true') === 'true',
571
+ },
572
+ {
573
+ type: 'confirm',
574
+ name: 'tdd',
575
+ message: 'TDD(テスト駆動)を必須にしますか? 実装前にテスト設計+テストコードを必ず書くルールになります',
576
+ initial: (cfg.TDD_ENABLED || 'true') === 'true',
577
+ },
578
+ {
579
+ type: 'select',
580
+ name: 'ci',
581
+ message: 'CI/CD プラットフォームを選択してください',
582
+ choices: ['github-actions', 'gitlab-ci', 'none'],
583
+ },
584
+ {
585
+ type: 'select',
586
+ name: 'language',
587
+ message: 'ドキュメントの言語を選択してください',
588
+ choices: ['ja', 'en'],
589
+ },
590
+ ]);
591
+
592
+ const domainPath = (answers.domainPath || d.domainPath).trim();
593
+ const usecasePath = (answers.usecasePath || d.usecasePath).trim();
594
+ const infraPath = (answers.infraPath || d.infraPath).trim();
595
+ const migrationDir = (answers.migrationDir || d.migrationDir).trim();
596
+ const sourceExt = (answers.sourceExtensions || d.sourceExtensions).trim();
597
+ const forbiddenPat = (answers.domainForbiddenGrepPattern || d.domainForbiddenGrepPattern).trim();
598
+ const testDir = (answers.testDir || d.testDir).trim() || 'tests';
599
+
600
+ const vars = {
601
+ FRAMEWORK: cfg.SPEC_RUNNER_FRAMEWORK || 'custom',
602
+ SPEC_RUNNER_TOOLS: cfg.SPEC_RUNNER_TOOLS || 'claude,cursor,copilot',
603
+ RUNTIME_MEMO: (answers.runtimeMemo || '').trim().replace(/\s+/g, ' '),
604
+ LANGUAGE: answers.language || 'ja',
605
+ DDD_ENABLED: answers.ddd ? 'true' : 'false',
606
+ TDD_ENABLED: answers.tdd !== false ? 'true' : 'false',
607
+ SOURCE_EXTENSIONS: sourceExt,
608
+ TEST_EXTENSIONS: d.testExtensions,
609
+ APP_SOURCE_DIR: d.appSourceDir,
610
+ TEST_DIR: testDir,
611
+ MIGRATION_DIR: migrationDir,
612
+ BUILD_CMD: cfg.BUILD_CMD || d.buildCmd,
613
+ TEST_CMD: cfg.TEST_CMD || d.testCmd,
614
+ LINT_CMD: cfg.LINT_CMD || d.lintCmd,
615
+ DOMAIN_PATH: domainPath,
616
+ USECASE_PATH: usecasePath,
617
+ INFRA_PATH: infraPath,
618
+ DDD_LAYER_PATTERN: dddLayerPatternFromPaths(domainPath, usecasePath, infraPath),
619
+ DOMAIN_FORBIDDEN_GREP_PATTERN: forbiddenPat,
620
+ CI_PLATFORM: answers.ci || 'github-actions',
621
+ DOC_LANGUAGE: answers.language || 'ja',
622
+ CONFIGURED: 'true',
623
+ };
624
+
625
+ generateConfigSh(vars);
626
+ log('');
627
+ ok('設定を保存しました: .spec-runner/config.sh');
628
+ log('');
629
+ }
630
+
631
+ // ── 更新モード ────────────────────────────────────────────────────────────────
632
+ async function runUpdate() {
633
+ log(chalk.bold('📦 spec-runner を最新版に更新します'));
634
+ log('');
635
+
636
+ if (!fs.existsSync(CONFIG_SH)) {
637
+ err('.spec-runner/config.sh が見つかりません。まず npx spec-runner を実行してください。');
638
+ }
639
+
640
+ const configContent = fs.readFileSync(CONFIG_SH, 'utf8');
641
+ const cfg = parseConfigSh(configContent);
642
+ const d = DEFAULT_STRUCTURE;
643
+
644
+ info(`現在の構造: ${cfg.DOMAIN_PATH || d.domainPath} → ${cfg.USECASE_PATH || d.usecasePath} → ${cfg.INFRA_PATH || d.infraPath}`);
645
+ log('');
646
+
647
+ const updateTargets = [{ rel: 'scripts/spec-runner.sh', from: 'base' }];
648
+ if (fs.existsSync(path.join(CWD, '.claude'))) {
649
+ updateTargets.push(
650
+ { rel: '.claude/hooks/pre-tool-use.sh', from: 'claude' },
651
+ { rel: '.claude/settings.json', from: 'claude' },
652
+ );
653
+ }
654
+
655
+ const templatesDir = path.join(PKG_DIR, 'templates');
656
+ const vars = {
657
+ FRAMEWORK: cfg.SPEC_RUNNER_FRAMEWORK || 'custom',
658
+ LANGUAGE: cfg.SPEC_RUNNER_LANGUAGE || 'ja',
659
+ DDD_ENABLED: cfg.SPEC_RUNNER_DDD_ENABLED || 'true',
660
+ SOURCE_EXTENSIONS: cfg.SOURCE_EXTENSIONS || d.sourceExtensions,
661
+ TEST_EXTENSIONS: cfg.TEST_EXTENSIONS || d.testExtensions,
662
+ APP_SOURCE_DIR: cfg.APP_SOURCE_DIR || d.appSourceDir,
663
+ TEST_DIR: cfg.TEST_DIR || d.testDir,
664
+ MIGRATION_DIR: cfg.MIGRATION_DIR || d.migrationDir,
665
+ BUILD_CMD: cfg.BUILD_CMD || d.buildCmd,
666
+ TEST_CMD: cfg.TEST_CMD || d.testCmd,
667
+ LINT_CMD: cfg.LINT_CMD || d.lintCmd,
668
+ DOMAIN_PATH: cfg.DOMAIN_PATH || d.domainPath,
669
+ USECASE_PATH: cfg.USECASE_PATH || d.usecasePath,
670
+ INFRA_PATH: cfg.INFRA_PATH || d.infraPath,
671
+ DDD_LAYER_PATTERN: cfg.DDD_LAYER_PATTERN || dddLayerPatternFromPaths(cfg.DOMAIN_PATH, cfg.USECASE_PATH, cfg.INFRA_PATH),
672
+ DOMAIN_FORBIDDEN_GREP_PATTERN: cfg.DOMAIN_FORBIDDEN_GREP_PATTERN || '',
673
+ CI_PLATFORM: cfg.CI_PLATFORM || 'github-actions',
674
+ DOC_LANGUAGE: cfg.SPEC_RUNNER_DOC_LANGUAGE || 'ja',
675
+ USECASE: '',
676
+ DATE: new Date().toISOString().slice(0, 10),
677
+ };
678
+
679
+ // 要件テンプレートが無ければ追加(init で必須のため)
680
+ const requirementTemplateDest = path.join(CWD, 'templates', 'requirement', 'template.md');
681
+ if (!fs.existsSync(requirementTemplateDest)) {
682
+ const requirementTemplateSrc = path.join(templatesDir, 'base', 'templates', 'requirement', 'template.md');
683
+ if (fs.existsSync(requirementTemplateSrc)) {
684
+ copyFile(requirementTemplateSrc, requirementTemplateDest, vars);
685
+ }
686
+ }
687
+
688
+ log(chalk.bold('更新するファイル:'));
689
+ for (const { rel, from } of updateTargets) {
690
+ const srcDir = path.join(templatesDir, from);
691
+ const src = path.join(srcDir, rel);
692
+ const dest = path.join(CWD, rel);
693
+ if (fs.existsSync(src)) {
694
+ // バックアップ
695
+ if (fs.existsSync(dest) && !isDryRun) {
696
+ fs.copyFileSync(dest, dest + '.bak');
697
+ }
698
+ copyFile(src, dest, vars);
699
+ }
700
+ }
701
+
702
+ // 実行権限
703
+ const devShPath = path.join(CWD, 'scripts', 'spec-runner.sh');
704
+ if (fs.existsSync(devShPath) && !isDryRun) fs.chmodSync(devShPath, '755');
705
+
706
+ log('');
707
+ log(chalk.bold.green('✅ 更新完了'));
708
+ info('バックアップ: scripts/spec-runner.sh.bak など');
709
+ }
710
+
711
+ // ── エントリー ────────────────────────────────────────────────────────────────
712
+ main().catch(e => {
713
+ console.error(chalk.red('予期せぬエラーが発生しました:'), e.message);
714
+ process.exit(1);
715
+ });