@vibecheckai/cli 3.2.0 → 3.2.2
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/runners/lib/agent-firewall/change-packet/builder.js +214 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/analysis-core.js +198 -180
- package/bin/runners/lib/analyzers.js +1119 -536
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +144 -738
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/truth.js +1004 -321
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runScan.js +113 -10
- package/bin/runners/runShip.js +7 -8
- package/bin/runners/runTruth.js +89 -0
- package/mcp-server/agent-firewall-interceptor.js +164 -0
- package/mcp-server/index.js +347 -313
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1412 -1045
- package/package.json +1 -1
package/bin/runners/lib/truth.js
CHANGED
|
@@ -18,6 +18,19 @@ const { buildEnforcementTruth } = require("./enforcement");
|
|
|
18
18
|
// Multi-framework route detection v2
|
|
19
19
|
const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
|
|
20
20
|
|
|
21
|
+
// ---------- constants ----------
|
|
22
|
+
const IGNORE_GLOBS = [
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/.next/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/.turbo/**",
|
|
28
|
+
"**/.git/**",
|
|
29
|
+
"**/.vibecheck/**",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const CODE_FILE_GLOBS = ["**/*.{ts,tsx,js,jsx}"];
|
|
33
|
+
|
|
21
34
|
// ---------- helpers ----------
|
|
22
35
|
function sha256(text) {
|
|
23
36
|
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
@@ -29,15 +42,43 @@ function canonicalizeMethod(m) {
|
|
|
29
42
|
return u;
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
function stripQueryHash(s) {
|
|
46
|
+
const v = String(s || "");
|
|
47
|
+
const q = v.indexOf("?");
|
|
48
|
+
const h = v.indexOf("#");
|
|
49
|
+
const cut = (q === -1 ? h : (h === -1 ? q : Math.min(q, h)));
|
|
50
|
+
return cut === -1 ? v : v.slice(0, cut);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonical path rules:
|
|
55
|
+
* - ensure leading slash
|
|
56
|
+
* - collapse multiple slashes
|
|
57
|
+
* - strip query/hash
|
|
58
|
+
* - normalize Next dynamic segments:
|
|
59
|
+
* [[...slug]] -> *slug?
|
|
60
|
+
* [...slug] -> *slug
|
|
61
|
+
* [id] -> :id
|
|
62
|
+
*/
|
|
32
63
|
function canonicalizePath(p) {
|
|
33
|
-
let s = String(p || "").trim();
|
|
64
|
+
let s = stripQueryHash(String(p || "").trim());
|
|
65
|
+
|
|
66
|
+
// If someone passed a full URL, only keep pathname-like portion if possible.
|
|
67
|
+
// (We still require local routes to start with "/" for client refs.)
|
|
68
|
+
const protoIdx = s.indexOf("://");
|
|
69
|
+
if (protoIdx !== -1) {
|
|
70
|
+
// attempt to strip scheme+host
|
|
71
|
+
const slashAfterHost = s.indexOf("/", protoIdx + 3);
|
|
72
|
+
s = slashAfterHost === -1 ? "/" : s.slice(slashAfterHost);
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
if (!s.startsWith("/")) s = "/" + s;
|
|
35
76
|
s = s.replace(/\/+/g, "/");
|
|
36
77
|
|
|
37
|
-
// Next dynamic segments
|
|
78
|
+
// Next dynamic segments (filesystem style)
|
|
38
79
|
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
39
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1");
|
|
40
|
-
s = s.replace(/\[([^\]]+)\]/g, ":$1");
|
|
80
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
81
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
41
82
|
|
|
42
83
|
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
43
84
|
return s;
|
|
@@ -51,12 +92,46 @@ function joinPaths(prefix, p) {
|
|
|
51
92
|
return canonicalizePath(a + "/" + b);
|
|
52
93
|
}
|
|
53
94
|
|
|
54
|
-
function parseFile(code) {
|
|
55
|
-
|
|
95
|
+
function parseFile(code, fileAbsForErrors) {
|
|
96
|
+
// Be permissive: production repos contain decorators, top-level await, etc.
|
|
97
|
+
return parser.parse(code, {
|
|
98
|
+
sourceType: "unambiguous",
|
|
99
|
+
errorRecovery: true,
|
|
100
|
+
allowImportExportEverywhere: true,
|
|
101
|
+
plugins: [
|
|
102
|
+
"typescript",
|
|
103
|
+
"jsx",
|
|
104
|
+
"dynamicImport",
|
|
105
|
+
"importMeta",
|
|
106
|
+
"topLevelAwait",
|
|
107
|
+
"classProperties",
|
|
108
|
+
"classPrivateProperties",
|
|
109
|
+
"classPrivateMethods",
|
|
110
|
+
"optionalChaining",
|
|
111
|
+
"nullishCoalescingOperator",
|
|
112
|
+
"decorators-legacy",
|
|
113
|
+
],
|
|
114
|
+
sourceFilename: fileAbsForErrors || undefined,
|
|
115
|
+
});
|
|
56
116
|
}
|
|
57
117
|
|
|
118
|
+
// File cache for performance (avoids reading the same file multiple times)
|
|
119
|
+
const _FILE_CACHE = new Map();
|
|
120
|
+
|
|
58
121
|
function safeRead(fileAbs) {
|
|
59
|
-
return
|
|
122
|
+
if (_FILE_CACHE.has(fileAbs)) return _FILE_CACHE.get(fileAbs);
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(fileAbs, "utf8");
|
|
125
|
+
_FILE_CACHE.set(fileAbs, content);
|
|
126
|
+
return content;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clear cache to free memory after a scan (important for long-running processes)
|
|
133
|
+
function clearCache() {
|
|
134
|
+
_FILE_CACHE.clear();
|
|
60
135
|
}
|
|
61
136
|
|
|
62
137
|
function ensureDir(p) {
|
|
@@ -65,68 +140,154 @@ function ensureDir(p) {
|
|
|
65
140
|
|
|
66
141
|
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
67
142
|
if (!loc) return null;
|
|
68
|
-
const
|
|
143
|
+
const code = safeRead(fileAbs);
|
|
144
|
+
if (!code) return null;
|
|
145
|
+
|
|
146
|
+
const lines = code.split(/\r?\n/);
|
|
69
147
|
const start = Math.max(1, loc.start?.line || 1);
|
|
70
148
|
const end = Math.max(start, loc.end?.line || start);
|
|
71
149
|
const snippet = lines.slice(start - 1, end).join("\n");
|
|
150
|
+
|
|
72
151
|
return {
|
|
73
152
|
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
74
153
|
file: fileRel,
|
|
75
154
|
lines: `${start}-${end}`,
|
|
76
155
|
snippetHash: sha256(snippet),
|
|
77
|
-
reason
|
|
156
|
+
reason,
|
|
78
157
|
};
|
|
79
158
|
}
|
|
80
159
|
|
|
160
|
+
function normalizeRel(repoRoot, fileAbs) {
|
|
161
|
+
return path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scoreConfidence(c) {
|
|
165
|
+
if (c === "high") return 3;
|
|
166
|
+
if (c === "med") return 2;
|
|
167
|
+
if (c === "low") return 1;
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isRouteGroupSegment(seg) {
|
|
172
|
+
// Next route group: (group)
|
|
173
|
+
return seg.startsWith("(") && seg.endsWith(")");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isParallelSegment(seg) {
|
|
177
|
+
// Next parallel routes: @slot
|
|
178
|
+
return seg.startsWith("@");
|
|
179
|
+
}
|
|
180
|
+
|
|
81
181
|
// ---------- Next: app router API ----------
|
|
82
|
-
const HTTP_EXPORTS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
|
|
182
|
+
const HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
|
|
183
|
+
|
|
184
|
+
function nextAppApiPathFromRel(fileRel) {
|
|
185
|
+
const idx = fileRel.indexOf("app/api/");
|
|
186
|
+
if (idx === -1) return null;
|
|
187
|
+
|
|
188
|
+
let sub = fileRel.slice(idx + "app/api/".length);
|
|
189
|
+
|
|
190
|
+
// route.ts / route.js / route.tsx / route.jsx
|
|
191
|
+
sub = sub.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
192
|
+
|
|
193
|
+
// remove route groups + parallel segments from the filesystem path
|
|
194
|
+
const parts = sub.split("/").filter(Boolean).filter((seg) => !isRouteGroupSegment(seg) && !isParallelSegment(seg));
|
|
195
|
+
sub = parts.join("/");
|
|
196
|
+
|
|
197
|
+
return canonicalizePath("/api/" + sub);
|
|
198
|
+
}
|
|
83
199
|
|
|
84
|
-
async function resolveNextAppApiRoutes(repoRoot) {
|
|
85
|
-
const files = await fg(["**/app/api/**/route.@(ts|js)"], {
|
|
200
|
+
async function resolveNextAppApiRoutes(repoRoot, stats) {
|
|
201
|
+
const files = await fg(["**/app/api/**/route.@(ts|tsx|js|jsx)"], {
|
|
86
202
|
cwd: repoRoot,
|
|
87
203
|
absolute: true,
|
|
88
|
-
ignore:
|
|
204
|
+
ignore: IGNORE_GLOBS,
|
|
89
205
|
});
|
|
90
206
|
|
|
91
207
|
const out = [];
|
|
92
208
|
|
|
93
209
|
for (const fileAbs of files) {
|
|
94
|
-
const fileRel =
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const routePath = canonicalizePath("/api/" + sub);
|
|
210
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
211
|
+
const routePath = nextAppApiPathFromRel(fileRel);
|
|
212
|
+
if (!routePath) continue;
|
|
98
213
|
|
|
99
214
|
const code = safeRead(fileAbs);
|
|
215
|
+
if (!code) continue;
|
|
216
|
+
|
|
100
217
|
let ast;
|
|
101
|
-
try {
|
|
218
|
+
try {
|
|
219
|
+
ast = parseFile(code, fileAbs);
|
|
220
|
+
} catch {
|
|
221
|
+
stats.parseErrors++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
102
224
|
|
|
103
225
|
const methods = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
traverse(ast, {
|
|
229
|
+
// export async function GET() {}
|
|
230
|
+
ExportNamedDeclaration(p) {
|
|
231
|
+
const decl = p.node.declaration;
|
|
232
|
+
|
|
233
|
+
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
234
|
+
const n = decl.id.name.toUpperCase();
|
|
235
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc, why: "export function" });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// export const GET = async () => {}
|
|
239
|
+
if (t.isVariableDeclaration(decl)) {
|
|
240
|
+
for (const d of decl.declarations) {
|
|
241
|
+
if (!t.isVariableDeclarator(d)) continue;
|
|
242
|
+
if (!t.isIdentifier(d.id)) continue;
|
|
243
|
+
const n = d.id.name.toUpperCase();
|
|
244
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: d.loc || decl.loc, why: "export const" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// export { GET } from "./handler"
|
|
249
|
+
for (const s of p.node.specifiers || []) {
|
|
250
|
+
if (!t.isExportSpecifier(s)) continue;
|
|
251
|
+
if (!t.isIdentifier(s.exported)) continue;
|
|
252
|
+
const n = s.exported.name.toUpperCase();
|
|
253
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: s.loc || p.node.loc, why: "export re-export" });
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
// Babel traverse can fail on some edge-case files; skip them
|
|
259
|
+
stats.parseErrors++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
113
262
|
|
|
114
263
|
if (methods.length === 0) {
|
|
115
|
-
|
|
264
|
+
// Still include route.ts, but with "*" and low confidence to avoid missing-route spam.
|
|
265
|
+
out.push({
|
|
266
|
+
method: "*",
|
|
267
|
+
path: routePath,
|
|
268
|
+
handler: fileRel,
|
|
269
|
+
confidence: "low",
|
|
270
|
+
framework: "next",
|
|
271
|
+
evidence: [],
|
|
272
|
+
});
|
|
116
273
|
continue;
|
|
117
274
|
}
|
|
118
275
|
|
|
119
276
|
for (const m of methods) {
|
|
120
277
|
const ev = evidenceFromLoc({
|
|
121
|
-
fileAbs,
|
|
122
|
-
|
|
278
|
+
fileAbs,
|
|
279
|
+
fileRel,
|
|
280
|
+
loc: m.loc,
|
|
281
|
+
reason: `Next app router ${m.method} (${m.why})`,
|
|
123
282
|
});
|
|
283
|
+
|
|
124
284
|
out.push({
|
|
125
285
|
method: m.method,
|
|
126
286
|
path: routePath,
|
|
127
287
|
handler: fileRel,
|
|
128
|
-
confidence: "high",
|
|
129
|
-
|
|
288
|
+
confidence: m.why === "export re-export" ? "med" : "high",
|
|
289
|
+
framework: "next",
|
|
290
|
+
evidence: ev ? [ev] : [],
|
|
130
291
|
});
|
|
131
292
|
}
|
|
132
293
|
}
|
|
@@ -135,51 +296,133 @@ async function resolveNextAppApiRoutes(repoRoot) {
|
|
|
135
296
|
}
|
|
136
297
|
|
|
137
298
|
// ---------- Next: pages router API ----------
|
|
138
|
-
|
|
139
|
-
const
|
|
299
|
+
function nextPagesApiPathFromRel(fileRel) {
|
|
300
|
+
const idx = fileRel.indexOf("pages/api/");
|
|
301
|
+
if (idx === -1) return null;
|
|
302
|
+
|
|
303
|
+
let sub = fileRel.slice(idx + "pages/api/".length);
|
|
304
|
+
sub = sub.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
305
|
+
|
|
306
|
+
// pages/api/foo/index.ts -> /api/foo
|
|
307
|
+
if (sub === "index") sub = "";
|
|
308
|
+
sub = sub.replace(/\/index$/, "");
|
|
309
|
+
|
|
310
|
+
return canonicalizePath("/api/" + sub);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function resolveNextPagesApiRoutes(repoRoot, stats) {
|
|
314
|
+
const files = await fg(["**/pages/api/**/*.@(ts|tsx|js|jsx)"], {
|
|
140
315
|
cwd: repoRoot,
|
|
141
316
|
absolute: true,
|
|
142
|
-
ignore:
|
|
317
|
+
ignore: IGNORE_GLOBS,
|
|
143
318
|
});
|
|
144
319
|
|
|
145
320
|
const out = [];
|
|
321
|
+
|
|
146
322
|
for (const fileAbs of files) {
|
|
147
|
-
const fileRel =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
323
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
324
|
+
|
|
325
|
+
// Skip Next.js special files that aren't API routes (_app, _document, _utils, etc.)
|
|
326
|
+
if (fileRel.includes("/_") && !fileRel.includes("/_next")) continue;
|
|
327
|
+
|
|
328
|
+
const routePath = nextPagesApiPathFromRel(fileRel);
|
|
329
|
+
if (!routePath) continue;
|
|
330
|
+
|
|
331
|
+
const code = safeRead(fileAbs);
|
|
332
|
+
if (!code) continue;
|
|
333
|
+
|
|
334
|
+
// Parse to verify it's actually a route (has export default)
|
|
335
|
+
let ast;
|
|
336
|
+
try {
|
|
337
|
+
ast = parseFile(code, fileAbs);
|
|
338
|
+
} catch {
|
|
339
|
+
stats.parseErrors++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for 'export default' (Required for Pages Router API routes)
|
|
344
|
+
// Files without default export are helper files (db.ts, types.ts, utils.ts)
|
|
345
|
+
let hasDefaultExport = false;
|
|
346
|
+
try {
|
|
347
|
+
traverse(ast, {
|
|
348
|
+
ExportDefaultDeclaration(p) {
|
|
349
|
+
hasDefaultExport = true;
|
|
350
|
+
p.stop(); // Found it, stop traversing
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
// Traverse failed, skip this file
|
|
355
|
+
stats.parseErrors++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!hasDefaultExport) continue; // It's a helper file, not an API route
|
|
151
360
|
|
|
152
361
|
out.push({
|
|
153
|
-
method: "*",
|
|
362
|
+
method: "*", // Pages router handles all methods in one function
|
|
154
363
|
path: routePath,
|
|
155
364
|
handler: fileRel,
|
|
156
365
|
confidence: "med",
|
|
157
|
-
|
|
366
|
+
framework: "next",
|
|
367
|
+
evidence: [],
|
|
158
368
|
});
|
|
159
369
|
}
|
|
370
|
+
|
|
160
371
|
return out;
|
|
161
372
|
}
|
|
162
373
|
|
|
163
374
|
// ---------- minimal relative module resolver ----------
|
|
164
375
|
function exists(p) {
|
|
165
|
-
try {
|
|
376
|
+
try {
|
|
377
|
+
return fs.statSync(p).isFile();
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
166
381
|
}
|
|
382
|
+
|
|
167
383
|
function resolveRelativeModule(fromFileAbs, spec) {
|
|
168
384
|
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
385
|
+
|
|
169
386
|
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
170
387
|
const candidates = [
|
|
171
388
|
base,
|
|
172
389
|
base + ".ts",
|
|
390
|
+
base + ".tsx",
|
|
173
391
|
base + ".js",
|
|
392
|
+
base + ".jsx",
|
|
174
393
|
path.join(base, "index.ts"),
|
|
175
|
-
path.join(base, "index.
|
|
394
|
+
path.join(base, "index.tsx"),
|
|
395
|
+
path.join(base, "index.js"),
|
|
396
|
+
path.join(base, "index.jsx"),
|
|
176
397
|
];
|
|
398
|
+
|
|
177
399
|
for (const c of candidates) if (exists(c)) return c;
|
|
178
400
|
return null;
|
|
179
401
|
}
|
|
180
402
|
|
|
403
|
+
function extractRequireOrImportSpec(node) {
|
|
404
|
+
// require("./x")
|
|
405
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
|
|
406
|
+
const a0 = node.arguments && node.arguments[0];
|
|
407
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// import("./x")
|
|
411
|
+
if (t.isCallExpression(node) && node.callee && node.callee.type === "Import") {
|
|
412
|
+
const a0 = node.arguments && node.arguments[0];
|
|
413
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// await import("./x")
|
|
417
|
+
if (t.isAwaitExpression(node)) {
|
|
418
|
+
return extractRequireOrImportSpec(node.argument);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
181
424
|
// ---------- Fastify route extraction ----------
|
|
182
|
-
const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
|
|
425
|
+
const FASTIFY_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
183
426
|
|
|
184
427
|
function isFastifyMethod(name) {
|
|
185
428
|
return FASTIFY_METHODS.has(name);
|
|
@@ -222,18 +465,18 @@ function extractRouteObject(objExpr) {
|
|
|
222
465
|
if (key === "method") {
|
|
223
466
|
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
224
467
|
if (t.isArrayExpression(p.value)) {
|
|
225
|
-
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
468
|
+
methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
|
|
226
469
|
}
|
|
227
470
|
}
|
|
228
471
|
|
|
229
472
|
if (key === "handler") hasHandler = true;
|
|
230
|
-
if (["preHandler","onRequest","preValidation","preSerialization"].includes(key)) hooks.push(key);
|
|
473
|
+
if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
|
|
231
474
|
}
|
|
232
475
|
|
|
233
476
|
return { url, methods, hasHandler, hooks };
|
|
234
477
|
}
|
|
235
478
|
|
|
236
|
-
function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
479
|
+
function resolveFastifyRoutes(repoRoot, entryAbs, stats) {
|
|
237
480
|
const seen = new Set();
|
|
238
481
|
const routes = [];
|
|
239
482
|
const gaps = [];
|
|
@@ -242,217 +485,392 @@ function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
|
242
485
|
if (!fileAbs || seen.has(fileAbs)) return;
|
|
243
486
|
seen.add(fileAbs);
|
|
244
487
|
|
|
245
|
-
const fileRel =
|
|
488
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
246
489
|
const code = safeRead(fileAbs);
|
|
490
|
+
if (!code) return;
|
|
247
491
|
|
|
248
492
|
let ast;
|
|
249
|
-
try {
|
|
493
|
+
try {
|
|
494
|
+
ast = parseFile(code, fileAbs);
|
|
495
|
+
} catch {
|
|
496
|
+
stats.parseErrors++;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
250
499
|
|
|
251
500
|
// best-effort: fastify instance identifiers
|
|
252
501
|
const fastifyNames = new Set(["fastify"]);
|
|
253
502
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
503
|
+
try {
|
|
504
|
+
traverse(ast, {
|
|
505
|
+
VariableDeclarator(p) {
|
|
506
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
507
|
+
const id = p.node.id.name;
|
|
508
|
+
const init = p.node.init;
|
|
509
|
+
if (!init) return;
|
|
510
|
+
|
|
511
|
+
// const app = Fastify()
|
|
512
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
513
|
+
const cal = init.callee.name;
|
|
514
|
+
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// const app = require("fastify")()
|
|
518
|
+
if (t.isCallExpression(init) && t.isCallExpression(init.callee)) {
|
|
519
|
+
const inner = init.callee;
|
|
520
|
+
if (t.isIdentifier(inner.callee, { name: "require" }) && t.isStringLiteral(inner.arguments?.[0], { value: "fastify" })) {
|
|
521
|
+
fastifyNames.add(id);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
} catch {
|
|
527
|
+
// Babel traverse can fail on some edge-case files; skip this step
|
|
528
|
+
}
|
|
266
529
|
|
|
267
530
|
// helper: resolve imports for register(pluginIdent,...)
|
|
268
531
|
function resolveImportSpecForLocal(localName) {
|
|
269
532
|
let spec = null;
|
|
270
533
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
534
|
+
try {
|
|
535
|
+
traverse(ast, {
|
|
536
|
+
ImportDeclaration(ip) {
|
|
537
|
+
for (const s of ip.node.specifiers) {
|
|
538
|
+
if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
|
|
539
|
+
spec = ip.node.source.value;
|
|
540
|
+
}
|
|
276
541
|
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
}
|
|
542
|
+
},
|
|
543
|
+
VariableDeclarator(vp) {
|
|
544
|
+
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
545
|
+
const init = vp.node.init;
|
|
546
|
+
if (!t.isCallExpression(init)) return;
|
|
547
|
+
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
548
|
+
const a0 = init.arguments[0];
|
|
549
|
+
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
} catch {
|
|
553
|
+
// Babel traverse can fail; ignore
|
|
554
|
+
}
|
|
288
555
|
|
|
289
556
|
return spec;
|
|
290
557
|
}
|
|
291
558
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
handler: fileRel,
|
|
320
|
-
confidence: "med",
|
|
321
|
-
evidence: ev ? [ev] : []
|
|
322
|
-
});
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// fastify.route({ method, url, handler })
|
|
327
|
-
if (prop === "route") {
|
|
328
|
-
const arg0 = p.node.arguments[0];
|
|
329
|
-
if (!t.isObjectExpression(arg0)) return;
|
|
330
|
-
|
|
331
|
-
const r = extractRouteObject(arg0);
|
|
332
|
-
if (!r.url) return;
|
|
333
|
-
|
|
334
|
-
const fullPath = joinPaths(prefix, r.url);
|
|
335
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
336
|
-
|
|
337
|
-
const ev = evidenceFromLoc({
|
|
338
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
339
|
-
reason: `Fastify.route({ url: "${r.url}" })`
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
for (const m of ms) {
|
|
559
|
+
try {
|
|
560
|
+
traverse(ast, {
|
|
561
|
+
CallExpression(p) {
|
|
562
|
+
const callee = p.node.callee;
|
|
563
|
+
if (!t.isMemberExpression(callee)) return;
|
|
564
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
565
|
+
|
|
566
|
+
const obj = callee.object.name;
|
|
567
|
+
const prop = callee.property.name;
|
|
568
|
+
|
|
569
|
+
if (!fastifyNames.has(obj)) return;
|
|
570
|
+
|
|
571
|
+
// fastify.get('/x', ...)
|
|
572
|
+
if (isFastifyMethod(prop)) {
|
|
573
|
+
const routeStr = extractStringLiteral(p.node.arguments[0]);
|
|
574
|
+
if (!routeStr) return;
|
|
575
|
+
|
|
576
|
+
const fullPath = joinPaths(prefix, routeStr);
|
|
577
|
+
const method = canonicalizeMethod(prop);
|
|
578
|
+
|
|
579
|
+
const ev = evidenceFromLoc({
|
|
580
|
+
fileAbs,
|
|
581
|
+
fileRel,
|
|
582
|
+
loc: p.node.loc,
|
|
583
|
+
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
|
|
584
|
+
});
|
|
585
|
+
|
|
343
586
|
routes.push({
|
|
344
|
-
method
|
|
587
|
+
method,
|
|
345
588
|
path: fullPath,
|
|
346
589
|
handler: fileRel,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
evidence: ev ? [ev] : []
|
|
590
|
+
confidence: "med",
|
|
591
|
+
framework: "fastify",
|
|
592
|
+
evidence: ev ? [ev] : [],
|
|
350
593
|
});
|
|
594
|
+
return;
|
|
351
595
|
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// fastify.register(plugin, { prefix })
|
|
356
|
-
if (prop === "register") {
|
|
357
|
-
const pluginArg = p.node.arguments[0];
|
|
358
|
-
const optsArg = p.node.arguments[1];
|
|
359
|
-
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
360
|
-
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
361
|
-
|
|
362
|
-
// inline plugin
|
|
363
|
-
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
364
|
-
const param0 = pluginArg.params[0];
|
|
365
|
-
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
366
|
-
|
|
367
|
-
// traverse just the plugin body (best effort)
|
|
368
|
-
traverse(pluginArg.body, {
|
|
369
|
-
CallExpression(pp) {
|
|
370
|
-
const c = pp.node.callee;
|
|
371
|
-
if (!t.isMemberExpression(c)) return;
|
|
372
|
-
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
373
|
-
if (c.object.name !== innerName) return;
|
|
374
|
-
|
|
375
|
-
const pr = c.property.name;
|
|
376
|
-
|
|
377
|
-
if (isFastifyMethod(pr)) {
|
|
378
|
-
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
379
|
-
if (!rs) return;
|
|
380
|
-
const fullPath = joinPaths(childPrefix, rs);
|
|
381
|
-
const method = canonicalizeMethod(pr);
|
|
382
|
-
|
|
383
|
-
const ev = evidenceFromLoc({
|
|
384
|
-
fileAbs, fileRel, loc: pp.node.loc,
|
|
385
|
-
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
routes.push({ method, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
389
|
-
}
|
|
390
596
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (!r.url) return;
|
|
396
|
-
const fullPath = joinPaths(childPrefix, r.url);
|
|
397
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
597
|
+
// fastify.route({ method, url, handler })
|
|
598
|
+
if (prop === "route") {
|
|
599
|
+
const arg0 = p.node.arguments[0];
|
|
600
|
+
if (!t.isObjectExpression(arg0)) return;
|
|
398
601
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`
|
|
402
|
-
});
|
|
602
|
+
const r = extractRouteObject(arg0);
|
|
603
|
+
if (!r.url) return;
|
|
403
604
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
605
|
+
const fullPath = joinPaths(prefix, r.url);
|
|
606
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
607
|
+
|
|
608
|
+
const ev = evidenceFromLoc({
|
|
609
|
+
fileAbs,
|
|
610
|
+
fileRel,
|
|
611
|
+
loc: p.node.loc,
|
|
612
|
+
reason: `Fastify.route({ url: "${r.url}" })`,
|
|
613
|
+
});
|
|
408
614
|
|
|
615
|
+
for (const m of ms) {
|
|
616
|
+
routes.push({
|
|
617
|
+
method: m,
|
|
618
|
+
path: fullPath,
|
|
619
|
+
handler: fileRel,
|
|
620
|
+
hooks: r.hooks,
|
|
621
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
622
|
+
framework: "fastify",
|
|
623
|
+
evidence: ev ? [ev] : [],
|
|
624
|
+
});
|
|
625
|
+
}
|
|
409
626
|
return;
|
|
410
627
|
}
|
|
411
628
|
|
|
412
|
-
//
|
|
413
|
-
if (
|
|
414
|
-
const
|
|
415
|
-
const
|
|
629
|
+
// fastify.register(plugin, { prefix })
|
|
630
|
+
if (prop === "register") {
|
|
631
|
+
const pluginArgRaw = p.node.arguments[0];
|
|
632
|
+
const optsArg = p.node.arguments[1];
|
|
633
|
+
|
|
634
|
+
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
635
|
+
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
636
|
+
|
|
637
|
+
// inline plugin
|
|
638
|
+
if (t.isFunctionExpression(pluginArgRaw) || t.isArrowFunctionExpression(pluginArgRaw)) {
|
|
639
|
+
const param0 = pluginArgRaw.params[0];
|
|
640
|
+
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
641
|
+
|
|
642
|
+
// traverse just the plugin body (best effort)
|
|
643
|
+
try {
|
|
644
|
+
traverse(
|
|
645
|
+
pluginArgRaw.body,
|
|
646
|
+
{
|
|
647
|
+
CallExpression(pp) {
|
|
648
|
+
const c = pp.node.callee;
|
|
649
|
+
if (!t.isMemberExpression(c)) return;
|
|
650
|
+
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
651
|
+
if (c.object.name !== innerName) return;
|
|
652
|
+
|
|
653
|
+
const pr = c.property.name;
|
|
654
|
+
|
|
655
|
+
if (isFastifyMethod(pr)) {
|
|
656
|
+
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
657
|
+
if (!rs) return;
|
|
658
|
+
|
|
659
|
+
const fullPath = joinPaths(childPrefix, rs);
|
|
660
|
+
const method = canonicalizeMethod(pr);
|
|
661
|
+
|
|
662
|
+
const ev = evidenceFromLoc({
|
|
663
|
+
fileAbs,
|
|
664
|
+
fileRel,
|
|
665
|
+
loc: pp.node.loc,
|
|
666
|
+
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
routes.push({
|
|
670
|
+
method,
|
|
671
|
+
path: fullPath,
|
|
672
|
+
handler: fileRel,
|
|
673
|
+
confidence: "med",
|
|
674
|
+
framework: "fastify",
|
|
675
|
+
evidence: ev ? [ev] : [],
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (pr === "route") {
|
|
680
|
+
const a0 = pp.node.arguments[0];
|
|
681
|
+
if (!t.isObjectExpression(a0)) return;
|
|
682
|
+
|
|
683
|
+
const r = extractRouteObject(a0);
|
|
684
|
+
if (!r.url) return;
|
|
685
|
+
|
|
686
|
+
const fullPath = joinPaths(childPrefix, r.url);
|
|
687
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
688
|
+
|
|
689
|
+
const ev = evidenceFromLoc({
|
|
690
|
+
fileAbs,
|
|
691
|
+
fileRel,
|
|
692
|
+
loc: pp.node.loc,
|
|
693
|
+
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
for (const m of ms) {
|
|
697
|
+
routes.push({
|
|
698
|
+
method: m,
|
|
699
|
+
path: fullPath,
|
|
700
|
+
handler: fileRel,
|
|
701
|
+
confidence: "med",
|
|
702
|
+
framework: "fastify",
|
|
703
|
+
evidence: ev ? [ev] : [],
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
p.scope,
|
|
710
|
+
p
|
|
711
|
+
);
|
|
712
|
+
} catch {
|
|
713
|
+
// Inner traverse can fail; skip this plugin body
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
416
718
|
|
|
417
|
-
|
|
418
|
-
|
|
719
|
+
// Resolve dynamic require/import spec directly (fastify.register(require("./x")) / import("./x"))
|
|
720
|
+
const dynSpec = extractRequireOrImportSpec(pluginArgRaw);
|
|
721
|
+
if (dynSpec) {
|
|
722
|
+
const resolved = resolveRelativeModule(fileAbs, dynSpec);
|
|
723
|
+
if (!resolved) {
|
|
724
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec: dynSpec });
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
scanFile(resolved, childPrefix);
|
|
419
728
|
return;
|
|
420
729
|
}
|
|
421
730
|
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
|
|
731
|
+
// imported plugin identifier
|
|
732
|
+
if (t.isIdentifier(pluginArgRaw)) {
|
|
733
|
+
const localName = pluginArgRaw.name;
|
|
734
|
+
const spec = resolveImportSpecForLocal(localName);
|
|
735
|
+
|
|
736
|
+
if (!spec) {
|
|
737
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const resolved = resolveRelativeModule(fileAbs, spec);
|
|
742
|
+
if (!resolved) {
|
|
743
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
scanFile(resolved, childPrefix);
|
|
425
748
|
return;
|
|
426
749
|
}
|
|
427
750
|
|
|
428
|
-
|
|
751
|
+
// Anything else: unknown plugin shape. Mark a gap so analyzers can be lenient.
|
|
752
|
+
gaps.push({
|
|
753
|
+
kind: "fastify_plugin_unresolved",
|
|
754
|
+
file: fileRel,
|
|
755
|
+
note: "register() plugin not statically resolvable",
|
|
756
|
+
});
|
|
429
757
|
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
} catch {
|
|
761
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
762
|
+
stats.parseErrors++;
|
|
763
|
+
}
|
|
433
764
|
}
|
|
434
765
|
|
|
435
766
|
scanFile(entryAbs, "/");
|
|
436
767
|
return { routes, gaps };
|
|
437
768
|
}
|
|
438
769
|
|
|
439
|
-
// ---------- client refs (fetch + axios
|
|
770
|
+
// ---------- client refs (fetch + axios + template literals best-effort) ----------
|
|
440
771
|
function isAxiosMember(node) {
|
|
441
|
-
return
|
|
772
|
+
return (
|
|
773
|
+
t.isMemberExpression(node) &&
|
|
442
774
|
t.isIdentifier(node.object) &&
|
|
443
775
|
t.isIdentifier(node.property) &&
|
|
444
|
-
["get","post","put","patch","delete"].includes(node.property.name)
|
|
776
|
+
["get", "post", "put", "patch", "delete"].includes(node.property.name)
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function isAxiosCallee(node) {
|
|
781
|
+
return t.isIdentifier(node, { name: "axios" }) || isAxiosMember(node);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function extractUrlLike(node) {
|
|
785
|
+
// "literal"
|
|
786
|
+
if (t.isStringLiteral(node)) return { url: node.value, confidence: "high", note: "string" };
|
|
787
|
+
|
|
788
|
+
// `literal`
|
|
789
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
790
|
+
return { url: node.quasis.map((q) => q.value.cooked || "").join(""), confidence: "high", note: "template_static" };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// `/api/x/${id}` -> "/api/x/:id" (med confidence)
|
|
794
|
+
if (t.isTemplateLiteral(node) && node.quasis.length >= 1) {
|
|
795
|
+
const start = node.quasis[0]?.value?.cooked || "";
|
|
796
|
+
if (!start.startsWith("/")) return null;
|
|
797
|
+
|
|
798
|
+
let built = "";
|
|
799
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
800
|
+
built += node.quasis[i].value.cooked || "";
|
|
801
|
+
if (i < node.expressions.length) {
|
|
802
|
+
const expr = node.expressions[i];
|
|
803
|
+
if (t.isIdentifier(expr)) built += `:${expr.name}`;
|
|
804
|
+
else built += "*";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return { url: built, confidence: "med", note: "template_dynamic" };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// "/api/x" + "/y" or "/api/x" + id (low confidence)
|
|
811
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
812
|
+
if (t.isStringLiteral(node.left) && node.left.value.startsWith("/")) {
|
|
813
|
+
const left = node.left.value;
|
|
814
|
+
let right = "";
|
|
815
|
+
if (t.isStringLiteral(node.right)) right = node.right.value;
|
|
816
|
+
else if (t.isIdentifier(node.right)) right = `:${node.right.name}`;
|
|
817
|
+
else right = "*";
|
|
818
|
+
return { url: left + right, confidence: "low", note: "concat" };
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return null;
|
|
445
823
|
}
|
|
446
824
|
|
|
447
|
-
|
|
448
|
-
|
|
825
|
+
function extractFetchMethodFromOptions(node) {
|
|
826
|
+
if (!t.isObjectExpression(node)) return "*";
|
|
827
|
+
for (const prop of node.properties) {
|
|
828
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
829
|
+
const key =
|
|
830
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
831
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
832
|
+
null;
|
|
833
|
+
if (key === "method" && t.isStringLiteral(prop.value)) return canonicalizeMethod(prop.value.value);
|
|
834
|
+
}
|
|
835
|
+
return "*";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function extractAxiosConfig(node) {
|
|
839
|
+
// axios({ url: "/api/x", method: "post" })
|
|
840
|
+
if (!t.isObjectExpression(node)) return null;
|
|
841
|
+
|
|
842
|
+
let urlNode = null;
|
|
843
|
+
let methodNode = null;
|
|
844
|
+
|
|
845
|
+
for (const prop of node.properties) {
|
|
846
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
847
|
+
const key =
|
|
848
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
849
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
850
|
+
null;
|
|
851
|
+
if (!key) continue;
|
|
852
|
+
|
|
853
|
+
if (key === "url") urlNode = prop.value;
|
|
854
|
+
if (key === "method") methodNode = prop.value;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const urlInfo = urlNode ? extractUrlLike(urlNode) : null;
|
|
858
|
+
if (!urlInfo) return null;
|
|
859
|
+
|
|
860
|
+
const method =
|
|
861
|
+
methodNode && t.isStringLiteral(methodNode)
|
|
862
|
+
? canonicalizeMethod(methodNode.value)
|
|
863
|
+
: "*";
|
|
864
|
+
|
|
865
|
+
return { method, urlInfo };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function resolveClientRouteRefs(repoRoot, stats) {
|
|
869
|
+
const files = await fg(CODE_FILE_GLOBS, {
|
|
449
870
|
cwd: repoRoot,
|
|
450
871
|
absolute: true,
|
|
451
872
|
ignore: [
|
|
452
|
-
|
|
453
|
-
"**/.next/**",
|
|
454
|
-
"**/dist/**",
|
|
455
|
-
"**/build/**",
|
|
873
|
+
...IGNORE_GLOBS,
|
|
456
874
|
"**/test/**",
|
|
457
875
|
"**/tests/**",
|
|
458
876
|
"**/__tests__/**",
|
|
@@ -462,141 +880,372 @@ async function resolveClientRouteRefs(repoRoot) {
|
|
|
462
880
|
"**/jest.config.*",
|
|
463
881
|
"**/*.mock.*",
|
|
464
882
|
"**/mocks/**",
|
|
465
|
-
"**/fixtures/**"
|
|
466
|
-
]
|
|
883
|
+
"**/fixtures/**",
|
|
884
|
+
],
|
|
467
885
|
});
|
|
468
886
|
|
|
469
887
|
const out = [];
|
|
470
888
|
|
|
471
889
|
for (const fileAbs of files) {
|
|
472
|
-
const fileRel =
|
|
890
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
473
891
|
const code = safeRead(fileAbs);
|
|
892
|
+
if (!code) continue;
|
|
474
893
|
|
|
475
894
|
let ast;
|
|
476
|
-
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
895
|
+
try {
|
|
896
|
+
ast = parseFile(code, fileAbs);
|
|
897
|
+
} catch {
|
|
898
|
+
stats.parseErrors++;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
traverse(ast, {
|
|
904
|
+
CallExpression(p) {
|
|
905
|
+
const callee = p.node.callee;
|
|
906
|
+
|
|
907
|
+
// fetch(url, opts)
|
|
908
|
+
if (t.isIdentifier(callee, { name: "fetch" })) {
|
|
909
|
+
const a0 = p.node.arguments[0];
|
|
910
|
+
const a1 = p.node.arguments[1];
|
|
911
|
+
|
|
912
|
+
const urlInfo = extractUrlLike(a0);
|
|
913
|
+
if (!urlInfo) return;
|
|
914
|
+
|
|
915
|
+
const url = urlInfo.url;
|
|
916
|
+
if (!url.startsWith("/")) return;
|
|
917
|
+
|
|
918
|
+
const method = extractFetchMethodFromOptions(a1);
|
|
919
|
+
|
|
920
|
+
const ev = evidenceFromLoc({
|
|
921
|
+
fileAbs,
|
|
922
|
+
fileRel,
|
|
923
|
+
loc: p.node.loc,
|
|
924
|
+
reason: `Client fetch(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
out.push({
|
|
928
|
+
method,
|
|
929
|
+
path: canonicalizePath(url),
|
|
930
|
+
source: fileRel,
|
|
931
|
+
confidence: urlInfo.confidence,
|
|
932
|
+
kind: "fetch",
|
|
933
|
+
evidence: ev ? [ev] : [],
|
|
934
|
+
});
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// axios.get("/api/x") etc
|
|
939
|
+
if (isAxiosMember(callee)) {
|
|
940
|
+
const verb = callee.property.name.toUpperCase();
|
|
941
|
+
const a0 = p.node.arguments[0];
|
|
942
|
+
|
|
943
|
+
const urlInfo = extractUrlLike(a0);
|
|
944
|
+
if (!urlInfo) return;
|
|
945
|
+
|
|
946
|
+
const url = urlInfo.url;
|
|
947
|
+
if (!url.startsWith("/")) return;
|
|
948
|
+
|
|
949
|
+
const ev = evidenceFromLoc({
|
|
950
|
+
fileAbs,
|
|
951
|
+
fileRel,
|
|
952
|
+
loc: p.node.loc,
|
|
953
|
+
reason: `Client axios.${verb.toLowerCase()}(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
out.push({
|
|
957
|
+
method: canonicalizeMethod(verb),
|
|
958
|
+
path: canonicalizePath(url),
|
|
959
|
+
source: fileRel,
|
|
960
|
+
confidence: urlInfo.confidence,
|
|
961
|
+
kind: "axios_member",
|
|
962
|
+
evidence: ev ? [ev] : [],
|
|
963
|
+
});
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// axios({ url, method })
|
|
968
|
+
if (t.isIdentifier(callee, { name: "axios" })) {
|
|
969
|
+
const a0 = p.node.arguments[0];
|
|
970
|
+
const cfg = extractAxiosConfig(a0);
|
|
971
|
+
if (!cfg) return;
|
|
972
|
+
|
|
973
|
+
const url = cfg.urlInfo.url;
|
|
974
|
+
if (!url.startsWith("/")) return;
|
|
975
|
+
|
|
976
|
+
const ev = evidenceFromLoc({
|
|
977
|
+
fileAbs,
|
|
978
|
+
fileRel,
|
|
979
|
+
loc: p.node.loc,
|
|
980
|
+
reason: `Client axios(config:${cfg.urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
out.push({
|
|
984
|
+
method: cfg.method,
|
|
985
|
+
path: canonicalizePath(url),
|
|
986
|
+
source: fileRel,
|
|
987
|
+
confidence: cfg.urlInfo.confidence === "high" ? "high" : "med",
|
|
988
|
+
kind: "axios_config",
|
|
989
|
+
evidence: ev ? [ev] : [],
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// useSWR("/api/user", fetcher) - Modern React data fetching
|
|
995
|
+
if (t.isIdentifier(callee, { name: "useSWR" })) {
|
|
996
|
+
const a0 = p.node.arguments[0];
|
|
997
|
+
|
|
998
|
+
const urlInfo = extractUrlLike(a0);
|
|
999
|
+
if (!urlInfo) return;
|
|
1000
|
+
|
|
1001
|
+
const url = urlInfo.url;
|
|
1002
|
+
if (!url.startsWith("/")) return;
|
|
1003
|
+
|
|
1004
|
+
const ev = evidenceFromLoc({
|
|
1005
|
+
fileAbs,
|
|
1006
|
+
fileRel,
|
|
1007
|
+
loc: p.node.loc,
|
|
1008
|
+
reason: `Client useSWR(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
out.push({
|
|
1012
|
+
method: "GET", // SWR is almost always GET
|
|
1013
|
+
path: canonicalizePath(url),
|
|
1014
|
+
source: fileRel,
|
|
1015
|
+
confidence: urlInfo.confidence,
|
|
1016
|
+
kind: "useSWR",
|
|
1017
|
+
evidence: ev ? [ev] : [],
|
|
1018
|
+
});
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// useQuery (React Query / TanStack Query) - Another popular data fetching library
|
|
1023
|
+
if (t.isIdentifier(callee, { name: "useQuery" })) {
|
|
1024
|
+
// useQuery({ queryKey: [...], queryFn: () => fetch("/api/x") })
|
|
1025
|
+
// or useQuery(["key"], () => fetch("/api/x"))
|
|
1026
|
+
const a0 = p.node.arguments[0];
|
|
1027
|
+
|
|
1028
|
+
// Try to extract URL from the arguments (often in queryFn)
|
|
1029
|
+
if (t.isObjectExpression(a0)) {
|
|
1030
|
+
for (const prop of a0.properties) {
|
|
1031
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
1032
|
+
if (!t.isIdentifier(prop.key, { name: "queryFn" })) continue;
|
|
1033
|
+
|
|
1034
|
+
// queryFn is often an arrow function with fetch inside
|
|
1035
|
+
const fn = prop.value;
|
|
1036
|
+
if (t.isArrowFunctionExpression(fn) || t.isFunctionExpression(fn)) {
|
|
1037
|
+
// Best effort: look for string literals that look like API paths
|
|
1038
|
+
const fnCode = code.slice(fn.start, fn.end);
|
|
1039
|
+
const urlMatch = fnCode.match(/["'`](\/api\/[^"'`]+)["'`]/);
|
|
1040
|
+
if (urlMatch) {
|
|
1041
|
+
const url = urlMatch[1].split("?")[0].split("#")[0];
|
|
1042
|
+
const ev = evidenceFromLoc({
|
|
1043
|
+
fileAbs,
|
|
1044
|
+
fileRel,
|
|
1045
|
+
loc: p.node.loc,
|
|
1046
|
+
reason: `Client useQuery(queryFn) "${url}"`,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
out.push({
|
|
1050
|
+
method: "GET",
|
|
1051
|
+
path: canonicalizePath(url),
|
|
1052
|
+
source: fileRel,
|
|
1053
|
+
confidence: "low", // Less certain extraction
|
|
1054
|
+
kind: "useQuery",
|
|
1055
|
+
evidence: ev ? [ev] : [],
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
501
1059
|
}
|
|
502
1060
|
}
|
|
503
1061
|
}
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
} catch {
|
|
1065
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
1066
|
+
stats.parseErrors++;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
504
1069
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
1070
|
+
return out;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// ---------- workspace detection (best-effort, no new deps) ----------
|
|
1074
|
+
function readJsonIfExists(abs) {
|
|
1075
|
+
try {
|
|
1076
|
+
return JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
1077
|
+
} catch {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function detectWorkspaces(repoRoot) {
|
|
1083
|
+
const roots = [];
|
|
1084
|
+
|
|
1085
|
+
const pkg = readJsonIfExists(path.join(repoRoot, "package.json"));
|
|
1086
|
+
if (pkg && pkg.workspaces) {
|
|
1087
|
+
const ws = pkg.workspaces;
|
|
1088
|
+
const patterns = Array.isArray(ws) ? ws : Array.isArray(ws.packages) ? ws.packages : [];
|
|
1089
|
+
for (const pat of patterns) {
|
|
1090
|
+
if (typeof pat === "string") roots.push(pat);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// pnpm-workspace.yaml minimal parser (just handles `packages:` list)
|
|
1095
|
+
const pnpmWs = path.join(repoRoot, "pnpm-workspace.yaml");
|
|
1096
|
+
if (fs.existsSync(pnpmWs)) {
|
|
1097
|
+
const raw = safeRead(pnpmWs) || "";
|
|
1098
|
+
const lines = raw.split(/\r?\n/);
|
|
1099
|
+
let inPackages = false;
|
|
1100
|
+
for (const line of lines) {
|
|
1101
|
+
const l = line.trim();
|
|
1102
|
+
if (!l) continue;
|
|
1103
|
+
if (l.startsWith("packages:")) {
|
|
1104
|
+
inPackages = true;
|
|
1105
|
+
continue;
|
|
542
1106
|
}
|
|
543
|
-
|
|
1107
|
+
if (inPackages) {
|
|
1108
|
+
const m = l.match(/^-+\s*['"]?([^'"]+)['"]?\s*$/);
|
|
1109
|
+
if (m && m[1]) roots.push(m[1]);
|
|
1110
|
+
else if (!l.startsWith("-")) inPackages = false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
544
1113
|
}
|
|
545
1114
|
|
|
546
|
-
|
|
1115
|
+
// Expand to actual package.json roots
|
|
1116
|
+
const uniq = Array.from(new Set(roots)).filter(Boolean);
|
|
1117
|
+
const pkgJsonGlobs = uniq.map((p) => (p.endsWith("/") ? p : p + "/") + "package.json");
|
|
1118
|
+
|
|
1119
|
+
const found = pkgJsonGlobs.length
|
|
1120
|
+
? fg.sync(pkgJsonGlobs, { cwd: repoRoot, absolute: true, ignore: IGNORE_GLOBS })
|
|
1121
|
+
: [];
|
|
1122
|
+
|
|
1123
|
+
const workspaces = found
|
|
1124
|
+
.map((abs) => path.dirname(abs))
|
|
1125
|
+
.map((abs) => normalizeRel(repoRoot, abs))
|
|
1126
|
+
.sort();
|
|
1127
|
+
|
|
1128
|
+
return workspaces;
|
|
547
1129
|
}
|
|
548
1130
|
|
|
549
|
-
// ---------- fastify entry detection ----------
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1131
|
+
// ---------- fastify entry detection (monorepo-friendly) ----------
|
|
1132
|
+
async function detectFastifyEntries(repoRoot) {
|
|
1133
|
+
// Keep it targeted (fast), but broad enough for monorepos.
|
|
1134
|
+
const candidates = await fg(
|
|
1135
|
+
[
|
|
1136
|
+
"**/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1137
|
+
"**/src/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1138
|
+
"**/*fastify*.{ts,tsx,js,jsx}",
|
|
1139
|
+
"**/*api*.{ts,tsx,js,jsx}",
|
|
1140
|
+
],
|
|
1141
|
+
{
|
|
1142
|
+
cwd: repoRoot,
|
|
1143
|
+
absolute: true,
|
|
1144
|
+
ignore: IGNORE_GLOBS,
|
|
1145
|
+
}
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
const entries = [];
|
|
1149
|
+
const fastifySignal = /\b(Fastify\s*\(|fastify\s*\(|require\(['"]fastify['"]\)|from\s+['"]fastify['"])\b/;
|
|
1150
|
+
const listenSignal = /\.\s*(listen|ready)\s*\(/;
|
|
1151
|
+
|
|
1152
|
+
for (const fileAbs of candidates) {
|
|
1153
|
+
const code = safeRead(fileAbs);
|
|
1154
|
+
if (!code) continue;
|
|
1155
|
+
// Must look like fastify + server start-ish signal (reduces noise)
|
|
1156
|
+
if (fastifySignal.test(code) && listenSignal.test(code)) {
|
|
1157
|
+
entries.push(fileAbs);
|
|
1158
|
+
}
|
|
560
1159
|
}
|
|
561
|
-
|
|
1160
|
+
|
|
1161
|
+
return Array.from(new Set(entries));
|
|
562
1162
|
}
|
|
563
1163
|
|
|
564
1164
|
// ---------- truthpack build/write ----------
|
|
565
1165
|
async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
1166
|
+
const stats = {
|
|
1167
|
+
parseErrors: 0,
|
|
1168
|
+
fastifyEntries: 0,
|
|
1169
|
+
fastifyRoutes: 0,
|
|
1170
|
+
nextAppRoutes: 0,
|
|
1171
|
+
nextPagesRoutes: 0,
|
|
1172
|
+
clientRefs: 0,
|
|
1173
|
+
serverRoutes: 0,
|
|
1174
|
+
gaps: 0,
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// Workspaces (for metadata + future use)
|
|
1178
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
1179
|
+
|
|
566
1180
|
// Next.js routes (App Router + Pages Router)
|
|
567
|
-
const nextApp = await resolveNextAppApiRoutes(repoRoot);
|
|
568
|
-
const nextPages = await resolveNextPagesApiRoutes(repoRoot);
|
|
1181
|
+
const nextApp = await resolveNextAppApiRoutes(repoRoot, stats);
|
|
1182
|
+
const nextPages = await resolveNextPagesApiRoutes(repoRoot, stats);
|
|
1183
|
+
|
|
1184
|
+
stats.nextAppRoutes = nextApp.length;
|
|
1185
|
+
stats.nextPagesRoutes = nextPages.length;
|
|
569
1186
|
|
|
570
|
-
// Fastify routes (
|
|
571
|
-
const entryRel = fastifyEntry || detectFastifyEntry(repoRoot);
|
|
1187
|
+
// Fastify routes (monorepo-friendly)
|
|
572
1188
|
let fastify = { routes: [], gaps: [] };
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1189
|
+
|
|
1190
|
+
if (fastifyEntry) {
|
|
1191
|
+
const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
|
|
1192
|
+
if (exists(entryAbs)) {
|
|
1193
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1194
|
+
fastify.routes.push(...resolved.routes);
|
|
1195
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1196
|
+
stats.fastifyEntries = 1;
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
const entries = await detectFastifyEntries(repoRoot);
|
|
1200
|
+
stats.fastifyEntries = entries.length;
|
|
1201
|
+
|
|
1202
|
+
for (const entryAbs of entries) {
|
|
1203
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1204
|
+
fastify.routes.push(...resolved.routes);
|
|
1205
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1206
|
+
}
|
|
576
1207
|
}
|
|
577
1208
|
|
|
1209
|
+
stats.fastifyRoutes = fastify.routes.length;
|
|
1210
|
+
|
|
578
1211
|
// Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
|
|
579
1212
|
const multiFramework = await resolveAllRoutes(repoRoot);
|
|
580
1213
|
const detectedFrameworks = await detectFrameworks(repoRoot);
|
|
581
1214
|
|
|
582
1215
|
// Client refs (JS/TS fetch/axios + Python requests/httpx)
|
|
583
|
-
const clientRefs = await resolveClientRouteRefs(repoRoot);
|
|
584
|
-
const allClientRefs = [...clientRefs, ...multiFramework.clientRefs];
|
|
1216
|
+
const clientRefs = await resolveClientRouteRefs(repoRoot, stats);
|
|
1217
|
+
const allClientRefs = [...clientRefs, ...(multiFramework.clientRefs || [])];
|
|
1218
|
+
|
|
1219
|
+
stats.clientRefs = allClientRefs.length;
|
|
1220
|
+
|
|
1221
|
+
// Merge all server routes (dedupe with priority)
|
|
1222
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...(multiFramework.routes || [])];
|
|
585
1223
|
|
|
586
|
-
|
|
587
|
-
const serverRoutesRaw = [...nextApp, ...nextPages, ...fastify.routes, ...multiFramework.routes];
|
|
588
|
-
const seenRoutes = new Set();
|
|
589
|
-
const server = [];
|
|
1224
|
+
const bestByKey = new Map(); // key = method:path
|
|
590
1225
|
for (const r of serverRoutesRaw) {
|
|
591
|
-
const key = `${r.method}:${r.path}`;
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1226
|
+
const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
|
|
1227
|
+
|
|
1228
|
+
const prev = bestByKey.get(key);
|
|
1229
|
+
if (!prev) {
|
|
1230
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Prefer higher confidence, and prefer specific method over "*"
|
|
1235
|
+
const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
|
|
1236
|
+
const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
|
|
1237
|
+
|
|
1238
|
+
if (curScore > prevScore) {
|
|
1239
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
595
1240
|
}
|
|
596
1241
|
}
|
|
597
1242
|
|
|
1243
|
+
const server = Array.from(bestByKey.values());
|
|
1244
|
+
stats.serverRoutes = server.length;
|
|
1245
|
+
|
|
598
1246
|
// Merge gaps
|
|
599
1247
|
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
1248
|
+
stats.gaps = allGaps.length;
|
|
600
1249
|
|
|
601
1250
|
// Env Truth v1
|
|
602
1251
|
const env = await buildEnvTruth(repoRoot);
|
|
@@ -611,23 +1260,32 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
|
611
1260
|
const enforcement = buildEnforcementTruth(repoRoot, server);
|
|
612
1261
|
|
|
613
1262
|
// Determine frameworks
|
|
614
|
-
const frameworks = new Set(
|
|
615
|
-
detectedFrameworks.forEach(f => frameworks.add(f));
|
|
616
|
-
server.forEach(r => r.framework && frameworks.add(r.framework));
|
|
1263
|
+
const frameworks = new Set();
|
|
1264
|
+
detectedFrameworks.forEach((f) => frameworks.add(f));
|
|
1265
|
+
server.forEach((r) => r.framework && frameworks.add(r.framework));
|
|
1266
|
+
if (nextApp.length || nextPages.length) frameworks.add("next");
|
|
1267
|
+
if (fastify.routes.length) frameworks.add("fastify");
|
|
617
1268
|
|
|
618
1269
|
const truthpack = {
|
|
619
1270
|
meta: {
|
|
620
|
-
version: "2.
|
|
1271
|
+
version: "2.1.0",
|
|
621
1272
|
generatedAt: new Date().toISOString(),
|
|
622
1273
|
repoRoot,
|
|
623
|
-
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" }
|
|
1274
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
|
|
1275
|
+
stats,
|
|
1276
|
+
},
|
|
1277
|
+
project: {
|
|
1278
|
+
frameworks: Array.from(frameworks),
|
|
1279
|
+
workspaces,
|
|
1280
|
+
entrypoints: {
|
|
1281
|
+
fastify: fastifyEntry ? [fastifyEntry] : [], // entries auto-detected are not stored as rel here by default
|
|
1282
|
+
},
|
|
624
1283
|
},
|
|
625
|
-
project: { frameworks: Array.from(frameworks), workspaces: [], entrypoints: [] },
|
|
626
1284
|
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
627
1285
|
env,
|
|
628
1286
|
auth,
|
|
629
1287
|
billing,
|
|
630
|
-
enforcement
|
|
1288
|
+
enforcement,
|
|
631
1289
|
};
|
|
632
1290
|
|
|
633
1291
|
const hash = sha256(JSON.stringify(truthpack));
|
|
@@ -639,21 +1297,27 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
|
639
1297
|
function writeTruthpack(repoRoot, truthpack) {
|
|
640
1298
|
const dir = path.join(repoRoot, ".vibecheck");
|
|
641
1299
|
ensureDir(dir);
|
|
642
|
-
|
|
643
|
-
|
|
1300
|
+
|
|
1301
|
+
const target = path.join(dir, "truthpack.json");
|
|
1302
|
+
const tmp = path.join(dir, `truthpack.${process.pid}.${Date.now()}.tmp.json`);
|
|
1303
|
+
|
|
1304
|
+
// atomic-ish write: write tmp then rename
|
|
1305
|
+
fs.writeFileSync(tmp, JSON.stringify(truthpack, null, 2));
|
|
1306
|
+
fs.renameSync(tmp, target);
|
|
644
1307
|
}
|
|
645
1308
|
|
|
646
1309
|
function loadTruthpack(repoRoot) {
|
|
647
|
-
// Spec path: .vibecheck/truthpack.json
|
|
648
1310
|
const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
|
|
649
|
-
// Legacy path: .vibecheck/truth/truthpack.json (backward compat)
|
|
650
1311
|
const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
|
|
651
|
-
|
|
652
|
-
try {
|
|
653
|
-
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
654
|
-
} catch {
|
|
655
|
-
|
|
656
|
-
|
|
1312
|
+
|
|
1313
|
+
try {
|
|
1314
|
+
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
1315
|
+
} catch {
|
|
1316
|
+
try {
|
|
1317
|
+
return JSON.parse(fs.readFileSync(legacyPath, "utf8"));
|
|
1318
|
+
} catch {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
657
1321
|
}
|
|
658
1322
|
}
|
|
659
1323
|
|
|
@@ -663,5 +1327,24 @@ module.exports = {
|
|
|
663
1327
|
buildTruthpack,
|
|
664
1328
|
writeTruthpack,
|
|
665
1329
|
loadTruthpack,
|
|
666
|
-
|
|
1330
|
+
clearCache, // Clear file cache to free memory (important for long-running processes)
|
|
1331
|
+
// kept for backward compatibility if other code imports it,
|
|
1332
|
+
// but fastifyEntry is now optional and auto-detected.
|
|
1333
|
+
detectFastifyEntry: function detectFastifyEntry(repoRoot) {
|
|
1334
|
+
const candidates = [
|
|
1335
|
+
"src/server.ts",
|
|
1336
|
+
"src/server.js",
|
|
1337
|
+
"server.ts",
|
|
1338
|
+
"server.js",
|
|
1339
|
+
"src/index.ts",
|
|
1340
|
+
"src/index.js",
|
|
1341
|
+
"index.ts",
|
|
1342
|
+
"index.js",
|
|
1343
|
+
];
|
|
1344
|
+
for (const rel of candidates) {
|
|
1345
|
+
const abs = path.join(repoRoot, rel);
|
|
1346
|
+
if (exists(abs)) return rel;
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
},
|
|
667
1350
|
};
|