@sk8metal/michi-cli 0.0.3 → 0.0.5
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 +38 -0
- package/README.md +25 -25
- package/dist/scripts/config/config-schema.d.ts +109 -600
- package/dist/scripts/config/config-schema.d.ts.map +1 -1
- package/dist/scripts/config/config-schema.js.map +1 -1
- package/dist/scripts/config-interactive.d.ts +1 -1
- package/dist/scripts/config-interactive.d.ts.map +1 -1
- package/dist/scripts/config-interactive.js +7 -7
- package/dist/scripts/config-interactive.js.map +1 -1
- package/dist/scripts/confluence-sync.js +5 -5
- package/dist/scripts/confluence-sync.js.map +1 -1
- package/dist/scripts/create-project.d.ts +2 -2
- package/dist/scripts/create-project.js +2 -2
- package/dist/scripts/create-project.js.map +1 -1
- package/dist/scripts/jira-sync.js +8 -8
- package/dist/scripts/jira-sync.js.map +1 -1
- package/dist/scripts/markdown-to-confluence.js +1 -1
- package/dist/scripts/markdown-to-confluence.js.map +1 -1
- package/dist/scripts/multi-project-estimate.js +1 -1
- package/dist/scripts/phase-runner.js.map +1 -1
- package/dist/scripts/pre-flight-check.js +1 -1
- package/dist/scripts/setup-existing-project.js.map +1 -1
- package/dist/scripts/utils/__tests__/config-loader.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/config-loader.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/config-loader.test.js +201 -0
- package/dist/scripts/utils/__tests__/config-loader.test.js.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js +29 -16
- package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
- package/dist/scripts/utils/config-loader.d.ts +4 -0
- package/dist/scripts/utils/config-loader.d.ts.map +1 -1
- package/dist/scripts/utils/config-loader.js +24 -2
- package/dist/scripts/utils/config-loader.js.map +1 -1
- package/dist/scripts/utils/config-validator.d.ts.map +1 -1
- package/dist/scripts/utils/config-validator.js +50 -51
- package/dist/scripts/utils/config-validator.js.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.js +7 -7
- package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
- package/dist/scripts/validate-phase.js.map +1 -1
- package/dist/scripts/workflow-orchestrator.js.map +1 -1
- package/dist/src/cli.js +2 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +8 -4
- package/dist/vitest.config.js.map +1 -1
- package/docs/config-reference.md +76 -197
- package/docs/customization-guide.md +61 -9
- package/docs/multi-project.md +157 -25
- package/docs/new-project-setup.md +36 -36
- package/docs/quick-reference.md +23 -20
- package/docs/release.md +365 -0
- package/docs/setup.md +5 -3
- package/env.example +3 -1
- package/package.json +15 -13
- package/scripts/config/config-schema.ts +5 -5
- package/scripts/config-interactive.ts +7 -6
- package/scripts/confluence-sync.ts +5 -5
- package/scripts/create-project.ts +21 -21
- package/scripts/jira-sync.ts +8 -8
- package/scripts/markdown-to-confluence.ts +1 -1
- package/scripts/multi-project-estimate.ts +1 -1
- package/scripts/phase-runner.ts +8 -8
- package/scripts/pre-flight-check.ts +1 -1
- package/scripts/setup-existing-project.ts +9 -9
- package/scripts/setup-existing.sh +1 -1
- package/scripts/utils/__tests__/config-loader.test.ts +254 -0
- package/scripts/utils/__tests__/config-validator.test.ts +32 -16
- package/scripts/utils/config-loader.ts +30 -2
- package/scripts/utils/config-validator.ts +51 -50
- package/scripts/utils/confluence-hierarchy.ts +51 -51
- package/scripts/validate-phase.ts +11 -11
- package/scripts/workflow-orchestrator.ts +27 -27
- package/docs/testing.md +0 -202
package/scripts/jira-sync.ts
CHANGED
|
@@ -23,7 +23,7 @@ import axios from 'axios';
|
|
|
23
23
|
import { config } from 'dotenv';
|
|
24
24
|
import { loadProjectMeta } from './utils/project-meta.js';
|
|
25
25
|
import { validateFeatureNameOrThrow } from './utils/feature-name-validator.js';
|
|
26
|
-
import { getConfig } from './utils/config-loader.js';
|
|
26
|
+
import { getConfig, getConfigPath } from './utils/config-loader.js';
|
|
27
27
|
import { validateForJiraSync } from './utils/config-validator.js';
|
|
28
28
|
|
|
29
29
|
config();
|
|
@@ -355,7 +355,7 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
355
355
|
if (validation.errors.length > 0) {
|
|
356
356
|
console.error('❌ Configuration errors:');
|
|
357
357
|
validation.errors.forEach(error => console.error(` ${error}`));
|
|
358
|
-
const configPath =
|
|
358
|
+
const configPath = getConfigPath();
|
|
359
359
|
console.error(`\n設定ファイル: ${configPath}`);
|
|
360
360
|
throw new Error('JIRA同期に必要な設定値が不足しています。上記のエラーを確認して設定を修正してください。');
|
|
361
361
|
}
|
|
@@ -370,7 +370,7 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
370
370
|
if (!storyIssueTypeId) {
|
|
371
371
|
throw new Error(
|
|
372
372
|
'JIRA Story issue type ID is not configured. ' +
|
|
373
|
-
'Please set JIRA_ISSUE_TYPE_STORY environment variable or configure it in .
|
|
373
|
+
'Please set JIRA_ISSUE_TYPE_STORY environment variable or configure it in .michi/config.json. ' +
|
|
374
374
|
'You can find the issue type ID in JIRA UI (Settings > Issues > Issue types) or via REST API: ' +
|
|
375
375
|
'GET https://your-domain.atlassian.net/rest/api/3/issuetype'
|
|
376
376
|
);
|
|
@@ -600,13 +600,13 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
600
600
|
|
|
601
601
|
// JIRA APIエラーの詳細を表示
|
|
602
602
|
if (error.response?.data) {
|
|
603
|
-
console.error(
|
|
603
|
+
console.error(' 📋 JIRA API Error Details:', JSON.stringify(error.response.data, null, 2));
|
|
604
604
|
|
|
605
605
|
// Story Pointsフィールドのエラーの場合、警告を表示
|
|
606
606
|
if (error.response.data.errors && Object.keys(error.response.data.errors).some(key => key.includes('customfield'))) {
|
|
607
|
-
console.error(
|
|
608
|
-
console.error(
|
|
609
|
-
console.error(
|
|
607
|
+
console.error(' ⚠️ Story Pointsフィールドの設定に失敗しました。');
|
|
608
|
+
console.error(' 💡 環境変数 JIRA_STORY_POINTS_FIELD を正しいカスタムフィールドIDに設定してください。');
|
|
609
|
+
console.error(' 💡 JIRA管理画面でStory PointsのカスタムフィールドIDを確認してください。');
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
612
|
|
|
@@ -618,7 +618,7 @@ async function syncTasksToJIRA(featureName: string): Promise<void> {
|
|
|
618
618
|
const newStoryCount = createdStories.filter(key => !existingStoryKeys.has(key)).length;
|
|
619
619
|
const reusedStoryCount = createdStories.filter(key => existingStoryKeys.has(key)).length;
|
|
620
620
|
|
|
621
|
-
console.log(
|
|
621
|
+
console.log('\n✅ JIRA sync completed');
|
|
622
622
|
console.log(` Epic: ${epic.key}`);
|
|
623
623
|
console.log(` Stories: ${createdStories.length} processed (${newStoryCount} new, ${reusedStoryCount} reused)`);
|
|
624
624
|
}
|
|
@@ -153,7 +153,7 @@ function parseEstimateFromContent(content: string, featureName: string): Estimat
|
|
|
153
153
|
|
|
154
154
|
return estimate;
|
|
155
155
|
} catch (error) {
|
|
156
|
-
console.warn(
|
|
156
|
+
console.warn(' ⚠️ Failed to parse estimate:', error instanceof Error ? error.message : error);
|
|
157
157
|
return null;
|
|
158
158
|
} finally {
|
|
159
159
|
// 一時ファイルをクリーンアップ
|
package/scripts/phase-runner.ts
CHANGED
|
@@ -256,14 +256,14 @@ export async function runPhase(feature: string, phase: Phase): Promise<PhaseRunR
|
|
|
256
256
|
validateFeatureNameOrThrow(feature);
|
|
257
257
|
|
|
258
258
|
switch (phase) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
case 'requirements':
|
|
260
|
+
return await runRequirementsPhase(feature);
|
|
261
|
+
case 'design':
|
|
262
|
+
return await runDesignPhase(feature);
|
|
263
|
+
case 'tasks':
|
|
264
|
+
return await runTasksPhase(feature);
|
|
265
|
+
default:
|
|
266
|
+
throw new Error(`Unknown phase: ${phase}`);
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -178,7 +178,7 @@ async function checkJiraProject(projectKey: string): Promise<{ errors: string[],
|
|
|
178
178
|
errors.push(` 現在の設定: "${projectKey}" → 実際に存在するキーに変更`);
|
|
179
179
|
} else if (error.response?.status === 401) {
|
|
180
180
|
errors.push('❌ JIRA認証エラー(.envの認証情報を確認)');
|
|
181
|
-
errors.push(
|
|
181
|
+
errors.push(' → API Token管理: https://id.atlassian.com/manage-profile/security/api-tokens');
|
|
182
182
|
} else {
|
|
183
183
|
warnings.push(`⚠️ JIRAプロジェクトチェック失敗: ${error.message}`);
|
|
184
184
|
}
|
|
@@ -30,15 +30,15 @@ function parseArgs(): SetupConfig {
|
|
|
30
30
|
const value = args[i + 1];
|
|
31
31
|
|
|
32
32
|
switch (key) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
case 'michi-path':
|
|
34
|
+
config.michiPath = value;
|
|
35
|
+
break;
|
|
36
|
+
case 'project-name':
|
|
37
|
+
config.projectName = value;
|
|
38
|
+
break;
|
|
39
|
+
case 'jira-key':
|
|
40
|
+
config.jiraKey = value;
|
|
41
|
+
break;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-loader.ts のユニットテスト
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, rmSync } from 'fs';
|
|
7
|
+
import { resolve, join } from 'path';
|
|
8
|
+
import { tmpdir } from 'os';
|
|
9
|
+
import {
|
|
10
|
+
loadConfig,
|
|
11
|
+
getConfig,
|
|
12
|
+
getConfigPath,
|
|
13
|
+
clearConfigCache
|
|
14
|
+
} from '../config-loader.js';
|
|
15
|
+
|
|
16
|
+
describe('config-loader', () => {
|
|
17
|
+
let testProjectRoot: string;
|
|
18
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// テスト用の一時ディレクトリを作成(ランダム要素を追加して衝突を防ぐ)
|
|
22
|
+
testProjectRoot = resolve(tmpdir(), `michi-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
23
|
+
|
|
24
|
+
// 既存のディレクトリがあれば削除
|
|
25
|
+
if (existsSync(testProjectRoot)) {
|
|
26
|
+
rmSync(testProjectRoot, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mkdirSync(testProjectRoot, { recursive: true });
|
|
30
|
+
mkdirSync(join(testProjectRoot, '.michi'), { recursive: true });
|
|
31
|
+
|
|
32
|
+
// 環境変数をバックアップ
|
|
33
|
+
originalEnv = { ...process.env };
|
|
34
|
+
|
|
35
|
+
// キャッシュをクリア(クリーンな状態から開始)
|
|
36
|
+
clearConfigCache();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
// 環境変数を復元
|
|
41
|
+
process.env = originalEnv;
|
|
42
|
+
|
|
43
|
+
// テスト用ディレクトリを削除
|
|
44
|
+
if (existsSync(testProjectRoot)) {
|
|
45
|
+
// ファイルを削除
|
|
46
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
47
|
+
if (existsSync(configPath)) {
|
|
48
|
+
unlinkSync(configPath);
|
|
49
|
+
}
|
|
50
|
+
// ディレクトリを削除
|
|
51
|
+
try {
|
|
52
|
+
rmSync(testProjectRoot, { recursive: true, force: true });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// 削除失敗は無視
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// キャッシュをクリア(ディレクトリ削除後に実行)
|
|
59
|
+
clearConfigCache();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getConfigPath', () => {
|
|
63
|
+
it('.michi/config.jsonのパスを返す', () => {
|
|
64
|
+
clearConfigCache();
|
|
65
|
+
const configPath = getConfigPath(testProjectRoot);
|
|
66
|
+
expect(configPath).toBe(join(testProjectRoot, '.michi/config.json'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('設定ファイルが存在しない場合でも.michi/config.jsonのパスを返す', () => {
|
|
70
|
+
clearConfigCache();
|
|
71
|
+
const configPath = getConfigPath(testProjectRoot);
|
|
72
|
+
expect(configPath).toBe(join(testProjectRoot, '.michi/config.json'));
|
|
73
|
+
expect(existsSync(configPath)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('loadConfig', () => {
|
|
78
|
+
it('設定ファイルが存在しない場合はデフォルト設定を返す', () => {
|
|
79
|
+
clearConfigCache();
|
|
80
|
+
const config = loadConfig(testProjectRoot);
|
|
81
|
+
|
|
82
|
+
expect(config).toBeDefined();
|
|
83
|
+
expect(config.confluence).toBeDefined();
|
|
84
|
+
expect(config.confluence?.pageCreationGranularity).toBe('single');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('設定ファイルが存在する場合はマージされた設定を返す', () => {
|
|
88
|
+
clearConfigCache();
|
|
89
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
90
|
+
writeFileSync(configPath, JSON.stringify({
|
|
91
|
+
confluence: {
|
|
92
|
+
pageCreationGranularity: 'by-hierarchy',
|
|
93
|
+
spaces: {
|
|
94
|
+
requirements: 'TestSpace'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const config = loadConfig(testProjectRoot);
|
|
100
|
+
|
|
101
|
+
expect(config.confluence?.pageCreationGranularity).toBe('by-hierarchy');
|
|
102
|
+
expect(config.confluence?.spaces?.requirements).toBe('TestSpace');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('無効なJSONの場合はエラーをスロー', () => {
|
|
106
|
+
clearConfigCache(); // キャッシュをクリア
|
|
107
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
108
|
+
writeFileSync(configPath, '{ invalid json }');
|
|
109
|
+
|
|
110
|
+
expect(() => {
|
|
111
|
+
loadConfig(testProjectRoot);
|
|
112
|
+
}).toThrow(/Invalid JSON/);
|
|
113
|
+
|
|
114
|
+
// テスト後にファイルを削除して次のテストへの影響を防ぐ
|
|
115
|
+
if (existsSync(configPath)) {
|
|
116
|
+
unlinkSync(configPath);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('getConfig (キャッシュ付き)', () => {
|
|
122
|
+
// キャッシュテストでは、beforeEachでキャッシュをクリアしない
|
|
123
|
+
// 代わりに、各テストの最後にキャッシュをクリアする
|
|
124
|
+
|
|
125
|
+
it('設定ファイルが存在しない場合はデフォルト設定を返す', () => {
|
|
126
|
+
clearConfigCache(); // テスト開始時にクリア
|
|
127
|
+
const config = getConfig(testProjectRoot);
|
|
128
|
+
|
|
129
|
+
expect(config).toBeDefined();
|
|
130
|
+
expect(config.confluence).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('同じ設定ファイルを2回読み込む場合はキャッシュが使用される', () => {
|
|
134
|
+
clearConfigCache(); // テスト開始時にクリア
|
|
135
|
+
|
|
136
|
+
// .kiro/が存在しないことを確認(legacy警告を防ぐ)
|
|
137
|
+
const legacyDir = join(testProjectRoot, '.kiro');
|
|
138
|
+
if (existsSync(legacyDir)) {
|
|
139
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
143
|
+
writeFileSync(configPath, JSON.stringify({
|
|
144
|
+
confluence: {
|
|
145
|
+
pageCreationGranularity: 'by-hierarchy'
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
const config1 = getConfig(testProjectRoot);
|
|
150
|
+
const config2 = getConfig(testProjectRoot);
|
|
151
|
+
|
|
152
|
+
// 同じオブジェクト参照であることを確認(キャッシュが使用されている)
|
|
153
|
+
expect(config1).toBe(config2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('設定ファイルを変更するとキャッシュが無効化される', async () => {
|
|
157
|
+
clearConfigCache(); // テスト開始時にクリア
|
|
158
|
+
|
|
159
|
+
// .michiディレクトリが存在することを確認(前のテストで削除されている可能性)
|
|
160
|
+
const michiDir = join(testProjectRoot, '.michi');
|
|
161
|
+
if (!existsSync(michiDir)) {
|
|
162
|
+
mkdirSync(michiDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
166
|
+
writeFileSync(configPath, JSON.stringify({
|
|
167
|
+
confluence: {
|
|
168
|
+
pageCreationGranularity: 'single'
|
|
169
|
+
}
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const config1 = getConfig(testProjectRoot);
|
|
173
|
+
expect(config1.confluence?.pageCreationGranularity).toBe('single');
|
|
174
|
+
|
|
175
|
+
// ファイルシステムのmtime精度を考慮して少し待つ
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
177
|
+
|
|
178
|
+
// 設定ファイルを更新(ディレクトリが削除されている可能性があるため再確認)
|
|
179
|
+
if (!existsSync(michiDir)) {
|
|
180
|
+
mkdirSync(michiDir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
writeFileSync(configPath, JSON.stringify({
|
|
184
|
+
confluence: {
|
|
185
|
+
pageCreationGranularity: 'by-hierarchy'
|
|
186
|
+
}
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
const config2 = getConfig(testProjectRoot);
|
|
190
|
+
expect(config2.confluence?.pageCreationGranularity).toBe('by-hierarchy');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('警告メッセージ', () => {
|
|
195
|
+
it('legacyパス(.kiro/config.json)が存在する場合は警告を表示', () => {
|
|
196
|
+
// .michi/config.jsonが存在しないことを確認(警告が表示される条件)
|
|
197
|
+
const michiConfigPath = join(testProjectRoot, '.michi/config.json');
|
|
198
|
+
const michiDir = join(testProjectRoot, '.michi');
|
|
199
|
+
|
|
200
|
+
// .michi/config.jsonとディレクトリを削除
|
|
201
|
+
if (existsSync(michiConfigPath)) {
|
|
202
|
+
unlinkSync(michiConfigPath);
|
|
203
|
+
}
|
|
204
|
+
if (existsSync(michiDir)) {
|
|
205
|
+
rmSync(michiDir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// legacyパスにファイルを作成
|
|
209
|
+
mkdirSync(join(testProjectRoot, '.kiro'), { recursive: true });
|
|
210
|
+
const legacyConfigPath = join(testProjectRoot, '.kiro/config.json');
|
|
211
|
+
writeFileSync(legacyConfigPath, JSON.stringify({
|
|
212
|
+
confluence: {
|
|
213
|
+
pageCreationGranularity: 'single'
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
// 警告が表示されることを確認(console.warnをモック)
|
|
218
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
219
|
+
|
|
220
|
+
// loadConfigを呼ぶことでresolveConfigPathが実行され、警告が表示される
|
|
221
|
+
loadConfig(testProjectRoot);
|
|
222
|
+
|
|
223
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
224
|
+
expect(consoleWarnSpy.mock.calls[0][0]).toContain('Deprecated');
|
|
225
|
+
expect(consoleWarnSpy.mock.calls[0][0]).toContain('.kiro/config.json');
|
|
226
|
+
|
|
227
|
+
consoleWarnSpy.mockRestore();
|
|
228
|
+
|
|
229
|
+
// 次のテストのために.michiディレクトリを再作成
|
|
230
|
+
mkdirSync(michiDir, { recursive: true });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('legacyパスと新規パスの両方が存在する場合は警告を表示しない', () => {
|
|
234
|
+
// 両方のパスにファイルを作成
|
|
235
|
+
mkdirSync(join(testProjectRoot, '.kiro'), { recursive: true });
|
|
236
|
+
const legacyConfigPath = join(testProjectRoot, '.kiro/config.json');
|
|
237
|
+
const michiConfigPath = join(testProjectRoot, '.michi/config.json');
|
|
238
|
+
writeFileSync(legacyConfigPath, JSON.stringify({}));
|
|
239
|
+
writeFileSync(michiConfigPath, JSON.stringify({}));
|
|
240
|
+
|
|
241
|
+
// 警告が表示されないことを確認
|
|
242
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
243
|
+
|
|
244
|
+
// loadConfigを呼ぶことでresolveConfigPathが実行される
|
|
245
|
+
loadConfig(testProjectRoot);
|
|
246
|
+
|
|
247
|
+
// 警告は表示されない(新規パスが存在するため)
|
|
248
|
+
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
249
|
+
|
|
250
|
+
consoleWarnSpy.mockRestore();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
validateForJiraSync,
|
|
13
13
|
validateAndReport
|
|
14
14
|
} from '../config-validator.js';
|
|
15
|
+
import { clearConfigCache } from '../config-loader.js';
|
|
15
16
|
|
|
16
17
|
describe('config-validator', () => {
|
|
17
18
|
let testProjectRoot: string;
|
|
@@ -21,10 +22,13 @@ describe('config-validator', () => {
|
|
|
21
22
|
// テスト用の一時ディレクトリを作成
|
|
22
23
|
testProjectRoot = resolve(tmpdir(), `michi-test-${Date.now()}`);
|
|
23
24
|
mkdirSync(testProjectRoot, { recursive: true });
|
|
24
|
-
mkdirSync(join(testProjectRoot, '.
|
|
25
|
+
mkdirSync(join(testProjectRoot, '.michi'), { recursive: true });
|
|
25
26
|
|
|
26
27
|
// 環境変数をバックアップ
|
|
27
28
|
originalEnv = { ...process.env };
|
|
29
|
+
|
|
30
|
+
// キャッシュをクリア
|
|
31
|
+
clearConfigCache();
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
afterEach(() => {
|
|
@@ -34,7 +38,7 @@ describe('config-validator', () => {
|
|
|
34
38
|
// テスト用ディレクトリを削除
|
|
35
39
|
if (existsSync(testProjectRoot)) {
|
|
36
40
|
// ファイルを削除
|
|
37
|
-
const configPath = join(testProjectRoot, '.
|
|
41
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
38
42
|
if (existsSync(configPath)) {
|
|
39
43
|
unlinkSync(configPath);
|
|
40
44
|
}
|
|
@@ -49,6 +53,12 @@ describe('config-validator', () => {
|
|
|
49
53
|
|
|
50
54
|
describe('validateProjectConfig', () => {
|
|
51
55
|
it('設定ファイルが存在しない場合は情報メッセージを返す', () => {
|
|
56
|
+
// .michi/config.jsonが存在しないことを確認
|
|
57
|
+
const michiConfigPath = join(testProjectRoot, '.michi/config.json');
|
|
58
|
+
if (existsSync(michiConfigPath)) {
|
|
59
|
+
unlinkSync(michiConfigPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
const result = validateProjectConfig(testProjectRoot);
|
|
53
63
|
|
|
54
64
|
expect(result.valid).toBe(true);
|
|
@@ -59,7 +69,13 @@ describe('config-validator', () => {
|
|
|
59
69
|
});
|
|
60
70
|
|
|
61
71
|
it('有効な設定ファイルの場合は成功', () => {
|
|
62
|
-
|
|
72
|
+
// .michiディレクトリが存在することを確認
|
|
73
|
+
const michiDir = join(testProjectRoot, '.michi');
|
|
74
|
+
if (!existsSync(michiDir)) {
|
|
75
|
+
mkdirSync(michiDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
63
79
|
writeFileSync(configPath, JSON.stringify({
|
|
64
80
|
confluence: {
|
|
65
81
|
pageCreationGranularity: 'single',
|
|
@@ -76,7 +92,7 @@ describe('config-validator', () => {
|
|
|
76
92
|
});
|
|
77
93
|
|
|
78
94
|
it('無効なJSONの場合はエラーを返す', () => {
|
|
79
|
-
const configPath = join(testProjectRoot, '.
|
|
95
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
80
96
|
writeFileSync(configPath, '{ invalid json }');
|
|
81
97
|
|
|
82
98
|
const result = validateProjectConfig(testProjectRoot);
|
|
@@ -87,7 +103,7 @@ describe('config-validator', () => {
|
|
|
87
103
|
});
|
|
88
104
|
|
|
89
105
|
it('by-hierarchyモードでhierarchy設定がない場合はエラー', () => {
|
|
90
|
-
const configPath = join(testProjectRoot, '.
|
|
106
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
91
107
|
writeFileSync(configPath, JSON.stringify({
|
|
92
108
|
confluence: {
|
|
93
109
|
pageCreationGranularity: 'by-hierarchy'
|
|
@@ -102,7 +118,7 @@ describe('config-validator', () => {
|
|
|
102
118
|
});
|
|
103
119
|
|
|
104
120
|
it('selected-phasesモードでselectedPhases設定がない場合はエラー', () => {
|
|
105
|
-
const configPath = join(testProjectRoot, '.
|
|
121
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
106
122
|
writeFileSync(configPath, JSON.stringify({
|
|
107
123
|
jira: {
|
|
108
124
|
storyCreationGranularity: 'selected-phases'
|
|
@@ -122,7 +138,7 @@ describe('config-validator', () => {
|
|
|
122
138
|
// 環境変数をクリア
|
|
123
139
|
delete process.env.CONFLUENCE_PRD_SPACE;
|
|
124
140
|
|
|
125
|
-
const configPath = join(testProjectRoot, '.
|
|
141
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
126
142
|
writeFileSync(configPath, JSON.stringify({
|
|
127
143
|
confluence: {
|
|
128
144
|
pageCreationGranularity: 'single'
|
|
@@ -140,7 +156,7 @@ describe('config-validator', () => {
|
|
|
140
156
|
});
|
|
141
157
|
|
|
142
158
|
it('spaces設定がある場合は成功', () => {
|
|
143
|
-
const configPath = join(testProjectRoot, '.
|
|
159
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
144
160
|
writeFileSync(configPath, JSON.stringify({
|
|
145
161
|
confluence: {
|
|
146
162
|
spaces: {
|
|
@@ -158,7 +174,7 @@ describe('config-validator', () => {
|
|
|
158
174
|
it('by-hierarchyモードでhierarchy設定がない場合はエラー', () => {
|
|
159
175
|
// デフォルト設定を上書きするため、hierarchyキーを削除
|
|
160
176
|
// デフォルト設定にhierarchyがあるため、実際にはエラーにならない可能性がある
|
|
161
|
-
const configPath = join(testProjectRoot, '.
|
|
177
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
162
178
|
writeFileSync(configPath, JSON.stringify({
|
|
163
179
|
confluence: {
|
|
164
180
|
pageCreationGranularity: 'by-hierarchy',
|
|
@@ -177,7 +193,7 @@ describe('config-validator', () => {
|
|
|
177
193
|
});
|
|
178
194
|
|
|
179
195
|
it('manualモードでstructure設定がない場合はエラー', () => {
|
|
180
|
-
const configPath = join(testProjectRoot, '.
|
|
196
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
181
197
|
writeFileSync(configPath, JSON.stringify({
|
|
182
198
|
confluence: {
|
|
183
199
|
pageCreationGranularity: 'manual',
|
|
@@ -196,7 +212,7 @@ describe('config-validator', () => {
|
|
|
196
212
|
|
|
197
213
|
it('環境変数CONFLUENCE_PRD_SPACEがある場合は情報メッセージ', () => {
|
|
198
214
|
process.env.CONFLUENCE_PRD_SPACE = 'Michi';
|
|
199
|
-
const configPath = join(testProjectRoot, '.
|
|
215
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
200
216
|
writeFileSync(configPath, JSON.stringify({
|
|
201
217
|
confluence: {
|
|
202
218
|
pageCreationGranularity: 'single'
|
|
@@ -216,7 +232,7 @@ describe('config-validator', () => {
|
|
|
216
232
|
|
|
217
233
|
describe('validateForJiraSync', () => {
|
|
218
234
|
it('issueTypes.story設定がない場合はエラー', () => {
|
|
219
|
-
const configPath = join(testProjectRoot, '.
|
|
235
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
220
236
|
writeFileSync(configPath, JSON.stringify({
|
|
221
237
|
jira: {}
|
|
222
238
|
}));
|
|
@@ -229,7 +245,7 @@ describe('config-validator', () => {
|
|
|
229
245
|
});
|
|
230
246
|
|
|
231
247
|
it('issueTypes.story設定がある場合は成功', () => {
|
|
232
|
-
const configPath = join(testProjectRoot, '.
|
|
248
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
233
249
|
writeFileSync(configPath, JSON.stringify({
|
|
234
250
|
jira: {
|
|
235
251
|
issueTypes: {
|
|
@@ -246,7 +262,7 @@ describe('config-validator', () => {
|
|
|
246
262
|
|
|
247
263
|
it('環境変数JIRA_ISSUE_TYPE_STORYがある場合は情報メッセージ', () => {
|
|
248
264
|
process.env.JIRA_ISSUE_TYPE_STORY = '10036';
|
|
249
|
-
const configPath = join(testProjectRoot, '.
|
|
265
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
250
266
|
writeFileSync(configPath, JSON.stringify({
|
|
251
267
|
jira: {
|
|
252
268
|
createEpic: true
|
|
@@ -264,7 +280,7 @@ describe('config-validator', () => {
|
|
|
264
280
|
});
|
|
265
281
|
|
|
266
282
|
it('issueTypes.subtask設定がない場合は警告', () => {
|
|
267
|
-
const configPath = join(testProjectRoot, '.
|
|
283
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
268
284
|
writeFileSync(configPath, JSON.stringify({
|
|
269
285
|
jira: {
|
|
270
286
|
issueTypes: {
|
|
@@ -281,7 +297,7 @@ describe('config-validator', () => {
|
|
|
281
297
|
});
|
|
282
298
|
|
|
283
299
|
it('selected-phasesモードでselectedPhases設定がない場合はエラー', () => {
|
|
284
|
-
const configPath = join(testProjectRoot, '.
|
|
300
|
+
const configPath = join(testProjectRoot, '.michi/config.json');
|
|
285
301
|
writeFileSync(configPath, JSON.stringify({
|
|
286
302
|
jira: {
|
|
287
303
|
storyCreationGranularity: 'selected-phases',
|
|
@@ -140,11 +140,32 @@ function validateConfigPath(configPath: string, projectRoot: string): boolean {
|
|
|
140
140
|
return !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 設定ファイルのパスを解決
|
|
145
|
+
* 新規パス: .michi/config.json
|
|
146
|
+
* legacyパス(.kiro/config.json)が存在する場合は警告のみ表示
|
|
147
|
+
*/
|
|
148
|
+
function resolveConfigPath(projectRoot: string): string {
|
|
149
|
+
const michiConfigPath = resolve(projectRoot, '.michi/config.json');
|
|
150
|
+
const legacyConfigPath = resolve(projectRoot, '.kiro/config.json');
|
|
151
|
+
|
|
152
|
+
// legacyパスが存在する場合は警告(移行推奨)
|
|
153
|
+
if (existsSync(legacyConfigPath) && !existsSync(michiConfigPath)) {
|
|
154
|
+
console.warn(
|
|
155
|
+
'⚠️ Deprecated: .kiro/config.json is deprecated.\n' +
|
|
156
|
+
' Please migrate to .michi/config.json\n' +
|
|
157
|
+
' The legacy path will not be supported in future versions.\n'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return michiConfigPath;
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
/**
|
|
144
165
|
* プロジェクト固有設定を読み込む
|
|
145
166
|
*/
|
|
146
167
|
function loadProjectConfig(projectRoot: string = process.cwd()): Partial<AppConfig> | null {
|
|
147
|
-
const projectConfigPath =
|
|
168
|
+
const projectConfigPath = resolveConfigPath(projectRoot);
|
|
148
169
|
|
|
149
170
|
// パストラバーサル対策: パスを検証
|
|
150
171
|
if (!validateConfigPath(projectConfigPath, projectRoot)) {
|
|
@@ -247,7 +268,7 @@ let cachedConfigMtime: number | null = null;
|
|
|
247
268
|
let cachedDefaultConfigMtime: number | null = null;
|
|
248
269
|
|
|
249
270
|
export function getConfig(projectRoot: string = process.cwd()): AppConfig {
|
|
250
|
-
const projectConfigPath =
|
|
271
|
+
const projectConfigPath = resolveConfigPath(projectRoot);
|
|
251
272
|
const currentFileUrl = import.meta.url;
|
|
252
273
|
const currentFilePath = fileURLToPath(currentFileUrl);
|
|
253
274
|
const currentDir = resolve(currentFilePath, '..');
|
|
@@ -324,3 +345,10 @@ export function clearConfigCache(): void {
|
|
|
324
345
|
cachedDefaultConfigMtime = null;
|
|
325
346
|
}
|
|
326
347
|
|
|
348
|
+
/**
|
|
349
|
+
* 設定ファイルのパスを解決(外部から使用可能)
|
|
350
|
+
*/
|
|
351
|
+
export function getConfigPath(projectRoot: string = process.cwd()): string {
|
|
352
|
+
return resolveConfigPath(projectRoot);
|
|
353
|
+
}
|
|
354
|
+
|