@vibecheckai/cli 3.4.0 → 3.5.1
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/bin/registry.js +154 -338
- package/bin/runners/context/generators/mcp.js +13 -15
- package/bin/runners/context/proof-context.js +1 -248
- package/bin/runners/lib/analysis-core.js +180 -198
- package/bin/runners/lib/analyzers.js +223 -1669
- package/bin/runners/lib/cli-output.js +210 -242
- package/bin/runners/lib/detectors-v2.js +785 -547
- package/bin/runners/lib/entitlements-v2.js +458 -96
- package/bin/runners/lib/error-handler.js +9 -16
- package/bin/runners/lib/global-flags.js +0 -37
- package/bin/runners/lib/route-truth.js +322 -1167
- package/bin/runners/lib/scan-output.js +469 -448
- package/bin/runners/lib/ship-output.js +27 -280
- package/bin/runners/lib/terminal-ui.js +733 -231
- package/bin/runners/lib/truth.js +321 -1004
- package/bin/runners/lib/unified-output.js +158 -162
- package/bin/runners/lib/upsell.js +204 -104
- package/bin/runners/runAllowlist.js +324 -0
- package/bin/runners/runAuth.js +95 -324
- package/bin/runners/runCheckpoint.js +21 -39
- package/bin/runners/runContext.js +24 -136
- package/bin/runners/runDoctor.js +67 -115
- package/bin/runners/runEvidencePack.js +219 -0
- package/bin/runners/runFix.js +5 -6
- package/bin/runners/runGuard.js +118 -212
- package/bin/runners/runInit.js +2 -14
- package/bin/runners/runInstall.js +281 -0
- package/bin/runners/runLabs.js +341 -0
- package/bin/runners/runMcp.js +52 -130
- package/bin/runners/runPolish.js +20 -43
- package/bin/runners/runProve.js +3 -13
- package/bin/runners/runReality.js +0 -14
- package/bin/runners/runReport.js +2 -3
- package/bin/runners/runScan.js +44 -511
- package/bin/runners/runShip.js +14 -28
- package/bin/runners/runValidate.js +2 -19
- package/bin/runners/runWatch.js +54 -118
- package/bin/vibecheck.js +41 -148
- package/mcp-server/ARCHITECTURE.md +339 -0
- package/mcp-server/__tests__/cache.test.ts +313 -0
- package/mcp-server/__tests__/executor.test.ts +239 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
- package/mcp-server/__tests__/ids.test.ts +345 -0
- package/mcp-server/__tests__/integration/tools.test.ts +410 -0
- package/mcp-server/__tests__/registry.test.ts +365 -0
- package/mcp-server/__tests__/sandbox.test.ts +323 -0
- package/mcp-server/__tests__/schemas.test.ts +372 -0
- package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
- package/mcp-server/examples/doctor.request.json +14 -0
- package/mcp-server/examples/doctor.response.json +53 -0
- package/mcp-server/examples/error.response.json +15 -0
- package/mcp-server/examples/scan.request.json +14 -0
- package/mcp-server/examples/scan.response.json +108 -0
- package/mcp-server/handlers/tool-handler.ts +671 -0
- package/mcp-server/index-v3.ts +293 -0
- package/mcp-server/index.js +1072 -1573
- package/mcp-server/index.old.js +4137 -0
- package/mcp-server/lib/cache.ts +341 -0
- package/mcp-server/lib/errors.ts +346 -0
- package/mcp-server/lib/executor.ts +792 -0
- package/mcp-server/lib/ids.ts +238 -0
- package/mcp-server/lib/logger.ts +368 -0
- package/mcp-server/lib/metrics.ts +365 -0
- package/mcp-server/lib/sandbox.ts +337 -0
- package/mcp-server/lib/validator.ts +229 -0
- package/mcp-server/package-lock.json +165 -0
- package/mcp-server/package.json +32 -7
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/registry/tools.json +476 -0
- package/mcp-server/schemas/error-envelope.schema.json +125 -0
- package/mcp-server/schemas/finding.schema.json +167 -0
- package/mcp-server/schemas/report-artifact.schema.json +88 -0
- package/mcp-server/schemas/run-request.schema.json +75 -0
- package/mcp-server/schemas/verdict.schema.json +168 -0
- package/mcp-server/tier-auth.d.ts +71 -0
- package/mcp-server/tier-auth.js +371 -183
- package/mcp-server/truth-context.js +90 -131
- package/mcp-server/truth-firewall-tools.js +1000 -1611
- package/mcp-server/tsconfig.json +34 -0
- package/mcp-server/vibecheck-tools.js +2 -2
- package/mcp-server/vitest.config.ts +16 -0
- package/package.json +3 -4
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
- package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
- package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
- package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
- package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
- package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
- package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
- package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
- package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
- package/bin/runners/lib/agent-firewall/logger.js +0 -141
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
- package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
- package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
- package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
- package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
- package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
- package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
- package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
- package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
- package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
- package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
- package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
- package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
- package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
- package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
- package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
- package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
- package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
- package/bin/runners/lib/api-client.js +0 -269
- package/bin/runners/lib/authority-badge.js +0 -425
- package/bin/runners/lib/engines/accessibility-engine.js +0 -190
- package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
- package/bin/runners/lib/engines/ast-cache.js +0 -99
- package/bin/runners/lib/engines/code-quality-engine.js +0 -255
- package/bin/runners/lib/engines/console-logs-engine.js +0 -115
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
- package/bin/runners/lib/engines/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
- package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
- package/bin/runners/lib/engines/file-filter.js +0 -131
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
- package/bin/runners/lib/engines/mock-data-engine.js +0 -272
- package/bin/runners/lib/engines/parallel-processor.js +0 -71
- package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
- package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
- package/bin/runners/lib/engines/type-aware-engine.js +0 -152
- package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
- package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
- package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
- package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
- package/bin/runners/lib/exit-codes.js +0 -275
- package/bin/runners/lib/fingerprint.js +0 -377
- package/bin/runners/lib/help-formatter.js +0 -413
- package/bin/runners/lib/logger.js +0 -38
- package/bin/runners/lib/ship-output-enterprise.js +0 -239
- package/bin/runners/lib/unified-cli-output.js +0 -604
- package/bin/runners/runAgent.d.ts +0 -5
- package/bin/runners/runAgent.js +0 -161
- package/bin/runners/runApprove.js +0 -1200
- package/bin/runners/runClassify.js +0 -859
- package/bin/runners/runContext.d.ts +0 -4
- package/bin/runners/runFirewall.d.ts +0 -5
- package/bin/runners/runFirewall.js +0 -134
- package/bin/runners/runFirewallHook.d.ts +0 -5
- package/bin/runners/runFirewallHook.js +0 -56
- package/bin/runners/runPolish.d.ts +0 -4
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runTruth.d.ts +0 -5
- package/bin/runners/runTruth.js +0 -101
- package/mcp-server/HARDENING_SUMMARY.md +0 -299
- package/mcp-server/agent-firewall-interceptor.js +0 -500
- package/mcp-server/authority-tools.js +0 -569
- package/mcp-server/conductor/conflict-resolver.js +0 -588
- package/mcp-server/conductor/execution-planner.js +0 -544
- package/mcp-server/conductor/index.js +0 -377
- package/mcp-server/conductor/lock-manager.js +0 -615
- package/mcp-server/conductor/request-queue.js +0 -550
- package/mcp-server/conductor/session-manager.js +0 -500
- package/mcp-server/conductor/tools.js +0 -510
- package/mcp-server/lib/api-client.cjs +0 -13
- package/mcp-server/lib/logger.cjs +0 -30
- package/mcp-server/logger.js +0 -173
- package/mcp-server/tools-v3.js +0 -706
- package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
|
@@ -1,1155 +1,322 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Route Truth v1 - JavaScript Runtime
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Next.js App
|
|
6
|
-
* - Fastify
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Route Truth v1 - JavaScript Runtime
|
|
3
|
+
*
|
|
4
|
+
* Generates a normalized route map with evidence from:
|
|
5
|
+
* - Next.js (App Router + Pages Router)
|
|
6
|
+
* - Fastify (shorthand + .route() + register prefixes)
|
|
7
|
+
*
|
|
8
|
+
* Then implements validate_claim(route_exists) on top of it.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const fs = require(
|
|
12
|
-
const path = require(
|
|
13
|
-
const crypto = require(
|
|
14
|
-
|
|
15
|
-
let fg = null;
|
|
16
|
-
try {
|
|
17
|
-
fg = require("fast-glob");
|
|
18
|
-
} catch { /* optional */ }
|
|
19
|
-
|
|
20
|
-
const parser = require("@babel/parser");
|
|
21
|
-
const traverse = require("@babel/traverse").default;
|
|
22
|
-
const t = require("@babel/types");
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const crypto = require('crypto');
|
|
23
14
|
|
|
24
15
|
// ============================================================================
|
|
25
|
-
// CANONICALIZATION
|
|
16
|
+
// CANONICALIZATION
|
|
26
17
|
// ============================================================================
|
|
27
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Canonicalize a path to standard format.
|
|
21
|
+
*/
|
|
28
22
|
function canonicalizePath(p) {
|
|
29
|
-
let s =
|
|
30
|
-
if (!s)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
s = s.replace(/\[
|
|
36
|
-
s = s.replace(/\[
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
23
|
+
let s = p.trim();
|
|
24
|
+
if (!s.startsWith('/')) s = '/' + s;
|
|
25
|
+
s = s.replace(/\/+/g, '/');
|
|
26
|
+
|
|
27
|
+
// Convert Next.js dynamic segments
|
|
28
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, '*$1?'); // [[...slug]] → *slug?
|
|
29
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, '*$1'); // [...slug] → *slug
|
|
30
|
+
s = s.replace(/\[([^\]]+)\]/g, ':$1'); // [id] → :id
|
|
31
|
+
|
|
32
|
+
if (s.length > 1) s = s.replace(/\/$/, '');
|
|
40
33
|
return s;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
function canonicalizeMethod(m) {
|
|
44
|
-
const u =
|
|
45
|
-
if (u ===
|
|
46
|
-
return u
|
|
37
|
+
const u = m.toUpperCase();
|
|
38
|
+
if (u === 'ALL' || u === 'ANY') return '*';
|
|
39
|
+
return u;
|
|
47
40
|
}
|
|
48
41
|
|
|
49
42
|
function joinPrefix(prefix, p) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
if (b === "/") return a;
|
|
54
|
-
return canonicalizePath(a + "/" + b);
|
|
43
|
+
const cleanPrefix = prefix.replace(/\/$/, '');
|
|
44
|
+
const cleanPath = p.startsWith('/') ? p : '/' + p;
|
|
45
|
+
return canonicalizePath(cleanPrefix + cleanPath);
|
|
55
46
|
}
|
|
56
47
|
|
|
57
|
-
function isParameterizedPath(
|
|
58
|
-
|
|
59
|
-
return s.includes(":") || s.includes("*");
|
|
48
|
+
function isParameterizedPath(path) {
|
|
49
|
+
return path.includes(':') || path.includes('*');
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
/**
|
|
63
|
-
* Match a pattern against a concrete path.
|
|
64
|
-
* Supported:
|
|
65
|
-
* - :id matches one segment
|
|
66
|
-
* - *slug or *slug? matches the rest of the path (0+ segments)
|
|
67
|
-
*/
|
|
68
52
|
function matchPath(pattern, concrete) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (pat === con) return true;
|
|
73
|
-
|
|
74
|
-
const pParts = pat.split("/").filter(Boolean);
|
|
75
|
-
const cParts = con.split("/").filter(Boolean);
|
|
76
|
-
|
|
53
|
+
const patternParts = pattern.split('/');
|
|
54
|
+
const concreteParts = concrete.split('/');
|
|
55
|
+
|
|
77
56
|
let pIdx = 0, cIdx = 0;
|
|
78
|
-
|
|
79
|
-
while (pIdx <
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (pSeg.startsWith(":")) {
|
|
88
|
-
pIdx++; cIdx++; continue;
|
|
89
|
-
}
|
|
90
|
-
if (pSeg !== cSeg) return false;
|
|
91
|
-
|
|
57
|
+
|
|
58
|
+
while (pIdx < patternParts.length && cIdx < concreteParts.length) {
|
|
59
|
+
const pPart = patternParts[pIdx];
|
|
60
|
+
const cPart = concreteParts[cIdx];
|
|
61
|
+
|
|
62
|
+
if (pPart.startsWith('*')) return true;
|
|
63
|
+
if (pPart.startsWith(':')) { pIdx++; cIdx++; continue; }
|
|
64
|
+
if (pPart !== cPart) return false;
|
|
92
65
|
pIdx++; cIdx++;
|
|
93
66
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (pIdx === pParts.length - 1 && pParts[pIdx]?.startsWith("*")) return true;
|
|
97
|
-
|
|
98
|
-
return pIdx === pParts.length && cIdx === cParts.length;
|
|
67
|
+
|
|
68
|
+
return pIdx === patternParts.length && cIdx === concreteParts.length;
|
|
99
69
|
}
|
|
100
70
|
|
|
101
71
|
function matchMethod(pattern, concrete) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (p === "*") return true;
|
|
105
|
-
return p === c;
|
|
72
|
+
if (pattern === '*') return true;
|
|
73
|
+
return pattern === concrete;
|
|
106
74
|
}
|
|
107
75
|
|
|
108
76
|
// ============================================================================
|
|
109
|
-
//
|
|
77
|
+
// NEXT.JS RESOLVER
|
|
110
78
|
// ============================================================================
|
|
111
79
|
|
|
112
|
-
const NEXT_HTTP_METHODS =
|
|
80
|
+
const NEXT_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
113
81
|
let evidenceCounter = 0;
|
|
114
82
|
|
|
115
|
-
function sha256Short(txt) {
|
|
116
|
-
return crypto.createHash("sha256").update(String(txt || "")).digest("hex").slice(0, 16);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
83
|
function createEvidence(file, lines, reason, snippet) {
|
|
120
84
|
evidenceCounter++;
|
|
121
85
|
return {
|
|
122
|
-
id: `ev_${String(evidenceCounter).padStart(4,
|
|
86
|
+
id: `ev_${String(evidenceCounter).padStart(4, '0')}`,
|
|
123
87
|
file,
|
|
124
88
|
lines,
|
|
125
|
-
snippetHash: `sha256:${
|
|
89
|
+
snippetHash: `sha256:${crypto.createHash('sha256').update(snippet || '').digest('hex').slice(0, 16)}`,
|
|
126
90
|
reason,
|
|
127
91
|
};
|
|
128
92
|
}
|
|
129
93
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
function parseFile(code) {
|
|
135
|
-
return parser.parse(code, {
|
|
136
|
-
sourceType: "unambiguous",
|
|
137
|
-
plugins: ["typescript", "jsx"],
|
|
138
|
-
errorRecovery: true,
|
|
139
|
-
ranges: false,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
144
|
-
if (!loc || !loc.start) return [];
|
|
145
|
-
const code = safeRead(fileAbs);
|
|
146
|
-
if (!code) return [];
|
|
147
|
-
const lines = code.split(/\r?\n/);
|
|
148
|
-
const start = Math.max(1, loc.start.line || 1);
|
|
149
|
-
const end = Math.max(start, loc.end?.line || start);
|
|
150
|
-
const snippet = lines.slice(start - 1, end).join("\n");
|
|
151
|
-
return [createEvidence(fileRel, `${start}-${end}`, reason, snippet)];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function findFilesFallback(dirAbs, includeRe, excludeRe) {
|
|
155
|
-
const out = [];
|
|
94
|
+
function findFiles(dir, include, exclude) {
|
|
95
|
+
const files = [];
|
|
96
|
+
|
|
156
97
|
function walk(d) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
walk(full);
|
|
171
|
-
} else if (ent.isFile()) {
|
|
172
|
-
if (excludeRe && excludeRe.test(ent.name)) continue;
|
|
173
|
-
if (includeRe.test(ent.name)) out.push(full);
|
|
98
|
+
try {
|
|
99
|
+
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const fullPath = path.join(d, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
104
|
+
walk(fullPath);
|
|
105
|
+
}
|
|
106
|
+
} else if (entry.isFile()) {
|
|
107
|
+
if (include.test(entry.name) && (!exclude || !exclude.test(entry.name))) {
|
|
108
|
+
files.push(fullPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
174
111
|
}
|
|
175
|
-
}
|
|
112
|
+
} catch {}
|
|
176
113
|
}
|
|
177
|
-
|
|
178
|
-
|
|
114
|
+
|
|
115
|
+
walk(dir);
|
|
116
|
+
return files;
|
|
179
117
|
}
|
|
180
118
|
|
|
181
|
-
|
|
182
|
-
if (fg) {
|
|
183
|
-
return fg(patterns, {
|
|
184
|
-
cwd: repoRoot,
|
|
185
|
-
absolute: true,
|
|
186
|
-
dot: false,
|
|
187
|
-
ignore: ignore || [
|
|
188
|
-
"**/node_modules/**",
|
|
189
|
-
"**/.next/**",
|
|
190
|
-
"**/dist/**",
|
|
191
|
-
"**/build/**",
|
|
192
|
-
"**/coverage/**",
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// fallback: only supports the specific Next patterns we use
|
|
198
|
-
const out = [];
|
|
199
|
-
for (const ptn of patterns) {
|
|
200
|
-
// minimal handling: find root dirs from patterns
|
|
201
|
-
if (ptn.includes("app/api") || ptn.includes("src/app/api")) {
|
|
202
|
-
const dir1 = path.join(repoRoot, "app", "api");
|
|
203
|
-
const dir2 = path.join(repoRoot, "src", "app", "api");
|
|
204
|
-
if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /route\.(ts|js)$/));
|
|
205
|
-
if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /route\.(ts|js)$/));
|
|
206
|
-
}
|
|
207
|
-
if (ptn.includes("pages/api") || ptn.includes("src/pages/api")) {
|
|
208
|
-
const dir1 = path.join(repoRoot, "pages", "api");
|
|
209
|
-
const dir2 = path.join(repoRoot, "src", "pages", "api");
|
|
210
|
-
if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /\.(ts|js)$/, /\.d\.ts$/));
|
|
211
|
-
if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /\.(ts|js)$/, /\.d\.ts$/));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return Array.from(new Set(out));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// NEXT.JS RESOLVER (AST)
|
|
219
|
-
// ============================================================================
|
|
220
|
-
|
|
221
|
-
function extractNextAppRouterMethodsAST(ast) {
|
|
119
|
+
function extractAppRouterMethods(code) {
|
|
222
120
|
const methods = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const n = d.id.name.toUpperCase();
|
|
240
|
-
if (NEXT_HTTP_METHODS.has(n)) {
|
|
241
|
-
methods.push({ name: n, loc: d.loc || decl.loc });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
121
|
+
const lines = code.split('\n');
|
|
122
|
+
|
|
123
|
+
const patterns = [
|
|
124
|
+
/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(/,
|
|
125
|
+
/export\s+const\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*=/,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
const match = lines[i].match(pattern);
|
|
131
|
+
if (match && NEXT_HTTP_METHODS.includes(match[1].toUpperCase())) {
|
|
132
|
+
methods.push({
|
|
133
|
+
name: match[1].toUpperCase(),
|
|
134
|
+
line: i + 1,
|
|
135
|
+
snippet: lines[i].trim(),
|
|
136
|
+
});
|
|
244
137
|
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
247
141
|
return methods;
|
|
248
142
|
}
|
|
249
143
|
|
|
250
|
-
function deriveNextAppRoutePath(fileRel) {
|
|
251
|
-
// matches: app/api/**/route.ts|js OR src/app/api/**/route.ts|js
|
|
252
|
-
const m = fileRel.match(/(?:^|\/)(?:src\/)?app\/api\/(.+)\/route\.(ts|js)$/);
|
|
253
|
-
if (!m) return null;
|
|
254
|
-
const sub = m[1];
|
|
255
|
-
return canonicalizePath("/api/" + sub);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function deriveNextPagesRoutePath(fileRel) {
|
|
259
|
-
// matches: pages/api/**.ts|js OR src/pages/api/**.ts|js
|
|
260
|
-
const m = fileRel.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(ts|js)$/);
|
|
261
|
-
if (!m) return null;
|
|
262
|
-
let sub = m[1];
|
|
263
|
-
sub = sub.replace(/\/index$/, ""); // /foo/index -> /foo
|
|
264
|
-
return canonicalizePath("/api/" + sub);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
144
|
async function resolveNextRoutes(repoRoot) {
|
|
268
145
|
const routes = [];
|
|
269
|
-
|
|
270
|
-
// App Router
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
146
|
+
|
|
147
|
+
// App Router: app/api/**/route.ts|js
|
|
148
|
+
const appDirs = ['app', 'src/app'];
|
|
149
|
+
for (const appDir of appDirs) {
|
|
150
|
+
const apiDir = path.join(repoRoot, appDir, 'api');
|
|
151
|
+
if (!fs.existsSync(apiDir)) continue;
|
|
152
|
+
|
|
153
|
+
const routeFiles = findFiles(apiDir, /route\.(ts|js)$/);
|
|
154
|
+
|
|
155
|
+
for (const file of routeFiles) {
|
|
156
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
157
|
+
const apiIdx = relPath.indexOf('/api/');
|
|
158
|
+
const sub = relPath.slice(apiIdx + '/api/'.length).replace(/\/route\.(ts|js)$/, '');
|
|
159
|
+
const routePath = canonicalizePath('/api/' + sub);
|
|
160
|
+
|
|
161
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
162
|
+
const methods = extractAppRouterMethods(code);
|
|
163
|
+
|
|
164
|
+
if (methods.length === 0) {
|
|
165
|
+
routes.push({
|
|
166
|
+
method: '*',
|
|
167
|
+
path: routePath,
|
|
168
|
+
handler: relPath,
|
|
169
|
+
framework: 'next',
|
|
170
|
+
routerType: 'app',
|
|
171
|
+
confidence: 'low',
|
|
172
|
+
evidence: [createEvidence(relPath, '1', 'route file with no exports', code.slice(0, 100))],
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const m of methods) {
|
|
178
|
+
routes.push({
|
|
179
|
+
method: m.name,
|
|
180
|
+
path: routePath,
|
|
181
|
+
handler: `${relPath}:${m.line}`,
|
|
182
|
+
framework: 'next',
|
|
183
|
+
routerType: 'app',
|
|
184
|
+
confidence: 'high',
|
|
185
|
+
evidence: [createEvidence(relPath, String(m.line), `export ${m.name}`, m.snippet)],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
300
188
|
}
|
|
301
|
-
|
|
302
|
-
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Pages Router: pages/api/**/*.ts|js
|
|
192
|
+
const pagesDirs = ['pages', 'src/pages'];
|
|
193
|
+
for (const pagesDir of pagesDirs) {
|
|
194
|
+
const apiDir = path.join(repoRoot, pagesDir, 'api');
|
|
195
|
+
if (!fs.existsSync(apiDir)) continue;
|
|
196
|
+
|
|
197
|
+
const apiFiles = findFiles(apiDir, /\.(ts|js)$/, /\.d\.ts$/);
|
|
198
|
+
|
|
199
|
+
for (const file of apiFiles) {
|
|
200
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
201
|
+
const apiIdx = relPath.indexOf('/api/');
|
|
202
|
+
const sub = relPath
|
|
203
|
+
.slice(apiIdx + '/api/'.length)
|
|
204
|
+
.replace(/\.(ts|js)$/, '')
|
|
205
|
+
.replace(/\/index$/, '');
|
|
206
|
+
|
|
207
|
+
const routePath = canonicalizePath('/api/' + sub);
|
|
208
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
209
|
+
const hasDefaultExport = /export\s+default/.test(code);
|
|
210
|
+
|
|
303
211
|
routes.push({
|
|
304
|
-
method:
|
|
212
|
+
method: '*',
|
|
305
213
|
path: routePath,
|
|
306
|
-
handler:
|
|
307
|
-
framework:
|
|
308
|
-
routerType:
|
|
309
|
-
confidence:
|
|
310
|
-
evidence:
|
|
214
|
+
handler: relPath,
|
|
215
|
+
framework: 'next',
|
|
216
|
+
routerType: 'pages',
|
|
217
|
+
confidence: hasDefaultExport ? 'med' : 'low',
|
|
218
|
+
evidence: [createEvidence(relPath, '1', 'Pages API route', code.slice(0, 100))],
|
|
311
219
|
});
|
|
312
220
|
}
|
|
313
221
|
}
|
|
314
|
-
|
|
315
|
-
// Pages Router
|
|
316
|
-
const pagesFiles = await globFiles(repoRoot, [
|
|
317
|
-
"**/pages/api/**/*.@(ts|js)",
|
|
318
|
-
"**/src/pages/api/**/*.@(ts|js)",
|
|
319
|
-
]);
|
|
320
|
-
|
|
321
|
-
for (const fileAbs of pagesFiles) {
|
|
322
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
323
|
-
if (fileRel.endsWith(".d.ts")) continue;
|
|
324
|
-
|
|
325
|
-
const routePath = deriveNextPagesRoutePath(fileRel);
|
|
326
|
-
if (!routePath) continue;
|
|
327
|
-
|
|
328
|
-
const code = safeRead(fileAbs);
|
|
329
|
-
if (!code) continue;
|
|
330
|
-
|
|
331
|
-
const hasDefaultExport = /\bexport\s+default\b/.test(code);
|
|
332
|
-
|
|
333
|
-
routes.push({
|
|
334
|
-
method: "*",
|
|
335
|
-
path: routePath,
|
|
336
|
-
handler: fileRel,
|
|
337
|
-
framework: "next",
|
|
338
|
-
routerType: "pages",
|
|
339
|
-
confidence: hasDefaultExport ? "med" : "low",
|
|
340
|
-
evidence: [createEvidence(fileRel, "1", "Next pages API route", code.slice(0, 140))],
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
222
|
+
|
|
344
223
|
return routes;
|
|
345
224
|
}
|
|
346
225
|
|
|
347
226
|
// ============================================================================
|
|
348
|
-
// FASTIFY RESOLVER (
|
|
227
|
+
// FASTIFY RESOLVER (Simplified - regex based)
|
|
349
228
|
// ============================================================================
|
|
350
229
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Find project root by walking up from a file until we find package.json
|
|
363
|
-
*/
|
|
364
|
-
function findProjectRoot(fromFileAbs) {
|
|
365
|
-
let dir = path.dirname(fromFileAbs);
|
|
366
|
-
const root = path.parse(dir).root;
|
|
367
|
-
while (dir !== root) {
|
|
368
|
-
if (existsFile(path.join(dir, "package.json"))) return dir;
|
|
369
|
-
const parent = path.dirname(dir);
|
|
370
|
-
if (parent === dir) break;
|
|
371
|
-
dir = parent;
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Load and cache tsconfig.json paths for a project
|
|
378
|
-
*/
|
|
379
|
-
const tsconfigCache = new Map();
|
|
380
|
-
|
|
381
|
-
function loadTsConfigPaths(projectRoot) {
|
|
382
|
-
if (!projectRoot) return null;
|
|
383
|
-
if (tsconfigCache.has(projectRoot)) return tsconfigCache.get(projectRoot);
|
|
384
|
-
|
|
385
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
386
|
-
if (!existsFile(tsconfigPath)) {
|
|
387
|
-
tsconfigCache.set(projectRoot, null);
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
393
|
-
// Remove comments (// and /* */) for JSON parsing
|
|
394
|
-
const cleaned = raw
|
|
395
|
-
.replace(/\/\/.*$/gm, "")
|
|
396
|
-
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
397
|
-
const tsconfig = JSON.parse(cleaned);
|
|
398
|
-
|
|
399
|
-
const paths = tsconfig?.compilerOptions?.paths || {};
|
|
400
|
-
const baseUrl = tsconfig?.compilerOptions?.baseUrl || ".";
|
|
401
|
-
const baseDir = path.resolve(projectRoot, baseUrl);
|
|
402
|
-
|
|
403
|
-
const result = { paths, baseDir, projectRoot };
|
|
404
|
-
tsconfigCache.set(projectRoot, result);
|
|
405
|
-
return result;
|
|
406
|
-
} catch {
|
|
407
|
-
tsconfigCache.set(projectRoot, null);
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Resolve a module specifier using TypeScript path mappings
|
|
414
|
-
*/
|
|
415
|
-
function resolveWithTsConfigPaths(spec, tsConfig) {
|
|
416
|
-
if (!tsConfig || !tsConfig.paths) return null;
|
|
417
|
-
|
|
418
|
-
const { paths, baseDir } = tsConfig;
|
|
419
|
-
|
|
420
|
-
for (const [pattern, targets] of Object.entries(paths)) {
|
|
421
|
-
// Handle exact match: "@vibecheck/core" -> ["../../packages/core/dist"]
|
|
422
|
-
if (pattern === spec) {
|
|
423
|
-
for (const target of targets) {
|
|
424
|
-
const resolved = path.resolve(baseDir, target.replace(/\/\*$/, ""));
|
|
425
|
-
const candidates = [
|
|
426
|
-
resolved,
|
|
427
|
-
resolved + ".ts",
|
|
428
|
-
resolved + ".js",
|
|
429
|
-
path.join(resolved, "index.ts"),
|
|
430
|
-
path.join(resolved, "index.js"),
|
|
431
|
-
];
|
|
432
|
-
for (const c of candidates) if (existsFile(c)) return c;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Handle wildcard pattern: "@/*" -> ["./src/*"]
|
|
437
|
-
if (pattern.endsWith("/*")) {
|
|
438
|
-
const prefix = pattern.slice(0, -2);
|
|
439
|
-
if (spec.startsWith(prefix + "/")) {
|
|
440
|
-
const rest = spec.slice(prefix.length + 1);
|
|
441
|
-
for (const target of targets) {
|
|
442
|
-
const targetBase = target.replace(/\/\*$/, "");
|
|
443
|
-
const resolved = path.resolve(baseDir, targetBase, rest);
|
|
444
|
-
const candidates = [
|
|
445
|
-
resolved,
|
|
446
|
-
resolved + ".ts",
|
|
447
|
-
resolved + ".js",
|
|
448
|
-
path.join(resolved, "index.ts"),
|
|
449
|
-
path.join(resolved, "index.js"),
|
|
450
|
-
];
|
|
451
|
-
for (const c of candidates) if (existsFile(c)) return c;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Parse package.json and find the main entrypoint
|
|
462
|
-
*/
|
|
463
|
-
function getPackageEntrypoint(pkgJsonPath) {
|
|
464
|
-
try {
|
|
465
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
466
|
-
const pkgDir = path.dirname(pkgJsonPath);
|
|
467
|
-
|
|
468
|
-
// Priority: exports["."] > main > index.js
|
|
469
|
-
// Handle exports field (modern packages)
|
|
470
|
-
if (pkgJson.exports) {
|
|
471
|
-
const exp = pkgJson.exports;
|
|
472
|
-
|
|
473
|
-
// exports: "./lib/index.js" (string shorthand)
|
|
474
|
-
if (typeof exp === "string") {
|
|
475
|
-
const resolved = path.resolve(pkgDir, exp);
|
|
476
|
-
if (existsFile(resolved)) return resolved;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// exports: { ".": "./lib/index.js" } or { ".": { "require": "...", "import": "..." } }
|
|
480
|
-
if (typeof exp === "object" && exp["."]) {
|
|
481
|
-
const dotExport = exp["."];
|
|
482
|
-
|
|
483
|
-
if (typeof dotExport === "string") {
|
|
484
|
-
const resolved = path.resolve(pkgDir, dotExport);
|
|
485
|
-
if (existsFile(resolved)) return resolved;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Conditional exports - prefer require for CommonJS, then import, then default
|
|
489
|
-
if (typeof dotExport === "object") {
|
|
490
|
-
const conditions = ["require", "node", "import", "default"];
|
|
491
|
-
for (const cond of conditions) {
|
|
492
|
-
if (dotExport[cond]) {
|
|
493
|
-
const val = dotExport[cond];
|
|
494
|
-
const target = typeof val === "string" ? val : val?.default;
|
|
495
|
-
if (target) {
|
|
496
|
-
const resolved = path.resolve(pkgDir, target);
|
|
497
|
-
if (existsFile(resolved)) return resolved;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Fallback to main field
|
|
506
|
-
if (pkgJson.main) {
|
|
507
|
-
const resolved = path.resolve(pkgDir, pkgJson.main);
|
|
508
|
-
if (existsFile(resolved)) return resolved;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Final fallback: index.js
|
|
512
|
-
const indexJs = path.join(pkgDir, "index.js");
|
|
513
|
-
if (existsFile(indexJs)) return indexJs;
|
|
514
|
-
|
|
515
|
-
return null;
|
|
516
|
-
} catch {
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Resolve a package specifier from node_modules
|
|
523
|
-
* Handles: "fastify", "@fastify/cors", "@vibecheck/core"
|
|
524
|
-
*/
|
|
525
|
-
function resolveFromNodeModules(spec, projectRoot) {
|
|
526
|
-
if (!projectRoot || !spec) return null;
|
|
527
|
-
|
|
528
|
-
// Don't resolve built-in Node.js modules
|
|
529
|
-
const builtins = new Set([
|
|
530
|
-
"fs", "path", "http", "https", "url", "crypto", "os", "util", "stream",
|
|
531
|
-
"events", "buffer", "querystring", "child_process", "cluster", "dgram",
|
|
532
|
-
"dns", "net", "readline", "tls", "tty", "zlib", "assert", "async_hooks",
|
|
533
|
-
"perf_hooks", "v8", "vm", "worker_threads", "module", "process"
|
|
534
|
-
]);
|
|
535
|
-
|
|
536
|
-
const pkgName = spec.startsWith("@")
|
|
537
|
-
? spec.split("/").slice(0, 2).join("/") // @scope/name
|
|
538
|
-
: spec.split("/")[0]; // name
|
|
539
|
-
|
|
540
|
-
if (builtins.has(pkgName)) return null;
|
|
541
|
-
|
|
542
|
-
// Walk up directory tree looking for node_modules
|
|
543
|
-
let searchDir = projectRoot;
|
|
544
|
-
const root = path.parse(searchDir).root;
|
|
545
|
-
|
|
546
|
-
while (searchDir !== root) {
|
|
547
|
-
const nodeModulesDir = path.join(searchDir, "node_modules");
|
|
548
|
-
|
|
549
|
-
if (existsDir(nodeModulesDir)) {
|
|
550
|
-
const pkgDir = path.join(nodeModulesDir, pkgName);
|
|
551
|
-
|
|
552
|
-
if (existsDir(pkgDir)) {
|
|
553
|
-
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
554
|
-
|
|
555
|
-
if (existsFile(pkgJsonPath)) {
|
|
556
|
-
// Check if spec has a subpath: "@fastify/cors/lib/foo"
|
|
557
|
-
const subpath = spec.slice(pkgName.length);
|
|
558
|
-
|
|
559
|
-
if (subpath && subpath !== "/") {
|
|
560
|
-
// Resolve subpath within the package
|
|
561
|
-
const subpathResolved = path.join(pkgDir, subpath);
|
|
562
|
-
const candidates = [
|
|
563
|
-
subpathResolved,
|
|
564
|
-
subpathResolved + ".js",
|
|
565
|
-
subpathResolved + ".ts",
|
|
566
|
-
path.join(subpathResolved, "index.js"),
|
|
567
|
-
path.join(subpathResolved, "index.ts"),
|
|
568
|
-
];
|
|
569
|
-
for (const c of candidates) if (existsFile(c)) return c;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Resolve main entrypoint
|
|
573
|
-
const entry = getPackageEntrypoint(pkgJsonPath);
|
|
574
|
-
if (entry) return entry;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const parent = path.dirname(searchDir);
|
|
580
|
-
if (parent === searchDir) break;
|
|
581
|
-
searchDir = parent;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Check if a package is a "non-route" Fastify plugin
|
|
589
|
-
* These plugins add functionality but don't define routes themselves
|
|
590
|
-
*/
|
|
591
|
-
function isNonRoutePlugin(spec) {
|
|
592
|
-
const nonRoutePlugins = new Set([
|
|
593
|
-
// @fastify/* scoped plugins
|
|
594
|
-
"@fastify/cors",
|
|
595
|
-
"@fastify/helmet",
|
|
596
|
-
"@fastify/compress",
|
|
597
|
-
"@fastify/cookie",
|
|
598
|
-
"@fastify/secure-session",
|
|
599
|
-
"@fastify/session",
|
|
600
|
-
"@fastify/rate-limit",
|
|
601
|
-
"@fastify/jwt",
|
|
602
|
-
"@fastify/auth",
|
|
603
|
-
"@fastify/bearer-auth",
|
|
604
|
-
"@fastify/basic-auth",
|
|
605
|
-
"@fastify/multipart",
|
|
606
|
-
"@fastify/formbody",
|
|
607
|
-
"@fastify/static",
|
|
608
|
-
"@fastify/view",
|
|
609
|
-
"@fastify/sensible",
|
|
610
|
-
"@fastify/env",
|
|
611
|
-
"@fastify/accepts",
|
|
612
|
-
"@fastify/caching",
|
|
613
|
-
"@fastify/etag",
|
|
614
|
-
"@fastify/circuit-breaker",
|
|
615
|
-
"@fastify/response-validation",
|
|
616
|
-
"@fastify/request-context",
|
|
617
|
-
"@fastify/under-pressure",
|
|
618
|
-
"@fastify/middie",
|
|
619
|
-
"@fastify/express",
|
|
620
|
-
"@fastify/http-proxy",
|
|
621
|
-
"@fastify/reply-from",
|
|
622
|
-
"@fastify/websocket",
|
|
623
|
-
"@fastify/type-provider-json-schema-to-ts",
|
|
624
|
-
"@fastify/type-provider-typebox",
|
|
625
|
-
"@fastify/type-provider-zod",
|
|
626
|
-
"@fastify/mongodb",
|
|
627
|
-
"@fastify/postgres",
|
|
628
|
-
"@fastify/mysql",
|
|
629
|
-
"@fastify/redis",
|
|
630
|
-
"@fastify/leveldb",
|
|
631
|
-
"@fastify/elasticsearch",
|
|
632
|
-
"@fastify/metrics",
|
|
633
|
-
"@fastify/request-id",
|
|
634
|
-
// Legacy fastify-* plugins
|
|
635
|
-
"fastify-plugin",
|
|
636
|
-
"fastify-cors",
|
|
637
|
-
"fastify-helmet",
|
|
638
|
-
"fastify-compress",
|
|
639
|
-
"fastify-cookie",
|
|
640
|
-
"fastify-session",
|
|
641
|
-
"fastify-rate-limit",
|
|
642
|
-
"fastify-jwt",
|
|
643
|
-
"fastify-auth",
|
|
644
|
-
"fastify-sensible",
|
|
645
|
-
"fastify-multipart",
|
|
646
|
-
"fastify-formbody",
|
|
647
|
-
"fastify-static",
|
|
648
|
-
"fastify-websocket",
|
|
649
|
-
]);
|
|
650
|
-
|
|
651
|
-
const pkgName = spec.startsWith("@")
|
|
652
|
-
? spec.split("/").slice(0, 2).join("/")
|
|
653
|
-
: spec.split("/")[0];
|
|
654
|
-
|
|
655
|
-
return nonRoutePlugins.has(pkgName);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Check if this is @fastify/autoload which needs special directory handling
|
|
660
|
-
*/
|
|
661
|
-
function isAutoloadPlugin(spec) {
|
|
662
|
-
return spec === "@fastify/autoload" || spec === "fastify-autoload";
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function resolveRelativeModule(fromFileAbs, spec) {
|
|
666
|
-
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
667
|
-
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
668
|
-
const candidates = [
|
|
669
|
-
base,
|
|
670
|
-
base + ".ts",
|
|
671
|
-
base + ".js",
|
|
672
|
-
path.join(base, "index.ts"),
|
|
673
|
-
path.join(base, "index.js"),
|
|
230
|
+
async function resolveFastifyRoutes(repoRoot) {
|
|
231
|
+
const routes = [];
|
|
232
|
+
const gaps = [];
|
|
233
|
+
|
|
234
|
+
const entryPoints = [
|
|
235
|
+
'src/server.ts', 'src/server.js', 'src/index.ts', 'src/index.js',
|
|
236
|
+
'server.ts', 'server.js', 'apps/api/src/server.ts', 'apps/api/src/index.ts',
|
|
674
237
|
];
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// 1. Relative imports
|
|
687
|
-
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
688
|
-
const resolved = resolveRelativeModule(fromFileAbs, spec);
|
|
689
|
-
return {
|
|
690
|
-
resolved,
|
|
691
|
-
isNonRoute: false,
|
|
692
|
-
reason: resolved ? "relative import" : "relative module not found"
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// 2. Check if it's a non-route plugin (skip scanning)
|
|
697
|
-
if (isNonRoutePlugin(spec)) {
|
|
698
|
-
return {
|
|
699
|
-
resolved: null,
|
|
700
|
-
isNonRoute: true,
|
|
701
|
-
reason: `non-route plugin: ${spec}`
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const projectRoot = findProjectRoot(fromFileAbs);
|
|
706
|
-
|
|
707
|
-
// 3. TypeScript path mappings
|
|
708
|
-
if (projectRoot) {
|
|
709
|
-
const tsConfig = loadTsConfigPaths(projectRoot);
|
|
710
|
-
if (tsConfig) {
|
|
711
|
-
const resolved = resolveWithTsConfigPaths(spec, tsConfig);
|
|
712
|
-
if (resolved) {
|
|
713
|
-
return { resolved, isNonRoute: false, reason: "tsconfig paths" };
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// 4. Node modules resolution
|
|
719
|
-
if (projectRoot) {
|
|
720
|
-
const resolved = resolveFromNodeModules(spec, projectRoot);
|
|
721
|
-
if (resolved) {
|
|
722
|
-
return { resolved, isNonRoute: false, reason: "node_modules" };
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return {
|
|
727
|
-
resolved: null,
|
|
728
|
-
isNonRoute: false,
|
|
729
|
-
reason: `unresolved package: ${spec}`
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function extractStringLiteral(node) {
|
|
734
|
-
return t.isStringLiteral(node) ? node.value : null;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function extractPrefixFromOpts(node) {
|
|
738
|
-
if (!t.isObjectExpression(node)) return null;
|
|
739
|
-
for (const p of node.properties) {
|
|
740
|
-
if (!t.isObjectProperty(p)) continue;
|
|
741
|
-
const key =
|
|
742
|
-
t.isIdentifier(p.key) ? p.key.name :
|
|
743
|
-
t.isStringLiteral(p.key) ? p.key.value :
|
|
744
|
-
null;
|
|
745
|
-
if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
|
|
746
|
-
}
|
|
747
|
-
return null;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function extractRouteObject(objExpr) {
|
|
751
|
-
let url = null;
|
|
752
|
-
let methods = [];
|
|
753
|
-
let hasHandler = false;
|
|
754
|
-
|
|
755
|
-
for (const p of objExpr.properties) {
|
|
756
|
-
if (!t.isObjectProperty(p)) continue;
|
|
757
|
-
|
|
758
|
-
const key =
|
|
759
|
-
t.isIdentifier(p.key) ? p.key.name :
|
|
760
|
-
t.isStringLiteral(p.key) ? p.key.value :
|
|
761
|
-
null;
|
|
762
|
-
if (!key) continue;
|
|
763
|
-
|
|
764
|
-
if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
|
|
765
|
-
|
|
766
|
-
if (key === "method") {
|
|
767
|
-
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
768
|
-
if (t.isArrayExpression(p.value)) {
|
|
769
|
-
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
770
|
-
}
|
|
238
|
+
|
|
239
|
+
const fastifyMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all'];
|
|
240
|
+
|
|
241
|
+
// Find source files
|
|
242
|
+
const srcDirs = ['src', 'apps/api/src', 'server'];
|
|
243
|
+
const files = [];
|
|
244
|
+
|
|
245
|
+
for (const srcDir of srcDirs) {
|
|
246
|
+
const fullDir = path.join(repoRoot, srcDir);
|
|
247
|
+
if (fs.existsSync(fullDir)) {
|
|
248
|
+
files.push(...findFiles(fullDir, /\.(ts|js)$/, /\.d\.ts$/));
|
|
771
249
|
}
|
|
772
|
-
|
|
773
|
-
if (key === "handler") hasHandler = true;
|
|
774
250
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
"
|
|
784
|
-
"
|
|
785
|
-
"apps/api/src/server.ts",
|
|
786
|
-
"apps/api/src/index.ts",
|
|
251
|
+
|
|
252
|
+
// Patterns to detect routes
|
|
253
|
+
const patterns = [
|
|
254
|
+
// fastify.get('/path', handler)
|
|
255
|
+
/(?:fastify|app|server)\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
256
|
+
// router.get('/path', handler)
|
|
257
|
+
/router\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
258
|
+
// .route({ method: 'GET', url: '/path' })
|
|
259
|
+
/\.route\s*\(\s*\{[^}]*method:\s*['"`]([^'"`]+)['"`][^}]*url:\s*['"`]([^'"`]+)['"`]/gi,
|
|
260
|
+
/\.route\s*\(\s*\{[^}]*url:\s*['"`]([^'"`]+)['"`][^}]*method:\s*['"`]([^'"`]+)['"`]/gi,
|
|
787
261
|
];
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (!init) return;
|
|
820
|
-
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
821
|
-
const cal = init.callee.name;
|
|
822
|
-
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
823
|
-
}
|
|
824
|
-
},
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
function resolveImportSpecForLocal(localName) {
|
|
828
|
-
let spec = null;
|
|
829
|
-
|
|
830
|
-
traverse(ast, {
|
|
831
|
-
ImportDeclaration(ip) {
|
|
832
|
-
for (const s of ip.node.specifiers) {
|
|
833
|
-
if (
|
|
834
|
-
(t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) &&
|
|
835
|
-
s.local.name === localName
|
|
836
|
-
) {
|
|
837
|
-
spec = ip.node.source.value;
|
|
262
|
+
|
|
263
|
+
// Track prefixes from register calls
|
|
264
|
+
const prefixMap = new Map(); // file → prefix
|
|
265
|
+
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
try {
|
|
268
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
269
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
270
|
+
const lines = code.split('\n');
|
|
271
|
+
|
|
272
|
+
// Detect prefix from register calls
|
|
273
|
+
const registerPattern = /\.register\s*\([^,]+,\s*\{[^}]*prefix:\s*['"`]([^'"`]+)['"`]/g;
|
|
274
|
+
let match;
|
|
275
|
+
while ((match = registerPattern.exec(code)) !== null) {
|
|
276
|
+
prefixMap.set(relPath, match[1]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Extract routes
|
|
280
|
+
for (const pattern of patterns) {
|
|
281
|
+
pattern.lastIndex = 0;
|
|
282
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
283
|
+
let method, routePath;
|
|
284
|
+
|
|
285
|
+
if (match[0].includes('.route')) {
|
|
286
|
+
// Handle .route() pattern - order varies
|
|
287
|
+
if (match[0].indexOf('method') < match[0].indexOf('url')) {
|
|
288
|
+
method = match[1];
|
|
289
|
+
routePath = match[2];
|
|
290
|
+
} else {
|
|
291
|
+
routePath = match[1];
|
|
292
|
+
method = match[2];
|
|
838
293
|
}
|
|
294
|
+
} else {
|
|
295
|
+
method = match[1];
|
|
296
|
+
routePath = match[2];
|
|
839
297
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const a0 = init.arguments[0];
|
|
847
|
-
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
848
|
-
},
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
return spec;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
traverse(ast, {
|
|
855
|
-
CallExpression(p) {
|
|
856
|
-
const callee = p.node.callee;
|
|
857
|
-
if (!t.isMemberExpression(callee)) return;
|
|
858
|
-
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
859
|
-
|
|
860
|
-
const obj = callee.object.name;
|
|
861
|
-
const prop = callee.property.name;
|
|
862
|
-
|
|
863
|
-
if (!fastifyNames.has(obj)) return;
|
|
864
|
-
|
|
865
|
-
// fastify.get('/x', ...)
|
|
866
|
-
if (FASTIFY_METHODS.has(prop)) {
|
|
867
|
-
const routeStr = extractStringLiteral(p.node.arguments[0]);
|
|
868
|
-
if (!routeStr) return;
|
|
869
|
-
|
|
298
|
+
|
|
299
|
+
const prefix = prefixMap.get(relPath) || '';
|
|
300
|
+
const fullPath = joinPrefix(prefix, routePath);
|
|
301
|
+
const lineNum = code.substring(0, match.index).split('\n').length;
|
|
302
|
+
const snippet = lines[lineNum - 1] || '';
|
|
303
|
+
|
|
870
304
|
routes.push({
|
|
871
|
-
method: canonicalizeMethod(
|
|
872
|
-
path:
|
|
873
|
-
handler:
|
|
874
|
-
framework:
|
|
875
|
-
confidence:
|
|
876
|
-
evidence:
|
|
877
|
-
fileAbs,
|
|
878
|
-
fileRel,
|
|
879
|
-
loc: p.node.loc,
|
|
880
|
-
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
|
|
881
|
-
}),
|
|
305
|
+
method: canonicalizeMethod(method),
|
|
306
|
+
path: fullPath,
|
|
307
|
+
handler: `${relPath}:${lineNum}`,
|
|
308
|
+
framework: 'fastify',
|
|
309
|
+
confidence: 'med',
|
|
310
|
+
evidence: [createEvidence(relPath, String(lineNum), `fastify.${method}('${routePath}')`, snippet)],
|
|
882
311
|
});
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// fastify.route({ method, url, handler })
|
|
887
|
-
if (prop === "route") {
|
|
888
|
-
const arg0 = p.node.arguments[0];
|
|
889
|
-
if (!t.isObjectExpression(arg0)) return;
|
|
890
|
-
|
|
891
|
-
const r = extractRouteObject(arg0);
|
|
892
|
-
if (!r.url) return;
|
|
893
|
-
|
|
894
|
-
const fullPath = joinPrefix(prefix, r.url);
|
|
895
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
896
|
-
|
|
897
|
-
for (const m of ms) {
|
|
898
|
-
routes.push({
|
|
899
|
-
method: m,
|
|
900
|
-
path: fullPath,
|
|
901
|
-
handler: fileRel,
|
|
902
|
-
framework: "fastify",
|
|
903
|
-
confidence: r.hasHandler ? "med" : "low",
|
|
904
|
-
evidence: evidenceFromLoc({
|
|
905
|
-
fileAbs,
|
|
906
|
-
fileRel,
|
|
907
|
-
loc: p.node.loc,
|
|
908
|
-
reason: `Fastify.route({ url: "${r.url}" })`,
|
|
909
|
-
}),
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// fastify.register(plugin, { prefix })
|
|
916
|
-
if (prop === "register") {
|
|
917
|
-
const pluginArg = p.node.arguments[0];
|
|
918
|
-
const optsArg = p.node.arguments[1];
|
|
919
|
-
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
920
|
-
const childPrefix = childPrefixRaw ? joinPrefix(prefix, childPrefixRaw) : prefix;
|
|
921
|
-
|
|
922
|
-
// inline plugin: fastify.register((f, opts) => { f.get(...) }, { prefix })
|
|
923
|
-
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
924
|
-
const param0 = pluginArg.params[0];
|
|
925
|
-
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
926
|
-
|
|
927
|
-
traverse(
|
|
928
|
-
pluginArg.body,
|
|
929
|
-
{
|
|
930
|
-
CallExpression(pp) {
|
|
931
|
-
const c = pp.node.callee;
|
|
932
|
-
if (!t.isMemberExpression(c)) return;
|
|
933
|
-
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
934
|
-
if (c.object.name !== innerName) return;
|
|
935
|
-
|
|
936
|
-
const pr = c.property.name;
|
|
937
|
-
|
|
938
|
-
if (FASTIFY_METHODS.has(pr)) {
|
|
939
|
-
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
940
|
-
if (!rs) return;
|
|
941
|
-
|
|
942
|
-
routes.push({
|
|
943
|
-
method: canonicalizeMethod(pr),
|
|
944
|
-
path: joinPrefix(childPrefix, rs),
|
|
945
|
-
handler: fileRel,
|
|
946
|
-
framework: "fastify",
|
|
947
|
-
confidence: "med",
|
|
948
|
-
evidence: evidenceFromLoc({
|
|
949
|
-
fileAbs,
|
|
950
|
-
fileRel,
|
|
951
|
-
loc: pp.node.loc,
|
|
952
|
-
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
|
|
953
|
-
}),
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if (pr === "route") {
|
|
958
|
-
const a0 = pp.node.arguments[0];
|
|
959
|
-
if (!t.isObjectExpression(a0)) return;
|
|
960
|
-
const r = extractRouteObject(a0);
|
|
961
|
-
if (!r.url) return;
|
|
962
|
-
|
|
963
|
-
const fullPath = joinPrefix(childPrefix, r.url);
|
|
964
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
965
|
-
|
|
966
|
-
for (const m of ms) {
|
|
967
|
-
routes.push({
|
|
968
|
-
method: m,
|
|
969
|
-
path: fullPath,
|
|
970
|
-
handler: fileRel,
|
|
971
|
-
framework: "fastify",
|
|
972
|
-
confidence: r.hasHandler ? "med" : "low",
|
|
973
|
-
evidence: evidenceFromLoc({
|
|
974
|
-
fileAbs,
|
|
975
|
-
fileRel,
|
|
976
|
-
loc: pp.node.loc,
|
|
977
|
-
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
|
|
978
|
-
}),
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
},
|
|
983
|
-
},
|
|
984
|
-
p.scope,
|
|
985
|
-
p
|
|
986
|
-
);
|
|
987
|
-
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// imported plugin identifier: resolve module (relative, package, or TS paths)
|
|
992
|
-
if (t.isIdentifier(pluginArg)) {
|
|
993
|
-
const localName = pluginArg.name;
|
|
994
|
-
const spec = resolveImportSpecForLocal(localName);
|
|
995
|
-
|
|
996
|
-
if (!spec) {
|
|
997
|
-
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Handle @fastify/autoload: scan the specified directory
|
|
1002
|
-
if (isAutoloadPlugin(spec)) {
|
|
1003
|
-
const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
|
|
1004
|
-
if (autoloadDir && existsDir(autoloadDir)) {
|
|
1005
|
-
scanAutoloadDir(autoloadDir, childPrefix);
|
|
1006
|
-
}
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
|
|
1011
|
-
|
|
1012
|
-
// Skip non-route plugins silently (they don't add routes)
|
|
1013
|
-
if (isNonRoute) {
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (!resolved) {
|
|
1018
|
-
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
scanFile(resolved, childPrefix);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Direct require/import: fastify.register(require('./routes'))
|
|
1026
|
-
if (t.isCallExpression(pluginArg)) {
|
|
1027
|
-
const callee = pluginArg.callee;
|
|
1028
|
-
if (t.isIdentifier(callee) && callee.name === "require") {
|
|
1029
|
-
const reqArg = pluginArg.arguments[0];
|
|
1030
|
-
if (t.isStringLiteral(reqArg)) {
|
|
1031
|
-
const spec = reqArg.value;
|
|
1032
|
-
|
|
1033
|
-
// Handle @fastify/autoload via require
|
|
1034
|
-
if (isAutoloadPlugin(spec)) {
|
|
1035
|
-
const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
|
|
1036
|
-
if (autoloadDir && existsDir(autoloadDir)) {
|
|
1037
|
-
scanAutoloadDir(autoloadDir, childPrefix);
|
|
1038
|
-
}
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
|
|
1043
|
-
|
|
1044
|
-
if (isNonRoute) return;
|
|
1045
|
-
|
|
1046
|
-
if (!resolved) {
|
|
1047
|
-
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
scanFile(resolved, childPrefix);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
},
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Extract the 'dir' option from autoload config
|
|
1062
|
-
* Handles: { dir: path.join(__dirname, 'routes') } or { dir: './routes' }
|
|
1063
|
-
*/
|
|
1064
|
-
function extractAutoloadDir(optsNode, fromFileAbs) {
|
|
1065
|
-
if (!t.isObjectExpression(optsNode)) return null;
|
|
1066
|
-
|
|
1067
|
-
for (const prop of optsNode.properties) {
|
|
1068
|
-
if (!t.isObjectProperty(prop)) continue;
|
|
1069
|
-
|
|
1070
|
-
const key = t.isIdentifier(prop.key) ? prop.key.name :
|
|
1071
|
-
t.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
1072
|
-
|
|
1073
|
-
if (key !== "dir") continue;
|
|
1074
|
-
|
|
1075
|
-
// Simple string: { dir: './routes' }
|
|
1076
|
-
if (t.isStringLiteral(prop.value)) {
|
|
1077
|
-
return path.resolve(path.dirname(fromFileAbs), prop.value.value);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// path.join(__dirname, 'routes')
|
|
1081
|
-
if (t.isCallExpression(prop.value)) {
|
|
1082
|
-
const callee = prop.value.callee;
|
|
1083
|
-
if (t.isMemberExpression(callee) &&
|
|
1084
|
-
t.isIdentifier(callee.object) && callee.object.name === "path" &&
|
|
1085
|
-
t.isIdentifier(callee.property) && callee.property.name === "join") {
|
|
1086
|
-
const args = prop.value.arguments;
|
|
1087
|
-
// path.join(__dirname, 'subdir')
|
|
1088
|
-
if (args.length >= 2 && t.isIdentifier(args[0]) && args[0].name === "__dirname") {
|
|
1089
|
-
const parts = args.slice(1)
|
|
1090
|
-
.filter(a => t.isStringLiteral(a))
|
|
1091
|
-
.map(a => a.value);
|
|
1092
|
-
if (parts.length > 0) {
|
|
1093
|
-
return path.resolve(path.dirname(fromFileAbs), ...parts);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
312
|
}
|
|
1097
313
|
}
|
|
1098
|
-
|
|
1099
|
-
// Template literal or identifier - can't resolve statically
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* Scan a directory for route files (used by @fastify/autoload)
|
|
1108
|
-
*/
|
|
1109
|
-
function scanAutoloadDir(dirAbs, prefix) {
|
|
1110
|
-
if (!existsDir(dirAbs)) return;
|
|
1111
|
-
|
|
1112
|
-
let entries;
|
|
1113
|
-
try {
|
|
1114
|
-
entries = fs.readdirSync(dirAbs, { withFileTypes: true });
|
|
1115
|
-
} catch {
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
for (const ent of entries) {
|
|
1120
|
-
const fullPath = path.join(dirAbs, ent.name);
|
|
1121
|
-
|
|
1122
|
-
if (ent.isDirectory()) {
|
|
1123
|
-
// Subdirectory: recurse with directory name as prefix segment
|
|
1124
|
-
// @fastify/autoload uses directory names as route prefixes
|
|
1125
|
-
const subPrefix = joinPrefix(prefix, "/" + ent.name);
|
|
1126
|
-
scanAutoloadDir(fullPath, subPrefix);
|
|
1127
|
-
} else if (ent.isFile() && /\.(ts|js)$/.test(ent.name) && !ent.name.endsWith(".d.ts")) {
|
|
1128
|
-
// Skip index files for prefix (they define routes at current prefix level)
|
|
1129
|
-
const isIndex = /^index\.(ts|js)$/.test(ent.name);
|
|
1130
|
-
const filePrefix = isIndex ? prefix : joinPrefix(prefix, "/" + ent.name.replace(/\.(ts|js)$/, ""));
|
|
1131
|
-
|
|
1132
|
-
scanFile(fullPath, filePrefix);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
314
|
+
} catch {}
|
|
1135
315
|
}
|
|
1136
|
-
|
|
1137
|
-
scanFile(entryAbs, "/");
|
|
316
|
+
|
|
1138
317
|
return { routes, gaps };
|
|
1139
318
|
}
|
|
1140
319
|
|
|
1141
|
-
async function resolveFastifyRoutes(repoRoot, entryRel = null) {
|
|
1142
|
-
const entry = entryRel || detectFastifyEntry(repoRoot);
|
|
1143
|
-
if (!entry) return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: null }] };
|
|
1144
|
-
|
|
1145
|
-
const entryAbs = path.isAbsolute(entry) ? entry : path.join(repoRoot, entry);
|
|
1146
|
-
if (!existsFile(entryAbs)) {
|
|
1147
|
-
return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: entry }] };
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
return resolveFastifyRoutesFromEntry(repoRoot, entryAbs);
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
320
|
// ============================================================================
|
|
1154
321
|
// ROUTE INDEX
|
|
1155
322
|
// ============================================================================
|
|
@@ -1162,88 +329,87 @@ class RouteIndex {
|
|
|
1162
329
|
this.parameterized = [];
|
|
1163
330
|
this.gaps = [];
|
|
1164
331
|
}
|
|
1165
|
-
|
|
1166
|
-
async build(repoRoot
|
|
332
|
+
|
|
333
|
+
async build(repoRoot) {
|
|
334
|
+
// Resolve Next.js routes
|
|
1167
335
|
const nextRoutes = await resolveNextRoutes(repoRoot);
|
|
1168
336
|
this.routes.push(...nextRoutes);
|
|
1169
|
-
|
|
1170
|
-
|
|
337
|
+
|
|
338
|
+
// Resolve Fastify routes
|
|
339
|
+
const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot);
|
|
1171
340
|
this.routes.push(...fastifyRoutes);
|
|
1172
|
-
this.gaps.push(...
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
if (!this.
|
|
1182
|
-
this.
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (isParameterizedPath(p)) this.parameterized.push(r);
|
|
341
|
+
this.gaps.push(...gaps);
|
|
342
|
+
|
|
343
|
+
// Build indexes
|
|
344
|
+
for (const route of this.routes) {
|
|
345
|
+
const methodKey = route.method;
|
|
346
|
+
if (!this.byMethod.has(methodKey)) this.byMethod.set(methodKey, []);
|
|
347
|
+
this.byMethod.get(methodKey).push(route);
|
|
348
|
+
|
|
349
|
+
const pathKey = route.path;
|
|
350
|
+
if (!this.byPath.has(pathKey)) this.byPath.set(pathKey, []);
|
|
351
|
+
this.byPath.get(pathKey).push(route);
|
|
352
|
+
|
|
353
|
+
if (isParameterizedPath(route.path)) {
|
|
354
|
+
this.parameterized.push(route);
|
|
355
|
+
}
|
|
1188
356
|
}
|
|
1189
|
-
|
|
357
|
+
|
|
1190
358
|
return this;
|
|
1191
359
|
}
|
|
1192
|
-
|
|
1193
|
-
findRoutes(method,
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
|
|
360
|
+
|
|
361
|
+
findRoutes(method, path) {
|
|
362
|
+
const canonicalMethod = canonicalizeMethod(method);
|
|
363
|
+
const canonicalPath = canonicalizePath(path);
|
|
1197
364
|
const matches = [];
|
|
1198
|
-
|
|
365
|
+
|
|
1199
366
|
// Exact path match
|
|
1200
|
-
|
|
1201
|
-
|
|
367
|
+
const pathMatches = this.byPath.get(canonicalPath) || [];
|
|
368
|
+
for (const route of pathMatches) {
|
|
369
|
+
if (matchMethod(route.method, canonicalMethod)) {
|
|
370
|
+
matches.push(route);
|
|
371
|
+
}
|
|
1202
372
|
}
|
|
1203
|
-
|
|
1204
|
-
// Wildcard method match
|
|
1205
|
-
|
|
1206
|
-
|
|
373
|
+
|
|
374
|
+
// Wildcard method match
|
|
375
|
+
const wildcardMethods = this.byMethod.get('*') || [];
|
|
376
|
+
for (const route of wildcardMethods) {
|
|
377
|
+
if (route.path === canonicalPath && !matches.includes(route)) {
|
|
378
|
+
matches.push(route);
|
|
379
|
+
}
|
|
1207
380
|
}
|
|
1208
|
-
|
|
381
|
+
|
|
1209
382
|
// Parameterized route match
|
|
1210
|
-
for (const
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1213
|
-
if (!matches.includes(r)) matches.push(r);
|
|
383
|
+
for (const route of this.parameterized) {
|
|
384
|
+
if (matchPath(route.path, canonicalPath) && matchMethod(route.method, canonicalMethod)) {
|
|
385
|
+
if (!matches.includes(route)) matches.push(route);
|
|
1214
386
|
}
|
|
1215
387
|
}
|
|
1216
|
-
|
|
388
|
+
|
|
1217
389
|
return matches;
|
|
1218
390
|
}
|
|
1219
|
-
|
|
1220
|
-
findClosestRoutes(
|
|
1221
|
-
const
|
|
1222
|
-
const pathParts =
|
|
1223
|
-
|
|
1224
|
-
const scored = this.routes.map(
|
|
1225
|
-
const routeParts =
|
|
391
|
+
|
|
392
|
+
findClosestRoutes(path, limit = 3) {
|
|
393
|
+
const canonicalPath = canonicalizePath(path);
|
|
394
|
+
const pathParts = canonicalPath.split('/').filter(Boolean);
|
|
395
|
+
|
|
396
|
+
const scored = this.routes.map(route => {
|
|
397
|
+
const routeParts = route.path.split('/').filter(Boolean);
|
|
1226
398
|
let score = 0;
|
|
1227
|
-
|
|
399
|
+
|
|
1228
400
|
for (let i = 0; i < Math.min(pathParts.length, routeParts.length); i++) {
|
|
1229
|
-
if (pathParts[i] === routeParts[i]
|
|
1230
|
-
|
|
1231
|
-
else
|
|
1232
|
-
else break;
|
|
401
|
+
if (pathParts[i] === routeParts[i] || routeParts[i].startsWith(':')) {
|
|
402
|
+
score++;
|
|
403
|
+
} else break;
|
|
1233
404
|
}
|
|
1234
|
-
|
|
1235
|
-
if (pathParts.length === routeParts.length) score += 0.
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
return { route: r, score };
|
|
405
|
+
|
|
406
|
+
if (pathParts.length === routeParts.length) score += 0.5;
|
|
407
|
+
return { route, score };
|
|
1239
408
|
});
|
|
1240
|
-
|
|
1241
|
-
return scored
|
|
1242
|
-
.sort((a, b) => b.score - a.score)
|
|
1243
|
-
.slice(0, Math.max(0, limit))
|
|
1244
|
-
.map((s) => s.route);
|
|
409
|
+
|
|
410
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(s => s.route);
|
|
1245
411
|
}
|
|
1246
|
-
|
|
412
|
+
|
|
1247
413
|
getRouteMap() {
|
|
1248
414
|
return {
|
|
1249
415
|
server: this.routes,
|
|
@@ -1261,53 +427,43 @@ class RouteIndex {
|
|
|
1261
427
|
async function validateRouteExists(claim, repoRoot, routeIndex) {
|
|
1262
428
|
const index = routeIndex || new RouteIndex();
|
|
1263
429
|
if (!routeIndex) await index.build(repoRoot);
|
|
1264
|
-
|
|
1265
|
-
const method = claim
|
|
1266
|
-
const routePath = claim
|
|
1267
|
-
|
|
1268
|
-
if (!routePath) {
|
|
1269
|
-
return {
|
|
1270
|
-
result: "unknown",
|
|
1271
|
-
confidence: "low",
|
|
1272
|
-
evidence: [],
|
|
1273
|
-
nextSteps: ["Claim missing path"],
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
|
|
430
|
+
|
|
431
|
+
const method = claim.method || '*';
|
|
432
|
+
const routePath = claim.path;
|
|
433
|
+
|
|
1277
434
|
const matches = index.findRoutes(method, routePath);
|
|
1278
|
-
|
|
435
|
+
|
|
1279
436
|
if (matches.length > 0) {
|
|
1280
|
-
const best = matches[0];
|
|
1281
437
|
return {
|
|
1282
|
-
result:
|
|
1283
|
-
confidence:
|
|
1284
|
-
evidence:
|
|
1285
|
-
matchedRoute:
|
|
438
|
+
result: 'true',
|
|
439
|
+
confidence: matches[0].confidence,
|
|
440
|
+
evidence: matches[0].evidence,
|
|
441
|
+
matchedRoute: matches[0],
|
|
1286
442
|
};
|
|
1287
443
|
}
|
|
1288
|
-
|
|
1289
|
-
const closest = index.findClosestRoutes(routePath
|
|
1290
|
-
const hasGaps =
|
|
1291
|
-
|
|
444
|
+
|
|
445
|
+
const closest = index.findClosestRoutes(routePath);
|
|
446
|
+
const hasGaps = index.gaps.length > 0;
|
|
447
|
+
|
|
1292
448
|
if (hasGaps) {
|
|
1293
449
|
return {
|
|
1294
|
-
result:
|
|
1295
|
-
confidence:
|
|
450
|
+
result: 'unknown',
|
|
451
|
+
confidence: 'low',
|
|
1296
452
|
evidence: [],
|
|
1297
453
|
closestRoutes: closest,
|
|
1298
454
|
gaps: index.gaps,
|
|
1299
|
-
nextSteps: [
|
|
455
|
+
nextSteps: ['Some routes may not be detected due to unresolved plugins'],
|
|
1300
456
|
};
|
|
1301
457
|
}
|
|
1302
|
-
|
|
458
|
+
|
|
1303
459
|
return {
|
|
1304
|
-
result:
|
|
1305
|
-
confidence:
|
|
460
|
+
result: 'false',
|
|
461
|
+
confidence: 'high',
|
|
1306
462
|
evidence: [],
|
|
1307
463
|
closestRoutes: closest,
|
|
1308
|
-
nextSteps: closest.length
|
|
1309
|
-
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(
|
|
1310
|
-
: [
|
|
464
|
+
nextSteps: closest.length > 0
|
|
465
|
+
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(', ')}?`]
|
|
466
|
+
: ['No similar routes found'],
|
|
1311
467
|
};
|
|
1312
468
|
}
|
|
1313
469
|
|
|
@@ -1316,7 +472,6 @@ module.exports = {
|
|
|
1316
472
|
canonicalizeMethod,
|
|
1317
473
|
resolveNextRoutes,
|
|
1318
474
|
resolveFastifyRoutes,
|
|
1319
|
-
detectFastifyEntry,
|
|
1320
475
|
RouteIndex,
|
|
1321
476
|
validateRouteExists,
|
|
1322
477
|
};
|