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 +313 -108
- package/lib/test-runner.js +651 -189
- package/package.json +1 -1
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,
|
|
5
|
-
*
|
|
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
|
-
// ───
|
|
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:
|
|
41
|
-
'Next.js':
|
|
42
|
-
Vue:
|
|
43
|
-
Angular:
|
|
44
|
-
Svelte:
|
|
45
|
-
Vite:
|
|
46
|
-
Nuxt:
|
|
47
|
-
Gatsby:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
72
|
-
PostgreSQL:
|
|
73
|
-
MySQL:
|
|
74
|
-
SQLite:
|
|
75
|
-
Redis:
|
|
76
|
-
Supabase:
|
|
77
|
-
Prisma:
|
|
78
|
-
Firebase:
|
|
79
|
-
DynamoDB:
|
|
80
|
-
Elasticsearch:{ files: [], deps: ['@elastic/elasticsearch'], envKeys: ['ELASTICSEARCH_URL'] },
|
|
81
|
-
'SQL Server':
|
|
82
|
-
Cassandra:
|
|
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:
|
|
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:
|
|
89
|
-
Vercel:
|
|
90
|
-
Netlify:
|
|
91
|
-
Heroku:
|
|
92
|
-
Cloudflare:
|
|
93
|
-
Railway:
|
|
94
|
-
Render:
|
|
95
|
-
ImageKit:
|
|
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
|
-
//
|
|
224
|
+
// All extensions present
|
|
162
225
|
const extSet = new Set(allFiles.map(f => path.extname(f).toLowerCase()).filter(Boolean));
|
|
163
226
|
|
|
164
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
if (/django/i.test(
|
|
200
|
-
if (/flask/i.test(
|
|
201
|
-
if (/fastapi/i.test(
|
|
202
|
-
if (/pytest/i.test(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (/
|
|
206
|
-
if (/
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
// ──
|
|
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:
|
|
362
|
+
projectName: path.basename(rootDir),
|
|
241
363
|
rootDir,
|
|
242
|
-
scannedAt:
|
|
243
|
-
languages:
|
|
244
|
-
frameworks:
|
|
245
|
-
databases:
|
|
246
|
-
cloud:
|
|
247
|
-
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:
|
|
250
|
-
totalFiles:
|
|
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
|
-
|
|
377
|
+
// ─── Structure detection ──────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
function detectStructure(rootDir, relFiles, frameworks, languages) {
|
|
256
380
|
const struct = { type: 'unknown', parts: [] };
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
//
|
|
273
|
-
struct.
|
|
274
|
-
|
|
275
|
-
|
|
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
|
}
|