@sk8metal/michi-cli 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -2
  3. package/dist/scripts/__tests__/create-project.test.d.ts +2 -0
  4. package/dist/scripts/__tests__/create-project.test.d.ts.map +1 -0
  5. package/dist/scripts/__tests__/create-project.test.js +247 -0
  6. package/dist/scripts/__tests__/create-project.test.js.map +1 -0
  7. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +2 -0
  8. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +1 -0
  9. package/dist/scripts/__tests__/multi-project-estimate.test.js +119 -0
  10. package/dist/scripts/__tests__/multi-project-estimate.test.js.map +1 -0
  11. package/dist/scripts/__tests__/setup-existing-project.test.d.ts +2 -0
  12. package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +1 -0
  13. package/dist/scripts/__tests__/setup-existing-project.test.js +67 -0
  14. package/dist/scripts/__tests__/setup-existing-project.test.js.map +1 -0
  15. package/dist/scripts/__tests__/setup-interactive.test.d.ts +2 -0
  16. package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +1 -0
  17. package/dist/scripts/__tests__/setup-interactive.test.js +160 -0
  18. package/dist/scripts/__tests__/setup-interactive.test.js.map +1 -0
  19. package/dist/scripts/config/default-config.json +57 -0
  20. package/dist/scripts/confluence-sync.d.ts +4 -0
  21. package/dist/scripts/confluence-sync.d.ts.map +1 -1
  22. package/dist/scripts/confluence-sync.js +12 -23
  23. package/dist/scripts/confluence-sync.js.map +1 -1
  24. package/dist/scripts/create-project.js +198 -137
  25. package/dist/scripts/create-project.js.map +1 -1
  26. package/dist/scripts/jira-sync.d.ts.map +1 -1
  27. package/dist/scripts/jira-sync.js +15 -0
  28. package/dist/scripts/jira-sync.js.map +1 -1
  29. package/dist/scripts/list-projects.d.ts.map +1 -1
  30. package/dist/scripts/list-projects.js +42 -15
  31. package/dist/scripts/list-projects.js.map +1 -1
  32. package/dist/scripts/multi-project-estimate.d.ts.map +1 -1
  33. package/dist/scripts/multi-project-estimate.js +56 -21
  34. package/dist/scripts/multi-project-estimate.js.map +1 -1
  35. package/dist/scripts/resource-dashboard.d.ts.map +1 -1
  36. package/dist/scripts/resource-dashboard.js +74 -17
  37. package/dist/scripts/resource-dashboard.js.map +1 -1
  38. package/dist/scripts/setup-existing-project.js +248 -214
  39. package/dist/scripts/setup-existing-project.js.map +1 -1
  40. package/dist/scripts/setup-interactive.d.ts +10 -0
  41. package/dist/scripts/setup-interactive.d.ts.map +1 -0
  42. package/dist/scripts/setup-interactive.js +413 -0
  43. package/dist/scripts/setup-interactive.js.map +1 -0
  44. package/dist/scripts/utils/__tests__/config-validator.test.js +5 -0
  45. package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
  46. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +5 -0
  47. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +1 -0
  48. package/dist/scripts/utils/__tests__/spec-updater.test.js +158 -0
  49. package/dist/scripts/utils/__tests__/spec-updater.test.js.map +1 -0
  50. package/dist/scripts/utils/confluence-hierarchy.d.ts +2 -1
  51. package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
  52. package/dist/scripts/utils/confluence-hierarchy.js +5 -0
  53. package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
  54. package/dist/scripts/utils/project-finder.d.ts +30 -0
  55. package/dist/scripts/utils/project-finder.d.ts.map +1 -0
  56. package/dist/scripts/utils/project-finder.js +147 -0
  57. package/dist/scripts/utils/project-finder.js.map +1 -0
  58. package/dist/scripts/utils/spec-updater.d.ts +72 -0
  59. package/dist/scripts/utils/spec-updater.d.ts.map +1 -0
  60. package/dist/scripts/utils/spec-updater.js +141 -0
  61. package/dist/scripts/utils/spec-updater.js.map +1 -0
  62. package/dist/vitest.config.d.ts.map +1 -1
  63. package/dist/vitest.config.js +8 -6
  64. package/dist/vitest.config.js.map +1 -1
  65. package/docs/README.md +2 -2
  66. package/docs/contributing/development.md +37 -0
  67. package/docs/getting-started/{new-project-setup.md → new-repository-setup.md} +66 -19
  68. package/docs/getting-started/setup.md +305 -182
  69. package/docs/guides/customization.md +1 -1
  70. package/docs/guides/multi-project.md +11 -8
  71. package/docs/reference/quick-reference.md +2 -2
  72. package/docs/testing-strategy.md +87 -0
  73. package/package.json +17 -5
  74. package/scripts/__tests__/create-project.test.ts +292 -0
  75. package/scripts/__tests__/multi-project-estimate.test.ts +145 -0
  76. package/scripts/__tests__/setup-existing-project.test.ts +79 -0
  77. package/scripts/__tests__/setup-interactive.test.ts +199 -0
  78. package/scripts/confluence-sync.ts +17 -29
  79. package/scripts/copy-static-assets.js +50 -0
  80. package/scripts/create-project.ts +219 -156
  81. package/scripts/jira-sync.ts +16 -1
  82. package/scripts/list-projects.ts +51 -24
  83. package/scripts/multi-project-estimate.ts +58 -22
  84. package/scripts/resource-dashboard.ts +91 -26
  85. package/scripts/setup-existing-project.ts +264 -223
  86. package/scripts/setup-existing.sh +29 -22
  87. package/scripts/setup-interactive.ts +565 -0
  88. package/scripts/utils/__tests__/config-validator.test.ts +6 -0
  89. package/scripts/utils/__tests__/spec-updater.test.ts +220 -0
  90. package/scripts/utils/confluence-hierarchy.ts +7 -1
  91. package/scripts/utils/project-finder.ts +184 -0
  92. package/scripts/utils/spec-updater.ts +212 -0
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { basename } from 'path';
3
+
4
+ // validateProjectId関数のロジックを再現
5
+ function validateProjectId(projectId: string): boolean {
6
+ // 空文字、空白のみを拒否
7
+ if (!projectId.trim() || /^\s+$/.test(projectId)) {
8
+ return false;
9
+ }
10
+ // パストラバーサル文字を拒否
11
+ if (projectId.includes('..') || projectId.includes('/') || projectId.includes('\\')) {
12
+ return false;
13
+ }
14
+ // 許可する文字のみ(英数字、ハイフン、アンダースコア)
15
+ return /^[A-Za-z0-9_-]+$/.test(projectId);
16
+ }
17
+
18
+ describe('setup-interactive.ts 修正内容のテスト', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ describe('validateProjectId の呼び出し', () => {
24
+ it('getProjectMetadata 内で validateProjectId が呼び出される', () => {
25
+ const projectIdDefault = 'test-project';
26
+ const projectId = projectIdDefault;
27
+
28
+ // バリデーション(getProjectMetadata内のロジックを再現)
29
+ if (!validateProjectId(projectId)) {
30
+ throw new Error('無効なプロジェクトIDです。英数字、ハイフン、アンダースコアのみ使用できます。');
31
+ }
32
+
33
+ // 有効なプロジェクトIDはエラーを投げない
34
+ expect(projectId).toBe('test-project');
35
+ });
36
+
37
+ it('無効なプロジェクトID(パストラバーサル)は拒否される', () => {
38
+ const invalidIds = ['../test', 'test/../', 'test\\..', '../../'];
39
+
40
+ invalidIds.forEach(id => {
41
+ expect(validateProjectId(id)).toBe(false);
42
+ });
43
+ });
44
+
45
+ it('無効なプロジェクトID(特殊文字)は拒否される', () => {
46
+ const invalidIds = ['test@project', 'test#project', 'test$project', 'test project'];
47
+
48
+ invalidIds.forEach(id => {
49
+ expect(validateProjectId(id)).toBe(false);
50
+ });
51
+ });
52
+
53
+ it('有効なプロジェクトIDは受け入れられる', () => {
54
+ const validIds = ['test-project', 'test_project', 'test123', 'Test-Project_123'];
55
+
56
+ validIds.forEach(id => {
57
+ expect(validateProjectId(id)).toBe(true);
58
+ });
59
+ });
60
+ });
61
+
62
+ describe('main 関数の戻り値', () => {
63
+ it('main 関数は Promise<number> を返す', async () => {
64
+ // main関数のシグネチャを再現
65
+ async function main(): Promise<number> {
66
+ // 正常終了
67
+ return 0;
68
+ }
69
+
70
+ const result = await main();
71
+ expect(typeof result).toBe('number');
72
+ expect(result).toBe(0);
73
+ });
74
+
75
+ it('エラー時は 1 を返す', async () => {
76
+ // main関数のエラーハンドリングを再現
77
+ async function main(): Promise<number> {
78
+ try {
79
+ throw new Error('テストエラー');
80
+ } catch (error) {
81
+ return 1;
82
+ }
83
+ }
84
+
85
+ const result = await main();
86
+ expect(result).toBe(1);
87
+ });
88
+
89
+ it('早期終了時は 0 を返す', async () => {
90
+ // 早期終了のロジックを再現
91
+ async function main(): Promise<number> {
92
+ // 設定する項目がない場合
93
+ const configureProject = false;
94
+ const configureEnv = false;
95
+
96
+ if (!configureProject && !configureEnv) {
97
+ return 0;
98
+ }
99
+
100
+ return 0;
101
+ }
102
+
103
+ const result = await main();
104
+ expect(result).toBe(0);
105
+ });
106
+ });
107
+
108
+ describe('process.exit の削除', () => {
109
+ it('process.exit の代わりに return を使用する', () => {
110
+ // 修正前: process.exit(0)
111
+ // 修正後: return 0
112
+
113
+ function oldStyle(): void {
114
+ // process.exit(0); // 削除
115
+ }
116
+
117
+ function newStyle(): number {
118
+ return 0; // 新しいスタイル
119
+ }
120
+
121
+ const result = newStyle();
122
+ expect(result).toBe(0);
123
+ expect(typeof result).toBe('number');
124
+ });
125
+
126
+ it('エラー時も process.exit の代わりに return を使用する', () => {
127
+ // 修正前: process.exit(1)
128
+ // 修正後: return 1
129
+
130
+ function oldStyle(): void {
131
+ // process.exit(1); // 削除
132
+ }
133
+
134
+ function newStyle(): number {
135
+ return 1; // 新しいスタイル
136
+ }
137
+
138
+ const result = newStyle();
139
+ expect(result).toBe(1);
140
+ expect(typeof result).toBe('number');
141
+ });
142
+ });
143
+
144
+ describe('CLIエントリーポイントでの process.exit 呼び出し', () => {
145
+ it('main() の戻り値を受け取って process.exit を呼び出す', async () => {
146
+ // CLIエントリーポイントのロジックを再現
147
+ let exitCode: number | null = null;
148
+
149
+ const mockProcessExit = (code: number) => {
150
+ exitCode = code;
151
+ };
152
+
153
+ async function main(): Promise<number> {
154
+ return 0;
155
+ }
156
+
157
+ // CLIエントリーポイント
158
+ main()
159
+ .then(code => {
160
+ mockProcessExit(code);
161
+ })
162
+ .catch(() => {
163
+ mockProcessExit(1);
164
+ });
165
+
166
+ // 非同期処理を待つ
167
+ await new Promise(resolve => setTimeout(resolve, 10));
168
+
169
+ expect(exitCode).toBe(0);
170
+ });
171
+
172
+ it('エラー時は 1 で終了する', async () => {
173
+ let exitCode: number | null = null;
174
+
175
+ const mockProcessExit = (code: number) => {
176
+ exitCode = code;
177
+ };
178
+
179
+ async function main(): Promise<number> {
180
+ throw new Error('テストエラー');
181
+ }
182
+
183
+ // CLIエントリーポイント
184
+ main()
185
+ .then(code => {
186
+ mockProcessExit(code);
187
+ })
188
+ .catch(() => {
189
+ mockProcessExit(1);
190
+ });
191
+
192
+ // 非同期処理を待つ
193
+ await new Promise(resolve => setTimeout(resolve, 10));
194
+
195
+ expect(exitCode).toBe(1);
196
+ });
197
+ });
198
+ });
199
+
@@ -3,16 +3,16 @@
3
3
  * GitHub の Markdown ファイルを Confluence に同期
4
4
  */
5
5
 
6
- import { readFileSync, existsSync } from 'fs';
7
- import { resolve, join } from 'path';
6
+ import { readFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
8
  import axios from 'axios';
9
9
  import { config } from 'dotenv';
10
- import { loadProjectMeta, type ProjectMetadata } from './utils/project-meta.js';
11
- import { convertMarkdownToConfluence, createConfluencePage } from './markdown-to-confluence.js';
10
+ import { loadProjectMeta } from './utils/project-meta.js';
12
11
  import { validateFeatureNameOrThrow } from './utils/feature-name-validator.js';
13
12
  import { getConfig, getConfigPath } from './utils/config-loader.js';
14
13
  import { createPagesByGranularity } from './utils/confluence-hierarchy.js';
15
14
  import { validateForConfluenceSync } from './utils/config-validator.js';
15
+ import { updateSpecJsonAfterConfluenceSync, loadSpecJson } from './utils/spec-updater.js';
16
16
 
17
17
  // 環境変数読み込み
18
18
  config();
@@ -42,7 +42,7 @@ interface ConfluenceConfig {
42
42
  /**
43
43
  * Confluence設定を環境変数から取得
44
44
  */
45
- function getConfluenceConfig(): ConfluenceConfig {
45
+ export function getConfluenceConfig(): ConfluenceConfig {
46
46
  const url = process.env.ATLASSIAN_URL;
47
47
  const email = process.env.ATLASSIAN_EMAIL;
48
48
  const apiToken = process.env.ATLASSIAN_API_TOKEN;
@@ -312,27 +312,6 @@ class ConfluenceClient {
312
312
  }
313
313
  }
314
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
315
 
337
316
  /**
338
317
  * Markdownファイルを Confluence に同期
@@ -408,7 +387,7 @@ async function syncToConfluence(
408
387
  let spaceKey: string;
409
388
  let spaceKeySource: string;
410
389
 
411
- if (specJson?.confluence?.spaceKey) {
390
+ if (specJson.confluence?.spaceKey) {
412
391
  spaceKey = specJson.confluence.spaceKey;
413
392
  spaceKeySource = 'spec.json';
414
393
  } else if (confluenceConfig.spaces?.[docType]) {
@@ -444,14 +423,23 @@ async function syncToConfluence(
444
423
 
445
424
  const firstPageUrl = result.pages[0].url;
446
425
  console.log(`✅ Sync completed: ${result.pages.length} page(s) created/updated`);
447
-
426
+
448
427
  if (result.pages.length > 1) {
449
428
  console.log('📄 Created pages:');
450
429
  result.pages.forEach((page, index) => {
451
430
  console.log(` ${index + 1}. ${page.title} - ${page.url}`);
452
431
  });
453
432
  }
454
-
433
+
434
+ // spec.json を更新
435
+ const firstPage = result.pages[0];
436
+ updateSpecJsonAfterConfluenceSync(featureName, docType, {
437
+ pageId: firstPage.id,
438
+ url: firstPage.url,
439
+ title: firstPage.title,
440
+ spaceKey: spaceKey
441
+ });
442
+
455
443
  return firstPageUrl;
456
444
  }
457
445
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ビルド時に静的アセット(JSON等)をdistディレクトリにコピー
3
+ * tscはTypeScriptのトランスパイルのみで、JSONファイルは自動コピーされないため
4
+ */
5
+
6
+ import { copyFileSync, mkdirSync, existsSync } from 'fs';
7
+ import { dirname, resolve } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const projectRoot = resolve(__dirname, '..');
12
+
13
+ /**
14
+ * ファイルをコピーし、ディレクトリが存在しない場合は作成
15
+ */
16
+ function copyFile(src, dest) {
17
+ const srcPath = resolve(projectRoot, src);
18
+ const destPath = resolve(projectRoot, dest);
19
+
20
+ if (!existsSync(srcPath)) {
21
+ console.error(`❌ Source file not found: ${srcPath}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ // コピー先のディレクトリを作成
26
+ const destDir = dirname(destPath);
27
+ if (!existsSync(destDir)) {
28
+ mkdirSync(destDir, { recursive: true });
29
+ }
30
+
31
+ try {
32
+ copyFileSync(srcPath, destPath);
33
+ console.log(`✅ Copied: ${src} → ${dest}`);
34
+ } catch (error) {
35
+ console.error(`❌ Failed to copy ${src}:`, error.message);
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ // コピー対象のファイル一覧
41
+ const filesToCopy = [
42
+ {
43
+ src: 'scripts/config/default-config.json',
44
+ dest: 'dist/scripts/config/default-config.json'
45
+ }
46
+ ];
47
+
48
+ console.log('📦 Copying static assets...');
49
+ filesToCopy.forEach(({ src, dest }) => copyFile(src, dest));
50
+ console.log('✅ All static assets copied successfully.');