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/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 · ${new Date(scannedAt).toLocaleString()} · ${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> · <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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { generateHTML };
|