infernoflow 0.32.8 → 0.32.9

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 (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -321
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,231 +1,4 @@
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
- * Enriches capabilities.json with a `codeAnalysis` block on each capability,
9
- * and saves the full scan report to inferno/scan.json.
10
- *
11
- * Usage:
12
- * infernoflow scan Scan project, enrich capabilities
13
- * infernoflow scan --dir src/ Scan specific directory
14
- * infernoflow scan --json Print scan.json to stdout
15
- * infernoflow scan --dry-run Print without writing files
16
- * infernoflow scan --capability auth-login Scan one capability only
17
- */
18
-
19
- import * as fs from "node:fs";
20
- import * as path from "node:path";
21
- import { createRequire } from "node:module";
22
- import { execSync } from "node:child_process";
23
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
24
-
25
- const require = createRequire(import.meta.url);
26
-
27
- // ── TypeScript compiler API (global install) ──────────────────────────────────
28
-
29
- const TS_PATHS = [
30
- "/usr/local/lib/node_modules_global/lib/node_modules/typescript",
31
- "/usr/lib/node_modules/typescript",
32
- path.join(process.env.HOME || "", ".npm-global/lib/node_modules/typescript"),
33
- ];
34
-
35
- function loadTypeScript() {
36
- for (const p of TS_PATHS) {
37
- try { return require(path.join(p, "lib/typescript.js")); } catch {}
38
- }
39
- try { return require("typescript"); } catch {}
40
- return null;
41
- }
42
-
43
- const ts = loadTypeScript();
44
-
45
- // ── external service fingerprints ────────────────────────────────────────────
46
-
47
- const SERVICE_PATTERNS = [
48
- { service: "stripe", patterns: ["stripe", "Stripe", "createPaymentIntent", "charges.create"] },
49
- { service: "sendgrid", patterns: ["sendgrid", "@sendgrid", "sgMail", "sendgrid.send"] },
50
- { service: "ses", patterns: ["SES", "ses.sendEmail", "aws-sdk/ses", "nodemailer"] },
51
- { service: "s3", patterns: ["S3", "s3.upload", "s3.getObject", "PutObjectCommand", "@aws-sdk/s3"] },
52
- { service: "redis", patterns: ["redis", "Redis", "ioredis", "createClient"] },
53
- { service: "jwt", patterns: ["jwt", "jsonwebtoken", "sign(", "verify(", "decode("] },
54
- { service: "bcrypt", patterns: ["bcrypt", "argon2", "scrypt", "hashSync", "compare("] },
55
- { service: "prisma", patterns: ["prisma.", "PrismaClient", "@prisma/client"] },
56
- { service: "mongoose", patterns: ["mongoose", ".save()", ".findOne(", ".aggregate("] },
57
- { service: "postgres", patterns: ["pg", "Pool(", "Client(", "query(", "postgres("] },
58
- { service: "mysql", patterns: ["mysql", "mysql2", "createConnection"] },
59
- { service: "graphql", patterns: ["graphql", "gql`", "ApolloServer", "GraphQLSchema"] },
60
- { service: "firebase", patterns: ["firebase", "firestore", "initializeApp"] },
61
- { service: "twilio", patterns: ["twilio", "Twilio(", "messages.create"] },
62
- { service: "openai", patterns: ["openai", "OpenAI(", "createCompletion", "chat.completions"] },
63
- ];
64
-
65
- function detectServices(text) {
66
- const found = new Set();
67
- for (const { service, patterns } of SERVICE_PATTERNS) {
68
- if (patterns.some(p => text.includes(p))) found.add(service);
69
- }
70
- return [...found];
71
- }
72
-
73
- // ── DB call patterns ──────────────────────────────────────────────────────────
74
-
75
- const DB_PATTERNS = [
76
- /\.(find|findOne|findMany|findById|findAll)\s*\(/g,
77
- /\.(create|insert|insertOne|insertMany|save)\s*\(/g,
78
- /\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,
79
- /\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,
80
- /\.(query|execute|raw)\s*\(/g,
81
- /\.(aggregate|groupBy|count|sum)\s*\(/g,
82
- /db\.\w+\s*\(/g,
83
- /prisma\.\w+\.\w+\s*\(/g,
84
- ];
85
-
86
- function detectDbCalls(text) {
87
- const calls = new Set();
88
- for (const re of DB_PATTERNS) {
89
- const r = new RegExp(re.source, "g");
90
- let m;
91
- while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
92
- }
93
- return [...calls].slice(0, 10);
94
- }
95
-
96
- // ── HTTP call patterns ────────────────────────────────────────────────────────
97
-
98
- const HTTP_PATTERNS = [
99
- /fetch\s*\(/g,
100
- /axios\.(get|post|put|patch|delete)\s*\(/g,
101
- /http\.(get|post|request)\s*\(/g,
102
- /got\.(get|post|put|delete)\s*\(/g,
103
- /request\.(get|post|put|delete)\s*\(/g,
104
- /\$http\.(get|post|put|delete)\s*\(/g,
105
- ];
106
-
107
- function detectHttpCalls(text) {
108
- const calls = new Set();
109
- for (const re of HTTP_PATTERNS) {
110
- const r = new RegExp(re.source, "g");
111
- let m;
112
- while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
113
- }
114
- return [...calls].slice(0, 8);
115
- }
116
-
117
- // ── TypeScript / JavaScript AST analysis ─────────────────────────────────────
118
-
119
- function getNodeName(node) {
120
- if (!ts) return null;
121
- if (node.name && ts.isIdentifier(node.name)) return node.name.text;
122
- return null;
123
- }
124
-
125
- /**
126
- * Walk all descendants of root using node.forEachChild (instance method).
127
- * Collects all call expressions and throw statements globally,
128
- * then assigns them to containing functions by source position range.
129
- */
130
- function collectAllNodes(root) {
131
- const calls = []; // { pos, end, name }
132
- const throws = []; // { pos, end, name }
133
-
134
- function walk(node) {
135
- if (ts.isCallExpression(node)) {
136
- const expr = node.expression;
137
- if (ts.isIdentifier(expr)) {
138
- calls.push({ pos: node.pos, end: node.end, name: expr.text + "()" });
139
- } else if (ts.isPropertyAccessExpression(expr)) {
140
- calls.push({ pos: node.pos, end: node.end, name: expr.name.text + "()" });
141
- }
142
- }
143
- if (ts.isThrowStatement(node) && node.expression) {
144
- if (ts.isNewExpression(node.expression) && ts.isIdentifier(node.expression.expression)) {
145
- throws.push({ pos: node.pos, end: node.end, name: node.expression.expression.text });
146
- }
147
- }
148
- node.forEachChild?.(walk);
149
- }
150
- walk(root);
151
- return { calls, throws };
152
- }
153
-
154
- function callsInRange(allCalls, pos, end) {
155
- return [...new Set(
156
- allCalls.filter(c => c.pos >= pos && c.end <= end).map(c => c.name)
157
- )].slice(0, 20);
158
- }
159
-
160
- function throwsInRange(allThrows, pos, end) {
161
- return [...new Set(
162
- allThrows.filter(t => t.pos >= pos && t.end <= end).map(t => t.name)
163
- )];
164
- }
165
-
166
- function isFunctionNode(node) {
167
- if (!ts) return false;
168
- return (
169
- ts.isFunctionDeclaration(node) ||
170
- ts.isFunctionExpression(node) ||
171
- ts.isArrowFunction(node) ||
172
- ts.isMethodDeclaration(node)
173
- );
174
- }
175
-
176
- function getParentVariableName(node) {
177
- // For arrow functions assigned to const: const foo = () => {}
178
- if (!ts) return null;
179
- if (node.parent && ts.isVariableDeclaration(node.parent)) {
180
- return getNodeName(node.parent);
181
- }
182
- if (node.parent && ts.isPropertyAssignment(node.parent)) {
183
- return getNodeName(node.parent);
184
- }
185
- return null;
186
- }
187
-
188
- function analyzeJsTs(filePath, code) {
189
- if (!ts) return null;
190
-
191
- let srcFile;
192
- try {
193
- srcFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, /*setParentNodes*/ true);
194
- } catch {
195
- return null;
196
- }
197
-
198
- // Collect ALL call/throw nodes in one pass from root
199
- const { calls: allCalls, throws: allThrows } = collectAllNodes(srcFile);
200
-
201
- const functions = [];
202
-
203
- function visit(node) {
204
- if (isFunctionNode(node)) {
205
- const name = getNodeName(node) || getParentVariableName(node) || "<anonymous>";
206
- const text = code.slice(node.pos, node.end);
207
- const calls = callsInRange(allCalls, node.pos, node.end);
208
- const throws = throwsInRange(allThrows, node.pos, node.end);
209
- functions.push({
210
- name,
211
- calls,
212
- throws,
213
- services: detectServices(text),
214
- dbCalls: detectDbCalls(text),
215
- httpCalls: detectHttpCalls(text),
216
- loc: srcFile.getLineAndCharacterOfPosition(node.pos).line + 1,
217
- });
218
- }
219
- node.forEachChild?.(visit);
220
- }
221
-
222
- visit(srcFile);
223
- return functions;
224
- }
225
-
226
- // ── Python AST analysis via child_process ─────────────────────────────────────
227
-
228
- const PYTHON_SCRIPT = `
1
+ import*as w from"node:fs";import*as g from"node:path";import{createRequire as V}from"node:module";import{execSync as G}from"node:child_process";import{bold as P,cyan as K,gray as u,green as k,yellow as j,red as F}from"../ui/output.mjs";const z=V(import.meta.url),Q=["/usr/local/lib/node_modules_global/lib/node_modules/typescript","/usr/lib/node_modules/typescript",g.join(process.env.HOME||"",".npm-global/lib/node_modules/typescript")];function X(){for(const e of Q)try{return z(g.join(e,"lib/typescript.js"))}catch{}try{return z("typescript")}catch{}return null}const l=X(),Y=[{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 O(e){const t=new Set;for(const{service:n,patterns:s}of Y)s.some(i=>e.includes(i))&&t.add(n);return[...t]}const Z=[/\.(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 T(e){const t=new Set;for(const n of Z){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,10)}const ee=[/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 _(e){const t=new Set;for(const n of ee){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,8)}function N(e){return l&&e.name&&l.isIdentifier(e.name)?e.name.text:null}function se(e){const t=[],n=[];function s(i){if(l.isCallExpression(i)){const r=i.expression;l.isIdentifier(r)?t.push({pos:i.pos,end:i.end,name:r.text+"()"}):l.isPropertyAccessExpression(r)&&t.push({pos:i.pos,end:i.end,name:r.name.text+"()"})}l.isThrowStatement(i)&&i.expression&&l.isNewExpression(i.expression)&&l.isIdentifier(i.expression.expression)&&n.push({pos:i.pos,end:i.end,name:i.expression.expression.text}),i.forEachChild?.(s)}return s(e),{calls:t,throws:n}}function te(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))].slice(0,20)}function ne(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))]}function ie(e){return l?l.isFunctionDeclaration(e)||l.isFunctionExpression(e)||l.isArrowFunction(e)||l.isMethodDeclaration(e):!1}function oe(e){return l&&(e.parent&&l.isVariableDeclaration(e.parent)||e.parent&&l.isPropertyAssignment(e.parent))?N(e.parent):null}function re(e,t){if(!l)return null;let n;try{n=l.createSourceFile(e,t,l.ScriptTarget.Latest,!0)}catch{return null}const{calls:s,throws:i}=se(n),r=[];function a(c){if(ie(c)){const b=N(c)||oe(c)||"<anonymous>",f=t.slice(c.pos,c.end),d=te(s,c.pos,c.end),x=ne(i,c.pos,c.end);r.push({name:b,calls:d,throws:x,services:O(f),dbCalls:T(f),httpCalls:_(f),loc:n.getLineAndCharacterOfPosition(c.pos).line+1})}c.forEachChild?.(a)}return a(n),r}const ce=`
229
2
  import ast, json, sys
230
3
 
231
4
  def get_calls(node):
@@ -263,333 +36,7 @@ try:
263
36
  print(json.dumps(functions))
264
37
  except Exception as e:
265
38
  print(json.dumps([]))
266
- `;
267
-
268
- function analyzePython(filePath) {
269
- try {
270
- const result = execSync(
271
- `python3 -c ${JSON.stringify(PYTHON_SCRIPT)} ${JSON.stringify(filePath)}`,
272
- { timeout: 8000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
273
- );
274
- const fns = JSON.parse(result.trim() || "[]");
275
- // add service/db/http detection from raw file text
276
- const code = fs.readFileSync(filePath, "utf8");
277
- return fns.map(f => ({
278
- ...f,
279
- services: detectServices(code),
280
- dbCalls: detectDbCalls(code),
281
- httpCalls: detectHttpCalls(code),
282
- }));
283
- } catch {
284
- return null;
285
- }
286
- }
287
-
288
- // ── regex fallback (Go, Ruby, Java, other) ────────────────────────────────────
289
-
290
- const FUNC_PATTERNS = [
291
- { re: /^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm, lang: "go" },
292
- { re: /^\s*(?:def|async def)\s+(\w+)\s*\(/gm, lang: "py" },
293
- { re: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm, lang: "java" },
294
- { re: /^\s*def\s+(\w+)\s*[\(\|]/gm, lang: "rb" },
295
- ];
296
-
297
- function analyzeWithRegex(filePath, code) {
298
- const ext = path.extname(filePath).slice(1);
299
- const pattern = FUNC_PATTERNS.find(p => p.lang === ext);
300
- if (!pattern) return null;
301
-
302
- const functions = [];
303
- const r = new RegExp(pattern.re.source, "gm");
304
- let m;
305
- while ((m = r.exec(code)) !== null) {
306
- // grab up to 60 lines after the match for context
307
- const start = m.index;
308
- const end = Math.min(start + 2000, code.length);
309
- const chunk = code.slice(start, end);
310
- functions.push({
311
- name: m[1],
312
- calls: [],
313
- throws: [],
314
- services: detectServices(chunk),
315
- dbCalls: detectDbCalls(chunk),
316
- httpCalls: detectHttpCalls(chunk),
317
- loc: code.slice(0, start).split("\n").length,
318
- });
319
- }
320
- return functions.length > 0 ? functions : null;
321
- }
322
-
323
- // ── file walker ───────────────────────────────────────────────────────────────
324
-
325
- const SKIP_DIRS = new Set([
326
- "node_modules", ".git", "dist", "build", "out", ".next", ".nuxt",
327
- "coverage", "__pycache__", ".pytest_cache", "vendor", "tmp", ".turbo",
328
- "target", ".gradle", "public", "static", "assets",
329
- ]);
330
-
331
- const SUPPORTED_EXTS = new Set([
332
- ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
333
- ".py", ".go", ".rb", ".java",
334
- ]);
335
-
336
- const TEST_FILE = /\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;
337
-
338
- function* walkFiles(dir) {
339
- let entries;
340
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
341
- catch { return; }
342
- for (const e of entries) {
343
- if (e.isDirectory()) {
344
- if (!SKIP_DIRS.has(e.name)) yield* walkFiles(path.join(dir, e.name));
345
- } else if (e.isFile()) {
346
- const ext = path.extname(e.name);
347
- if (SUPPORTED_EXTS.has(ext) && !TEST_FILE.test(e.name)) {
348
- yield path.join(dir, e.name);
349
- }
350
- }
351
- }
352
- }
353
-
354
- // ── per-file analyzer ─────────────────────────────────────────────────────────
355
-
356
- function analyzeFile(filePath) {
357
- let code;
358
- try { code = fs.readFileSync(filePath, "utf8"); }
359
- catch { return []; }
360
-
361
- const ext = path.extname(filePath);
362
-
363
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
364
- return analyzeJsTs(filePath, code) || analyzeWithRegex(filePath, code) || [];
365
- }
366
- if (ext === ".py") {
367
- return analyzePython(filePath) || analyzeWithRegex(filePath, code) || [];
368
- }
369
- return analyzeWithRegex(filePath, code) || [];
370
- }
371
-
372
- // ── capability matcher ────────────────────────────────────────────────────────
373
-
374
- function tokenise(str) {
375
- return str.replace(/([a-z])([A-Z])/g, "$1 $2")
376
- .toLowerCase().split(/[\s_\-/.]+/).filter(t => t.length > 1);
377
- }
378
-
379
- function overlap(a, b) {
380
- const sa = new Set(a), sb = new Set(b);
381
- let n = 0;
382
- for (const t of sa) if (sb.has(t)) n++;
383
- const u = sa.size + sb.size - n;
384
- return u === 0 ? 0 : n / u;
385
- }
386
-
387
- function matchFunctionToCapability(fn, capabilities) {
388
- const fnTokens = tokenise(fn.name);
389
- let best = null, bestScore = 0;
390
- for (const cap of capabilities) {
391
- const score = Math.max(
392
- overlap(fnTokens, tokenise(cap.id || "")),
393
- overlap(fnTokens, tokenise(cap.name || cap.title || "")),
394
- );
395
- if (score > bestScore) { bestScore = score; best = cap; }
396
- }
397
- return bestScore >= 0.2 ? { cap: best, score: bestScore } : null;
398
- }
399
-
400
- // ── merge analysis into capability ────────────────────────────────────────────
401
-
402
- function mergeAnalysis(existing = {}, fn, filePath, cwd) {
403
- const rel = path.relative(cwd, filePath);
404
-
405
- // merge arrays without duplicates
406
- const merge = (a = [], b = []) => [...new Set([...a, ...b])];
407
-
408
- return {
409
- functions: merge(existing.functions, [fn.name]),
410
- sourceFiles: merge(existing.sourceFiles, [rel]),
411
- calls: merge(existing.calls, fn.calls),
412
- throws: merge(existing.throws, fn.throws),
413
- services: merge(existing.services, fn.services),
414
- dbCalls: merge(existing.dbCalls, fn.dbCalls),
415
- httpCalls: merge(existing.httpCalls, fn.httpCalls),
416
- scannedAt: new Date().toISOString(),
417
- };
418
- }
419
-
420
- // ── reporters ─────────────────────────────────────────────────────────────────
421
-
422
- function printReport(enriched) {
423
- console.log();
424
- console.log(bold(" Scan Results"));
425
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
426
-
427
- for (const [capId, analysis] of Object.entries(enriched)) {
428
- const { codeAnalysis: a } = analysis;
429
- if (!a) continue;
430
-
431
- console.log();
432
- console.log(` ${green("●")} ${bold(capId)}`);
433
- if (a.sourceFiles?.length) console.log(gray(` files: `) + a.sourceFiles.join(", "));
434
- if (a.functions?.length) console.log(gray(` funcs: `) + a.functions.join(", "));
435
- if (a.services?.length) console.log(gray(` services: `) + cyan(a.services.join(", ")));
436
- if (a.dbCalls?.length) console.log(gray(` db: `) + a.dbCalls.slice(0, 4).join(", "));
437
- if (a.httpCalls?.length) console.log(gray(` http: `) + a.httpCalls.slice(0, 4).join(", "));
438
- if (a.throws?.length) console.log(gray(` throws: `) + yellow(a.throws.join(", ")));
439
- }
440
-
441
- console.log();
442
- console.log(gray(" ─────────────────────────────────────────────────────────────────"));
443
- }
444
-
445
- // ── entry point ───────────────────────────────────────────────────────────────
446
-
447
- export async function scanCommand(rawArgs) {
448
- const args = rawArgs || [];
449
- const dryRun = args.includes("--dry-run");
450
- const jsonMode = args.includes("--json");
451
- const dirIdx = args.indexOf("--dir");
452
- const extraDirs = dirIdx !== -1 ? [args[dirIdx + 1]] : [];
453
- const capFilter = (() => { const i = args.indexOf("--capability"); return i !== -1 ? args[i + 1] : null; })();
454
-
455
- const cwd = process.cwd();
456
- const infernoDir = path.join(cwd, "inferno");
457
-
458
- // Load capabilities
459
- const capsPath = path.join(infernoDir, "capabilities.json");
460
- if (!fs.existsSync(capsPath)) {
461
- console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
462
- process.exit(1);
463
- }
464
- let capabilities;
465
- let capsFileIsObject = false; // track original format so we can write it back correctly
466
- let capsFileWrapper = null;
467
- try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
468
- catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
469
-
470
- if (!Array.isArray(capabilities)) {
471
- // handle object format { capabilities: [...] }
472
- if (capabilities.capabilities) {
473
- capsFileIsObject = true;
474
- capsFileWrapper = capabilities; // preserve other top-level keys
475
- capabilities = capabilities.capabilities;
476
- }
477
- else { console.error(red("✗ Unexpected capabilities.json format.")); process.exit(1); }
478
- }
479
-
480
- // Filter by --capability flag
481
- const targetCaps = capFilter
482
- ? capabilities.filter(c => c.id === capFilter || (c.name || "").toLowerCase() === capFilter.toLowerCase())
483
- : capabilities;
484
-
485
- if (targetCaps.length === 0) {
486
- console.log(yellow(capFilter ? `No capability matched: ${capFilter}` : "No capabilities found."));
487
- process.exit(0);
488
- }
489
-
490
- // Walk source files
491
- const scanDirs = [cwd, ...extraDirs];
492
- if (!jsonMode) process.stdout.write(gray(" Walking source files…"));
493
- const files = [];
494
- for (const dir of scanDirs) {
495
- for (const f of walkFiles(dir)) files.push(f);
496
- }
497
- if (!jsonMode) process.stdout.write(`\r Found ${files.length} source files. \n`);
498
-
499
- // Analyze files
500
- if (!jsonMode) process.stdout.write(gray(" Analyzing…"));
501
- const allFunctions = []; // { fn, filePath }
502
- let analyzed = 0;
503
- for (const filePath of files) {
504
- const fns = analyzeFile(filePath);
505
- for (const fn of fns) allFunctions.push({ fn, filePath });
506
- analyzed++;
507
- if (!jsonMode && analyzed % 20 === 0) {
508
- process.stdout.write(`\r Analyzed ${analyzed}/${files.length} files…`);
509
- }
510
- }
511
- if (!jsonMode) process.stdout.write(`\r Analyzed ${files.length} files, found ${allFunctions.length} functions. \n`);
512
-
513
- // Map functions to capabilities
514
- const enriched = {}; // capId → { ...cap, codeAnalysis: {...} }
515
-
516
- for (const cap of targetCaps) {
517
- enriched[cap.id] = { ...cap, codeAnalysis: null };
518
- }
519
-
520
- for (const { fn, filePath } of allFunctions) {
521
- const match = matchFunctionToCapability(fn, targetCaps);
522
- if (!match) continue;
523
- const { cap } = match;
524
- const existing = enriched[cap.id]?.codeAnalysis || {};
525
- enriched[cap.id].codeAnalysis = mergeAnalysis(existing, fn, filePath, cwd);
526
- }
527
-
528
- // Compute stats
529
- const total = Object.keys(enriched).length;
530
- const matched = Object.values(enriched).filter(e => e.codeAnalysis).length;
531
-
532
- if (jsonMode) {
533
- const out = {
534
- scannedAt: new Date().toISOString(),
535
- files: files.length,
536
- functions: allFunctions.length,
537
- capabilities: Object.entries(enriched).map(([id, data]) => ({
538
- id,
539
- name: data.name || data.title,
540
- codeAnalysis: data.codeAnalysis,
541
- })),
542
- };
543
- console.log(JSON.stringify(out, null, 2));
544
- return;
545
- }
546
-
547
- printReport(enriched);
548
- console.log(` ${green("✔")} Matched ${matched}/${total} capabilities to source functions`);
549
- console.log();
550
-
551
- if (dryRun) {
552
- console.log(yellow(" --dry-run: no files written."));
553
- return;
554
- }
555
-
556
- // Write scan.json
557
- const scanData = {
558
- scannedAt: new Date().toISOString(),
559
- files: files.length,
560
- functions: allFunctions.length,
561
- capabilities: Object.entries(enriched).map(([id, data]) => ({
562
- id,
563
- name: data.name || data.title,
564
- codeAnalysis: data.codeAnalysis,
565
- })),
566
- };
567
- const scanPath = path.join(infernoDir, "scan.json");
568
- fs.writeFileSync(scanPath, JSON.stringify(scanData, null, 2));
569
- console.log(gray(` Saved → inferno/scan.json`));
570
-
571
- // Enrich capabilities.json
572
- let changed = 0;
573
- const updatedCaps = capabilities.map(cap => {
574
- const analysis = enriched[cap.id]?.codeAnalysis;
575
- if (!analysis) return cap;
576
- changed++;
577
- return { ...cap, codeAnalysis: analysis };
578
- });
579
-
580
- if (changed > 0) {
581
- // Preserve the original file format: object wrapper or bare array
582
- const toWrite = capsFileIsObject
583
- ? { ...capsFileWrapper, capabilities: updatedCaps }
584
- : updatedCaps;
585
- fs.writeFileSync(capsPath, JSON.stringify(toWrite, null, 2));
586
- console.log(gray(` Updated ${changed} capability entries in capabilities.json`));
587
- }
588
-
589
- console.log();
590
- if (!ts) {
591
- console.log(yellow(" ⚠ TypeScript compiler not found — JS/TS analyzed with regex fallback."));
592
- console.log(gray(` For deeper analysis: npm install -g typescript`));
593
- console.log();
594
- }
595
- }
39
+ `;function le(e){try{const t=G(`python3 -c ${JSON.stringify(ce)} ${JSON.stringify(e)}`,{timeout:8e3,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),n=JSON.parse(t.trim()||"[]"),s=w.readFileSync(e,"utf8");return n.map(i=>({...i,services:O(s),dbCalls:T(s),httpCalls:_(s)}))}catch{return null}}const ae=[{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 E(e,t){const n=g.extname(e).slice(1),s=ae.find(c=>c.lang===n);if(!s)return null;const i=[],r=new RegExp(s.re.source,"gm");let a;for(;(a=r.exec(t))!==null;){const c=a.index,b=Math.min(c+2e3,t.length),f=t.slice(c,b);i.push({name:a[1],calls:[],throws:[],services:O(f),dbCalls:T(f),httpCalls:_(f),loc:t.slice(0,c).split(`
40
+ `).length})}return i.length>0?i:null}const pe=new Set(["node_modules",".git","dist","build","out",".next",".nuxt","coverage","__pycache__",".pytest_cache","vendor","tmp",".turbo","target",".gradle","public","static","assets"]),ue=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".rb",".java"]),fe=/\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;function*q(e){let t;try{t=w.readdirSync(e,{withFileTypes:!0})}catch{return}for(const n of t)if(n.isDirectory())pe.has(n.name)||(yield*q(g.join(e,n.name)));else if(n.isFile()){const s=g.extname(n.name);ue.has(s)&&!fe.test(n.name)&&(yield g.join(e,n.name))}}function de(e){let t;try{t=w.readFileSync(e,"utf8")}catch{return[]}const n=g.extname(e);return[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(n)?re(e,t)||E(e,t)||[]:n===".py"?le(e)||E(e,t)||[]:E(e,t)||[]}function $(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(t=>t.length>1)}function M(e,t){const n=new Set(e),s=new Set(t);let i=0;for(const a of n)s.has(a)&&i++;const r=n.size+s.size-i;return r===0?0:i/r}function ge(e,t){const n=$(e.name);let s=null,i=0;for(const r of t){const a=Math.max(M(n,$(r.id||"")),M(n,$(r.name||r.title||"")));a>i&&(i=a,s=r)}return i>=.2?{cap:s,score:i}:null}function me(e={},t,n,s){const i=g.relative(s,n),r=(a=[],c=[])=>[...new Set([...a,...c])];return{functions:r(e.functions,[t.name]),sourceFiles:r(e.sourceFiles,[i]),calls:r(e.calls,t.calls),throws:r(e.throws,t.throws),services:r(e.services,t.services),dbCalls:r(e.dbCalls,t.dbCalls),httpCalls:r(e.httpCalls,t.httpCalls),scannedAt:new Date().toISOString()}}function ye(e){console.log(),console.log(P(" Scan Results")),console.log(u(" \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[t,n]of Object.entries(e)){const{codeAnalysis:s}=n;s&&(console.log(),console.log(` ${k("\u25CF")} ${P(t)}`),s.sourceFiles?.length&&console.log(u(" files: ")+s.sourceFiles.join(", ")),s.functions?.length&&console.log(u(" funcs: ")+s.functions.join(", ")),s.services?.length&&console.log(u(" services: ")+K(s.services.join(", "))),s.dbCalls?.length&&console.log(u(" db: ")+s.dbCalls.slice(0,4).join(", ")),s.httpCalls?.length&&console.log(u(" http: ")+s.httpCalls.slice(0,4).join(", ")),s.throws?.length&&console.log(u(" throws: ")+j(s.throws.join(", "))))}console.log(),console.log(u(" \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 Se(e){const t=e||[],n=t.includes("--dry-run"),s=t.includes("--json"),i=t.indexOf("--dir"),r=i!==-1?[t[i+1]]:[],a=(()=>{const o=t.indexOf("--capability");return o!==-1?t[o+1]:null})(),c=process.cwd(),b=g.join(c,"inferno"),f=g.join(b,"capabilities.json");w.existsSync(f)||(console.error(F("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let d,x=!1,I=null;try{d=JSON.parse(w.readFileSync(f,"utf8"))}catch(o){console.error(F("\u2717 Failed to parse capabilities.json: "+o.message)),process.exit(1)}Array.isArray(d)||(d.capabilities?(x=!0,I=d,d=d.capabilities):(console.error(F("\u2717 Unexpected capabilities.json format.")),process.exit(1)));const v=a?d.filter(o=>o.id===a||(o.name||"").toLowerCase()===a.toLowerCase()):d;v.length===0&&(console.log(j(a?`No capability matched: ${a}`:"No capabilities found.")),process.exit(0));const J=[c,...r];s||process.stdout.write(u(" Walking source files\u2026"));const h=[];for(const o of J)for(const p of q(o))h.push(p);s||process.stdout.write(`\r Found ${h.length} source files.
41
+ `),s||process.stdout.write(u(" Analyzing\u2026"));const S=[];let C=0;for(const o of h){const p=de(o);for(const y of p)S.push({fn:y,filePath:o});C++,!s&&C%20===0&&process.stdout.write(`\r Analyzed ${C}/${h.length} files\u2026`)}s||process.stdout.write(`\r Analyzed ${h.length} files, found ${S.length} functions.
42
+ `);const m={};for(const o of v)m[o.id]={...o,codeAnalysis:null};for(const{fn:o,filePath:p}of S){const y=ge(o,v);if(!y)continue;const{cap:D}=y,W=m[D.id]?.codeAnalysis||{};m[D.id].codeAnalysis=me(W,o,p,c)}const L=Object.keys(m).length,H=Object.values(m).filter(o=>o.codeAnalysis).length;if(s){const o={scannedAt:new Date().toISOString(),files:h.length,functions:S.length,capabilities:Object.entries(m).map(([p,y])=>({id:p,name:y.name||y.title,codeAnalysis:y.codeAnalysis}))};console.log(JSON.stringify(o,null,2));return}if(ye(m),console.log(` ${k("\u2714")} Matched ${H}/${L} capabilities to source functions`),console.log(),n){console.log(j(" --dry-run: no files written."));return}const B={scannedAt:new Date().toISOString(),files:h.length,functions:S.length,capabilities:Object.entries(m).map(([o,p])=>({id:o,name:p.name||p.title,codeAnalysis:p.codeAnalysis}))},U=g.join(b,"scan.json");w.writeFileSync(U,JSON.stringify(B,null,2)),console.log(u(" Saved \u2192 inferno/scan.json"));let A=0;const R=d.map(o=>{const p=m[o.id]?.codeAnalysis;return p?(A++,{...o,codeAnalysis:p}):o});if(A>0){const o=x?{...I,capabilities:R}:R;w.writeFileSync(f,JSON.stringify(o,null,2)),console.log(u(` Updated ${A} capability entries in capabilities.json`))}console.log(),l||(console.log(j(" \u26A0 TypeScript compiler not found \u2014 JS/TS analyzed with regex fallback.")),console.log(u(" For deeper analysis: npm install -g typescript")),console.log())}export{Se as scanCommand};