@sk8metal/michi-cli 0.8.7 → 0.11.0

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 (116) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +77 -847
  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/phase-runner.js +1 -1
  8. package/dist/scripts/phase-runner.js.map +1 -1
  9. package/dist/scripts/utils/multi-repo-validator.d.ts +38 -1
  10. package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -1
  11. package/dist/scripts/utils/multi-repo-validator.js +166 -1
  12. package/dist/scripts/utils/multi-repo-validator.js.map +1 -1
  13. package/dist/scripts/utils/tasks-format-validator.js +3 -3
  14. package/dist/scripts/utils/tasks-format-validator.js.map +1 -1
  15. package/docs/README.md +20 -83
  16. package/docs/getting-started/configuration.md +379 -0
  17. package/docs/getting-started/installation.md +59 -0
  18. package/docs/getting-started/quick-start.md +76 -0
  19. package/docs/guides/ai-tools.md +311 -0
  20. package/docs/guides/atlassian-integration.md +116 -0
  21. package/docs/guides/claude-code.md +155 -0
  22. package/docs/guides/multi-repo.md +117 -0
  23. package/docs/guides/workflow.md +382 -0
  24. package/docs/reference/ai-commands.md +92 -0
  25. package/docs/reference/cli.md +756 -0
  26. package/docs/reference/environment-variables.md +192 -0
  27. package/docs/troubleshooting.md +543 -0
  28. package/package.json +1 -1
  29. package/scripts/__tests__/multi-repo-config-schema.test.ts +106 -0
  30. package/scripts/__tests__/multi-repo-validator.test.ts +229 -1
  31. package/scripts/config/config-schema.ts +20 -0
  32. package/scripts/phase-runner.ts +1 -1
  33. package/scripts/utils/__tests__/multi-repo-validator.test.ts +159 -1
  34. package/scripts/utils/multi-repo-validator.ts +210 -1
  35. package/scripts/utils/tasks-format-validator.ts +3 -3
  36. package/templates/claude/agents/e2e-first-planner/AGENT.md +1 -1
  37. package/templates/claude/agents/mermaid-validator/AGENT.md +257 -0
  38. package/templates/claude/agents/pr-resolver/AGENT.md +15 -3
  39. package/templates/claude/commands/michi/e2e-plan.md +1 -1
  40. package/templates/claude/commands/michi/spec-design.md +2 -2
  41. package/templates/claude/commands/michi/spec-tasks.md +156 -0
  42. package/templates/claude/commands/michi/test-planning.md +1 -1
  43. package/templates/claude/commands/michi/validate-design.md +3 -3
  44. package/templates/claude/commands/michi-multi-repo/impl-all.md +293 -0
  45. package/templates/claude/commands/michi-multi-repo/propagate-specs.md +284 -0
  46. package/templates/claude/commands/michi-multi-repo/spec-design.md +66 -3
  47. package/templates/claude/commands/michi-multi-repo/spec-review.md +261 -0
  48. package/templates/claude/skills/mermaid-validator/SKILL.md +261 -0
  49. package/templates/claude-agent/agents/cross-repo-reviewer.md +194 -0
  50. package/templates/claude-agent/agents/repo-spec-executor.md +113 -0
  51. package/templates/claude-agent/commands/michi/spec-tasks.md +117 -0
  52. package/templates/claude-agent/rules/code-size-monitor.md +26 -0
  53. package/templates/claude-agent/rules/code-size-rules.md +32 -0
  54. package/templates/codex/AGENTS.override.md +1 -1
  55. package/templates/codex/rules/README.md +2 -2
  56. package/templates/cursor/commands/michi/spec-tasks.md +117 -0
  57. package/templates/michi/cc-sdd-overrides/settings/rules/design-review-michi.md +1 -1
  58. package/docs/context.md +0 -59
  59. package/docs/michi-development/contributing/development.md +0 -341
  60. package/docs/michi-development/contributing/release.md +0 -365
  61. package/docs/michi-development/design/config-unification.md +0 -733
  62. package/docs/michi-development/design/design-config-current-state.md +0 -330
  63. package/docs/michi-development/design/design-config-implementation.md +0 -628
  64. package/docs/michi-development/design/design-config-migration.md +0 -952
  65. package/docs/michi-development/design/design-config-security.md +0 -771
  66. package/docs/michi-development/design/design-config-solution.md +0 -583
  67. package/docs/michi-development/design/design-config-testing.md +0 -892
  68. package/docs/michi-development/testing/manual-verification-flow.md +0 -871
  69. package/docs/michi-development/testing/manual-verification-other-tools.md +0 -1279
  70. package/docs/michi-development/testing/manual-verification-troubleshooting.md +0 -122
  71. package/docs/michi-development/testing/pre-publish-checklist.md +0 -560
  72. package/docs/michi-development/testing-strategy.md +0 -87
  73. package/docs/plan.md +0 -275
  74. package/docs/user-guide/getting-started/github-token-setup.md +0 -510
  75. package/docs/user-guide/getting-started/new-repository-setup.md +0 -704
  76. package/docs/user-guide/getting-started/quick-start.md +0 -212
  77. package/docs/user-guide/getting-started/setup.md +0 -819
  78. package/docs/user-guide/guides/agent-skills-integration.md +0 -222
  79. package/docs/user-guide/guides/customization.md +0 -537
  80. package/docs/user-guide/guides/internationalization.md +0 -540
  81. package/docs/user-guide/guides/migration-guide.md +0 -138
  82. package/docs/user-guide/guides/multi-project.md +0 -368
  83. package/docs/user-guide/guides/multi-repo-guide.md +0 -1147
  84. package/docs/user-guide/guides/phase-automation.md +0 -419
  85. package/docs/user-guide/guides/workflow.md +0 -584
  86. package/docs/user-guide/hands-on/README.md +0 -142
  87. package/docs/user-guide/hands-on/claude-agent-setup.md +0 -597
  88. package/docs/user-guide/hands-on/claude-setup.md +0 -452
  89. package/docs/user-guide/hands-on/cursor-setup.md +0 -353
  90. package/docs/user-guide/hands-on/troubleshooting.md +0 -964
  91. package/docs/user-guide/hands-on/verification-checklist.md +0 -439
  92. package/docs/user-guide/hands-on/workflow-walkthrough.md +0 -909
  93. package/docs/user-guide/reference/config.md +0 -589
  94. package/docs/user-guide/reference/multi-repo-api.md +0 -771
  95. package/docs/user-guide/reference/quick-reference.md +0 -297
  96. package/docs/user-guide/reference/security-test-payloads.md +0 -50
  97. package/docs/user-guide/reference/tasks-template.md +0 -550
  98. package/docs/user-guide/release/ci-setup-java.md +0 -114
  99. package/docs/user-guide/release/ci-setup-nodejs.md +0 -94
  100. package/docs/user-guide/release/ci-setup-php.md +0 -102
  101. package/docs/user-guide/release/ci-setup-troubleshooting.md +0 -94
  102. package/docs/user-guide/release/ci-setup.md +0 -188
  103. package/docs/user-guide/release/release-flow.md +0 -476
  104. package/docs/user-guide/templates/test-specs/README.md +0 -173
  105. package/docs/user-guide/templates/test-specs/e2e-test-spec-template.md +0 -553
  106. package/docs/user-guide/templates/test-specs/integration-test-spec-template.md +0 -435
  107. package/docs/user-guide/templates/test-specs/performance-test-spec-template.md +0 -454
  108. package/docs/user-guide/templates/test-specs/security-test-spec-template.md +0 -625
  109. package/docs/user-guide/templates/test-specs/unit-test-spec-template.md +0 -328
  110. package/docs/user-guide/testing/integration-tests.md +0 -312
  111. package/docs/user-guide/testing/tdd-cycle.md +0 -349
  112. package/docs/user-guide/testing/test-execution-flow.md +0 -396
  113. package/docs/user-guide/testing/test-failure-handling.md +0 -521
  114. package/docs/user-guide/testing/test-planning-flow.md +0 -185
  115. package/docs/user-guide/testing-strategy.md +0 -185
  116. package/docs/verification-guide.md +0 -518
@@ -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
  /**
@@ -229,7 +229,7 @@ async function checkTasksPrerequisites(
229
229
  const tasksPath = join(process.cwd(), '.kiro', 'specs', feature, 'tasks.md');
230
230
  if (!existsSync(tasksPath)) {
231
231
  errors.push(
232
- 'tasks.mdが存在しません。先に/kiro:spec-tasks を実行してください',
232
+ 'tasks.mdが存在しません。先に/michi:spec-tasks を実行してください',
233
233
  );
234
234
  return { valid: false, errors };
235
235
  }
@@ -3,12 +3,19 @@
3
3
  * Task 1.2: バリデーション関数の実装
4
4
  */
5
5
 
6
- import { describe, it, expect } from 'vitest';
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
7
  import {
8
8
  validateProjectName,
9
9
  validateJiraKey,
10
10
  validateRepositoryUrl,
11
+ hasMichiSetup,
12
+ getMichiSetupCommand,
13
+ validateLocalPath,
11
14
  } from '../multi-repo-validator';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as os from 'os';
18
+ import type { Repository } from '../../config/config-schema';
12
19
 
13
20
  describe('validateProjectName', () => {
14
21
  describe('正常ケース', () => {
@@ -333,3 +340,154 @@ describe('validateRepositoryUrl', () => {
333
340
  });
334
341
  });
335
342
  });
343
+
344
+ describe('hasMichiSetup', () => {
345
+ let tempDir: string;
346
+
347
+ beforeEach(() => {
348
+ // 一時ディレクトリを作成
349
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'michi-test-'));
350
+ });
351
+
352
+ afterEach(() => {
353
+ // 一時ディレクトリを削除
354
+ fs.rmSync(tempDir, { recursive: true, force: true });
355
+ });
356
+
357
+ describe('Michi導入済みの場合', () => {
358
+ it('.kiro/project.json が存在する場合はtrueを返す', () => {
359
+ // .kiro ディレクトリを作成
360
+ const kiroDir = path.join(tempDir, '.kiro');
361
+ fs.mkdirSync(kiroDir);
362
+
363
+ // project.json を作成
364
+ const projectJson = path.join(kiroDir, 'project.json');
365
+ fs.writeFileSync(projectJson, '{}');
366
+
367
+ const result = hasMichiSetup(tempDir);
368
+ expect(result).toBe(true);
369
+ });
370
+ });
371
+
372
+ describe('Michi未導入の場合', () => {
373
+ it('.kiro/project.json が存在しない場合はfalseを返す', () => {
374
+ // .kiro ディレクトリのみ作成(project.jsonなし)
375
+ const kiroDir = path.join(tempDir, '.kiro');
376
+ fs.mkdirSync(kiroDir);
377
+
378
+ const result = hasMichiSetup(tempDir);
379
+ expect(result).toBe(false);
380
+ });
381
+
382
+ it('.kiro ディレクトリ自体が存在しない場合はfalseを返す', () => {
383
+ // .kiro ディレクトリを作成しない
384
+ const result = hasMichiSetup(tempDir);
385
+ expect(result).toBe(false);
386
+ });
387
+ });
388
+ });
389
+
390
+ describe('getMichiSetupCommand', () => {
391
+ it('正しいセットアップコマンドを生成する(シングルクォートでラップ)', () => {
392
+ const localPath = '/path/to/repo';
393
+ const command = getMichiSetupCommand(localPath);
394
+ expect(command).toBe(
395
+ "cd '/path/to/repo' && npx @sk8metal/michi-cli@latest init",
396
+ );
397
+ });
398
+
399
+ it('スペースを含むパスを正しくエスケープする', () => {
400
+ const localPath = '/path/to/my repo';
401
+ const command = getMichiSetupCommand(localPath);
402
+ expect(command).toBe(
403
+ "cd '/path/to/my repo' && npx @sk8metal/michi-cli@latest init",
404
+ );
405
+ });
406
+
407
+ it('シングルクォートを含むパスを正しくエスケープする', () => {
408
+ const localPath = "/path/to/user's repo";
409
+ const command = getMichiSetupCommand(localPath);
410
+ expect(command).toBe(
411
+ "cd '/path/to/user'\\''s repo' && npx @sk8metal/michi-cli@latest init",
412
+ );
413
+ });
414
+
415
+ it('複数のシングルクォートを含むパスを正しくエスケープする', () => {
416
+ const localPath = "/path/to/'test'/repo's/dir";
417
+ const command = getMichiSetupCommand(localPath);
418
+ expect(command).toBe(
419
+ "cd '/path/to/'\\''test'\\''/repo'\\''s/dir' && npx @sk8metal/michi-cli@latest init",
420
+ );
421
+ });
422
+
423
+ it('スペースとシングルクォート両方を含むパスを正しくエスケープする', () => {
424
+ const localPath = "/path/to/user's test repo";
425
+ const command = getMichiSetupCommand(localPath);
426
+ expect(command).toBe(
427
+ "cd '/path/to/user'\\''s test repo' && npx @sk8metal/michi-cli@latest init",
428
+ );
429
+ });
430
+ });
431
+
432
+ describe('validateLocalPath - Michi導入チェック', () => {
433
+ let tempDir: string;
434
+ let repository: Repository;
435
+
436
+ beforeEach(() => {
437
+ // 一時ディレクトリを作成
438
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'michi-test-'));
439
+
440
+ // リポジトリオブジェクトを作成
441
+ repository = {
442
+ name: 'test-repo',
443
+ url: 'https://github.com/test/repo',
444
+ branch: 'main',
445
+ localPath: tempDir,
446
+ };
447
+ });
448
+
449
+ afterEach(() => {
450
+ // 一時ディレクトリを削除
451
+ fs.rmSync(tempDir, { recursive: true, force: true });
452
+ });
453
+
454
+ it('Michi導入済みリポジトリの場合、hasMichiSetupがtrueになる', () => {
455
+ // .git ディレクトリを作成(Gitリポジトリとして認識させる)
456
+ fs.mkdirSync(path.join(tempDir, '.git'));
457
+
458
+ // .kiro/project.json を作成(Michi導入済み)
459
+ const kiroDir = path.join(tempDir, '.kiro');
460
+ fs.mkdirSync(kiroDir);
461
+ fs.writeFileSync(path.join(kiroDir, 'project.json'), '{}');
462
+
463
+ const result = validateLocalPath(repository);
464
+ expect(result.hasMichiSetup).toBe(true);
465
+ expect(result.michiSetupCommand).toBeNull();
466
+ });
467
+
468
+ it('Michi未導入リポジトリの場合、hasMichiSetupがfalseになり警告が追加される', () => {
469
+ // .git ディレクトリを作成(Gitリポジトリとして認識させる)
470
+ fs.mkdirSync(path.join(tempDir, '.git'));
471
+
472
+ // .kiro/project.json は作成しない(Michi未導入)
473
+
474
+ const result = validateLocalPath(repository);
475
+ expect(result.hasMichiSetup).toBe(false);
476
+ expect(result.michiSetupCommand).not.toBeNull();
477
+ expect(result.warnings).toContain(
478
+ `Repository 'test-repo' does not have Michi setup (.kiro/project.json not found)`,
479
+ );
480
+ });
481
+
482
+ it('Gitリポジトリでない場合、hasMichiSetupはfalseでmichiSetupCommandはnull', () => {
483
+ // .git ディレクトリを作成しない(Gitリポジトリでない)
484
+
485
+ const result = validateLocalPath(repository);
486
+ expect(result.isValid).toBe(false);
487
+ expect(result.hasMichiSetup).toBe(false);
488
+ expect(result.michiSetupCommand).toBeNull();
489
+ expect(result.errors).toContain(
490
+ `localPath '${tempDir}' is not a Git repository (no .git directory)`,
491
+ );
492
+ });
493
+ });
@@ -2,9 +2,14 @@
2
2
  * multi-repo-validator.ts
3
3
  * Multi-Repo機能のバリデーションユーティリティ
4
4
  *
5
- * プロジェクト名、JIRAキー、リポジトリURLのバリデーションとセキュリティチェックを行います。
5
+ * プロジェクト名、JIRAキー、リポジトリURL、localPathのバリデーションとセキュリティチェックを行います。
6
6
  */
7
7
 
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { execSync } from 'child_process';
11
+ import type { Repository } from '../config/config-schema.js';
12
+
8
13
  /**
9
14
  * バリデーション結果
10
15
  */
@@ -14,6 +19,19 @@ export interface ValidationResult {
14
19
  warnings: string[];
15
20
  }
16
21
 
22
+ /**
23
+ * LocalPathバリデーション結果(詳細情報付き)
24
+ */
25
+ export interface LocalPathValidationResult extends ValidationResult {
26
+ exists: boolean;
27
+ isGitRepository: boolean;
28
+ currentBranch: string | null;
29
+ branchMatches: boolean;
30
+ hasUncommittedChanges: boolean;
31
+ hasMichiSetup: boolean;
32
+ michiSetupCommand: string | null;
33
+ }
34
+
17
35
  /**
18
36
  * プロジェクト名のバリデーション
19
37
  * セキュリティ対策: パストラバーサル、相対パス、制御文字をチェック
@@ -139,3 +157,194 @@ export function validateRepositoryUrl(url: string): ValidationResult {
139
157
  warnings,
140
158
  };
141
159
  }
160
+
161
+ /**
162
+ * Michi導入状況をチェック
163
+ * .kiro/project.json の存在でMichi導入済みと判定
164
+ *
165
+ * @param localPath - 子リポジトリのlocalPath
166
+ * @returns Michi導入済みかどうか
167
+ */
168
+ export function hasMichiSetup(localPath: string): boolean {
169
+ const projectJsonPath = path.join(localPath, '.kiro', 'project.json');
170
+ return fs.existsSync(projectJsonPath);
171
+ }
172
+
173
+ /**
174
+ * Michi導入コマンドを生成
175
+ * パスに空白やシングルクォートが含まれる場合に対応
176
+ *
177
+ * @param localPath - 子リポジトリのlocalPath
178
+ * @returns セットアップコマンド文字列
179
+ */
180
+ export function getMichiSetupCommand(localPath: string): string {
181
+ // シングルクォートをエスケープ: ' → '\''
182
+ // eslint-disable-next-line quotes
183
+ const escapedPath = localPath.replace(/'/g, "'\\''");
184
+ return `cd '${escapedPath}' && npx @sk8metal/michi-cli@latest init`;
185
+ }
186
+
187
+ /**
188
+ * LocalPathのバリデーション
189
+ * ディレクトリ存在、Gitリポジトリ、ブランチ、未コミット変更をチェック
190
+ *
191
+ * @param repository - リポジトリ設定
192
+ * @returns バリデーション結果(詳細情報付き)
193
+ */
194
+ export function validateLocalPath(
195
+ repository: Repository,
196
+ ): LocalPathValidationResult {
197
+ const errors: string[] = [];
198
+ const warnings: string[] = [];
199
+
200
+ // 初期値
201
+ let exists = false;
202
+ let isGitRepository = false;
203
+ let currentBranch: string | null = null;
204
+ let branchMatches = false;
205
+ let hasUncommittedChanges = false;
206
+ let hasMichiSetupResult = false;
207
+ let michiSetupCommand: string | null = null;
208
+
209
+ // 1. localPath設定確認
210
+ if (!repository.localPath) {
211
+ warnings.push(
212
+ `Repository '${repository.name}' does not have localPath configured`,
213
+ );
214
+ return {
215
+ isValid: false,
216
+ errors,
217
+ warnings,
218
+ exists,
219
+ isGitRepository,
220
+ currentBranch,
221
+ branchMatches,
222
+ hasUncommittedChanges,
223
+ hasMichiSetup: hasMichiSetupResult,
224
+ michiSetupCommand,
225
+ };
226
+ }
227
+
228
+ const localPath = repository.localPath;
229
+
230
+ // 2. ディレクトリ存在確認
231
+ try {
232
+ const stats = fs.statSync(localPath);
233
+ if (!stats.isDirectory()) {
234
+ errors.push(
235
+ `localPath '${localPath}' exists but is not a directory`,
236
+ );
237
+ return {
238
+ isValid: false,
239
+ errors,
240
+ warnings,
241
+ exists: true,
242
+ isGitRepository,
243
+ currentBranch,
244
+ branchMatches,
245
+ hasUncommittedChanges,
246
+ hasMichiSetup: hasMichiSetupResult,
247
+ michiSetupCommand,
248
+ };
249
+ }
250
+ exists = true;
251
+ } catch (_error) {
252
+ errors.push(`localPath '${localPath}' does not exist`);
253
+ return {
254
+ isValid: false,
255
+ errors,
256
+ warnings,
257
+ exists,
258
+ isGitRepository,
259
+ currentBranch,
260
+ branchMatches,
261
+ hasUncommittedChanges,
262
+ hasMichiSetup: hasMichiSetupResult,
263
+ michiSetupCommand,
264
+ };
265
+ }
266
+
267
+ // 3. Gitリポジトリ確認
268
+ const gitDir = path.join(localPath, '.git');
269
+ if (!fs.existsSync(gitDir)) {
270
+ errors.push(
271
+ `localPath '${localPath}' is not a Git repository (no .git directory)`,
272
+ );
273
+ return {
274
+ isValid: false,
275
+ errors,
276
+ warnings,
277
+ exists,
278
+ isGitRepository,
279
+ currentBranch,
280
+ branchMatches,
281
+ hasUncommittedChanges,
282
+ hasMichiSetup: hasMichiSetupResult,
283
+ michiSetupCommand,
284
+ };
285
+ }
286
+ isGitRepository = true;
287
+
288
+ // 4. ブランチ確認
289
+ try {
290
+ currentBranch = execSync('git branch --show-current', {
291
+ cwd: localPath,
292
+ encoding: 'utf-8',
293
+ }).trim();
294
+
295
+ if (currentBranch !== repository.branch) {
296
+ warnings.push(
297
+ `Current branch '${currentBranch}' does not match configured branch '${repository.branch}'`,
298
+ );
299
+ branchMatches = false;
300
+ } else {
301
+ branchMatches = true;
302
+ }
303
+ } catch (error) {
304
+ warnings.push(
305
+ `Failed to get current branch: ${error instanceof Error ? error.message : String(error)}`,
306
+ );
307
+ }
308
+
309
+ // 5. 未コミット変更確認
310
+ try {
311
+ const statusOutput = execSync('git status --porcelain', {
312
+ cwd: localPath,
313
+ encoding: 'utf-8',
314
+ }).trim();
315
+
316
+ if (statusOutput.length > 0) {
317
+ warnings.push(
318
+ `Repository '${repository.name}' has uncommitted changes`,
319
+ );
320
+ hasUncommittedChanges = true;
321
+ }
322
+ } catch (error) {
323
+ warnings.push(
324
+ `Failed to check uncommitted changes: ${error instanceof Error ? error.message : String(error)}`,
325
+ );
326
+ }
327
+
328
+ // 6. Michi導入状況確認
329
+ hasMichiSetupResult = hasMichiSetup(localPath);
330
+
331
+ if (!hasMichiSetupResult) {
332
+ michiSetupCommand = getMichiSetupCommand(localPath);
333
+ warnings.push(
334
+ `Repository '${repository.name}' does not have Michi setup (.kiro/project.json not found)`,
335
+ );
336
+ }
337
+
338
+ return {
339
+ isValid: errors.length === 0,
340
+ errors,
341
+ warnings,
342
+ exists,
343
+ isGitRepository,
344
+ currentBranch,
345
+ branchMatches,
346
+ hasUncommittedChanges,
347
+ hasMichiSetup: hasMichiSetupResult,
348
+ michiSetupCommand,
349
+ };
350
+ }