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 +7 -0
- package/lib/commands/adopt.mjs +208 -3
- package/lib/commands/init.mjs +2 -0
- package/package.json +1 -1
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
|
|
package/lib/commands/adopt.mjs
CHANGED
|
@@ -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
|
}
|
package/lib/commands/init.mjs
CHANGED
|
@@ -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
|