@sk8metal/michi-cli 0.0.6 → 0.0.8

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/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/dist/scripts/__tests__/create-project.test.d.ts +2 -0
  4. package/dist/scripts/__tests__/create-project.test.d.ts.map +1 -0
  5. package/dist/scripts/__tests__/create-project.test.js +247 -0
  6. package/dist/scripts/__tests__/create-project.test.js.map +1 -0
  7. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +2 -0
  8. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +1 -0
  9. package/dist/scripts/__tests__/multi-project-estimate.test.js +119 -0
  10. package/dist/scripts/__tests__/multi-project-estimate.test.js.map +1 -0
  11. package/dist/scripts/__tests__/setup-existing-project.test.d.ts +2 -0
  12. package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +1 -0
  13. package/dist/scripts/__tests__/setup-existing-project.test.js +67 -0
  14. package/dist/scripts/__tests__/setup-existing-project.test.js.map +1 -0
  15. package/dist/scripts/__tests__/setup-interactive.test.d.ts +2 -0
  16. package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +1 -0
  17. package/dist/scripts/__tests__/setup-interactive.test.js +160 -0
  18. package/dist/scripts/__tests__/setup-interactive.test.js.map +1 -0
  19. package/dist/scripts/config/default-config.json +57 -0
  20. package/dist/scripts/confluence-sync.d.ts +4 -0
  21. package/dist/scripts/confluence-sync.d.ts.map +1 -1
  22. package/dist/scripts/confluence-sync.js +12 -23
  23. package/dist/scripts/confluence-sync.js.map +1 -1
  24. package/dist/scripts/create-project.js +198 -137
  25. package/dist/scripts/create-project.js.map +1 -1
  26. package/dist/scripts/jira-sync.d.ts.map +1 -1
  27. package/dist/scripts/jira-sync.js +15 -0
  28. package/dist/scripts/jira-sync.js.map +1 -1
  29. package/dist/scripts/list-projects.d.ts.map +1 -1
  30. package/dist/scripts/list-projects.js +42 -15
  31. package/dist/scripts/list-projects.js.map +1 -1
  32. package/dist/scripts/multi-project-estimate.d.ts.map +1 -1
  33. package/dist/scripts/multi-project-estimate.js +56 -21
  34. package/dist/scripts/multi-project-estimate.js.map +1 -1
  35. package/dist/scripts/resource-dashboard.d.ts.map +1 -1
  36. package/dist/scripts/resource-dashboard.js +74 -17
  37. package/dist/scripts/resource-dashboard.js.map +1 -1
  38. package/dist/scripts/setup-existing-project.js +248 -214
  39. package/dist/scripts/setup-existing-project.js.map +1 -1
  40. package/dist/scripts/setup-interactive.d.ts +10 -0
  41. package/dist/scripts/setup-interactive.d.ts.map +1 -0
  42. package/dist/scripts/setup-interactive.js +413 -0
  43. package/dist/scripts/setup-interactive.js.map +1 -0
  44. package/dist/scripts/utils/__tests__/config-validator.test.js +5 -0
  45. package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
  46. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +5 -0
  47. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +1 -0
  48. package/dist/scripts/utils/__tests__/spec-updater.test.js +158 -0
  49. package/dist/scripts/utils/__tests__/spec-updater.test.js.map +1 -0
  50. package/dist/scripts/utils/confluence-hierarchy.d.ts +2 -1
  51. package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
  52. package/dist/scripts/utils/confluence-hierarchy.js +5 -0
  53. package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
  54. package/dist/scripts/utils/project-finder.d.ts +30 -0
  55. package/dist/scripts/utils/project-finder.d.ts.map +1 -0
  56. package/dist/scripts/utils/project-finder.js +147 -0
  57. package/dist/scripts/utils/project-finder.js.map +1 -0
  58. package/dist/scripts/utils/spec-updater.d.ts +72 -0
  59. package/dist/scripts/utils/spec-updater.d.ts.map +1 -0
  60. package/dist/scripts/utils/spec-updater.js +141 -0
  61. package/dist/scripts/utils/spec-updater.js.map +1 -0
  62. package/dist/src/cli.d.ts.map +1 -1
  63. package/dist/src/cli.js +30 -7
  64. package/dist/src/cli.js.map +1 -1
  65. package/dist/vitest.config.d.ts.map +1 -1
  66. package/dist/vitest.config.js +8 -6
  67. package/dist/vitest.config.js.map +1 -1
  68. package/docs/README.md +2 -2
  69. package/docs/contributing/development.md +37 -0
  70. package/docs/getting-started/{new-project-setup.md → new-repository-setup.md} +66 -19
  71. package/docs/getting-started/setup.md +305 -182
  72. package/docs/guides/customization.md +1 -1
  73. package/docs/guides/multi-project.md +11 -8
  74. package/docs/reference/quick-reference.md +2 -2
  75. package/docs/testing-strategy.md +87 -0
  76. package/package.json +17 -5
  77. package/scripts/__tests__/create-project.test.ts +292 -0
  78. package/scripts/__tests__/multi-project-estimate.test.ts +145 -0
  79. package/scripts/__tests__/setup-existing-project.test.ts +79 -0
  80. package/scripts/__tests__/setup-interactive.test.ts +199 -0
  81. package/scripts/confluence-sync.ts +17 -29
  82. package/scripts/copy-static-assets.js +50 -0
  83. package/scripts/create-project.ts +219 -156
  84. package/scripts/jira-sync.ts +16 -1
  85. package/scripts/list-projects.ts +51 -24
  86. package/scripts/multi-project-estimate.ts +58 -22
  87. package/scripts/resource-dashboard.ts +91 -26
  88. package/scripts/setup-existing-project.ts +264 -223
  89. package/scripts/setup-existing.sh +29 -22
  90. package/scripts/setup-interactive.ts +565 -0
  91. package/scripts/utils/__tests__/config-validator.test.ts +6 -0
  92. package/scripts/utils/__tests__/spec-updater.test.ts +220 -0
  93. package/scripts/utils/confluence-hierarchy.ts +7 -1
  94. package/scripts/utils/project-finder.ts +184 -0
  95. package/scripts/utils/spec-updater.ts +212 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * spec-updater のテスト
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import {
9
+ loadSpecJson,
10
+ saveSpecJson,
11
+ updateSpecJsonAfterConfluenceSync,
12
+ updateSpecJsonAfterJiraSync,
13
+ type SpecJson
14
+ } from '../spec-updater.js';
15
+
16
+ const testDir = resolve(__dirname, '../../../.test-tmp');
17
+ const testFeatureName = 'test-feature';
18
+
19
+ describe('spec-updater', () => {
20
+ beforeEach(() => {
21
+ // テスト用ディレクトリを作成
22
+ if (existsSync(testDir)) {
23
+ rmSync(testDir, { recursive: true });
24
+ }
25
+ mkdirSync(resolve(testDir, '.kiro/specs', testFeatureName), { recursive: true });
26
+ });
27
+
28
+ afterEach(() => {
29
+ // テスト用ディレクトリを削除
30
+ if (existsSync(testDir)) {
31
+ rmSync(testDir, { recursive: true });
32
+ }
33
+ });
34
+
35
+ describe('loadSpecJson', () => {
36
+ it('spec.jsonが存在しない場合、最小限の構造を返す', () => {
37
+ const spec = loadSpecJson(testFeatureName, testDir);
38
+
39
+ expect(spec).toEqual({
40
+ featureName: testFeatureName,
41
+ confluence: {},
42
+ jira: {},
43
+ milestones: {}
44
+ });
45
+ });
46
+
47
+ it('spec.jsonが存在する場合、その内容を返す', () => {
48
+ const specPath = resolve(testDir, '.kiro/specs', testFeatureName, 'spec.json');
49
+ const testSpec: SpecJson = {
50
+ featureName: testFeatureName,
51
+ projectName: 'Test Project',
52
+ confluence: {
53
+ spaceKey: 'TEST'
54
+ },
55
+ jira: {},
56
+ milestones: {}
57
+ };
58
+
59
+ writeFileSync(specPath, JSON.stringify(testSpec, null, 2));
60
+
61
+ const spec = loadSpecJson(testFeatureName, testDir);
62
+ expect(spec).toEqual(testSpec);
63
+ });
64
+ });
65
+
66
+ describe('saveSpecJson', () => {
67
+ it('spec.jsonを保存し、lastUpdatedを追加する', () => {
68
+ const spec: SpecJson = {
69
+ featureName: testFeatureName,
70
+ confluence: {},
71
+ jira: {},
72
+ milestones: {}
73
+ };
74
+
75
+ saveSpecJson(testFeatureName, spec, testDir);
76
+
77
+ const specPath = resolve(testDir, '.kiro/specs', testFeatureName, 'spec.json');
78
+ expect(existsSync(specPath)).toBe(true);
79
+
80
+ const saved = JSON.parse(readFileSync(specPath, 'utf-8'));
81
+ expect(saved.featureName).toBe(testFeatureName);
82
+ expect(saved.lastUpdated).toBeDefined();
83
+ });
84
+ });
85
+
86
+ describe('updateSpecJsonAfterConfluenceSync', () => {
87
+ it('Confluence同期後にspec.jsonを正しく更新する(requirements)', () => {
88
+ updateSpecJsonAfterConfluenceSync(
89
+ testFeatureName,
90
+ 'requirements',
91
+ {
92
+ pageId: 'page123',
93
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
94
+ title: 'Test Requirements',
95
+ spaceKey: 'TEST'
96
+ },
97
+ testDir
98
+ );
99
+
100
+ const spec = loadSpecJson(testFeatureName, testDir);
101
+
102
+ expect(spec.confluence?.spaceKey).toBe('TEST');
103
+ expect(spec.confluence?.requirements).toEqual({
104
+ pageId: 'page123',
105
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
106
+ title: 'Test Requirements'
107
+ });
108
+ expect(spec.milestones?.requirementsCompleted).toBe(true);
109
+ });
110
+
111
+ it('Confluence同期後にspec.jsonを正しく更新する(design)', () => {
112
+ updateSpecJsonAfterConfluenceSync(
113
+ testFeatureName,
114
+ 'design',
115
+ {
116
+ pageId: 'page456',
117
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page456',
118
+ title: 'Test Design',
119
+ spaceKey: 'TEST'
120
+ },
121
+ testDir
122
+ );
123
+
124
+ const spec = loadSpecJson(testFeatureName, testDir);
125
+
126
+ expect(spec.confluence?.design).toEqual({
127
+ pageId: 'page456',
128
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page456',
129
+ title: 'Test Design'
130
+ });
131
+ expect(spec.milestones?.designCompleted).toBe(true);
132
+ });
133
+
134
+ it('Confluence同期後にspec.jsonを正しく更新する(tasks)', () => {
135
+ updateSpecJsonAfterConfluenceSync(
136
+ testFeatureName,
137
+ 'tasks',
138
+ {
139
+ pageId: 'page789',
140
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page789',
141
+ title: 'Test Tasks',
142
+ spaceKey: 'TEST'
143
+ },
144
+ testDir
145
+ );
146
+
147
+ const spec = loadSpecJson(testFeatureName, testDir);
148
+
149
+ expect(spec.confluence?.tasks).toEqual({
150
+ pageId: 'page789',
151
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page789',
152
+ title: 'Test Tasks'
153
+ });
154
+ expect(spec.milestones?.tasksCompleted).toBe(true);
155
+ });
156
+ });
157
+
158
+ describe('updateSpecJsonAfterJiraSync', () => {
159
+ it('JIRA同期後にspec.jsonを正しく更新する', () => {
160
+ updateSpecJsonAfterJiraSync(
161
+ testFeatureName,
162
+ {
163
+ projectKey: 'TEST',
164
+ epicKey: 'TEST-123',
165
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
166
+ storyKeys: ['TEST-124', 'TEST-125', 'TEST-126']
167
+ },
168
+ testDir
169
+ );
170
+
171
+ const spec = loadSpecJson(testFeatureName, testDir);
172
+
173
+ expect(spec.jira).toEqual({
174
+ projectKey: 'TEST',
175
+ epicKey: 'TEST-123',
176
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
177
+ storyKeys: ['TEST-124', 'TEST-125', 'TEST-126']
178
+ });
179
+ expect(spec.milestones?.jiraSyncCompleted).toBe(true);
180
+ });
181
+ });
182
+
183
+ describe('統合シナリオ', () => {
184
+ it('Confluence → JIRA の順に同期した場合、両方の情報が保持される', () => {
185
+ // 1. Confluence 同期(requirements)
186
+ updateSpecJsonAfterConfluenceSync(
187
+ testFeatureName,
188
+ 'requirements',
189
+ {
190
+ pageId: 'page123',
191
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
192
+ title: 'Test Requirements',
193
+ spaceKey: 'TEST'
194
+ },
195
+ testDir
196
+ );
197
+
198
+ // 2. JIRA 同期
199
+ updateSpecJsonAfterJiraSync(
200
+ testFeatureName,
201
+ {
202
+ projectKey: 'TEST',
203
+ epicKey: 'TEST-123',
204
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
205
+ storyKeys: ['TEST-124']
206
+ },
207
+ testDir
208
+ );
209
+
210
+ // 両方の情報が保持されているか確認
211
+ const spec = loadSpecJson(testFeatureName, testDir);
212
+
213
+ expect(spec.confluence?.spaceKey).toBe('TEST');
214
+ expect(spec.confluence?.requirements).toBeDefined();
215
+ expect(spec.jira?.epicKey).toBe('TEST-123');
216
+ expect(spec.milestones?.requirementsCompleted).toBe(true);
217
+ expect(spec.milestones?.jiraSyncCompleted).toBe(true);
218
+ });
219
+ });
220
+ });
@@ -14,8 +14,9 @@ import type { ConfluenceConfig, ConfluencePageCreationGranularity } from '../con
14
14
  * ページ作成結果
15
15
  */
16
16
  export interface PageCreationResult {
17
+ id: string; // ページID(idとpageIdの両方をサポート)
18
+ pageId: string; // ページID(後方互換性のため)
17
19
  url: string;
18
- pageId: string;
19
20
  title: string;
20
21
  }
21
22
 
@@ -281,6 +282,7 @@ export async function createSinglePage(
281
282
  const baseUrl = process.env.ATLASSIAN_URL || '';
282
283
  return {
283
284
  pages: [{
285
+ id: page.id,
284
286
  url: `${baseUrl}/wiki${page._links.webui}`,
285
287
  pageId: page.id,
286
288
  title: pageTitle
@@ -351,6 +353,7 @@ export async function createBySectionPages(
351
353
 
352
354
  const baseUrl = process.env.ATLASSIAN_URL || '';
353
355
  pages.push({
356
+ id: page.id,
354
357
  url: `${baseUrl}/wiki${page._links.webui}`,
355
358
  pageId: page.id,
356
359
  title: pageTitle
@@ -474,6 +477,7 @@ export async function createByHierarchySimplePages(
474
477
  const baseUrl = process.env.ATLASSIAN_URL || '';
475
478
  return {
476
479
  pages: [{
480
+ id: childPage.id,
477
481
  url: `${baseUrl}/wiki${childPage._links.webui}`,
478
482
  pageId: childPage.id,
479
483
  title: childPageTitle
@@ -609,6 +613,7 @@ export async function createByHierarchyNestedPages(
609
613
 
610
614
  const baseUrl = process.env.ATLASSIAN_URL || '';
611
615
  pages.push({
616
+ id: sectionPage.id,
612
617
  url: `${baseUrl}/wiki${sectionPage._links.webui}`,
613
618
  pageId: sectionPage.id,
614
619
  title: sectionPageTitle
@@ -756,6 +761,7 @@ export async function createManualPages(
756
761
 
757
762
  const baseUrl = process.env.ATLASSIAN_URL || '';
758
763
  pages.push({
764
+ id: page.id,
759
765
  url: `${baseUrl}/wiki${page._links.webui}`,
760
766
  pageId: page.id,
761
767
  title: pageTitle
@@ -0,0 +1,184 @@
1
+ /**
2
+ * プロジェクト検出ユーティリティ
3
+ * リポジトリルートを検出し、projects/配下のプロジェクトを検索
4
+ * 複数プロジェクトが存在する場合、選択機能を提供
5
+ */
6
+
7
+ import { existsSync, readdirSync } from 'fs';
8
+ import { resolve, join, dirname } from 'path';
9
+ import { readFileSync } from 'fs';
10
+ import type { ProjectMetadata } from './project-meta.js';
11
+
12
+ export interface ProjectLocation {
13
+ path: string;
14
+ projectId: string;
15
+ projectName: string;
16
+ jiraProjectKey: string;
17
+ }
18
+
19
+ /**
20
+ * 指定されたディレクトリに.kiro/project.jsonが存在するか確認
21
+ */
22
+ function hasProjectJson(dir: string): boolean {
23
+ const projectJsonPath = join(dir, '.kiro', 'project.json');
24
+ return existsSync(projectJsonPath);
25
+ }
26
+
27
+ /**
28
+ * プロジェクトメタデータを読み込む
29
+ */
30
+ function loadProjectMetadata(dir: string): ProjectMetadata | null {
31
+ const projectJsonPath = join(dir, '.kiro', 'project.json');
32
+
33
+ if (!existsSync(projectJsonPath)) {
34
+ return null;
35
+ }
36
+
37
+ try {
38
+ const content = readFileSync(projectJsonPath, 'utf-8');
39
+ const meta = JSON.parse(content) as ProjectMetadata;
40
+ return meta;
41
+ } catch (error) {
42
+ // パースエラーなどは無視
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 現在のディレクトリから親ディレクトリを遡って.kiro/project.jsonを検索
49
+ */
50
+ export function findCurrentProject(startDir: string = process.cwd()): ProjectLocation | null {
51
+ let currentDir = resolve(startDir);
52
+ const root = resolve('/');
53
+
54
+ while (currentDir !== root && currentDir !== dirname(currentDir)) {
55
+ if (hasProjectJson(currentDir)) {
56
+ const meta = loadProjectMetadata(currentDir);
57
+ if (meta) {
58
+ return {
59
+ path: currentDir,
60
+ projectId: meta.projectId,
61
+ projectName: meta.projectName,
62
+ jiraProjectKey: meta.jiraProjectKey
63
+ };
64
+ }
65
+ }
66
+
67
+ const parentDir = dirname(currentDir);
68
+ if (parentDir === currentDir) {
69
+ break;
70
+ }
71
+ currentDir = parentDir;
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * リポジトリルートを検出
79
+ * .gitディレクトリまたはprojects/ディレクトリの存在から判断
80
+ */
81
+ export function findRepositoryRoot(startDir: string = process.cwd()): string {
82
+ let currentDir = resolve(startDir);
83
+ const root = resolve('/');
84
+
85
+ while (currentDir !== root && currentDir !== dirname(currentDir)) {
86
+ // .gitディレクトリまたはprojects/ディレクトリが存在する場合、リポジトリルートと判断
87
+ if (existsSync(join(currentDir, '.git')) || existsSync(join(currentDir, 'projects'))) {
88
+ return currentDir;
89
+ }
90
+
91
+ const parentDir = dirname(currentDir);
92
+ if (parentDir === currentDir) {
93
+ break;
94
+ }
95
+ currentDir = parentDir;
96
+ }
97
+
98
+ // リポジトリルートが見つからない場合、現在のディレクトリを返す
99
+ return resolve(startDir);
100
+ }
101
+
102
+ /**
103
+ * リポジトリルートからprojects/配下の全プロジェクトを検索
104
+ * 統一されたディレクトリ構成: すべてのプロジェクトはprojects/{project-id}/配下に配置
105
+ */
106
+ export function findAllProjects(
107
+ searchDir?: string
108
+ ): ProjectLocation[] {
109
+ const projects: ProjectLocation[] = [];
110
+
111
+ // リポジトリルートを検出
112
+ const repoRoot = searchDir ? findRepositoryRoot(searchDir) : findRepositoryRoot();
113
+ const projectsDir = join(repoRoot, 'projects');
114
+
115
+ // projects/ディレクトリが存在しない場合は空配列を返す
116
+ if (!existsSync(projectsDir)) {
117
+ return projects;
118
+ }
119
+
120
+ try {
121
+ // projects/配下のディレクトリを取得
122
+ const entries = readdirSync(projectsDir, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ // 隠しディレクトリはスキップ
126
+ if (entry.name.startsWith('.')) {
127
+ continue;
128
+ }
129
+
130
+ if (entry.isDirectory()) {
131
+ const projectDir = join(projectsDir, entry.name);
132
+ // プロジェクトディレクトリに.kiro/project.jsonがあるか確認
133
+ if (hasProjectJson(projectDir)) {
134
+ const meta = loadProjectMetadata(projectDir);
135
+ if (meta) {
136
+ projects.push({
137
+ path: projectDir,
138
+ projectId: meta.projectId,
139
+ projectName: meta.projectName,
140
+ jiraProjectKey: meta.jiraProjectKey
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ } catch (error) {
147
+ // アクセス権限エラーなどは無視
148
+ }
149
+
150
+ return projects;
151
+ }
152
+
153
+ /**
154
+ * 複数プロジェクトから選択する(対話的)
155
+ */
156
+ export async function selectProject(
157
+ projects: ProjectLocation[],
158
+ question: (prompt: string) => Promise<string>
159
+ ): Promise<ProjectLocation | null> {
160
+ if (projects.length === 0) {
161
+ return null;
162
+ }
163
+
164
+ if (projects.length === 1) {
165
+ return projects[0];
166
+ }
167
+
168
+ console.log('\n📋 複数のプロジェクトが見つかりました:');
169
+ projects.forEach((project, index) => {
170
+ console.log(` ${index + 1}. ${project.projectName} (${project.projectId}) - ${project.path}`);
171
+ });
172
+
173
+ const answer = await question(`\n選択してください [1-${projects.length}]: `);
174
+ const index = parseInt(answer.trim(), 10) - 1;
175
+
176
+ if (index >= 0 && index < projects.length) {
177
+ return projects[index];
178
+ }
179
+
180
+ // 無効な入力の場合は最初のプロジェクトを返す
181
+ console.log('⚠️ 無効な選択です。最初のプロジェクトを使用します。');
182
+ return projects[0];
183
+ }
184
+
@@ -0,0 +1,212 @@
1
+ /**
2
+ * spec.json 更新ユーティリティ
3
+ * Confluence/JIRA 同期後に spec.json を更新する
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
+ import { resolve, dirname } from 'path';
8
+
9
+ /**
10
+ * spec.json の型定義
11
+ */
12
+ export interface SpecJson {
13
+ featureName?: string;
14
+ projectName?: string;
15
+ confluence?: {
16
+ spaceKey?: string;
17
+ requirements?: {
18
+ pageId?: string;
19
+ url?: string;
20
+ title?: string;
21
+ };
22
+ design?: {
23
+ pageId?: string;
24
+ url?: string;
25
+ title?: string;
26
+ };
27
+ tasks?: {
28
+ pageId?: string;
29
+ url?: string;
30
+ title?: string;
31
+ };
32
+ };
33
+ jira?: {
34
+ projectKey?: string;
35
+ epicKey?: string;
36
+ epicUrl?: string;
37
+ storyKeys?: string[];
38
+ };
39
+ milestones?: {
40
+ requirementsCompleted?: boolean;
41
+ designCompleted?: boolean;
42
+ tasksCompleted?: boolean;
43
+ jiraSyncCompleted?: boolean;
44
+ };
45
+ lastUpdated?: string;
46
+ }
47
+
48
+ /**
49
+ * spec.json を読み込む
50
+ */
51
+ export function loadSpecJson(featureName: string, projectRoot: string = process.cwd()): SpecJson {
52
+ const specPath = resolve(projectRoot, `.kiro/specs/${featureName}/spec.json`);
53
+
54
+ if (!existsSync(specPath)) {
55
+ // 新規作成する場合は最低限の構造を返す
56
+ return {
57
+ featureName,
58
+ confluence: {},
59
+ jira: {},
60
+ milestones: {},
61
+ };
62
+ }
63
+
64
+ try {
65
+ const content = readFileSync(specPath, 'utf-8');
66
+ return JSON.parse(content);
67
+ } catch (error) {
68
+ console.warn(`⚠️ Failed to load spec.json from ${specPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
69
+ return {
70
+ featureName,
71
+ confluence: {},
72
+ jira: {},
73
+ milestones: {},
74
+ };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * spec.json を保存する
80
+ * @param featureName 機能名
81
+ * @param spec 保存する spec オブジェクト(lastUpdated フィールドが更新されます)
82
+ * @param projectRoot プロジェクトルート(デフォルト: process.cwd())
83
+ */
84
+ export function saveSpecJson(featureName: string, spec: SpecJson, projectRoot: string = process.cwd()): void {
85
+ const specDir = resolve(projectRoot, `.kiro/specs/${featureName}`);
86
+ const specPath = resolve(specDir, 'spec.json');
87
+
88
+ // ディレクトリが存在しない場合は作成
89
+ if (!existsSync(specDir)) {
90
+ mkdirSync(specDir, { recursive: true });
91
+ }
92
+
93
+ // lastUpdated を更新
94
+ spec.lastUpdated = new Date().toISOString();
95
+
96
+ try {
97
+ writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf-8');
98
+ console.log(`✅ Updated spec.json: ${specPath}`);
99
+ } catch (error) {
100
+ console.error(`❌ Failed to save spec.json to ${specPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Confluence 同期後に spec.json を更新
107
+ */
108
+ export function updateSpecJsonAfterConfluenceSync(
109
+ featureName: string,
110
+ docType: 'requirements' | 'design' | 'tasks',
111
+ pageInfo: {
112
+ pageId: string;
113
+ url: string;
114
+ title: string;
115
+ spaceKey: string;
116
+ },
117
+ projectRoot: string = process.cwd()
118
+ ): void {
119
+ const spec = loadSpecJson(featureName, projectRoot);
120
+
121
+ // Confluence 情報を更新
122
+ if (!spec.confluence) {
123
+ spec.confluence = {};
124
+ }
125
+
126
+ // スペースキーを設定
127
+ if (pageInfo.spaceKey) {
128
+ spec.confluence.spaceKey = pageInfo.spaceKey;
129
+ }
130
+
131
+ // 新形式:ドキュメントタイプごとのページ情報を更新
132
+ spec.confluence[docType] = {
133
+ pageId: pageInfo.pageId,
134
+ url: pageInfo.url,
135
+ title: pageInfo.title,
136
+ };
137
+
138
+ // 旧形式(後方互換性のため併記)
139
+ if (docType === 'requirements') {
140
+ (spec.confluence as any).requirementsPageId = pageInfo.pageId;
141
+ (spec.confluence as any).requirementsUrl = pageInfo.url;
142
+ } else if (docType === 'design') {
143
+ (spec.confluence as any).designPageId = pageInfo.pageId;
144
+ (spec.confluence as any).designUrl = pageInfo.url;
145
+ } else if (docType === 'tasks') {
146
+ (spec.confluence as any).tasksPageId = pageInfo.pageId;
147
+ (spec.confluence as any).tasksUrl = pageInfo.url;
148
+ }
149
+
150
+ // マイルストーンを更新
151
+ if (!spec.milestones) {
152
+ spec.milestones = {};
153
+ }
154
+
155
+ // 新形式:フラットなフィールド
156
+ if (docType === 'requirements') {
157
+ spec.milestones.requirementsCompleted = true;
158
+ // 旧形式(後方互換性のため併記)
159
+ if (!(spec.milestones as any).requirements) {
160
+ (spec.milestones as any).requirements = {};
161
+ }
162
+ (spec.milestones as any).requirements.completed = true;
163
+ } else if (docType === 'design') {
164
+ spec.milestones.designCompleted = true;
165
+ // 旧形式(後方互換性のため併記)
166
+ if (!(spec.milestones as any).design) {
167
+ (spec.milestones as any).design = {};
168
+ }
169
+ (spec.milestones as any).design.completed = true;
170
+ } else if (docType === 'tasks') {
171
+ spec.milestones.tasksCompleted = true;
172
+ // 旧形式(後方互換性のため併記)
173
+ if (!(spec.milestones as any).tasks) {
174
+ (spec.milestones as any).tasks = {};
175
+ }
176
+ (spec.milestones as any).tasks.completed = true;
177
+ }
178
+
179
+ saveSpecJson(featureName, spec, projectRoot);
180
+ }
181
+
182
+ /**
183
+ * JIRA 同期後に spec.json を更新
184
+ */
185
+ export function updateSpecJsonAfterJiraSync(
186
+ featureName: string,
187
+ jiraInfo: {
188
+ projectKey: string;
189
+ epicKey: string;
190
+ epicUrl: string;
191
+ storyKeys: string[];
192
+ },
193
+ projectRoot: string = process.cwd()
194
+ ): void {
195
+ const spec = loadSpecJson(featureName, projectRoot);
196
+
197
+ // JIRA 情報を更新
198
+ spec.jira = {
199
+ projectKey: jiraInfo.projectKey,
200
+ epicKey: jiraInfo.epicKey,
201
+ epicUrl: jiraInfo.epicUrl,
202
+ storyKeys: jiraInfo.storyKeys,
203
+ };
204
+
205
+ // マイルストーンを更新
206
+ if (!spec.milestones) {
207
+ spec.milestones = {};
208
+ }
209
+ spec.milestones.jiraSyncCompleted = true;
210
+
211
+ saveSpecJson(featureName, spec, projectRoot);
212
+ }