infernoflow 0.37.1 → 0.37.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,460 +1,8 @@
1
- /**
2
- * infernoflow scan
3
- *
4
- * Deep AST-based code analysis. Reads actual function bodies — not just names.
5
- * Extracts: external calls, DB operations, HTTP calls, auth patterns, error types,
6
- * external service usage (Stripe, S3, SendGrid, etc.).
7
- *
8
- * Sprint 4 additions:
9
- * - Route discovery (Express / Fastify / Next.js App Router / Next.js Pages API)
10
- * - HTTP URL extraction — captures actual URL strings, not just call patterns
11
- * - Capability name inference from route paths (POST /api/users → CreateUser)
12
- * - Entry point classification (route handlers + exported functions vs helpers)
13
- * - --suggest flag: shows untracked entry points as capability candidates
14
- *
15
- * Enriches capabilities.json with a `codeAnalysis` block on each capability,
16
- * and saves the full scan report to inferno/scan.json.
17
- *
18
- * Usage:
19
- * infernoflow scan Scan project, enrich capabilities
20
- * infernoflow scan --dir src/ Scan specific directory
21
- * infernoflow scan --json Print scan.json to stdout
22
- * infernoflow scan --dry-run Print without writing files
23
- * infernoflow scan --capability auth-login Scan one capability only
24
- * infernoflow scan --suggest Show untracked entry points as new capability candidates
25
- */
26
-
27
- import * as fs from "node:fs";
28
- import * as path from "node:path";
29
- import { createRequire } from "node:module";
30
- import { execSync } from "node:child_process";
31
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
32
-
33
- const require = createRequire(import.meta.url);
34
-
35
- // ── TypeScript compiler API (global install) ──────────────────────────────────
36
-
37
- const TS_PATHS = [
38
- "/usr/local/lib/node_modules_global/lib/node_modules/typescript",
39
- "/usr/lib/node_modules/typescript",
40
- path.join(process.env.HOME || "", ".npm-global/lib/node_modules/typescript"),
41
- ];
42
-
43
- function loadTypeScript() {
44
- for (const p of TS_PATHS) {
45
- try { return require(path.join(p, "lib/typescript.js")); } catch {}
46
- }
47
- try { return require("typescript"); } catch {}
48
- return null;
49
- }
50
-
51
- const ts = loadTypeScript();
52
-
53
- // ── external service fingerprints ────────────────────────────────────────────
54
-
55
- const SERVICE_PATTERNS = [
56
- { service: "stripe", patterns: ["stripe", "Stripe", "createPaymentIntent", "charges.create"] },
57
- { service: "sendgrid", patterns: ["sendgrid", "@sendgrid", "sgMail", "sendgrid.send"] },
58
- { service: "ses", patterns: ["SES", "ses.sendEmail", "aws-sdk/ses", "nodemailer"] },
59
- { service: "s3", patterns: ["S3", "s3.upload", "s3.getObject", "PutObjectCommand", "@aws-sdk/s3"] },
60
- { service: "redis", patterns: ["redis", "Redis", "ioredis", "createClient"] },
61
- { service: "jwt", patterns: ["jwt", "jsonwebtoken", "sign(", "verify(", "decode("] },
62
- { service: "bcrypt", patterns: ["bcrypt", "argon2", "scrypt", "hashSync", "compare("] },
63
- { service: "prisma", patterns: ["prisma.", "PrismaClient", "@prisma/client"] },
64
- { service: "mongoose", patterns: ["mongoose", ".save()", ".findOne(", ".aggregate("] },
65
- { service: "postgres", patterns: ["pg", "Pool(", "Client(", "query(", "postgres("] },
66
- { service: "mysql", patterns: ["mysql", "mysql2", "createConnection"] },
67
- { service: "graphql", patterns: ["graphql", "gql`", "ApolloServer", "GraphQLSchema"] },
68
- { service: "firebase", patterns: ["firebase", "firestore", "initializeApp"] },
69
- { service: "twilio", patterns: ["twilio", "Twilio(", "messages.create"] },
70
- { service: "openai", patterns: ["openai", "OpenAI(", "createCompletion", "chat.completions"] },
71
- ];
72
-
73
- function detectServices(text) {
74
- const found = new Set();
75
- for (const { service, patterns } of SERVICE_PATTERNS) {
76
- if (patterns.some(p => text.includes(p))) found.add(service);
77
- }
78
- return [...found];
79
- }
80
-
81
- // ── DB call patterns ──────────────────────────────────────────────────────────
82
-
83
- const DB_PATTERNS = [
84
- /\.(find|findOne|findMany|findById|findAll)\s*\(/g,
85
- /\.(create|insert|insertOne|insertMany|save)\s*\(/g,
86
- /\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,
87
- /\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,
88
- /\.(query|execute|raw)\s*\(/g,
89
- /\.(aggregate|groupBy|count|sum)\s*\(/g,
90
- /db\.\w+\s*\(/g,
91
- /prisma\.\w+\.\w+\s*\(/g,
92
- ];
93
-
94
- function detectDbCalls(text) {
95
- const calls = new Set();
96
- for (const re of DB_PATTERNS) {
97
- const r = new RegExp(re.source, "g");
98
- let m;
99
- while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
100
- }
101
- return [...calls].slice(0, 10);
102
- }
103
-
104
- // ── HTTP call patterns + URL extraction ──────────────────────────────────────
105
-
106
- const HTTP_PATTERNS = [
107
- /fetch\s*\(/g,
108
- /axios\.(get|post|put|patch|delete)\s*\(/g,
109
- /http\.(get|post|request)\s*\(/g,
110
- /got\.(get|post|put|delete)\s*\(/g,
111
- /request\.(get|post|put|delete)\s*\(/g,
112
- /\$http\.(get|post|put|delete)\s*\(/g,
113
- ];
114
-
115
- function detectHttpCalls(text) {
116
- const calls = new Set();
117
- for (const re of HTTP_PATTERNS) {
118
- const r = new RegExp(re.source, "g");
119
- let m;
120
- while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
121
- }
122
- return [...calls].slice(0, 8);
123
- }
124
-
125
- /**
126
- * Extract actual URL strings from HTTP calls.
127
- * axios.post('/api/users', data) → { method: 'POST', url: '/api/users' }
128
- * fetch('/api/tasks') → { method: 'GET', url: '/api/tasks' }
129
- */
130
- const HTTP_URL_CALL_RE = /(?:(?:axios|got|request|\$http)\.(get|post|put|patch|delete)\s*\(\s*|fetch\s*\(\s*)['"`]([^'"`\s\)]+)['"`]/g;
131
-
132
- function extractHttpCallUrls(text) {
133
- const calls = [];
134
- const r = new RegExp(HTTP_URL_CALL_RE.source, "g");
135
- let m;
136
- while ((m = r.exec(text)) !== null) {
137
- const methodLiteral = m[1]; // undefined for fetch
138
- const url = m[2];
139
- // Only internal paths (start with / or contain /api/)
140
- if (!url.startsWith("/") && !url.includes("/api/")) continue;
141
- const method = methodLiteral ? methodLiteral.toUpperCase() : "GET";
142
- calls.push({ method, url });
143
- }
144
- return calls;
145
- }
146
-
147
- // ── Route discovery ───────────────────────────────────────────────────────────
148
-
149
- const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "all"];
150
-
151
- // Express/Koa/Hapi: app.get('/path', fn) or router.post('/path', fn)
152
- const ROUTE_RE = new RegExp(
153
- `(?:app|router|server|api|routes?)\\.(${HTTP_METHODS.join("|")})\\s*\\(\\s*['"\`]([^'"\`\\s)]+)['"\`]`,
154
- "g"
155
- );
156
-
157
- // Fastify: fastify.route({ method: 'POST', url: '/path' ... })
158
- const FASTIFY_ROUTE_RE = /fastify\.route\s*\(\s*\{[^}]*?method\s*:\s*['"](\w+)['"][^}]*?url\s*:\s*['"]([^'"]+)['"]/gs;
159
-
160
- // Express router.route('/path').get(...) chaining
161
- const ROUTE_CHAIN_RE = /(?:app|router)\.route\s*\(\s*['"`]([^'"`\s)]+)['"`]\s*\)\s*\.(get|post|put|patch|delete)/g;
162
-
163
- // Next.js App Router: export async function GET(req) in route.ts/route.js
164
- const NEXT_EXPORT_RE = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g;
165
-
166
- /**
167
- * Extract route definitions from a source file.
168
- * Returns: [{ method, path, source, filePath, loc }]
169
- */
170
- function extractRoutes(filePath, code) {
171
- const routes = [];
172
- const isNextAppRouter = /app[/\\].*route\.[jt]sx?$/.test(filePath) ||
173
- /app[/\\].*\broute\b.*\.[jt]sx?$/.test(filePath);
174
- const isNextApiPages = /pages[/\\]api[/\\]/.test(filePath);
175
-
176
- let m;
177
-
178
- // Express/Koa style
179
- const rr = new RegExp(ROUTE_RE.source, "g");
180
- while ((m = rr.exec(code)) !== null) {
181
- const method = m[1].toUpperCase();
182
- if (method === "ALL") continue; // skip catch-alls for capability inference
183
- routes.push({
184
- method,
185
- path: m[2],
186
- source: "express",
187
- filePath,
188
- loc: code.slice(0, m.index).split("\n").length,
189
- });
190
- }
191
-
192
- // Fastify route()
193
- const fr = new RegExp(FASTIFY_ROUTE_RE.source, "gs");
194
- while ((m = fr.exec(code)) !== null) {
195
- routes.push({
196
- method: m[1].toUpperCase(),
197
- path: m[2],
198
- source: "fastify",
199
- filePath,
200
- loc: code.slice(0, m.index).split("\n").length,
201
- });
202
- }
203
-
204
- // Express router.route('/path').get(...)
205
- const cr = new RegExp(ROUTE_CHAIN_RE.source, "g");
206
- while ((m = cr.exec(code)) !== null) {
207
- routes.push({
208
- method: m[2].toUpperCase(),
209
- path: m[1],
210
- source: "express-chain",
211
- filePath,
212
- loc: code.slice(0, m.index).split("\n").length,
213
- });
214
- }
215
-
216
- // Next.js App Router
217
- if (isNextAppRouter) {
218
- const nr = new RegExp(NEXT_EXPORT_RE.source, "g");
219
- while ((m = nr.exec(code)) !== null) {
220
- // Infer URL from file path: app/users/[id]/route.ts → /users/:id
221
- const routePath = filePath
222
- .replace(/\\/g, "/")
223
- .replace(/.*\/app\//, "/")
224
- .replace(/\/route\.[jt]sx?$/, "")
225
- .replace(/\[([^\]]+)\]/g, ":$1") || "/";
226
- routes.push({
227
- method: m[1].toUpperCase(),
228
- path: routePath,
229
- source: "next-app",
230
- filePath,
231
- loc: code.slice(0, m.index).split("\n").length,
232
- });
233
- }
234
- }
235
-
236
- // Next.js Pages API (export default handler)
237
- if (isNextApiPages) {
238
- const routePath = filePath
239
- .replace(/\\/g, "/")
240
- .replace(/.*\/pages\/api\//, "/api/")
241
- .replace(/\.[jt]sx?$/, "")
242
- .replace(/\/index$/, "")
243
- .replace(/\[([^\]]+)\]/g, ":$1");
244
- routes.push({
245
- method: "*",
246
- path: routePath || "/api",
247
- source: "next-pages",
248
- filePath,
249
- loc: 1,
250
- });
251
- }
252
-
253
- return routes;
254
- }
255
-
256
- // ── Capability name inference from routes ─────────────────────────────────────
257
-
258
- /**
259
- * Derive a human-readable capability name from a route.
260
- * POST /api/users → CreateUser
261
- * GET /api/users/:id → GetUser
262
- * DELETE /api/tasks/:id → DeleteTask
263
- * GET /api/tasks/:id/comments → ListTaskComment
264
- * PUT /api/upload → UpdateUpload
265
- */
266
- function capNameFromRoute(method, routePath) {
267
- // Normalise: strip leading /api or /v1 etc.
268
- const clean = routePath
269
- .replace(/^\/+/, "")
270
- .replace(/^api\/v?\d+\//, "")
271
- .replace(/^api\//, "");
272
-
273
- const parts = clean.split("/").filter(Boolean);
274
- const resources = parts.filter(p => !p.startsWith(":"));
275
- const hasId = parts.some(p => p.startsWith(":"));
276
-
277
- const noun = resources[resources.length - 1] || "Resource";
278
- const parent = resources.length > 1 ? resources[resources.length - 2] : null;
279
-
280
- const singularize = (s) => {
281
- if (s.endsWith("ies")) return s.slice(0, -3) + "y";
282
- if (s.endsWith("ses")) return s.slice(0, -2);
283
- if (s.endsWith("s") && !s.endsWith("ss")) return s.slice(0, -1);
284
- return s;
285
- };
286
- const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
287
- const toCamel = (s) => s.split(/[-_]/).map(capitalize).join("");
288
-
289
- const nounCap = capitalize(toCamel(singularize(noun)));
290
- const parentCap = parent ? capitalize(toCamel(singularize(parent))) : "";
291
-
292
- const verbMap = {
293
- GET: hasId ? "Get" : "List",
294
- POST: hasId ? "Add" : "Create",
295
- PUT: "Update",
296
- PATCH: "Update",
297
- DELETE: "Delete",
298
- HEAD: "Check",
299
- OPTIONS: "Options",
300
- "*": "Handle",
301
- };
302
-
303
- const verb = verbMap[method] || "Handle";
304
-
305
- // Nested resource: /tasks/:id/comments → ListTaskComment
306
- if (parentCap && resources.length > 1) return `${verb}${parentCap}${nounCap}`;
307
- return `${verb}${nounCap}`;
308
- }
309
-
310
- /**
311
- * Convert a capability name to a kebab-case id.
312
- * CreateUser → create-user
313
- */
314
- function nameToId(name) {
315
- return name
316
- .replace(/([a-z])([A-Z])/g, "$1-$2")
317
- .toLowerCase();
318
- }
319
-
320
- // ── TypeScript / JavaScript AST analysis ─────────────────────────────────────
321
-
322
- function getNodeName(node) {
323
- if (!ts) return null;
324
- if (node.name && ts.isIdentifier(node.name)) return node.name.text;
325
- return null;
326
- }
327
-
328
- /**
329
- * Walk all descendants of root using node.forEachChild (instance method).
330
- * Collects all call expressions and throw statements globally,
331
- * then assigns them to containing functions by source position range.
332
- */
333
- function collectAllNodes(root) {
334
- const calls = []; // { pos, end, name }
335
- const throws = []; // { pos, end, name }
336
-
337
- function walk(node) {
338
- if (ts.isCallExpression(node)) {
339
- const expr = node.expression;
340
- if (ts.isIdentifier(expr)) {
341
- calls.push({ pos: node.pos, end: node.end, name: expr.text + "()" });
342
- } else if (ts.isPropertyAccessExpression(expr)) {
343
- calls.push({ pos: node.pos, end: node.end, name: expr.name.text + "()" });
344
- }
345
- }
346
- if (ts.isThrowStatement(node) && node.expression) {
347
- if (ts.isNewExpression(node.expression) && ts.isIdentifier(node.expression.expression)) {
348
- throws.push({ pos: node.pos, end: node.end, name: node.expression.expression.text });
349
- }
350
- }
351
- node.forEachChild?.(walk);
352
- }
353
- walk(root);
354
- return { calls, throws };
355
- }
356
-
357
- function callsInRange(allCalls, pos, end) {
358
- return [...new Set(
359
- allCalls.filter(c => c.pos >= pos && c.end <= end).map(c => c.name)
360
- )].slice(0, 20);
361
- }
362
-
363
- function throwsInRange(allThrows, pos, end) {
364
- return [...new Set(
365
- allThrows.filter(t => t.pos >= pos && t.end <= end).map(t => t.name)
366
- )];
367
- }
368
-
369
- function isFunctionNode(node) {
370
- if (!ts) return false;
371
- return (
372
- ts.isFunctionDeclaration(node) ||
373
- ts.isFunctionExpression(node) ||
374
- ts.isArrowFunction(node) ||
375
- ts.isMethodDeclaration(node)
376
- );
377
- }
378
-
379
- function getParentVariableName(node) {
380
- // For arrow functions assigned to const: const foo = () => {}
381
- if (!ts) return null;
382
- if (node.parent && ts.isVariableDeclaration(node.parent)) {
383
- return getNodeName(node.parent);
384
- }
385
- if (node.parent && ts.isPropertyAssignment(node.parent)) {
386
- return getNodeName(node.parent);
387
- }
388
- return null;
389
- }
390
-
391
- /**
392
- * Check whether a node has the `export` modifier.
393
- */
394
- function isExportedNode(node) {
395
- if (!ts) return false;
396
- try {
397
- const flags = ts.getCombinedModifierFlags(node);
398
- return !!(flags & ts.ModifierFlags.Export);
399
- } catch { return false; }
400
- }
401
-
402
- function analyzeJsTs(filePath, code) {
403
- if (!ts) return null;
404
-
405
- let srcFile;
406
- try {
407
- srcFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, /*setParentNodes*/ true);
408
- } catch {
409
- return null;
410
- }
411
-
412
- // Collect ALL call/throw nodes in one pass from root
413
- const { calls: allCalls, throws: allThrows } = collectAllNodes(srcFile);
414
-
415
- const functions = [];
416
-
417
- function visit(node) {
418
- if (isFunctionNode(node)) {
419
- const name = getNodeName(node) || getParentVariableName(node) || "<anonymous>";
420
- const text = code.slice(node.pos, node.end);
421
- const calls = callsInRange(allCalls, node.pos, node.end);
422
- const throws = throwsInRange(allThrows, node.pos, node.end);
423
-
424
- // Check export status: either node itself or parent VariableStatement is exported
425
- let exported = isExportedNode(node);
426
- if (!exported && node.parent) {
427
- // const foo = () => {} inside export const foo = ...
428
- if (ts.isVariableDeclaration(node.parent) && node.parent.parent) {
429
- const varList = node.parent.parent;
430
- if (ts.isVariableDeclarationList(varList) && varList.parent) {
431
- exported = isExportedNode(varList.parent);
432
- }
433
- }
434
- }
435
-
436
- functions.push({
437
- name,
438
- calls,
439
- throws,
440
- services: detectServices(text),
441
- dbCalls: detectDbCalls(text),
442
- httpCalls: detectHttpCalls(text),
443
- httpCallUrls: extractHttpCallUrls(text),
444
- isExported: exported,
445
- loc: srcFile.getLineAndCharacterOfPosition(node.pos).line + 1,
446
- });
447
- }
448
- node.forEachChild?.(visit);
449
- }
450
-
451
- visit(srcFile);
452
- return functions;
453
- }
454
-
455
- // ── Python AST analysis via child_process ─────────────────────────────────────
456
-
457
- const PYTHON_SCRIPT = `
1
+ import*as j from"node:fs";import*as x from"node:path";import{createRequire as re}from"node:module";import{execSync as ie}from"node:child_process";import{bold as _,cyan as T,gray as d,green as F,yellow as R,red as D}from"../ui/output.mjs";const G=re(import.meta.url),le=["/usr/local/lib/node_modules_global/lib/node_modules/typescript","/usr/lib/node_modules/typescript",x.join(process.env.HOME||"",".npm-global/lib/node_modules/typescript")];function ce(){for(const e of le)try{return G(x.join(e,"lib/typescript.js"))}catch{}try{return G("typescript")}catch{}return null}const g=ce(),ae=[{service:"stripe",patterns:["stripe","Stripe","createPaymentIntent","charges.create"]},{service:"sendgrid",patterns:["sendgrid","@sendgrid","sgMail","sendgrid.send"]},{service:"ses",patterns:["SES","ses.sendEmail","aws-sdk/ses","nodemailer"]},{service:"s3",patterns:["S3","s3.upload","s3.getObject","PutObjectCommand","@aws-sdk/s3"]},{service:"redis",patterns:["redis","Redis","ioredis","createClient"]},{service:"jwt",patterns:["jwt","jsonwebtoken","sign(","verify(","decode("]},{service:"bcrypt",patterns:["bcrypt","argon2","scrypt","hashSync","compare("]},{service:"prisma",patterns:["prisma.","PrismaClient","@prisma/client"]},{service:"mongoose",patterns:["mongoose",".save()",".findOne(",".aggregate("]},{service:"postgres",patterns:["pg","Pool(","Client(","query(","postgres("]},{service:"mysql",patterns:["mysql","mysql2","createConnection"]},{service:"graphql",patterns:["graphql","gql`","ApolloServer","GraphQLSchema"]},{service:"firebase",patterns:["firebase","firestore","initializeApp"]},{service:"twilio",patterns:["twilio","Twilio(","messages.create"]},{service:"openai",patterns:["openai","OpenAI(","createCompletion","chat.completions"]}];function I(e){const t=new Set;for(const{service:r,patterns:n}of ae)n.some(s=>e.includes(s))&&t.add(r);return[...t]}const pe=[/\.(find|findOne|findMany|findById|findAll)\s*\(/g,/\.(create|insert|insertOne|insertMany|save)\s*\(/g,/\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,/\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,/\.(query|execute|raw)\s*\(/g,/\.(aggregate|groupBy|count|sum)\s*\(/g,/db\.\w+\s*\(/g,/prisma\.\w+\.\w+\s*\(/g];function M(e){const t=new Set;for(const r of pe){const n=new RegExp(r.source,"g");let s;for(;(s=n.exec(e))!==null;)t.add(s[0].replace(/\s*\($/,"()"))}return[...t].slice(0,10)}const ue=[/fetch\s*\(/g,/axios\.(get|post|put|patch|delete)\s*\(/g,/http\.(get|post|request)\s*\(/g,/got\.(get|post|put|delete)\s*\(/g,/request\.(get|post|put|delete)\s*\(/g,/\$http\.(get|post|put|delete)\s*\(/g];function H(e){const t=new Set;for(const r of ue){const n=new RegExp(r.source,"g");let s;for(;(s=n.exec(e))!==null;)t.add(s[0].replace(/\s*\($/,"()"))}return[...t].slice(0,8)}const fe=/(?:(?:axios|got|request|\$http)\.(get|post|put|patch|delete)\s*\(\s*|fetch\s*\(\s*)['"`]([^'"`\s\)]+)['"`]/g;function L(e){const t=[],r=new RegExp(fe.source,"g");let n;for(;(n=r.exec(e))!==null;){const s=n[1],o=n[2];if(!o.startsWith("/")&&!o.includes("/api/"))continue;const l=s?s.toUpperCase():"GET";t.push({method:l,url:o})}return t}const de=["get","post","put","patch","delete","head","options","all"],ge=new RegExp(`(?:app|router|server|api|routes?)\\.(${de.join("|")})\\s*\\(\\s*['"\`]([^'"\`\\s)]+)['"\`]`,"g"),he=/fastify\.route\s*\(\s*\{[^}]*?method\s*:\s*['"](\w+)['"][^}]*?url\s*:\s*['"]([^'"]+)['"]/gs,me=/(?:app|router)\.route\s*\(\s*['"`]([^'"`\s)]+)['"`]\s*\)\s*\.(get|post|put|patch|delete)/g,ye=/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g;function we(e,t){const r=[],n=/app[/\\].*route\.[jt]sx?$/.test(e)||/app[/\\].*\broute\b.*\.[jt]sx?$/.test(e),s=/pages[/\\]api[/\\]/.test(e);let o;const l=new RegExp(ge.source,"g");for(;(o=l.exec(t))!==null;){const p=o[1].toUpperCase();p!=="ALL"&&r.push({method:p,path:o[2],source:"express",filePath:e,loc:t.slice(0,o.index).split(`
2
+ `).length})}const i=new RegExp(he.source,"gs");for(;(o=i.exec(t))!==null;)r.push({method:o[1].toUpperCase(),path:o[2],source:"fastify",filePath:e,loc:t.slice(0,o.index).split(`
3
+ `).length});const a=new RegExp(me.source,"g");for(;(o=a.exec(t))!==null;)r.push({method:o[2].toUpperCase(),path:o[1],source:"express-chain",filePath:e,loc:t.slice(0,o.index).split(`
4
+ `).length});if(n){const p=new RegExp(ye.source,"g");for(;(o=p.exec(t))!==null;){const c=e.replace(/\\/g,"/").replace(/.*\/app\//,"/").replace(/\/route\.[jt]sx?$/,"").replace(/\[([^\]]+)\]/g,":$1")||"/";r.push({method:o[1].toUpperCase(),path:c,source:"next-app",filePath:e,loc:t.slice(0,o.index).split(`
5
+ `).length})}}if(s){const p=e.replace(/\\/g,"/").replace(/.*\/pages\/api\//,"/api/").replace(/\.[jt]sx?$/,"").replace(/\/index$/,"").replace(/\[([^\]]+)\]/g,":$1");r.push({method:"*",path:p||"/api",source:"next-pages",filePath:e,loc:1})}return r}function k(e,t){const n=t.replace(/^\/+/,"").replace(/^api\/v?\d+\//,"").replace(/^api\//,"").split("/").filter(Boolean),s=n.filter(h=>!h.startsWith(":")),o=n.some(h=>h.startsWith(":")),l=s[s.length-1]||"Resource",i=s.length>1?s[s.length-2]:null,a=h=>h.endsWith("ies")?h.slice(0,-3)+"y":h.endsWith("ses")?h.slice(0,-2):h.endsWith("s")&&!h.endsWith("ss")?h.slice(0,-1):h,p=h=>h.charAt(0).toUpperCase()+h.slice(1),c=h=>h.split(/[-_]/).map(p).join(""),f=p(c(a(l))),m=i?p(c(a(i))):"",y={GET:o?"Get":"List",POST:o?"Add":"Create",PUT:"Update",PATCH:"Update",DELETE:"Delete",HEAD:"Check",OPTIONS:"Options","*":"Handle"}[e]||"Handle";return m&&s.length>1?`${y}${m}${f}`:`${y}${f}`}function K(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function z(e){return g&&e.name&&g.isIdentifier(e.name)?e.name.text:null}function xe(e){const t=[],r=[];function n(s){if(g.isCallExpression(s)){const o=s.expression;g.isIdentifier(o)?t.push({pos:s.pos,end:s.end,name:o.text+"()"}):g.isPropertyAccessExpression(o)&&t.push({pos:s.pos,end:s.end,name:o.name.text+"()"})}g.isThrowStatement(s)&&s.expression&&g.isNewExpression(s.expression)&&g.isIdentifier(s.expression.expression)&&r.push({pos:s.pos,end:s.end,name:s.expression.expression.text}),s.forEachChild?.(n)}return n(e),{calls:t,throws:r}}function Ce(e,t,r){return[...new Set(e.filter(n=>n.pos>=t&&n.end<=r).map(n=>n.name))].slice(0,20)}function be(e,t,r){return[...new Set(e.filter(n=>n.pos>=t&&n.end<=r).map(n=>n.name))]}function $e(e){return g?g.isFunctionDeclaration(e)||g.isFunctionExpression(e)||g.isArrowFunction(e)||g.isMethodDeclaration(e):!1}function Se(e){return g&&(e.parent&&g.isVariableDeclaration(e.parent)||e.parent&&g.isPropertyAssignment(e.parent))?z(e.parent):null}function V(e){if(!g)return!1;try{return!!(g.getCombinedModifierFlags(e)&g.ModifierFlags.Export)}catch{return!1}}function Ee(e,t){if(!g)return null;let r;try{r=g.createSourceFile(e,t,g.ScriptTarget.Latest,!0)}catch{return null}const{calls:n,throws:s}=xe(r),o=[];function l(i){if($e(i)){const a=z(i)||Se(i)||"<anonymous>",p=t.slice(i.pos,i.end),c=Ce(n,i.pos,i.end),f=be(s,i.pos,i.end);let m=V(i);if(!m&&i.parent&&g.isVariableDeclaration(i.parent)&&i.parent.parent){const w=i.parent.parent;g.isVariableDeclarationList(w)&&w.parent&&(m=V(w.parent))}o.push({name:a,calls:c,throws:f,services:I(p),dbCalls:M(p),httpCalls:H(p),httpCallUrls:L(p),isExported:m,loc:r.getLineAndCharacterOfPosition(i.pos).line+1})}i.forEachChild?.(l)}return l(r),o}const ve=`
458
6
  import ast, json, sys
459
7
 
460
8
  def get_calls(node):
@@ -492,664 +40,9 @@ try:
492
40
  print(json.dumps(functions))
493
41
  except Exception as e:
494
42
  print(json.dumps([]))
495
- `;
496
-
497
- function analyzePython(filePath) {
498
- try {
499
- const result = execSync(
500
- `python3 -c ${JSON.stringify(PYTHON_SCRIPT)} ${JSON.stringify(filePath)}`,
501
- { timeout: 8000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
502
- );
503
- const fns = JSON.parse(result.trim() || "[]");
504
- // add service/db/http detection from raw file text
505
- const code = fs.readFileSync(filePath, "utf8");
506
- return fns.map(f => ({
507
- ...f,
508
- services: detectServices(code),
509
- dbCalls: detectDbCalls(code),
510
- httpCalls: detectHttpCalls(code),
511
- httpCallUrls: extractHttpCallUrls(code),
512
- isExported: false,
513
- }));
514
- } catch {
515
- return null;
516
- }
517
- }
518
-
519
- // ── regex fallback (Go, Ruby, Java, other) ────────────────────────────────────
520
-
521
- const FUNC_PATTERNS = [
522
- { re: /^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm, lang: "go" },
523
- { re: /^\s*(?:def|async def)\s+(\w+)\s*\(/gm, lang: "py" },
524
- { re: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm, lang: "java" },
525
- { re: /^\s*def\s+(\w+)\s*[\(\|]/gm, lang: "rb" },
526
- ];
527
-
528
- function analyzeWithRegex(filePath, code) {
529
- const ext = path.extname(filePath).slice(1);
530
- const pattern = FUNC_PATTERNS.find(p => p.lang === ext);
531
- if (!pattern) return null;
532
-
533
- const functions = [];
534
- const r = new RegExp(pattern.re.source, "gm");
535
- let m;
536
- while ((m = r.exec(code)) !== null) {
537
- // grab up to 60 lines after the match for context
538
- const start = m.index;
539
- const end = Math.min(start + 2000, code.length);
540
- const chunk = code.slice(start, end);
541
- functions.push({
542
- name: m[1],
543
- calls: [],
544
- throws: [],
545
- services: detectServices(chunk),
546
- dbCalls: detectDbCalls(chunk),
547
- httpCalls: detectHttpCalls(chunk),
548
- httpCallUrls: extractHttpCallUrls(chunk),
549
- isExported: false,
550
- loc: code.slice(0, start).split("\n").length,
551
- });
552
- }
553
- return functions.length > 0 ? functions : null;
554
- }
555
-
556
- // ── file walker ───────────────────────────────────────────────────────────────
557
-
558
- const SKIP_DIRS = new Set([
559
- "node_modules", ".git", "dist", "build", "out", ".next", ".nuxt",
560
- "coverage", "__pycache__", ".pytest_cache", "vendor", "tmp", ".turbo",
561
- "target", ".gradle", "public", "static", "assets",
562
- ]);
563
-
564
- const SUPPORTED_EXTS = new Set([
565
- ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
566
- ".py", ".go", ".rb", ".java",
567
- ]);
568
-
569
- const TEST_FILE = /\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;
570
-
571
- function* walkFiles(dir) {
572
- let entries;
573
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
574
- catch { return; }
575
- for (const e of entries) {
576
- if (e.isDirectory()) {
577
- if (!SKIP_DIRS.has(e.name)) yield* walkFiles(path.join(dir, e.name));
578
- } else if (e.isFile()) {
579
- const ext = path.extname(e.name);
580
- if (SUPPORTED_EXTS.has(ext) && !TEST_FILE.test(e.name)) {
581
- yield path.join(dir, e.name);
582
- }
583
- }
584
- }
585
- }
586
-
587
- // ── per-file analyzer ─────────────────────────────────────────────────────────
588
-
589
- function analyzeFile(filePath) {
590
- let code;
591
- try { code = fs.readFileSync(filePath, "utf8"); }
592
- catch { return { functions: [], routes: [] }; }
593
-
594
- const ext = path.extname(filePath);
595
- let functions = [];
596
-
597
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
598
- functions = analyzeJsTs(filePath, code) || analyzeWithRegex(filePath, code) || [];
599
- } else if (ext === ".py") {
600
- functions = analyzePython(filePath) || analyzeWithRegex(filePath, code) || [];
601
- } else {
602
- functions = analyzeWithRegex(filePath, code) || [];
603
- }
604
-
605
- const routes = ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext))
606
- ? extractRoutes(filePath, code)
607
- : [];
608
-
609
- return { functions, routes };
610
- }
611
-
612
- // ── entry point classification ────────────────────────────────────────────────
613
-
614
- /**
615
- * Mark each function as isEntryPoint / isHelper based on:
616
- * 1. It's an exported function
617
- * 2. It's registered as a route handler (function name appears near a route definition)
618
- * 3. It directly has HTTP/DB calls (leaf service calls are likely entry-adjacent)
619
- */
620
- function classifyEntryPoints(allFunctions, allRoutes) {
621
- // Build set of function names used in route registration lines
622
- // e.g. router.post('/api/x', createUser) — "createUser" is a handler
623
- const routeHandlerNames = new Set();
624
- for (const route of allRoutes) {
625
- if (route.handler) routeHandlerNames.add(route.handler);
626
- }
627
-
628
- // Build caller graph
629
- const calledByCount = new Map(); // name → number of callers
630
- for (const { fn } of allFunctions) {
631
- for (const callee of fn.calls || []) {
632
- const name = callee.replace("()", "");
633
- calledByCount.set(name, (calledByCount.get(name) || 0) + 1);
634
- }
635
- }
636
-
637
- return allFunctions.map(({ fn, filePath }) => {
638
- const isRouteHandler = routeHandlerNames.has(fn.name);
639
- const isExported = fn.isExported || false;
640
- const callerCount = calledByCount.get(fn.name) || 0;
641
- const hasServiceCalls = (fn.dbCalls?.length || 0) + (fn.services?.length || 0) +
642
- (fn.httpCallUrls?.length || 0) > 0;
643
-
644
- // Entry point: exported OR a known route handler
645
- // Also treat functions with service calls that are NOT called by anyone as entry-point candidates
646
- const isEntryPoint = isRouteHandler || isExported ||
647
- (hasServiceCalls && callerCount === 0);
648
- const isHelper = !isEntryPoint && callerCount > 0;
649
-
650
- return {
651
- fn: { ...fn, isEntryPoint, isHelper, callerCount },
652
- filePath,
653
- };
654
- });
655
- }
656
-
657
- // ── HTTP chain resolver ───────────────────────────────────────────────────────
658
-
659
- /**
660
- * Build a route index for fast URL lookup.
661
- * Normalises dynamic segments: /users/:id and /users/123 both map to the same key.
662
- * Returns: Map<"METHOD /normalised/path" → route>
663
- */
664
- function buildRouteIndex(allRoutes) {
665
- const index = new Map();
666
-
667
- for (const route of allRoutes) {
668
- // Normalise path: replace :param with :*
669
- const norm = route.path.replace(/:[^/]+/g, ":*");
670
- const key = `${route.method} ${norm}`;
671
- if (!index.has(key)) index.set(key, route);
672
- // Also index the raw path
673
- const rawKey = `${route.method} ${route.path}`;
674
- if (!index.has(rawKey)) index.set(rawKey, route);
675
- }
676
- return index;
677
- }
678
-
679
- /**
680
- * Match an outbound HTTP call URL to a discovered route.
681
- * Handles: exact match, normalised dynamic segments, prefix stripping (/api/v1/).
682
- */
683
- function resolveUrl(method, url, routeIndex) {
684
- // Strip query string
685
- const cleanUrl = url.split("?")[0];
686
-
687
- // Try exact match first
688
- const exact = routeIndex.get(`${method} ${cleanUrl}`);
689
- if (exact) return exact;
690
-
691
- // Normalise dynamics and try
692
- const norm = cleanUrl.replace(/\/[0-9a-f-]{8,}|\/\d+/g, "/:*");
693
- const normKey = `${method} ${norm}`;
694
- const normMatch = routeIndex.get(normKey);
695
- if (normMatch) return normMatch;
696
-
697
- // Strip common API prefixes and retry
698
- const stripped = cleanUrl.replace(/^\/api\/v?\d+/, "").replace(/^\/api/, "");
699
- if (stripped !== cleanUrl) {
700
- const strippedKey = `${method} ${stripped}`;
701
- const strippedMatch = routeIndex.get(strippedKey);
702
- if (strippedMatch) return strippedMatch;
703
- }
704
-
705
- // Wildcard method: some routes registered as ALL or *
706
- const wildKey = `* ${cleanUrl}`;
707
- return routeIndex.get(wildKey) || null;
708
- }
709
-
710
- /**
711
- * Build end-to-end HTTP chains for all functions.
712
- *
713
- * For each function that makes outbound HTTP calls, resolve each URL to its
714
- * route handler and produce a chain entry:
715
- * { caller, method, url, handler, handlerFile, resolved }
716
- *
717
- * Also resolves transitively: if the handler itself calls another route,
718
- * the chain continues (up to depth 3 to avoid cycles).
719
- *
720
- * Returns: Map<callerFnName → ChainStep[]>
721
- */
722
- function buildHttpChains(classifiedFunctions, allRoutes, cwd) {
723
- const routeIndex = buildRouteIndex(allRoutes);
724
-
725
- // Build a name → { fn, filePath } index for handler lookup
726
- const fnByName = new Map();
727
- for (const { fn, filePath } of classifiedFunctions) {
728
- if (!fnByName.has(fn.name)) fnByName.set(fn.name, { fn, filePath });
729
- }
730
-
731
- const chains = new Map(); // callerName → ChainStep[]
732
-
733
- function resolveChain(fnName, depth, visited) {
734
- if (depth > 3 || visited.has(fnName)) return [];
735
- visited.add(fnName);
736
-
737
- const entry = fnByName.get(fnName);
738
- if (!entry) return [];
739
-
740
- const steps = [];
741
- for (const { method, url } of entry.fn.httpCallUrls || []) {
742
- const route = resolveUrl(method, url, routeIndex);
743
- const step = {
744
- caller: fnName,
745
- method,
746
- url,
747
- resolved: !!route,
748
- handler: route?.handler || null,
749
- handlerFile: route ? path.relative(cwd, route.filePath) : null,
750
- suggestedName: route ? capNameFromRoute(route.method, route.path) : null,
751
- };
752
- steps.push(step);
753
-
754
- // Recurse into the handler if found
755
- if (route?.handler && !visited.has(route.handler)) {
756
- const nested = resolveChain(route.handler, depth + 1, new Set(visited));
757
- steps.push(...nested.map(s => ({ ...s, via: fnName })));
758
- }
759
- }
760
- return steps;
761
- }
762
-
763
- for (const { fn } of classifiedFunctions) {
764
- if ((fn.httpCallUrls || []).length === 0) continue;
765
- const steps = resolveChain(fn.name, 0, new Set());
766
- if (steps.length) chains.set(fn.name, steps);
767
- }
768
-
769
- return chains;
770
- }
771
-
772
- // ── capability matcher ────────────────────────────────────────────────────────
773
-
774
- function tokenise(str) {
775
- return str.replace(/([a-z])([A-Z])/g, "$1 $2")
776
- .toLowerCase().split(/[\s_\-/.]+/).filter(t => t.length > 1);
777
- }
778
-
779
- function overlap(a, b) {
780
- const sa = new Set(a), sb = new Set(b);
781
- let n = 0;
782
- for (const t of sa) if (sb.has(t)) n++;
783
- const u = sa.size + sb.size - n;
784
- return u === 0 ? 0 : n / u;
785
- }
786
-
787
- function matchFunctionToCapability(fn, capabilities) {
788
- const fnTokens = tokenise(fn.name);
789
- let best = null, bestScore = 0;
790
- for (const cap of capabilities) {
791
- const score = Math.max(
792
- overlap(fnTokens, tokenise(cap.id || "")),
793
- overlap(fnTokens, tokenise(cap.name || cap.title || "")),
794
- );
795
- if (score > bestScore) { bestScore = score; best = cap; }
796
- }
797
- return bestScore >= 0.2 ? { cap: best, score: bestScore } : null;
798
- }
799
-
800
- // ── merge analysis into capability ────────────────────────────────────────────
801
-
802
- function mergeAnalysis(existing = {}, fn, filePath, cwd, chainSteps) {
803
- const rel = path.relative(cwd, filePath);
804
-
805
- // merge arrays without duplicates
806
- const merge = (a = [], b = []) => [...new Set([...a, ...b])];
807
-
808
- // Format chain steps for storage: "callerFn → METHOD /url → handlerFn"
809
- const newChains = (chainSteps || []).map(s => {
810
- const handler = s.handler ? ` → ${s.handler}` : (s.resolved ? "" : " [unresolved]");
811
- return `${s.caller} → ${s.method} ${s.url}${handler}`;
812
- });
813
-
814
- return {
815
- functions: merge(existing.functions, [fn.name]),
816
- sourceFiles: merge(existing.sourceFiles, [rel]),
817
- calls: merge(existing.calls, fn.calls),
818
- throws: merge(existing.throws, fn.throws),
819
- services: merge(existing.services, fn.services),
820
- dbCalls: merge(existing.dbCalls, fn.dbCalls),
821
- httpCalls: merge(existing.httpCalls, fn.httpCalls),
822
- httpCallUrls: merge(existing.httpCallUrls || [],
823
- (fn.httpCallUrls || []).map(c => `${c.method} ${c.url}`)),
824
- httpChains: merge(existing.httpChains || [], newChains),
825
- isEntryPoint: fn.isEntryPoint || existing.isEntryPoint || false,
826
- scannedAt: new Date().toISOString(),
827
- };
828
- }
829
-
830
- // ── --suggest: show untracked entry points ────────────────────────────────────
831
-
832
- function printSuggestions(allFunctions, allRoutes, capabilities, cwd) {
833
- const existingIds = new Set(capabilities.map(c => c.id));
834
- const existingNames = new Set(capabilities.map(c => (c.name || c.title || "").toLowerCase()));
835
-
836
- console.log();
837
- console.log(bold(" Capability Candidates"));
838
- console.log(gray(" Untracked entry points discovered in your codebase:"));
839
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
840
-
841
- const seen = new Set();
842
- const candidates = [];
843
-
844
- // Route-based candidates (highest confidence)
845
- for (const route of allRoutes) {
846
- const suggestedName = capNameFromRoute(route.method, route.path);
847
- const suggestedId = nameToId(suggestedName);
848
- if (existingIds.has(suggestedId) || existingNames.has(suggestedName.toLowerCase())) continue;
849
- if (seen.has(suggestedId)) continue;
850
- seen.add(suggestedId);
851
-
852
- const rel = path.relative(cwd, route.filePath);
853
- candidates.push({
854
- id: suggestedId,
855
- name: suggestedName,
856
- source: `${route.method} ${route.path}`,
857
- file: rel,
858
- confidence: "high",
859
- });
860
- }
861
-
862
- // Function-based candidates (entry points with service calls, no matching cap)
863
- for (const { fn, filePath } of allFunctions) {
864
- if (!fn.isEntryPoint) continue;
865
- if (fn.name === "<anonymous>" || fn.name.length < 3) continue;
866
- const match = matchFunctionToCapability(fn, capabilities);
867
- if (match && match.score >= 0.35) continue; // already tracked
868
- const id = nameToId(fn.name);
869
- if (existingIds.has(id) || seen.has(id)) continue;
870
- seen.add(id);
871
-
872
- const rel = path.relative(cwd, filePath);
873
- candidates.push({
874
- id,
875
- name: fn.name,
876
- source: `function in ${rel}:${fn.loc}`,
877
- file: rel,
878
- confidence: "medium",
879
- });
880
- }
881
-
882
- if (!candidates.length) {
883
- console.log(gray(" All entry points are already tracked as capabilities. ✓"));
884
- console.log();
885
- return;
886
- }
887
-
888
- const high = candidates.filter(c => c.confidence === "high");
889
- const medium = candidates.filter(c => c.confidence === "medium");
890
-
891
- if (high.length) {
892
- console.log();
893
- console.log(cyan(" ● High confidence (from route definitions):"));
894
- for (const c of high) {
895
- console.log(` ${green(c.id.padEnd(35))} ${gray(c.source)}`);
896
- }
897
- }
898
-
899
- if (medium.length) {
900
- console.log();
901
- console.log(cyan(" ● Medium confidence (exported / top-level functions):"));
902
- for (const c of medium.slice(0, 10)) {
903
- console.log(` ${yellow(c.id.padEnd(35))} ${gray(c.source)}`);
904
- }
905
- if (medium.length > 10) {
906
- console.log(gray(` … and ${medium.length - 10} more`));
907
- }
908
- }
909
-
910
- console.log();
911
- console.log(gray(" To add these, run:"));
912
- for (const c of [...high, ...medium.slice(0, 3)]) {
913
- console.log(gray(` infernoflow add "${c.id}" "${c.name}"`));
914
- }
915
- console.log();
916
- }
917
-
918
- // ── reporters ─────────────────────────────────────────────────────────────────
919
-
920
- function printReport(enriched, allRoutes, cwd) {
921
- console.log();
922
- console.log(bold(" Scan Results"));
923
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
924
-
925
- for (const [capId, analysis] of Object.entries(enriched)) {
926
- const { codeAnalysis: a } = analysis;
927
- if (!a) continue;
928
-
929
- console.log();
930
- const epTag = a.isEntryPoint ? cyan(" [entry]") : "";
931
- console.log(` ${green("●")} ${bold(capId)}${epTag}`);
932
- if (a.sourceFiles?.length) console.log(gray(` files: `) + a.sourceFiles.join(", "));
933
- if (a.functions?.length) console.log(gray(` funcs: `) + a.functions.join(", "));
934
- if (a.services?.length) console.log(gray(` services: `) + cyan(a.services.join(", ")));
935
- if (a.dbCalls?.length) console.log(gray(` db: `) + a.dbCalls.slice(0, 4).join(", "));
936
- if (a.httpChains?.length) {
937
- console.log(gray(` chains: `));
938
- for (const chain of a.httpChains.slice(0, 5)) {
939
- console.log(gray(` `) + cyan(chain));
940
- }
941
- } else if (a.httpCallUrls?.length) {
942
- console.log(gray(` calls: `) + a.httpCallUrls.slice(0, 4).join(", "));
943
- } else if (a.httpCalls?.length) {
944
- console.log(gray(` http: `) + a.httpCalls.slice(0, 4).join(", "));
945
- }
946
- if (a.throws?.length) console.log(gray(` throws: `) + yellow(a.throws.join(", ")));
947
- }
948
-
949
- // Show discovered routes summary
950
- if (allRoutes.length) {
951
- console.log();
952
- console.log(bold(" Discovered Routes"));
953
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
954
- const byFile = new Map();
955
- for (const r of allRoutes) {
956
- const rel = path.relative(cwd, r.filePath);
957
- if (!byFile.has(rel)) byFile.set(rel, []);
958
- byFile.get(rel).push(r);
959
- }
960
- for (const [file, routes] of byFile) {
961
- console.log(gray(`\n ${file}`));
962
- for (const r of routes) {
963
- const name = r.method !== "*" ? capNameFromRoute(r.method, r.path) : "";
964
- const tag = name ? gray(` → ${name}`) : "";
965
- console.log(` ${cyan(r.method.padEnd(7))} ${r.path}${tag}`);
966
- }
967
- }
968
- }
969
-
970
- console.log();
971
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
972
- }
973
-
974
- // ── entry point ───────────────────────────────────────────────────────────────
975
-
976
- export async function scanCommand(rawArgs) {
977
- const args = rawArgs || [];
978
- const dryRun = args.includes("--dry-run");
979
- const jsonMode = args.includes("--json");
980
- const suggestMode = args.includes("--suggest") || args.includes("-s");
981
- const dirIdx = args.indexOf("--dir");
982
- const extraDirs = dirIdx !== -1 ? [args[dirIdx + 1]] : [];
983
- const capFilter = (() => { const i = args.indexOf("--capability"); return i !== -1 ? args[i + 1] : null; })();
984
-
985
- const cwd = process.cwd();
986
- const infernoDir = path.join(cwd, "inferno");
987
-
988
- // Load capabilities
989
- const capsPath = path.join(infernoDir, "capabilities.json");
990
- if (!fs.existsSync(capsPath)) {
991
- console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
992
- process.exit(1);
993
- }
994
- let capabilities;
995
- let capsFileIsObject = false;
996
- let capsFileWrapper = null;
997
- try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
998
- catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
999
-
1000
- if (!Array.isArray(capabilities)) {
1001
- if (capabilities.capabilities) {
1002
- capsFileIsObject = true;
1003
- capsFileWrapper = capabilities;
1004
- capabilities = capabilities.capabilities;
1005
- }
1006
- else { console.error(red("✗ Unexpected capabilities.json format.")); process.exit(1); }
1007
- }
1008
-
1009
- // Filter by --capability flag
1010
- const targetCaps = capFilter
1011
- ? capabilities.filter(c => c.id === capFilter || (c.name || "").toLowerCase() === capFilter.toLowerCase())
1012
- : capabilities;
1013
-
1014
- if (targetCaps.length === 0 && !suggestMode) {
1015
- console.log(yellow(capFilter ? `No capability matched: ${capFilter}` : "No capabilities found."));
1016
- process.exit(0);
1017
- }
1018
-
1019
- // Walk source files
1020
- const scanDirs = [cwd, ...extraDirs];
1021
- if (!jsonMode) process.stdout.write(gray(" Walking source files…"));
1022
- const files = [];
1023
- for (const dir of scanDirs) {
1024
- for (const f of walkFiles(dir)) files.push(f);
1025
- }
1026
- if (!jsonMode) process.stdout.write(`\r Found ${files.length} source files. \n`);
1027
-
1028
- // Analyze files
1029
- if (!jsonMode) process.stdout.write(gray(" Analyzing…"));
1030
- const allFunctions = []; // { fn, filePath }
1031
- const allRoutes = []; // route definitions discovered
1032
- let analyzed = 0;
1033
- for (const filePath of files) {
1034
- const { functions, routes } = analyzeFile(filePath);
1035
- for (const fn of functions) allFunctions.push({ fn, filePath });
1036
- for (const r of routes) allRoutes.push(r);
1037
- analyzed++;
1038
- if (!jsonMode && analyzed % 20 === 0) {
1039
- process.stdout.write(`\r Analyzed ${analyzed}/${files.length} files…`);
1040
- }
1041
- }
1042
- if (!jsonMode) {
1043
- process.stdout.write(
1044
- `\r Analyzed ${files.length} files · ${allFunctions.length} functions · ${allRoutes.length} routes \n`
1045
- );
1046
- }
1047
-
1048
- // Classify entry points
1049
- const classifiedFunctions = classifyEntryPoints(allFunctions, allRoutes);
1050
-
1051
- // Build end-to-end HTTP chains
1052
- const httpChains = buildHttpChains(classifiedFunctions, allRoutes, cwd);
1053
- const resolvedChainCount = [...httpChains.values()].flat().filter(s => s.resolved).length;
1054
- if (!jsonMode && httpChains.size > 0) {
1055
- process.stdout.write(
1056
- gray(` Resolved ${resolvedChainCount} HTTP chain${resolvedChainCount !== 1 ? "s" : ""} end-to-end\n`)
1057
- );
1058
- }
1059
-
1060
- // --suggest mode: skip capability matching, just show candidates
1061
- if (suggestMode) {
1062
- printSuggestions(classifiedFunctions, allRoutes, capabilities, cwd);
1063
- return;
1064
- }
1065
-
1066
- // Map functions to capabilities
1067
- const enriched = {}; // capId → { ...cap, codeAnalysis: {...} }
1068
-
1069
- for (const cap of targetCaps) {
1070
- enriched[cap.id] = { ...cap, codeAnalysis: null };
1071
- }
1072
-
1073
- for (const { fn, filePath } of classifiedFunctions) {
1074
- const match = matchFunctionToCapability(fn, targetCaps);
1075
- if (!match) continue;
1076
- const { cap } = match;
1077
- const existing = enriched[cap.id]?.codeAnalysis || {};
1078
- const chainSteps = httpChains.get(fn.name) || [];
1079
- enriched[cap.id].codeAnalysis = mergeAnalysis(existing, fn, filePath, cwd, chainSteps);
1080
- }
1081
-
1082
- // Compute stats
1083
- const total = Object.keys(enriched).length;
1084
- const matched = Object.values(enriched).filter(e => e.codeAnalysis).length;
1085
-
1086
- if (jsonMode) {
1087
- const out = {
1088
- scannedAt: new Date().toISOString(),
1089
- files: files.length,
1090
- functions: allFunctions.length,
1091
- routes: allRoutes,
1092
- httpChains: Object.fromEntries(httpChains),
1093
- capabilities: Object.entries(enriched).map(([id, data]) => ({
1094
- id,
1095
- name: data.name || data.title,
1096
- codeAnalysis: data.codeAnalysis,
1097
- })),
1098
- };
1099
- console.log(JSON.stringify(out, null, 2));
1100
- return;
1101
- }
1102
-
1103
- printReport(enriched, allRoutes, cwd);
1104
- console.log(` ${green("✔")} Matched ${matched}/${total} capabilities to source functions`);
1105
- if (allRoutes.length) {
1106
- console.log(` ${green("✔")} Discovered ${allRoutes.length} route${allRoutes.length !== 1 ? "s" : ""} — run ${cyan("infernoflow scan --suggest")} to see untracked ones`);
1107
- }
1108
- console.log();
1109
-
1110
- if (dryRun) {
1111
- console.log(yellow(" --dry-run: no files written."));
1112
- return;
1113
- }
1114
-
1115
- // Write scan.json
1116
- const scanData = {
1117
- scannedAt: new Date().toISOString(),
1118
- files: files.length,
1119
- functions: allFunctions.length,
1120
- routes: allRoutes,
1121
- httpChains: Object.fromEntries(httpChains),
1122
- capabilities: Object.entries(enriched).map(([id, data]) => ({
1123
- id,
1124
- name: data.name || data.title,
1125
- codeAnalysis: data.codeAnalysis,
1126
- })),
1127
- };
1128
- const scanPath = path.join(infernoDir, "scan.json");
1129
- fs.writeFileSync(scanPath, JSON.stringify(scanData, null, 2));
1130
- console.log(gray(` Saved → inferno/scan.json`));
1131
-
1132
- // Enrich capabilities.json
1133
- let changed = 0;
1134
- const updatedCaps = capabilities.map(cap => {
1135
- const analysis = enriched[cap.id]?.codeAnalysis;
1136
- if (!analysis) return cap;
1137
- changed++;
1138
- return { ...cap, codeAnalysis: analysis };
1139
- });
1140
-
1141
- if (changed > 0) {
1142
- const toWrite = capsFileIsObject
1143
- ? { ...capsFileWrapper, capabilities: updatedCaps }
1144
- : updatedCaps;
1145
- fs.writeFileSync(capsPath, JSON.stringify(toWrite, null, 2));
1146
- console.log(gray(` Updated ${changed} capability entries in capabilities.json`));
1147
- }
1148
-
1149
- console.log();
1150
- if (!ts) {
1151
- console.log(yellow(" ⚠ TypeScript compiler not found — JS/TS analyzed with regex fallback."));
1152
- console.log(gray(` For deeper analysis: npm install -g typescript`));
1153
- console.log();
1154
- }
1155
- }
43
+ `;function je(e){try{const t=ie(`python3 -c ${JSON.stringify(ve)} ${JSON.stringify(e)}`,{timeout:8e3,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),r=JSON.parse(t.trim()||"[]"),n=j.readFileSync(e,"utf8");return r.map(s=>({...s,services:I(n),dbCalls:M(n),httpCalls:H(n),httpCallUrls:L(n),isExported:!1}))}catch{return null}}const Te=[{re:/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm,lang:"go"},{re:/^\s*(?:def|async def)\s+(\w+)\s*\(/gm,lang:"py"},{re:/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm,lang:"java"},{re:/^\s*def\s+(\w+)\s*[\(\|]/gm,lang:"rb"}];function q(e,t){const r=x.extname(e).slice(1),n=Te.find(i=>i.lang===r);if(!n)return null;const s=[],o=new RegExp(n.re.source,"gm");let l;for(;(l=o.exec(t))!==null;){const i=l.index,a=Math.min(i+2e3,t.length),p=t.slice(i,a);s.push({name:l[1],calls:[],throws:[],services:I(p),dbCalls:M(p),httpCalls:H(p),httpCallUrls:L(p),isExported:!1,loc:t.slice(0,i).split(`
44
+ `).length})}return s.length>0?s:null}const Ae=new Set(["node_modules",".git","dist","build","out",".next",".nuxt","coverage","__pycache__",".pytest_cache","vendor","tmp",".turbo","target",".gradle","public","static","assets"]),Oe=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".rb",".java"]),Re=/\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;function*X(e){let t;try{t=j.readdirSync(e,{withFileTypes:!0})}catch{return}for(const r of t)if(r.isDirectory())Ae.has(r.name)||(yield*X(x.join(e,r.name)));else if(r.isFile()){const n=x.extname(r.name);Oe.has(n)&&!Re.test(r.name)&&(yield x.join(e,r.name))}}function _e(e){let t;try{t=j.readFileSync(e,"utf8")}catch{return{functions:[],routes:[]}}const r=x.extname(e);let n=[];[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(r)?n=Ee(e,t)||q(e,t)||[]:r===".py"?n=je(e)||q(e,t)||[]:n=q(e,t)||[];const s=[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(r)?we(e,t):[];return{functions:n,routes:s}}function Fe(e,t){const r=new Set;for(const s of t)s.handler&&r.add(s.handler);const n=new Map;for(const{fn:s}of e)for(const o of s.calls||[]){const l=o.replace("()","");n.set(l,(n.get(l)||0)+1)}return e.map(({fn:s,filePath:o})=>{const l=r.has(s.name),i=s.isExported||!1,a=n.get(s.name)||0,p=(s.dbCalls?.length||0)+(s.services?.length||0)+(s.httpCallUrls?.length||0)>0,c=l||i||p&&a===0,f=!c&&a>0;return{fn:{...s,isEntryPoint:c,isHelper:f,callerCount:a},filePath:o}})}function Pe(e){const t=new Map;for(const r of e){const n=r.path.replace(/:[^/]+/g,":*"),s=`${r.method} ${n}`;t.has(s)||t.set(s,r);const o=`${r.method} ${r.path}`;t.has(o)||t.set(o,r)}return t}function Ne(e,t,r){const n=t.split("?")[0],s=r.get(`${e} ${n}`);if(s)return s;const o=n.replace(/\/[0-9a-f-]{8,}|\/\d+/g,"/:*"),l=`${e} ${o}`,i=r.get(l);if(i)return i;const a=n.replace(/^\/api\/v?\d+/,"").replace(/^\/api/,"");if(a!==n){const c=`${e} ${a}`,f=r.get(c);if(f)return f}const p=`* ${n}`;return r.get(p)||null}function Ue(e,t,r){const n=Pe(t),s=new Map;for(const{fn:i,filePath:a}of e)s.has(i.name)||s.set(i.name,{fn:i,filePath:a});const o=new Map;function l(i,a,p){if(a>3||p.has(i))return[];p.add(i);const c=s.get(i);if(!c)return[];const f=[];for(const{method:m,url:w}of c.fn.httpCallUrls||[]){const y=Ne(m,w,n),h={caller:i,method:m,url:w,resolved:!!y,handler:y?.handler||null,handlerFile:y?x.relative(r,y.filePath):null,suggestedName:y?k(y.method,y.path):null};if(f.push(h),y?.handler&&!p.has(y.handler)){const $=l(y.handler,a+1,new Set(p));f.push(...$.map(v=>({...v,via:i})))}}return f}for(const{fn:i}of e){if((i.httpCallUrls||[]).length===0)continue;const a=l(i.name,0,new Set);a.length&&o.set(i.name,a)}return o}function W(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(t=>t.length>1)}function Y(e,t){const r=new Set(e),n=new Set(t);let s=0;for(const l of r)n.has(l)&&s++;const o=r.size+n.size-s;return o===0?0:s/o}function Z(e,t){const r=W(e.name);let n=null,s=0;for(const o of t){const l=Math.max(Y(r,W(o.id||"")),Y(r,W(o.name||o.title||"")));l>s&&(s=l,n=o)}return s>=.2?{cap:n,score:s}:null}function De(e={},t,r,n,s){const o=x.relative(n,r),l=(a=[],p=[])=>[...new Set([...a,...p])],i=(s||[]).map(a=>{const p=a.handler?` \u2192 ${a.handler}`:a.resolved?"":" [unresolved]";return`${a.caller} \u2192 ${a.method} ${a.url}${p}`});return{functions:l(e.functions,[t.name]),sourceFiles:l(e.sourceFiles,[o]),calls:l(e.calls,t.calls),throws:l(e.throws,t.throws),services:l(e.services,t.services),dbCalls:l(e.dbCalls,t.dbCalls),httpCalls:l(e.httpCalls,t.httpCalls),httpCallUrls:l(e.httpCallUrls||[],(t.httpCallUrls||[]).map(a=>`${a.method} ${a.url}`)),httpChains:l(e.httpChains||[],i),isEntryPoint:t.isEntryPoint||e.isEntryPoint||!1,scannedAt:new Date().toISOString()}}function Ie(e,t,r,n){const s=new Set(r.map(c=>c.id)),o=new Set(r.map(c=>(c.name||c.title||"").toLowerCase()));console.log(),console.log(_(" Capability Candidates")),console.log(d(" Untracked entry points discovered in your codebase:")),console.log(d(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));const l=new Set,i=[];for(const c of t){const f=k(c.method,c.path),m=K(f);if(s.has(m)||o.has(f.toLowerCase())||l.has(m))continue;l.add(m);const w=x.relative(n,c.filePath);i.push({id:m,name:f,source:`${c.method} ${c.path}`,file:w,confidence:"high"})}for(const{fn:c,filePath:f}of e){if(!c.isEntryPoint||c.name==="<anonymous>"||c.name.length<3)continue;const m=Z(c,r);if(m&&m.score>=.35)continue;const w=K(c.name);if(s.has(w)||l.has(w))continue;l.add(w);const y=x.relative(n,f);i.push({id:w,name:c.name,source:`function in ${y}:${c.loc}`,file:y,confidence:"medium"})}if(!i.length){console.log(d(" All entry points are already tracked as capabilities. \u2713")),console.log();return}const a=i.filter(c=>c.confidence==="high"),p=i.filter(c=>c.confidence==="medium");if(a.length){console.log(),console.log(T(" \u25CF High confidence (from route definitions):"));for(const c of a)console.log(` ${F(c.id.padEnd(35))} ${d(c.source)}`)}if(p.length){console.log(),console.log(T(" \u25CF Medium confidence (exported / top-level functions):"));for(const c of p.slice(0,10))console.log(` ${R(c.id.padEnd(35))} ${d(c.source)}`);p.length>10&&console.log(d(` \u2026 and ${p.length-10} more`))}console.log(),console.log(d(" To add these, run:"));for(const c of[...a,...p.slice(0,3)])console.log(d(` infernoflow add "${c.id}" "${c.name}"`));console.log()}function Me(e,t,r){console.log(),console.log(_(" Scan Results")),console.log(d(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));for(const[n,s]of Object.entries(e)){const{codeAnalysis:o}=s;if(!o)continue;console.log();const l=o.isEntryPoint?T(" [entry]"):"";if(console.log(` ${F("\u25CF")} ${_(n)}${l}`),o.sourceFiles?.length&&console.log(d(" files: ")+o.sourceFiles.join(", ")),o.functions?.length&&console.log(d(" funcs: ")+o.functions.join(", ")),o.services?.length&&console.log(d(" services: ")+T(o.services.join(", "))),o.dbCalls?.length&&console.log(d(" db: ")+o.dbCalls.slice(0,4).join(", ")),o.httpChains?.length){console.log(d(" chains: "));for(const i of o.httpChains.slice(0,5))console.log(d(" ")+T(i))}else o.httpCallUrls?.length?console.log(d(" calls: ")+o.httpCallUrls.slice(0,4).join(", ")):o.httpCalls?.length&&console.log(d(" http: ")+o.httpCalls.slice(0,4).join(", "));o.throws?.length&&console.log(d(" throws: ")+R(o.throws.join(", ")))}if(t.length){console.log(),console.log(_(" Discovered Routes")),console.log(d(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));const n=new Map;for(const s of t){const o=x.relative(r,s.filePath);n.has(o)||n.set(o,[]),n.get(o).push(s)}for(const[s,o]of n){console.log(d(`
45
+ ${s}`));for(const l of o){const i=l.method!=="*"?k(l.method,l.path):"",a=i?d(` \u2192 ${i}`):"";console.log(` ${T(l.method.padEnd(7))} ${l.path}${a}`)}}}console.log(),console.log(d(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"))}async function ze(e){const t=e||[],r=t.includes("--dry-run"),n=t.includes("--json"),s=t.includes("--suggest")||t.includes("-s"),o=t.indexOf("--dir"),l=o!==-1?[t[o+1]]:[],i=(()=>{const u=t.indexOf("--capability");return u!==-1?t[u+1]:null})(),a=process.cwd(),p=x.join(a,"inferno"),c=x.join(p,"capabilities.json");j.existsSync(c)||(console.error(D("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let f,m=!1,w=null;try{f=JSON.parse(j.readFileSync(c,"utf8"))}catch(u){console.error(D("\u2717 Failed to parse capabilities.json: "+u.message)),process.exit(1)}Array.isArray(f)||(f.capabilities?(m=!0,w=f,f=f.capabilities):(console.error(D("\u2717 Unexpected capabilities.json format.")),process.exit(1)));const y=i?f.filter(u=>u.id===i||(u.name||"").toLowerCase()===i.toLowerCase()):f;y.length===0&&!s&&(console.log(R(i?`No capability matched: ${i}`:"No capabilities found.")),process.exit(0));const h=[a,...l];n||process.stdout.write(d(" Walking source files\u2026"));const $=[];for(const u of h)for(const C of X(u))$.push(C);n||process.stdout.write(`\r Found ${$.length} source files.
46
+ `),n||process.stdout.write(d(" Analyzing\u2026"));const v=[],b=[];let P=0;for(const u of $){const{functions:C,routes:E}=_e(u);for(const A of C)v.push({fn:A,filePath:u});for(const A of E)b.push(A);P++,!n&&P%20===0&&process.stdout.write(`\r Analyzed ${P}/${$.length} files\u2026`)}n||process.stdout.write(`\r Analyzed ${$.length} files \xB7 ${v.length} functions \xB7 ${b.length} routes
47
+ `);const N=Fe(v,b),O=Ue(N,b,a),J=[...O.values()].flat().filter(u=>u.resolved).length;if(!n&&O.size>0&&process.stdout.write(d(` Resolved ${J} HTTP chain${J!==1?"s":""} end-to-end
48
+ `)),s){Ie(N,b,f,a);return}const S={};for(const u of y)S[u.id]={...u,codeAnalysis:null};for(const{fn:u,filePath:C}of N){const E=Z(u,y);if(!E)continue;const{cap:A}=E,ne=S[A.id]?.codeAnalysis||{},oe=O.get(u.name)||[];S[A.id].codeAnalysis=De(ne,u,C,a,oe)}const Q=Object.keys(S).length,ee=Object.values(S).filter(u=>u.codeAnalysis).length;if(n){const u={scannedAt:new Date().toISOString(),files:$.length,functions:v.length,routes:b,httpChains:Object.fromEntries(O),capabilities:Object.entries(S).map(([C,E])=>({id:C,name:E.name||E.title,codeAnalysis:E.codeAnalysis}))};console.log(JSON.stringify(u,null,2));return}if(Me(S,b,a),console.log(` ${F("\u2714")} Matched ${ee}/${Q} capabilities to source functions`),b.length&&console.log(` ${F("\u2714")} Discovered ${b.length} route${b.length!==1?"s":""} \u2014 run ${T("infernoflow scan --suggest")} to see untracked ones`),console.log(),r){console.log(R(" --dry-run: no files written."));return}const te={scannedAt:new Date().toISOString(),files:$.length,functions:v.length,routes:b,httpChains:Object.fromEntries(O),capabilities:Object.entries(S).map(([u,C])=>({id:u,name:C.name||C.title,codeAnalysis:C.codeAnalysis}))},se=x.join(p,"scan.json");j.writeFileSync(se,JSON.stringify(te,null,2)),console.log(d(" Saved \u2192 inferno/scan.json"));let U=0;const B=f.map(u=>{const C=S[u.id]?.codeAnalysis;return C?(U++,{...u,codeAnalysis:C}):u});if(U>0){const u=m?{...w,capabilities:B}:B;j.writeFileSync(c,JSON.stringify(u,null,2)),console.log(d(` Updated ${U} capability entries in capabilities.json`))}console.log(),g||(console.log(R(" \u26A0 TypeScript compiler not found \u2014 JS/TS analyzed with regex fallback.")),console.log(d(" For deeper analysis: npm install -g typescript")),console.log())}export{ze as scanCommand};