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.
- package/README.md +173 -0
- package/bin/project-scan.js +281 -0
- package/lib/html-report.js +174 -0
- package/lib/md-report.js +277 -0
- package/lib/scanner.js +280 -0
- package/lib/test-runner.js +447 -0
- package/package.json +35 -0
package/lib/md-report.js
ADDED
|
@@ -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 };
|