opencode-conductor-cdd-plugin 1.0.0-beta.18 → 1.0.0-beta.20
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/README.md +19 -3
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/setup.test.js +40 -118
- package/dist/prompts/cdd/setup.test.ts +40 -143
- package/dist/utils/codebaseAnalysis.d.ts +61 -0
- package/dist/utils/codebaseAnalysis.js +429 -0
- package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
- package/dist/utils/codebaseAnalysis.test.js +556 -0
- package/dist/utils/configDetection.d.ts +12 -0
- package/dist/utils/configDetection.js +23 -9
- package/dist/utils/configDetection.test.js +204 -7
- package/dist/utils/documentGeneration.d.ts +97 -0
- package/dist/utils/documentGeneration.js +301 -0
- package/dist/utils/documentGeneration.test.d.ts +1 -0
- package/dist/utils/documentGeneration.test.js +380 -0
- package/dist/utils/interactiveMenu.d.ts +56 -0
- package/dist/utils/interactiveMenu.js +144 -0
- package/dist/utils/interactiveMenu.test.d.ts +1 -0
- package/dist/utils/interactiveMenu.test.js +231 -0
- package/dist/utils/interactiveSetup.d.ts +43 -0
- package/dist/utils/interactiveSetup.js +131 -0
- package/dist/utils/interactiveSetup.test.d.ts +1 -0
- package/dist/utils/interactiveSetup.test.js +124 -0
- package/dist/utils/projectMaturity.d.ts +53 -0
- package/dist/utils/projectMaturity.js +179 -0
- package/dist/utils/projectMaturity.test.d.ts +1 -0
- package/dist/utils/projectMaturity.test.js +298 -0
- package/dist/utils/questionGenerator.d.ts +51 -0
- package/dist/utils/questionGenerator.js +535 -0
- package/dist/utils/questionGenerator.test.d.ts +1 -0
- package/dist/utils/questionGenerator.test.js +328 -0
- package/dist/utils/setupIntegration.d.ts +72 -0
- package/dist/utils/setupIntegration.js +179 -0
- package/dist/utils/setupIntegration.test.d.ts +1 -0
- package/dist/utils/setupIntegration.test.js +344 -0
- package/dist/utils/synergyState.test.js +17 -3
- package/package.json +2 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CodebaseAnalysis } from './codebaseAnalysis.js';
|
|
2
|
+
/**
|
|
3
|
+
* Question Generation Engine
|
|
4
|
+
*
|
|
5
|
+
* Generates context-aware questions for the CDD setup process:
|
|
6
|
+
* - Classifies questions as Additive (multiple) or Exclusive (single choice)
|
|
7
|
+
* - Generates brownfield questions based on codebase analysis
|
|
8
|
+
* - Generates greenfield questions based on common patterns
|
|
9
|
+
* - Creates 5 answer options (A-C: contextual, D: custom, E: autogenerate)
|
|
10
|
+
*
|
|
11
|
+
* Based on reference implementations:
|
|
12
|
+
* - derekbar90/opencode-conductor
|
|
13
|
+
* - gemini-cli-extensions/conductor
|
|
14
|
+
*/
|
|
15
|
+
export type QuestionType = 'additive' | 'exclusive';
|
|
16
|
+
export type Section = 'product' | 'guidelines' | 'tech-stack' | 'styleguides' | 'workflow';
|
|
17
|
+
export interface Question {
|
|
18
|
+
id: string;
|
|
19
|
+
text: string;
|
|
20
|
+
type: QuestionType;
|
|
21
|
+
section: Section;
|
|
22
|
+
options: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Classify question type based on question text
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyQuestionType(questionText: string): QuestionType;
|
|
28
|
+
/**
|
|
29
|
+
* Add suffix to question text based on type
|
|
30
|
+
*/
|
|
31
|
+
export declare function addQuestionSuffix(questionText: string, type: QuestionType): string;
|
|
32
|
+
/**
|
|
33
|
+
* Generate brownfield questions based on codebase analysis
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateBrownfieldQuestions(section: Section, analysis: CodebaseAnalysis): Question[];
|
|
36
|
+
/**
|
|
37
|
+
* Generate greenfield questions based on common patterns
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateGreenfieldQuestions(section: Section): Question[];
|
|
40
|
+
/**
|
|
41
|
+
* Generate answer options (always 5 options: A-C contextual, D custom, E autogenerate)
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateAnswerOptions(contextualOptions: string[], section: Section): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Format question for display with lettered options
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatQuestion(question: Question, questionNumber: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Main function: Generate questions for a section
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateQuestionsForSection(section: Section, projectMaturity: 'brownfield' | 'greenfield', codebaseAnalysis?: CodebaseAnalysis): Question[];
|