infernoflow 0.10.5 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,12 @@ Override detected stack during adoption:
62
62
  infernoflow init --adopt --lang ts --framework angular --project-type frontend
63
63
  ```
64
64
 
65
+ C# / ASP.NET example:
66
+
67
+ ```bash
68
+ infernoflow init --adopt --lang cs --framework aspnet --project-type backend --report-human-only
69
+ ```
70
+
65
71
  JSON report for CI/logging:
66
72
 
67
73
  ```bash
@@ -85,6 +91,7 @@ What adoption creates:
85
91
  - `inferno/capabilities.json` (inferred registry)
86
92
  - `inferno/scenarios/adoption_baseline.json` (coverage baseline)
87
93
  - `inferno/adoption_profile.json` (detected components, display fields, external libraries, UI layout, styling hints)
94
+ - `inferno/adoption_profile.json` (detected components, display fields, external libraries, UI layout, styling hints, API call map)
88
95
  - `inferno/context-state.json` (saved development profile: language/framework/project type)
89
96
  - `inferno/CHANGELOG.md` (adoption entry)
90
97
 
@@ -43,7 +43,7 @@ export function discoverCapabilities(cwd) {
43
43
 
44
44
  function collectCodeFiles(cwd) {
45
45
  const files = [];
46
- const roots = ["src", "server", "app", "backend", "frontend", "api"];
46
+ const roots = ["src", "server", "app", "backend", "frontend", "api", "Controllers"];
47
47
  for (const r of roots) {
48
48
  const root = path.join(cwd, r);
49
49
  if (!fs.existsSync(root)) continue;
@@ -55,12 +55,19 @@ function collectCodeFiles(cwd) {
55
55
  if (entry.isDirectory()) {
56
56
  if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
57
57
  stack.push(p);
58
- } else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm)$/.test(entry.name)) {
58
+ } else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(entry.name)) {
59
59
  files.push(p);
60
60
  }
61
61
  }
62
62
  }
63
63
  }
64
+ // Include common root-level .NET entry files without scanning the whole repo.
65
+ for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
66
+ if (!entry.isFile()) continue;
67
+ if (/^(Program\.cs|.+\.csproj)$/i.test(entry.name)) {
68
+ files.push(path.join(cwd, entry.name));
69
+ }
70
+ }
64
71
  return files;
65
72
  }
66
73
 
@@ -237,6 +244,21 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
237
244
  const autoLanguage = sortedLang[0]?.[1] > 0 ? sortedLang[0][0] : "unknown";
238
245
 
239
246
  let autoFramework = "unknown";
247
+ let hasDotnetWebSdk = false;
248
+ let hasMinimalApi = false;
249
+ let hasBlazor = false;
250
+ for (const filePath of files) {
251
+ const base = path.basename(filePath).toLowerCase();
252
+ if (base.endsWith(".csproj")) {
253
+ const text = safeRead(filePath);
254
+ if (/Microsoft\.NET\.Sdk\.Web/i.test(text)) hasDotnetWebSdk = true;
255
+ if (/Blazor/i.test(text) || /Microsoft\.AspNetCore\.Components/i.test(text)) hasBlazor = true;
256
+ }
257
+ if (base === "program.cs") {
258
+ const text = safeRead(filePath);
259
+ if (/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(text)) hasMinimalApi = true;
260
+ }
261
+ }
240
262
  const hasDep = (name) => externalLibraries.includes(name);
241
263
  if (externalLibraries.some((d) => d.startsWith("@angular/"))) autoFramework = "angular";
242
264
  else if (hasDep("react")) autoFramework = "react";
@@ -250,14 +272,18 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
250
272
  else if (hasDep("flask")) autoFramework = "flask";
251
273
  else if (hasDep("django")) autoFramework = "django";
252
274
  else if (hasDep("spring-boot")) autoFramework = "spring";
275
+ else if (hasBlazor) autoFramework = "blazor";
276
+ else if (hasMinimalApi) autoFramework = "minimalapi";
277
+ else if (hasDotnetWebSdk || extCount.cs > 0) autoFramework = "aspnet";
253
278
 
254
279
  let autoProjectType = "fullstack";
255
280
  const hasClientRoots = ["src", "frontend", "app"].some((d) => fs.existsSync(path.join(cwd, d)));
256
281
  const hasServerRoots = ["server", "backend", "api"].some((d) => fs.existsSync(path.join(cwd, d)));
257
282
  if (["react", "angular", "vue", "svelte", "nextjs", "nuxt"].includes(autoFramework)) autoProjectType = "frontend";
258
- if (["express", "nestjs", "fastify", "flask", "django", "spring"].includes(autoFramework)) autoProjectType = "backend";
283
+ if (["express", "nestjs", "fastify", "flask", "django", "spring", "aspnet", "minimalapi"].includes(autoFramework)) autoProjectType = "backend";
259
284
  if (hasClientRoots && hasServerRoots) autoProjectType = "fullstack";
260
285
  if (!hasClientRoots && !hasServerRoots) autoProjectType = "library";
286
+ if (autoFramework === "blazor") autoProjectType = "frontend";
261
287
 
262
288
  return {
263
289
  language: overrides.language || autoLanguage,
@@ -271,6 +297,175 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
271
297
  };
272
298
  }
273
299
 
300
+ function detectApiCalls(cwd, files) {
301
+ const calls = [];
302
+ const seen = new Set();
303
+ const addCall = (call) => {
304
+ const key = `${call.method}|${call.endpointPattern}|${call.sourceFile}|${call.style}`;
305
+ if (seen.has(key)) return;
306
+ seen.add(key);
307
+ calls.push(call);
308
+ };
309
+
310
+ for (const filePath of files) {
311
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(filePath)) continue;
312
+ const rel = path.relative(cwd, filePath);
313
+ const text = safeRead(filePath);
314
+ const looksLikeService = /service|api|client|controller|program\.cs/i.test(rel) || /HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(text);
315
+ if (!looksLikeService) continue;
316
+
317
+ const normalized = text.replace(/\r\n/g, "\n");
318
+
319
+ const constStrings = {};
320
+ const normalizeExpr = (expr) => {
321
+ let out = String(expr || "").trim();
322
+ if (!out) return "";
323
+ out = out.replace(/;+$/, "").trim();
324
+ out = out.replace(/\(\s*$/, ""); // e.g. "this._nextPage("
325
+ // unwrap surrounding quotes/templates
326
+ while ((out.startsWith("'") && out.endsWith("'")) || (out.startsWith('"') && out.endsWith('"')) || (out.startsWith("`") && out.endsWith("`"))) {
327
+ out = out.slice(1, -1).trim();
328
+ }
329
+ return out;
330
+ };
331
+ const isLikelyEndpoint = (value) => {
332
+ const v = String(value || "").trim();
333
+ if (!v) return false;
334
+ if (/^https?:\/\//i.test(v)) return true;
335
+ if (v.startsWith("/")) return true;
336
+ if (/\bapi\b/i.test(v)) return true;
337
+ if (/\$\{[^}]+\}/.test(v)) return true;
338
+ if (/\?[^=\s]+=?/.test(v)) return true;
339
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(v)) return true;
340
+ return false;
341
+ };
342
+ const storeConst = (name, raw) => {
343
+ if (!name || !raw) return;
344
+ constStrings[name] = normalizeExpr(raw);
345
+ };
346
+
347
+ const constPattern = /(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
348
+ for (const m of normalized.matchAll(constPattern)) {
349
+ storeConst(m[1], m[2]);
350
+ }
351
+ const readonlyPattern =
352
+ /(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
353
+ for (const m of normalized.matchAll(readonlyPattern)) {
354
+ storeConst(m[1], m[2]);
355
+ }
356
+
357
+ const resolveExpr = (expr) => {
358
+ const trimmed = normalizeExpr(expr);
359
+ if (!trimmed) return "";
360
+ if (/^['"`][\s\S]*['"`]$/.test(trimmed)) {
361
+ return trimmed.replace(/^['"`]|['"`]$/g, "");
362
+ }
363
+ if (constStrings[trimmed] && isLikelyEndpoint(constStrings[trimmed])) return constStrings[trimmed];
364
+ const thisRef = trimmed.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
365
+ if (thisRef && constStrings[thisRef[1]] && isLikelyEndpoint(constStrings[thisRef[1]])) return constStrings[thisRef[1]];
366
+ const parts = trimmed.split("+").map((s) => s.trim()).filter(Boolean);
367
+ if (parts.length > 1) {
368
+ const rebuilt = parts
369
+ .map((p) => {
370
+ if (/^['"`][\s\S]*['"`]$/.test(p)) return p.replace(/^['"`]|['"`]$/g, "");
371
+ if (constStrings[p] && isLikelyEndpoint(constStrings[p])) return constStrings[p];
372
+ const thisP = p.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
373
+ if (thisP && constStrings[thisP[1]] && isLikelyEndpoint(constStrings[thisP[1]])) return constStrings[thisP[1]];
374
+ return `{${p}}`;
375
+ })
376
+ .join("");
377
+ if (rebuilt) return rebuilt;
378
+ }
379
+ const ternary = trimmed.match(/^(.+?)\?(.+?):(.+)$/s);
380
+ if (ternary) {
381
+ const left = resolveExpr(ternary[2]);
382
+ const right = resolveExpr(ternary[3]);
383
+ if (left || right) return `${left || "{optionA}"} | ${right || "{optionB}"}`;
384
+ }
385
+ return trimmed;
386
+ };
387
+
388
+ const httpClientPattern =
389
+ /\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;
390
+ for (const m of normalized.matchAll(httpClientPattern)) {
391
+ const method = m[1].toUpperCase();
392
+ const raw = resolveExpr(m[2]);
393
+ if (!raw || !isLikelyEndpoint(raw)) continue;
394
+ addCall({
395
+ method,
396
+ endpointPattern: normalizeExpr(raw),
397
+ style: "httpClient",
398
+ sourceFile: rel,
399
+ });
400
+ }
401
+
402
+ const fetchPattern = /\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;
403
+ for (const m of normalized.matchAll(fetchPattern)) {
404
+ const firstArg = resolveExpr(m[1]);
405
+ if (!firstArg || !isLikelyEndpoint(firstArg)) continue;
406
+ const fromIdx = m.index || 0;
407
+ const lookahead = normalized.slice(fromIdx, fromIdx + 260);
408
+ const methodMatch = /method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec(lookahead);
409
+ const method = (methodMatch?.[1] || "GET").toUpperCase();
410
+ addCall({
411
+ method,
412
+ endpointPattern: normalizeExpr(firstArg),
413
+ style: "fetch",
414
+ sourceFile: rel,
415
+ });
416
+ }
417
+
418
+ if (/\.cs$/i.test(filePath)) {
419
+ const mapPattern = /\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;
420
+ for (const m of normalized.matchAll(mapPattern)) {
421
+ addCall({
422
+ method: m[1].toUpperCase(),
423
+ endpointPattern: m[2],
424
+ style: "csharp-map",
425
+ sourceFile: rel,
426
+ });
427
+ }
428
+
429
+ const classRouteMatch = /\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(normalized);
430
+ const classRoute = classRouteMatch ? classRouteMatch[1] : "";
431
+ const attrPattern = /\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;
432
+ for (const m of normalized.matchAll(attrPattern)) {
433
+ const method = m[1].replace("Http", "").toUpperCase();
434
+ const attrRoute = m[2] || "";
435
+ const endpoint = [classRoute, attrRoute].filter(Boolean).join("/").replace(/\/+/g, "/");
436
+ addCall({
437
+ method,
438
+ endpointPattern: endpoint || classRoute || "{controller-route}",
439
+ style: "csharp-controller",
440
+ sourceFile: rel,
441
+ });
442
+ }
443
+
444
+ const httpClientPattern = /\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;
445
+ for (const m of normalized.matchAll(httpClientPattern)) {
446
+ const method = m[1].replace("Async", "").replace("Send", "SEND").toUpperCase();
447
+ addCall({
448
+ method,
449
+ endpointPattern: m[2],
450
+ style: "csharp-httpclient",
451
+ sourceFile: rel,
452
+ });
453
+ }
454
+ }
455
+ }
456
+
457
+ const byMethod = calls.reduce((acc, c) => {
458
+ acc[c.method] = (acc[c.method] || 0) + 1;
459
+ return acc;
460
+ }, {});
461
+
462
+ return {
463
+ totalCalls: calls.length,
464
+ byMethod,
465
+ calls: calls.slice(0, 80),
466
+ };
467
+ }
468
+
274
469
  export function discoverProjectSignals(cwd, profileOverrides = {}) {
275
470
  const files = collectCodeFiles(cwd);
276
471
  const inferred = new Map();
@@ -324,6 +519,7 @@ export function discoverProjectSignals(cwd, profileOverrides = {}) {
324
519
  uiLayout: detectUiLayout(files),
325
520
  styling: detectStyling(cwd, files, externalLibraries),
326
521
  developmentProfile: detectDevelopmentProfile(cwd, files, externalLibraries, profileOverrides),
522
+ apiCalls: detectApiCalls(cwd, files),
327
523
  };
328
524
  }
329
525
 
@@ -425,6 +621,14 @@ export function buildSignalsReport(signals) {
425
621
  ` - language : ${signals.developmentProfile?.language || "unknown"} (auto: ${signals.developmentProfile?.detected?.language || "unknown"})`,
426
622
  ` - framework : ${signals.developmentProfile?.framework || "unknown"} (auto: ${signals.developmentProfile?.detected?.framework || "unknown"})`,
427
623
  ` - project type: ${signals.developmentProfile?.projectType || "unknown"} (auto: ${signals.developmentProfile?.detected?.projectType || "unknown"})`,
624
+ "API calls",
625
+ "-".repeat(56),
626
+ ` - total calls : ${signals.apiCalls?.totalCalls ?? 0}`,
627
+ ` - by method : ${Object.entries(signals.apiCalls?.byMethod || {}).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}`,
628
+ ...(signals.apiCalls?.calls || []).slice(0, 6).map((c) => ` - ${c.method} ${c.endpointPattern} [${c.style}] (${c.sourceFile})`),
629
+ ...((signals.apiCalls?.calls || []).length > 6
630
+ ? [` - ... +${(signals.apiCalls?.calls || []).length - 6} more`]
631
+ : []),
428
632
  "=".repeat(56),
429
633
  ].join("\n");
430
634
  }
@@ -488,6 +692,7 @@ export function writeAdoptionBaseline(infernoDir, policyId, capabilities, signal
488
692
  projectType: "unknown",
489
693
  detected: { language: "unknown", framework: "unknown", projectType: "unknown" },
490
694
  },
695
+ apiCalls: signals.apiCalls || { totalCalls: 0, byMethod: {}, calls: [] },
491
696
  };
492
697
  fs.writeFileSync(path.join(infernoDir, "adoption_profile.json"), JSON.stringify(profile, null, 2) + "\n");
493
698
  }
@@ -216,6 +216,7 @@ export async function initCommand(args) {
216
216
  uiLayout: signals.uiLayout,
217
217
  styling: signals.styling,
218
218
  developmentProfile: signals.developmentProfile,
219
+ apiCalls: signals.apiCalls,
219
220
  },
220
221
  null,
221
222
  2
@@ -240,6 +241,7 @@ export async function initCommand(args) {
240
241
  uiLayout: signals.uiLayout,
241
242
  styling: signals.styling,
242
243
  developmentProfile: signals.developmentProfile,
244
+ apiCalls: signals.apiCalls,
243
245
  },
244
246
  null,
245
247
  2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {