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 ADDED
@@ -0,0 +1,173 @@
1
+ # 🔍 project-scan
2
+
3
+ > **Universal Project Summarizer & Test Runner**
4
+ > Scan any project, detect its full stack, run all its tests, and generate beautiful reports — in one command.
5
+
6
+ ---
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ # Use without installing (recommended for any project)
12
+ npx project-scan
13
+
14
+ # Install globally for permanent use
15
+ npm install -g project-scan
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Usage
21
+
22
+ Run from the **root of any project**:
23
+
24
+ ```bash
25
+ cd your-project
26
+ npx project-scan
27
+ ```
28
+
29
+ ### Flags
30
+
31
+ | Flag | Description |
32
+ |------|-------------|
33
+ | _(default)_ | Scan + run tests + write all reports |
34
+ | `--scan-only` | Only scan — skip running tests |
35
+ | `--tests-only` | Only run tests — skip scan |
36
+ | `--html` | Also open HTML report in browser |
37
+ | `--no-html` | Skip HTML report generation |
38
+ | `--out <dir>` | Write reports to a custom directory |
39
+ | `--json` | Print raw scan+test JSON to stdout |
40
+ | `--quiet, -q` | Suppress console output |
41
+ | `--help, -h` | Show help |
42
+
43
+ ### Examples
44
+
45
+ ```bash
46
+ # Full scan + tests + all reports
47
+ npx project-scan
48
+
49
+ # Scan only (no test execution)
50
+ npx project-scan --scan-only
51
+
52
+ # Run tests only
53
+ npx project-scan --tests-only
54
+
55
+ # Write reports to ./docs
56
+ npx project-scan --out ./docs
57
+
58
+ # Open HTML report in browser
59
+ npx project-scan --html
60
+
61
+ # Get raw JSON output for CI pipelines
62
+ npx project-scan --json > scan-result.json
63
+ ```
64
+
65
+ ---
66
+
67
+ ## What it generates
68
+
69
+ | File | Description |
70
+ |------|-------------|
71
+ | `SUMMARY.md` | Quick project overview with test pass/fail status |
72
+ | `ONBOARDING.md` | Full onboarding guide with stack breakdown + setup commands |
73
+ | `TESTS.md` | Detailed per-suite test results with output logs |
74
+ | `PROJECT_REPORT.html` | Visual self-contained HTML report, open in any browser |
75
+
76
+ ---
77
+
78
+ ## Supported Languages
79
+
80
+ | Language | Detection |
81
+ |----------|-----------|
82
+ | JavaScript | `package.json`, `.js` files |
83
+ | TypeScript | `tsconfig.json`, `.ts` files |
84
+ | Python | `requirements.txt`, `pyproject.toml`, `.py` files |
85
+ | Java | `pom.xml`, `build.gradle`, `.java` files |
86
+ | Kotlin | `build.gradle.kts`, `.kt` files |
87
+ | Go | `go.mod`, `.go` files |
88
+ | Rust | `Cargo.toml`, `.rs` files |
89
+ | C/C++ | `CMakeLists.txt`, `Makefile`, `.cpp/.c` files |
90
+ | C# | `.csproj`, `.sln`, `.cs` files |
91
+ | PHP | `composer.json`, `.php` files |
92
+ | Ruby | `Gemfile`, `.rb` files |
93
+ | Swift | `Package.swift`, `.swift` files |
94
+ | Dart/Flutter | `pubspec.yaml`, `.dart` files |
95
+ | Scala | `build.sbt`, `.scala` files |
96
+ | Elixir | `mix.exs`, `.ex` files |
97
+ | Shell | `.sh`, `.bash` files |
98
+ | Terraform | `.tf`, `.tfvars` files |
99
+
100
+ ---
101
+
102
+ ## Supported Test Frameworks
103
+
104
+ | Framework | Language |
105
+ |-----------|----------|
106
+ | **Jest** | JavaScript / TypeScript |
107
+ | **Vitest** | JavaScript / TypeScript |
108
+ | **Mocha** | JavaScript / TypeScript |
109
+ | **pytest** | Python |
110
+ | **go test** | Go |
111
+ | **cargo test** | Rust |
112
+ | **Maven (mvn test)** | Java / Kotlin |
113
+ | **Gradle (./gradlew test)** | Java / Kotlin |
114
+ | **PHPUnit** | PHP |
115
+ | **RSpec** | Ruby |
116
+ | **Flutter/Dart test** | Dart |
117
+ | **dotnet test** | C# |
118
+
119
+ ---
120
+
121
+ ## Detected Databases & Storage
122
+
123
+ `MongoDB` · `PostgreSQL` · `MySQL` · `SQLite` · `Redis` · `Supabase` · `Prisma` · `Firebase` · `DynamoDB` · `Elasticsearch` · `SQL Server` · `Cassandra`
124
+
125
+ Detection uses: npm dependency names, `.env` key prefixes, and config file presence.
126
+
127
+ ---
128
+
129
+ ## Detected Cloud & API Services
130
+
131
+ `AWS` · `Google Cloud` · `Azure` · `Vercel` · `Netlify` · `Heroku` · `Cloudflare` · `Railway` · `Render` · `ImageKit`
132
+
133
+ ---
134
+
135
+ ## Detected CI/CD Pipelines
136
+
137
+ `GitHub Actions` · `GitLab CI` · `CircleCI` · `Travis CI` · `Jenkins` · `Drone` · `Bitbucket Pipelines` · `Azure Pipelines`
138
+
139
+ ---
140
+
141
+ ## Works on Any Project
142
+
143
+ `project-scan` is **project-agnostic**. It works on:
144
+
145
+ - ✅ Node.js / React / Next.js / Vue / Angular apps
146
+ - ✅ Python / Django / Flask / FastAPI projects
147
+ - ✅ Java / Spring Boot / Gradle / Maven projects
148
+ - ✅ Go / Rust / C++ / C# projects
149
+ - ✅ Ruby on Rails / PHP Laravel apps
150
+ - ✅ Flutter / Dart mobile apps
151
+ - ✅ Monorepos and multi-language projects
152
+ - ✅ Completely empty or new projects (shows what's missing)
153
+
154
+ ---
155
+
156
+ ## CI/CD Integration
157
+
158
+ ```yaml
159
+ # .github/workflows/scan.yml
160
+ - name: Scan project
161
+ run: npx project-scan --tests-only --json > test-results.json
162
+ ```
163
+
164
+ ```bash
165
+ # Use exit code in CI (exits 1 if any test fails)
166
+ npx project-scan --tests-only || exit 1
167
+ ```
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * project-scan — Universal project summarizer & test runner
5
+ *
6
+ * Usage:
7
+ * npx project-scan # scan + run tests + generate all reports
8
+ * npx project-scan --scan-only # scan only, no tests
9
+ * npx project-scan --tests-only # run tests only, no scan output
10
+ * npx project-scan --report # generate reports from last scan
11
+ * npx project-scan --html # also open HTML report in browser
12
+ * npx project-scan --no-html # skip HTML report
13
+ * npx project-scan --out <dir> # write reports to a custom directory
14
+ * npx project-scan --help # show help
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ // ─── Parse flags ─────────────────────────────────────────────────────────────
21
+
22
+ const args = process.argv.slice(2);
23
+ const flags = {
24
+ scanOnly: args.includes('--scan-only'),
25
+ testsOnly: args.includes('--tests-only'),
26
+ html: !args.includes('--no-html'),
27
+ openHtml: args.includes('--html') || args.includes('--open'),
28
+ help: args.includes('--help') || args.includes('-h'),
29
+ quiet: args.includes('--quiet') || args.includes('-q'),
30
+ json: args.includes('--json'),
31
+ outDir: (() => { const i = args.indexOf('--out'); return i !== -1 ? args[i+1] : null; })(),
32
+ };
33
+
34
+ // ─── Colors ───────────────────────────────────────────────────────────────────
35
+
36
+ const isTTY = process.stdout.isTTY;
37
+ const c = {
38
+ reset: isTTY ? '\x1b[0m' : '',
39
+ bold: isTTY ? '\x1b[1m' : '',
40
+ dim: isTTY ? '\x1b[2m' : '',
41
+ green: isTTY ? '\x1b[32m' : '',
42
+ red: isTTY ? '\x1b[31m' : '',
43
+ yellow: isTTY ? '\x1b[33m' : '',
44
+ cyan: isTTY ? '\x1b[36m' : '',
45
+ blue: isTTY ? '\x1b[34m' : '',
46
+ magenta:isTTY ? '\x1b[35m' : '',
47
+ white: isTTY ? '\x1b[97m' : '',
48
+ };
49
+
50
+ function log(msg) { if (!flags.quiet) process.stdout.write(msg + '\n'); }
51
+ function dim(msg) { log(`${c.dim}${msg}${c.reset}`); }
52
+ function step(msg) { log(`\n${c.cyan}${c.bold}▶ ${msg}${c.reset}`); }
53
+ function ok(msg) { log(` ${c.green}✔${c.reset} ${msg}`); }
54
+ function warn(msg) { log(` ${c.yellow}⚠${c.reset} ${msg}`); }
55
+ function fail(msg) { log(` ${c.red}✖${c.reset} ${msg}`); }
56
+ function info(msg) { log(` ${c.blue}ℹ${c.reset} ${msg}`); }
57
+
58
+ // ─── Help ─────────────────────────────────────────────────────────────────────
59
+
60
+ if (flags.help) {
61
+ console.log(`
62
+ ${c.bold}${c.cyan}project-scan${c.reset} — Universal Project Summarizer & Test Runner
63
+
64
+ ${c.bold}USAGE${c.reset}
65
+ npx project-scan [flags]
66
+ project-scan [flags] (if installed globally)
67
+
68
+ ${c.bold}FLAGS${c.reset}
69
+ ${c.green}(default)${c.reset} Scan project + run tests + write all reports
70
+ ${c.green}--scan-only${c.reset} Only scan project, skip running tests
71
+ ${c.green}--tests-only${c.reset} Only run tests, skip scan report
72
+ ${c.green}--html${c.reset} Open HTML report in browser after generation
73
+ ${c.green}--no-html${c.reset} Skip HTML report generation
74
+ ${c.green}--out <dir>${c.reset} Write reports to a custom directory (default: current dir)
75
+ ${c.green}--json${c.reset} Print raw scan+test JSON to stdout
76
+ ${c.green}--quiet, -q${c.reset} Suppress console output
77
+ ${c.green}--help, -h${c.reset} Show this help
78
+
79
+ ${c.bold}OUTPUTS${c.reset}
80
+ SUMMARY.md Quick project overview with test status
81
+ ONBOARDING.md Full onboarding guide with stack + setup commands
82
+ TESTS.md Detailed per-suite test results
83
+ PROJECT_REPORT.html Visual HTML report (open in any browser)
84
+
85
+ ${c.bold}SUPPORTED LANGUAGES${c.reset}
86
+ JavaScript · TypeScript · Python · Java · Kotlin · Go · Rust
87
+ C/C++ · C# · PHP · Ruby · Swift · Dart · Scala · Elixir · R · Shell
88
+
89
+ ${c.bold}SUPPORTED TEST FRAMEWORKS${c.reset}
90
+ Jest · Vitest · Mocha · pytest · Go test · Cargo test
91
+ Maven · Gradle · PHPUnit · RSpec · Flutter/Dart · dotnet test
92
+
93
+ ${c.bold}DETECTED DATABASES${c.reset}
94
+ MongoDB · PostgreSQL · MySQL · SQLite · Redis · Supabase
95
+ Prisma · Firebase · DynamoDB · Elasticsearch · SQL Server · Cassandra
96
+
97
+ ${c.bold}DETECTED CLOUD${c.reset}
98
+ AWS · Google Cloud · Azure · Vercel · Netlify · Heroku
99
+ Cloudflare · Railway · Render · ImageKit
100
+
101
+ ${c.bold}DETECTED CI/CD${c.reset}
102
+ GitHub Actions · GitLab CI · CircleCI · Travis CI
103
+ Jenkins · Drone · Bitbucket Pipelines · Azure Pipelines
104
+
105
+ ${c.bold}EXAMPLES${c.reset}
106
+ cd my-project && npx project-scan
107
+ npx project-scan --scan-only --out ./docs
108
+ npx project-scan --tests-only --json
109
+ `);
110
+ process.exit(0);
111
+ }
112
+
113
+ // ─── Main ─────────────────────────────────────────────────────────────────────
114
+
115
+ async function main() {
116
+ const rootDir = process.cwd();
117
+ const outDir = flags.outDir ? path.resolve(rootDir, flags.outDir) : rootDir;
118
+
119
+ if (flags.outDir) {
120
+ fs.mkdirSync(outDir, { recursive: true });
121
+ }
122
+
123
+ printBanner(rootDir);
124
+
125
+ // ── 1. Scan ───────────────────────────────────────────────────────────────
126
+ step('Scanning project structure, languages, and stack...');
127
+ const { scan } = require(path.join(__dirname, '..', 'lib/scanner'));
128
+ const scanResult = scan(rootDir);
129
+
130
+ if (!flags.testsOnly) {
131
+ printScanSummary(scanResult);
132
+ }
133
+
134
+ // ── 2. Run tests ──────────────────────────────────────────────────────────
135
+ let testResults = [];
136
+ if (!flags.scanOnly) {
137
+ step('Detecting and running test suites...');
138
+ const { runAllTests } = require(path.join(__dirname, '..', 'lib/test-runner'));
139
+ testResults = runAllTests(rootDir);
140
+ printTestResults(testResults);
141
+ }
142
+
143
+ // ── 3. Generate reports ───────────────────────────────────────────────────
144
+ if (!flags.testsOnly || flags.scanOnly === false) {
145
+ step('Writing reports...');
146
+ const { generateSummary, generateOnboarding, generateTestsReport } = require(path.join(__dirname, '..', 'lib/md-report'));
147
+
148
+ const summaryPath = path.join(outDir, 'SUMMARY.md');
149
+ const onboardPath = path.join(outDir, 'ONBOARDING.md');
150
+ const testsPath = path.join(outDir, 'TESTS.md');
151
+
152
+ fs.writeFileSync(summaryPath, generateSummary(scanResult, testResults));
153
+ fs.writeFileSync(onboardPath, generateOnboarding(scanResult));
154
+ fs.writeFileSync(testsPath, generateTestsReport(scanResult, testResults));
155
+
156
+ ok(`SUMMARY.md → ${path.relative(rootDir, summaryPath) || 'SUMMARY.md'}`);
157
+ ok(`ONBOARDING.md → ${path.relative(rootDir, onboardPath) || 'ONBOARDING.md'}`);
158
+ ok(`TESTS.md → ${path.relative(rootDir, testsPath) || 'TESTS.md'}`);
159
+
160
+ if (flags.html) {
161
+ const { generateHTML } = require(path.join(__dirname, '..', 'lib/html-report'));
162
+ const htmlPath = path.join(outDir, 'PROJECT_REPORT.html');
163
+ fs.writeFileSync(htmlPath, generateHTML(scanResult, testResults));
164
+ ok(`PROJECT_REPORT.html → ${path.relative(rootDir, htmlPath) || 'PROJECT_REPORT.html'}`);
165
+
166
+ if (flags.openHtml) {
167
+ openInBrowser(htmlPath);
168
+ }
169
+ }
170
+ }
171
+
172
+ // ── 4. JSON output ─────────────────────────────────────────────────────────
173
+ if (flags.json) {
174
+ process.stdout.write(JSON.stringify({ scan: scanResult, tests: testResults }, null, 2) + '\n');
175
+ }
176
+
177
+ // ── 5. Final status ────────────────────────────────────────────────────────
178
+ const totalFailed = testResults.reduce((s, r) => s + (r.failed || 0), 0);
179
+ const totalPassed = testResults.reduce((s, r) => s + (r.passed || 0), 0);
180
+
181
+ log('');
182
+ log(`${c.bold}${'─'.repeat(50)}${c.reset}`);
183
+ if (!flags.scanOnly && testResults.length > 0) {
184
+ if (totalFailed > 0) {
185
+ log(`${c.red}${c.bold} ✖ ${totalFailed} test(s) failed.${c.reset}`);
186
+ process.exitCode = 1;
187
+ } else if (totalPassed > 0) {
188
+ log(`${c.green}${c.bold} ✔ All ${totalPassed} tests passed.${c.reset}`);
189
+ } else {
190
+ log(`${c.yellow}${c.bold} ○ No tests ran.${c.reset}`);
191
+ }
192
+ }
193
+ log(`${c.dim} Done. Reports written to: ${outDir}${c.reset}`);
194
+ log('');
195
+ }
196
+
197
+ // ─── Pretty-print scan summary ────────────────────────────────────────────────
198
+
199
+ function printScanSummary(r) {
200
+ const box = (label, items, color) => {
201
+ if (!items || items.length === 0) return;
202
+ log(`\n ${c.bold}${color}${label}${c.reset}`);
203
+ items.forEach(i => log(` ${c.dim}·${c.reset} ${i}`));
204
+ };
205
+
206
+ log(`\n ${c.bold}${c.white}Project:${c.reset} ${r.projectName} ${c.dim}(${r.structure.type})${c.reset}`);
207
+ log(` ${c.dim}${r.totalFiles.toLocaleString()} files scanned · ${r.scannedAt}${c.reset}`);
208
+
209
+ box('💻 Languages', r.languages, c.blue);
210
+ box('⚙️ Frameworks', r.frameworks, c.magenta);
211
+ box('🗄️ Databases', r.databases, c.cyan);
212
+ box('☁️ Cloud & APIs', r.cloud, c.green);
213
+ box('🔄 CI/CD Pipelines', r.pipelines, c.yellow);
214
+
215
+ if (r.structure.entryPoints?.length > 0) {
216
+ log(`\n ${c.bold}${c.dim}Entry points:${c.reset}`);
217
+ r.structure.entryPoints.slice(0, 6).forEach(f => log(` ${c.dim}${f}${c.reset}`));
218
+ }
219
+ }
220
+
221
+ // ─── Pretty-print test results ────────────────────────────────────────────────
222
+
223
+ function printTestResults(results) {
224
+ let totalPassed = 0, totalFailed = 0, totalSkipped = 0;
225
+
226
+ for (const r of results) {
227
+ const icon = r.status === 'passed' ? `${c.green}✔` : r.status === 'failed' ? `${c.red}✖` : r.status === 'error' ? `${c.yellow}⚠` : `${c.dim}○`;
228
+ const ms = r.duration ? `${c.dim} (${(r.duration/1000).toFixed(2)}s)${c.reset}` : '';
229
+ log(`\n ${icon}${c.reset} ${c.bold}${r.suite}${c.reset}${ms}`);
230
+
231
+ if (r.passed !== undefined) { log(` ${c.green}✔ ${r.passed} passed${c.reset}`); totalPassed += r.passed; }
232
+ if (r.failed > 0) { log(` ${c.red}✖ ${r.failed} failed${c.reset}`); totalFailed += r.failed; }
233
+ if (r.skipped > 0) { log(` ${c.dim}○ ${r.skipped} skipped${c.reset}`); totalSkipped += r.skipped; }
234
+ if (r.suites) { log(` ${c.dim}${r.suites}${c.reset}`); }
235
+ if (r.status === 'skipped' && r.output) { warn(r.output); }
236
+ if (r.status === 'error' && r.error) { fail(r.error.slice(0, 200)); }
237
+
238
+ // Print first failed test details
239
+ if (r.status === 'failed' && r.output) {
240
+ const lines = r.output.split('\n').filter(l => l.includes('✖') || l.includes('FAIL') || l.includes('Error') || l.includes('expect'));
241
+ if (lines.length > 0) {
242
+ log(` ${c.dim}${c.red}${lines.slice(0, 5).join('\n ')}${c.reset}`);
243
+ }
244
+ }
245
+ }
246
+
247
+ log(`\n ${c.bold}Total: ${c.green}${totalPassed} passed${c.reset} · ${c.red}${totalFailed} failed${c.reset} · ${c.dim}${totalSkipped} skipped${c.reset}`);
248
+ }
249
+
250
+ // ─── Banner ───────────────────────────────────────────────────────────────────
251
+
252
+ function printBanner(rootDir) {
253
+ const name = path.basename(rootDir);
254
+ log('');
255
+ log(`${c.cyan}${c.bold}╔══════════════════════════════════════════════╗${c.reset}`);
256
+ log(`${c.cyan}${c.bold}║ 🔍 project-scan · Universal Analyzer ║${c.reset}`);
257
+ log(`${c.cyan}${c.bold}╚══════════════════════════════════════════════╝${c.reset}`);
258
+ dim(` Scanning: ${rootDir}`);
259
+ }
260
+
261
+ // ─── Open in browser ──────────────────────────────────────────────────────────
262
+
263
+ function openInBrowser(filePath) {
264
+ const { execSync } = require('child_process');
265
+ const url = `file://${filePath}`;
266
+ try {
267
+ const platform = process.platform;
268
+ if (platform === 'darwin') execSync(`open "${filePath}"`, { stdio: 'ignore' });
269
+ else if (platform === 'win32') execSync(`start "" "${filePath}"`,{ stdio: 'ignore', shell: true });
270
+ else execSync(`xdg-open "${filePath}" 2>/dev/null || sensible-browser "${filePath}" 2>/dev/null || true`, { stdio: 'ignore' });
271
+ ok(`Opening ${url} in browser`);
272
+ } catch { warn(`Could not auto-open browser. Open manually: ${url}`); }
273
+ }
274
+
275
+ // ─── Run ─────────────────────────────────────────────────────────────────────
276
+
277
+ main().catch(err => {
278
+ process.stderr.write(`\n${c.red}✖ project-scan error: ${err.message}${c.reset}\n`);
279
+ if (process.env.DEBUG) process.stderr.write(err.stack + '\n');
280
+ process.exit(1);
281
+ });
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+ /**
3
+ * html-report.js — Generates a self-contained HTML report
4
+ * from scanner and test-runner results.
5
+ */
6
+
7
+ function generateHTML(scanResult, testResults) {
8
+ const { projectName, scannedAt, languages, frameworks, databases, cloud, pipelines, structure, packageMeta, totalFiles, envFilesFound } = scanResult;
9
+
10
+ const totalPassed = testResults.reduce((s, r) => s + (r.passed || 0), 0);
11
+ const totalFailed = testResults.reduce((s, r) => s + (r.failed || 0), 0);
12
+ const totalSkipped = testResults.reduce((s, r) => s + (r.skipped || 0), 0);
13
+ const totalTests = totalPassed + totalFailed + totalSkipped;
14
+ const overallPass = testResults.length > 0 && totalFailed === 0 && testResults.some(r => r.status !== 'skipped');
15
+ const overallBadge = testResults.length === 0 ? 'NO TESTS' : (overallPass ? 'ALL PASSED' : (totalFailed > 0 ? `${totalFailed} FAILED` : 'SKIPPED'));
16
+ const badgeColor = overallPass ? '#22c55e' : (totalFailed > 0 ? '#ef4444' : '#94a3b8');
17
+
18
+ const pill = (text, color = '#3b82d4') =>
19
+ `<span style="display:inline-block;padding:2px 10px;border-radius:999px;background:${color}18;color:${color};border:1px solid ${color}40;font-size:12px;font-weight:600;margin:2px 3px 2px 0">${esc(text)}</span>`;
20
+
21
+ const sectionTitle = (t) =>
22
+ `<h2 style="font-size:15px;font-weight:700;margin:28px 0 10px;padding-bottom:6px;border-bottom:1px solid #e5e7eb;color:#1f2328">${t}</h2>`;
23
+
24
+ const card = (content, extra = '') =>
25
+ `<div style="background:#f7f8fa;border:1px solid #e5e7eb;border-radius:8px;padding:16px 18px;margin-bottom:10px${extra}">${content}</div>`;
26
+
27
+ // Test suite rows
28
+ const testRows = testResults.map(r => {
29
+ const icon = r.status === 'passed' ? '✔' : r.status === 'failed' ? '✖' : r.status === 'error' ? '⚠' : '○';
30
+ const color = r.status === 'passed' ? '#22c55e' : r.status === 'failed' ? '#ef4444' : r.status === 'error' ? '#f59e0b' : '#94a3b8';
31
+ const ms = r.duration ? `${(r.duration / 1000).toFixed(2)}s` : '—';
32
+ const output = r.output ? `<pre style="margin:8px 0 0;padding:10px;background:#1e1e2e;color:#cdd6f4;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap;max-height:220px;overflow-y:auto">${esc(r.output.slice(-2500))}</pre>` : '';
33
+ const errOut = r.error ? `<pre style="margin:8px 0 0;padding:10px;background:#2a0a0a;color:#fca5a5;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap">${esc(r.error)}</pre>` : '';
34
+ const suitesMeta = r.suites ? `<span style="margin-left:8px;color:#57606a;font-size:12px">(${r.suites})</span>` : '';
35
+ return `
36
+ <div style="border:1px solid #e5e7eb;border-radius:7px;padding:14px 16px;margin-bottom:8px;background:#fff">
37
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px">
38
+ <span style="font-weight:600;color:#1f2328;font-size:14px">
39
+ <span style="color:${color};margin-right:6px;font-size:16px">${icon}</span>${esc(r.suite)}${suitesMeta}
40
+ </span>
41
+ <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
42
+ ${r.passed !== undefined ? `<span style="color:#22c55e;font-size:12px;font-weight:700">${r.passed} passed</span>` : ''}
43
+ ${r.failed !== undefined && r.failed > 0 ? `<span style="color:#ef4444;font-size:12px;font-weight:700">${r.failed} failed</span>` : ''}
44
+ ${r.skipped !== undefined && r.skipped > 0 ? `<span style="color:#94a3b8;font-size:12px;font-weight:700">${r.skipped} skipped</span>` : ''}
45
+ <span style="color:#57606a;font-size:12px">${ms}</span>
46
+ </div>
47
+ </div>
48
+ ${output}${errOut}
49
+ </div>`;
50
+ }).join('');
51
+
52
+ // Structure tree
53
+ const structureLines = structure.parts.map(p => `<li style="margin:2px 0"><code>/${p}</code></li>`).join('');
54
+ const entryPoints = (structure.entryPoints || []).slice(0, 12).map(f => `<li style="margin:2px 0;color:#57606a;font-size:12px"><code>${esc(f)}</code></li>`).join('');
55
+ const testFilesList = (structure.testFiles || []).slice(0, 20).map(f => `<li style="margin:2px 0;color:#57606a;font-size:12px"><code>${esc(f)}</code></li>`).join('');
56
+
57
+ const progressBar = totalTests > 0 ? `
58
+ <div style="background:#e5e7eb;border-radius:999px;height:10px;margin:10px 0;overflow:hidden">
59
+ <div style="background:#22c55e;height:100%;width:${Math.round((totalPassed/totalTests)*100)}%;display:inline-block;border-radius:999px 0 0 999px"></div>
60
+ </div>
61
+ <div style="display:flex;gap:16px;font-size:12px;color:#57606a">
62
+ <span>✔ <b style="color:#22c55e">${totalPassed}</b> passed</span>
63
+ <span>✖ <b style="color:${totalFailed>0?'#ef4444':'#94a3b8'}">${totalFailed}</b> failed</span>
64
+ <span>○ <b>${totalSkipped}</b> skipped</span>
65
+ <span style="margin-left:auto">${totalTests} total assertions</span>
66
+ </div>` : '';
67
+
68
+ const envSection = envFilesFound.length > 0 ? `
69
+ ${sectionTitle('🔒 Environment Files Found')}
70
+ ${card(envFilesFound.map(f => `<code style="margin-right:8px">${esc(f)}</code>`).join(''))}` : '';
71
+
72
+ const pkgSection = packageMeta ? `
73
+ ${sectionTitle('📦 Package Metadata')}
74
+ ${card(`
75
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
76
+ ${packageMeta.name ? `<div><span style="color:#57606a;font-size:12px">Name</span><br><b>${esc(packageMeta.name)}</b></div>` : ''}
77
+ ${packageMeta.version ? `<div><span style="color:#57606a;font-size:12px">Version</span><br><b>${esc(packageMeta.version)}</b></div>` : ''}
78
+ ${packageMeta.description ? `<div style="grid-column:1/-1"><span style="color:#57606a;font-size:12px">Description</span><br>${esc(packageMeta.description)}</div>` : ''}
79
+ ${packageMeta.scripts && Object.keys(packageMeta.scripts).length > 0 ? `<div style="grid-column:1/-1"><span style="color:#57606a;font-size:12px">Scripts</span><br>${Object.keys(packageMeta.scripts).map(s => `<code style="margin-right:6px">${esc(s)}</code>`).join('')}</div>` : ''}
80
+ </div>
81
+ `)}` : '';
82
+
83
+ return `<!DOCTYPE html>
84
+ <html lang="en">
85
+ <head>
86
+ <meta charset="UTF-8">
87
+ <meta name="viewport" content="width=device-width,initial-scale=1">
88
+ <title>Project Scan — ${esc(projectName)}</title>
89
+ <style>
90
+ *{box-sizing:border-box}
91
+ body{font-family:-apple-system,"Segoe UI",system-ui,sans-serif;font-size:14px;line-height:1.6;background:#fff;color:#1f2328;margin:0;padding:24px 16px}
92
+ .wrap{max-width:820px;margin:0 auto}
93
+ code{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;font-size:12px;background:#f0f3f6;padding:1px 5px;border-radius:3px}
94
+ ul{margin:6px 0;padding-left:20px} li{margin:1px 0}
95
+ @media(max-width:600px){.grid2{grid-template-columns:1fr!important}}
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="wrap">
100
+
101
+ <!-- Header -->
102
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:24px;padding-bottom:18px;border-bottom:2px solid #e5e7eb">
103
+ <div>
104
+ <h1 style="margin:0;font-size:22px;font-weight:800;color:#1f2328">🔍 ${esc(projectName)}</h1>
105
+ <p style="margin:4px 0 0;color:#57606a;font-size:12px">Universal Project Scan &nbsp;·&nbsp; ${new Date(scannedAt).toLocaleString()} &nbsp;·&nbsp; ${totalFiles.toLocaleString()} files scanned</p>
106
+ </div>
107
+ <div style="text-align:right">
108
+ <div style="display:inline-block;padding:6px 16px;border-radius:8px;background:${badgeColor}18;border:1.5px solid ${badgeColor};color:${badgeColor};font-weight:800;font-size:14px;letter-spacing:.5px">${overallBadge}</div>
109
+ <div style="margin-top:4px;color:#57606a;font-size:11px">Test Status</div>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Project type -->
114
+ <div style="margin-bottom:20px">
115
+ <span style="font-size:13px;color:#57606a">Project type: </span>
116
+ <b style="color:#3b82d4">${esc(structure.type)}</b>
117
+ </div>
118
+
119
+ <!-- Stack overview grid -->
120
+ <div class="grid2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:4px">
121
+
122
+ ${sectionTitle('💻 Languages')}
123
+ <div></div>
124
+ ${languages.length > 0 ? `<div style="grid-column:1/-1">${languages.map(l => pill(l, '#3b82d4')).join('')}</div>` : `<div style="grid-column:1/-1;color:#94a3b8">None detected</div>`}
125
+
126
+ ${sectionTitle('⚙️ Frameworks & Libraries')}
127
+ <div></div>
128
+ ${frameworks.length > 0 ? `<div style="grid-column:1/-1">${frameworks.map(f => pill(f, '#7c5cd8')).join('')}</div>` : `<div style="grid-column:1/-1;color:#94a3b8">None detected</div>`}
129
+
130
+ ${sectionTitle('🗄️ Databases & Storage')}
131
+ <div></div>
132
+ ${databases.length > 0 ? `<div style="grid-column:1/-1">${databases.map(d => pill(d, '#0891b2')).join('')}</div>` : `<div style="grid-column:1/-1;color:#94a3b8">None detected</div>`}
133
+
134
+ ${sectionTitle('☁️ Cloud & API Services')}
135
+ <div></div>
136
+ ${cloud.length > 0 ? `<div style="grid-column:1/-1">${cloud.map(c => pill(c, '#059669')).join('')}</div>` : `<div style="grid-column:1/-1;color:#94a3b8">None detected</div>`}
137
+
138
+ ${sectionTitle('🔄 CI/CD Pipelines')}
139
+ <div></div>
140
+ ${pipelines.length > 0 ? `<div style="grid-column:1/-1">${pipelines.map(p => pill(p, '#d97706')).join('')}</div>` : `<div style="grid-column:1/-1;color:#94a3b8">None detected</div>`}
141
+
142
+ </div>
143
+
144
+ <!-- Structure -->
145
+ ${sectionTitle('📁 Project Structure')}
146
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
147
+ ${card(`<b style="font-size:12px;color:#57606a;display:block;margin-bottom:6px">TOP-LEVEL DIRECTORIES</b><ul>${structureLines || '<li style="color:#94a3b8">None</li>'}</ul>`)}
148
+ ${card(`<b style="font-size:12px;color:#57606a;display:block;margin-bottom:6px">ENTRY POINTS</b><ul>${entryPoints || '<li style="color:#94a3b8">None found</li>'}</ul>`)}
149
+ ${card(`<b style="font-size:12px;color:#57606a;display:block;margin-bottom:6px">TEST FILES (${(structure.testFiles||[]).length})</b><ul>${testFilesList || '<li style="color:#94a3b8">None found</li>'}</ul>`)}
150
+ </div>
151
+
152
+ ${pkgSection}
153
+ ${envSection}
154
+
155
+ <!-- Test Results -->
156
+ ${sectionTitle('🧪 Test Results')}
157
+ ${progressBar}
158
+ <div style="margin-top:12px">${testRows || `<div style="color:#94a3b8;padding:16px 0">No test results available.</div>`}</div>
159
+
160
+ <!-- Footer -->
161
+ <div style="margin-top:40px;padding-top:12px;border-top:1px solid #e5e7eb;text-align:center;color:#94a3b8;font-size:11px">
162
+ Generated by <b>project-scan</b> &nbsp;·&nbsp; <a href="https://github.com" style="color:#3b82d4">npm i -g project-scan</a>
163
+ </div>
164
+
165
+ </div>
166
+ </body>
167
+ </html>`;
168
+ }
169
+
170
+ function esc(str) {
171
+ return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
172
+ }
173
+
174
+ module.exports = { generateHTML };