@sk8metal/michi-cli 0.0.1 → 0.0.2
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 +24 -0
- package/README.md +24 -24
- package/dist/scripts/__tests__/validate-phase.test.d.ts +5 -0
- package/dist/scripts/__tests__/validate-phase.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/validate-phase.test.js +162 -0
- package/dist/scripts/__tests__/validate-phase.test.js.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js +247 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.js +106 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +1 -0
- package/dist/src/__tests__/cli.test.d.ts +5 -0
- package/dist/src/__tests__/cli.test.d.ts.map +1 -0
- package/dist/src/__tests__/cli.test.js +58 -0
- package/dist/src/__tests__/cli.test.js.map +1 -0
- package/docs/setup.md +1 -1
- package/package.json +5 -3
- package/scripts/__tests__/README.md +101 -0
- package/scripts/__tests__/validate-phase.test.ts +185 -0
- package/scripts/config/config-schema.ts +130 -0
- package/scripts/config/default-config.json +57 -0
- package/scripts/config-interactive.ts +494 -0
- package/scripts/confluence-sync.ts +503 -0
- package/scripts/create-project.ts +293 -0
- package/scripts/jira-sync.ts +644 -0
- package/scripts/list-projects.ts +85 -0
- package/scripts/markdown-to-confluence.ts +161 -0
- package/scripts/multi-project-estimate.ts +255 -0
- package/scripts/phase-runner.ts +303 -0
- package/scripts/pr-automation.ts +67 -0
- package/scripts/pre-flight-check.ts +285 -0
- package/scripts/resource-dashboard.ts +124 -0
- package/scripts/setup-env.sh +52 -0
- package/scripts/setup-existing-project.ts +381 -0
- package/scripts/setup-existing.sh +145 -0
- package/scripts/utils/__tests__/config-validator.test.ts +302 -0
- package/scripts/utils/__tests__/feature-name-validator.test.ts +129 -0
- package/scripts/utils/config-loader.ts +326 -0
- package/scripts/utils/config-validator.ts +347 -0
- package/scripts/utils/confluence-hierarchy.ts +854 -0
- package/scripts/utils/feature-name-validator.ts +135 -0
- package/scripts/utils/project-meta.ts +69 -0
- package/scripts/validate-phase.ts +279 -0
- package/scripts/workflow-orchestrator.ts +178 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 設定読み込み・マージ機能
|
|
3
|
+
* デフォルト設定 + プロジェクト固有設定をマージ
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
7
|
+
import { resolve, relative, isAbsolute } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { config } from 'dotenv';
|
|
10
|
+
import { AppConfigSchema, type AppConfig } from '../config/config-schema.js';
|
|
11
|
+
|
|
12
|
+
// 環境変数読み込み
|
|
13
|
+
config();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 深いマージ(Deep Merge)
|
|
17
|
+
* オブジェクトを再帰的にマージする
|
|
18
|
+
*/
|
|
19
|
+
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
20
|
+
const result = { ...target };
|
|
21
|
+
|
|
22
|
+
for (const key in source) {
|
|
23
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
24
|
+
result[key] = deepMerge(result[key] || {} as T[Extract<keyof T, string>], source[key] as Partial<T[Extract<keyof T, string>]>);
|
|
25
|
+
} else if (source[key] !== undefined) {
|
|
26
|
+
result[key] = source[key] as T[Extract<keyof T, string>];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 許可された環境変数のリスト
|
|
35
|
+
* セキュリティのため、設定ファイルで展開可能な環境変数を制限
|
|
36
|
+
*/
|
|
37
|
+
const ALLOWED_ENV_VARS = [
|
|
38
|
+
'CONFLUENCE_PRD_SPACE',
|
|
39
|
+
'CONFLUENCE_QA_SPACE',
|
|
40
|
+
'CONFLUENCE_RELEASE_SPACE'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 環境変数を文字列に展開
|
|
45
|
+
* ${VAR_NAME} 形式のプレースホルダーを環境変数の値に置換
|
|
46
|
+
* セキュリティのため、許可リストに含まれる環境変数のみ展開
|
|
47
|
+
*/
|
|
48
|
+
function expandEnvVars(str: string): string {
|
|
49
|
+
return str.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
50
|
+
if (ALLOWED_ENV_VARS.includes(varName)) {
|
|
51
|
+
return process.env[varName] || match;
|
|
52
|
+
}
|
|
53
|
+
// 許可されていない環境変数は警告を出して展開しない
|
|
54
|
+
console.warn(`⚠️ Environment variable "${varName}" is not allowed in config. Skipping expansion.`);
|
|
55
|
+
return match;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 設定オブジェクト内の文字列値を環境変数で展開
|
|
61
|
+
* 循環参照を防ぐため、処理済みオブジェクトを追跡
|
|
62
|
+
*/
|
|
63
|
+
function expandEnvVarsInConfig(config: any, visited: WeakSet<object> = new WeakSet()): any {
|
|
64
|
+
if (typeof config === 'string') {
|
|
65
|
+
return expandEnvVars(config);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(config)) {
|
|
69
|
+
return config.map(item => expandEnvVarsInConfig(item, visited));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (config && typeof config === 'object') {
|
|
73
|
+
// 循環参照のチェック
|
|
74
|
+
if (visited.has(config)) {
|
|
75
|
+
console.warn('⚠️ Circular reference detected in config. Skipping expansion.');
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visited.add(config);
|
|
80
|
+
const result: any = {};
|
|
81
|
+
for (const key in config) {
|
|
82
|
+
result[key] = expandEnvVarsInConfig(config[key], visited);
|
|
83
|
+
}
|
|
84
|
+
visited.delete(config);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* デフォルト設定を読み込む
|
|
93
|
+
*/
|
|
94
|
+
function loadDefaultConfig(): AppConfig {
|
|
95
|
+
// import.meta.urlからディレクトリパスを取得
|
|
96
|
+
const currentFileUrl = import.meta.url;
|
|
97
|
+
const currentFilePath = fileURLToPath(currentFileUrl);
|
|
98
|
+
const currentDir = resolve(currentFilePath, '..');
|
|
99
|
+
const defaultConfigPath = resolve(currentDir, '../config/default-config.json');
|
|
100
|
+
|
|
101
|
+
if (!existsSync(defaultConfigPath)) {
|
|
102
|
+
throw new Error(`Default config file not found: ${defaultConfigPath}\nPlease ensure the file exists in the scripts/config directory.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = readFileSync(defaultConfigPath, 'utf-8');
|
|
107
|
+
const parsed = JSON.parse(content);
|
|
108
|
+
|
|
109
|
+
// 環境変数を展開
|
|
110
|
+
const expanded = expandEnvVarsInConfig(parsed);
|
|
111
|
+
|
|
112
|
+
// スキーマでバリデーション
|
|
113
|
+
return AppConfigSchema.parse(expanded);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof SyntaxError) {
|
|
116
|
+
throw new Error(`Invalid JSON in default config file ${defaultConfigPath}: ${error.message}\nLine: ${(error as any).line}, Column: ${(error as any).column}`);
|
|
117
|
+
}
|
|
118
|
+
if (error instanceof Error && error.name === 'ZodError') {
|
|
119
|
+
throw new Error(`Default config validation failed: ${error.message}\nFile: ${defaultConfigPath}`);
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Failed to load default config from ${defaultConfigPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* パストラバーサル攻撃を防ぐため、パスを検証
|
|
127
|
+
*/
|
|
128
|
+
function validateConfigPath(configPath: string, projectRoot: string): boolean {
|
|
129
|
+
const resolvedPath = resolve(configPath);
|
|
130
|
+
const resolvedRoot = resolve(projectRoot);
|
|
131
|
+
const relativePath = relative(resolvedRoot, resolvedPath);
|
|
132
|
+
|
|
133
|
+
// プロジェクトルート自体の場合は許可
|
|
134
|
+
if (!relativePath) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 相対パスが '..' で始まる、または絶対パスの場合は拒否
|
|
139
|
+
// これにより、プロジェクトルート外のパスを防ぐ
|
|
140
|
+
return !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* プロジェクト固有設定を読み込む
|
|
145
|
+
*/
|
|
146
|
+
function loadProjectConfig(projectRoot: string = process.cwd()): Partial<AppConfig> | null {
|
|
147
|
+
const projectConfigPath = resolve(projectRoot, '.kiro/config.json');
|
|
148
|
+
|
|
149
|
+
// パストラバーサル対策: パスを検証
|
|
150
|
+
if (!validateConfigPath(projectConfigPath, projectRoot)) {
|
|
151
|
+
throw new Error(`Invalid config path: ${projectConfigPath} is outside project root`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!existsSync(projectConfigPath)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const content = readFileSync(projectConfigPath, 'utf-8');
|
|
160
|
+
const parsed = JSON.parse(content);
|
|
161
|
+
|
|
162
|
+
// 環境変数を展開
|
|
163
|
+
const expanded = expandEnvVarsInConfig(parsed);
|
|
164
|
+
|
|
165
|
+
// 部分的な設定なので、スキーマで厳密にバリデーションしない
|
|
166
|
+
// ただし、存在するキーについては型チェック
|
|
167
|
+
return expanded as Partial<AppConfig>;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof SyntaxError) {
|
|
170
|
+
throw new Error(`Invalid JSON in ${projectConfigPath}: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
if (error instanceof Error && error.message.includes('Invalid config path')) {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Failed to load project config from ${projectConfigPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 設定を読み込んでマージ
|
|
181
|
+
*
|
|
182
|
+
* マージ順序:
|
|
183
|
+
* 1. デフォルト設定
|
|
184
|
+
* 2. プロジェクト固有設定(上書き)
|
|
185
|
+
* 3. 環境変数(最終上書き、既存の動作を維持)
|
|
186
|
+
*/
|
|
187
|
+
export function loadConfig(projectRoot: string = process.cwd()): AppConfig {
|
|
188
|
+
// デフォルト設定を読み込み
|
|
189
|
+
const defaultConfig = loadDefaultConfig();
|
|
190
|
+
|
|
191
|
+
// プロジェクト固有設定を読み込み
|
|
192
|
+
const projectConfig = loadProjectConfig(projectRoot);
|
|
193
|
+
|
|
194
|
+
// マージ(プロジェクト設定がデフォルトを上書き)
|
|
195
|
+
let mergedConfig: AppConfig = projectConfig
|
|
196
|
+
? deepMerge(defaultConfig, projectConfig)
|
|
197
|
+
: defaultConfig;
|
|
198
|
+
|
|
199
|
+
// 環境変数で最終上書き(条件付き)
|
|
200
|
+
// 注意: config.jsonにspaces設定がある場合は環境変数を無視(config.jsonを優先)
|
|
201
|
+
if (process.env.CONFLUENCE_PRD_SPACE) {
|
|
202
|
+
if (!mergedConfig.confluence) {
|
|
203
|
+
mergedConfig.confluence = {
|
|
204
|
+
pageCreationGranularity: 'single',
|
|
205
|
+
pageTitleFormat: '[{projectName}] {featureName} {docTypeLabel}',
|
|
206
|
+
autoLabels: ['{projectLabel}', '{docType}', '{featureName}', 'github-sync']
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// spacesオブジェクトを確実に作成
|
|
210
|
+
if (!mergedConfig.confluence.spaces) {
|
|
211
|
+
mergedConfig.confluence.spaces = {};
|
|
212
|
+
}
|
|
213
|
+
// 各フィールドを個別にチェックし、未定義のフィールドのみ環境変数で設定
|
|
214
|
+
// 既に定義されている値は変更しない
|
|
215
|
+
if (!mergedConfig.confluence.spaces.requirements) {
|
|
216
|
+
mergedConfig.confluence.spaces.requirements = process.env.CONFLUENCE_PRD_SPACE;
|
|
217
|
+
}
|
|
218
|
+
if (!mergedConfig.confluence.spaces.design) {
|
|
219
|
+
mergedConfig.confluence.spaces.design = process.env.CONFLUENCE_PRD_SPACE;
|
|
220
|
+
}
|
|
221
|
+
if (!mergedConfig.confluence.spaces.tasks) {
|
|
222
|
+
mergedConfig.confluence.spaces.tasks = process.env.CONFLUENCE_PRD_SPACE;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// JIRA issue type IDを環境変数から取得(インスタンス固有の値のため)
|
|
227
|
+
if (mergedConfig.jira && mergedConfig.jira.issueTypes) {
|
|
228
|
+
if (process.env.JIRA_ISSUE_TYPE_STORY) {
|
|
229
|
+
mergedConfig.jira.issueTypes.story = process.env.JIRA_ISSUE_TYPE_STORY;
|
|
230
|
+
}
|
|
231
|
+
if (process.env.JIRA_ISSUE_TYPE_SUBTASK) {
|
|
232
|
+
mergedConfig.jira.issueTypes.subtask = process.env.JIRA_ISSUE_TYPE_SUBTASK;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// スキーマで最終バリデーション
|
|
237
|
+
return AppConfigSchema.parse(mergedConfig);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 設定を取得(キャッシュ付き)
|
|
242
|
+
* 設定ファイルの変更を検知してキャッシュを無効化
|
|
243
|
+
*/
|
|
244
|
+
let cachedConfig: AppConfig | null = null;
|
|
245
|
+
let cachedProjectRoot: string | null = null;
|
|
246
|
+
let cachedConfigMtime: number | null = null;
|
|
247
|
+
let cachedDefaultConfigMtime: number | null = null;
|
|
248
|
+
|
|
249
|
+
export function getConfig(projectRoot: string = process.cwd()): AppConfig {
|
|
250
|
+
const projectConfigPath = resolve(projectRoot, '.kiro/config.json');
|
|
251
|
+
const currentFileUrl = import.meta.url;
|
|
252
|
+
const currentFilePath = fileURLToPath(currentFileUrl);
|
|
253
|
+
const currentDir = resolve(currentFilePath, '..');
|
|
254
|
+
const defaultConfigPath = resolve(currentDir, '../config/default-config.json');
|
|
255
|
+
|
|
256
|
+
// デフォルト設定ファイルの更新時刻をチェック
|
|
257
|
+
let defaultConfigChanged = false;
|
|
258
|
+
try {
|
|
259
|
+
if (existsSync(defaultConfigPath)) {
|
|
260
|
+
const defaultStats = statSync(defaultConfigPath);
|
|
261
|
+
if (cachedDefaultConfigMtime !== defaultStats.mtimeMs) {
|
|
262
|
+
defaultConfigChanged = true;
|
|
263
|
+
cachedDefaultConfigMtime = defaultStats.mtimeMs;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// ファイルが存在しない場合はキャッシュを無効化
|
|
267
|
+
if (cachedDefaultConfigMtime !== null) {
|
|
268
|
+
defaultConfigChanged = true;
|
|
269
|
+
cachedDefaultConfigMtime = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// ファイルアクセスエラー(削除された場合など)は変更として扱う
|
|
274
|
+
defaultConfigChanged = true;
|
|
275
|
+
cachedDefaultConfigMtime = null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// プロジェクト設定ファイルの更新時刻をチェック
|
|
279
|
+
let projectConfigChanged = false;
|
|
280
|
+
try {
|
|
281
|
+
if (existsSync(projectConfigPath)) {
|
|
282
|
+
const projectStats = statSync(projectConfigPath);
|
|
283
|
+
if (cachedConfigMtime !== projectStats.mtimeMs) {
|
|
284
|
+
projectConfigChanged = true;
|
|
285
|
+
cachedConfigMtime = projectStats.mtimeMs;
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
// ファイルが存在しない場合はキャッシュを無効化
|
|
289
|
+
if (cachedConfigMtime !== null) {
|
|
290
|
+
projectConfigChanged = true;
|
|
291
|
+
cachedConfigMtime = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// ファイルアクセスエラー(削除された場合など)は変更として扱う
|
|
296
|
+
projectConfigChanged = true;
|
|
297
|
+
cachedConfigMtime = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// キャッシュが有効で、設定ファイルが変更されていない場合はキャッシュを返す
|
|
301
|
+
if (
|
|
302
|
+
cachedConfig &&
|
|
303
|
+
cachedProjectRoot === projectRoot &&
|
|
304
|
+
!defaultConfigChanged &&
|
|
305
|
+
!projectConfigChanged
|
|
306
|
+
) {
|
|
307
|
+
return cachedConfig;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 設定を再読み込み
|
|
311
|
+
cachedConfig = loadConfig(projectRoot);
|
|
312
|
+
cachedProjectRoot = projectRoot;
|
|
313
|
+
|
|
314
|
+
return cachedConfig;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 設定キャッシュをクリア(テスト用)
|
|
319
|
+
*/
|
|
320
|
+
export function clearConfigCache(): void {
|
|
321
|
+
cachedConfig = null;
|
|
322
|
+
cachedProjectRoot = null;
|
|
323
|
+
cachedConfigMtime = null;
|
|
324
|
+
cachedDefaultConfigMtime = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 設定ファイルのバリデーション
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { AppConfigSchema } from '../config/config-schema.js';
|
|
8
|
+
import type { AppConfig } from '../config/config-schema.js';
|
|
9
|
+
import { getConfig } from './config-loader.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* バリデーション結果
|
|
13
|
+
*/
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
info: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* プロジェクト設定ファイルをバリデーション
|
|
23
|
+
*/
|
|
24
|
+
export function validateProjectConfig(projectRoot: string = process.cwd()): ValidationResult {
|
|
25
|
+
const errors: string[] = [];
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
const info: string[] = [];
|
|
28
|
+
|
|
29
|
+
const configPath = resolve(projectRoot, '.kiro/config.json');
|
|
30
|
+
|
|
31
|
+
if (!existsSync(configPath)) {
|
|
32
|
+
// 設定ファイルが存在しない場合は情報メッセージ(デフォルト設定を使用)
|
|
33
|
+
info.push('Project config file not found. Using default configuration.');
|
|
34
|
+
return {
|
|
35
|
+
valid: true,
|
|
36
|
+
errors: [],
|
|
37
|
+
warnings: [],
|
|
38
|
+
info
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
44
|
+
const parsed = JSON.parse(content);
|
|
45
|
+
|
|
46
|
+
// スキーマでバリデーション
|
|
47
|
+
const result = AppConfigSchema.safeParse(parsed);
|
|
48
|
+
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
result.error.errors.forEach(error => {
|
|
51
|
+
const path = error.path.join('.');
|
|
52
|
+
errors.push(`${path}: ${error.message}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
errors,
|
|
58
|
+
warnings: [],
|
|
59
|
+
info: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 追加のバリデーション
|
|
64
|
+
const config = result.data;
|
|
65
|
+
|
|
66
|
+
// Confluence設定のバリデーション
|
|
67
|
+
if (config.confluence) {
|
|
68
|
+
const confluence = config.confluence;
|
|
69
|
+
|
|
70
|
+
// hierarchy設定の整合性チェック
|
|
71
|
+
if (confluence.pageCreationGranularity === 'by-hierarchy' || confluence.pageCreationGranularity === 'manual') {
|
|
72
|
+
if (!confluence.hierarchy) {
|
|
73
|
+
errors.push('confluence.hierarchy is required when pageCreationGranularity is "by-hierarchy" or "manual"');
|
|
74
|
+
} else {
|
|
75
|
+
if (confluence.pageCreationGranularity === 'by-hierarchy' && !confluence.hierarchy.parentPageTitle) {
|
|
76
|
+
warnings.push('confluence.hierarchy.parentPageTitle is recommended for "by-hierarchy" mode');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (confluence.pageCreationGranularity === 'manual' && !confluence.hierarchy.structure) {
|
|
80
|
+
errors.push('confluence.hierarchy.structure is required when pageCreationGranularity is "manual"');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// JIRA設定のバリデーション
|
|
87
|
+
if (config.jira) {
|
|
88
|
+
const jira = config.jira;
|
|
89
|
+
|
|
90
|
+
if (jira.storyCreationGranularity === 'selected-phases' && !jira.selectedPhases) {
|
|
91
|
+
errors.push('jira.selectedPhases is required when storyCreationGranularity is "selected-phases"');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (jira.selectedPhases && jira.selectedPhases.length === 0) {
|
|
95
|
+
warnings.push('jira.selectedPhases is empty. No stories will be created.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ワークフロー設定のバリデーション
|
|
100
|
+
if (config.workflow) {
|
|
101
|
+
const workflow = config.workflow;
|
|
102
|
+
|
|
103
|
+
if (workflow.enabledPhases && workflow.enabledPhases.length === 0) {
|
|
104
|
+
warnings.push('workflow.enabledPhases is empty. No phases will be executed.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validPhases = ['requirements', 'design', 'tasks'];
|
|
108
|
+
const invalidPhases = workflow.enabledPhases?.filter(phase => !validPhases.includes(phase));
|
|
109
|
+
if (invalidPhases && invalidPhases.length > 0) {
|
|
110
|
+
warnings.push(`Unknown phases in workflow.enabledPhases: ${invalidPhases.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
valid: errors.length === 0,
|
|
116
|
+
errors,
|
|
117
|
+
warnings,
|
|
118
|
+
info: []
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof SyntaxError) {
|
|
122
|
+
errors.push(`Invalid JSON: ${error.message}`);
|
|
123
|
+
} else {
|
|
124
|
+
errors.push(`Error reading config file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
errors,
|
|
130
|
+
warnings: [],
|
|
131
|
+
info: []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 設定ファイルのバリデーションを実行して結果を表示
|
|
138
|
+
*/
|
|
139
|
+
export function validateAndReport(projectRoot: string = process.cwd()): boolean {
|
|
140
|
+
const result = validateProjectConfig(projectRoot);
|
|
141
|
+
|
|
142
|
+
if (result.info.length > 0) {
|
|
143
|
+
console.log('ℹ️ Info:');
|
|
144
|
+
result.info.forEach(message => {
|
|
145
|
+
console.log(` - ${message}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result.warnings.length > 0) {
|
|
150
|
+
console.log('⚠️ Warnings:');
|
|
151
|
+
result.warnings.forEach(warning => {
|
|
152
|
+
console.log(` - ${warning}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.errors.length > 0) {
|
|
157
|
+
console.error('❌ Validation errors:');
|
|
158
|
+
result.errors.forEach(error => {
|
|
159
|
+
console.error(` - ${error}`);
|
|
160
|
+
});
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.valid) {
|
|
165
|
+
console.log('✅ Configuration is valid');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result.valid;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Confluence同期実行前の必須設定値チェック
|
|
173
|
+
* @param docType ドキュメントタイプ(requirements, design, tasks)
|
|
174
|
+
* @param projectRoot プロジェクトルート(デフォルト: process.cwd())
|
|
175
|
+
* @returns バリデーション結果
|
|
176
|
+
*/
|
|
177
|
+
export function validateForConfluenceSync(
|
|
178
|
+
docType: 'requirements' | 'design' | 'tasks',
|
|
179
|
+
projectRoot: string = process.cwd()
|
|
180
|
+
): ValidationResult {
|
|
181
|
+
const errors: string[] = [];
|
|
182
|
+
const warnings: string[] = [];
|
|
183
|
+
const info: string[] = [];
|
|
184
|
+
|
|
185
|
+
const config = getConfig(projectRoot);
|
|
186
|
+
const configPath = resolve(projectRoot, '.kiro/config.json');
|
|
187
|
+
|
|
188
|
+
// Confluence設定のチェック
|
|
189
|
+
if (!config.confluence) {
|
|
190
|
+
warnings.push('confluence設定がありません。デフォルト設定を使用します。');
|
|
191
|
+
} else {
|
|
192
|
+
const confluence = config.confluence;
|
|
193
|
+
|
|
194
|
+
// spaces設定のチェック
|
|
195
|
+
if (!confluence.spaces || !confluence.spaces[docType]) {
|
|
196
|
+
if (!process.env.CONFLUENCE_PRD_SPACE) {
|
|
197
|
+
warnings.push(
|
|
198
|
+
`confluence.spaces.${docType}が設定されていません。` +
|
|
199
|
+
`環境変数CONFLUENCE_PRD_SPACEも設定されていないため、デフォルト値(PRD)を使用します。` +
|
|
200
|
+
`\n 推奨: .kiro/config.jsonに以下を追加してください:\n` +
|
|
201
|
+
` {\n` +
|
|
202
|
+
` "confluence": {\n` +
|
|
203
|
+
` "spaces": {\n` +
|
|
204
|
+
` "${docType}": "YOUR_SPACE_KEY"\n` +
|
|
205
|
+
` }\n` +
|
|
206
|
+
` }\n` +
|
|
207
|
+
` }`
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
info.push(`confluence.spaces.${docType}が設定されていませんが、環境変数CONFLUENCE_PRD_SPACE(${process.env.CONFLUENCE_PRD_SPACE})を使用します。`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// hierarchy設定のチェック(by-hierarchyモードの場合)
|
|
215
|
+
if (confluence.pageCreationGranularity === 'by-hierarchy' || confluence.pageCreationGranularity === 'manual') {
|
|
216
|
+
if (!confluence.hierarchy) {
|
|
217
|
+
errors.push(
|
|
218
|
+
`confluence.hierarchyが設定されていません。` +
|
|
219
|
+
`pageCreationGranularityが"${confluence.pageCreationGranularity}"の場合、hierarchy設定が必須です。` +
|
|
220
|
+
`\n 解決方法: .kiro/config.jsonに以下を追加してください:\n` +
|
|
221
|
+
` {\n` +
|
|
222
|
+
` "confluence": {\n` +
|
|
223
|
+
` "hierarchy": {\n` +
|
|
224
|
+
` "mode": "simple",\n` +
|
|
225
|
+
` "parentPageTitle": "[{projectName}] {featureName}"\n` +
|
|
226
|
+
` }\n` +
|
|
227
|
+
` }\n` +
|
|
228
|
+
` }`
|
|
229
|
+
);
|
|
230
|
+
} else if (confluence.pageCreationGranularity === 'by-hierarchy' && confluence.hierarchy && !confluence.hierarchy.parentPageTitle) {
|
|
231
|
+
warnings.push(
|
|
232
|
+
`confluence.hierarchy.parentPageTitleが設定されていません。` +
|
|
233
|
+
`by-hierarchyモードでは推奨されます。`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (confluence.pageCreationGranularity === 'manual' && confluence.hierarchy && !confluence.hierarchy.structure) {
|
|
238
|
+
errors.push(
|
|
239
|
+
`confluence.hierarchy.structureが設定されていません。` +
|
|
240
|
+
`pageCreationGranularityが"manual"の場合、structure設定が必須です。`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
valid: errors.length === 0,
|
|
248
|
+
errors,
|
|
249
|
+
warnings,
|
|
250
|
+
info
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* JIRA同期実行前の必須設定値チェック
|
|
256
|
+
* @param projectRoot プロジェクトルート(デフォルト: process.cwd())
|
|
257
|
+
* @returns バリデーション結果
|
|
258
|
+
*/
|
|
259
|
+
export function validateForJiraSync(projectRoot: string = process.cwd()): ValidationResult {
|
|
260
|
+
const errors: string[] = [];
|
|
261
|
+
const warnings: string[] = [];
|
|
262
|
+
const info: string[] = [];
|
|
263
|
+
|
|
264
|
+
const config = getConfig(projectRoot);
|
|
265
|
+
const configPath = resolve(projectRoot, '.kiro/config.json');
|
|
266
|
+
|
|
267
|
+
// JIRA設定のチェック
|
|
268
|
+
if (!config.jira) {
|
|
269
|
+
warnings.push('jira設定がありません。デフォルト設定を使用します。');
|
|
270
|
+
} else {
|
|
271
|
+
const jira = config.jira;
|
|
272
|
+
|
|
273
|
+
// issueTypes設定のチェック
|
|
274
|
+
if (!jira.issueTypes) {
|
|
275
|
+
if (!process.env.JIRA_ISSUE_TYPE_STORY) {
|
|
276
|
+
errors.push(
|
|
277
|
+
`jira.issueTypes.storyが設定されていません。` +
|
|
278
|
+
`環境変数JIRA_ISSUE_TYPE_STORYも設定されていないため、JIRA同期を実行できません。` +
|
|
279
|
+
`\n 解決方法1: 環境変数を設定:\n` +
|
|
280
|
+
` export JIRA_ISSUE_TYPE_STORY=10036 # JIRAインスタンス固有のID\n` +
|
|
281
|
+
`\n 解決方法2: .kiro/config.jsonに以下を追加:\n` +
|
|
282
|
+
` {\n` +
|
|
283
|
+
` "jira": {\n` +
|
|
284
|
+
` "issueTypes": {\n` +
|
|
285
|
+
` "story": "10036",\n` +
|
|
286
|
+
` "subtask": "10037"\n` +
|
|
287
|
+
` }\n` +
|
|
288
|
+
` }\n` +
|
|
289
|
+
` }` +
|
|
290
|
+
`\n 確認方法: JIRA管理画面(Settings > Issues > Issue types)またはREST API: GET /rest/api/3/issuetype`
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
info.push(`jira.issueTypes.storyが設定されていませんが、環境変数JIRA_ISSUE_TYPE_STORY(${process.env.JIRA_ISSUE_TYPE_STORY})を使用します。`);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
if (!jira.issueTypes.story) {
|
|
297
|
+
if (!process.env.JIRA_ISSUE_TYPE_STORY) {
|
|
298
|
+
errors.push(
|
|
299
|
+
`jira.issueTypes.storyが設定されていません。` +
|
|
300
|
+
`環境変数JIRA_ISSUE_TYPE_STORYも設定されていないため、JIRA同期を実行できません。` +
|
|
301
|
+
`\n 解決方法: .kiro/config.jsonのjira.issueTypes.storyに値を設定するか、` +
|
|
302
|
+
`環境変数JIRA_ISSUE_TYPE_STORYを設定してください。`
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
info.push(`jira.issueTypes.storyが設定されていませんが、環境変数JIRA_ISSUE_TYPE_STORY(${process.env.JIRA_ISSUE_TYPE_STORY})を使用します。`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!jira.issueTypes.subtask) {
|
|
310
|
+
if (!process.env.JIRA_ISSUE_TYPE_SUBTASK) {
|
|
311
|
+
warnings.push(
|
|
312
|
+
`jira.issueTypes.subtaskが設定されていません。` +
|
|
313
|
+
`環境変数JIRA_ISSUE_TYPE_SUBTASKも設定されていないため、サブタスクは作成されません。`
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
info.push(`jira.issueTypes.subtaskが設定されていませんが、環境変数JIRA_ISSUE_TYPE_SUBTASK(${process.env.JIRA_ISSUE_TYPE_SUBTASK})を使用します。`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// selectedPhases設定のチェック
|
|
322
|
+
if (jira.storyCreationGranularity === 'selected-phases' && !jira.selectedPhases) {
|
|
323
|
+
errors.push(
|
|
324
|
+
`jira.selectedPhasesが設定されていません。` +
|
|
325
|
+
`storyCreationGranularityが"selected-phases"の場合、selectedPhases設定が必須です。`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (jira.selectedPhases && jira.selectedPhases.length === 0) {
|
|
330
|
+
warnings.push('jira.selectedPhasesが空です。ストーリーは作成されません。');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
valid: errors.length === 0,
|
|
336
|
+
errors,
|
|
337
|
+
warnings,
|
|
338
|
+
info
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// CLI実行
|
|
343
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
344
|
+
const valid = validateAndReport();
|
|
345
|
+
process.exit(valid ? 0 : 1);
|
|
346
|
+
}
|
|
347
|
+
|