@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -225
  3. package/index.js +14 -776
  4. package/package.json +25 -7
  5. package/setup.json +805 -0
  6. package/src/cli/cli.js +427 -0
  7. package/src/cli/commands/askGpt.js +29 -0
  8. package/src/cli/commands/autoDocs.js +150 -0
  9. package/src/cli/commands/consilium.js +86 -0
  10. package/src/cli/commands/createSnapshot.js +601 -0
  11. package/src/cli/commands/detectProfiles.js +98 -0
  12. package/src/cli/commands/detectProject.js +112 -0
  13. package/src/cli/commands/generateProfileGuide.js +91 -0
  14. package/src/cli/commands/pruneSnapshot.js +106 -0
  15. package/src/cli/commands/restoreSnapshot.js +173 -0
  16. package/src/cli/commands/setupGemini.js +149 -0
  17. package/src/cli/commands/setupGemini.test.js +115 -0
  18. package/src/cli/commands/trainTokens.js +38 -0
  19. package/src/config.js +81 -0
  20. package/src/services/authService.js +20 -0
  21. package/src/services/claudeCliService.js +621 -0
  22. package/src/services/claudeCliService.test.js +267 -0
  23. package/src/services/dispatcherService.js +33 -0
  24. package/src/services/gptService.js +302 -0
  25. package/src/services/gptService.test.js +120 -0
  26. package/src/templates/agent-prompt.template.md +29 -0
  27. package/src/templates/architect-prompt.template.md +50 -0
  28. package/src/templates/envScanRequest.md +4 -0
  29. package/src/templates/gitWorkflow.md +32 -0
  30. package/src/templates/multiAgent.md +164 -0
  31. package/src/templates/vectorMode.md +22 -0
  32. package/src/utils/aiHeader.js +303 -0
  33. package/src/utils/fileUtils.js +928 -0
  34. package/src/utils/projectDetector.js +704 -0
  35. package/src/utils/tokenEstimator.js +198 -0
  36. 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
+ }