@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +95 -1
  2. package/README.md +4 -4
  3. package/dist/scripts/config/config-schema.d.ts +3 -0
  4. package/dist/scripts/config/config-schema.d.ts.map +1 -1
  5. package/dist/scripts/config/config-schema.js +18 -0
  6. package/dist/scripts/config/config-schema.js.map +1 -1
  7. package/dist/scripts/constants/environments.js +3 -3
  8. package/dist/scripts/constants/environments.js.map +1 -1
  9. package/dist/scripts/utils/multi-repo-validator.d.ts +20 -1
  10. package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -1
  11. package/dist/scripts/utils/multi-repo-validator.js +124 -1
  12. package/dist/scripts/utils/multi-repo-validator.js.map +1 -1
  13. package/docs/michi-development/testing/manual-verification-other-tools.md +10 -8
  14. package/docs/user-guide/getting-started/new-repository-setup.md +4 -3
  15. package/docs/user-guide/guides/migration-guide.md +138 -0
  16. package/docs/user-guide/guides/multi-repo-guide.md +573 -10
  17. package/docs/user-guide/guides/workflow.md +4 -14
  18. package/docs/user-guide/hands-on/workflow-walkthrough.md +173 -4
  19. package/package.json +1 -1
  20. package/scripts/__tests__/multi-repo-config-schema.test.ts +106 -0
  21. package/scripts/__tests__/multi-repo-validator.test.ts +229 -1
  22. package/scripts/config/config-schema.ts +20 -0
  23. package/scripts/constants/__tests__/environments.test.ts +3 -3
  24. package/scripts/constants/environments.ts +3 -3
  25. package/scripts/utils/multi-repo-validator.ts +160 -1
  26. package/templates/claude/agents/mermaid-validator/AGENT.md +257 -0
  27. package/templates/claude/commands/michi-multi-repo/impl-all.md +264 -0
  28. package/templates/claude/commands/michi-multi-repo/propagate-specs.md +271 -0
  29. package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-design.md +69 -6
  30. package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-init.md +6 -6
  31. package/templates/claude/commands/{michi_multi_repo → michi-multi-repo}/spec-requirements.md +2 -2
  32. package/templates/claude/commands/michi-multi-repo/spec-review.md +247 -0
  33. package/templates/claude/skills/mermaid-validator/SKILL.md +261 -0
  34. package/templates/claude-agent/agents/cross-repo-reviewer.md +194 -0
  35. 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
- - [ ] テストタイプを選択(Phase 0.3)
824
- - [ ] テスト仕様書を作成(Phase 0.4)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sk8metal/michi-cli",
3
- "version": "0.8.6",
3
+ "version": "0.10.1",
4
4
  "description": "Managed Intelligent Comprehensive Hub for Integration - AI-driven development workflow automation",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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/kiro');
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/kiro');
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/kiro');
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/kiro',
24
+ commandsDir: '.claude/commands',
25
25
  templateSource: 'claude',
26
26
  },
27
27
  'claude-agent': {
28
28
  rulesDir: '.claude/agents',
29
- commandsDir: '.claude/commands/kiro',
29
+ commandsDir: '.claude/commands',
30
30
  templateSource: 'claude-agent',
31
31
  },
32
32
  cursor: {
33
33
  rulesDir: '.cursor/rules',
34
- commandsDir: '.cursor/commands/kiro',
34
+ commandsDir: '.cursor/commands',
35
35
  templateSource: 'cursor',
36
36
  },
37
37
  gemini: {