@sk8metal/michi-cli 0.8.2 → 0.8.4

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 (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/scripts/confluence-sync.js +2 -2
  3. package/dist/scripts/confluence-sync.js.map +1 -1
  4. package/dist/scripts/jira-sync.js +2 -2
  5. package/dist/scripts/jira-sync.js.map +1 -1
  6. package/dist/scripts/multi-project-estimate.js +2 -2
  7. package/dist/scripts/multi-project-estimate.js.map +1 -1
  8. package/dist/scripts/pr-automation.js +2 -2
  9. package/dist/scripts/pr-automation.js.map +1 -1
  10. package/dist/scripts/pre-flight-check.js +2 -2
  11. package/dist/scripts/pre-flight-check.js.map +1 -1
  12. package/dist/scripts/resource-dashboard.js +2 -2
  13. package/dist/scripts/resource-dashboard.js.map +1 -1
  14. package/dist/scripts/spec-impl-workflow.js +2 -2
  15. package/dist/scripts/spec-impl-workflow.js.map +1 -1
  16. package/dist/scripts/template/multi-repo-renderer.d.ts +1 -1
  17. package/dist/scripts/template/multi-repo-renderer.d.ts.map +1 -1
  18. package/dist/scripts/template/multi-repo-renderer.js +8 -3
  19. package/dist/scripts/template/multi-repo-renderer.js.map +1 -1
  20. package/dist/scripts/test-workflow-stages.js +2 -2
  21. package/dist/scripts/test-workflow-stages.js.map +1 -1
  22. package/dist/scripts/utils/config-loader.d.ts.map +1 -1
  23. package/dist/scripts/utils/config-loader.js +3 -2
  24. package/dist/scripts/utils/config-loader.js.map +1 -1
  25. package/dist/scripts/utils/env-loader.d.ts +11 -0
  26. package/dist/scripts/utils/env-loader.d.ts.map +1 -0
  27. package/dist/scripts/utils/env-loader.js +23 -0
  28. package/dist/scripts/utils/env-loader.js.map +1 -0
  29. package/dist/scripts/workflow-orchestrator.js +2 -2
  30. package/dist/scripts/workflow-orchestrator.js.map +1 -1
  31. package/dist/src/cli.js +3 -3
  32. package/dist/src/cli.js.map +1 -1
  33. package/dist/src/commands/multi-repo-ci-status.js +1 -1
  34. package/dist/src/commands/multi-repo-ci-status.js.map +1 -1
  35. package/dist/src/commands/multi-repo-confluence-sync.js +1 -1
  36. package/dist/src/commands/multi-repo-confluence-sync.js.map +1 -1
  37. package/dist/src/commands/multi-repo-init.js +1 -1
  38. package/dist/src/commands/multi-repo-init.js.map +1 -1
  39. package/dist/src/commands/multi-repo-test.js +1 -1
  40. package/dist/src/commands/multi-repo-test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/scripts/confluence-sync.ts +2 -2
  43. package/scripts/jira-sync.ts +2 -2
  44. package/scripts/multi-project-estimate.ts +2 -2
  45. package/scripts/pr-automation.ts +2 -2
  46. package/scripts/pre-flight-check.ts +2 -2
  47. package/scripts/resource-dashboard.ts +2 -2
  48. package/scripts/spec-impl-workflow.ts +2 -2
  49. package/scripts/template/__tests__/multi-repo-renderer.test.ts +15 -10
  50. package/scripts/template/multi-repo-renderer.ts +9 -3
  51. package/scripts/test-workflow-stages.ts +2 -2
  52. package/scripts/utils/__tests__/env-loader.test.ts +145 -0
  53. package/scripts/utils/config-loader.ts +3 -2
  54. package/scripts/utils/env-loader.ts +25 -0
  55. package/scripts/workflow-orchestrator.ts +2 -2
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  import { Octokit } from '@octokit/rest';
6
- import { config } from 'dotenv';
6
+ import { loadEnv } from './utils/env-loader.js';
7
7
 
8
- config();
8
+ loadEnv();
9
9
 
10
10
  interface PROptions {
11
11
  branch: string;
@@ -6,9 +6,9 @@
6
6
  import { existsSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import axios from 'axios';
9
- import { config } from 'dotenv';
9
+ import { loadEnv } from './utils/env-loader.js';
10
10
 
11
- config();
11
+ loadEnv();
12
12
 
13
13
  interface PreFlightResult {
14
14
  valid: boolean;
@@ -4,11 +4,11 @@
4
4
  */
5
5
 
6
6
  import { Octokit } from '@octokit/rest';
7
- import { config } from 'dotenv';
7
+ import { loadEnv } from './utils/env-loader.js';
8
8
  import { ConfluenceClient, getConfluenceConfig } from './confluence-sync.js';
9
9
  import { getConfig } from './utils/config-loader.js';
10
10
 
11
- config();
11
+ loadEnv();
12
12
 
13
13
  interface ProjectResource {
14
14
  projectName: string;
@@ -7,7 +7,7 @@
7
7
  * - 終了時: PR作成、Epic + 最初の Story を「レビュー待ち」に移動、PRリンクをコメント
8
8
  */
9
9
 
10
- import { config } from 'dotenv';
10
+ import { loadEnv } from './utils/env-loader.js';
11
11
  import { JIRAClient } from './jira-sync.js';
12
12
  import { getConfig } from './utils/config-loader.js';
13
13
  import {
@@ -16,7 +16,7 @@ import {
16
16
  JiraInfo,
17
17
  } from './utils/spec-loader.js';
18
18
 
19
- config();
19
+ loadEnv();
20
20
 
21
21
  /**
22
22
  * spec-impl 統合ワークフローのオプション
@@ -13,6 +13,13 @@ import {
13
13
  type MultiRepoTemplateContext,
14
14
  } from '../multi-repo-renderer.js';
15
15
  import * as fs from 'fs';
16
+ import { fileURLToPath } from 'url';
17
+ import { dirname, resolve } from 'path';
18
+
19
+ // Calculate MICHI_PACKAGE_ROOT for tests
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ const MICHI_PACKAGE_ROOT = resolve(__dirname, '..', '..', '..');
16
23
 
17
24
  vi.mock('fs');
18
25
 
@@ -63,10 +70,11 @@ describe('loadMultiRepoTemplate', () => {
63
70
  const mockContent = '# {{PROJECT_NAME}} - Requirements';
64
71
  vi.spyOn(fs, 'readFileSync').mockReturnValue(mockContent);
65
72
 
66
- const content = loadMultiRepoTemplate('overview/requirements', '/test/root');
73
+ const content = loadMultiRepoTemplate('overview/requirements');
67
74
 
75
+ const expectedPath = resolve(MICHI_PACKAGE_ROOT, 'templates', 'multi-repo', 'overview', 'requirements.md');
68
76
  expect(fs.readFileSync).toHaveBeenCalledWith(
69
- '/test/root/templates/multi-repo/overview/requirements.md',
77
+ expectedPath,
70
78
  'utf-8'
71
79
  );
72
80
  expect(content).toBe(mockContent);
@@ -77,7 +85,7 @@ describe('loadMultiRepoTemplate', () => {
77
85
  throw new Error('ENOENT: no such file or directory');
78
86
  });
79
87
 
80
- expect(() => loadMultiRepoTemplate('invalid/template', '/test/root')).toThrow(
88
+ expect(() => loadMultiRepoTemplate('invalid/template')).toThrow(
81
89
  'Multi-Repo template not found: invalid/template.md'
82
90
  );
83
91
  });
@@ -151,8 +159,7 @@ describe('loadAndRenderMultiRepoTemplate', () => {
151
159
 
152
160
  const rendered = loadAndRenderMultiRepoTemplate(
153
161
  'overview/requirements',
154
- context,
155
- '/test/root'
162
+ context
156
163
  );
157
164
 
158
165
  expect(rendered).toBe('# test-project Requirements\n\n**JIRA**: TEST');
@@ -184,8 +191,7 @@ describe('renderMultiRepoTemplates', () => {
184
191
 
185
192
  const rendered = renderMultiRepoTemplates(
186
193
  ['overview/requirements', 'overview/architecture'],
187
- context,
188
- '/test/root'
194
+ context
189
195
  );
190
196
 
191
197
  expect(rendered['overview/requirements']).toBe('# test-project Requirements');
@@ -200,7 +206,7 @@ describe('renderMultiRepoTemplates', () => {
200
206
  CREATED_AT: '2025-12-14T10:00:00Z',
201
207
  };
202
208
 
203
- const rendered = renderMultiRepoTemplates([], context, '/test/root');
209
+ const rendered = renderMultiRepoTemplates([], context);
204
210
 
205
211
  expect(rendered).toEqual({});
206
212
  });
@@ -248,8 +254,7 @@ describe('統合テスト', () => {
248
254
 
249
255
  const rendered = loadAndRenderMultiRepoTemplate(
250
256
  'overview/requirements',
251
- context,
252
- '/test/root'
257
+ context
253
258
  );
254
259
 
255
260
  expect(rendered).toContain('# my-awesome-project - 要件定義書');
@@ -5,9 +5,15 @@
5
5
  */
6
6
 
7
7
  import { readFileSync } from 'fs';
8
- import { resolve, relative, isAbsolute } from 'path';
8
+ import { resolve, relative, isAbsolute, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
9
10
  import { renderTemplate, type TemplateContext } from './renderer.js';
10
11
 
12
+ // Resolve Michi package root directory
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const MICHI_PACKAGE_ROOT = resolve(__dirname, '..', '..');
16
+
11
17
  export interface MultiRepoTemplateContext {
12
18
  PROJECT_NAME: string;
13
19
  JIRA_KEY: string;
@@ -51,7 +57,7 @@ export const createMultiRepoTemplateContext = (
51
57
  */
52
58
  export const loadMultiRepoTemplate = (
53
59
  templateName: string,
54
- projectRoot: string = process.cwd()
60
+ _projectRoot: string = process.cwd()
55
61
  ): string => {
56
62
  // Security Layer 1: Validate template name
57
63
  // Reject path traversal characters (../, ..\, absolute paths)
@@ -63,7 +69,7 @@ export const loadMultiRepoTemplate = (
63
69
  }
64
70
 
65
71
  // Security Layer 2: Resolve absolute paths
66
- const templateDir = resolve(projectRoot, 'templates', 'multi-repo');
72
+ const templateDir = resolve(MICHI_PACKAGE_ROOT, 'templates', 'multi-repo');
67
73
  const templatePath = resolve(templateDir, `${templateName}.md`);
68
74
 
69
75
  // Security Layer 3: Verify path containment
@@ -3,10 +3,10 @@
3
3
  * testとreleaseステージのみを実行
4
4
  */
5
5
 
6
- import { config } from 'dotenv';
6
+ import { loadEnv } from './utils/env-loader.js';
7
7
  import { WorkflowOrchestrator, WorkflowConfig } from './workflow-orchestrator.js';
8
8
 
9
- config();
9
+ loadEnv();
10
10
 
11
11
  async function main() {
12
12
  const args = process.argv.slice(2);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * env-loader.ts のユニットテスト
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { config } from 'dotenv';
9
+
10
+ describe('env-loader', () => {
11
+ const testDir = join(process.cwd(), 'test-temp-env-loader');
12
+ const testGlobalDir = join(testDir, '.michi');
13
+ const testGlobalEnvPath = join(testGlobalDir, '.env');
14
+ const testLocalEnvPath = join(testDir, '.env');
15
+
16
+ // 元の環境変数を保存
17
+ const originalEnv = { ...process.env };
18
+ const originalHome = process.env.HOME;
19
+ const originalCwd = process.cwd();
20
+
21
+ beforeEach(() => {
22
+ // テスト用ディレクトリ作成
23
+ if (!existsSync(testDir)) {
24
+ mkdirSync(testDir, { recursive: true });
25
+ }
26
+ if (!existsSync(testGlobalDir)) {
27
+ mkdirSync(testGlobalDir, { recursive: true });
28
+ }
29
+
30
+ // カレントディレクトリをテストディレクトリに変更
31
+ process.chdir(testDir);
32
+
33
+ // 環境変数をクリア
34
+ for (const key in process.env) {
35
+ if (key.startsWith('TEST_')) {
36
+ delete process.env[key];
37
+ }
38
+ }
39
+ });
40
+
41
+ afterEach(() => {
42
+ // カレントディレクトリを戻す
43
+ process.chdir(originalCwd);
44
+
45
+ // クリーンアップ
46
+ if (existsSync(testDir)) {
47
+ rmSync(testDir, { recursive: true, force: true });
48
+ }
49
+
50
+ // 環境変数を復元
51
+ process.env = { ...originalEnv };
52
+ if (originalHome) {
53
+ process.env.HOME = originalHome;
54
+ }
55
+ });
56
+
57
+ it('should load global .env if exists', () => {
58
+ // グローバル .env を作成
59
+ const globalContent = `TEST_GLOBAL_VAR=global_value
60
+ TEST_SHARED_VAR=global_shared`;
61
+ writeFileSync(testGlobalEnvPath, globalContent, 'utf-8');
62
+
63
+ // HOMEを一時的に変更
64
+ process.env.HOME = testDir;
65
+
66
+ // dotenvで読み込みをシミュレート(env-loader.tsと同じロジック)
67
+ if (existsSync(testGlobalEnvPath)) {
68
+ config({ path: testGlobalEnvPath });
69
+ }
70
+
71
+ expect(process.env.TEST_GLOBAL_VAR).toBe('global_value');
72
+ expect(process.env.TEST_SHARED_VAR).toBe('global_shared');
73
+ });
74
+
75
+ it('should load local .env if exists', () => {
76
+ // ローカル .env を作成
77
+ const localContent = `TEST_LOCAL_VAR=local_value
78
+ TEST_SHARED_VAR=local_shared`;
79
+ writeFileSync(testLocalEnvPath, localContent, 'utf-8');
80
+
81
+ // dotenvでローカル.envを読み込み
82
+ config();
83
+
84
+ expect(process.env.TEST_LOCAL_VAR).toBe('local_value');
85
+ expect(process.env.TEST_SHARED_VAR).toBe('local_shared');
86
+ });
87
+
88
+ it('should override global with local', () => {
89
+ // グローバル .env を作成
90
+ const globalContent = `TEST_GLOBAL_VAR=global_value
91
+ TEST_SHARED_VAR=global_shared`;
92
+ writeFileSync(testGlobalEnvPath, globalContent, 'utf-8');
93
+
94
+ // ローカル .env を作成
95
+ const localContent = `TEST_LOCAL_VAR=local_value
96
+ TEST_SHARED_VAR=local_shared`;
97
+ writeFileSync(testLocalEnvPath, localContent, 'utf-8');
98
+
99
+ // HOMEを一時的に変更
100
+ process.env.HOME = testDir;
101
+
102
+ // env-loader.tsと同じロジックで読み込み
103
+ if (existsSync(testGlobalEnvPath)) {
104
+ config({ path: testGlobalEnvPath });
105
+ }
106
+ config({ override: true }); // ローカル .env(グローバルを上書き)
107
+
108
+ // ローカルがグローバルを上書きする
109
+ expect(process.env.TEST_GLOBAL_VAR).toBe('global_value');
110
+ expect(process.env.TEST_LOCAL_VAR).toBe('local_value');
111
+ expect(process.env.TEST_SHARED_VAR).toBe('local_shared'); // ローカルで上書き
112
+ });
113
+
114
+ it('should handle missing files gracefully', () => {
115
+ // 両方のファイルを作成しない
116
+ process.env.HOME = join(testDir, 'nonexistent');
117
+
118
+ // エラーを投げないことを確認
119
+ expect(() => {
120
+ if (existsSync(join(process.env.HOME!, '.michi', '.env'))) {
121
+ config({ path: join(process.env.HOME!, '.michi', '.env') });
122
+ }
123
+ config();
124
+ }).not.toThrow();
125
+ });
126
+
127
+ it('should not fail if global directory does not exist', () => {
128
+ // ローカル .env のみ作成
129
+ const localContent = `TEST_LOCAL_VAR=local_value`;
130
+ writeFileSync(testLocalEnvPath, localContent, 'utf-8');
131
+
132
+ // .michiディレクトリが存在しないHOMEを設定
133
+ process.env.HOME = join(testDir, 'no-michi-dir');
134
+
135
+ expect(() => {
136
+ const globalPath = join(process.env.HOME!, '.michi', '.env');
137
+ if (existsSync(globalPath)) {
138
+ config({ path: globalPath });
139
+ }
140
+ config();
141
+ }).not.toThrow();
142
+
143
+ expect(process.env.TEST_LOCAL_VAR).toBe('local_value');
144
+ });
145
+ });
@@ -7,7 +7,8 @@ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, unlinkSy
7
7
  import { resolve, relative, isAbsolute, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { homedir } from 'os';
10
- import { config, parse as dotenvParse } from 'dotenv';
10
+ import { parse as dotenvParse } from 'dotenv';
11
+ import { loadEnv } from './env-loader.js';
11
12
  import {
12
13
  AppConfigSchema,
13
14
  MultiRepoProjectSchema,
@@ -18,7 +19,7 @@ import {
18
19
  } from '../config/config-schema.js';
19
20
 
20
21
  // 環境変数読み込み
21
- config();
22
+ loadEnv();
22
23
 
23
24
  /**
24
25
  * グローバル設定ファイルのパス定数
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 環境変数読み込みユーティリティ
3
+ * グローバル設定(~/.michi/.env)とローカル設定(.env)を読み込む
4
+ */
5
+
6
+ import { config } from 'dotenv';
7
+ import { existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ /**
12
+ * 環境変数を読み込む
13
+ * グローバル設定(~/.michi/.env)→ローカル設定(.env)の順で読み込み
14
+ * ローカル設定がグローバル設定を上書きする
15
+ */
16
+ export function loadEnv(): void {
17
+ // 1. グローバル設定 ~/.michi/.env を読み込む
18
+ const globalEnvPath = join(homedir(), '.michi', '.env');
19
+ if (existsSync(globalEnvPath)) {
20
+ config({ path: globalEnvPath });
21
+ }
22
+
23
+ // 2. ローカル設定 .env を読み込む(グローバル設定を上書き)
24
+ config({ override: true });
25
+ }
@@ -3,7 +3,7 @@
3
3
  * AI開発フロー全体を統合実行
4
4
  */
5
5
 
6
- import { config } from 'dotenv';
6
+ import { loadEnv } from './utils/env-loader.js';
7
7
  import { loadProjectMeta } from './utils/project-meta.js';
8
8
  import { syncToConfluence, getConfluenceConfig } from './confluence-sync.js';
9
9
  import { syncTasksToJIRA } from './jira-sync.js';
@@ -12,7 +12,7 @@ import { executeTests, generateTestReport } from './utils/test-runner.js';
12
12
  import { createReleaseNotes } from './utils/release-notes-generator.js';
13
13
  import { pollForApproval, waitForManualApproval } from './utils/confluence-approval.js';
14
14
 
15
- config();
15
+ loadEnv();
16
16
 
17
17
  export interface WorkflowConfig {
18
18
  feature: string;