@sk8metal/michi-cli 0.8.6 → 0.10.1
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 +95 -1
- package/README.md +4 -4
- package/dist/scripts/config/config-schema.d.ts +3 -0
- package/dist/scripts/config/config-schema.d.ts.map +1 -1
- package/dist/scripts/config/config-schema.js +18 -0
- package/dist/scripts/config/config-schema.js.map +1 -1
- package/dist/scripts/constants/environments.js +3 -3
- package/dist/scripts/constants/environments.js.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.d.ts +20 -1
- package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.js +124 -1
- package/dist/scripts/utils/multi-repo-validator.js.map +1 -1
- package/docs/michi-development/testing/manual-verification-other-tools.md +10 -8
- package/docs/user-guide/getting-started/new-repository-setup.md +4 -3
- package/docs/user-guide/guides/migration-guide.md +138 -0
- package/docs/user-guide/guides/multi-repo-guide.md +573 -10
- package/docs/user-guide/guides/workflow.md +4 -14
- package/docs/user-guide/hands-on/workflow-walkthrough.md +173 -4
- package/package.json +1 -1
- package/scripts/__tests__/multi-repo-config-schema.test.ts +106 -0
- package/scripts/__tests__/multi-repo-validator.test.ts +229 -1
- package/scripts/config/config-schema.ts +20 -0
- package/scripts/constants/__tests__/environments.test.ts +3 -3
- package/scripts/constants/environments.ts +3 -3
- package/scripts/utils/multi-repo-validator.ts +160 -1
- package/templates/claude/agents/mermaid-validator/AGENT.md +257 -0
- package/templates/claude/commands/michi-multi-repo/impl-all.md +264 -0
- package/templates/claude/commands/michi-multi-repo/propagate-specs.md +271 -0
- package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-design.md +69 -6
- package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-init.md +6 -6
- package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-requirements.md +2 -2
- package/templates/claude/commands/michi-multi-repo/spec-review.md +247 -0
- package/templates/claude/skills/mermaid-validator/SKILL.md +261 -0
- package/templates/claude-agent/agents/cross-repo-reviewer.md +194 -0
- package/templates/claude-agent/agents/repo-spec-executor.md +113 -0
|
@@ -817,11 +817,180 @@ michi jira:comment DEMO-103 "PRを作成しました: https://github.com/..."
|
|
|
817
817
|
- [ ] Confluenceページが作成された(設計書)
|
|
818
818
|
- [ ] `spec.json` に `confluence.designPageId` が記録された
|
|
819
819
|
|
|
820
|
-
### Phase 0.3-0.4:
|
|
820
|
+
### Phase 0.3-0.4: テスト計画
|
|
821
821
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
-
|
|
822
|
+
#### Phase 0.3: テストタイプの選択
|
|
823
|
+
|
|
824
|
+
health-check-endpointの場合、以下のテストタイプを選択します:
|
|
825
|
+
|
|
826
|
+
**判断基準**:
|
|
827
|
+
|
|
828
|
+
| テストタイプ | 必要性 | 判断理由 |
|
|
829
|
+
|-------------|--------|----------|
|
|
830
|
+
| **単体テスト** | ✅ 必須 | HealthControllerとHealthServiceのロジックを検証 |
|
|
831
|
+
| **統合テスト** | ✅ 推奨 | API全体の動作とレスポンス形式を検証 |
|
|
832
|
+
| E2Eテスト | ❌ 不要 | 単一エンドポイントのみで複雑なフローなし |
|
|
833
|
+
| パフォーマンステスト | ⚠️ 任意 | ヘルスチェックは高頻度で呼ばれるため検討可能 |
|
|
834
|
+
| セキュリティテスト | ❌ 不要 | 認証不要の公開エンドポイント |
|
|
835
|
+
|
|
836
|
+
**結論**: health-check-endpointでは**単体テスト**と**統合テスト**を実施します。
|
|
837
|
+
|
|
838
|
+
#### Phase 0.4: テスト仕様書の作成
|
|
839
|
+
|
|
840
|
+
テストタイプごとに仕様書を作成します。
|
|
841
|
+
|
|
842
|
+
##### 単体テスト仕様書
|
|
843
|
+
|
|
844
|
+
**作成場所**: `tests/specs/unit-test-spec.md`
|
|
845
|
+
|
|
846
|
+
**期待される内容(例)**:
|
|
847
|
+
|
|
848
|
+
```markdown
|
|
849
|
+
# health-check-endpoint 単体テスト仕様
|
|
850
|
+
|
|
851
|
+
## テスト対象
|
|
852
|
+
|
|
853
|
+
- HealthController
|
|
854
|
+
- HealthService
|
|
855
|
+
|
|
856
|
+
## HealthControllerTest
|
|
857
|
+
|
|
858
|
+
### TC-U-001: 正常系 - ステータス取得
|
|
859
|
+
|
|
860
|
+
**目的**: HealthControllerが正常にヘルスチェック情報を返す
|
|
861
|
+
|
|
862
|
+
**前提条件**:
|
|
863
|
+
- HealthServiceがモック化されている
|
|
864
|
+
- HealthService.getStatus()が`{status: "ok", timestamp: "2025-01-15T10:00:00Z"}`を返す
|
|
865
|
+
|
|
866
|
+
**実行手順**:
|
|
867
|
+
1. GET /health エンドポイントを呼び出す
|
|
868
|
+
|
|
869
|
+
**期待結果**:
|
|
870
|
+
- HTTPステータス: 200
|
|
871
|
+
- レスポンスボディ: `{"status": "ok", "timestamp": "2025-01-15T10:00:00Z"}`
|
|
872
|
+
|
|
873
|
+
### TC-U-002: 異常系 - サービスエラー
|
|
874
|
+
|
|
875
|
+
**目的**: サービス層でエラーが発生した場合の処理
|
|
876
|
+
|
|
877
|
+
**前提条件**:
|
|
878
|
+
- HealthService.getStatus()が例外をスローする
|
|
879
|
+
|
|
880
|
+
**実行手順**:
|
|
881
|
+
1. GET /health エンドポイントを呼び出す
|
|
882
|
+
|
|
883
|
+
**期待結果**:
|
|
884
|
+
- HTTPステータス: 503
|
|
885
|
+
- レスポンスボディ: `{"status": "error", "message": "..."}`
|
|
886
|
+
|
|
887
|
+
## HealthServiceTest
|
|
888
|
+
|
|
889
|
+
### TC-U-101: システムステータス正常
|
|
890
|
+
|
|
891
|
+
**目的**: HealthServiceがシステムの正常状態を正しく判定
|
|
892
|
+
|
|
893
|
+
**前提条件**:
|
|
894
|
+
- すべての依存サービスが正常
|
|
895
|
+
|
|
896
|
+
**実行手順**:
|
|
897
|
+
1. HealthService.getStatus()を呼び出す
|
|
898
|
+
|
|
899
|
+
**期待結果**:
|
|
900
|
+
- status: "ok"
|
|
901
|
+
- timestamp: 現在時刻のISO 8601形式
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
##### 統合テスト仕様書
|
|
905
|
+
|
|
906
|
+
**作成場所**: `tests/specs/integration-test-spec.md`
|
|
907
|
+
|
|
908
|
+
**期待される内容(例)**:
|
|
909
|
+
|
|
910
|
+
```markdown
|
|
911
|
+
# health-check-endpoint 統合テスト仕様
|
|
912
|
+
|
|
913
|
+
## テスト対象
|
|
914
|
+
|
|
915
|
+
- Health API全体(Controller → Service → Repository)
|
|
916
|
+
|
|
917
|
+
## TC-I-001: 正常系 - エンドツーエンド
|
|
918
|
+
|
|
919
|
+
**目的**: Health APIが期待通りに動作する
|
|
920
|
+
|
|
921
|
+
**前提条件**:
|
|
922
|
+
- テストサーバーが起動している
|
|
923
|
+
- データベース接続が有効
|
|
924
|
+
|
|
925
|
+
**実行手順**:
|
|
926
|
+
1. GET http://localhost:8080/health をHTTPクライアントで呼び出す
|
|
927
|
+
|
|
928
|
+
**期待結果**:
|
|
929
|
+
- HTTPステータス: 200
|
|
930
|
+
- Content-Type: application/json
|
|
931
|
+
- レスポンスボディ:
|
|
932
|
+
```json
|
|
933
|
+
{
|
|
934
|
+
"status": "ok",
|
|
935
|
+
"timestamp": "<ISO 8601形式>",
|
|
936
|
+
"version": "1.0.0"
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
## TC-I-002: 異常系 - データベース切断
|
|
941
|
+
|
|
942
|
+
**目的**: DB接続エラー時の503レスポンス
|
|
943
|
+
|
|
944
|
+
**前提条件**:
|
|
945
|
+
- テストサーバーが起動している
|
|
946
|
+
- データベースが停止している
|
|
947
|
+
|
|
948
|
+
**実行手順**:
|
|
949
|
+
1. GET http://localhost:8080/health を呼び出す
|
|
950
|
+
|
|
951
|
+
**期待結果**:
|
|
952
|
+
- HTTPステータス: 503
|
|
953
|
+
- Content-Type: application/json
|
|
954
|
+
- レスポンスボディ:
|
|
955
|
+
```json
|
|
956
|
+
{
|
|
957
|
+
"status": "error",
|
|
958
|
+
"message": "Database connection failed"
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
#### ディレクトリ構造の準備
|
|
964
|
+
|
|
965
|
+
テスト仕様書を配置するディレクトリを作成します:
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
mkdir -p tests/specs
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**確認**:
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
ls -la tests/specs/
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**期待されるファイル**:
|
|
978
|
+
|
|
979
|
+
```
|
|
980
|
+
tests/specs/
|
|
981
|
+
├── unit-test-spec.md
|
|
982
|
+
└── integration-test-spec.md
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
#### 検証チェックリスト
|
|
986
|
+
|
|
987
|
+
テスト計画が完了したら、以下を確認してください:
|
|
988
|
+
|
|
989
|
+
- [ ] Phase 0.3: 必要なテストタイプを選択した(単体テスト + 統合テスト)
|
|
990
|
+
- [ ] Phase 0.4: 単体テスト仕様書を作成した(`tests/specs/unit-test-spec.md`)
|
|
991
|
+
- [ ] Phase 0.4: 統合テスト仕様書を作成した(`tests/specs/integration-test-spec.md`)
|
|
992
|
+
- [ ] テストケースにID(TC-U-XXX、TC-I-XXX)を付与した
|
|
993
|
+
- [ ] 各テストケースに目的・前提条件・実行手順・期待結果を記載した
|
|
825
994
|
|
|
826
995
|
### Phase 0.5-0.6: タスク分割とJIRA同期
|
|
827
996
|
|
package/package.json
CHANGED
|
@@ -37,6 +37,50 @@ describe('RepositorySchema', () => {
|
|
|
37
37
|
expect(result.data.branch).toBe('main');
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it('localPathが未指定の場合も受け入れる(オプショナル)', () => {
|
|
42
|
+
const repoWithoutLocalPath = {
|
|
43
|
+
name: 'my-repo',
|
|
44
|
+
url: 'https://github.com/owner/repo',
|
|
45
|
+
branch: 'main',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = RepositorySchema.safeParse(repoWithoutLocalPath);
|
|
49
|
+
expect(result.success).toBe(true);
|
|
50
|
+
if (result.success) {
|
|
51
|
+
expect(result.data.localPath).toBeUndefined();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('Unix絶対パス(/path/to/repo)を受け入れる', () => {
|
|
56
|
+
const repoWithUnixPath = {
|
|
57
|
+
name: 'my-repo',
|
|
58
|
+
url: 'https://github.com/owner/repo',
|
|
59
|
+
branch: 'main',
|
|
60
|
+
localPath: '/Users/user/repos/my-repo',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = RepositorySchema.safeParse(repoWithUnixPath);
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
if (result.success) {
|
|
66
|
+
expect(result.data.localPath).toBe('/Users/user/repos/my-repo');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('Windows絶対パス(C:\\path\\to\\repo)を受け入れる', () => {
|
|
71
|
+
const repoWithWindowsPath = {
|
|
72
|
+
name: 'my-repo',
|
|
73
|
+
url: 'https://github.com/owner/repo',
|
|
74
|
+
branch: 'main',
|
|
75
|
+
localPath: 'C:\\Users\\user\\repos\\my-repo',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = RepositorySchema.safeParse(repoWithWindowsPath);
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
if (result.success) {
|
|
81
|
+
expect(result.data.localPath).toBe('C:\\Users\\user\\repos\\my-repo');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
40
84
|
});
|
|
41
85
|
|
|
42
86
|
describe('異常ケース', () => {
|
|
@@ -73,6 +117,68 @@ describe('RepositorySchema', () => {
|
|
|
73
117
|
expect(result.success).toBe(false);
|
|
74
118
|
});
|
|
75
119
|
});
|
|
120
|
+
|
|
121
|
+
describe('localPathバリデーション', () => {
|
|
122
|
+
it('相対パス(./repo)を含む場合はエラー', () => {
|
|
123
|
+
const invalidRepo = {
|
|
124
|
+
name: 'my-repo',
|
|
125
|
+
url: 'https://github.com/owner/repo',
|
|
126
|
+
branch: 'main',
|
|
127
|
+
localPath: './repos/my-repo',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = RepositorySchema.safeParse(invalidRepo);
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('相対パス(../repo)を含む場合はエラー', () => {
|
|
135
|
+
const invalidRepo = {
|
|
136
|
+
name: 'my-repo',
|
|
137
|
+
url: 'https://github.com/owner/repo',
|
|
138
|
+
branch: 'main',
|
|
139
|
+
localPath: '../repos/my-repo',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = RepositorySchema.safeParse(invalidRepo);
|
|
143
|
+
expect(result.success).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('Windowsの相対パス(.\\repo)を含む場合はエラー', () => {
|
|
147
|
+
const invalidRepo = {
|
|
148
|
+
name: 'my-repo',
|
|
149
|
+
url: 'https://github.com/owner/repo',
|
|
150
|
+
branch: 'main',
|
|
151
|
+
localPath: '.\\repos\\my-repo',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = RepositorySchema.safeParse(invalidRepo);
|
|
155
|
+
expect(result.success).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('空文字列の場合はエラー', () => {
|
|
159
|
+
const invalidRepo = {
|
|
160
|
+
name: 'my-repo',
|
|
161
|
+
url: 'https://github.com/owner/repo',
|
|
162
|
+
branch: 'main',
|
|
163
|
+
localPath: '',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = RepositorySchema.safeParse(invalidRepo);
|
|
167
|
+
expect(result.success).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('相対パス(repo/subdir)を含む場合はエラー', () => {
|
|
171
|
+
const invalidRepo = {
|
|
172
|
+
name: 'my-repo',
|
|
173
|
+
url: 'https://github.com/owner/repo',
|
|
174
|
+
branch: 'main',
|
|
175
|
+
localPath: 'repos/my-repo',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = RepositorySchema.safeParse(invalidRepo);
|
|
179
|
+
expect(result.success).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
76
182
|
});
|
|
77
183
|
|
|
78
184
|
describe('MultiRepoProjectSchema', () => {
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
* Task 11.1: バリデーション関数の単体テスト
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
7
10
|
import {
|
|
8
11
|
validateProjectName,
|
|
9
12
|
validateJiraKey,
|
|
10
13
|
validateRepositoryUrl,
|
|
14
|
+
validateLocalPath,
|
|
11
15
|
} from '../utils/multi-repo-validator.js';
|
|
16
|
+
import type { Repository } from '../config/config-schema.js';
|
|
12
17
|
|
|
13
18
|
describe('validateProjectName', () => {
|
|
14
19
|
describe('正常ケース', () => {
|
|
@@ -522,3 +527,226 @@ describe('validateRepositoryUrl', () => {
|
|
|
522
527
|
});
|
|
523
528
|
});
|
|
524
529
|
});
|
|
530
|
+
|
|
531
|
+
describe('validateLocalPath', () => {
|
|
532
|
+
let tempDir: string;
|
|
533
|
+
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
// テスト用の一時ディレクトリを作成
|
|
536
|
+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'test-temp-'));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
afterEach(() => {
|
|
540
|
+
// テスト用の一時ディレクトリを削除
|
|
541
|
+
if (fs.existsSync(tempDir)) {
|
|
542
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
describe('localPath未設定ケース', () => {
|
|
547
|
+
it('localPathが未設定の場合、警告を返す', () => {
|
|
548
|
+
const repository: Repository = {
|
|
549
|
+
name: 'test-repo',
|
|
550
|
+
url: 'https://github.com/owner/repo',
|
|
551
|
+
branch: 'main',
|
|
552
|
+
// localPath: undefined (省略)
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const result = validateLocalPath(repository);
|
|
556
|
+
|
|
557
|
+
expect(result.isValid).toBe(false);
|
|
558
|
+
expect(result.errors).toHaveLength(0);
|
|
559
|
+
expect(result.warnings).toHaveLength(1);
|
|
560
|
+
expect(result.warnings[0]).toContain(
|
|
561
|
+
"Repository 'test-repo' does not have localPath configured",
|
|
562
|
+
);
|
|
563
|
+
expect(result.exists).toBe(false);
|
|
564
|
+
expect(result.isGitRepository).toBe(false);
|
|
565
|
+
expect(result.currentBranch).toBeNull();
|
|
566
|
+
expect(result.branchMatches).toBe(false);
|
|
567
|
+
expect(result.hasUncommittedChanges).toBe(false);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('ディレクトリ存在確認', () => {
|
|
572
|
+
it('localPathが存在しない場合、エラーを返す', () => {
|
|
573
|
+
const nonExistentPath = path.join(tempDir, 'non-existent');
|
|
574
|
+
const repository: Repository = {
|
|
575
|
+
name: 'test-repo',
|
|
576
|
+
url: 'https://github.com/owner/repo',
|
|
577
|
+
branch: 'main',
|
|
578
|
+
localPath: nonExistentPath,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const result = validateLocalPath(repository);
|
|
582
|
+
|
|
583
|
+
expect(result.isValid).toBe(false);
|
|
584
|
+
expect(result.errors).toHaveLength(1);
|
|
585
|
+
expect(result.errors[0]).toContain('does not exist');
|
|
586
|
+
expect(result.exists).toBe(false);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('localPathがファイルの場合、エラーを返す', () => {
|
|
590
|
+
const filePath = path.join(tempDir, 'test-file.txt');
|
|
591
|
+
fs.writeFileSync(filePath, 'test content');
|
|
592
|
+
|
|
593
|
+
const repository: Repository = {
|
|
594
|
+
name: 'test-repo',
|
|
595
|
+
url: 'https://github.com/owner/repo',
|
|
596
|
+
branch: 'main',
|
|
597
|
+
localPath: filePath,
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const result = validateLocalPath(repository);
|
|
601
|
+
|
|
602
|
+
expect(result.isValid).toBe(false);
|
|
603
|
+
expect(result.errors).toHaveLength(1);
|
|
604
|
+
expect(result.errors[0]).toContain('is not a directory');
|
|
605
|
+
expect(result.exists).toBe(true);
|
|
606
|
+
expect(result.isGitRepository).toBe(false);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe('Gitリポジトリ確認', () => {
|
|
611
|
+
it('Gitリポジトリではないディレクトリの場合、エラーを返す', () => {
|
|
612
|
+
const nonGitDir = path.join(tempDir, 'non-git-repo');
|
|
613
|
+
fs.mkdirSync(nonGitDir);
|
|
614
|
+
|
|
615
|
+
const repository: Repository = {
|
|
616
|
+
name: 'test-repo',
|
|
617
|
+
url: 'https://github.com/owner/repo',
|
|
618
|
+
branch: 'main',
|
|
619
|
+
localPath: nonGitDir,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const result = validateLocalPath(repository);
|
|
623
|
+
|
|
624
|
+
expect(result.isValid).toBe(false);
|
|
625
|
+
expect(result.errors).toHaveLength(1);
|
|
626
|
+
expect(result.errors[0]).toContain('is not a Git repository');
|
|
627
|
+
expect(result.exists).toBe(true);
|
|
628
|
+
expect(result.isGitRepository).toBe(false);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('Gitリポジトリの場合、基本的なチェックを通過する', () => {
|
|
632
|
+
const gitDir = path.join(tempDir, 'git-repo');
|
|
633
|
+
fs.mkdirSync(gitDir);
|
|
634
|
+
|
|
635
|
+
// Gitリポジトリを初期化
|
|
636
|
+
execSync('git init', { cwd: gitDir });
|
|
637
|
+
execSync('git config user.email "test@example.com"', { cwd: gitDir });
|
|
638
|
+
execSync('git config user.name "Test User"', { cwd: gitDir });
|
|
639
|
+
|
|
640
|
+
// 初回コミットを作成(ブランチが作成されるため)
|
|
641
|
+
fs.writeFileSync(path.join(gitDir, 'README.md'), '# Test');
|
|
642
|
+
execSync('git add .', { cwd: gitDir });
|
|
643
|
+
execSync('git commit -m "Initial commit"', { cwd: gitDir });
|
|
644
|
+
|
|
645
|
+
// mainブランチに切り替え(git initでデフォルトブランチ名が異なる可能性があるため)
|
|
646
|
+
try {
|
|
647
|
+
execSync('git branch -M main', { cwd: gitDir });
|
|
648
|
+
} catch {
|
|
649
|
+
// すでにmainの場合は無視
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const repository: Repository = {
|
|
653
|
+
name: 'test-repo',
|
|
654
|
+
url: 'https://github.com/owner/repo',
|
|
655
|
+
branch: 'main',
|
|
656
|
+
localPath: gitDir,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const result = validateLocalPath(repository);
|
|
660
|
+
|
|
661
|
+
expect(result.isValid).toBe(true);
|
|
662
|
+
expect(result.errors).toHaveLength(0);
|
|
663
|
+
expect(result.exists).toBe(true);
|
|
664
|
+
expect(result.isGitRepository).toBe(true);
|
|
665
|
+
expect(result.currentBranch).toBe('main');
|
|
666
|
+
expect(result.branchMatches).toBe(true);
|
|
667
|
+
expect(result.hasUncommittedChanges).toBe(false);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('ブランチが一致しない場合、警告を返す', () => {
|
|
671
|
+
const gitDir = path.join(tempDir, 'git-repo-branch-mismatch');
|
|
672
|
+
fs.mkdirSync(gitDir);
|
|
673
|
+
|
|
674
|
+
// Gitリポジトリを初期化
|
|
675
|
+
execSync('git init', { cwd: gitDir });
|
|
676
|
+
execSync('git config user.email "test@example.com"', { cwd: gitDir });
|
|
677
|
+
execSync('git config user.name "Test User"', { cwd: gitDir });
|
|
678
|
+
|
|
679
|
+
// 初回コミットを作成
|
|
680
|
+
fs.writeFileSync(path.join(gitDir, 'README.md'), '# Test');
|
|
681
|
+
execSync('git add .', { cwd: gitDir });
|
|
682
|
+
execSync('git commit -m "Initial commit"', { cwd: gitDir });
|
|
683
|
+
|
|
684
|
+
// developブランチに切り替え
|
|
685
|
+
execSync('git checkout -b develop', { cwd: gitDir });
|
|
686
|
+
|
|
687
|
+
const repository: Repository = {
|
|
688
|
+
name: 'test-repo',
|
|
689
|
+
url: 'https://github.com/owner/repo',
|
|
690
|
+
branch: 'main', // 実際はdevelopブランチにいる
|
|
691
|
+
localPath: gitDir,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const result = validateLocalPath(repository);
|
|
695
|
+
|
|
696
|
+
expect(result.isValid).toBe(true); // 警告のみでエラーではない
|
|
697
|
+
expect(result.errors).toHaveLength(0);
|
|
698
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
699
|
+
expect(result.warnings.some((w) => w.includes('does not match'))).toBe(
|
|
700
|
+
true,
|
|
701
|
+
);
|
|
702
|
+
expect(result.exists).toBe(true);
|
|
703
|
+
expect(result.isGitRepository).toBe(true);
|
|
704
|
+
expect(result.currentBranch).toBe('develop');
|
|
705
|
+
expect(result.branchMatches).toBe(false);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('未コミット変更がある場合、警告を返す', () => {
|
|
709
|
+
const gitDir = path.join(tempDir, 'git-repo-uncommitted');
|
|
710
|
+
fs.mkdirSync(gitDir);
|
|
711
|
+
|
|
712
|
+
// Gitリポジトリを初期化
|
|
713
|
+
execSync('git init', { cwd: gitDir });
|
|
714
|
+
execSync('git config user.email "test@example.com"', { cwd: gitDir });
|
|
715
|
+
execSync('git config user.name "Test User"', { cwd: gitDir });
|
|
716
|
+
|
|
717
|
+
// 初回コミットを作成
|
|
718
|
+
fs.writeFileSync(path.join(gitDir, 'README.md'), '# Test');
|
|
719
|
+
execSync('git add .', { cwd: gitDir });
|
|
720
|
+
execSync('git commit -m "Initial commit"', { cwd: gitDir });
|
|
721
|
+
|
|
722
|
+
// mainブランチに切り替え
|
|
723
|
+
try {
|
|
724
|
+
execSync('git branch -M main', { cwd: gitDir });
|
|
725
|
+
} catch {
|
|
726
|
+
// すでにmainの場合は無視
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 未コミットの変更を追加
|
|
730
|
+
fs.writeFileSync(path.join(gitDir, 'new-file.txt'), 'uncommitted');
|
|
731
|
+
|
|
732
|
+
const repository: Repository = {
|
|
733
|
+
name: 'test-repo',
|
|
734
|
+
url: 'https://github.com/owner/repo',
|
|
735
|
+
branch: 'main',
|
|
736
|
+
localPath: gitDir,
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const result = validateLocalPath(repository);
|
|
740
|
+
|
|
741
|
+
expect(result.isValid).toBe(true); // 警告のみでエラーではない
|
|
742
|
+
expect(result.errors).toHaveLength(0);
|
|
743
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
744
|
+
expect(
|
|
745
|
+
result.warnings.some((w) => w.includes('uncommitted changes')),
|
|
746
|
+
).toBe(true);
|
|
747
|
+
expect(result.exists).toBe(true);
|
|
748
|
+
expect(result.isGitRepository).toBe(true);
|
|
749
|
+
expect(result.hasUncommittedChanges).toBe(true);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
});
|
|
@@ -183,6 +183,26 @@ export const RepositorySchema = z.object({
|
|
|
183
183
|
message: 'GitHub URL must be in format: https://github.com/{owner}/{repo}',
|
|
184
184
|
}),
|
|
185
185
|
branch: z.string().default('main'),
|
|
186
|
+
localPath: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.refine(
|
|
190
|
+
(path) => {
|
|
191
|
+
// localPath が指定されていない場合は検証スキップ
|
|
192
|
+
if (path === undefined) return true;
|
|
193
|
+
// 空文字列はエラー
|
|
194
|
+
if (path === '') return false;
|
|
195
|
+
// 絶対パスであることを検証(セキュリティ考慮)
|
|
196
|
+
// Unix系: '/' で始まる、Windows系: 'C:\' 等で始まる
|
|
197
|
+
const isUnixAbsolutePath = path.startsWith('/');
|
|
198
|
+
const isWindowsAbsolutePath = /^[A-Za-z]:\\/.test(path);
|
|
199
|
+
return isUnixAbsolutePath || isWindowsAbsolutePath;
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
message:
|
|
203
|
+
'localPath must be an absolute path (Unix: /path/to/repo, Windows: C:\\path\\to\\repo)',
|
|
204
|
+
},
|
|
205
|
+
),
|
|
186
206
|
});
|
|
187
207
|
|
|
188
208
|
/**
|
|
@@ -22,21 +22,21 @@ describe('environments', () => {
|
|
|
22
22
|
it('should have correct structure for claude', () => {
|
|
23
23
|
const config = ENV_CONFIG.claude;
|
|
24
24
|
expect(config.rulesDir).toBe('.claude/rules');
|
|
25
|
-
expect(config.commandsDir).toBe('.claude/commands
|
|
25
|
+
expect(config.commandsDir).toBe('.claude/commands');
|
|
26
26
|
expect(config.templateSource).toBe('claude');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it('should have correct structure for claude-agent', () => {
|
|
30
30
|
const config = ENV_CONFIG['claude-agent'];
|
|
31
31
|
expect(config.rulesDir).toBe('.claude/agents');
|
|
32
|
-
expect(config.commandsDir).toBe('.claude/commands
|
|
32
|
+
expect(config.commandsDir).toBe('.claude/commands');
|
|
33
33
|
expect(config.templateSource).toBe('claude-agent');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it('should have correct structure for cursor', () => {
|
|
37
37
|
const config = ENV_CONFIG.cursor;
|
|
38
38
|
expect(config.rulesDir).toBe('.cursor/rules');
|
|
39
|
-
expect(config.commandsDir).toBe('.cursor/commands
|
|
39
|
+
expect(config.commandsDir).toBe('.cursor/commands');
|
|
40
40
|
expect(config.templateSource).toBe('cursor');
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -21,17 +21,17 @@ export type Environment =
|
|
|
21
21
|
export const ENV_CONFIG: Record<Environment, EnvironmentConfig> = {
|
|
22
22
|
claude: {
|
|
23
23
|
rulesDir: '.claude/rules',
|
|
24
|
-
commandsDir: '.claude/commands
|
|
24
|
+
commandsDir: '.claude/commands',
|
|
25
25
|
templateSource: 'claude',
|
|
26
26
|
},
|
|
27
27
|
'claude-agent': {
|
|
28
28
|
rulesDir: '.claude/agents',
|
|
29
|
-
commandsDir: '.claude/commands
|
|
29
|
+
commandsDir: '.claude/commands',
|
|
30
30
|
templateSource: 'claude-agent',
|
|
31
31
|
},
|
|
32
32
|
cursor: {
|
|
33
33
|
rulesDir: '.cursor/rules',
|
|
34
|
-
commandsDir: '.cursor/commands
|
|
34
|
+
commandsDir: '.cursor/commands',
|
|
35
35
|
templateSource: 'cursor',
|
|
36
36
|
},
|
|
37
37
|
gemini: {
|