@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.
- package/CHANGELOG.md +30 -0
- package/README.md +3 -2
- package/dist/scripts/__tests__/create-project.test.d.ts +2 -0
- package/dist/scripts/__tests__/create-project.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/create-project.test.js +247 -0
- package/dist/scripts/__tests__/create-project.test.js.map +1 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +2 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.js +119 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.js.map +1 -0
- package/dist/scripts/__tests__/setup-existing-project.test.d.ts +2 -0
- package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/setup-existing-project.test.js +67 -0
- package/dist/scripts/__tests__/setup-existing-project.test.js.map +1 -0
- package/dist/scripts/__tests__/setup-interactive.test.d.ts +2 -0
- package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/setup-interactive.test.js +160 -0
- package/dist/scripts/__tests__/setup-interactive.test.js.map +1 -0
- package/dist/scripts/config/default-config.json +57 -0
- package/dist/scripts/confluence-sync.d.ts +4 -0
- package/dist/scripts/confluence-sync.d.ts.map +1 -1
- package/dist/scripts/confluence-sync.js +12 -23
- package/dist/scripts/confluence-sync.js.map +1 -1
- package/dist/scripts/create-project.js +198 -137
- package/dist/scripts/create-project.js.map +1 -1
- package/dist/scripts/jira-sync.d.ts.map +1 -1
- package/dist/scripts/jira-sync.js +15 -0
- package/dist/scripts/jira-sync.js.map +1 -1
- package/dist/scripts/list-projects.d.ts.map +1 -1
- package/dist/scripts/list-projects.js +42 -15
- package/dist/scripts/list-projects.js.map +1 -1
- package/dist/scripts/multi-project-estimate.d.ts.map +1 -1
- package/dist/scripts/multi-project-estimate.js +56 -21
- package/dist/scripts/multi-project-estimate.js.map +1 -1
- package/dist/scripts/resource-dashboard.d.ts.map +1 -1
- package/dist/scripts/resource-dashboard.js +74 -17
- package/dist/scripts/resource-dashboard.js.map +1 -1
- package/dist/scripts/setup-existing-project.js +248 -214
- package/dist/scripts/setup-existing-project.js.map +1 -1
- package/dist/scripts/setup-interactive.d.ts +10 -0
- package/dist/scripts/setup-interactive.d.ts.map +1 -0
- package/dist/scripts/setup-interactive.js +413 -0
- package/dist/scripts/setup-interactive.js.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js +5 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
- package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.js +158 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.js.map +1 -0
- package/dist/scripts/utils/confluence-hierarchy.d.ts +2 -1
- package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.js +5 -0
- package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
- package/dist/scripts/utils/project-finder.d.ts +30 -0
- package/dist/scripts/utils/project-finder.d.ts.map +1 -0
- package/dist/scripts/utils/project-finder.js +147 -0
- package/dist/scripts/utils/project-finder.js.map +1 -0
- package/dist/scripts/utils/spec-updater.d.ts +72 -0
- package/dist/scripts/utils/spec-updater.d.ts.map +1 -0
- package/dist/scripts/utils/spec-updater.js +141 -0
- package/dist/scripts/utils/spec-updater.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +30 -7
- package/dist/src/cli.js.map +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +8 -6
- package/dist/vitest.config.js.map +1 -1
- package/docs/README.md +2 -2
- package/docs/contributing/development.md +37 -0
- package/docs/getting-started/{new-project-setup.md → new-repository-setup.md} +66 -19
- package/docs/getting-started/setup.md +305 -182
- package/docs/guides/customization.md +1 -1
- package/docs/guides/multi-project.md +11 -8
- package/docs/reference/quick-reference.md +2 -2
- package/docs/testing-strategy.md +87 -0
- package/package.json +17 -5
- package/scripts/__tests__/create-project.test.ts +292 -0
- package/scripts/__tests__/multi-project-estimate.test.ts +145 -0
- package/scripts/__tests__/setup-existing-project.test.ts +79 -0
- package/scripts/__tests__/setup-interactive.test.ts +199 -0
- package/scripts/confluence-sync.ts +17 -29
- package/scripts/copy-static-assets.js +50 -0
- package/scripts/create-project.ts +219 -156
- package/scripts/jira-sync.ts +16 -1
- package/scripts/list-projects.ts +51 -24
- package/scripts/multi-project-estimate.ts +58 -22
- package/scripts/resource-dashboard.ts +91 -26
- package/scripts/setup-existing-project.ts +264 -223
- package/scripts/setup-existing.sh +29 -22
- package/scripts/setup-interactive.ts +565 -0
- package/scripts/utils/__tests__/config-validator.test.ts +6 -0
- package/scripts/utils/__tests__/spec-updater.test.ts +220 -0
- package/scripts/utils/confluence-hierarchy.ts +7 -1
- package/scripts/utils/project-finder.ts +184 -0
- 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
|
+
}
|