spec-runner 1.0.6 → 1.0.7

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