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,447 @@
1
+ 'use strict';
2
+ /**
3
+ * test-runner.js — Universal test runner
4
+ * Detects and executes test frameworks across all major languages.
5
+ * Returns structured results: suite name, status, passed/failed/skipped counts, output.
6
+ */
7
+
8
+ const { execSync, spawnSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ // ─── Per-language runner configs ─────────────────────────────────────────────
13
+
14
+ /**
15
+ * Each runner: { name, detect(rootDir), run(rootDir) → RunResult }
16
+ * RunResult: { suite, passed, failed, skipped, duration, status, output, error? }
17
+ */
18
+ const RUNNERS = [
19
+
20
+ // ── Node.js: Jest ──────────────────────────────────────────────────────────
21
+ {
22
+ name: 'Jest',
23
+ detect(root) {
24
+ const pkg = tryReadJSON(path.join(root, 'package.json'));
25
+ if (pkg && (allDeps(pkg).includes('jest') || pkg.jest)) return true;
26
+ // subdirectory package.jsons
27
+ return findSubPackages(root).some(p => {
28
+ const sp = tryReadJSON(p);
29
+ return sp && (allDeps(sp).includes('jest') || sp.jest || sp.scripts?.test?.includes('jest'));
30
+ });
31
+ },
32
+ run(root) {
33
+ const targets = [root, ...findSubDirs(root)].filter(dir => {
34
+ const pkg = tryReadJSON(path.join(dir, 'package.json'));
35
+ return pkg && (allDeps(pkg).includes('jest') || pkg.jest || pkg.scripts?.test?.includes('jest'));
36
+ });
37
+ const results = [];
38
+ for (const dir of targets) {
39
+ const label = dir === root ? '(root)' : path.relative(root, dir);
40
+ results.push(runJest(dir, label));
41
+ }
42
+ return results;
43
+ }
44
+ },
45
+
46
+ // ── Node.js: Vitest ────────────────────────────────────────────────────────
47
+ {
48
+ name: 'Vitest',
49
+ detect(root) {
50
+ const pkg = tryReadJSON(path.join(root, 'package.json'));
51
+ if (pkg && allDeps(pkg).includes('vitest')) return true;
52
+ return findSubPackages(root).some(p => {
53
+ const sp = tryReadJSON(p);
54
+ return sp && allDeps(sp).includes('vitest');
55
+ });
56
+ },
57
+ run(root) {
58
+ const targets = [root, ...findSubDirs(root)].filter(dir => {
59
+ const pkg = tryReadJSON(path.join(dir, 'package.json'));
60
+ return pkg && allDeps(pkg).includes('vitest');
61
+ });
62
+ return targets.map(dir => runCommand({
63
+ dir, label: dir === root ? '(root)' : path.relative(root, dir),
64
+ cmd: hasBin(dir, 'vitest') ? 'npx vitest run --reporter=verbose' : null,
65
+ suite: 'Vitest',
66
+ parseOutput: parseVitestOutput,
67
+ }));
68
+ }
69
+ },
70
+
71
+ // ── Node.js: Mocha ─────────────────────────────────────────────────────────
72
+ {
73
+ name: 'Mocha',
74
+ detect(root) {
75
+ const pkg = tryReadJSON(path.join(root, 'package.json'));
76
+ if (pkg && allDeps(pkg).includes('mocha')) return true;
77
+ return findSubPackages(root).some(p => {
78
+ const sp = tryReadJSON(p);
79
+ return sp && allDeps(sp).includes('mocha');
80
+ });
81
+ },
82
+ run(root) {
83
+ return [runCommand({
84
+ dir: root, label: '(root)',
85
+ cmd: 'npx mocha --reporter spec',
86
+ suite: 'Mocha',
87
+ parseOutput: parseMochaOutput,
88
+ })];
89
+ }
90
+ },
91
+
92
+ // ── Python: pytest ─────────────────────────────────────────────────────────
93
+ {
94
+ name: 'pytest',
95
+ detect(root) {
96
+ return fs.existsSync(path.join(root, 'requirements.txt')) ||
97
+ fs.existsSync(path.join(root, 'pyproject.toml')) ||
98
+ fs.existsSync(path.join(root, 'pytest.ini')) ||
99
+ fs.existsSync(path.join(root, 'setup.py')) ||
100
+ hasSourceExt(root, '.py');
101
+ },
102
+ run(root) {
103
+ const cmd = commandExists('pytest') ? 'pytest -v --tb=short 2>&1'
104
+ : commandExists('python3') ? 'python3 -m pytest -v --tb=short 2>&1'
105
+ : commandExists('python') ? 'python -m pytest -v --tb=short 2>&1'
106
+ : null;
107
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'pytest', parseOutput: parsePytestOutput })];
108
+ }
109
+ },
110
+
111
+ // ── Go: go test ────────────────────────────────────────────────────────────
112
+ {
113
+ name: 'Go test',
114
+ detect(root) { return fs.existsSync(path.join(root, 'go.mod')); },
115
+ run(root) {
116
+ return [runCommand({
117
+ dir: root, label: '(root)',
118
+ cmd: commandExists('go') ? 'go test ./... -v 2>&1' : null,
119
+ suite: 'Go test',
120
+ parseOutput: parseGoTestOutput,
121
+ })];
122
+ }
123
+ },
124
+
125
+ // ── Rust: cargo test ───────────────────────────────────────────────────────
126
+ {
127
+ name: 'Cargo test',
128
+ detect(root) { return fs.existsSync(path.join(root, 'Cargo.toml')); },
129
+ run(root) {
130
+ return [runCommand({
131
+ dir: root, label: '(root)',
132
+ cmd: commandExists('cargo') ? 'cargo test 2>&1' : null,
133
+ suite: 'Cargo test',
134
+ parseOutput: parseCargoOutput,
135
+ })];
136
+ }
137
+ },
138
+
139
+ // ── Java/Kotlin: Maven ─────────────────────────────────────────────────────
140
+ {
141
+ name: 'Maven',
142
+ detect(root) { return fs.existsSync(path.join(root, 'pom.xml')); },
143
+ run(root) {
144
+ const cmd = commandExists('mvn') ? 'mvn test -B 2>&1' : null;
145
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Maven', parseOutput: parseMavenOutput })];
146
+ }
147
+ },
148
+
149
+ // ── Java/Kotlin: Gradle ────────────────────────────────────────────────────
150
+ {
151
+ name: 'Gradle',
152
+ detect(root) {
153
+ return fs.existsSync(path.join(root, 'build.gradle')) ||
154
+ fs.existsSync(path.join(root, 'build.gradle.kts'));
155
+ },
156
+ run(root) {
157
+ const gradleCmd = fs.existsSync(path.join(root, 'gradlew')) ? './gradlew' : (commandExists('gradle') ? 'gradle' : null);
158
+ return [runCommand({ dir: root, label: '(root)', cmd: gradleCmd ? `${gradleCmd} test 2>&1` : null, suite: 'Gradle', parseOutput: parseGradleOutput })];
159
+ }
160
+ },
161
+
162
+ // ── PHP: PHPUnit ───────────────────────────────────────────────────────────
163
+ {
164
+ name: 'PHPUnit',
165
+ detect(root) {
166
+ return fs.existsSync(path.join(root, 'phpunit.xml')) ||
167
+ fs.existsSync(path.join(root, 'phpunit.xml.dist')) ||
168
+ fs.existsSync(path.join(root, 'vendor/bin/phpunit')) ||
169
+ hasSourceExt(root, '.php');
170
+ },
171
+ run(root) {
172
+ const cmd = fs.existsSync(path.join(root, 'vendor/bin/phpunit')) ? './vendor/bin/phpunit --testdox 2>&1'
173
+ : commandExists('phpunit') ? 'phpunit --testdox 2>&1' : null;
174
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'PHPUnit', parseOutput: parsePHPUnitOutput })];
175
+ }
176
+ },
177
+
178
+ // ── Ruby: RSpec ────────────────────────────────────────────────────────────
179
+ {
180
+ name: 'RSpec',
181
+ detect(root) {
182
+ return fs.existsSync(path.join(root, 'Gemfile')) ||
183
+ fs.existsSync(path.join(root, '.rspec')) ||
184
+ fs.existsSync(path.join(root, 'spec'));
185
+ },
186
+ run(root) {
187
+ const cmd = commandExists('rspec') ? 'rspec --format progress 2>&1' : (commandExists('bundle') ? 'bundle exec rspec --format progress 2>&1' : null);
188
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'RSpec', parseOutput: parseRSpecOutput })];
189
+ }
190
+ },
191
+
192
+ // ── Dart/Flutter ───────────────────────────────────────────────────────────
193
+ {
194
+ name: 'Flutter/Dart test',
195
+ detect(root) { return fs.existsSync(path.join(root, 'pubspec.yaml')); },
196
+ run(root) {
197
+ const cmd = commandExists('flutter') ? 'flutter test 2>&1' : (commandExists('dart') ? 'dart test 2>&1' : null);
198
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Flutter/Dart', parseOutput: parseDartOutput })];
199
+ }
200
+ },
201
+
202
+ // ── .NET: dotnet test ──────────────────────────────────────────────────────
203
+ {
204
+ name: 'dotnet test',
205
+ detect(root) {
206
+ try {
207
+ return fs.readdirSync(root).some(f => f.endsWith('.csproj') || f.endsWith('.sln'));
208
+ } catch { return false; }
209
+ },
210
+ run(root) {
211
+ const cmd = commandExists('dotnet') ? 'dotnet test --logger "console;verbosity=normal" 2>&1' : null;
212
+ return [runCommand({ dir: root, label: '(root)', cmd, suite: 'dotnet test', parseOutput: parseDotnetOutput })];
213
+ }
214
+ },
215
+ ];
216
+
217
+ // ─── Core runner ─────────────────────────────────────────────────────────────
218
+
219
+ function runAllTests(rootDir) {
220
+ const results = [];
221
+ const activeRunners = RUNNERS.filter(r => r.detect(rootDir));
222
+
223
+ if (activeRunners.length === 0) {
224
+ return [{
225
+ suite: 'No test framework detected',
226
+ status: 'skipped',
227
+ passed: 0, failed: 0, skipped: 0,
228
+ output: 'No recognized test framework was found in this project.',
229
+ duration: 0,
230
+ }];
231
+ }
232
+
233
+ for (const runner of activeRunners) {
234
+ try {
235
+ const suiteResults = runner.run(rootDir);
236
+ results.push(...suiteResults);
237
+ } catch (err) {
238
+ results.push({
239
+ suite: runner.name,
240
+ status: 'error',
241
+ passed: 0, failed: 0, skipped: 0,
242
+ output: '',
243
+ error: err.message,
244
+ duration: 0,
245
+ });
246
+ }
247
+ }
248
+ return results;
249
+ }
250
+
251
+ // ─── Jest runner (special: uses JSON reporter) ────────────────────────────────
252
+
253
+ function runJest(dir, label) {
254
+ const start = Date.now();
255
+ if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest')) &&
256
+ !fs.existsSync(path.join(dir, 'node_modules', 'jest'))) {
257
+ // Try npm test fallback
258
+ const pkg = tryReadJSON(path.join(dir, 'package.json'));
259
+ if (!pkg?.scripts?.test) {
260
+ return { suite: `Jest (${label})`, status: 'skipped', passed: 0, failed: 0, skipped: 0, output: 'Jest not installed. Run npm install first.', duration: 0 };
261
+ }
262
+ }
263
+
264
+ const result = spawnSync('npx', ['jest', '--no-coverage', '--json', '--forceExit'], {
265
+ cwd: dir, encoding: 'utf8', timeout: 120000,
266
+ env: { ...process.env, FORCE_COLOR: '0' }
267
+ });
268
+
269
+ const duration = Date.now() - start;
270
+ const rawOutput = (result.stdout || '') + (result.stderr || '');
271
+
272
+ // Try to parse JSON output
273
+ const jsonStart = rawOutput.indexOf('{');
274
+ const jsonEnd = rawOutput.lastIndexOf('}');
275
+ if (jsonStart !== -1 && jsonEnd !== -1) {
276
+ try {
277
+ const jResult = JSON.parse(rawOutput.slice(jsonStart, jsonEnd + 1));
278
+ const passed = jResult.numPassedTests || 0;
279
+ const failed = jResult.numFailedTests || 0;
280
+ const skipped = jResult.numPendingTests || 0;
281
+ const status = failed > 0 ? 'failed' : (passed > 0 ? 'passed' : 'skipped');
282
+
283
+ // Build readable test breakdown
284
+ const lines = [];
285
+ for (const suite of (jResult.testResults || [])) {
286
+ const relPath = path.relative(dir, suite.testFilePath);
287
+ lines.push(`\n 📄 ${relPath}`);
288
+ for (const t of (suite.testResults || [])) {
289
+ const icon = t.status === 'passed' ? ' ✔' : t.status === 'failed' ? ' ✖' : ' ○';
290
+ lines.push(`${icon} ${t.fullName}`);
291
+ if (t.status === 'failed' && t.failureMessages?.length) {
292
+ lines.push(` ${t.failureMessages[0].split('\n').slice(0, 3).join('\n ')}`);
293
+ }
294
+ }
295
+ }
296
+
297
+ return {
298
+ suite: `Jest (${label})`,
299
+ status,
300
+ passed, failed, skipped,
301
+ duration,
302
+ output: lines.join('\n'),
303
+ suites: jResult.numPassedTestSuites + '/' + jResult.numTotalTestSuites + ' suites passed',
304
+ };
305
+ } catch { /* fall through to text parse */ }
306
+ }
307
+
308
+ return parseJestTextOutput(rawOutput, label, duration);
309
+ }
310
+
311
+ // ─── Generic command runner ───────────────────────────────────────────────────
312
+
313
+ function runCommand({ dir, label, cmd, suite, parseOutput }) {
314
+ if (!cmd) {
315
+ return { suite: `${suite} (${label})`, status: 'skipped', passed: 0, failed: 0, skipped: 0, output: `${suite} not installed or not found in PATH.`, duration: 0 };
316
+ }
317
+ const start = Date.now();
318
+ try {
319
+ const output = execSync(cmd, { cwd: dir, encoding: 'utf8', timeout: 120000, env: { ...process.env, FORCE_COLOR: '0' }, stdio: 'pipe' });
320
+ const duration = Date.now() - start;
321
+ return { suite: `${suite} (${label})`, duration, ...parseOutput(output, true) };
322
+ } catch (err) {
323
+ const duration = Date.now() - start;
324
+ const output = (err.stdout || '') + (err.stderr || '') + (err.message || '');
325
+ return { suite: `${suite} (${label})`, duration, ...parseOutput(output, false) };
326
+ }
327
+ }
328
+
329
+ // ─── Output parsers ───────────────────────────────────────────────────────────
330
+
331
+ function parseJestTextOutput(output, label, duration) {
332
+ const passMatch = output.match(/(\d+)\s+passed/);
333
+ const failMatch = output.match(/(\d+)\s+failed/);
334
+ const skipMatch = output.match(/(\d+)\s+(?:skipped|pending)/);
335
+ const passed = passMatch ? parseInt(passMatch[1]) : 0;
336
+ const failed = failMatch ? parseInt(failMatch[1]) : 0;
337
+ const skipped = skipMatch ? parseInt(skipMatch[1]) : 0;
338
+ return { suite: `Jest (${label})`, status: failed > 0 ? 'failed' : (passed > 0 ? 'passed' : 'unknown'), passed, failed, skipped, duration, output: output.slice(-3000) };
339
+ }
340
+
341
+ function parsePytestOutput(output, exitOk) {
342
+ const m = output.match(/(\d+)\s+passed(?:,\s*(\d+)\s+failed)?(?:,\s*(\d+)\s+(?:warning|skipped))?/);
343
+ const failed = output.match(/(\d+)\s+failed/)?.[1];
344
+ const passed = m?.[1] || '0';
345
+ const skipped = output.match(/(\d+)\s+(?:skipped|deselected)/)?.[1] || '0';
346
+ const status = parseInt(failed || '0') > 0 ? 'failed' : (exitOk ? 'passed' : 'failed');
347
+ return { status, passed: parseInt(passed), failed: parseInt(failed || '0'), skipped: parseInt(skipped), output: output.slice(-3000) };
348
+ }
349
+
350
+ function parseGoTestOutput(output, exitOk) {
351
+ const passLines = (output.match(/--- PASS:/g) || []).length;
352
+ const failLines = (output.match(/--- FAIL:/g) || []).length;
353
+ const skipLines = (output.match(/--- SKIP:/g) || []).length;
354
+ return { status: failLines > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed: passLines, failed: failLines, skipped: skipLines, output: output.slice(-3000) };
355
+ }
356
+
357
+ function parseCargoOutput(output, exitOk) {
358
+ const m = output.match(/test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
359
+ return { status: m && parseInt(m[2]) > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed: parseInt(m?.[1] || '0'), failed: parseInt(m?.[2] || '0'), skipped: parseInt(m?.[3] || '0'), output: output.slice(-3000) };
360
+ }
361
+
362
+ function parseMavenOutput(output, exitOk) {
363
+ const m = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
364
+ const failed = m ? parseInt(m[2]) + parseInt(m[3]) : 0;
365
+ const passed = m ? parseInt(m[1]) - failed - parseInt(m[4]) : 0;
366
+ const skipped = m ? parseInt(m[4]) : 0;
367
+ return { status: failed > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed, failed, skipped, output: output.slice(-3000) };
368
+ }
369
+
370
+ function parseGradleOutput(output, exitOk) {
371
+ const m = output.match(/(\d+)\s+tests?\s+completed(?:,\s*(\d+)\s+failed)?/);
372
+ const failed = m?.[2] ? parseInt(m[2]) : 0;
373
+ const total = m ? parseInt(m[1]) : 0;
374
+ return { status: failed > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed: total - failed, failed, skipped: 0, output: output.slice(-3000) };
375
+ }
376
+
377
+ function parseMochaOutput(output, exitOk) {
378
+ const passMatch = output.match(/(\d+)\s+passing/);
379
+ const failMatch = output.match(/(\d+)\s+failing/);
380
+ const pending = output.match(/(\d+)\s+pending/);
381
+ const passed = passMatch ? parseInt(passMatch[1]) : 0;
382
+ const failed = failMatch ? parseInt(failMatch[1]) : 0;
383
+ const skipped = pending ? parseInt(pending[1]) : 0;
384
+ return { status: failed > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed, failed, skipped, output: output.slice(-3000) };
385
+ }
386
+
387
+ function parseVitestOutput(output, exitOk) {
388
+ const passMatch = output.match(/(\d+)\s+(?:test[s]?\s+)?passed/i);
389
+ const failMatch = output.match(/(\d+)\s+(?:test[s]?\s+)?failed/i);
390
+ const passed = passMatch ? parseInt(passMatch[1]) : 0;
391
+ const failed = failMatch ? parseInt(failMatch[1]) : 0;
392
+ return { status: failed > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed, failed, skipped: 0, output: output.slice(-3000) };
393
+ }
394
+
395
+ function parsePHPUnitOutput(output, exitOk) {
396
+ const m = output.match(/OK\s+\((\d+)\s+tests?/);
397
+ const f = output.match(/FAILURES!\s*\nTests:\s*(\d+),.*?Failures:\s*(\d+)/s);
398
+ if (f) return { status: 'failed', passed: parseInt(f[1]) - parseInt(f[2]), failed: parseInt(f[2]), skipped: 0, output: output.slice(-3000) };
399
+ return { status: exitOk ? 'passed' : 'failed', passed: m ? parseInt(m[1]) : 0, failed: 0, skipped: 0, output: output.slice(-3000) };
400
+ }
401
+
402
+ function parseRSpecOutput(output, exitOk) {
403
+ const m = output.match(/(\d+)\s+examples?,\s*(\d+)\s+failures?/);
404
+ return { status: m && parseInt(m[2]) > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed: m ? parseInt(m[1]) - parseInt(m[2]) : 0, failed: m ? parseInt(m[2]) : 0, skipped: 0, output: output.slice(-3000) };
405
+ }
406
+
407
+ function parseDartOutput(output, exitOk) {
408
+ const passMatch = output.match(/(\d+) tests? passed/);
409
+ const failMatch = output.match(/(\d+) tests? failed/);
410
+ const passed = passMatch ? parseInt(passMatch[1]) : 0;
411
+ const failed = failMatch ? parseInt(failMatch[1]) : 0;
412
+ return { status: failed > 0 ? 'failed' : (exitOk ? 'passed' : 'failed'), passed, failed, skipped: 0, output: output.slice(-3000) };
413
+ }
414
+
415
+ function parseDotnetOutput(output, exitOk) {
416
+ const m = output.match(/Passed!\s+-\s+Failed:\s*(\d+),\s*Passed:\s*(\d+),\s*Skipped:\s*(\d+)/);
417
+ if (m) return { status: parseInt(m[1]) > 0 ? 'failed' : 'passed', passed: parseInt(m[2]), failed: parseInt(m[1]), skipped: parseInt(m[3]), output: output.slice(-3000) };
418
+ const f = output.match(/Failed:\s*(\d+)/); const p = output.match(/Passed:\s*(\d+)/);
419
+ return { status: exitOk ? 'passed' : 'failed', passed: p ? parseInt(p[1]) : 0, failed: f ? parseInt(f[1]) : 0, skipped: 0, output: output.slice(-3000) };
420
+ }
421
+
422
+ // ─── Utility helpers ──────────────────────────────────────────────────────────
423
+
424
+ function tryReadJSON(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
425
+ function allDeps(pkg) { return [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; }
426
+ function commandExists(cmd) { try { execSync(`which ${cmd} 2>/dev/null || command -v ${cmd} 2>/dev/null`, { encoding: 'utf8' }).trim(); return true; } catch { return false; } }
427
+ function hasBin(dir, bin) { return fs.existsSync(path.join(dir, 'node_modules', '.bin', bin)); }
428
+ function hasSourceExt(root, ext) {
429
+ try { return fs.readdirSync(root).some(f => f.endsWith(ext)); } catch { return false; }
430
+ }
431
+ function findSubPackages(root) {
432
+ try {
433
+ return fs.readdirSync(root, { withFileTypes: true })
434
+ .filter(e => e.isDirectory() && !['node_modules', '.git', 'dist', 'build'].includes(e.name))
435
+ .map(e => path.join(root, e.name, 'package.json'))
436
+ .filter(p => fs.existsSync(p));
437
+ } catch { return []; }
438
+ }
439
+ function findSubDirs(root) {
440
+ try {
441
+ return fs.readdirSync(root, { withFileTypes: true })
442
+ .filter(e => e.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'venv', '.venv'].includes(e.name))
443
+ .map(e => path.join(root, e.name));
444
+ } catch { return []; }
445
+ }
446
+
447
+ module.exports = { runAllTests };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "project-scan",
3
+ "version": "1.0.0",
4
+ "description": "Universal project summarizer & test runner — scans any project for languages, frameworks, databases, cloud services, CI/CD pipelines and runs tests. Works with JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, PHP, Ruby, Dart, C#, and more.",
5
+ "main": "lib/scanner.js",
6
+ "bin": {
7
+ "project-scan": "bin/project-scan.js"
8
+ },
9
+ "scripts": {
10
+ "scan": "node bin/project-scan.js",
11
+ "start": "node bin/project-scan.js",
12
+ "test": "node bin/project-scan.js --tests-only"
13
+ },
14
+ "keywords": [
15
+ "project-scan",
16
+ "project-summarizer",
17
+ "test-runner",
18
+ "documentation",
19
+ "code-analysis",
20
+ "devtools",
21
+ "cli",
22
+ "universal"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ },
29
+ "preferGlobal": true,
30
+ "files": [
31
+ "bin/",
32
+ "lib/",
33
+ "README.md"
34
+ ]
35
+ }