@xelth/eck-snapshot 2.2.0 → 4.0.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/LICENSE +21 -0
- package/README.md +119 -225
- package/index.js +14 -776
- package/package.json +25 -7
- package/setup.json +805 -0
- package/src/cli/cli.js +427 -0
- package/src/cli/commands/askGpt.js +29 -0
- package/src/cli/commands/autoDocs.js +150 -0
- package/src/cli/commands/consilium.js +86 -0
- package/src/cli/commands/createSnapshot.js +601 -0
- package/src/cli/commands/detectProfiles.js +98 -0
- package/src/cli/commands/detectProject.js +112 -0
- package/src/cli/commands/generateProfileGuide.js +91 -0
- package/src/cli/commands/pruneSnapshot.js +106 -0
- package/src/cli/commands/restoreSnapshot.js +173 -0
- package/src/cli/commands/setupGemini.js +149 -0
- package/src/cli/commands/setupGemini.test.js +115 -0
- package/src/cli/commands/trainTokens.js +38 -0
- package/src/config.js +81 -0
- package/src/services/authService.js +20 -0
- package/src/services/claudeCliService.js +621 -0
- package/src/services/claudeCliService.test.js +267 -0
- package/src/services/dispatcherService.js +33 -0
- package/src/services/gptService.js +302 -0
- package/src/services/gptService.test.js +120 -0
- package/src/templates/agent-prompt.template.md +29 -0
- package/src/templates/architect-prompt.template.md +50 -0
- package/src/templates/envScanRequest.md +4 -0
- package/src/templates/gitWorkflow.md +32 -0
- package/src/templates/multiAgent.md +164 -0
- package/src/templates/vectorMode.md +22 -0
- package/src/utils/aiHeader.js +303 -0
- package/src/utils/fileUtils.js +928 -0
- package/src/utils/projectDetector.js +704 -0
- package/src/utils/tokenEstimator.js +198 -0
- package/.ecksnapshot.config.js +0 -35
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadSetupConfig } from '../config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects the type of project based on file structure and configuration
|
|
7
|
+
* @param {string} projectPath - Path to the project root
|
|
8
|
+
* @returns {Promise<{type: string, confidence: number, details: object}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function detectProjectType(projectPath = '.') {
|
|
11
|
+
const config = await loadSetupConfig();
|
|
12
|
+
const patterns = config.projectDetection?.patterns || {};
|
|
13
|
+
|
|
14
|
+
const detections = [];
|
|
15
|
+
|
|
16
|
+
for (const [type, pattern] of Object.entries(patterns)) {
|
|
17
|
+
const score = await calculateTypeScore(projectPath, pattern);
|
|
18
|
+
if (score > 0) {
|
|
19
|
+
detections.push({
|
|
20
|
+
type,
|
|
21
|
+
score,
|
|
22
|
+
priority: pattern.priority || 0,
|
|
23
|
+
details: await getProjectDetails(projectPath, type)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Sort by priority and score
|
|
29
|
+
detections.sort((a, b) => (b.priority * 10 + b.score) - (a.priority * 10 + a.score));
|
|
30
|
+
|
|
31
|
+
if (detections.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
type: 'unknown',
|
|
34
|
+
confidence: 0,
|
|
35
|
+
details: {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const best = detections[0];
|
|
40
|
+
|
|
41
|
+
// Special handling for mixed monorepos
|
|
42
|
+
const isLikelyMonorepo = detections.length > 1 && detections.some(d => d.score >= 40);
|
|
43
|
+
|
|
44
|
+
if (isLikelyMonorepo) {
|
|
45
|
+
// If we have multiple strong detections, prefer the highest priority with substantial evidence
|
|
46
|
+
const strongDetections = detections.filter(d => d.score >= 40);
|
|
47
|
+
if (strongDetections.length > 1) {
|
|
48
|
+
const primaryType = strongDetections[0].type;
|
|
49
|
+
return {
|
|
50
|
+
type: primaryType,
|
|
51
|
+
confidence: Math.min(strongDetections[0].score / 100, 1.0),
|
|
52
|
+
details: {
|
|
53
|
+
...strongDetections[0].details,
|
|
54
|
+
isMonorepo: true,
|
|
55
|
+
additionalTypes: strongDetections.slice(1).map(d => d.type)
|
|
56
|
+
},
|
|
57
|
+
allDetections: detections
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Boost confidence for strong workspace indicators
|
|
63
|
+
if (best.details && (best.details.isWorkspace || best.details.workspaceSize)) {
|
|
64
|
+
const boostedScore = best.score + 20; // Bonus for workspace structure
|
|
65
|
+
return {
|
|
66
|
+
type: best.type,
|
|
67
|
+
confidence: Math.min(boostedScore / 100, 1.0),
|
|
68
|
+
details: best.details,
|
|
69
|
+
allDetections: detections
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
type: best.type,
|
|
75
|
+
confidence: Math.min(best.score / 100, 1.0),
|
|
76
|
+
details: best.details,
|
|
77
|
+
allDetections: detections
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculates a score for how well a project matches a specific type pattern
|
|
83
|
+
*/
|
|
84
|
+
async function calculateTypeScore(projectPath, pattern) {
|
|
85
|
+
let score = 0;
|
|
86
|
+
|
|
87
|
+
// Check for required files (check both root and common subdirectories)
|
|
88
|
+
if (pattern.files) {
|
|
89
|
+
for (const file of pattern.files) {
|
|
90
|
+
// Check in root directory first
|
|
91
|
+
const rootExists = await fileExists(path.join(projectPath, file));
|
|
92
|
+
if (rootExists) {
|
|
93
|
+
score += 25; // Each required file adds points
|
|
94
|
+
} else {
|
|
95
|
+
// For Cargo.toml and other project files, also check common subdirectory patterns
|
|
96
|
+
const commonSubdirs = ['src', 'lib', 'app', 'core', 'backend', 'frontend'];
|
|
97
|
+
// Add project-type specific subdirectories
|
|
98
|
+
if (file === 'Cargo.toml') {
|
|
99
|
+
commonSubdirs.push('codex-rs', 'rust', 'server', 'api');
|
|
100
|
+
}
|
|
101
|
+
if (file === 'package.json') {
|
|
102
|
+
commonSubdirs.push('codex-cli', 'cli', 'client', 'web', 'ui');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const subdir of commonSubdirs) {
|
|
106
|
+
const subdirExists = await fileExists(path.join(projectPath, subdir, file));
|
|
107
|
+
if (subdirExists) {
|
|
108
|
+
score += 20; // Slightly lower score for subdirectory finds
|
|
109
|
+
break; // Only count once per file type
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for required directories (check both root and one level deep)
|
|
117
|
+
if (pattern.directories) {
|
|
118
|
+
for (const dir of pattern.directories) {
|
|
119
|
+
const rootExists = await directoryExists(path.join(projectPath, dir));
|
|
120
|
+
if (rootExists) {
|
|
121
|
+
score += 20; // Each required directory adds points
|
|
122
|
+
} else {
|
|
123
|
+
// Check in common project subdirectories
|
|
124
|
+
const projectSubdirs = ['codex-rs', 'codex-cli', 'src', 'lib', 'app'];
|
|
125
|
+
for (const projDir of projectSubdirs) {
|
|
126
|
+
const subdirExists = await directoryExists(path.join(projectPath, projDir, dir));
|
|
127
|
+
if (subdirExists) {
|
|
128
|
+
score += 15; // Lower score for nested directory finds
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for manifest files (Android specific) - limit search depth
|
|
137
|
+
if (pattern.manifestFiles) {
|
|
138
|
+
for (const manifest of pattern.manifestFiles) {
|
|
139
|
+
const manifestPath = await findFileRecursive(projectPath, manifest, 2); // Reduced to 2 levels
|
|
140
|
+
if (manifestPath) {
|
|
141
|
+
score += 30; // Manifest files are strong indicators
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for content patterns in package.json (React Native, etc.)
|
|
147
|
+
if (pattern.patterns) {
|
|
148
|
+
try {
|
|
149
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
150
|
+
const packageContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
151
|
+
const packageJson = JSON.parse(packageContent);
|
|
152
|
+
|
|
153
|
+
for (const patternText of pattern.patterns) {
|
|
154
|
+
const allDeps = {
|
|
155
|
+
...packageJson.dependencies,
|
|
156
|
+
...packageJson.devDependencies,
|
|
157
|
+
...packageJson.peerDependencies
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Check for exact dependency names (more precise matching)
|
|
161
|
+
const foundInDeps = Object.keys(allDeps).some(dep => dep === patternText || dep.startsWith(patternText + '/'));
|
|
162
|
+
// Only check for exact matches in keywords array, not description (too broad)
|
|
163
|
+
const foundInKeywords = packageJson.keywords && Array.isArray(packageJson.keywords)
|
|
164
|
+
? packageJson.keywords.some(keyword => keyword.toLowerCase() === patternText.toLowerCase())
|
|
165
|
+
: false;
|
|
166
|
+
|
|
167
|
+
if (foundInDeps || foundInKeywords) {
|
|
168
|
+
score += 25; // Higher score for actual dependencies
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// Ignore if package.json doesn't exist or is malformed
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return score;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Gets detailed information about the detected project type
|
|
181
|
+
*/
|
|
182
|
+
async function getProjectDetails(projectPath, type) {
|
|
183
|
+
const details = { type };
|
|
184
|
+
|
|
185
|
+
switch (type) {
|
|
186
|
+
case 'android':
|
|
187
|
+
return await getAndroidDetails(projectPath);
|
|
188
|
+
case 'nodejs':
|
|
189
|
+
return await getNodejsDetails(projectPath);
|
|
190
|
+
case 'flutter':
|
|
191
|
+
return await getFlutterDetails(projectPath);
|
|
192
|
+
case 'react-native':
|
|
193
|
+
return await getReactNativeDetails(projectPath);
|
|
194
|
+
case 'python-poetry':
|
|
195
|
+
case 'python-pip':
|
|
196
|
+
case 'python-conda':
|
|
197
|
+
case 'django':
|
|
198
|
+
case 'flask':
|
|
199
|
+
return await getPythonDetails(projectPath, type);
|
|
200
|
+
case 'rust':
|
|
201
|
+
return await getRustDetails(projectPath);
|
|
202
|
+
case 'go':
|
|
203
|
+
return await getGoDetails(projectPath);
|
|
204
|
+
case 'dotnet':
|
|
205
|
+
return await getDotnetDetails(projectPath);
|
|
206
|
+
default:
|
|
207
|
+
return details;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function getAndroidDetails(projectPath) {
|
|
212
|
+
const details = { type: 'android' };
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Check build.gradle files
|
|
216
|
+
const buildGradleFiles = [];
|
|
217
|
+
const appBuildGradle = path.join(projectPath, 'app', 'build.gradle');
|
|
218
|
+
const appBuildGradleKts = path.join(projectPath, 'app', 'build.gradle.kts');
|
|
219
|
+
|
|
220
|
+
if (await fileExists(appBuildGradle)) {
|
|
221
|
+
buildGradleFiles.push('app/build.gradle');
|
|
222
|
+
const content = await fs.readFile(appBuildGradle, 'utf-8');
|
|
223
|
+
details.language = content.includes('kotlin') ? 'kotlin' : 'java';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (await fileExists(appBuildGradleKts)) {
|
|
227
|
+
buildGradleFiles.push('app/build.gradle.kts');
|
|
228
|
+
details.language = 'kotlin';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
details.buildFiles = buildGradleFiles;
|
|
232
|
+
|
|
233
|
+
// Check for source directories
|
|
234
|
+
const sourceDirs = [];
|
|
235
|
+
const kotlinDir = path.join(projectPath, 'app', 'src', 'main', 'kotlin');
|
|
236
|
+
const javaDir = path.join(projectPath, 'app', 'src', 'main', 'java');
|
|
237
|
+
|
|
238
|
+
if (await directoryExists(kotlinDir)) {
|
|
239
|
+
sourceDirs.push('app/src/main/kotlin');
|
|
240
|
+
}
|
|
241
|
+
if (await directoryExists(javaDir)) {
|
|
242
|
+
sourceDirs.push('app/src/main/java');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
details.sourceDirs = sourceDirs;
|
|
246
|
+
|
|
247
|
+
// Check for AndroidManifest.xml
|
|
248
|
+
const manifestPath = path.join(projectPath, 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
249
|
+
if (await fileExists(manifestPath)) {
|
|
250
|
+
details.hasManifest = true;
|
|
251
|
+
|
|
252
|
+
// Extract package name from manifest
|
|
253
|
+
try {
|
|
254
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
255
|
+
const packageMatch = manifestContent.match(/package="([^"]+)"/);
|
|
256
|
+
if (packageMatch) {
|
|
257
|
+
details.packageName = packageMatch[1];
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Ignore parsing errors
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check for libs directory
|
|
265
|
+
const libsDir = path.join(projectPath, 'app', 'libs');
|
|
266
|
+
if (await directoryExists(libsDir)) {
|
|
267
|
+
details.hasLibs = true;
|
|
268
|
+
try {
|
|
269
|
+
const libFiles = await fs.readdir(libsDir);
|
|
270
|
+
details.libFiles = libFiles.filter(f => f.endsWith('.aar') || f.endsWith('.jar'));
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Ignore
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn('Error getting Android project details:', error.message);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return details;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function getNodejsDetails(projectPath) {
|
|
284
|
+
const details = { type: 'nodejs' };
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
288
|
+
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
289
|
+
const packageJson = JSON.parse(content);
|
|
290
|
+
|
|
291
|
+
details.name = packageJson.name;
|
|
292
|
+
details.version = packageJson.version;
|
|
293
|
+
details.hasTypescript = !!packageJson.devDependencies?.typescript || !!packageJson.dependencies?.typescript;
|
|
294
|
+
details.framework = detectNodejsFramework(packageJson);
|
|
295
|
+
|
|
296
|
+
// Check if it's a monorepo - be more strict
|
|
297
|
+
const hasWorkspaces = !!packageJson.workspaces;
|
|
298
|
+
const hasLerna = await fileExists(path.join(projectPath, 'lerna.json')) || !!packageJson.lerna;
|
|
299
|
+
const hasNx = await fileExists(path.join(projectPath, 'nx.json'));
|
|
300
|
+
const hasRush = await fileExists(path.join(projectPath, 'rush.json'));
|
|
301
|
+
const hasPackagesDir = await directoryExists(path.join(projectPath, 'packages'));
|
|
302
|
+
const hasAppsDir = await directoryExists(path.join(projectPath, 'apps'));
|
|
303
|
+
const hasLibsDir = await directoryExists(path.join(projectPath, 'libs'));
|
|
304
|
+
|
|
305
|
+
// Check if packages/apps/libs directories contain actual packages
|
|
306
|
+
let hasSubPackages = false;
|
|
307
|
+
|
|
308
|
+
for (const dir of ['packages', 'apps', 'libs']) {
|
|
309
|
+
const dirPath = path.join(projectPath, dir);
|
|
310
|
+
if (await directoryExists(dirPath)) {
|
|
311
|
+
try {
|
|
312
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
if (entry.isDirectory()) {
|
|
315
|
+
const packageJsonPath = path.join(dirPath, entry.name, 'package.json');
|
|
316
|
+
if (await fileExists(packageJsonPath)) {
|
|
317
|
+
hasSubPackages = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (hasSubPackages) break;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
// Ignore
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Only consider it a monorepo if it has workspace configuration AND actual sub-packages
|
|
330
|
+
details.isMonorepo = !!(
|
|
331
|
+
(hasWorkspaces || hasLerna || hasNx || hasRush) &&
|
|
332
|
+
hasSubPackages
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (details.isMonorepo) {
|
|
336
|
+
details.type = 'nodejs-monorepo';
|
|
337
|
+
|
|
338
|
+
// Count workspaces
|
|
339
|
+
if (packageJson.workspaces) {
|
|
340
|
+
if (Array.isArray(packageJson.workspaces)) {
|
|
341
|
+
details.workspaceCount = packageJson.workspaces.length;
|
|
342
|
+
} else if (packageJson.workspaces.packages) {
|
|
343
|
+
details.workspaceCount = packageJson.workspaces.packages.length;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Detect monorepo tool
|
|
348
|
+
if (hasLerna) {
|
|
349
|
+
details.monorepoTool = 'lerna';
|
|
350
|
+
} else if (hasNx) {
|
|
351
|
+
details.monorepoTool = 'nx';
|
|
352
|
+
} else if (hasRush) {
|
|
353
|
+
details.monorepoTool = 'rush';
|
|
354
|
+
} else if (hasWorkspaces) {
|
|
355
|
+
details.monorepoTool = 'npm-workspaces';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.warn('Error getting Node.js project details:', error.message);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return details;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function getFlutterDetails(projectPath) {
|
|
367
|
+
const details = { type: 'flutter' };
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const pubspecPath = path.join(projectPath, 'pubspec.yaml');
|
|
371
|
+
const content = await fs.readFile(pubspecPath, 'utf-8');
|
|
372
|
+
|
|
373
|
+
// Basic parsing of pubspec.yaml
|
|
374
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
375
|
+
if (nameMatch) {
|
|
376
|
+
details.name = nameMatch[1].trim();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const versionMatch = content.match(/^version:\s*(.+)$/m);
|
|
380
|
+
if (versionMatch) {
|
|
381
|
+
details.version = versionMatch[1].trim();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn('Error getting Flutter project details:', error.message);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return details;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function getReactNativeDetails(projectPath) {
|
|
392
|
+
const details = { type: 'react-native' };
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
396
|
+
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
397
|
+
const packageJson = JSON.parse(content);
|
|
398
|
+
|
|
399
|
+
details.name = packageJson.name;
|
|
400
|
+
details.version = packageJson.version;
|
|
401
|
+
details.reactNativeVersion = packageJson.dependencies?.['react-native'];
|
|
402
|
+
details.hasTypescript = !!packageJson.devDependencies?.typescript;
|
|
403
|
+
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.warn('Error getting React Native project details:', error.message);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return details;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function detectNodejsFramework(packageJson) {
|
|
412
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
413
|
+
|
|
414
|
+
if (deps.express) return 'express';
|
|
415
|
+
if (deps.next) return 'next.js';
|
|
416
|
+
if (deps.nuxt) return 'nuxt.js';
|
|
417
|
+
if (deps.vue) return 'vue';
|
|
418
|
+
if (deps.react) return 'react';
|
|
419
|
+
if (deps.electron) return 'electron';
|
|
420
|
+
if (deps.fastify) return 'fastify';
|
|
421
|
+
if (deps.koa) return 'koa';
|
|
422
|
+
if (deps.hapi) return 'hapi';
|
|
423
|
+
|
|
424
|
+
return 'node.js';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function getPythonDetails(projectPath, type) {
|
|
428
|
+
const details = { type };
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
// Check for Poetry project
|
|
432
|
+
if (type === 'python-poetry') {
|
|
433
|
+
const pyprojectPath = path.join(projectPath, 'pyproject.toml');
|
|
434
|
+
const content = await fs.readFile(pyprojectPath, 'utf-8');
|
|
435
|
+
|
|
436
|
+
// Basic TOML parsing for project name and version
|
|
437
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
438
|
+
const versionMatch = content.match(/version\s*=\s*"([^"]+)"/);
|
|
439
|
+
|
|
440
|
+
if (nameMatch) details.name = nameMatch[1];
|
|
441
|
+
if (versionMatch) details.version = versionMatch[1];
|
|
442
|
+
|
|
443
|
+
details.packageManager = 'poetry';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check for requirements.txt
|
|
447
|
+
if (await fileExists(path.join(projectPath, 'requirements.txt'))) {
|
|
448
|
+
const reqContent = await fs.readFile(path.join(projectPath, 'requirements.txt'), 'utf-8');
|
|
449
|
+
details.dependencies = reqContent.split('\n').filter(line => line.trim() && !line.startsWith('#')).length;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Check for Django
|
|
453
|
+
if (type === 'django' || await fileExists(path.join(projectPath, 'manage.py'))) {
|
|
454
|
+
details.framework = 'django';
|
|
455
|
+
details.type = 'django';
|
|
456
|
+
|
|
457
|
+
// Look for Django apps
|
|
458
|
+
try {
|
|
459
|
+
const entries = await fs.readdir(projectPath, { withFileTypes: true });
|
|
460
|
+
const djangoApps = [];
|
|
461
|
+
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
464
|
+
const appPath = path.join(projectPath, entry.name);
|
|
465
|
+
if (await fileExists(path.join(appPath, 'models.py')) ||
|
|
466
|
+
await fileExists(path.join(appPath, 'views.py'))) {
|
|
467
|
+
djangoApps.push(entry.name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
details.djangoApps = djangoApps;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
// Ignore
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Check for Flask
|
|
479
|
+
if (type === 'flask' || await fileExists(path.join(projectPath, 'app.py'))) {
|
|
480
|
+
details.framework = 'flask';
|
|
481
|
+
details.type = 'flask';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check for virtual environment
|
|
485
|
+
if (await directoryExists(path.join(projectPath, 'venv')) ||
|
|
486
|
+
await directoryExists(path.join(projectPath, '.venv')) ||
|
|
487
|
+
await directoryExists(path.join(projectPath, 'env'))) {
|
|
488
|
+
details.hasVirtualEnv = true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.warn('Error getting Python project details:', error.message);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return details;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function getRustDetails(projectPath) {
|
|
499
|
+
const details = { type: 'rust' };
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
// Check both root and common subdirectories for Cargo.toml
|
|
503
|
+
let cargoPath = path.join(projectPath, 'Cargo.toml');
|
|
504
|
+
let cargoContent = null;
|
|
505
|
+
|
|
506
|
+
if (await fileExists(cargoPath)) {
|
|
507
|
+
cargoContent = await fs.readFile(cargoPath, 'utf-8');
|
|
508
|
+
} else {
|
|
509
|
+
// Check common Rust project subdirectories
|
|
510
|
+
const rustSubdirs = ['codex-rs', 'rust', 'src', 'core', 'server'];
|
|
511
|
+
for (const subdir of rustSubdirs) {
|
|
512
|
+
const subdirCargoPath = path.join(projectPath, subdir, 'Cargo.toml');
|
|
513
|
+
if (await fileExists(subdirCargoPath)) {
|
|
514
|
+
cargoPath = subdirCargoPath;
|
|
515
|
+
cargoContent = await fs.readFile(subdirCargoPath, 'utf-8');
|
|
516
|
+
details.primaryLocation = subdir;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!cargoContent) {
|
|
523
|
+
return details;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const nameMatch = cargoContent.match(/name\s*=\s*"([^"]+)"/);
|
|
527
|
+
const versionMatch = cargoContent.match(/version\s*=\s*"([^"]+)"/);
|
|
528
|
+
const editionMatch = cargoContent.match(/edition\s*=\s*"([^"]+)"/);
|
|
529
|
+
|
|
530
|
+
if (nameMatch) details.name = nameMatch[1];
|
|
531
|
+
if (versionMatch) details.version = versionMatch[1];
|
|
532
|
+
if (editionMatch) details.edition = editionMatch[1];
|
|
533
|
+
|
|
534
|
+
// Check if it's a workspace
|
|
535
|
+
if (cargoContent.includes('[workspace]')) {
|
|
536
|
+
details.isWorkspace = true;
|
|
537
|
+
|
|
538
|
+
// Count workspace members
|
|
539
|
+
const workspaceMatch = cargoContent.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
540
|
+
if (workspaceMatch) {
|
|
541
|
+
const members = workspaceMatch[1].split(',').map(m => m.trim().replace(/"/g, '')).filter(m => m);
|
|
542
|
+
details.workspaceMembers = members.length;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check for multiple Cargo.toml files (indicates workspace structure)
|
|
547
|
+
if (details.primaryLocation) {
|
|
548
|
+
const subdirPath = path.join(projectPath, details.primaryLocation);
|
|
549
|
+
try {
|
|
550
|
+
const subdirs = await fs.readdir(subdirPath, { withFileTypes: true });
|
|
551
|
+
let cargoCount = 0;
|
|
552
|
+
for (const entry of subdirs) {
|
|
553
|
+
if (entry.isDirectory()) {
|
|
554
|
+
const memberCargoPath = path.join(subdirPath, entry.name, 'Cargo.toml');
|
|
555
|
+
if (await fileExists(memberCargoPath)) {
|
|
556
|
+
cargoCount++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (cargoCount > 3) { // If many workspace members, this is definitely a Rust project
|
|
561
|
+
details.workspaceSize = 'large';
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
// Ignore
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.warn('Error getting Rust project details:', error.message);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return details;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function getGoDetails(projectPath) {
|
|
576
|
+
const details = { type: 'go' };
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const goModPath = path.join(projectPath, 'go.mod');
|
|
580
|
+
const content = await fs.readFile(goModPath, 'utf-8');
|
|
581
|
+
|
|
582
|
+
const moduleMatch = content.match(/module\s+([^\s\n]+)/);
|
|
583
|
+
const goVersionMatch = content.match(/go\s+([0-9.]+)/);
|
|
584
|
+
|
|
585
|
+
if (moduleMatch) details.module = moduleMatch[1];
|
|
586
|
+
if (goVersionMatch) details.goVersion = goVersionMatch[1];
|
|
587
|
+
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.warn('Error getting Go project details:', error.message);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return details;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function getDotnetDetails(projectPath) {
|
|
596
|
+
const details = { type: 'dotnet' };
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
// Look for project files
|
|
600
|
+
const entries = await fs.readdir(projectPath);
|
|
601
|
+
const projectFiles = entries.filter(file =>
|
|
602
|
+
file.endsWith('.csproj') ||
|
|
603
|
+
file.endsWith('.fsproj') ||
|
|
604
|
+
file.endsWith('.vbproj')
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
if (projectFiles.length > 0) {
|
|
608
|
+
details.projectFiles = projectFiles;
|
|
609
|
+
|
|
610
|
+
// Determine language
|
|
611
|
+
if (projectFiles.some(f => f.endsWith('.csproj'))) {
|
|
612
|
+
details.language = 'C#';
|
|
613
|
+
} else if (projectFiles.some(f => f.endsWith('.fsproj'))) {
|
|
614
|
+
details.language = 'F#';
|
|
615
|
+
} else if (projectFiles.some(f => f.endsWith('.vbproj'))) {
|
|
616
|
+
details.language = 'VB.NET';
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check for solution file
|
|
621
|
+
const solutionFiles = entries.filter(file => file.endsWith('.sln'));
|
|
622
|
+
if (solutionFiles.length > 0) {
|
|
623
|
+
details.hasSolution = true;
|
|
624
|
+
details.solutionFiles = solutionFiles;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.warn('Error getting .NET project details:', error.message);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return details;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Utility functions
|
|
635
|
+
async function fileExists(filePath) {
|
|
636
|
+
try {
|
|
637
|
+
await fs.access(filePath);
|
|
638
|
+
return true;
|
|
639
|
+
} catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function directoryExists(dirPath) {
|
|
645
|
+
try {
|
|
646
|
+
const stat = await fs.stat(dirPath);
|
|
647
|
+
return stat.isDirectory();
|
|
648
|
+
} catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function findFileRecursive(basePath, fileName, maxDepth = 3) {
|
|
654
|
+
const searchInDir = async (currentPath, depth) => {
|
|
655
|
+
if (depth > maxDepth) return null;
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
659
|
+
|
|
660
|
+
// First, check if the file exists in current directory
|
|
661
|
+
if (items.some(item => item.name === fileName && item.isFile())) {
|
|
662
|
+
return path.join(currentPath, fileName);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Then search in subdirectories
|
|
666
|
+
for (const item of items) {
|
|
667
|
+
if (item.isDirectory() && !item.name.startsWith('.')) {
|
|
668
|
+
const found = await searchInDir(path.join(currentPath, item.name), depth + 1);
|
|
669
|
+
if (found) return found;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
// Ignore permission errors
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return null;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
return await searchInDir(basePath, 0);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Gets project-specific filtering configuration
|
|
684
|
+
* @param {string} projectType - The detected project type
|
|
685
|
+
* @returns {object} Project-specific filtering rules
|
|
686
|
+
*/
|
|
687
|
+
export async function getProjectSpecificFiltering(projectType) {
|
|
688
|
+
const config = await loadSetupConfig();
|
|
689
|
+
const projectSpecific = config.fileFiltering?.projectSpecific?.[projectType];
|
|
690
|
+
|
|
691
|
+
if (!projectSpecific) {
|
|
692
|
+
return {
|
|
693
|
+
filesToIgnore: [],
|
|
694
|
+
dirsToIgnore: [],
|
|
695
|
+
extensionsToIgnore: []
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
filesToIgnore: projectSpecific.filesToIgnore || [],
|
|
701
|
+
dirsToIgnore: projectSpecific.dirsToIgnore || [],
|
|
702
|
+
extensionsToIgnore: projectSpecific.extensionsToIgnore || []
|
|
703
|
+
};
|
|
704
|
+
}
|