infernoflow 0.24.0 → 0.27.0
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/dist/bin/infernoflow.mjs +23 -0
- package/dist/lib/commands/dashboard.mjs +312 -3
- package/dist/lib/commands/graph.mjs +337 -0
- package/dist/lib/commands/impact.mjs +325 -0
- package/dist/lib/commands/scan.mjs +45 -26
- package/dist/lib/commands/why.mjs +358 -0
- package/package.json +1 -1
|
@@ -122,29 +122,45 @@ function getNodeName(node) {
|
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
133
147
|
}
|
|
148
|
+
node.forEachChild?.(walk);
|
|
134
149
|
}
|
|
135
|
-
|
|
136
|
-
return calls;
|
|
150
|
+
walk(root);
|
|
151
|
+
return { calls, throws };
|
|
137
152
|
}
|
|
138
153
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
)];
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
function isFunctionNode(node) {
|
|
@@ -179,25 +195,28 @@ function analyzeJsTs(filePath, code) {
|
|
|
179
195
|
return null;
|
|
180
196
|
}
|
|
181
197
|
|
|
198
|
+
// Collect ALL call/throw nodes in one pass from root
|
|
199
|
+
const { calls: allCalls, throws: allThrows } = collectAllNodes(srcFile);
|
|
200
|
+
|
|
182
201
|
const functions = [];
|
|
183
202
|
|
|
184
203
|
function visit(node) {
|
|
185
204
|
if (isFunctionNode(node)) {
|
|
186
|
-
const name
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
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);
|
|
190
209
|
functions.push({
|
|
191
210
|
name,
|
|
192
211
|
calls,
|
|
193
212
|
throws,
|
|
194
|
-
services:
|
|
195
|
-
dbCalls:
|
|
213
|
+
services: detectServices(text),
|
|
214
|
+
dbCalls: detectDbCalls(text),
|
|
196
215
|
httpCalls: detectHttpCalls(text),
|
|
197
216
|
loc: srcFile.getLineAndCharacterOfPosition(node.pos).line + 1,
|
|
198
217
|
});
|
|
199
218
|
}
|
|
200
|
-
|
|
219
|
+
node.forEachChild?.(visit);
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
visit(srcFile);
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow why
|
|
3
|
+
*
|
|
4
|
+
* Given a file path or function name — answer:
|
|
5
|
+
* • Which capability does this serve?
|
|
6
|
+
* • What is its stability level?
|
|
7
|
+
* • What scenarios cover it?
|
|
8
|
+
* • Who introduced it and when?
|
|
9
|
+
* • What does it call / what calls it?
|
|
10
|
+
*
|
|
11
|
+
* Pure correlation — no AI needed. Uses scan.json + graph.json + scenarios/ + git log.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* infernoflow why src/auth.ts
|
|
15
|
+
* infernoflow why loginUser
|
|
16
|
+
* infernoflow why src/auth.ts --function loginUser
|
|
17
|
+
* infernoflow why --json src/auth.ts
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
24
|
+
|
|
25
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function loadJson(p) {
|
|
28
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
29
|
+
catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runGit(cmd, cwd) {
|
|
33
|
+
try {
|
|
34
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
35
|
+
} catch { return ""; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LEVEL_ICON = { frozen: "🧊", stable: "〰️ ", experimental: "🌊" };
|
|
39
|
+
const LEVEL_COLOR = { frozen: red, stable: yellow, experimental: green };
|
|
40
|
+
|
|
41
|
+
function stability(cap) {
|
|
42
|
+
return cap?.stability || "experimental";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── matchers ─────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Given a target (file path or function name), find all matching
|
|
49
|
+
* capabilities from scan.json.
|
|
50
|
+
* Returns: [{ capId, capEntry, matchedVia, score }]
|
|
51
|
+
*/
|
|
52
|
+
function findCapabilities(target, scanCaps, allCaps, cwd) {
|
|
53
|
+
const results = [];
|
|
54
|
+
const isFile = target.includes("/") || target.includes("\\") || target.includes(".");
|
|
55
|
+
const relTarget = isFile ? path.relative(cwd, path.resolve(cwd, target)) : null;
|
|
56
|
+
|
|
57
|
+
for (const entry of scanCaps) {
|
|
58
|
+
const analysis = entry.codeAnalysis;
|
|
59
|
+
if (!analysis) continue;
|
|
60
|
+
|
|
61
|
+
const capFull = allCaps.find(c => c.id === entry.id) || {};
|
|
62
|
+
|
|
63
|
+
if (isFile) {
|
|
64
|
+
// Match by source file
|
|
65
|
+
const fileMatch = (analysis.sourceFiles || []).some(f =>
|
|
66
|
+
f === relTarget || f.endsWith(relTarget) || relTarget?.endsWith(f)
|
|
67
|
+
);
|
|
68
|
+
if (fileMatch) {
|
|
69
|
+
results.push({ capId: entry.id, capEntry: entry, capFull, matchedVia: "file", score: 1.0 });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isFile) {
|
|
75
|
+
// Match by function name (exact or contains)
|
|
76
|
+
const fnMatch = (analysis.functions || []).some(fn =>
|
|
77
|
+
fn.toLowerCase() === target.toLowerCase() ||
|
|
78
|
+
fn.toLowerCase().includes(target.toLowerCase()) ||
|
|
79
|
+
target.toLowerCase().includes(fn.toLowerCase())
|
|
80
|
+
);
|
|
81
|
+
if (fnMatch) {
|
|
82
|
+
results.push({ capId: entry.id, capEntry: entry, capFull, matchedVia: "function", score: 1.0 });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── scenario finder ───────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function findScenarios(capId, infernoDir) {
|
|
94
|
+
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
95
|
+
if (!fs.existsSync(scenariosDir)) return [];
|
|
96
|
+
|
|
97
|
+
const found = [];
|
|
98
|
+
for (const f of fs.readdirSync(scenariosDir)) {
|
|
99
|
+
if (!f.endsWith(".json")) continue;
|
|
100
|
+
try {
|
|
101
|
+
const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
|
|
102
|
+
const covered = s.capabilitiesCovered || s.capabilities || [];
|
|
103
|
+
if (covered.includes(capId) || covered.some(c =>
|
|
104
|
+
c.toLowerCase() === capId.toLowerCase()
|
|
105
|
+
)) {
|
|
106
|
+
found.push({ file: f, scenario: s });
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── git history ───────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function getGitHistory(filePath, cwd, limit = 5) {
|
|
116
|
+
if (!filePath) return [];
|
|
117
|
+
const rel = path.relative(cwd, path.resolve(cwd, filePath));
|
|
118
|
+
const log = runGit(
|
|
119
|
+
`git log --follow --format="%h|%aI|%ae|%s" -${limit} -- ${JSON.stringify(rel)}`,
|
|
120
|
+
cwd
|
|
121
|
+
);
|
|
122
|
+
if (!log) return [];
|
|
123
|
+
|
|
124
|
+
return log.split("\n").filter(Boolean).map(line => {
|
|
125
|
+
const [hash, date, author, ...subjectParts] = line.split("|");
|
|
126
|
+
return {
|
|
127
|
+
hash: hash?.trim(),
|
|
128
|
+
date: date?.trim() ? new Date(date.trim()).toLocaleDateString() : "",
|
|
129
|
+
author: author?.trim(),
|
|
130
|
+
subject: subjectParts.join("|").trim(),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getFirstCommit(filePath, cwd) {
|
|
136
|
+
if (!filePath) return null;
|
|
137
|
+
const rel = path.relative(cwd, path.resolve(cwd, filePath));
|
|
138
|
+
const log = runGit(
|
|
139
|
+
`git log --follow --format="%h|%aI|%ae|%s" -- ${JSON.stringify(rel)}`,
|
|
140
|
+
cwd
|
|
141
|
+
);
|
|
142
|
+
if (!log) return null;
|
|
143
|
+
const lines = log.split("\n").filter(Boolean);
|
|
144
|
+
if (!lines.length) return null;
|
|
145
|
+
const [hash, date, author, ...subjectParts] = lines[lines.length - 1].split("|");
|
|
146
|
+
return {
|
|
147
|
+
hash: hash?.trim(),
|
|
148
|
+
date: date?.trim() ? new Date(date.trim()).toLocaleDateString() : "",
|
|
149
|
+
author: author?.trim(),
|
|
150
|
+
subject: subjectParts.join("|").trim(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── printer ───────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function printResult(result, scenarios, history, firstCommit, graph, allCaps, target) {
|
|
157
|
+
const { capId, capEntry, capFull, matchedVia } = result;
|
|
158
|
+
const level = stability(capFull);
|
|
159
|
+
const icon = LEVEL_ICON[level] || "🌊";
|
|
160
|
+
const color = LEVEL_COLOR[level] || green;
|
|
161
|
+
|
|
162
|
+
console.log();
|
|
163
|
+
console.log(bold(` ${icon} ${color(capId)}`));
|
|
164
|
+
if (capFull.name || capFull.title) {
|
|
165
|
+
console.log(gray(` ${capFull.name || capFull.title}`));
|
|
166
|
+
}
|
|
167
|
+
if (capFull.description) {
|
|
168
|
+
console.log(gray(` ${capFull.description}`));
|
|
169
|
+
}
|
|
170
|
+
console.log();
|
|
171
|
+
|
|
172
|
+
// Matched via
|
|
173
|
+
console.log(gray(` matched via: `) + matchedVia + gray(` → `) + cyan(target));
|
|
174
|
+
|
|
175
|
+
// Stability
|
|
176
|
+
console.log(gray(` stability: `) + color(level));
|
|
177
|
+
|
|
178
|
+
// Source files
|
|
179
|
+
const files = capEntry.codeAnalysis?.sourceFiles || [];
|
|
180
|
+
if (files.length) {
|
|
181
|
+
console.log(gray(` source files: `) + files.join(", "));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Functions
|
|
185
|
+
const fns = capEntry.codeAnalysis?.functions || [];
|
|
186
|
+
if (fns.length) {
|
|
187
|
+
console.log(gray(` functions: `) + fns.join(", "));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// External services
|
|
191
|
+
const services = capEntry.codeAnalysis?.services || [];
|
|
192
|
+
if (services.length) {
|
|
193
|
+
console.log(gray(` uses: `) + cyan(services.join(", ")));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Throws
|
|
197
|
+
const throws = capEntry.codeAnalysis?.throws || [];
|
|
198
|
+
if (throws.length) {
|
|
199
|
+
console.log(gray(` throws: `) + yellow(throws.join(", ")));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Dependencies from graph
|
|
203
|
+
if (graph) {
|
|
204
|
+
const deps = graph.deps?.[capId] || [];
|
|
205
|
+
const dependents = graph.dependents?.[capId] || [];
|
|
206
|
+
if (deps.length) {
|
|
207
|
+
console.log(gray(` calls: `) + deps.map(d => {
|
|
208
|
+
const dCap = allCaps.find(c => c.id === d);
|
|
209
|
+
const dIcon = LEVEL_ICON[stability(dCap)] || "🌊";
|
|
210
|
+
return `${dIcon} ${d}`;
|
|
211
|
+
}).join(" "));
|
|
212
|
+
}
|
|
213
|
+
if (dependents.length) {
|
|
214
|
+
console.log(gray(` called by: `) + dependents.map(d => {
|
|
215
|
+
const dCap = allCaps.find(c => c.id === d);
|
|
216
|
+
const dIcon = LEVEL_ICON[stability(dCap)] || "🌊";
|
|
217
|
+
return `${dIcon} ${d}`;
|
|
218
|
+
}).join(" "));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
// Scenarios
|
|
225
|
+
if (scenarios.length > 0) {
|
|
226
|
+
console.log(bold(" Scenarios that cover this capability:"));
|
|
227
|
+
for (const { scenario } of scenarios) {
|
|
228
|
+
const steps = scenario.steps?.length || 0;
|
|
229
|
+
console.log(` ${green("✔")} ${scenario.scenarioId || scenario.description || scenario.file}`);
|
|
230
|
+
if (scenario.description) console.log(gray(` ${scenario.description}`));
|
|
231
|
+
if (steps) console.log(gray(` ${steps} step(s)`));
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
} else {
|
|
235
|
+
console.log(yellow(" ⚠ No scenarios found for this capability."));
|
|
236
|
+
console.log(gray(` Run: infernoflow suggest "add scenario for ${capId}"`));
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Git history
|
|
241
|
+
if (firstCommit) {
|
|
242
|
+
console.log(bold(" Origin:"));
|
|
243
|
+
console.log(` ${gray("first commit:")} ${firstCommit.hash} · ${firstCommit.date} · ${firstCommit.author}`);
|
|
244
|
+
console.log(` ${gray("subject:")} ${firstCommit.subject}`);
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (history.length > 0) {
|
|
249
|
+
console.log(bold(" Recent changes:"));
|
|
250
|
+
for (const h of history.slice(0, 4)) {
|
|
251
|
+
console.log(` ${gray(h.hash)} ${gray(h.date.padEnd(12))} ${h.subject}`);
|
|
252
|
+
}
|
|
253
|
+
console.log();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Frozen warning
|
|
257
|
+
if (level === "frozen") {
|
|
258
|
+
console.log(red(" 🧊 This capability is FROZEN — do not modify without explicit instruction."));
|
|
259
|
+
console.log();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export async function whyCommand(rawArgs) {
|
|
266
|
+
const args = (rawArgs || []).slice(1); // skip command name
|
|
267
|
+
const jsonMode = args.includes("--json");
|
|
268
|
+
const fnFlag = args.indexOf("--function");
|
|
269
|
+
const fnFilter = fnFlag !== -1 ? args[fnFlag + 1] : null;
|
|
270
|
+
|
|
271
|
+
// Target: first non-flag argument (skip the value after --function if present)
|
|
272
|
+
const target = args.find((a, i) => !a.startsWith("--") && (fnFlag === -1 || i !== fnFlag + 1));
|
|
273
|
+
|
|
274
|
+
if (!target) {
|
|
275
|
+
console.error(red("✗ Usage: infernoflow why <file-or-function> [--function <name>] [--json]"));
|
|
276
|
+
console.error(gray(" Examples:"));
|
|
277
|
+
console.error(gray(" infernoflow why src/auth.ts"));
|
|
278
|
+
console.error(gray(" infernoflow why loginUser"));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
284
|
+
|
|
285
|
+
// Load scan.json
|
|
286
|
+
const scan = loadJson(path.join(infernoDir, "scan.json"));
|
|
287
|
+
if (!scan) {
|
|
288
|
+
console.error(red("✗ inferno/scan.json not found — run `infernoflow scan` first."));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Load capabilities.json for stability + metadata
|
|
293
|
+
let allCaps = [];
|
|
294
|
+
const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
|
|
295
|
+
if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
|
|
296
|
+
|
|
297
|
+
// Load graph.json
|
|
298
|
+
const graph = loadJson(path.join(infernoDir, "graph.json"));
|
|
299
|
+
|
|
300
|
+
// Find capabilities
|
|
301
|
+
const scanCaps = scan.capabilities || [];
|
|
302
|
+
let results = findCapabilities(target, scanCaps, allCaps, cwd);
|
|
303
|
+
|
|
304
|
+
// Apply --function filter
|
|
305
|
+
if (fnFilter && results.length > 1) {
|
|
306
|
+
results = results.filter(r =>
|
|
307
|
+
(r.capEntry.codeAnalysis?.functions || []).some(fn =>
|
|
308
|
+
fn.toLowerCase().includes(fnFilter.toLowerCase())
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (results.length === 0) {
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(yellow(` No capability found matching: ${bold(target)}`));
|
|
316
|
+
console.log(gray(" Tip: run `infernoflow scan` to update code analysis, then try again."));
|
|
317
|
+
console.log(gray(" Tip: use a function name or relative file path."));
|
|
318
|
+
console.log();
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (jsonMode) {
|
|
323
|
+
const out = results.map(r => {
|
|
324
|
+
const scenarios = findScenarios(r.capId, infernoDir);
|
|
325
|
+
const files = r.capEntry.codeAnalysis?.sourceFiles || [];
|
|
326
|
+
const history = getGitHistory(files[0], cwd);
|
|
327
|
+
const first = getFirstCommit(files[0], cwd);
|
|
328
|
+
return {
|
|
329
|
+
capId: r.capId,
|
|
330
|
+
name: r.capFull.name || r.capFull.title,
|
|
331
|
+
stability: stability(r.capFull),
|
|
332
|
+
matchedVia: r.matchedVia,
|
|
333
|
+
sourceFiles: files,
|
|
334
|
+
functions: r.capEntry.codeAnalysis?.functions || [],
|
|
335
|
+
services: r.capEntry.codeAnalysis?.services || [],
|
|
336
|
+
throws: r.capEntry.codeAnalysis?.throws || [],
|
|
337
|
+
deps: graph?.deps?.[r.capId] || [],
|
|
338
|
+
dependents: graph?.dependents?.[r.capId] || [],
|
|
339
|
+
scenarios: scenarios.map(s => s.scenario?.scenarioId || s.file),
|
|
340
|
+
firstCommit: first,
|
|
341
|
+
recentHistory: history,
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
console.log(JSON.stringify(out, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(gray(`\n infernoflow why → ${bold(target)}`));
|
|
349
|
+
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
350
|
+
|
|
351
|
+
for (const result of results) {
|
|
352
|
+
const files = result.capEntry.codeAnalysis?.sourceFiles || [];
|
|
353
|
+
const scenarios = findScenarios(result.capId, infernoDir);
|
|
354
|
+
const history = getGitHistory(files[0], cwd);
|
|
355
|
+
const first = getFirstCommit(files[0], cwd);
|
|
356
|
+
printResult(result, scenarios, history, first, graph, allCaps, target);
|
|
357
|
+
}
|
|
358
|
+
}
|