opencode-conductor-cdd-plugin 1.0.0-beta.17 → 1.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/prompts/agent/cdd.md +16 -16
  2. package/dist/prompts/agent/implementer.md +5 -5
  3. package/dist/prompts/agent.md +7 -7
  4. package/dist/prompts/cdd/implement.json +1 -1
  5. package/dist/prompts/cdd/revert.json +1 -1
  6. package/dist/prompts/cdd/setup.json +2 -2
  7. package/dist/prompts/cdd/setup.test.js +40 -118
  8. package/dist/prompts/cdd/setup.test.ts +40 -143
  9. package/dist/test/integration/rebrand.test.js +15 -14
  10. package/dist/utils/agentMapping.js +2 -0
  11. package/dist/utils/archive-tracks.d.ts +28 -0
  12. package/dist/utils/archive-tracks.js +154 -1
  13. package/dist/utils/archive-tracks.test.d.ts +1 -0
  14. package/dist/utils/archive-tracks.test.js +495 -0
  15. package/dist/utils/codebaseAnalysis.d.ts +61 -0
  16. package/dist/utils/codebaseAnalysis.js +429 -0
  17. package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
  18. package/dist/utils/codebaseAnalysis.test.js +556 -0
  19. package/dist/utils/documentGeneration.d.ts +97 -0
  20. package/dist/utils/documentGeneration.js +301 -0
  21. package/dist/utils/documentGeneration.test.d.ts +1 -0
  22. package/dist/utils/documentGeneration.test.js +380 -0
  23. package/dist/utils/interactiveMenu.d.ts +56 -0
  24. package/dist/utils/interactiveMenu.js +144 -0
  25. package/dist/utils/interactiveMenu.test.d.ts +1 -0
  26. package/dist/utils/interactiveMenu.test.js +231 -0
  27. package/dist/utils/interactiveSetup.d.ts +43 -0
  28. package/dist/utils/interactiveSetup.js +131 -0
  29. package/dist/utils/interactiveSetup.test.d.ts +1 -0
  30. package/dist/utils/interactiveSetup.test.js +124 -0
  31. package/dist/utils/metadataTracker.d.ts +39 -0
  32. package/dist/utils/metadataTracker.js +105 -0
  33. package/dist/utils/metadataTracker.test.d.ts +1 -0
  34. package/dist/utils/metadataTracker.test.js +265 -0
  35. package/dist/utils/planParser.d.ts +25 -0
  36. package/dist/utils/planParser.js +107 -0
  37. package/dist/utils/planParser.test.d.ts +1 -0
  38. package/dist/utils/planParser.test.js +119 -0
  39. package/dist/utils/projectMaturity.d.ts +53 -0
  40. package/dist/utils/projectMaturity.js +179 -0
  41. package/dist/utils/projectMaturity.test.d.ts +1 -0
  42. package/dist/utils/projectMaturity.test.js +298 -0
  43. package/dist/utils/questionGenerator.d.ts +51 -0
  44. package/dist/utils/questionGenerator.js +535 -0
  45. package/dist/utils/questionGenerator.test.d.ts +1 -0
  46. package/dist/utils/questionGenerator.test.js +328 -0
  47. package/dist/utils/setupIntegration.d.ts +72 -0
  48. package/dist/utils/setupIntegration.js +179 -0
  49. package/dist/utils/setupIntegration.test.d.ts +1 -0
  50. package/dist/utils/setupIntegration.test.js +344 -0
  51. package/dist/utils/statusDisplay.d.ts +35 -0
  52. package/dist/utils/statusDisplay.js +81 -0
  53. package/dist/utils/statusDisplay.test.d.ts +1 -0
  54. package/dist/utils/statusDisplay.test.js +102 -0
  55. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { addPhaseDelimiters, extractActivePhase, extractPhaseByName } from "./planParser.js";
3
+ describe("planParser", () => {
4
+ const samplePlan = `# Implementation Plan: Sample Track
5
+
6
+ This is the introduction.
7
+
8
+ ## Phase 1: Setup
9
+ *Goal: Set up the project infrastructure.*
10
+
11
+ - [x] Task: Initialize repository (abc1234)
12
+ - [x] Task: Configure tools (def5678)
13
+ - [x] Task: Orchestrator - User Manual Verification 'Phase 1: Setup' [checkpoint: aaa1111]
14
+
15
+ ## Phase 2: Implementation
16
+ *Goal: Implement core features.*
17
+
18
+ - [~] Task: Design API
19
+ - [ ] Task: Implement endpoints
20
+ - [ ] Task: Orchestrator - User Manual Verification 'Phase 2: Implementation'
21
+
22
+ ## Phase 3: Testing
23
+ *Goal: Write and run tests.*
24
+
25
+ - [ ] Task: Unit tests
26
+ - [ ] Task: Integration tests
27
+ - [ ] Task: Orchestrator - User Manual Verification 'Phase 3: Testing'
28
+ `;
29
+ describe("addPhaseDelimiters", () => {
30
+ it("should add XML-style phase delimiters to plan.md content", () => {
31
+ const delimited = addPhaseDelimiters(samplePlan);
32
+ expect(delimited).toContain("<!-- PHASE_START:phase1 -->");
33
+ expect(delimited).toContain("<!-- PHASE_END:phase1 -->");
34
+ expect(delimited).toContain("<!-- PHASE_START:phase2 -->");
35
+ expect(delimited).toContain("<!-- PHASE_END:phase2 -->");
36
+ expect(delimited).toContain("<!-- PHASE_START:phase3 -->");
37
+ expect(delimited).toContain("<!-- PHASE_END:phase3 -->");
38
+ });
39
+ it("should preserve all original content", () => {
40
+ const delimited = addPhaseDelimiters(samplePlan);
41
+ expect(delimited).toContain("# Implementation Plan: Sample Track");
42
+ expect(delimited).toContain("## Phase 1: Setup");
43
+ expect(delimited).toContain("- [x] Task: Initialize repository");
44
+ expect(delimited).toContain("## Phase 2: Implementation");
45
+ expect(delimited).toContain("## Phase 3: Testing");
46
+ });
47
+ it("should handle plan with no phases", () => {
48
+ const noPhasePlan = "# Plan\n\nJust some intro text.";
49
+ const delimited = addPhaseDelimiters(noPhasePlan);
50
+ expect(delimited).toBe(noPhasePlan);
51
+ });
52
+ });
53
+ describe("extractActivePhase", () => {
54
+ it("should extract the first in-progress phase", () => {
55
+ const delimited = addPhaseDelimiters(samplePlan);
56
+ const activePhase = extractActivePhase(delimited);
57
+ expect(activePhase).not.toBeNull();
58
+ expect(activePhase?.phaseId).toBe("phase2");
59
+ expect(activePhase?.content).toContain("## Phase 2: Implementation");
60
+ expect(activePhase?.content).toContain("- [~] Task: Design API");
61
+ });
62
+ it("should extract the first pending phase if no in-progress phase", () => {
63
+ const allCompletedPlan = `# Plan
64
+
65
+ <!-- PHASE_START:phase1 -->
66
+ ## Phase 1: Setup
67
+ - [x] Task: Done [checkpoint: xxx]
68
+ <!-- PHASE_END:phase1 -->
69
+
70
+ <!-- PHASE_START:phase2 -->
71
+ ## Phase 2: Testing
72
+ - [ ] Task: Todo
73
+ <!-- PHASE_END:phase2 -->`;
74
+ const activePhase = extractActivePhase(allCompletedPlan);
75
+ expect(activePhase).not.toBeNull();
76
+ expect(activePhase?.phaseId).toBe("phase2");
77
+ });
78
+ it("should return null if all phases are completed", () => {
79
+ const allCompletedPlan = `# Plan
80
+
81
+ <!-- PHASE_START:phase1 -->
82
+ ## Phase 1: Setup
83
+ - [x] Task: Done [checkpoint: xxx]
84
+ <!-- PHASE_END:phase1 -->
85
+
86
+ <!-- PHASE_START:phase2 -->
87
+ ## Phase 2: Testing
88
+ - [x] Task: Done [checkpoint: yyy]
89
+ <!-- PHASE_END:phase2 -->`;
90
+ const activePhase = extractActivePhase(allCompletedPlan);
91
+ expect(activePhase).toBeNull();
92
+ });
93
+ it("should return null if no delimiters found", () => {
94
+ const activePhase = extractActivePhase(samplePlan);
95
+ expect(activePhase).toBeNull();
96
+ });
97
+ });
98
+ describe("extractPhaseByName", () => {
99
+ it("should extract a specific phase by name", () => {
100
+ const delimited = addPhaseDelimiters(samplePlan);
101
+ const phase = extractPhaseByName(delimited, "Phase 1: Setup");
102
+ expect(phase).not.toBeNull();
103
+ expect(phase?.phaseId).toBe("phase1");
104
+ expect(phase?.content).toContain("## Phase 1: Setup");
105
+ expect(phase?.content).toContain("- [x] Task: Initialize repository");
106
+ });
107
+ it("should return null if phase name not found", () => {
108
+ const delimited = addPhaseDelimiters(samplePlan);
109
+ const phase = extractPhaseByName(delimited, "Phase 99: Nonexistent");
110
+ expect(phase).toBeNull();
111
+ });
112
+ it("should handle partial name matches", () => {
113
+ const delimited = addPhaseDelimiters(samplePlan);
114
+ const phase = extractPhaseByName(delimited, "Implementation");
115
+ expect(phase).not.toBeNull();
116
+ expect(phase?.phaseId).toBe("phase2");
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Project Maturity Detection Module
3
+ *
4
+ * Determines whether a project is brownfield (existing) or greenfield (new).
5
+ *
6
+ * Based on reference implementations:
7
+ * - derekbar90/opencode-conductor
8
+ * - gemini-cli-extensions/conductor
9
+ */
10
+ export type ProjectMaturity = 'brownfield' | 'greenfield';
11
+ /**
12
+ * Detect project maturity (brownfield vs greenfield)
13
+ *
14
+ * A project is considered BROWNFIELD if ANY of:
15
+ * - Version control directory exists (.git, .svn, .hg)
16
+ * - Git repository is dirty (uncommitted changes)
17
+ * - Dependency manifests exist
18
+ * - Source directories contain code files
19
+ *
20
+ * A project is considered GREENFIELD only if NONE of the above are true.
21
+ *
22
+ * @param projectPath - Absolute path to project directory
23
+ * @returns 'brownfield' or 'greenfield'
24
+ */
25
+ export declare function detectProjectMaturity(projectPath: string): ProjectMaturity;
26
+ /**
27
+ * Check if git repository has uncommitted changes
28
+ *
29
+ * @param projectPath - Absolute path to project directory
30
+ * @returns true if repository is dirty, false otherwise
31
+ */
32
+ export declare function checkGitStatus(projectPath: string): boolean;
33
+ /**
34
+ * Check for dependency manifest files
35
+ *
36
+ * @param projectPath - Absolute path to project directory
37
+ * @returns Array of found manifest file names
38
+ */
39
+ export declare function checkManifestFiles(projectPath: string): string[];
40
+ /**
41
+ * Check for source directories containing code files
42
+ *
43
+ * @param projectPath - Absolute path to project directory
44
+ * @returns Array of found source directory names
45
+ */
46
+ export declare function checkSourceDirectories(projectPath: string): string[];
47
+ /**
48
+ * Check for version control system directories
49
+ *
50
+ * @param projectPath - Absolute path to project directory
51
+ * @returns Name of VCS directory found, or null if none found
52
+ */
53
+ export declare function checkVersionControl(projectPath: string): string | null;
@@ -0,0 +1,179 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ /**
5
+ * Manifest files that indicate a brownfield project
6
+ */
7
+ const MANIFEST_FILES = [
8
+ 'package.json',
9
+ 'pom.xml',
10
+ 'requirements.txt',
11
+ 'go.mod',
12
+ 'Cargo.toml',
13
+ 'Gemfile',
14
+ ];
15
+ /**
16
+ * Source directory names to check for code
17
+ */
18
+ const SOURCE_DIRECTORIES = ['src', 'app', 'lib'];
19
+ /**
20
+ * Code file extensions to detect
21
+ */
22
+ const CODE_EXTENSIONS = ['.ts', '.js', '.py', '.go', '.rs', '.rb', '.java', '.kt', '.swift'];
23
+ /**
24
+ * Version control system directories
25
+ */
26
+ const VCS_DIRECTORIES = ['.git', '.svn', '.hg'];
27
+ /**
28
+ * Detect project maturity (brownfield vs greenfield)
29
+ *
30
+ * A project is considered BROWNFIELD if ANY of:
31
+ * - Version control directory exists (.git, .svn, .hg)
32
+ * - Git repository is dirty (uncommitted changes)
33
+ * - Dependency manifests exist
34
+ * - Source directories contain code files
35
+ *
36
+ * A project is considered GREENFIELD only if NONE of the above are true.
37
+ *
38
+ * @param projectPath - Absolute path to project directory
39
+ * @returns 'brownfield' or 'greenfield'
40
+ */
41
+ export function detectProjectMaturity(projectPath) {
42
+ // Check version control
43
+ const vcs = checkVersionControl(projectPath);
44
+ if (vcs) {
45
+ return 'brownfield';
46
+ }
47
+ // Check git status (dirty repository)
48
+ const isDirty = checkGitStatus(projectPath);
49
+ if (isDirty) {
50
+ return 'brownfield';
51
+ }
52
+ // Check for dependency manifests
53
+ const manifests = checkManifestFiles(projectPath);
54
+ if (manifests.length > 0) {
55
+ return 'brownfield';
56
+ }
57
+ // Check for source directories with code
58
+ const sourceDirs = checkSourceDirectories(projectPath);
59
+ if (sourceDirs.length > 0) {
60
+ return 'brownfield';
61
+ }
62
+ // No brownfield indicators found
63
+ return 'greenfield';
64
+ }
65
+ /**
66
+ * Check if git repository has uncommitted changes
67
+ *
68
+ * @param projectPath - Absolute path to project directory
69
+ * @returns true if repository is dirty, false otherwise
70
+ */
71
+ export function checkGitStatus(projectPath) {
72
+ try {
73
+ const gitDir = path.join(projectPath, '.git');
74
+ if (!fs.existsSync(gitDir)) {
75
+ return false;
76
+ }
77
+ const output = execSync('git status --porcelain', {
78
+ cwd: projectPath,
79
+ encoding: 'utf-8',
80
+ stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr
81
+ });
82
+ return output.trim().length > 0;
83
+ }
84
+ catch (error) {
85
+ // Git command failed or not a git repository
86
+ return false;
87
+ }
88
+ }
89
+ /**
90
+ * Check for dependency manifest files
91
+ *
92
+ * @param projectPath - Absolute path to project directory
93
+ * @returns Array of found manifest file names
94
+ */
95
+ export function checkManifestFiles(projectPath) {
96
+ const found = [];
97
+ for (const manifest of MANIFEST_FILES) {
98
+ const manifestPath = path.join(projectPath, manifest);
99
+ if (fs.existsSync(manifestPath)) {
100
+ found.push(manifest);
101
+ }
102
+ }
103
+ return found;
104
+ }
105
+ /**
106
+ * Check for source directories containing code files
107
+ *
108
+ * @param projectPath - Absolute path to project directory
109
+ * @returns Array of found source directory names
110
+ */
111
+ export function checkSourceDirectories(projectPath) {
112
+ const found = [];
113
+ for (const dirName of SOURCE_DIRECTORIES) {
114
+ const dirPath = path.join(projectPath, dirName);
115
+ if (!fs.existsSync(dirPath)) {
116
+ continue;
117
+ }
118
+ const stat = fs.statSync(dirPath);
119
+ if (!stat.isDirectory()) {
120
+ continue;
121
+ }
122
+ // Check if directory contains code files
123
+ if (containsCodeFiles(dirPath)) {
124
+ found.push(dirName);
125
+ }
126
+ }
127
+ return found;
128
+ }
129
+ /**
130
+ * Check if directory contains code files (recursive, depth-limited)
131
+ *
132
+ * @param dirPath - Absolute path to directory
133
+ * @param maxDepth - Maximum recursion depth (default: 3)
134
+ * @param currentDepth - Current recursion depth
135
+ * @returns true if code files found
136
+ */
137
+ function containsCodeFiles(dirPath, maxDepth = 3, currentDepth = 0) {
138
+ if (currentDepth > maxDepth) {
139
+ return false;
140
+ }
141
+ try {
142
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
143
+ for (const entry of entries) {
144
+ const entryPath = path.join(dirPath, entry.name);
145
+ if (entry.isFile()) {
146
+ const ext = path.extname(entry.name);
147
+ if (CODE_EXTENSIONS.includes(ext)) {
148
+ return true;
149
+ }
150
+ }
151
+ else if (entry.isDirectory()) {
152
+ // Recursively check subdirectories
153
+ if (containsCodeFiles(entryPath, maxDepth, currentDepth + 1)) {
154
+ return true;
155
+ }
156
+ }
157
+ }
158
+ return false;
159
+ }
160
+ catch (error) {
161
+ // Permission error or other issue
162
+ return false;
163
+ }
164
+ }
165
+ /**
166
+ * Check for version control system directories
167
+ *
168
+ * @param projectPath - Absolute path to project directory
169
+ * @returns Name of VCS directory found, or null if none found
170
+ */
171
+ export function checkVersionControl(projectPath) {
172
+ for (const vcs of VCS_DIRECTORIES) {
173
+ const vcsPath = path.join(projectPath, vcs);
174
+ if (fs.existsSync(vcsPath)) {
175
+ return vcs;
176
+ }
177
+ }
178
+ return null;
179
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,298 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import { execSync } from 'child_process';
4
+ import { detectProjectMaturity, checkGitStatus, checkManifestFiles, checkSourceDirectories, checkVersionControl, } from './projectMaturity.js';
5
+ /**
6
+ * Project Maturity Detection Module Tests
7
+ *
8
+ * This module is responsible for determining whether a project is:
9
+ * - Brownfield (existing project with code/configuration)
10
+ * - Greenfield (new/empty project)
11
+ *
12
+ * Based on reference implementations from:
13
+ * - derekbar90/opencode-conductor
14
+ * - gemini-cli-extensions/conductor
15
+ */
16
+ // Mock file system and child_process
17
+ vi.mock('fs');
18
+ vi.mock('child_process');
19
+ describe('Project Maturity Detection', () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+ describe('detectProjectMaturity', () => {
24
+ it('should detect brownfield project with .git directory', () => {
25
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
26
+ return p.toString().endsWith('.git');
27
+ });
28
+ const result = detectProjectMaturity('/test/project');
29
+ expect(result).toBe('brownfield');
30
+ });
31
+ it('should detect brownfield project with dirty git repository', () => {
32
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
33
+ return p.toString().endsWith('.git');
34
+ });
35
+ vi.mocked(execSync).mockReturnValue('M file.txt\n');
36
+ const result = detectProjectMaturity('/test/project');
37
+ expect(result).toBe('brownfield');
38
+ });
39
+ it('should detect brownfield project with package.json manifest', () => {
40
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
41
+ return p.toString().endsWith('package.json');
42
+ });
43
+ const result = detectProjectMaturity('/test/project');
44
+ expect(result).toBe('brownfield');
45
+ });
46
+ it('should detect brownfield project with pom.xml manifest', () => {
47
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
48
+ return p.toString().endsWith('pom.xml');
49
+ });
50
+ const result = detectProjectMaturity('/test/project');
51
+ expect(result).toBe('brownfield');
52
+ });
53
+ it('should detect brownfield project with requirements.txt manifest', () => {
54
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
55
+ return p.toString().endsWith('requirements.txt');
56
+ });
57
+ const result = detectProjectMaturity('/test/project');
58
+ expect(result).toBe('brownfield');
59
+ });
60
+ it('should detect brownfield project with go.mod manifest', () => {
61
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
62
+ return p.toString().endsWith('go.mod');
63
+ });
64
+ const result = detectProjectMaturity('/test/project');
65
+ expect(result).toBe('brownfield');
66
+ });
67
+ it('should detect brownfield project with src/ directory containing code', () => {
68
+ vi.mocked(fs.existsSync).mockReturnValue(true);
69
+ vi.mocked(fs.statSync).mockReturnValue({
70
+ isDirectory: () => true,
71
+ });
72
+ vi.mocked(fs.readdirSync).mockReturnValue([
73
+ { name: 'index.ts', isFile: () => true, isDirectory: () => false },
74
+ ]);
75
+ const result = detectProjectMaturity('/test/project');
76
+ expect(result).toBe('brownfield');
77
+ });
78
+ it('should detect brownfield project with app/ directory containing code', () => {
79
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
80
+ return p.toString().includes('/app');
81
+ });
82
+ vi.mocked(fs.statSync).mockReturnValue({
83
+ isDirectory: () => true,
84
+ });
85
+ vi.mocked(fs.readdirSync).mockReturnValue([
86
+ { name: 'main.py', isFile: () => true, isDirectory: () => false },
87
+ ]);
88
+ const result = detectProjectMaturity('/test/project');
89
+ expect(result).toBe('brownfield');
90
+ });
91
+ it('should detect brownfield project with lib/ directory containing code', () => {
92
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
93
+ return p.toString().includes('/lib');
94
+ });
95
+ vi.mocked(fs.statSync).mockReturnValue({
96
+ isDirectory: () => true,
97
+ });
98
+ vi.mocked(fs.readdirSync).mockReturnValue([
99
+ { name: 'utils.js', isFile: () => true, isDirectory: () => false },
100
+ ]);
101
+ const result = detectProjectMaturity('/test/project');
102
+ expect(result).toBe('brownfield');
103
+ });
104
+ it('should detect brownfield project with .svn directory', () => {
105
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
106
+ return p.toString().endsWith('.svn');
107
+ });
108
+ const result = detectProjectMaturity('/test/project');
109
+ expect(result).toBe('brownfield');
110
+ });
111
+ it('should detect brownfield project with .hg directory', () => {
112
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
113
+ return p.toString().endsWith('.hg');
114
+ });
115
+ const result = detectProjectMaturity('/test/project');
116
+ expect(result).toBe('brownfield');
117
+ });
118
+ it('should detect greenfield project when directory is empty', () => {
119
+ vi.mocked(fs.existsSync).mockReturnValue(false);
120
+ const result = detectProjectMaturity('/test/project');
121
+ expect(result).toBe('greenfield');
122
+ });
123
+ it('should detect greenfield project with only README.md', () => {
124
+ vi.mocked(fs.existsSync).mockReturnValue(false);
125
+ const result = detectProjectMaturity('/test/project');
126
+ expect(result).toBe('greenfield');
127
+ });
128
+ it('should detect greenfield when no brownfield indicators present', () => {
129
+ vi.mocked(fs.existsSync).mockReturnValue(false);
130
+ const result = detectProjectMaturity('/test/project');
131
+ expect(result).toBe('greenfield');
132
+ });
133
+ it('should prioritize brownfield if ANY indicator is present', () => {
134
+ // Only .git exists, nothing else
135
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
136
+ return p.toString().endsWith('.git');
137
+ });
138
+ const result = detectProjectMaturity('/test/project');
139
+ expect(result).toBe('brownfield');
140
+ });
141
+ });
142
+ describe('checkGitStatus', () => {
143
+ it('should return true for dirty repository', () => {
144
+ vi.mocked(fs.existsSync).mockReturnValue(true);
145
+ vi.mocked(execSync).mockReturnValue('M file.txt\n');
146
+ const result = checkGitStatus('/test/project');
147
+ expect(result).toBe(true);
148
+ });
149
+ it('should return false for clean repository', () => {
150
+ vi.mocked(fs.existsSync).mockReturnValue(true);
151
+ vi.mocked(execSync).mockReturnValue('');
152
+ const result = checkGitStatus('/test/project');
153
+ expect(result).toBe(false);
154
+ });
155
+ it('should handle missing .git directory gracefully', () => {
156
+ vi.mocked(fs.existsSync).mockReturnValue(false);
157
+ const result = checkGitStatus('/test/project');
158
+ expect(result).toBe(false);
159
+ });
160
+ });
161
+ describe('checkManifestFiles', () => {
162
+ it('should detect package.json', () => {
163
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
164
+ return p.toString().endsWith('package.json');
165
+ });
166
+ const result = checkManifestFiles('/test/project');
167
+ expect(result).toContain('package.json');
168
+ });
169
+ it('should detect pom.xml', () => {
170
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
171
+ return p.toString().endsWith('pom.xml');
172
+ });
173
+ const result = checkManifestFiles('/test/project');
174
+ expect(result).toContain('pom.xml');
175
+ });
176
+ it('should detect requirements.txt', () => {
177
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
178
+ return p.toString().endsWith('requirements.txt');
179
+ });
180
+ const result = checkManifestFiles('/test/project');
181
+ expect(result).toContain('requirements.txt');
182
+ });
183
+ it('should detect go.mod', () => {
184
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
185
+ return p.toString().endsWith('go.mod');
186
+ });
187
+ const result = checkManifestFiles('/test/project');
188
+ expect(result).toContain('go.mod');
189
+ });
190
+ it('should detect Cargo.toml', () => {
191
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
192
+ return p.toString().endsWith('Cargo.toml');
193
+ });
194
+ const result = checkManifestFiles('/test/project');
195
+ expect(result).toContain('Cargo.toml');
196
+ });
197
+ it('should detect Gemfile', () => {
198
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
199
+ return p.toString().endsWith('Gemfile');
200
+ });
201
+ const result = checkManifestFiles('/test/project');
202
+ expect(result).toContain('Gemfile');
203
+ });
204
+ it('should return empty array when no manifests found', () => {
205
+ vi.mocked(fs.existsSync).mockReturnValue(false);
206
+ const result = checkManifestFiles('/test/project');
207
+ expect(result).toEqual([]);
208
+ });
209
+ });
210
+ describe('checkSourceDirectories', () => {
211
+ it('should detect src/ directory with .ts files', () => {
212
+ vi.mocked(fs.existsSync).mockReturnValue(true);
213
+ vi.mocked(fs.statSync).mockReturnValue({
214
+ isDirectory: () => true,
215
+ });
216
+ vi.mocked(fs.readdirSync).mockReturnValue([
217
+ { name: 'index.ts', isFile: () => true, isDirectory: () => false },
218
+ ]);
219
+ const result = checkSourceDirectories('/test/project');
220
+ expect(result).toContain('src');
221
+ });
222
+ it('should detect app/ directory with .py files', () => {
223
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
224
+ return p.toString().includes('/app');
225
+ });
226
+ vi.mocked(fs.statSync).mockReturnValue({
227
+ isDirectory: () => true,
228
+ });
229
+ vi.mocked(fs.readdirSync).mockReturnValue([
230
+ { name: 'main.py', isFile: () => true, isDirectory: () => false },
231
+ ]);
232
+ const result = checkSourceDirectories('/test/project');
233
+ expect(result).toContain('app');
234
+ });
235
+ it('should detect lib/ directory with .js files', () => {
236
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
237
+ return p.toString().includes('/lib');
238
+ });
239
+ vi.mocked(fs.statSync).mockReturnValue({
240
+ isDirectory: () => true,
241
+ });
242
+ vi.mocked(fs.readdirSync).mockReturnValue([
243
+ { name: 'utils.js', isFile: () => true, isDirectory: () => false },
244
+ ]);
245
+ const result = checkSourceDirectories('/test/project');
246
+ expect(result).toContain('lib');
247
+ });
248
+ it('should ignore empty source directories', () => {
249
+ vi.mocked(fs.existsSync).mockReturnValue(true);
250
+ vi.mocked(fs.statSync).mockReturnValue({
251
+ isDirectory: () => true,
252
+ });
253
+ vi.mocked(fs.readdirSync).mockReturnValue([]);
254
+ const result = checkSourceDirectories('/test/project');
255
+ expect(result).toEqual([]);
256
+ });
257
+ it('should ignore directories with only non-code files', () => {
258
+ vi.mocked(fs.existsSync).mockReturnValue(true);
259
+ vi.mocked(fs.statSync).mockReturnValue({
260
+ isDirectory: () => true,
261
+ });
262
+ vi.mocked(fs.readdirSync).mockReturnValue([
263
+ { name: 'README.md', isFile: () => true, isDirectory: () => false },
264
+ { name: 'notes.txt', isFile: () => true, isDirectory: () => false },
265
+ ]);
266
+ const result = checkSourceDirectories('/test/project');
267
+ expect(result).toEqual([]);
268
+ });
269
+ });
270
+ describe('checkVersionControl', () => {
271
+ it('should detect .git directory', () => {
272
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
273
+ return p.toString().endsWith('.git');
274
+ });
275
+ const result = checkVersionControl('/test/project');
276
+ expect(result).toBe('.git');
277
+ });
278
+ it('should detect .svn directory', () => {
279
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
280
+ return p.toString().endsWith('.svn');
281
+ });
282
+ const result = checkVersionControl('/test/project');
283
+ expect(result).toBe('.svn');
284
+ });
285
+ it('should detect .hg directory', () => {
286
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
287
+ return p.toString().endsWith('.hg');
288
+ });
289
+ const result = checkVersionControl('/test/project');
290
+ expect(result).toBe('.hg');
291
+ });
292
+ it('should return null when no VCS found', () => {
293
+ vi.mocked(fs.existsSync).mockReturnValue(false);
294
+ const result = checkVersionControl('/test/project');
295
+ expect(result).toBeNull();
296
+ });
297
+ });
298
+ });