project-scan 1.0.3 → 1.0.5

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/lib/scanner.js CHANGED
@@ -1,14 +1,15 @@
1
1
  'use strict';
2
2
  /**
3
3
  * scanner.js — Universal project scanner
4
- * Detects languages, frameworks, databases, cloud infra, and CI/CD pipelines
5
- * by inspecting config files and source extensions recursively.
4
+ * Detects languages, frameworks, databases, cloud infra, CI/CD pipelines,
5
+ * game engines, desktop apps, DSA/competitive-programming, embedded/systems,
6
+ * and mobile projects.
6
7
  */
7
8
 
8
9
  const fs = require('fs');
9
10
  const path = require('path');
10
11
 
11
- // ─── Signature maps ──────────────────────────────────────────────────────────
12
+ // ─── Language signatures ──────────────────────────────────────────────────────
12
13
 
13
14
  const LANGUAGE_SIGS = {
14
15
  JavaScript: { files: ['package.json'], exts: ['.js', '.mjs', '.cjs'] },
@@ -26,8 +27,17 @@ const LANGUAGE_SIGS = {
26
27
  Dart: { files: ['pubspec.yaml', 'pubspec.lock'], exts: ['.dart'] },
27
28
  Scala: { files: ['build.sbt'], exts: ['.scala'] },
28
29
  Elixir: { files: ['mix.exs', 'mix.lock'], exts: ['.ex', '.exs'] },
30
+ Haskell: { files: ['stack.yaml', 'cabal.project'], exts: ['.hs', '.lhs'] },
31
+ OCaml: { files: ['dune-project', 'dune'], exts: ['.ml', '.mli'] },
32
+ 'F#': { files: [], exts: ['.fs', '.fsx', '.fsproj'] },
29
33
  Lua: { files: [], exts: ['.lua'] },
34
+ GDScript: { files: ['project.godot', 'default_env.tres'], exts: ['.gd', '.gdshader'] },
35
+ HLSL: { files: [], exts: ['.hlsl', '.glsl', '.vert', '.frag', '.shader'] },
30
36
  R: { files: ['DESCRIPTION', 'NAMESPACE'], exts: ['.r', '.R'] },
37
+ Julia: { files: ['Project.toml', 'Manifest.toml'], exts: ['.jl'] },
38
+ Nim: { files: ['*.nimble'], exts: ['.nim'] },
39
+ Zig: { files: ['build.zig', 'build.zig.zon'], exts: ['.zig'] },
40
+ 'C (Arduino)':{ files: ['platformio.ini', 'arduino.json'], exts: ['.ino'] },
31
41
  Shell: { files: [], exts: ['.sh', '.bash', '.zsh'] },
32
42
  PowerShell: { files: [], exts: ['.ps1', '.psm1'] },
33
43
  Terraform: { files: [], exts: ['.tf', '.tfvars'] },
@@ -35,66 +45,118 @@ const LANGUAGE_SIGS = {
35
45
  Dockerfile: { files: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml'], exts: [] },
36
46
  };
37
47
 
48
+ // ─── Framework / engine signatures ───────────────────────────────────────────
49
+
38
50
  const FRAMEWORK_SIGS = {
39
- // Frontend
40
- React: { files: [], deps: ['react', 'react-dom'] },
41
- 'Next.js': { files: ['next.config.js', 'next.config.ts', 'next.config.mjs'], deps: ['next'] },
42
- Vue: { files: ['vue.config.js'], deps: ['vue'] },
43
- Angular: { files: ['angular.json'], deps: ['@angular/core'] },
44
- Svelte: { files: ['svelte.config.js'], deps: ['svelte'] },
45
- Vite: { files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'], deps: ['vite'] },
46
- Nuxt: { files: ['nuxt.config.js', 'nuxt.config.ts'], deps: ['nuxt'] },
47
- Gatsby: { files: ['gatsby-config.js', 'gatsby-config.ts'], deps: ['gatsby'] },
48
- // Backend
49
- Express: { files: [], deps: ['express'] },
50
- Fastify: { files: [], deps: ['fastify'] },
51
- NestJS: { files: ['nest-cli.json'], deps: ['@nestjs/core'] },
52
- Koa: { files: [], deps: ['koa'] },
53
- Hapi: { files: [], deps: ['@hapi/hapi'] },
54
- Django: { files: ['manage.py'], deps: [] },
55
- Flask: { files: [], deps: [] },
56
- FastAPI: { files: [], deps: [] },
57
- Laravel: { files: ['artisan'], deps: [] },
58
- Rails: { files: ['config/routes.rb', 'Gemfile'], deps: [] },
59
- Spring: { files: [], deps: [] }, // detected via pom.xml content
60
- // Mobile
61
- 'React Native': { files: [], deps: ['react-native'] },
62
- Flutter: { files: ['pubspec.yaml'], deps: [] },
63
- // Testing frameworks
64
- Jest: { files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'], deps: ['jest'] },
65
- Vitest: { files: ['vitest.config.js', 'vitest.config.ts'], deps: ['vitest'] },
66
- Mocha: { files: ['.mocharc.js', '.mocharc.yml'], deps: ['mocha'] },
67
- Pytest: { files: ['pytest.ini', 'pyproject.toml'], deps: [] },
51
+ // ── Web Frontend ────────────────────────────────────────────────────────────
52
+ React: { files: [], deps: ['react', 'react-dom'] },
53
+ 'Next.js': { files: ['next.config.js', 'next.config.ts', 'next.config.mjs'], deps: ['next'] },
54
+ Vue: { files: ['vue.config.js'], deps: ['vue'] },
55
+ Angular: { files: ['angular.json'], deps: ['@angular/core'] },
56
+ Svelte: { files: ['svelte.config.js'], deps: ['svelte'] },
57
+ Vite: { files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'], deps: ['vite'] },
58
+ Nuxt: { files: ['nuxt.config.js', 'nuxt.config.ts'], deps: ['nuxt'] },
59
+ Gatsby: { files: ['gatsby-config.js', 'gatsby-config.ts'], deps: ['gatsby'] },
60
+ SvelteKit: { files: ['svelte.config.js'], deps: ['@sveltejs/kit'] },
61
+ Remix: { files: ['remix.config.js'], deps: ['@remix-run/react'] },
62
+ SolidJS: { files: [], deps: ['solid-js'] },
63
+ Qwik: { files: [], deps: ['@builder.io/qwik'] },
64
+ // ── Web Backend ─────────────────────────────────────────────────────────────
65
+ Express: { files: [], deps: ['express'] },
66
+ Fastify: { files: [], deps: ['fastify'] },
67
+ NestJS: { files: ['nest-cli.json'], deps: ['@nestjs/core'] },
68
+ Koa: { files: [], deps: ['koa'] },
69
+ Hapi: { files: [], deps: ['@hapi/hapi'] },
70
+ Django: { files: ['manage.py'], deps: [] },
71
+ Flask: { files: [], deps: [] },
72
+ FastAPI: { files: [], deps: [] },
73
+ Laravel: { files: ['artisan'], deps: [] },
74
+ Rails: { files: ['config/routes.rb', 'Gemfile'], deps: [] },
75
+ Spring: { files: [], deps: [] },
76
+ Gin: { files: [], deps: [] }, // detected via go.mod scan
77
+ Echo: { files: [], deps: [] },
78
+ Fiber: { files: [], deps: [] },
79
+ Actix: { files: [], deps: [] }, // detected via Cargo.toml scan
80
+ Axum: { files: [], deps: [] },
81
+ // ── Game Engines ─────────────────────────────────────────────────────────────
82
+ Unity: { files: ['Assets/', 'ProjectSettings/ProjectVersion.txt', 'UnityPackageManager/'], deps: [] },
83
+ Godot: { files: ['project.godot', 'default_env.tres'], deps: [] },
84
+ Unreal: { files: [], exts: ['.uproject', '.uplugin'], deps: [] },
85
+ Pygame: { files: [], deps: ['pygame', 'pygame-ce'] },
86
+ 'SFML (C++)': { files: [], deps: [] }, // detected via CMakeLists or src scan
87
+ 'SDL2': { files: [], deps: [] },
88
+ LibGDX: { files: ['desktop/src', 'core/src', 'android/src'], deps: [] },
89
+ MonoGame: { files: [], deps: [] },
90
+ Bevy: { files: [], deps: [] }, // detected via Cargo.toml scan
91
+ Phaser: { files: [], deps: ['phaser'] },
92
+ 'Three.js': { files: [], deps: ['three'] },
93
+ Babylon: { files: [], deps: ['babylonjs', '@babylonjs/core'] },
94
+ 'PixiJS': { files: [], deps: ['pixi.js'] },
95
+ // ── Desktop / GUI ─────────────────────────────────────────────────────────
96
+ Electron: { files: [], deps: ['electron'] },
97
+ Tauri: { files: ['src-tauri/tauri.conf.json'], deps: ['@tauri-apps/api'] },
98
+ 'Qt (C++)': { files: [], exts: ['.pro', '.qml'] },
99
+ wxWidgets: { files: [], deps: [] },
100
+ JavaFX: { files: [], deps: [] },
101
+ Tkinter: { files: [], deps: [] }, // detected via .py scan
102
+ PyQt: { files: [], deps: [] },
103
+ GTK: { files: [], deps: [] },
104
+ // ── Mobile ────────────────────────────────────────────────────────────────
105
+ 'React Native': { files: [], deps: ['react-native'] },
106
+ Flutter: { files: ['pubspec.yaml'], deps: [] },
107
+ Expo: { files: ['app.json', 'expo.json'], deps: ['expo'] },
108
+ // ── Testing ────────────────────────────────────────────────────────────────
109
+ Jest: { files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'], deps: ['jest'] },
110
+ Vitest: { files: ['vitest.config.js', 'vitest.config.ts'], deps: ['vitest'] },
111
+ Mocha: { files: ['.mocharc.js', '.mocharc.yml'], deps: ['mocha'] },
112
+ Pytest: { files: ['pytest.ini', 'pyproject.toml'], deps: [] },
113
+ JUnit: { files: [], deps: [] }, // detected via pom.xml/build.gradle
114
+ GoogleTest: { files: [], deps: [] }, // detected via CMakeLists
115
+ Catch2: { files: [], deps: [] },
116
+ Unity_Test: { files: [], deps: [] }, // Unity Test Framework
117
+ // ── ML / Data Science ─────────────────────────────────────────────────────
118
+ PyTorch: { files: [], deps: ['torch', 'torchvision'] },
119
+ TensorFlow: { files: [], deps: ['tensorflow', 'tensorflow-gpu', 'tf-nightly'] },
120
+ 'scikit-learn': { files: [], deps: ['scikit-learn', 'sklearn'] },
121
+ Jupyter: { files: [], exts: ['.ipynb'], deps: [] },
122
+ NumPy: { files: [], deps: ['numpy'] },
123
+ Pandas: { files: [], deps: ['pandas'] },
68
124
  };
69
125
 
126
+ // ─── Database signatures ──────────────────────────────────────────────────────
127
+
70
128
  const DATABASE_SIGS = {
71
- MongoDB: { files: [], deps: ['mongoose', 'mongodb'], envKeys: ['MONGO_URI', 'MONGODB_URI', 'MONGO_URL'] },
72
- PostgreSQL: { files: [], deps: ['pg', 'postgres', 'typeorm'], envKeys: ['DATABASE_URL', 'POSTGRES_URL', 'PG_URL'] },
73
- MySQL: { files: [], deps: ['mysql', 'mysql2', 'sequelize'], envKeys: ['MYSQL_URL', 'DATABASE_URL'] },
74
- SQLite: { files: ['database.db', 'db.sqlite', 'local.db'], deps: ['better-sqlite3', 'sqlite3', 'sqlite'] },
75
- Redis: { files: [], deps: ['ioredis', 'redis'], envKeys: ['REDIS_URL', 'REDIS_HOST'] },
76
- Supabase: { files: [], deps: ['@supabase/supabase-js'], envKeys: ['SUPABASE_URL'] },
77
- Prisma: { files: ['prisma/schema.prisma', 'schema.prisma'], deps: ['prisma', '@prisma/client'] },
78
- Firebase: { files: ['firebase.json', '.firebaserc'], deps: ['firebase', 'firebase-admin'], envKeys: ['FIREBASE_PROJECT_ID'] },
79
- DynamoDB: { files: [], deps: ['@aws-sdk/client-dynamodb', 'dynamoose'], envKeys: ['AWS_DYNAMODB_TABLE'] },
80
- Elasticsearch:{ files: [], deps: ['@elastic/elasticsearch'], envKeys: ['ELASTICSEARCH_URL'] },
81
- 'SQL Server': { files: [], deps: ['mssql', 'tedious'], envKeys: ['MSSQL_HOST'] },
82
- Cassandra: { files: [], deps: ['cassandra-driver'], envKeys: [] },
129
+ MongoDB: { files: [], deps: ['mongoose', 'mongodb'], envKeys: ['MONGO_URI', 'MONGODB_URI', 'MONGO_URL'] },
130
+ PostgreSQL: { files: [], deps: ['pg', 'postgres', 'typeorm'], envKeys: ['DATABASE_URL', 'POSTGRES_URL', 'PG_URL'] },
131
+ MySQL: { files: [], deps: ['mysql', 'mysql2', 'sequelize'], envKeys: ['MYSQL_URL', 'DATABASE_URL'] },
132
+ SQLite: { files: ['database.db', 'db.sqlite', 'local.db'], deps: ['better-sqlite3', 'sqlite3', 'sqlite'] },
133
+ Redis: { files: [], deps: ['ioredis', 'redis'], envKeys: ['REDIS_URL', 'REDIS_HOST'] },
134
+ Supabase: { files: [], deps: ['@supabase/supabase-js'], envKeys: ['SUPABASE_URL'] },
135
+ Prisma: { files: ['prisma/schema.prisma', 'schema.prisma'], deps: ['prisma', '@prisma/client'] },
136
+ Firebase: { files: ['firebase.json', '.firebaserc'], deps: ['firebase', 'firebase-admin'], envKeys: ['FIREBASE_PROJECT_ID'] },
137
+ DynamoDB: { files: [], deps: ['@aws-sdk/client-dynamodb', 'dynamoose'], envKeys: ['AWS_DYNAMODB_TABLE'] },
138
+ Elasticsearch: { files: [], deps: ['@elastic/elasticsearch'], envKeys: ['ELASTICSEARCH_URL'] },
139
+ 'SQL Server': { files: [], deps: ['mssql', 'tedious'], envKeys: ['MSSQL_HOST'] },
140
+ Cassandra: { files: [], deps: ['cassandra-driver'], envKeys: [] },
83
141
  };
84
142
 
143
+ // ─── Cloud / API signatures ───────────────────────────────────────────────────
144
+
85
145
  const CLOUD_SIGS = {
86
- AWS: { files: ['serverless.yml', 'serverless.yaml', 'sam.yaml', 'template.yaml', 'cdk.json', '.aws/config'], deps: ['aws-sdk', '@aws-sdk/client-s3'], envKeys: ['AWS_REGION', 'AWS_ACCESS_KEY_ID'] },
146
+ AWS: { files: ['serverless.yml', 'serverless.yaml', 'sam.yaml', 'template.yaml', 'cdk.json', '.aws/config'], deps: ['aws-sdk', '@aws-sdk/client-s3'], envKeys: ['AWS_REGION', 'AWS_ACCESS_KEY_ID'] },
87
147
  'Google Cloud':{ files: ['app.yaml', '.gcloudignore', 'cloudbuild.yaml'], deps: ['@google-cloud/storage', '@google-cloud/firestore'], envKeys: ['GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT'] },
88
- Azure: { files: ['azure-pipelines.yml', 'azure-functions.json'], deps: ['@azure/storage-blob', '@azure/identity'], envKeys: ['AZURE_TENANT_ID', 'AZURE_CLIENT_ID'] },
89
- Vercel: { files: ['vercel.json', '.vercel/project.json'], deps: [], envKeys: ['VERCEL_URL'] },
90
- Netlify: { files: ['netlify.toml', '_redirects', '_headers'], deps: ['netlify'], envKeys: ['NETLIFY_SITE_ID'] },
91
- Heroku: { files: ['Procfile', 'app.json'], deps: [], envKeys: ['HEROKU_APP_NAME'] },
92
- Cloudflare: { files: ['wrangler.toml', 'wrangler.json'], deps: ['wrangler', '@cloudflare/workers-types'], envKeys: [] },
93
- Railway: { files: ['railway.json', 'railway.toml'], deps: [], envKeys: ['RAILWAY_PROJECT_ID'] },
94
- Render: { files: ['render.yaml'], deps: [], envKeys: ['RENDER_SERVICE_ID'] },
95
- ImageKit: { files: [], deps: ['imagekit', '@imagekit/nodejs'], envKeys: ['IMAGEKIT_URL_ENDPOINT', 'IMAGEKIT_PUBLIC_KEY'] },
148
+ Azure: { files: ['azure-pipelines.yml', 'azure-functions.json'], deps: ['@azure/storage-blob', '@azure/identity'], envKeys: ['AZURE_TENANT_ID', 'AZURE_CLIENT_ID'] },
149
+ Vercel: { files: ['vercel.json', '.vercel/project.json'], deps: [], envKeys: ['VERCEL_URL'] },
150
+ Netlify: { files: ['netlify.toml', '_redirects', '_headers'], deps: ['netlify'], envKeys: ['NETLIFY_SITE_ID'] },
151
+ Heroku: { files: ['Procfile', 'app.json'], deps: [], envKeys: ['HEROKU_APP_NAME'] },
152
+ Cloudflare: { files: ['wrangler.toml', 'wrangler.json'], deps: ['wrangler', '@cloudflare/workers-types'], envKeys: [] },
153
+ Railway: { files: ['railway.json', 'railway.toml'], deps: [], envKeys: ['RAILWAY_PROJECT_ID'] },
154
+ Render: { files: ['render.yaml'], deps: [], envKeys: ['RENDER_SERVICE_ID'] },
155
+ ImageKit: { files: [], deps: ['imagekit', '@imagekit/nodejs'], envKeys: ['IMAGEKIT_URL_ENDPOINT', 'IMAGEKIT_PUBLIC_KEY'] },
96
156
  };
97
157
 
158
+ // ─── CI/CD signatures ─────────────────────────────────────────────────────────
159
+
98
160
  const CI_SIGS = {
99
161
  'GitHub Actions': { files: ['.github/workflows'] },
100
162
  'GitLab CI': { files: ['.gitlab-ci.yml'] },
@@ -107,11 +169,13 @@ const CI_SIGS = {
107
169
  TeamCity: { files: ['.teamcity'] },
108
170
  };
109
171
 
110
- // ─── Helper utilities ────────────────────────────────────────────────────────
172
+ // ─── Helper utilities ─────────────────────────────────────────────────────────
111
173
 
112
174
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
113
175
  'venv', '.venv', 'env', 'target', 'vendor', '.gradle', 'coverage', '.nyc_output',
114
- 'out', 'bin', 'obj', '.idea', '.vscode', '.dart_tool', '.pub-cache']);
176
+ 'out', 'bin', 'obj', '.idea', '.vscode', '.dart_tool', '.pub-cache', 'Library',
177
+ 'Temp', 'Logs', '.godot', 'DerivedData', 'Packages', 'ProjectSettings',
178
+ ]);
115
179
 
116
180
  function walk(dir, depth = 0, maxDepth = 4) {
117
181
  if (depth > maxDepth) return [];
@@ -130,7 +194,6 @@ function walk(dir, depth = 0, maxDepth = 4) {
130
194
  }
131
195
 
132
196
  function fileExists(root, rel) {
133
- // rel can be a file or directory check
134
197
  return fs.existsSync(path.join(root, rel));
135
198
  }
136
199
 
@@ -151,17 +214,17 @@ function allDeps(pkgJson) {
151
214
  ];
152
215
  }
153
216
 
154
- // ─── Main scanner ────────────────────────────────────────────────────────────
217
+ // ─── Main scanner ─────────────────────────────────────────────────────────────
155
218
 
156
219
  function scan(rootDir) {
157
220
  const allFiles = walk(rootDir);
158
221
  const relFiles = allFiles.map(f => f.slice(rootDir.length + 1).replace(/\\/g, '/'));
159
222
  const relFileSet = new Set(relFiles);
160
223
 
161
- // Collect all extensions present
224
+ // All extensions present
162
225
  const extSet = new Set(allFiles.map(f => path.extname(f).toLowerCase()).filter(Boolean));
163
226
 
164
- // Collect all package.json deps across the whole tree
227
+ // All package.json deps
165
228
  const allPackageDeps = new Set();
166
229
  const packageJsonFiles = allFiles.filter(f => path.basename(f) === 'package.json' && !f.includes('node_modules'));
167
230
  for (const pf of packageJsonFiles) {
@@ -169,7 +232,7 @@ function scan(rootDir) {
169
232
  if (pkg) allDeps(pkg).forEach(d => allPackageDeps.add(d));
170
233
  }
171
234
 
172
- // Collect .env keys present
235
+ // .env keys
173
236
  const envKeys = new Set();
174
237
  const envFiles = allFiles.filter(f => /\.env(\.|$)/.test(path.basename(f)) && !f.includes('node_modules'));
175
238
  for (const ef of envFiles) {
@@ -179,7 +242,26 @@ function scan(rootDir) {
179
242
  });
180
243
  }
181
244
 
182
- // ── Languages ──────────────────────────────────────────────────────────────
245
+ // Cargo.toml text (for Rust crate detection)
246
+ const cargoText = readText(path.join(rootDir, 'Cargo.toml'));
247
+
248
+ // go.mod text (for Go module detection)
249
+ const goModText = readText(path.join(rootDir, 'go.mod'));
250
+
251
+ // CMakeLists.txt text (for C++ lib detection)
252
+ const cmakeText = readText(path.join(rootDir, 'CMakeLists.txt'));
253
+
254
+ // Python deps text
255
+ const pyText = readText(path.join(rootDir, 'requirements.txt'))
256
+ + readText(path.join(rootDir, 'pyproject.toml'))
257
+ + readText(path.join(rootDir, 'Pipfile'));
258
+
259
+ // Java build text
260
+ const pomText = readText(path.join(rootDir, 'pom.xml'));
261
+ const gradleText = readText(path.join(rootDir, 'build.gradle'))
262
+ + readText(path.join(rootDir, 'build.gradle.kts'));
263
+
264
+ // ── Languages ────────────────────────────────────────────────────────────
183
265
  const languages = [];
184
266
  for (const [lang, sig] of Object.entries(LANGUAGE_SIGS)) {
185
267
  const byFile = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r.endsWith('/' + f) || r === f));
@@ -187,25 +269,65 @@ function scan(rootDir) {
187
269
  if (byFile || byExt) languages.push(lang);
188
270
  }
189
271
 
190
- // ── Frameworks ─────────────────────────────────────────────────────────────
272
+ // ── Frameworks / Engines ─────────────────────────────────────────────────
191
273
  const frameworks = [];
192
274
  for (const [fw, sig] of Object.entries(FRAMEWORK_SIGS)) {
193
- const byFile = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.endsWith('/' + f)));
194
- const byDep = sig.deps.some(d => allPackageDeps.has(d));
195
- if (byFile || byDep) frameworks.push(fw);
275
+ const byFile = (sig.files || []).some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.endsWith('/' + f)));
276
+ const byDep = (sig.deps || []).some(d => allPackageDeps.has(d));
277
+ const byExt = (sig.exts || []).some(e => extSet.has(e));
278
+ if (byFile || byDep || byExt) frameworks.push(fw);
196
279
  }
197
- // Extra: detect Python frameworks from requirements/pyproject text
198
- const reqText = readText(path.join(rootDir, 'requirements.txt')) + readText(path.join(rootDir, 'pyproject.toml')) + readText(path.join(rootDir, 'Pipfile'));
199
- if (/django/i.test(reqText) && !frameworks.includes('Django')) frameworks.push('Django');
200
- if (/flask/i.test(reqText) && !frameworks.includes('Flask')) frameworks.push('Flask');
201
- if (/fastapi/i.test(reqText) && !frameworks.includes('FastAPI')) frameworks.push('FastAPI');
202
- if (/pytest/i.test(reqText) && !frameworks.includes('Pytest')) frameworks.push('Pytest');
203
- // Spring Boot from pom.xml
204
- const pomText = readText(path.join(rootDir, 'pom.xml'));
205
- if (/spring-boot/i.test(pomText) && !frameworks.includes('Spring Boot')) frameworks.push('Spring Boot');
206
- if (/spring-web/i.test(pomText) && !frameworks.includes('Spring MVC')) frameworks.push('Spring MVC');
207
-
208
- // ── Databases ──────────────────────────────────────────────────────────────
280
+
281
+ // Python ecosystem
282
+ if (/django/i.test(pyText) && !frameworks.includes('Django')) frameworks.push('Django');
283
+ if (/flask/i.test(pyText) && !frameworks.includes('Flask')) frameworks.push('Flask');
284
+ if (/fastapi/i.test(pyText) && !frameworks.includes('FastAPI')) frameworks.push('FastAPI');
285
+ if (/pytest/i.test(pyText) && !frameworks.includes('Pytest')) frameworks.push('Pytest');
286
+ if (/pygame/i.test(pyText) && !frameworks.includes('Pygame')) frameworks.push('Pygame');
287
+ if (/tkinter/i.test(pyText) && !frameworks.includes('Tkinter')) frameworks.push('Tkinter');
288
+ if (/pyqt|pyside/i.test(pyText) && !frameworks.includes('PyQt')) frameworks.push('PyQt');
289
+ if (/torch/i.test(pyText) && !frameworks.includes('PyTorch')) frameworks.push('PyTorch');
290
+ if (/tensorflow/i.test(pyText) && !frameworks.includes('TensorFlow')) frameworks.push('TensorFlow');
291
+ if (/sklearn|scikit/i.test(pyText) && !frameworks.includes('scikit-learn')) frameworks.push('scikit-learn');
292
+ if (/numpy/i.test(pyText) && !frameworks.includes('NumPy')) frameworks.push('NumPy');
293
+ if (/pandas/i.test(pyText) && !frameworks.includes('Pandas')) frameworks.push('Pandas');
294
+
295
+ // Java/Kotlin ecosystem
296
+ if (/spring-boot/i.test(pomText + gradleText) && !frameworks.includes('Spring Boot')) frameworks.push('Spring Boot');
297
+ if (/spring-web/i.test(pomText + gradleText) && !frameworks.includes('Spring MVC')) frameworks.push('Spring MVC');
298
+ if (/junit/i.test(pomText + gradleText) && !frameworks.includes('JUnit')) frameworks.push('JUnit');
299
+ if (/libgdx/i.test(pomText + gradleText) && !frameworks.includes('LibGDX')) frameworks.push('LibGDX');
300
+ if (/javafx/i.test(pomText + gradleText) && !frameworks.includes('JavaFX')) frameworks.push('JavaFX');
301
+ if (/mockito/i.test(pomText + gradleText) && !frameworks.includes('Mockito')) frameworks.push('Mockito');
302
+
303
+ // Go ecosystem
304
+ if (/gin-gonic\/gin/i.test(goModText) && !frameworks.includes('Gin')) frameworks.push('Gin');
305
+ if (/labstack\/echo/i.test(goModText) && !frameworks.includes('Echo')) frameworks.push('Echo');
306
+ if (/gofiber\/fiber/i.test(goModText) && !frameworks.includes('Fiber')) frameworks.push('Fiber');
307
+ if (/testify/i.test(goModText) && !frameworks.includes('Testify')) frameworks.push('Testify');
308
+
309
+ // Rust ecosystem
310
+ if (/actix-web/i.test(cargoText) && !frameworks.includes('Actix')) frameworks.push('Actix');
311
+ if (/axum/i.test(cargoText) && !frameworks.includes('Axum')) frameworks.push('Axum');
312
+ if (/bevy/i.test(cargoText) && !frameworks.includes('Bevy')) frameworks.push('Bevy');
313
+ if (/tokio/i.test(cargoText) && !frameworks.includes('Tokio')) frameworks.push('Tokio');
314
+
315
+ // C++ ecosystem from CMakeLists
316
+ if (/sfml/i.test(cmakeText) && !frameworks.includes('SFML (C++)')) frameworks.push('SFML (C++)');
317
+ if (/sdl/i.test(cmakeText) && !frameworks.includes('SDL2')) frameworks.push('SDL2');
318
+ if (/gtest|googletest/i.test(cmakeText) && !frameworks.includes('GoogleTest')) frameworks.push('GoogleTest');
319
+ if (/catch2/i.test(cmakeText) && !frameworks.includes('Catch2')) frameworks.push('Catch2');
320
+ if (/boost/i.test(cmakeText) && !frameworks.includes('Boost')) frameworks.push('Boost');
321
+ if (/wxwidgets/i.test(cmakeText) && !frameworks.includes('wxWidgets')) frameworks.push('wxWidgets');
322
+ if (/qt/i.test(cmakeText) && !frameworks.includes('Qt (C++)')) frameworks.push('Qt (C++)');
323
+
324
+ // Unity — check for Assets/ directory (Unity project root structure)
325
+ if (fileExists(rootDir, 'Assets') && fileExists(rootDir, 'ProjectSettings') && !frameworks.includes('Unity')) frameworks.push('Unity');
326
+
327
+ // Unreal — .uproject file anywhere
328
+ if (relFiles.some(f => f.endsWith('.uproject')) && !frameworks.includes('Unreal')) frameworks.push('Unreal Engine');
329
+
330
+ // ── Databases ─────────────────────────────────────────────────────────────
209
331
  const databases = [];
210
332
  for (const [db, sig] of Object.entries(DATABASE_SIGS)) {
211
333
  const byFile = sig.files.some(f => relFileSet.has(f));
@@ -214,7 +336,7 @@ function scan(rootDir) {
214
336
  if (byFile || byDep || byEnv) databases.push(db);
215
337
  }
216
338
 
217
- // ── Cloud / API services ───────────────────────────────────────────────────
339
+ // ── Cloud / API services ──────────────────────────────────────────────────
218
340
  const cloud = [];
219
341
  for (const [svc, sig] of Object.entries(CLOUD_SIGS)) {
220
342
  const byFile = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.startsWith(f + '/')));
@@ -223,56 +345,139 @@ function scan(rootDir) {
223
345
  if (byFile || byDep || byEnv) cloud.push(svc);
224
346
  }
225
347
 
226
- // ── CI/CD pipelines ────────────────────────────────────────────────────────
348
+ // ── CI/CD pipelines ───────────────────────────────────────────────────────
227
349
  const pipelines = [];
228
350
  for (const [ci, sig] of Object.entries(CI_SIGS)) {
229
351
  const found = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.startsWith(f + '/')));
230
352
  if (found) pipelines.push(ci);
231
353
  }
232
354
 
233
- // ── Structural overview ────────────────────────────────────────────────────
234
- const structure = detectStructure(rootDir, relFiles);
355
+ // ── Structure ──────────────────────────────────────────────────────────────
356
+ const structure = detectStructure(rootDir, relFiles, frameworks, languages);
235
357
 
236
358
  // ── Package metadata ───────────────────────────────────────────────────────
237
359
  const rootPkg = readJSON(path.join(rootDir, 'package.json'));
238
360
 
239
361
  return {
240
- projectName: path.basename(rootDir),
362
+ projectName: path.basename(rootDir),
241
363
  rootDir,
242
- scannedAt: new Date().toISOString(),
243
- languages: [...new Set(languages)],
244
- frameworks: [...new Set(frameworks)],
245
- databases: [...new Set(databases)],
246
- cloud: [...new Set(cloud)],
247
- pipelines: [...new Set(pipelines)],
364
+ scannedAt: new Date().toISOString(),
365
+ languages: [...new Set(languages)],
366
+ frameworks: [...new Set(frameworks)],
367
+ databases: [...new Set(databases)],
368
+ cloud: [...new Set(cloud)],
369
+ pipelines: [...new Set(pipelines)],
248
370
  structure,
249
- packageMeta: rootPkg ? { name: rootPkg.name, version: rootPkg.version, description: rootPkg.description, scripts: rootPkg.scripts } : null,
250
- totalFiles: allFiles.length,
371
+ packageMeta: rootPkg ? { name: rootPkg.name, version: rootPkg.version, description: rootPkg.description, scripts: rootPkg.scripts } : null,
372
+ totalFiles: allFiles.length,
251
373
  envFilesFound: envFiles.map(f => path.relative(rootDir, f)),
252
374
  };
253
375
  }
254
376
 
255
- function detectStructure(rootDir, relFiles) {
377
+ // ─── Structure detection ──────────────────────────────────────────────────────
378
+
379
+ function detectStructure(rootDir, relFiles, frameworks, languages) {
256
380
  const struct = { type: 'unknown', parts: [] };
257
- const hasFrontend = fileExists(rootDir, 'frontend') || fileExists(rootDir, 'client') || fileExists(rootDir, 'web');
258
- const hasBackend = fileExists(rootDir, 'backend') || fileExists(rootDir, 'server') || fileExists(rootDir, 'api');
259
- const hasMobile = fileExists(rootDir, 'mobile') || fileExists(rootDir, 'app') || relFiles.some(f => f.endsWith('.dart') || f.endsWith('.swift'));
260
-
261
- if (hasFrontend && hasBackend) { struct.type = 'Fullstack'; }
262
- else if (hasFrontend) { struct.type = 'Frontend SPA'; }
263
- else if (hasBackend) { struct.type = 'Backend API'; }
264
- else if (hasMobile) { struct.type = 'Mobile App'; }
265
- else { struct.type = 'Monolith / Other'; }
266
-
267
- const dirs = fs.readdirSync(rootDir, { withFileTypes: true })
268
- .filter(e => e.isDirectory() && !SKIP_DIRS.has(e.name))
269
- .map(e => e.name);
381
+
382
+ const has = (p) => fs.existsSync(path.join(rootDir, p));
383
+
384
+ // ── Project type classification ──────────────────────────────────────────
385
+ const GAME_ENGINES = ['Unity', 'Godot', 'Unreal Engine', 'Bevy', 'Phaser', 'Three.js', 'Babylon', 'PixiJS', 'SFML (C++)', 'SDL2', 'LibGDX', 'MonoGame', 'Pygame'];
386
+ const DESKTOP_FRAMES = ['Electron', 'Tauri', 'Qt (C++)', 'wxWidgets', 'JavaFX', 'Tkinter', 'PyQt', 'GTK'];
387
+ const ML_FRAMES = ['PyTorch', 'TensorFlow', 'scikit-learn', 'Jupyter', 'NumPy', 'Pandas'];
388
+ const WEB_FRONT = ['React', 'Vue', 'Angular', 'Svelte', 'Next.js', 'Nuxt', 'Gatsby', 'SvelteKit', 'Remix', 'SolidJS', 'Qwik', 'Vite'];
389
+ const WEB_BACK = ['Express', 'Fastify', 'NestJS', 'Koa', 'Hapi', 'Django', 'Flask', 'FastAPI', 'Laravel', 'Rails', 'Spring Boot', 'Spring MVC', 'Gin', 'Echo', 'Fiber', 'Actix', 'Axum'];
390
+ const MOBILE_FRAMES = ['Flutter', 'React Native', 'Expo'];
391
+
392
+ const hasGameEngine = frameworks.some(f => GAME_ENGINES.includes(f));
393
+ const hasDesktop = frameworks.some(f => DESKTOP_FRAMES.includes(f));
394
+ const hasML = frameworks.some(f => ML_FRAMES.includes(f));
395
+ const hasWebFront = frameworks.some(f => WEB_FRONT.includes(f)) || has('frontend') || has('client') || has('web');
396
+ const hasWebBack = frameworks.some(f => WEB_BACK.includes(f)) || has('backend') || has('server') || has('api');
397
+ const hasMobile = frameworks.some(f => MOBILE_FRAMES.includes(f))|| has('mobile') || relFiles.some(f => f.endsWith('.dart') || f.endsWith('.swift'));
398
+
399
+ // DSA / competitive programming detection:
400
+ // - Only source files, no framework at all, flat structure with single-file solutions
401
+ // - Files named by pattern: TwoSum, solution, main, problem number
402
+ const isSingleFileDSA = relFiles.filter(f => !f.includes('node_modules') && !f.startsWith('.'))
403
+ .filter(f => /\.(py|js|ts|java|cpp|c|go|rs|cs|rb|swift|kt)$/.test(f))
404
+ .length > 0
405
+ && !hasGameEngine && !hasDesktop && !hasML && !hasWebFront && !hasWebBack && !hasMobile
406
+ && relFiles.filter(f => /\.(py|js|ts|java|cpp|c|go|rs|cs)$/.test(f) && !f.includes('node_modules')).length < 30;
407
+
408
+ // Competitive programming: typically has many small solution files, possibly in folders by platform
409
+ const isCompetitiveProg = relFiles.some(f =>
410
+ /\/(codeforces|leetcode|hackerrank|atcoder|codechef|spoj|usaco|topcoder)\//i.test(f)
411
+ ) || relFiles.filter(f => /solution\d*\.(cpp|py|java|go|js|rs)$/i.test(path.basename(f))).length >= 2;
412
+
413
+ // Embedded / systems: Arduino, PlatformIO, Zephyr, bare-metal C
414
+ const isEmbedded = has('platformio.ini') || has('arduino.json')
415
+ || relFiles.some(f => f.endsWith('.ino'))
416
+ || relFiles.some(f => /\b(zephyr|esp32|esp8266|stm32|avr|arm)\b/i.test(f));
417
+
418
+ // CLI tool: has a bin/cli entry, no UI framework
419
+ const isCLI = (has('bin') || relFiles.some(f => /^bin\//.test(f)))
420
+ && !hasWebFront && !hasGameEngine && !hasDesktop;
421
+
422
+ // Classify
423
+ if (isCompetitiveProg) { struct.type = 'Competitive Programming / DSA'; }
424
+ else if (isSingleFileDSA) { struct.type = 'DSA / Script'; }
425
+ else if (isEmbedded) { struct.type = 'Embedded / Systems'; }
426
+ else if (hasGameEngine) { struct.type = 'Game'; }
427
+ else if (hasDesktop) { struct.type = 'Desktop App'; }
428
+ else if (hasML) { struct.type = 'ML / Data Science'; }
429
+ else if (hasWebFront && hasWebBack) { struct.type = 'Fullstack Web'; }
430
+ else if (hasWebFront) { struct.type = 'Frontend SPA'; }
431
+ else if (hasWebBack) { struct.type = 'Backend API'; }
432
+ else if (hasMobile) { struct.type = 'Mobile App'; }
433
+ else if (isCLI) { struct.type = 'CLI Tool'; }
434
+ else { struct.type = 'Monolith / Library'; }
435
+
436
+ const dirs = [];
437
+ try {
438
+ fs.readdirSync(rootDir, { withFileTypes: true })
439
+ .filter(e => e.isDirectory() && !SKIP_DIRS.has(e.name))
440
+ .forEach(e => dirs.push(e.name));
441
+ } catch {}
270
442
  struct.parts = dirs;
271
443
 
272
- // Discover key files
273
- struct.entryPoints = relFiles.filter(f => /\b(server|app|main|index)\.(js|ts|py|go|rs|java|cs|rb|php)$/.test(f) && !f.includes('node_modules'));
274
- struct.testFiles = relFiles.filter(f => /(__tests__|test|spec).*\.(test|spec)\.(js|ts|py|go|java|rb|php|rs)$/.test(f) || /\.(test|spec)\.(js|ts|py|go)$/.test(f));
275
- struct.configFiles = relFiles.filter(f => /(\.env|docker-compose|Dockerfile|\.github|Jenkinsfile|\.travis|jest\.config|tsconfig|vite\.config|next\.config|package\.json)/.test(f) && !f.includes('node_modules'));
444
+ // Test file detection — broad, covers all languages and styles
445
+ struct.testFiles = relFiles.filter(f =>
446
+ !f.includes('node_modules') &&
447
+ (
448
+ // Named test folders
449
+ /\/(tests?|spec|__tests__|test_cases?|testdata)\//i.test('/' + f) ||
450
+ // Named test files (all languages)
451
+ /\.(test|spec)\.(js|ts|jsx|tsx|py|go|rb|php|rs|cs|java|kt|swift)$/.test(f) ||
452
+ // Python style: test_*.py or *_test.py
453
+ /\/test_[^/]+\.py$/.test('/' + f) || /\/[^/]+_test\.py$/.test('/' + f) ||
454
+ // Go style: *_test.go
455
+ /_test\.go$/.test(f) ||
456
+ // Java style: *Test.java, *Tests.java, *Spec.java
457
+ /[A-Z][a-zA-Z]*(Test|Tests|Spec|IT)\.java$/.test(path.basename(f)) ||
458
+ // Rust: files in tests/ directory or #[test] (we just detect tests/ folder)
459
+ /^tests\/[^/]+\.rs$/.test(f) ||
460
+ // C++: test files
461
+ /[Tt]est[^/]*\.(cpp|cc|cxx)$/.test(path.basename(f)) ||
462
+ // General: files with "test" in name in common patterns
463
+ /\/[^/]*\.(test|spec)\.[^/]+$/.test('/' + f)
464
+ )
465
+ );
466
+
467
+ // Entry point detection — broad
468
+ struct.entryPoints = relFiles.filter(f =>
469
+ !f.includes('node_modules') && (
470
+ /\b(server|app|main|index|program|game|solution|entry)\.(js|ts|mjs|py|go|rs|java|cs|rb|php|cpp|c|swift|kt)$/.test(f) ||
471
+ /^main\.(py|go|rs|cpp|c|java|cs|rb|swift)$/.test(path.basename(f)) ||
472
+ /^src\/main\.(py|go|rs|cpp|c|java|cs|rb|swift|kt)$/.test(f) ||
473
+ /^src\/lib\.rs$/.test(f)
474
+ )
475
+ );
476
+
477
+ struct.configFiles = relFiles.filter(f =>
478
+ /(\.env|docker-compose|Dockerfile|\.github|Jenkinsfile|\.travis|jest\.config|tsconfig|vite\.config|next\.config|package\.json|Cargo\.toml|go\.mod|pom\.xml|build\.gradle|CMakeLists|platformio\.ini|project\.godot)/.test(f)
479
+ && !f.includes('node_modules')
480
+ );
276
481
 
277
482
  return struct;
278
483
  }