project-scan 1.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.
@@ -0,0 +1,277 @@
1
+ 'use strict';
2
+ /**
3
+ * md-report.js — Generates Markdown summary files from scan + test results.
4
+ * Produces: SUMMARY.md, ONBOARDING.md, TESTS.md
5
+ */
6
+
7
+ const path = require('path');
8
+
9
+ // ─── SUMMARY.md ───────────────────────────────────────────────────────────────
10
+
11
+ function generateSummary(scanResult, testResults) {
12
+ const { projectName, scannedAt, languages, frameworks, databases, cloud, pipelines, structure, totalFiles, packageMeta, envFilesFound } = scanResult;
13
+
14
+ const totalPassed = testResults.reduce((s, r) => s + (r.passed || 0), 0);
15
+ const totalFailed = testResults.reduce((s, r) => s + (r.failed || 0), 0);
16
+ const totalSkipped = testResults.reduce((s, r) => s + (r.skipped || 0), 0);
17
+ const overallPass = totalFailed === 0 && testResults.some(r => r.status === 'passed');
18
+ const testBadge = testResults.length === 0 ? '⚪ No tests'
19
+ : overallPass ? `✅ PASSING (${totalPassed} tests)` : `❌ FAILING (${totalFailed} failures)`;
20
+
21
+ const pills = (arr) => arr.length > 0 ? arr.map(i => `\`${i}\``).join(' · ') : '_None detected_';
22
+
23
+ const testSuiteSummary = testResults.map(r => {
24
+ const icon = r.status === 'passed' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'error' ? '⚠️' : '⏭️';
25
+ const ms = r.duration ? `${(r.duration/1000).toFixed(2)}s` : '—';
26
+ return `| ${icon} ${r.suite} | ${r.passed||0} | ${r.failed||0} | ${r.skipped||0} | ${ms} |`;
27
+ }).join('\n');
28
+
29
+ const entryPoints = (structure.entryPoints || []).slice(0, 8).map(f => ` - \`${f}\``).join('\n') || ' - _None detected_';
30
+ const testFiles = (structure.testFiles || []).slice(0, 12).map(f => ` - \`${f}\``).join('\n') || ' - _None detected_';
31
+
32
+ return `# 📋 ${projectName} — Project Summary
33
+
34
+ > **Scanned:** ${scannedAt}
35
+ > **Project type:** ${structure.type}
36
+ > **Total files scanned:** ${totalFiles.toLocaleString()}
37
+ > **Test status:** ${testBadge}
38
+
39
+ ${packageMeta?.description ? `> ${packageMeta.description}\n` : ''}
40
+ ---
41
+
42
+ ## 💻 Languages
43
+ ${pills(languages)}
44
+
45
+ ## ⚙️ Frameworks & Libraries
46
+ ${pills(frameworks)}
47
+
48
+ ## 🗄️ Databases & Storage
49
+ ${pills(databases)}
50
+
51
+ ## ☁️ Cloud & API Services
52
+ ${pills(cloud)}
53
+
54
+ ## 🔄 CI/CD Pipelines
55
+ ${pills(pipelines)}
56
+
57
+ ---
58
+
59
+ ## 📁 Project Structure
60
+
61
+ **Type:** ${structure.type}
62
+
63
+ **Top-level directories:**
64
+ ${structure.parts.length > 0 ? structure.parts.map(p => `- \`/${p}\``).join('\n') : '- _No subdirectories_'}
65
+
66
+ **Entry points:**
67
+ ${entryPoints}
68
+
69
+ **Test files found (${(structure.testFiles||[]).length}):**
70
+ ${testFiles}
71
+
72
+ ---
73
+
74
+ ## 🧪 Test Summary
75
+
76
+ | Suite | ✅ Passed | ❌ Failed | ⏭️ Skipped | ⏱ Duration |
77
+ |-------|-----------|-----------|------------|------------|
78
+ ${testSuiteSummary || '| — | — | — | — | — |'}
79
+
80
+ **Total: ${totalPassed} passed · ${totalFailed} failed · ${totalSkipped} skipped**
81
+
82
+ ${envFilesFound.length > 0 ? `---\n\n## 🔒 Environment Files\n${envFilesFound.map(f => `- \`${f}\``).join('\n')}` : ''}
83
+ ---
84
+
85
+ _Generated by [project-scan](https://npmjs.com/package/project-scan)_
86
+ `;
87
+ }
88
+
89
+ // ─── ONBOARDING.md ────────────────────────────────────────────────────────────
90
+
91
+ function generateOnboarding(scanResult) {
92
+ const { projectName, scannedAt, languages, frameworks, databases, cloud, pipelines, structure, packageMeta } = scanResult;
93
+
94
+ const pills = (arr) => arr.length > 0 ? arr.map(i => `- **${i}**`).join('\n') : '- _None detected_';
95
+
96
+ const detectSetupCmd = () => {
97
+ const cmds = [];
98
+ if (languages.includes('JavaScript') || languages.includes('TypeScript')) {
99
+ cmds.push('npm install # or: yarn install / pnpm install');
100
+ }
101
+ if (languages.includes('Python')) {
102
+ cmds.push('pip install -r requirements.txt # or: pip install -e .');
103
+ }
104
+ if (languages.includes('Go')) cmds.push('go mod download');
105
+ if (languages.includes('Rust')) cmds.push('cargo build');
106
+ if (languages.includes('Java') || languages.includes('Kotlin')) cmds.push('mvn install # or: ./gradlew build');
107
+ if (languages.includes('PHP')) cmds.push('composer install');
108
+ if (languages.includes('Ruby')) cmds.push('bundle install');
109
+ if (languages.includes('Dart')) cmds.push('flutter pub get # or: dart pub get');
110
+ return cmds.length > 0 ? cmds.join('\n') : '# No standard setup detected';
111
+ };
112
+
113
+ const detectRunCmd = () => {
114
+ const scripts = packageMeta?.scripts || {};
115
+ const lines = [];
116
+ if (scripts.dev) lines.push(`npm run dev # ${scripts.dev}`);
117
+ if (scripts.start) lines.push(`npm start # ${scripts.start}`);
118
+ if (!lines.length && languages.includes('Python')) lines.push('python main.py # or: uvicorn main:app');
119
+ if (!lines.length && languages.includes('Go')) lines.push('go run .');
120
+ if (!lines.length && languages.includes('Rust')) lines.push('cargo run');
121
+ return lines.join('\n') || '# Check README for run instructions';
122
+ };
123
+
124
+ const archLines = [];
125
+ archLines.push('```');
126
+ archLines.push(`┌──────────────────────────────────────────────────┐`);
127
+ archLines.push(`│ Project: ${projectName.padEnd(39)}│`);
128
+ archLines.push(`│ Type: ${structure.type.padEnd(39)}│`);
129
+ archLines.push(`│ │`);
130
+ structure.parts.forEach(p => {
131
+ archLines.push(`│ ├── /${p.padEnd(43)}│`);
132
+ });
133
+ archLines.push(`└──────────────────────────────────────────────────┘`);
134
+ archLines.push('```');
135
+
136
+ return `# 🚀 Onboarding Guide — ${projectName}
137
+
138
+ > **Auto-generated on:** ${scannedAt}
139
+ > **Project type:** ${structure.type}
140
+
141
+ ---
142
+
143
+ ## 1. Technology Stack
144
+
145
+ ### Languages
146
+ ${pills(languages)}
147
+
148
+ ### Frameworks & Libraries
149
+ ${pills(frameworks)}
150
+
151
+ ### Databases & Storage
152
+ ${pills(databases)}
153
+
154
+ ### Cloud & API Services
155
+ ${pills(cloud)}
156
+
157
+ ### CI/CD Pipelines
158
+ ${pills(pipelines)}
159
+
160
+ ---
161
+
162
+ ## 2. Architecture Overview
163
+
164
+ ${archLines.join('\n')}
165
+
166
+ ${(structure.entryPoints||[]).length > 0 ? `### Entry Points\n${structure.entryPoints.slice(0,8).map(f => `- \`${f}\``).join('\n')}` : ''}
167
+
168
+ ---
169
+
170
+ ## 3. Getting Started
171
+
172
+ ### Install dependencies
173
+ \`\`\`bash
174
+ ${detectSetupCmd()}
175
+ \`\`\`
176
+
177
+ ### Run the project
178
+ \`\`\`bash
179
+ ${detectRunCmd()}
180
+ \`\`\`
181
+
182
+ ### Run tests
183
+ \`\`\`bash
184
+ npx project-scan --tests-only
185
+ # or use the local runner:
186
+ bash run_tests.sh
187
+ \`\`\`
188
+
189
+ ### Generate this report again
190
+ \`\`\`bash
191
+ npx project-scan --report
192
+ \`\`\`
193
+
194
+ ---
195
+
196
+ _Generated by [project-scan](https://npmjs.com/package/project-scan)_
197
+ `;
198
+ }
199
+
200
+ // ─── TESTS.md ─────────────────────────────────────────────────────────────────
201
+
202
+ function generateTestsReport(scanResult, testResults) {
203
+ const { projectName, scannedAt } = scanResult;
204
+
205
+ const totalPassed = testResults.reduce((s, r) => s + (r.passed || 0), 0);
206
+ const totalFailed = testResults.reduce((s, r) => s + (r.failed || 0), 0);
207
+ const totalSkipped = testResults.reduce((s, r) => s + (r.skipped || 0), 0);
208
+ const totalTests = totalPassed + totalFailed + totalSkipped;
209
+ const overallPass = totalFailed === 0 && testResults.some(r => r.status === 'passed');
210
+
211
+ const statusBanner = overallPass
212
+ ? `> ## ✅ ALL TESTS PASSING\n> ${totalPassed}/${totalTests} tests passed`
213
+ : totalFailed > 0
214
+ ? `> ## ❌ ${totalFailed} TEST(S) FAILING\n> ${totalPassed} passed · ${totalFailed} failed · ${totalSkipped} skipped`
215
+ : `> ## ⏭️ NO TESTS RAN\n> No test suites were executed`;
216
+
217
+ const suiteDetails = testResults.map(r => {
218
+ const icon = r.status === 'passed' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'error' ? '⚠️' : '⏭️';
219
+ const ms = r.duration ? `${(r.duration/1000).toFixed(2)}s` : '—';
220
+ const output = r.output ? `\n\n<details>\n<summary>Output log</summary>\n\n\`\`\`\n${r.output.slice(-2000)}\n\`\`\`\n</details>` : '';
221
+ const error = r.error ? `\n\n> **Error:** ${r.error}` : '';
222
+ return `### ${icon} ${r.suite}\n\n| Metric | Value |\n|--------|-------|\n| Status | \`${r.status}\` |\n| Passed | ${r.passed||0} |\n| Failed | ${r.failed||0} |\n| Skipped | ${r.skipped||0} |\n| Duration | ${ms} |\n${r.suites ? `| Suites | ${r.suites} |\n` : ''}${output}${error}`;
223
+ }).join('\n\n---\n\n');
224
+
225
+ const testFilesList = (scanResult.structure.testFiles || []).slice(0, 30).map(f => `- \`${f}\``).join('\n') || '- _None found_';
226
+
227
+ return `# 🧪 Test Report — ${projectName}
228
+
229
+ > **Generated:** ${scannedAt}
230
+ > **Project:** \`${projectName}\`
231
+
232
+ ${statusBanner}
233
+
234
+ ---
235
+
236
+ ## Summary
237
+
238
+ | Suite | Status | ✅ Passed | ❌ Failed | ⏭️ Skipped | ⏱ Duration |
239
+ |-------|--------|-----------|-----------|------------|------------|
240
+ ${testResults.map(r => {
241
+ const icon = r.status === 'passed' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'error' ? '⚠️' : '⏭️';
242
+ return `| ${r.suite} | ${icon} ${r.status} | ${r.passed||0} | ${r.failed||0} | ${r.skipped||0} | ${r.duration ? (r.duration/1000).toFixed(2)+'s' : '—'} |`;
243
+ }).join('\n') || '| — | — | — | — | — | — |'}
244
+
245
+ **Total: ${totalPassed} passed · ${totalFailed} failed · ${totalSkipped} skipped · ${totalTests} total**
246
+
247
+ ---
248
+
249
+ ## Test Files Detected
250
+
251
+ ${testFilesList}
252
+
253
+ ---
254
+
255
+ ## Detailed Suite Results
256
+
257
+ ${suiteDetails || '_No test suites ran._'}
258
+
259
+ ---
260
+
261
+ ## How to Run Tests
262
+
263
+ \`\`\`bash
264
+ # Run all tests via project-scan
265
+ npx project-scan --tests-only
266
+
267
+ # Or re-run manually
268
+ bash run_tests.sh
269
+ \`\`\`
270
+
271
+ ---
272
+
273
+ _Generated by [project-scan](https://npmjs.com/package/project-scan)_
274
+ `;
275
+ }
276
+
277
+ module.exports = { generateSummary, generateOnboarding, generateTestsReport };
package/lib/scanner.js ADDED
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+ /**
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.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // ─── Signature maps ──────────────────────────────────────────────────────────
12
+
13
+ const LANGUAGE_SIGS = {
14
+ JavaScript: { files: ['package.json'], exts: ['.js', '.mjs', '.cjs'] },
15
+ TypeScript: { files: ['tsconfig.json'], exts: ['.ts', '.tsx'] },
16
+ Python: { files: ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile', 'setup.cfg'], exts: ['.py'] },
17
+ Java: { files: ['pom.xml', 'build.gradle', 'build.gradle.kts'], exts: ['.java'] },
18
+ Kotlin: { files: ['settings.gradle.kts', 'build.gradle.kts'], exts: ['.kt', '.kts'] },
19
+ Go: { files: ['go.mod', 'go.sum'], exts: ['.go'] },
20
+ Rust: { files: ['Cargo.toml', 'Cargo.lock'], exts: ['.rs'] },
21
+ 'C/C++': { files: ['CMakeLists.txt', 'Makefile', 'meson.build'], exts: ['.c', '.cpp', '.cc', '.cxx', '.h', '.hpp'] },
22
+ 'C#': { files: [], exts: ['.cs', '.csproj', '.sln'] },
23
+ PHP: { files: ['composer.json', 'composer.lock'], exts: ['.php'] },
24
+ Ruby: { files: ['Gemfile', 'Gemfile.lock', '.ruby-version'], exts: ['.rb'] },
25
+ Swift: { files: ['Package.swift'], exts: ['.swift'] },
26
+ Dart: { files: ['pubspec.yaml', 'pubspec.lock'], exts: ['.dart'] },
27
+ Scala: { files: ['build.sbt'], exts: ['.scala'] },
28
+ Elixir: { files: ['mix.exs', 'mix.lock'], exts: ['.ex', '.exs'] },
29
+ Lua: { files: [], exts: ['.lua'] },
30
+ R: { files: ['DESCRIPTION', 'NAMESPACE'], exts: ['.r', '.R'] },
31
+ Shell: { files: [], exts: ['.sh', '.bash', '.zsh'] },
32
+ PowerShell: { files: [], exts: ['.ps1', '.psm1'] },
33
+ Terraform: { files: [], exts: ['.tf', '.tfvars'] },
34
+ YAML: { files: [], exts: ['.yaml', '.yml'] },
35
+ Dockerfile: { files: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml'], exts: [] },
36
+ };
37
+
38
+ 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: [] },
68
+ };
69
+
70
+ 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: [] },
83
+ };
84
+
85
+ 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'] },
87
+ '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'] },
96
+ };
97
+
98
+ const CI_SIGS = {
99
+ 'GitHub Actions': { files: ['.github/workflows'] },
100
+ 'GitLab CI': { files: ['.gitlab-ci.yml'] },
101
+ 'CircleCI': { files: ['.circleci/config.yml'] },
102
+ 'Travis CI': { files: ['.travis.yml'] },
103
+ Jenkins: { files: ['Jenkinsfile'] },
104
+ Drone: { files: ['.drone.yml'] },
105
+ Bitbucket: { files: ['bitbucket-pipelines.yml'] },
106
+ 'Azure Pipelines':{ files: ['azure-pipelines.yml'] },
107
+ TeamCity: { files: ['.teamcity'] },
108
+ };
109
+
110
+ // ─── Helper utilities ────────────────────────────────────────────────────────
111
+
112
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
113
+ 'venv', '.venv', 'env', 'target', 'vendor', '.gradle', 'coverage', '.nyc_output',
114
+ 'out', 'bin', 'obj', '.idea', '.vscode', '.dart_tool', '.pub-cache']);
115
+
116
+ function walk(dir, depth = 0, maxDepth = 4) {
117
+ if (depth > maxDepth) return [];
118
+ let results = [];
119
+ let entries;
120
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
121
+ for (const e of entries) {
122
+ const full = path.join(dir, e.name);
123
+ if (e.isDirectory()) {
124
+ if (!SKIP_DIRS.has(e.name)) results = results.concat(walk(full, depth + 1, maxDepth));
125
+ } else {
126
+ results.push(full);
127
+ }
128
+ }
129
+ return results;
130
+ }
131
+
132
+ function fileExists(root, rel) {
133
+ // rel can be a file or directory check
134
+ return fs.existsSync(path.join(root, rel));
135
+ }
136
+
137
+ function readJSON(p) {
138
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
139
+ }
140
+
141
+ function readText(p) {
142
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
143
+ }
144
+
145
+ function allDeps(pkgJson) {
146
+ if (!pkgJson) return [];
147
+ return [
148
+ ...Object.keys(pkgJson.dependencies || {}),
149
+ ...Object.keys(pkgJson.devDependencies || {}),
150
+ ...Object.keys(pkgJson.peerDependencies || {}),
151
+ ];
152
+ }
153
+
154
+ // ─── Main scanner ────────────────────────────────────────────────────────────
155
+
156
+ function scan(rootDir) {
157
+ const allFiles = walk(rootDir);
158
+ const relFiles = allFiles.map(f => f.slice(rootDir.length + 1).replace(/\\/g, '/'));
159
+ const relFileSet = new Set(relFiles);
160
+
161
+ // Collect all extensions present
162
+ const extSet = new Set(allFiles.map(f => path.extname(f).toLowerCase()).filter(Boolean));
163
+
164
+ // Collect all package.json deps across the whole tree
165
+ const allPackageDeps = new Set();
166
+ const packageJsonFiles = allFiles.filter(f => path.basename(f) === 'package.json' && !f.includes('node_modules'));
167
+ for (const pf of packageJsonFiles) {
168
+ const pkg = readJSON(pf);
169
+ if (pkg) allDeps(pkg).forEach(d => allPackageDeps.add(d));
170
+ }
171
+
172
+ // Collect .env keys present
173
+ const envKeys = new Set();
174
+ const envFiles = allFiles.filter(f => /\.env(\.|$)/.test(path.basename(f)) && !f.includes('node_modules'));
175
+ for (const ef of envFiles) {
176
+ readText(ef).split('\n').forEach(line => {
177
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=/);
178
+ if (m) envKeys.add(m[1]);
179
+ });
180
+ }
181
+
182
+ // ── Languages ──────────────────────────────────────────────────────────────
183
+ const languages = [];
184
+ for (const [lang, sig] of Object.entries(LANGUAGE_SIGS)) {
185
+ const byFile = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r.endsWith('/' + f) || r === f));
186
+ const byExt = sig.exts.some(e => extSet.has(e));
187
+ if (byFile || byExt) languages.push(lang);
188
+ }
189
+
190
+ // ── Frameworks ─────────────────────────────────────────────────────────────
191
+ const frameworks = [];
192
+ 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);
196
+ }
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 ──────────────────────────────────────────────────────────────
209
+ const databases = [];
210
+ for (const [db, sig] of Object.entries(DATABASE_SIGS)) {
211
+ const byFile = sig.files.some(f => relFileSet.has(f));
212
+ const byDep = sig.deps.some(d => allPackageDeps.has(d));
213
+ const byEnv = (sig.envKeys || []).some(k => envKeys.has(k));
214
+ if (byFile || byDep || byEnv) databases.push(db);
215
+ }
216
+
217
+ // ── Cloud / API services ───────────────────────────────────────────────────
218
+ const cloud = [];
219
+ for (const [svc, sig] of Object.entries(CLOUD_SIGS)) {
220
+ const byFile = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.startsWith(f + '/')));
221
+ const byDep = (sig.deps || []).some(d => allPackageDeps.has(d));
222
+ const byEnv = (sig.envKeys || []).some(k => envKeys.has(k));
223
+ if (byFile || byDep || byEnv) cloud.push(svc);
224
+ }
225
+
226
+ // ── CI/CD pipelines ────────────────────────────────────────────────────────
227
+ const pipelines = [];
228
+ for (const [ci, sig] of Object.entries(CI_SIGS)) {
229
+ const found = sig.files.some(f => relFileSet.has(f) || relFiles.some(r => r === f || r.startsWith(f + '/')));
230
+ if (found) pipelines.push(ci);
231
+ }
232
+
233
+ // ── Structural overview ────────────────────────────────────────────────────
234
+ const structure = detectStructure(rootDir, relFiles);
235
+
236
+ // ── Package metadata ───────────────────────────────────────────────────────
237
+ const rootPkg = readJSON(path.join(rootDir, 'package.json'));
238
+
239
+ return {
240
+ projectName: path.basename(rootDir),
241
+ 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)],
248
+ structure,
249
+ packageMeta: rootPkg ? { name: rootPkg.name, version: rootPkg.version, description: rootPkg.description, scripts: rootPkg.scripts } : null,
250
+ totalFiles: allFiles.length,
251
+ envFilesFound: envFiles.map(f => path.relative(rootDir, f)),
252
+ };
253
+ }
254
+
255
+ function detectStructure(rootDir, relFiles) {
256
+ 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);
270
+ struct.parts = dirs;
271
+
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'));
276
+
277
+ return struct;
278
+ }
279
+
280
+ module.exports = { scan };