@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.
- package/CHANGELOG.md +70 -1
- package/README.md +77 -847
- 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/phase-runner.js +1 -1
- package/dist/scripts/phase-runner.js.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.d.ts +38 -1
- package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -1
- package/dist/scripts/utils/multi-repo-validator.js +166 -1
- package/dist/scripts/utils/multi-repo-validator.js.map +1 -1
- package/dist/scripts/utils/tasks-format-validator.js +3 -3
- package/dist/scripts/utils/tasks-format-validator.js.map +1 -1
- package/docs/README.md +20 -83
- package/docs/getting-started/configuration.md +379 -0
- package/docs/getting-started/installation.md +59 -0
- package/docs/getting-started/quick-start.md +76 -0
- package/docs/guides/ai-tools.md +311 -0
- package/docs/guides/atlassian-integration.md +116 -0
- package/docs/guides/claude-code.md +155 -0
- package/docs/guides/multi-repo.md +117 -0
- package/docs/guides/workflow.md +382 -0
- package/docs/reference/ai-commands.md +92 -0
- package/docs/reference/cli.md +756 -0
- package/docs/reference/environment-variables.md +192 -0
- package/docs/troubleshooting.md +543 -0
- 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/phase-runner.ts +1 -1
- package/scripts/utils/__tests__/multi-repo-validator.test.ts +159 -1
- package/scripts/utils/multi-repo-validator.ts +210 -1
- package/scripts/utils/tasks-format-validator.ts +3 -3
- package/templates/claude/agents/e2e-first-planner/AGENT.md +1 -1
- package/templates/claude/agents/mermaid-validator/AGENT.md +257 -0
- package/templates/claude/agents/pr-resolver/AGENT.md +15 -3
- package/templates/claude/commands/michi/e2e-plan.md +1 -1
- package/templates/claude/commands/michi/spec-design.md +2 -2
- package/templates/claude/commands/michi/spec-tasks.md +156 -0
- package/templates/claude/commands/michi/test-planning.md +1 -1
- package/templates/claude/commands/michi/validate-design.md +3 -3
- package/templates/claude/commands/michi-multi-repo/impl-all.md +293 -0
- package/templates/claude/commands/michi-multi-repo/propagate-specs.md +284 -0
- package/templates/claude/commands/michi-multi-repo/spec-design.md +66 -3
- package/templates/claude/commands/michi-multi-repo/spec-review.md +261 -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
- package/templates/claude-agent/commands/michi/spec-tasks.md +117 -0
- package/templates/claude-agent/rules/code-size-monitor.md +26 -0
- package/templates/claude-agent/rules/code-size-rules.md +32 -0
- package/templates/codex/AGENTS.override.md +1 -1
- package/templates/codex/rules/README.md +2 -2
- package/templates/cursor/commands/michi/spec-tasks.md +117 -0
- package/templates/michi/cc-sdd-overrides/settings/rules/design-review-michi.md +1 -1
- package/docs/context.md +0 -59
- package/docs/michi-development/contributing/development.md +0 -341
- package/docs/michi-development/contributing/release.md +0 -365
- package/docs/michi-development/design/config-unification.md +0 -733
- package/docs/michi-development/design/design-config-current-state.md +0 -330
- package/docs/michi-development/design/design-config-implementation.md +0 -628
- package/docs/michi-development/design/design-config-migration.md +0 -952
- package/docs/michi-development/design/design-config-security.md +0 -771
- package/docs/michi-development/design/design-config-solution.md +0 -583
- package/docs/michi-development/design/design-config-testing.md +0 -892
- package/docs/michi-development/testing/manual-verification-flow.md +0 -871
- package/docs/michi-development/testing/manual-verification-other-tools.md +0 -1279
- package/docs/michi-development/testing/manual-verification-troubleshooting.md +0 -122
- package/docs/michi-development/testing/pre-publish-checklist.md +0 -560
- package/docs/michi-development/testing-strategy.md +0 -87
- package/docs/plan.md +0 -275
- package/docs/user-guide/getting-started/github-token-setup.md +0 -510
- package/docs/user-guide/getting-started/new-repository-setup.md +0 -704
- package/docs/user-guide/getting-started/quick-start.md +0 -212
- package/docs/user-guide/getting-started/setup.md +0 -819
- package/docs/user-guide/guides/agent-skills-integration.md +0 -222
- package/docs/user-guide/guides/customization.md +0 -537
- package/docs/user-guide/guides/internationalization.md +0 -540
- package/docs/user-guide/guides/migration-guide.md +0 -138
- package/docs/user-guide/guides/multi-project.md +0 -368
- package/docs/user-guide/guides/multi-repo-guide.md +0 -1147
- package/docs/user-guide/guides/phase-automation.md +0 -419
- package/docs/user-guide/guides/workflow.md +0 -584
- package/docs/user-guide/hands-on/README.md +0 -142
- package/docs/user-guide/hands-on/claude-agent-setup.md +0 -597
- package/docs/user-guide/hands-on/claude-setup.md +0 -452
- package/docs/user-guide/hands-on/cursor-setup.md +0 -353
- package/docs/user-guide/hands-on/troubleshooting.md +0 -964
- package/docs/user-guide/hands-on/verification-checklist.md +0 -439
- package/docs/user-guide/hands-on/workflow-walkthrough.md +0 -909
- package/docs/user-guide/reference/config.md +0 -589
- package/docs/user-guide/reference/multi-repo-api.md +0 -771
- package/docs/user-guide/reference/quick-reference.md +0 -297
- package/docs/user-guide/reference/security-test-payloads.md +0 -50
- package/docs/user-guide/reference/tasks-template.md +0 -550
- package/docs/user-guide/release/ci-setup-java.md +0 -114
- package/docs/user-guide/release/ci-setup-nodejs.md +0 -94
- package/docs/user-guide/release/ci-setup-php.md +0 -102
- package/docs/user-guide/release/ci-setup-troubleshooting.md +0 -94
- package/docs/user-guide/release/ci-setup.md +0 -188
- package/docs/user-guide/release/release-flow.md +0 -476
- package/docs/user-guide/templates/test-specs/README.md +0 -173
- package/docs/user-guide/templates/test-specs/e2e-test-spec-template.md +0 -553
- package/docs/user-guide/templates/test-specs/integration-test-spec-template.md +0 -435
- package/docs/user-guide/templates/test-specs/performance-test-spec-template.md +0 -454
- package/docs/user-guide/templates/test-specs/security-test-spec-template.md +0 -625
- package/docs/user-guide/templates/test-specs/unit-test-spec-template.md +0 -328
- package/docs/user-guide/testing/integration-tests.md +0 -312
- package/docs/user-guide/testing/tdd-cycle.md +0 -349
- package/docs/user-guide/testing/test-execution-flow.md +0 -396
- package/docs/user-guide/testing/test-failure-handling.md +0 -521
- package/docs/user-guide/testing/test-planning-flow.md +0 -185
- package/docs/user-guide/testing-strategy.md +0 -185
- 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
|
/**
|
package/scripts/phase-runner.ts
CHANGED
|
@@ -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が存在しません。先に/
|
|
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
|
+
}
|