projscan 0.1.11 → 0.2.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 +141 -32
- package/dist/cli/index.js +108 -144
- package/dist/cli/index.js.map +1 -1
- package/dist/core/fileInspector.d.ts +13 -0
- package/dist/core/fileInspector.js +205 -0
- package/dist/core/fileInspector.js.map +1 -0
- package/dist/core/hotspotAnalyzer.d.ts +16 -0
- package/dist/core/hotspotAnalyzer.js +342 -0
- package/dist/core/hotspotAnalyzer.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/prompts.d.ts +14 -0
- package/dist/mcp/prompts.js +126 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +8 -0
- package/dist/mcp/resources.js +57 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +205 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +176 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/reporters/consoleReporter.d.ts +3 -1
- package/dist/reporters/consoleReporter.js +127 -0
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/jsonReporter.d.ts +3 -1
- package/dist/reporters/jsonReporter.js +6 -0
- package/dist/reporters/jsonReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.d.ts +3 -1
- package/dist/reporters/markdownReporter.js +99 -0
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/types.d.ts +88 -0
- package/dist/utils/baseline.d.ts +4 -4
- package/dist/utils/baseline.js +71 -5
- package/dist/utils/baseline.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
**Instant codebase insights — doctor, x-ray, and architecture map for any repository.**
|
|
10
10
|
|
|
11
|
-
[Install](#install) · [Quick Start](#quick-start) · [Commands](#commands) · [Full Guide](docs/GUIDE.md)
|
|
11
|
+
[Install](#install) · [Quick Start](#quick-start) · [Commands](#commands) · [Full Guide](docs/GUIDE.md) · [Roadmap](docs/ROADMAP.md)
|
|
12
|
+
|
|
13
|
+
<img src="docs/hero.png" alt="projscan banner" width="600">
|
|
12
14
|
|
|
13
15
|
</div>
|
|
14
16
|
|
|
@@ -28,35 +30,19 @@ Answering these manually takes 10-30 minutes of poking through config files and
|
|
|
28
30
|
**projscan answers all of this in one command, in under 2 seconds.**
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Project Health Report
|
|
34
|
-
──────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
Health Score: C (67/100)
|
|
37
|
-
Found 3 warnings, 2 info
|
|
38
|
-
|
|
39
|
-
Issues Detected
|
|
40
|
-
──────────────────────────────────────────
|
|
41
|
-
⚠ No ESLint configuration
|
|
42
|
-
⚠ No Prettier configuration
|
|
43
|
-
⚠ No test framework detected
|
|
44
|
-
ℹ Missing .editorconfig
|
|
45
|
-
ℹ README is nearly empty
|
|
46
|
-
|
|
47
|
-
Run projscan fix to auto-fix 4 issues.
|
|
33
|
+
npx projscan
|
|
48
34
|
```
|
|
49
35
|
|
|
50
|
-
|
|
36
|
+
<img src="docs/npx%20projscan.png" alt="npx projscan" width="700">
|
|
37
|
+
|
|
38
|
+
Run `projscan doctor` for a focused health check:
|
|
51
39
|
|
|
52
40
|
```bash
|
|
53
|
-
|
|
54
|
-
✔ Installed ESLint with TypeScript support
|
|
55
|
-
✔ Installed Prettier with sensible defaults
|
|
56
|
-
✔ Installed Vitest with sample test
|
|
57
|
-
✔ Created .editorconfig
|
|
41
|
+
npx projscan doctor
|
|
58
42
|
```
|
|
59
43
|
|
|
44
|
+
<img src="docs/npx%20projscan%20doctor.png" alt="npx projscan doctor" width="700">
|
|
45
|
+
|
|
60
46
|
## Install
|
|
61
47
|
|
|
62
48
|
```bash
|
|
@@ -74,15 +60,20 @@ npx projscan
|
|
|
74
60
|
Run inside any repository:
|
|
75
61
|
|
|
76
62
|
```bash
|
|
77
|
-
projscan
|
|
78
|
-
projscan doctor
|
|
79
|
-
projscan
|
|
80
|
-
projscan
|
|
81
|
-
projscan
|
|
82
|
-
projscan
|
|
83
|
-
projscan
|
|
63
|
+
projscan # Full project analysis
|
|
64
|
+
projscan doctor # Health check
|
|
65
|
+
projscan hotspots # Rank files by risk (churn × complexity × issues × ownership)
|
|
66
|
+
projscan file <path> # Drill into a file — purpose, risk, ownership, issues
|
|
67
|
+
projscan fix # Auto-fix detected issues
|
|
68
|
+
projscan ci # CI health gate (exits 1 on low score)
|
|
69
|
+
projscan diff # Compare health + hotspot trends against a baseline
|
|
70
|
+
projscan diagram # Architecture visualization
|
|
71
|
+
projscan structure # Directory tree
|
|
72
|
+
projscan mcp # Run as an MCP server for AI coding agents
|
|
84
73
|
```
|
|
85
74
|
|
|
75
|
+
<img src="docs/npx%20projscan%20--help.png" alt="npx projscan --help" width="700">
|
|
76
|
+
|
|
86
77
|
For a comprehensive walkthrough, see the **[Full Guide](docs/GUIDE.md)**.
|
|
87
78
|
|
|
88
79
|
## Commands
|
|
@@ -91,14 +82,17 @@ For a comprehensive walkthrough, see the **[Full Guide](docs/GUIDE.md)**.
|
|
|
91
82
|
|---------|-------------|
|
|
92
83
|
| `projscan analyze` | Full analysis — languages, frameworks, dependencies, issues |
|
|
93
84
|
| `projscan doctor` | Health check — missing tooling, architecture smells, security risks |
|
|
85
|
+
| `projscan hotspots` | Rank files by risk — churn × complexity × issues × ownership |
|
|
86
|
+
| `projscan file <path>` | Drill into a file — purpose, risk, ownership, related issues |
|
|
94
87
|
| `projscan fix` | Auto-fix issues (ESLint, Prettier, Vitest, .editorconfig) |
|
|
95
88
|
| `projscan ci` | CI pipeline health gate — exits 1 if score below threshold |
|
|
96
|
-
| `projscan diff` | Compare current health against a
|
|
89
|
+
| `projscan diff` | Compare current health **and hotspot trends** against a baseline |
|
|
97
90
|
| `projscan explain <file>` | Explain a file's purpose, imports, exports, and issues |
|
|
98
91
|
| `projscan diagram` | ASCII architecture diagram of your project |
|
|
99
92
|
| `projscan structure` | Directory tree with file counts |
|
|
100
93
|
| `projscan dependencies` | Dependency analysis — counts, risks, recommendations |
|
|
101
94
|
| `projscan badge` | Generate a health score badge for your README |
|
|
95
|
+
| `projscan mcp` | Run as an MCP server for AI coding agents (Claude Code, Cursor, …) |
|
|
102
96
|
|
|
103
97
|
To see all commands and options, run:
|
|
104
98
|
|
|
@@ -106,6 +100,38 @@ To see all commands and options, run:
|
|
|
106
100
|
projscan --help
|
|
107
101
|
```
|
|
108
102
|
|
|
103
|
+
### Command Screenshots
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>projscan structure</strong> — Directory tree with file counts</summary>
|
|
107
|
+
|
|
108
|
+
<img src="docs/npx%20projscan%20structure.png" alt="npx projscan structure" width="700">
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
<details>
|
|
112
|
+
<summary><strong>projscan diagram</strong> — Architecture visualization</summary>
|
|
113
|
+
|
|
114
|
+
<img src="docs/npx%20projscan%20diagram.png" alt="npx projscan diagram" width="700">
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
<summary><strong>projscan dependencies</strong> — Dependency analysis</summary>
|
|
119
|
+
|
|
120
|
+
<img src="docs/npx%20projscan%20dependencies.png" alt="npx projscan dependencies" width="700">
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary><strong>projscan explain</strong> — File explanation</summary>
|
|
125
|
+
|
|
126
|
+
<img src="docs/npx%20projscan%20explain.png" alt="npx projscan explain" width="700">
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><strong>projscan badge</strong> — Health badge generation</summary>
|
|
131
|
+
|
|
132
|
+
<img src="docs/npx%20projscan%20badge.png" alt="npx projscan badge" width="700">
|
|
133
|
+
</details>
|
|
134
|
+
|
|
109
135
|
### Output Formats
|
|
110
136
|
|
|
111
137
|
All commands support `--format` for different output targets:
|
|
@@ -147,6 +173,8 @@ projscan badge
|
|
|
147
173
|
|
|
148
174
|
This outputs a [shields.io](https://shields.io) badge URL and markdown snippet you can paste into your README.
|
|
149
175
|
|
|
176
|
+
**Sample badge:** [](https://github.com/abhiyoheswaran1/projscan)
|
|
177
|
+
|
|
150
178
|
## What It Detects
|
|
151
179
|
|
|
152
180
|
**Languages**: TypeScript, JavaScript, Python, Go, Rust, Java, Ruby, C/C++, PHP, Swift, Kotlin, and 20+ more
|
|
@@ -180,6 +208,8 @@ projscan ci --min-score 70 # Exits 1 if score < 70
|
|
|
180
208
|
projscan ci --min-score 80 --format json # JSON output for parsing
|
|
181
209
|
```
|
|
182
210
|
|
|
211
|
+
<img src="docs/npx%20projscan%20ci%20--min-score%2070.png" alt="npx projscan ci --min-score 70" width="700">
|
|
212
|
+
|
|
183
213
|
### GitHub Actions
|
|
184
214
|
|
|
185
215
|
Copy the included workflow template to your project:
|
|
@@ -201,10 +231,89 @@ projscan diff # Compare against baseline
|
|
|
201
231
|
projscan diff --format markdown # Markdown diff for PRs
|
|
202
232
|
```
|
|
203
233
|
|
|
234
|
+
<img src="docs/npx%20projscan%20diff%20--save-baseline.png" alt="npx projscan diff --save-baseline" width="700">
|
|
235
|
+
|
|
236
|
+
## Hotspots — Where to Fix First
|
|
237
|
+
|
|
238
|
+
A flat health score doesn't tell you what to do. **`projscan hotspots`** combines `git log` churn, file complexity, open issues, recency, and **ownership** into a single risk score per file — so you know where refactoring or review will actually pay off.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
projscan hotspots # Top 10 hotspots
|
|
242
|
+
projscan hotspots --limit 20
|
|
243
|
+
projscan hotspots --since "6 months ago"
|
|
244
|
+
projscan hotspots --format json # Machine-readable for dashboards
|
|
245
|
+
projscan hotspots --format markdown # Drop into a PR or tech-debt ticket
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Hotspot ranking follows the classic Feathers "churn × complexity" heuristic with boosts for files that fail `projscan doctor`, changed recently, or show **bus factor 1** (single-author + high churn). Falls back gracefully outside a git repo.
|
|
249
|
+
|
|
250
|
+
### Drill Into a Hotspot
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
projscan file src/cli/index.ts
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Combines the file's purpose, imports, exports, hotspot risk, ownership, and every open issue that references it — the natural follow-up to `projscan hotspots`.
|
|
257
|
+
|
|
258
|
+
### Track Trends Over Time
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
projscan diff --save-baseline # Snapshots health + hotspots
|
|
262
|
+
# ...time passes, commits happen...
|
|
263
|
+
projscan diff # Shows which hotspots rose / fell
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The baseline file now captures top hotspots too, so `diff` surfaces files that are **getting worse** (not just new issues).
|
|
267
|
+
|
|
268
|
+
## AI Agent Integration (MCP)
|
|
269
|
+
|
|
270
|
+
**`projscan mcp`** starts an [MCP](https://modelcontextprotocol.io) server over stdio so AI coding agents can query projscan during a session.
|
|
271
|
+
|
|
272
|
+
**Tools** (7):
|
|
273
|
+
- `projscan_analyze` — full project report
|
|
274
|
+
- `projscan_doctor` — health score + issues
|
|
275
|
+
- `projscan_hotspots` — risk-ranked files (with `limit`, `since` args)
|
|
276
|
+
- `projscan_file` — per-file risk + ownership + related issues
|
|
277
|
+
- `projscan_explain` — per-file purpose, imports, exports, smells
|
|
278
|
+
- `projscan_structure` — directory tree
|
|
279
|
+
- `projscan_dependencies` — package audit
|
|
280
|
+
|
|
281
|
+
**Prompts** (2, parameterized with live project data):
|
|
282
|
+
- `prioritize_refactoring` — ranked plan grounded in current hotspots
|
|
283
|
+
- `investigate_file` — senior-engineer brief for a specific file
|
|
284
|
+
|
|
285
|
+
**Resources** (3, readable on demand):
|
|
286
|
+
- `projscan://health` · `projscan://hotspots` · `projscan://structure`
|
|
287
|
+
|
|
288
|
+
### Claude Code
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
claude mcp add projscan -- npx projscan mcp
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Cursor / Windsurf / any MCP client
|
|
295
|
+
|
|
296
|
+
Add to your MCP config:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"mcpServers": {
|
|
301
|
+
"projscan": {
|
|
302
|
+
"command": "npx",
|
|
303
|
+
"args": ["projscan", "mcp"]
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Now your agent can ask *"what are the riskiest files in this repo?"* and get a grounded answer, or run `projscan_doctor` before proposing an edit.
|
|
310
|
+
|
|
204
311
|
## Use Cases
|
|
205
312
|
|
|
206
313
|
- **Onboarding**: Understand any codebase in seconds, not hours
|
|
207
314
|
- **Code reviews**: Run `projscan doctor --format markdown` and paste into PRs
|
|
315
|
+
- **Tech-debt prioritization**: Use `projscan hotspots` to decide what deserves refactoring time
|
|
316
|
+
- **AI-assisted development**: Mount `projscan mcp` in your agent of choice for grounded edits
|
|
208
317
|
- **CI/CD**: Use `projscan ci` to enforce health standards in your pipeline
|
|
209
318
|
- **Security**: Catch committed secrets and `.env` files before they reach production
|
|
210
319
|
- **Consulting**: Quickly assess client projects before diving in
|
package/dist/cli/index.js
CHANGED
|
@@ -14,14 +14,17 @@ import { detectLanguages } from '../core/languageDetector.js';
|
|
|
14
14
|
import { detectFrameworks } from '../core/frameworkDetector.js';
|
|
15
15
|
import { analyzeDependencies } from '../core/dependencyAnalyzer.js';
|
|
16
16
|
import { collectIssues } from '../core/issueEngine.js';
|
|
17
|
+
import { analyzeHotspots } from '../core/hotspotAnalyzer.js';
|
|
18
|
+
import { inspectFile, extractImports, extractExports, inferPurpose, detectFileIssues, } from '../core/fileInspector.js';
|
|
17
19
|
import { getAllAvailableFixes } from '../fixes/fixRegistry.js';
|
|
18
20
|
import { setLogLevel } from '../utils/logger.js';
|
|
19
21
|
import { calculateScore, badgeUrl, badgeMarkdown } from '../utils/scoreCalculator.js';
|
|
20
22
|
import { showBanner, showCompactBanner, showHelp } from '../utils/banner.js';
|
|
21
23
|
import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
24
|
+
import { runMcpServer } from '../mcp/server.js';
|
|
25
|
+
import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, } from '../reporters/consoleReporter.js';
|
|
26
|
+
import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, } from '../reporters/jsonReporter.js';
|
|
27
|
+
import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, } from '../reporters/markdownReporter.js';
|
|
25
28
|
// ── CLI Setup ─────────────────────────────────────────────
|
|
26
29
|
const program = new Command();
|
|
27
30
|
program
|
|
@@ -205,12 +208,19 @@ program
|
|
|
205
208
|
try {
|
|
206
209
|
const scan = await scanRepository(rootPath);
|
|
207
210
|
const issues = await collectIssues(rootPath, scan.files);
|
|
211
|
+
const hotspotReport = await analyzeHotspots(rootPath, scan.files, issues, { limit: 20 });
|
|
208
212
|
if (cmdOpts.saveBaseline) {
|
|
209
|
-
const filePath = await saveBaseline(rootPath, issues);
|
|
213
|
+
const filePath = await saveBaseline(rootPath, issues, hotspotReport);
|
|
210
214
|
const { score, grade } = calculateScore(issues);
|
|
211
215
|
console.log(chalk.green(`\n Baseline saved to ${filePath}`));
|
|
212
216
|
console.log(` Score: ${chalk.bold(`${grade} (${score}/100)`)}`);
|
|
213
|
-
console.log(` Issues: ${issues.length}
|
|
217
|
+
console.log(` Issues: ${issues.length}`);
|
|
218
|
+
if (hotspotReport.available) {
|
|
219
|
+
console.log(` Hotspots snapshotted: ${hotspotReport.hotspots.length}\n`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
214
224
|
return;
|
|
215
225
|
}
|
|
216
226
|
let baseline;
|
|
@@ -222,7 +232,7 @@ program
|
|
|
222
232
|
console.error(` Run ${chalk.bold.cyan('projscan diff --save-baseline')} first to create one.\n`);
|
|
223
233
|
process.exit(1);
|
|
224
234
|
}
|
|
225
|
-
const diff = computeDiff(baseline, issues);
|
|
235
|
+
const diff = computeDiff(baseline, issues, hotspotReport);
|
|
226
236
|
switch (format) {
|
|
227
237
|
case 'json':
|
|
228
238
|
reportDiffJson(diff);
|
|
@@ -299,6 +309,42 @@ program
|
|
|
299
309
|
process.exit(1);
|
|
300
310
|
}
|
|
301
311
|
});
|
|
312
|
+
// ── Command: file ─────────────────────────────────────────
|
|
313
|
+
program
|
|
314
|
+
.command('file <file>')
|
|
315
|
+
.description('Drill into a file — purpose, risk, ownership, related issues')
|
|
316
|
+
.action(async (filePath) => {
|
|
317
|
+
setupLogLevel();
|
|
318
|
+
maybeCompactBanner();
|
|
319
|
+
const rootPath = getRootPath();
|
|
320
|
+
const format = getFormat();
|
|
321
|
+
const spinner = format === 'console' ? ora('Inspecting file...').start() : null;
|
|
322
|
+
try {
|
|
323
|
+
const inspection = await inspectFile(rootPath, filePath);
|
|
324
|
+
if (spinner)
|
|
325
|
+
spinner.stop();
|
|
326
|
+
if (!inspection.exists) {
|
|
327
|
+
console.error(chalk.red(`\n ${inspection.reason ?? 'File unavailable'}: ${filePath}\n`));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
switch (format) {
|
|
331
|
+
case 'json':
|
|
332
|
+
reportFileJson(inspection);
|
|
333
|
+
break;
|
|
334
|
+
case 'markdown':
|
|
335
|
+
reportFileMarkdown(inspection);
|
|
336
|
+
break;
|
|
337
|
+
default:
|
|
338
|
+
reportFileInspection(inspection);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (spinner)
|
|
343
|
+
spinner.fail('File inspection failed');
|
|
344
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
302
348
|
// ── Command: explain ──────────────────────────────────────
|
|
303
349
|
program
|
|
304
350
|
.command('explain <file>')
|
|
@@ -434,6 +480,61 @@ program
|
|
|
434
480
|
process.exit(1);
|
|
435
481
|
}
|
|
436
482
|
});
|
|
483
|
+
// ── Command: hotspots ─────────────────────────────────────
|
|
484
|
+
program
|
|
485
|
+
.command('hotspots')
|
|
486
|
+
.description('Rank files by risk (git churn × complexity × open issues)')
|
|
487
|
+
.option('--limit <n>', 'number of hotspots to show', '10')
|
|
488
|
+
.option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")', '12 months ago')
|
|
489
|
+
.action(async (cmdOpts) => {
|
|
490
|
+
setupLogLevel();
|
|
491
|
+
maybeCompactBanner();
|
|
492
|
+
const rootPath = getRootPath();
|
|
493
|
+
const format = getFormat();
|
|
494
|
+
const spinner = format === 'console' ? ora('Analyzing hotspots...').start() : null;
|
|
495
|
+
try {
|
|
496
|
+
const scan = await scanRepository(rootPath);
|
|
497
|
+
const issues = await collectIssues(rootPath, scan.files);
|
|
498
|
+
const limit = Math.max(1, Math.min(100, parseInt(cmdOpts.limit, 10) || 10));
|
|
499
|
+
const report = await analyzeHotspots(rootPath, scan.files, issues, {
|
|
500
|
+
since: cmdOpts.since,
|
|
501
|
+
limit,
|
|
502
|
+
});
|
|
503
|
+
if (spinner)
|
|
504
|
+
spinner.stop();
|
|
505
|
+
switch (format) {
|
|
506
|
+
case 'json':
|
|
507
|
+
reportHotspotsJson(report);
|
|
508
|
+
break;
|
|
509
|
+
case 'markdown':
|
|
510
|
+
reportHotspotsMarkdown(report);
|
|
511
|
+
break;
|
|
512
|
+
default:
|
|
513
|
+
reportHotspots(report);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (spinner)
|
|
518
|
+
spinner.fail('Hotspot analysis failed');
|
|
519
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
// ── Command: mcp ──────────────────────────────────────────
|
|
524
|
+
program
|
|
525
|
+
.command('mcp')
|
|
526
|
+
.description('Run projscan as an MCP server (stdio) for AI coding agents')
|
|
527
|
+
.action(async () => {
|
|
528
|
+
setLogLevel('quiet');
|
|
529
|
+
const rootPath = getRootPath();
|
|
530
|
+
try {
|
|
531
|
+
await runMcpServer(rootPath);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
437
538
|
// ── Command: badge ────────────────────────────────────────
|
|
438
539
|
program
|
|
439
540
|
.command('badge')
|
|
@@ -473,7 +574,7 @@ function analyzeFile(filePath, content) {
|
|
|
473
574
|
const lines = content.split('\n');
|
|
474
575
|
const imports = extractImports(content);
|
|
475
576
|
const exports = extractExports(content);
|
|
476
|
-
const purpose = inferPurpose(filePath,
|
|
577
|
+
const purpose = inferPurpose(filePath, exports);
|
|
477
578
|
const potentialIssues = detectFileIssues(content, lines.length);
|
|
478
579
|
return {
|
|
479
580
|
filePath: path.relative(process.cwd(), filePath),
|
|
@@ -484,143 +585,6 @@ function analyzeFile(filePath, content) {
|
|
|
484
585
|
lineCount: lines.length,
|
|
485
586
|
};
|
|
486
587
|
}
|
|
487
|
-
function extractImports(content) {
|
|
488
|
-
const imports = [];
|
|
489
|
-
const seen = new Set();
|
|
490
|
-
// ES import
|
|
491
|
-
const esImportRegex = /import\s+(?:(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
492
|
-
let match;
|
|
493
|
-
while ((match = esImportRegex.exec(content)) !== null) {
|
|
494
|
-
const source = match[1];
|
|
495
|
-
if (!seen.has(source)) {
|
|
496
|
-
seen.add(source);
|
|
497
|
-
imports.push({
|
|
498
|
-
source,
|
|
499
|
-
specifiers: [],
|
|
500
|
-
isRelative: source.startsWith('.') || source.startsWith('/'),
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// CommonJS require
|
|
505
|
-
const requireRegex = /(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/gm;
|
|
506
|
-
while ((match = requireRegex.exec(content)) !== null) {
|
|
507
|
-
const source = match[1];
|
|
508
|
-
if (!seen.has(source)) {
|
|
509
|
-
seen.add(source);
|
|
510
|
-
imports.push({
|
|
511
|
-
source,
|
|
512
|
-
specifiers: [],
|
|
513
|
-
isRelative: source.startsWith('.') || source.startsWith('/'),
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return imports;
|
|
518
|
-
}
|
|
519
|
-
function extractExports(content) {
|
|
520
|
-
const exports = [];
|
|
521
|
-
// export function
|
|
522
|
-
const funcRegex = /^export\s+(?:async\s+)?function\s+(\w+)/gm;
|
|
523
|
-
let match;
|
|
524
|
-
while ((match = funcRegex.exec(content)) !== null) {
|
|
525
|
-
exports.push({ name: match[1], type: 'function' });
|
|
526
|
-
}
|
|
527
|
-
// export class
|
|
528
|
-
const classRegex = /^export\s+class\s+(\w+)/gm;
|
|
529
|
-
while ((match = classRegex.exec(content)) !== null) {
|
|
530
|
-
exports.push({ name: match[1], type: 'class' });
|
|
531
|
-
}
|
|
532
|
-
// export const/let/var
|
|
533
|
-
const varRegex = /^export\s+(?:const|let|var)\s+(\w+)/gm;
|
|
534
|
-
while ((match = varRegex.exec(content)) !== null) {
|
|
535
|
-
exports.push({ name: match[1], type: 'variable' });
|
|
536
|
-
}
|
|
537
|
-
// export interface
|
|
538
|
-
const interfaceRegex = /^export\s+interface\s+(\w+)/gm;
|
|
539
|
-
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
540
|
-
exports.push({ name: match[1], type: 'interface' });
|
|
541
|
-
}
|
|
542
|
-
// export type
|
|
543
|
-
const typeRegex = /^export\s+type\s+(\w+)/gm;
|
|
544
|
-
while ((match = typeRegex.exec(content)) !== null) {
|
|
545
|
-
exports.push({ name: match[1], type: 'type' });
|
|
546
|
-
}
|
|
547
|
-
// export default
|
|
548
|
-
if (/^export\s+default/m.test(content)) {
|
|
549
|
-
exports.push({ name: 'default', type: 'default' });
|
|
550
|
-
}
|
|
551
|
-
return exports;
|
|
552
|
-
}
|
|
553
|
-
function inferPurpose(filePath, imports, exports) {
|
|
554
|
-
const name = path.basename(filePath, path.extname(filePath)).toLowerCase();
|
|
555
|
-
const dir = path.dirname(filePath).toLowerCase();
|
|
556
|
-
if (name.includes('test') || name.includes('spec'))
|
|
557
|
-
return 'Test file';
|
|
558
|
-
if (name.includes('config') || name.includes('rc'))
|
|
559
|
-
return 'Configuration file';
|
|
560
|
-
if (name === 'index')
|
|
561
|
-
return 'Module entry point / barrel file';
|
|
562
|
-
if (name === 'main' || name === 'app')
|
|
563
|
-
return 'Application entry point';
|
|
564
|
-
if (name.includes('route') || name.includes('router'))
|
|
565
|
-
return 'Route definitions';
|
|
566
|
-
if (name.includes('middleware'))
|
|
567
|
-
return 'Middleware handler';
|
|
568
|
-
if (name.includes('controller'))
|
|
569
|
-
return 'Request controller';
|
|
570
|
-
if (name.includes('service'))
|
|
571
|
-
return 'Service layer logic';
|
|
572
|
-
if (name.includes('model') || name.includes('schema'))
|
|
573
|
-
return 'Data model / schema definition';
|
|
574
|
-
if (name.includes('util') || name.includes('helper'))
|
|
575
|
-
return 'Utility functions';
|
|
576
|
-
if (name.includes('hook'))
|
|
577
|
-
return 'Custom hook';
|
|
578
|
-
if (name.includes('context') || name.includes('provider'))
|
|
579
|
-
return 'Context / state provider';
|
|
580
|
-
if (name.includes('type') || name.includes('interface'))
|
|
581
|
-
return 'Type definitions';
|
|
582
|
-
if (name.includes('constant') || name.includes('config'))
|
|
583
|
-
return 'Constants / configuration';
|
|
584
|
-
if (name.includes('migration'))
|
|
585
|
-
return 'Database migration';
|
|
586
|
-
if (name.includes('seed'))
|
|
587
|
-
return 'Database seed data';
|
|
588
|
-
if (name.includes('auth'))
|
|
589
|
-
return 'Authentication logic';
|
|
590
|
-
if (name.includes('api'))
|
|
591
|
-
return 'API endpoint handler';
|
|
592
|
-
if (dir.includes('component') || dir.includes('pages'))
|
|
593
|
-
return 'UI component';
|
|
594
|
-
if (dir.includes('service'))
|
|
595
|
-
return 'Service module';
|
|
596
|
-
if (dir.includes('model'))
|
|
597
|
-
return 'Data model';
|
|
598
|
-
if (dir.includes('util') || dir.includes('lib'))
|
|
599
|
-
return 'Library / utility module';
|
|
600
|
-
const exportTypes = exports.map((e) => e.type);
|
|
601
|
-
if (exportTypes.includes('class'))
|
|
602
|
-
return 'Class-based module';
|
|
603
|
-
if (exportTypes.filter((t) => t === 'function').length > 2)
|
|
604
|
-
return 'Function library';
|
|
605
|
-
return 'Source module';
|
|
606
|
-
}
|
|
607
|
-
function detectFileIssues(content, lineCount) {
|
|
608
|
-
const issues = [];
|
|
609
|
-
if (lineCount > 500)
|
|
610
|
-
issues.push(`Large file (${lineCount} lines) — consider splitting`);
|
|
611
|
-
if (lineCount > 1000)
|
|
612
|
-
issues.push('Very large file — strongly consider refactoring');
|
|
613
|
-
if (/console\.(log|warn|error|debug)\s*\(/.test(content)) {
|
|
614
|
-
issues.push('Contains console.log statements — consider using a proper logger');
|
|
615
|
-
}
|
|
616
|
-
if (/TODO|FIXME|HACK|XXX/i.test(content)) {
|
|
617
|
-
issues.push('Contains TODO/FIXME comments');
|
|
618
|
-
}
|
|
619
|
-
if (/any\b/.test(content) && /\.tsx?$/.test(content)) {
|
|
620
|
-
issues.push('Uses "any" type — consider using proper types');
|
|
621
|
-
}
|
|
622
|
-
return issues;
|
|
623
|
-
}
|
|
624
588
|
// ── Architecture Layer Detection ──────────────────────────
|
|
625
589
|
function buildArchitectureLayers(files, frameworkNames) {
|
|
626
590
|
const layers = [];
|