@sk8metal/michi-cli 0.0.1 → 0.0.3

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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +60 -24
  3. package/dist/scripts/__tests__/validate-phase.test.d.ts +5 -0
  4. package/dist/scripts/__tests__/validate-phase.test.d.ts.map +1 -0
  5. package/dist/scripts/__tests__/validate-phase.test.js +162 -0
  6. package/dist/scripts/__tests__/validate-phase.test.js.map +1 -0
  7. package/dist/scripts/utils/__tests__/config-validator.test.d.ts +5 -0
  8. package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +1 -0
  9. package/dist/scripts/utils/__tests__/config-validator.test.js +247 -0
  10. package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -0
  11. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +5 -0
  12. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +1 -0
  13. package/dist/scripts/utils/__tests__/feature-name-validator.test.js +106 -0
  14. package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +1 -0
  15. package/dist/scripts/utils/config-loader.js +1 -1
  16. package/dist/scripts/utils/config-loader.js.map +1 -1
  17. package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
  18. package/dist/scripts/utils/confluence-hierarchy.js +2 -1
  19. package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
  20. package/dist/src/__tests__/cli.test.d.ts +5 -0
  21. package/dist/src/__tests__/cli.test.d.ts.map +1 -0
  22. package/dist/src/__tests__/cli.test.js +58 -0
  23. package/dist/src/__tests__/cli.test.js.map +1 -0
  24. package/dist/src/cli.js +0 -0
  25. package/dist/vitest.config.d.ts +3 -0
  26. package/dist/vitest.config.d.ts.map +1 -0
  27. package/dist/vitest.config.js +29 -0
  28. package/dist/vitest.config.js.map +1 -0
  29. package/docs/setup.md +1 -1
  30. package/package.json +8 -4
  31. package/scripts/__tests__/README.md +101 -0
  32. package/scripts/__tests__/validate-phase.test.ts +185 -0
  33. package/scripts/config/config-schema.ts +130 -0
  34. package/scripts/config/default-config.json +57 -0
  35. package/scripts/config-interactive.ts +494 -0
  36. package/scripts/confluence-sync.ts +503 -0
  37. package/scripts/create-project.ts +293 -0
  38. package/scripts/jira-sync.ts +644 -0
  39. package/scripts/list-projects.ts +85 -0
  40. package/scripts/markdown-to-confluence.ts +161 -0
  41. package/scripts/multi-project-estimate.ts +255 -0
  42. package/scripts/phase-runner.ts +303 -0
  43. package/scripts/pr-automation.ts +67 -0
  44. package/scripts/pre-flight-check.ts +285 -0
  45. package/scripts/resource-dashboard.ts +124 -0
  46. package/scripts/setup-env.sh +52 -0
  47. package/scripts/setup-existing-project.ts +381 -0
  48. package/scripts/setup-existing.sh +145 -0
  49. package/scripts/utils/__tests__/config-validator.test.ts +302 -0
  50. package/scripts/utils/__tests__/feature-name-validator.test.ts +129 -0
  51. package/scripts/utils/config-loader.ts +326 -0
  52. package/scripts/utils/config-validator.ts +347 -0
  53. package/scripts/utils/confluence-hierarchy.ts +855 -0
  54. package/scripts/utils/feature-name-validator.ts +135 -0
  55. package/scripts/utils/project-meta.ts +69 -0
  56. package/scripts/validate-phase.ts +279 -0
  57. package/scripts/workflow-orchestrator.ts +178 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Confluence同期スクリプト
3
+ * GitHub の Markdown ファイルを Confluence に同期
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'fs';
7
+ import { resolve, join } from 'path';
8
+ import axios from 'axios';
9
+ import { config } from 'dotenv';
10
+ import { loadProjectMeta, type ProjectMetadata } from './utils/project-meta.js';
11
+ import { convertMarkdownToConfluence, createConfluencePage } from './markdown-to-confluence.js';
12
+ import { validateFeatureNameOrThrow } from './utils/feature-name-validator.js';
13
+ import { getConfig } from './utils/config-loader.js';
14
+ import { createPagesByGranularity } from './utils/confluence-hierarchy.js';
15
+ import { validateForConfluenceSync } from './utils/config-validator.js';
16
+
17
+ // 環境変数読み込み
18
+ config();
19
+
20
+ /**
21
+ * リクエスト間のスリープ処理(レートリミット対策)
22
+ */
23
+ function sleep(ms: number): Promise<void> {
24
+ return new Promise(resolve => setTimeout(resolve, ms));
25
+ }
26
+
27
+ /**
28
+ * リクエスト間の待機時間(ミリ秒)
29
+ * 環境変数 ATLASSIAN_REQUEST_DELAY で調整可能(デフォルト: 500ms)
30
+ */
31
+ function getRequestDelay(): number {
32
+ return parseInt(process.env.ATLASSIAN_REQUEST_DELAY || '500', 10);
33
+ }
34
+
35
+ interface ConfluenceConfig {
36
+ url: string;
37
+ email: string;
38
+ apiToken: string;
39
+ space: string;
40
+ }
41
+
42
+ /**
43
+ * Confluence設定を環境変数から取得
44
+ */
45
+ function getConfluenceConfig(): ConfluenceConfig {
46
+ const url = process.env.ATLASSIAN_URL;
47
+ const email = process.env.ATLASSIAN_EMAIL;
48
+ const apiToken = process.env.ATLASSIAN_API_TOKEN;
49
+ const space = process.env.CONFLUENCE_PRD_SPACE || 'PRD';
50
+
51
+ if (!url || !email || !apiToken) {
52
+ throw new Error('Missing Confluence credentials in .env file');
53
+ }
54
+
55
+ return { url, email, apiToken, space };
56
+ }
57
+
58
+ /**
59
+ * Confluence REST API クライアント
60
+ */
61
+ class ConfluenceClient {
62
+ private baseUrl: string;
63
+ private auth: string;
64
+ private requestDelay: number;
65
+
66
+ constructor(config: ConfluenceConfig) {
67
+ this.baseUrl = `${config.url}/wiki/rest/api`;
68
+ this.auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
69
+ this.requestDelay = getRequestDelay();
70
+ }
71
+
72
+ /**
73
+ * ページを検索
74
+ * @param spaceKey スペースキー
75
+ * @param title ページタイトル
76
+ * @param parentId 親ページID(オプション)。指定された場合、その親ページの子ページのみを検索
77
+ */
78
+ async searchPage(spaceKey: string, title: string, parentId?: string): Promise<any | null> {
79
+ // レートリミット対策: リクエスト前に待機
80
+ await sleep(this.requestDelay);
81
+
82
+ try {
83
+ // 親ページIDが指定されている場合、CQLクエリを使用して親ページの子ページのみを検索
84
+ if (parentId) {
85
+ // CQLクエリ: スペース、タイトル、親ページIDで検索
86
+ // タイトル内の特殊文字をエスケープ
87
+ const escapedTitle = title.replace(/"/g, '\\"');
88
+ // ancestorの代わりにparentを使用(Confluence CQLの正しい構文)
89
+ const cql = `space = ${spaceKey} AND title = "${escapedTitle}" AND parent = ${parentId}`;
90
+ console.log(` CQL Query: ${cql}`);
91
+
92
+ const response = await axios.get(`${this.baseUrl}/content/search`, {
93
+ params: {
94
+ cql,
95
+ expand: 'version'
96
+ },
97
+ headers: {
98
+ 'Authorization': `Basic ${this.auth}`,
99
+ 'Content-Type': 'application/json'
100
+ }
101
+ });
102
+
103
+ console.log(` CQL Search results: ${response.data.results?.length || 0} pages found`);
104
+
105
+ if (response.data.results && response.data.results.length > 0) {
106
+ return response.data.results[0];
107
+ }
108
+
109
+ // CQLクエリで見つからない場合、従来の方法で検索(親ページIDでフィルタリング)
110
+ console.log(` Falling back to standard search (may find pages in different parent)`);
111
+ return null;
112
+ }
113
+
114
+ // 親ページIDが指定されていない場合、従来の方法で検索
115
+ const response = await axios.get(`${this.baseUrl}/content`, {
116
+ params: {
117
+ spaceKey,
118
+ title,
119
+ expand: 'version'
120
+ },
121
+ headers: {
122
+ 'Authorization': `Basic ${this.auth}`,
123
+ 'Content-Type': 'application/json'
124
+ }
125
+ });
126
+
127
+ if (response.data.results && response.data.results.length > 0) {
128
+ return response.data.results[0];
129
+ }
130
+
131
+ return null;
132
+ } catch (error: any) {
133
+ // 404エラーは既存ページがないことを意味するので、nullを返す
134
+ if (error.response?.status === 404) {
135
+ return null;
136
+ }
137
+
138
+ // その他のエラーは詳細をログ出力
139
+ console.error('Error searching page:', error.message);
140
+ if (error.response) {
141
+ console.error(' Status:', error.response.status);
142
+ console.error(' Data:', JSON.stringify(error.response.data, null, 2));
143
+ }
144
+
145
+ // 404以外のエラーは再スロー(認証、権限、ネットワーク、サーバーエラーなど)
146
+ // エラーの詳細情報を含めて再スロー
147
+ if (error.response) {
148
+ // HTTPレスポンスがある場合(4xx/5xxエラー)
149
+ const enhancedError = new Error(
150
+ `Confluence API error: ${error.message} (status: ${error.response.status})`
151
+ );
152
+ (enhancedError as any).response = error.response;
153
+ throw enhancedError;
154
+ } else {
155
+ // ネットワークエラーなど、レスポンスがない場合
156
+ throw error;
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * ページを作成
163
+ */
164
+ async createPage(spaceKey: string, title: string, content: string, labels: string[] = [], parentId?: string): Promise<any> {
165
+ // レートリミット対策: リクエスト前に待機
166
+ await sleep(this.requestDelay);
167
+
168
+ const payload: any = {
169
+ type: 'page',
170
+ title,
171
+ space: { key: spaceKey },
172
+ body: {
173
+ storage: {
174
+ value: content,
175
+ representation: 'storage'
176
+ }
177
+ },
178
+ metadata: {
179
+ labels: labels.map(label => ({ name: label }))
180
+ }
181
+ };
182
+
183
+ // 親ページが指定されている場合、ancestorsを追加
184
+ if (parentId) {
185
+ payload.ancestors = [{ id: parentId }];
186
+ }
187
+
188
+ const response = await axios.post(`${this.baseUrl}/content`, payload, {
189
+ headers: {
190
+ 'Authorization': `Basic ${this.auth}`,
191
+ 'Content-Type': 'application/json'
192
+ }
193
+ });
194
+
195
+ return response.data;
196
+ }
197
+
198
+ /**
199
+ * 親ページの下に子ページを作成
200
+ */
201
+ async createPageUnderParent(
202
+ spaceKey: string,
203
+ title: string,
204
+ content: string,
205
+ labels: string[] = [],
206
+ parentId: string
207
+ ): Promise<any> {
208
+ return this.createPage(spaceKey, title, content, labels, parentId);
209
+ }
210
+
211
+ /**
212
+ * ページを更新
213
+ */
214
+ async updatePage(pageId: string, title: string, content: string, version: number): Promise<any> {
215
+ // レートリミット対策: リクエスト前に待機
216
+ await sleep(this.requestDelay);
217
+
218
+ const payload = {
219
+ version: { number: version + 1 },
220
+ title,
221
+ type: 'page',
222
+ body: {
223
+ storage: {
224
+ value: content,
225
+ representation: 'storage'
226
+ }
227
+ }
228
+ };
229
+
230
+ const response = await axios.put(`${this.baseUrl}/content/${pageId}`, payload, {
231
+ headers: {
232
+ 'Authorization': `Basic ${this.auth}`,
233
+ 'Content-Type': 'application/json'
234
+ }
235
+ });
236
+
237
+ return response.data;
238
+ }
239
+
240
+ /**
241
+ * ページの親情報を取得
242
+ * @param pageId ページID
243
+ * @returns 親ページID(ルートページの場合はnull)
244
+ */
245
+ async getPageParentId(pageId: string): Promise<string | null> {
246
+ // レートリミット対策: リクエスト前に待機
247
+ await sleep(this.requestDelay);
248
+
249
+ try {
250
+ const response = await axios.get(`${this.baseUrl}/content/${pageId}`, {
251
+ params: {
252
+ expand: 'ancestors'
253
+ },
254
+ headers: {
255
+ 'Authorization': `Basic ${this.auth}`,
256
+ 'Content-Type': 'application/json'
257
+ }
258
+ });
259
+
260
+ // ancestors配列の最後の要素が直接の親ページ
261
+ const ancestors = response.data.ancestors;
262
+ if (ancestors && ancestors.length > 0) {
263
+ return ancestors[ancestors.length - 1].id;
264
+ }
265
+
266
+ return null; // ルートページ
267
+ } catch (error: any) {
268
+ // 404エラーはページが存在しないことを意味する
269
+ if (error.response?.status === 404) {
270
+ return null;
271
+ }
272
+
273
+ // その他のエラーは詳細をログ出力
274
+ console.error('Error getting page parent:', error.message);
275
+ if (error.response) {
276
+ console.error(' Status:', error.response.status);
277
+ console.error(' Data:', JSON.stringify(error.response.data, null, 2));
278
+ }
279
+
280
+ // 404以外のエラーは再スロー
281
+ if (error.response) {
282
+ const enhancedError = new Error(
283
+ `Confluence API error: ${error.message} (status: ${error.response.status})`
284
+ );
285
+ (enhancedError as any).response = error.response;
286
+ throw enhancedError;
287
+ } else {
288
+ throw error;
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * ページのラベルを追加
295
+ */
296
+ async addLabels(pageId: string, labels: string[]): Promise<void> {
297
+ for (const label of labels) {
298
+ // レートリミット対策: リクエスト前に待機
299
+ await sleep(this.requestDelay);
300
+
301
+ await axios.post(
302
+ `${this.baseUrl}/content/${pageId}/label`,
303
+ [{ name: label }],
304
+ {
305
+ headers: {
306
+ 'Authorization': `Basic ${this.auth}`,
307
+ 'Content-Type': 'application/json'
308
+ }
309
+ }
310
+ );
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * spec.jsonを読み込む
317
+ * @param featureName 機能名
318
+ * @param projectRoot プロジェクトルート(デフォルト: process.cwd())
319
+ * @returns spec.jsonの内容、存在しない場合はnull
320
+ */
321
+ function loadSpecJson(featureName: string, projectRoot: string = process.cwd()): any | null {
322
+ const specPath = resolve(projectRoot, `.kiro/specs/${featureName}/spec.json`);
323
+
324
+ if (!existsSync(specPath)) {
325
+ return null;
326
+ }
327
+
328
+ try {
329
+ const content = readFileSync(specPath, 'utf-8');
330
+ return JSON.parse(content);
331
+ } catch (error) {
332
+ console.warn(`⚠️ Failed to load spec.json from ${specPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
333
+ return null;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Markdownファイルを Confluence に同期
339
+ */
340
+ async function syncToConfluence(
341
+ featureName: string,
342
+ docType: 'requirements' | 'design' | 'tasks' = 'requirements'
343
+ ): Promise<string> {
344
+ console.log(`Syncing ${docType} for feature: ${featureName}`);
345
+
346
+ // feature名のバリデーション(必須)
347
+ validateFeatureNameOrThrow(featureName);
348
+
349
+ // 実行前の必須設定値チェック
350
+ const validation = validateForConfluenceSync(docType);
351
+
352
+ if (validation.info.length > 0) {
353
+ validation.info.forEach(msg => console.log(`ℹ️ ${msg}`));
354
+ }
355
+
356
+ if (validation.warnings.length > 0) {
357
+ console.warn('⚠️ Warnings:');
358
+ validation.warnings.forEach(warning => console.warn(` ${warning}`));
359
+ }
360
+
361
+ if (validation.errors.length > 0) {
362
+ console.error('❌ Configuration errors:');
363
+ validation.errors.forEach(error => console.error(` ${error}`));
364
+ const configPath = resolve('.kiro/config.json');
365
+ console.error(`\n設定ファイル: ${configPath}`);
366
+ throw new Error('Confluence同期に必要な設定値が不足しています。上記のエラーを確認して設定を修正してください。');
367
+ }
368
+
369
+ console.log(`⏳ Request delay: ${getRequestDelay()}ms (set ATLASSIAN_REQUEST_DELAY to adjust)`);
370
+
371
+ // プロジェクトメタデータ読み込み
372
+ const projectMeta = loadProjectMeta();
373
+ console.log(`Project: ${projectMeta.projectName} (${projectMeta.projectId})`);
374
+
375
+ // 設定を読み込み
376
+ const appConfig = getConfig();
377
+ const confluenceConfig = appConfig.confluence || {
378
+ pageCreationGranularity: 'single',
379
+ pageTitleFormat: '[{projectName}] {featureName} {docTypeLabel}',
380
+ autoLabels: ['{projectLabel}', '{docType}', '{featureName}', 'github-sync']
381
+ };
382
+
383
+ console.log(`📋 Page creation granularity: ${confluenceConfig.pageCreationGranularity}`);
384
+
385
+ // 設定ソースのログ出力
386
+ if (confluenceConfig.spaces?.[docType]) {
387
+ console.log(`📝 Config source: config.json (spaces.${docType} = ${confluenceConfig.spaces[docType]})`);
388
+ } else if (process.env.CONFLUENCE_PRD_SPACE) {
389
+ console.log(`📝 Config source: environment variable (CONFLUENCE_PRD_SPACE = ${process.env.CONFLUENCE_PRD_SPACE})`);
390
+ } else {
391
+ console.log(`📝 Config source: default config`);
392
+ }
393
+
394
+ // Markdownファイル読み込み
395
+ const markdownPath = resolve(`.kiro/specs/${featureName}/${docType}.md`);
396
+ const markdown = readFileSync(markdownPath, 'utf-8');
397
+
398
+ // GitHub URL生成
399
+ const githubUrl = `${projectMeta.repository}/blob/main/.kiro/specs/${featureName}/${docType}.md`;
400
+
401
+ // Confluence設定を取得
402
+ const confluenceApiConfig = getConfluenceConfig();
403
+
404
+ // spec.jsonを読み込み
405
+ const specJson = loadSpecJson(featureName);
406
+
407
+ // スペースキーを決定(優先順位: spec.json → config.json → 環境変数/デフォルト)
408
+ let spaceKey: string;
409
+ let spaceKeySource: string;
410
+
411
+ if (specJson?.confluence?.spaceKey) {
412
+ spaceKey = specJson.confluence.spaceKey;
413
+ spaceKeySource = 'spec.json';
414
+ } else if (confluenceConfig.spaces?.[docType]) {
415
+ spaceKey = confluenceConfig.spaces[docType];
416
+ spaceKeySource = 'config.json';
417
+ } else {
418
+ // confluenceApiConfig.space は常に存在(getConfluenceConfig()で 'PRD' がデフォルト)
419
+ spaceKey = confluenceApiConfig.space;
420
+ spaceKeySource = process.env.CONFLUENCE_PRD_SPACE ? 'environment variable' : 'default from config';
421
+ }
422
+
423
+ console.log(`📌 Using Confluence space: ${spaceKey} (source: ${spaceKeySource})`);
424
+
425
+ // Confluenceクライアント初期化
426
+ const client = new ConfluenceClient(confluenceApiConfig);
427
+
428
+ // 階層構造に応じてページを作成
429
+ const result = await createPagesByGranularity(
430
+ client,
431
+ spaceKey,
432
+ markdown,
433
+ confluenceConfig,
434
+ projectMeta,
435
+ featureName,
436
+ docType,
437
+ githubUrl
438
+ );
439
+
440
+ // 最初のページのURLを返す(後方互換性のため)
441
+ if (result.pages.length === 0) {
442
+ throw new Error('No pages were created');
443
+ }
444
+
445
+ const firstPageUrl = result.pages[0].url;
446
+ console.log(`✅ Sync completed: ${result.pages.length} page(s) created/updated`);
447
+
448
+ if (result.pages.length > 1) {
449
+ console.log(`📄 Created pages:`);
450
+ result.pages.forEach((page, index) => {
451
+ console.log(` ${index + 1}. ${page.title} - ${page.url}`);
452
+ });
453
+ }
454
+
455
+ return firstPageUrl;
456
+ }
457
+
458
+ /**
459
+ * ドキュメントタイプのラベルを取得
460
+ */
461
+ function getDocTypeLabel(docType: string): string {
462
+ const labels: Record<string, string> = {
463
+ requirements: '要件定義',
464
+ design: '設計',
465
+ tasks: 'タスク分割'
466
+ };
467
+ return labels[docType] || docType;
468
+ }
469
+
470
+ // CLI実行
471
+ if (import.meta.url === `file://${process.argv[1]}`) {
472
+ const args = process.argv.slice(2);
473
+
474
+ if (args.length === 0) {
475
+ console.error('Usage: npm run confluence:sync <feature-name> [doc-type]');
476
+ console.error(' doc-type: requirements (default) | design | tasks');
477
+ process.exit(1);
478
+ }
479
+
480
+ const featureName = args[0];
481
+ const docType = (args[1] as any) || 'requirements';
482
+
483
+ syncToConfluence(featureName, docType)
484
+ .then(() => {
485
+ console.log('✅ Sync completed');
486
+ process.exit(0);
487
+ })
488
+ .catch((error) => {
489
+ console.error('❌ Sync failed:', error.message);
490
+ if (error.response) {
491
+ console.error('Response status:', error.response.status);
492
+ console.error('Response data:', JSON.stringify(error.response.data, null, 2));
493
+ }
494
+ if (error.config) {
495
+ console.error('Request URL:', error.config.url);
496
+ console.error('Request params:', JSON.stringify(error.config.params, null, 2));
497
+ }
498
+ process.exit(1);
499
+ });
500
+ }
501
+
502
+ export { syncToConfluence, ConfluenceClient };
503
+