infernoflow 0.37.1 → 0.37.4
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/CHANGELOG.md +71 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -520
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,438 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* infernoflow explain user-auth
|
|
12
|
-
* infernoflow explain payment-process --dry-run (print prompt only)
|
|
13
|
-
* infernoflow explain user-auth --json
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
import { execSync } from "node:child_process";
|
|
19
|
-
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
20
|
-
|
|
21
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
function loadJson(p) {
|
|
24
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
25
|
-
catch { return null; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function runGit(cmd, cwd) {
|
|
29
|
-
try {
|
|
30
|
-
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"] }).trim();
|
|
31
|
-
} catch { return ""; }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const LEVEL_ICON = { frozen: "🧊", stable: "〰️ ", experimental: "🌊" };
|
|
35
|
-
const LEVEL_COLOR = { frozen: red, stable: yellow, experimental: green };
|
|
36
|
-
|
|
37
|
-
function stability(cap) { return cap?.stability || "experimental"; }
|
|
38
|
-
|
|
39
|
-
// ── git helpers ───────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
function getFirstCommit(filePath, cwd) {
|
|
42
|
-
if (!filePath) return null;
|
|
43
|
-
const rel = path.relative(cwd, path.resolve(cwd, filePath));
|
|
44
|
-
const log = runGit(
|
|
45
|
-
`git log --follow --format="%h|%aI|%ae|%s" -- ${JSON.stringify(rel)}`, cwd
|
|
46
|
-
);
|
|
47
|
-
if (!log) return null;
|
|
48
|
-
const lines = log.split("\n").filter(Boolean);
|
|
49
|
-
if (!lines.length) return null;
|
|
50
|
-
const [hash, date, author, ...subjectParts] = lines[lines.length - 1].split("|");
|
|
51
|
-
return {
|
|
52
|
-
hash: hash?.trim(),
|
|
53
|
-
date: date?.trim() ? new Date(date.trim()).toLocaleDateString() : "",
|
|
54
|
-
author: author?.trim(),
|
|
55
|
-
subject: subjectParts.join("|").trim(),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getRecentHistory(filePath, cwd, limit = 5) {
|
|
60
|
-
if (!filePath) return [];
|
|
61
|
-
const rel = path.relative(cwd, path.resolve(cwd, filePath));
|
|
62
|
-
const log = runGit(
|
|
63
|
-
`git log --follow --format="%h|%aI|%ae|%s" -${limit} -- ${JSON.stringify(rel)}`, cwd
|
|
64
|
-
);
|
|
65
|
-
if (!log) return [];
|
|
66
|
-
return log.split("\n").filter(Boolean).map(line => {
|
|
67
|
-
const [hash, date, author, ...subjectParts] = line.split("|");
|
|
68
|
-
return {
|
|
69
|
-
hash: hash?.trim(),
|
|
70
|
-
date: date?.trim() ? new Date(date.trim()).toLocaleDateString() : "",
|
|
71
|
-
author: author?.trim(),
|
|
72
|
-
subject: subjectParts.join("|").trim(),
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── scenario finder ───────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
function findScenarios(capId, infernoDir) {
|
|
80
|
-
const dir = path.join(infernoDir, "scenarios");
|
|
81
|
-
if (!fs.existsSync(dir)) return [];
|
|
82
|
-
const found = [];
|
|
83
|
-
for (const f of fs.readdirSync(dir)) {
|
|
84
|
-
if (!f.endsWith(".json")) continue;
|
|
85
|
-
try {
|
|
86
|
-
const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
87
|
-
const covered = s.capabilitiesCovered || s.capabilities || [];
|
|
88
|
-
if (covered.some(c => c.toLowerCase() === capId.toLowerCase())) {
|
|
89
|
-
found.push(s);
|
|
90
|
-
}
|
|
91
|
-
} catch {}
|
|
92
|
-
}
|
|
93
|
-
return found;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── prompt builder ────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
function buildPrompt(capId, cap, scanEntry, graph, allCaps, scenarios, firstCommit, recentHistory) {
|
|
99
|
-
const level = stability(cap);
|
|
100
|
-
const files = scanEntry?.codeAnalysis?.sourceFiles || [];
|
|
101
|
-
const functions = scanEntry?.codeAnalysis?.functions || [];
|
|
102
|
-
const services = scanEntry?.codeAnalysis?.services || [];
|
|
103
|
-
const throws_ = scanEntry?.codeAnalysis?.throws || [];
|
|
104
|
-
const calls = scanEntry?.codeAnalysis?.calls || [];
|
|
105
|
-
const deps = graph?.deps?.[capId] || [];
|
|
106
|
-
const dependents = graph?.dependents?.[capId] || [];
|
|
107
|
-
|
|
108
|
-
const lines = [
|
|
109
|
-
`You are a senior engineer writing a brief, plain-English explanation of a software capability for a teammate who is about to modify it.`,
|
|
110
|
-
``,
|
|
111
|
-
`Write 3–5 sentences covering:`,
|
|
112
|
-
` 1. What this capability does and why it exists`,
|
|
113
|
-
` 2. The most important thing to know before changing it (stability, callers, risk)`,
|
|
114
|
-
` 3. What to test or verify after any modification`,
|
|
115
|
-
``,
|
|
116
|
-
`Be concrete and direct. Do not use bullet points. Do not repeat the capability ID verbatim in every sentence.`,
|
|
117
|
-
``,
|
|
118
|
-
`=== Capability: ${capId} ===`,
|
|
119
|
-
`Name: ${cap.name || cap.title || capId}`,
|
|
120
|
-
`Description: ${cap.description || "(none provided)"}`,
|
|
121
|
-
`Stability: ${level}`,
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
if (files.length) lines.push(`Source files: ${files.join(", ")}`);
|
|
125
|
-
if (functions.length) lines.push(`Functions: ${functions.join(", ")}`);
|
|
126
|
-
if (services.length) lines.push(`External services used: ${services.join(", ")}`);
|
|
127
|
-
if (throws_.length) lines.push(`Can throw: ${throws_.join(", ")}`);
|
|
128
|
-
if (calls.length) lines.push(`Internal calls: ${calls.join(", ")}`);
|
|
129
|
-
|
|
130
|
-
if (deps.length) {
|
|
131
|
-
const depDetails = deps.map(d => {
|
|
132
|
-
const dc = allCaps.find(c => c.id === d);
|
|
133
|
-
return `${d} (${stability(dc)})`;
|
|
134
|
-
});
|
|
135
|
-
lines.push(`Calls capabilities: ${depDetails.join(", ")}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (dependents.length) {
|
|
139
|
-
const depDetails = dependents.map(d => {
|
|
140
|
-
const dc = allCaps.find(c => c.id === d);
|
|
141
|
-
return `${d} (${stability(dc)})`;
|
|
142
|
-
});
|
|
143
|
-
lines.push(`Called by capabilities: ${depDetails.join(", ")}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (scenarios.length) {
|
|
147
|
-
lines.push(`Test scenarios: ${scenarios.map(s => s.scenarioId || s.description || "unnamed").join(", ")}`);
|
|
148
|
-
} else {
|
|
149
|
-
lines.push(`Test scenarios: none registered`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (firstCommit) {
|
|
153
|
-
lines.push(`First introduced: ${firstCommit.date} by ${firstCommit.author} — "${firstCommit.subject}"`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (recentHistory.length) {
|
|
157
|
-
lines.push(`Recent changes:`);
|
|
158
|
-
for (const h of recentHistory.slice(0, 3)) {
|
|
159
|
-
lines.push(` ${h.date} — ${h.subject}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (level === "frozen") {
|
|
164
|
-
lines.push(`IMPORTANT: This capability is FROZEN. Any modification requires explicit approval.`);
|
|
165
|
-
} else if (level === "stable") {
|
|
166
|
-
lines.push(`NOTE: This capability is STABLE. Prefer additive changes; avoid breaking the public API.`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return lines.join("\n");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── AI caller ─────────────────────────────────────────────────────────────────
|
|
173
|
-
|
|
174
|
-
async function callAI(prompt, cwd) {
|
|
175
|
-
try {
|
|
176
|
-
const { callAI: call } = await import("../ai/providerRouter.mjs");
|
|
177
|
-
return await call(prompt, cwd);
|
|
178
|
-
} catch {
|
|
179
|
-
// Provider not available — return a structured fallback
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── fallback narrative (no AI) ────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
function buildFallback(capId, cap, scanEntry, graph, allCaps, scenarios) {
|
|
187
|
-
const level = stability(cap);
|
|
188
|
-
const name = cap.name || cap.title || capId;
|
|
189
|
-
const services = scanEntry?.codeAnalysis?.services || [];
|
|
190
|
-
const dependents = graph?.dependents?.[capId] || [];
|
|
191
|
-
const deps = graph?.deps?.[capId] || [];
|
|
192
|
-
|
|
193
|
-
const parts = [];
|
|
194
|
-
|
|
195
|
-
// What it does
|
|
196
|
-
if (cap.description && cap.description !== "(none provided)") {
|
|
197
|
-
parts.push(`${name} — ${cap.description}.`);
|
|
198
|
-
} else {
|
|
199
|
-
parts.push(`${name} handles the ${capId} flow within this system.`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// External services
|
|
203
|
-
if (services.length) {
|
|
204
|
-
parts.push(`It integrates with ${services.join(" and ")}.`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Dependencies
|
|
208
|
-
if (deps.length) {
|
|
209
|
-
parts.push(`It depends on: ${deps.join(", ")}.`);
|
|
210
|
-
}
|
|
211
|
-
if (dependents.length) {
|
|
212
|
-
const frozenDeps = dependents.filter(d => stability(allCaps.find(c => c.id === d)) === "frozen");
|
|
213
|
-
if (frozenDeps.length) {
|
|
214
|
-
parts.push(`⚠️ ${frozenDeps.join(", ")} depend${frozenDeps.length === 1 ? "s" : ""} on this — changing it may break frozen capabilities.`);
|
|
215
|
-
} else {
|
|
216
|
-
parts.push(`${dependents.join(", ")} depend${dependents.length === 1 ? "s" : ""} on this capability.`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Stability advice
|
|
221
|
-
if (level === "frozen") {
|
|
222
|
-
parts.push(`This capability is FROZEN — do not modify without explicit instruction.`);
|
|
223
|
-
} else if (level === "stable") {
|
|
224
|
-
parts.push(`This capability is stable — prefer additive changes and avoid breaking the existing API surface.`);
|
|
225
|
-
} else {
|
|
226
|
-
parts.push(`This capability is experimental — free to refactor as needed.`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Test advice
|
|
230
|
-
if (scenarios.length) {
|
|
231
|
-
parts.push(`Before shipping changes, run the registered scenarios: ${scenarios.map(s => s.scenarioId || "unnamed").join(", ")}.`);
|
|
232
|
-
} else {
|
|
233
|
-
parts.push(`No test scenarios are registered — consider adding one before making changes.`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return parts.join(" ");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ── printer ───────────────────────────────────────────────────────────────────
|
|
240
|
-
|
|
241
|
-
function printExplain(capId, cap, narrative, provider, dryRun) {
|
|
242
|
-
const level = stability(cap);
|
|
243
|
-
const icon = LEVEL_ICON[level] || "🌊";
|
|
244
|
-
const color = LEVEL_COLOR[level] || green;
|
|
245
|
-
|
|
246
|
-
console.log();
|
|
247
|
-
console.log(bold(` ${icon} ${color(capId)}`));
|
|
248
|
-
if (cap.name || cap.title) console.log(gray(` ${cap.name || cap.title}`));
|
|
249
|
-
console.log();
|
|
250
|
-
|
|
251
|
-
if (dryRun) {
|
|
252
|
-
console.log(yellow(" [dry-run] Prompt only — no AI call made"));
|
|
253
|
-
console.log();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Word-wrap narrative at ~80 chars
|
|
258
|
-
const words = narrative.split(" ");
|
|
259
|
-
let line = " ";
|
|
260
|
-
const lines = [];
|
|
261
|
-
for (const word of words) {
|
|
262
|
-
if (line.length + word.length > 82) { lines.push(line); line = " " + word; }
|
|
263
|
-
else line += (line === " " ? "" : " ") + word;
|
|
264
|
-
}
|
|
265
|
-
if (line.trim()) lines.push(line);
|
|
266
|
-
|
|
267
|
-
for (const l of lines) console.log(l);
|
|
268
|
-
console.log();
|
|
269
|
-
|
|
270
|
-
if (provider) {
|
|
271
|
-
console.log(gray(` ── via ${provider}`));
|
|
272
|
-
} else {
|
|
273
|
-
console.log(gray(" ── structural summary (no AI provider configured)"));
|
|
274
|
-
console.log(` ${yellow("💡")} ${gray("For richer AI narratives:")} ${cyan("infernoflow ai setup")}`);
|
|
275
|
-
}
|
|
276
|
-
console.log();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// ── entry point ───────────────────────────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
export async function explainCommand(rawArgs) {
|
|
282
|
-
const args = (rawArgs || []).slice(1);
|
|
283
|
-
const dryRun = args.includes("--dry-run");
|
|
284
|
-
const jsonMode = args.includes("--json");
|
|
285
|
-
|
|
286
|
-
const arg = args.find(a => !a.startsWith("--"));
|
|
287
|
-
|
|
288
|
-
if (!arg) {
|
|
289
|
-
console.error(red("✗ Usage: infernoflow explain <capability-id|file-path> [--dry-run] [--json]"));
|
|
290
|
-
console.error(gray(" Examples:"));
|
|
291
|
-
console.error(gray(" infernoflow explain CreateTask"));
|
|
292
|
-
console.error(gray(" infernoflow explain src/components/TaskComposer.jsx"));
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const cwd = process.cwd();
|
|
297
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
298
|
-
|
|
299
|
-
// Load capabilities
|
|
300
|
-
let allCaps = [];
|
|
301
|
-
const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
|
|
302
|
-
if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
|
|
303
|
-
|
|
304
|
-
// Detect if arg is a file path and resolve to capability IDs
|
|
305
|
-
const isFilePath = arg.includes("/") || arg.includes("\\") || /\.\w+$/.test(arg);
|
|
306
|
-
|
|
307
|
-
let capIdsToExplain = [];
|
|
308
|
-
|
|
309
|
-
if (isFilePath) {
|
|
310
|
-
// Look up which capabilities are mapped to this file via scan.json
|
|
311
|
-
const scanData = loadJson(path.join(infernoDir, "scan.json"));
|
|
312
|
-
const scanCaps = scanData?.capabilities || [];
|
|
313
|
-
const relTarget = path.relative(cwd, path.resolve(cwd, arg));
|
|
314
|
-
|
|
315
|
-
for (const entry of scanCaps) {
|
|
316
|
-
const sourceFiles = entry.codeAnalysis?.sourceFiles || [];
|
|
317
|
-
const matched = sourceFiles.some(f =>
|
|
318
|
-
f === relTarget || f.endsWith(relTarget) || relTarget.endsWith(f)
|
|
319
|
-
);
|
|
320
|
-
if (matched) capIdsToExplain.push(entry.id);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (capIdsToExplain.length === 0) {
|
|
324
|
-
// scan.json may not exist yet — try capability-map.json
|
|
325
|
-
const capMap = loadJson(path.join(infernoDir, "capability-map.json"));
|
|
326
|
-
if (capMap) {
|
|
327
|
-
const relNorm = relTarget.replace(/\\/g, "/");
|
|
328
|
-
for (const [capId, files] of Object.entries(capMap)) {
|
|
329
|
-
const fileList = Array.isArray(files) ? files : [files];
|
|
330
|
-
if (fileList.some(f => f === relNorm || f.endsWith(relNorm) || relNorm.endsWith(f))) {
|
|
331
|
-
capIdsToExplain.push(capId);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (capIdsToExplain.length === 0) {
|
|
338
|
-
console.error(red(`✗ No capabilities found mapped to "${arg}"`));
|
|
339
|
-
console.error(gray(" Run: infernoflow scan — to build the source-file → capability map"));
|
|
340
|
-
console.error(gray(" Then retry: infernoflow explain " + arg));
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (!jsonMode) {
|
|
345
|
-
console.log(gray(`\n infernoflow explain → ${bold(arg)}`));
|
|
346
|
-
console.log(gray(` Found ${capIdsToExplain.length} mapped ${capIdsToExplain.length > 1 ? "capabilities" : "capability"}: ${capIdsToExplain.join(", ")}`));
|
|
347
|
-
console.log();
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
capIdsToExplain = [arg];
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Explain each capability
|
|
354
|
-
const jsonResults = [];
|
|
355
|
-
for (const capId of capIdsToExplain) {
|
|
356
|
-
const cap = allCaps.find(c => c.id === capId);
|
|
357
|
-
if (!cap) {
|
|
358
|
-
if (!jsonMode) {
|
|
359
|
-
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
360
|
-
console.error(gray(" Run: infernoflow stability — to list all capability IDs"));
|
|
361
|
-
}
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Load scan + graph for this cap
|
|
366
|
-
const scanData = loadJson(path.join(infernoDir, "scan.json"));
|
|
367
|
-
const graph = loadJson(path.join(infernoDir, "graph.json"));
|
|
368
|
-
const scanEntry = scanData?.capabilities?.find(c => c.id === capId);
|
|
369
|
-
|
|
370
|
-
// Git history
|
|
371
|
-
const files = scanEntry?.codeAnalysis?.sourceFiles || [];
|
|
372
|
-
const firstCommit = getFirstCommit(files[0], cwd);
|
|
373
|
-
const recentHistory = getRecentHistory(files[0], cwd);
|
|
374
|
-
|
|
375
|
-
// Scenarios
|
|
376
|
-
const scenarios = findScenarios(capId, infernoDir);
|
|
377
|
-
|
|
378
|
-
// Build prompt
|
|
379
|
-
const prompt = buildPrompt(capId, cap, scanEntry, graph, allCaps, scenarios, firstCommit, recentHistory);
|
|
380
|
-
|
|
381
|
-
if (dryRun && !jsonMode) {
|
|
382
|
-
console.log(gray(` ── ${bold(capId)} ──────────────────────────────────────────────────────`));
|
|
383
|
-
printExplain(capId, cap, "", null, true);
|
|
384
|
-
if (capIdsToExplain.length === 1) {
|
|
385
|
-
console.log(bold(" Prompt that would be sent to AI:"));
|
|
386
|
-
console.log();
|
|
387
|
-
console.log(prompt.split("\n").map(l => " " + l).join("\n"));
|
|
388
|
-
}
|
|
389
|
-
console.log();
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Call AI
|
|
394
|
-
let narrative = null;
|
|
395
|
-
let provider = null;
|
|
396
|
-
|
|
397
|
-
if (!dryRun) {
|
|
398
|
-
try {
|
|
399
|
-
const result = await callAI(prompt, cwd);
|
|
400
|
-
if (result?.text) {
|
|
401
|
-
narrative = result.text.trim();
|
|
402
|
-
provider = result.provider;
|
|
403
|
-
}
|
|
404
|
-
} catch {}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (!narrative) {
|
|
408
|
-
narrative = buildFallback(capId, cap, scanEntry, graph, allCaps, scenarios);
|
|
409
|
-
provider = null;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (jsonMode) {
|
|
413
|
-
jsonResults.push({
|
|
414
|
-
capId,
|
|
415
|
-
name: cap.name || cap.title,
|
|
416
|
-
stability: stability(cap),
|
|
417
|
-
narrative,
|
|
418
|
-
provider: provider || "fallback",
|
|
419
|
-
sourceFiles: files,
|
|
420
|
-
scenarios: scenarios.map(s => s.scenarioId || s.description),
|
|
421
|
-
firstCommit,
|
|
422
|
-
});
|
|
423
|
-
} else {
|
|
424
|
-
if (!isFilePath) {
|
|
425
|
-
console.log(gray(`\n infernoflow explain → ${bold(capId)}`));
|
|
426
|
-
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
427
|
-
}
|
|
428
|
-
printExplain(capId, cap, narrative, provider, false);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (jsonMode) {
|
|
433
|
-
const out = capIdsToExplain.length === 1 ? jsonResults[0] : jsonResults;
|
|
434
|
-
console.log(JSON.stringify(out, null, 2));
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
}
|
|
1
|
+
import*as S from"node:fs";import*as b from"node:path";import{execSync as R}from"node:child_process";import{bold as I,cyan as W,gray as y,green as L,yellow as N,red as C}from"../ui/output.mjs";function T(n){try{return JSON.parse(S.readFileSync(n,"utf8"))}catch{return null}}function k(n,e){try{return R(n,{cwd:e,encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}}const P={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},z={frozen:C,stable:N,experimental:L};function x(n){return n?.stability||"experimental"}function J(n,e){if(!n)return null;const l=b.relative(e,b.resolve(e,n)),a=k(`git log --follow --format="%h|%aI|%ae|%s" -- ${JSON.stringify(l)}`,e);if(!a)return null;const i=a.split(`
|
|
2
|
+
`).filter(Boolean);if(!i.length)return null;const[r,c,u,...d]=i[i.length-1].split("|");return{hash:r?.trim(),date:c?.trim()?new Date(c.trim()).toLocaleDateString():"",author:u?.trim(),subject:d.join("|").trim()}}function B(n,e,l=5){if(!n)return[];const a=b.relative(e,b.resolve(e,n)),i=k(`git log --follow --format="%h|%aI|%ae|%s" -${l} -- ${JSON.stringify(a)}`,e);return i?i.split(`
|
|
3
|
+
`).filter(Boolean).map(r=>{const[c,u,d,...p]=r.split("|");return{hash:c?.trim(),date:u?.trim()?new Date(u.trim()).toLocaleDateString():"",author:d?.trim(),subject:p.join("|").trim()}}):[]}function M(n,e){const l=b.join(e,"scenarios");if(!S.existsSync(l))return[];const a=[];for(const i of S.readdirSync(l))if(i.endsWith(".json"))try{const r=JSON.parse(S.readFileSync(b.join(l,i),"utf8"));(r.capabilitiesCovered||r.capabilities||[]).some(u=>u.toLowerCase()===n.toLowerCase())&&a.push(r)}catch{}return a}function _(n,e,l,a,i,r,c,u){const d=x(e),p=l?.codeAnalysis?.sourceFiles||[],f=l?.codeAnalysis?.functions||[],o=l?.codeAnalysis?.services||[],t=l?.codeAnalysis?.throws||[],m=l?.codeAnalysis?.calls||[],j=a?.deps?.[n]||[],v=a?.dependents?.[n]||[],s=["You are a senior engineer writing a brief, plain-English explanation of a software capability for a teammate who is about to modify it.","","Write 3\u20135 sentences covering:"," 1. What this capability does and why it exists"," 2. The most important thing to know before changing it (stability, callers, risk)"," 3. What to test or verify after any modification","","Be concrete and direct. Do not use bullet points. Do not repeat the capability ID verbatim in every sentence.","",`=== Capability: ${n} ===`,`Name: ${e.name||e.title||n}`,`Description: ${e.description||"(none provided)"}`,`Stability: ${d}`];if(p.length&&s.push(`Source files: ${p.join(", ")}`),f.length&&s.push(`Functions: ${f.join(", ")}`),o.length&&s.push(`External services used: ${o.join(", ")}`),t.length&&s.push(`Can throw: ${t.join(", ")}`),m.length&&s.push(`Internal calls: ${m.join(", ")}`),j.length){const h=j.map(g=>{const A=i.find(w=>w.id===g);return`${g} (${x(A)})`});s.push(`Calls capabilities: ${h.join(", ")}`)}if(v.length){const h=v.map(g=>{const A=i.find(w=>w.id===g);return`${g} (${x(A)})`});s.push(`Called by capabilities: ${h.join(", ")}`)}if(r.length?s.push(`Test scenarios: ${r.map(h=>h.scenarioId||h.description||"unnamed").join(", ")}`):s.push("Test scenarios: none registered"),c&&s.push(`First introduced: ${c.date} by ${c.author} \u2014 "${c.subject}"`),u.length){s.push("Recent changes:");for(const h of u.slice(0,3))s.push(` ${h.date} \u2014 ${h.subject}`)}return d==="frozen"?s.push("IMPORTANT: This capability is FROZEN. Any modification requires explicit approval."):d==="stable"&&s.push("NOTE: This capability is STABLE. Prefer additive changes; avoid breaking the public API."),s.join(`
|
|
4
|
+
`)}async function V(n,e){try{const{callAI:l}=await import("../ai/providerRouter.mjs");return await l(n,e)}catch{return null}}function Z(n,e,l,a,i,r){const c=x(e),u=e.name||e.title||n,d=l?.codeAnalysis?.services||[],p=a?.dependents?.[n]||[],f=a?.deps?.[n]||[],o=[];if(e.description&&e.description!=="(none provided)"?o.push(`${u} \u2014 ${e.description}.`):o.push(`${u} handles the ${n} flow within this system.`),d.length&&o.push(`It integrates with ${d.join(" and ")}.`),f.length&&o.push(`It depends on: ${f.join(", ")}.`),p.length){const t=p.filter(m=>x(i.find(j=>j.id===m))==="frozen");t.length?o.push(`\u26A0\uFE0F ${t.join(", ")} depend${t.length===1?"s":""} on this \u2014 changing it may break frozen capabilities.`):o.push(`${p.join(", ")} depend${p.length===1?"s":""} on this capability.`)}return c==="frozen"?o.push("This capability is FROZEN \u2014 do not modify without explicit instruction."):c==="stable"?o.push("This capability is stable \u2014 prefer additive changes and avoid breaking the existing API surface."):o.push("This capability is experimental \u2014 free to refactor as needed."),r.length?o.push(`Before shipping changes, run the registered scenarios: ${r.map(t=>t.scenarioId||"unnamed").join(", ")}.`):o.push("No test scenarios are registered \u2014 consider adding one before making changes."),o.join(" ")}function E(n,e,l,a,i){const r=x(e),c=P[r]||"\u{1F30A}",u=z[r]||L;if(console.log(),console.log(I(` ${c} ${u(n)}`)),(e.name||e.title)&&console.log(y(` ${e.name||e.title}`)),console.log(),i){console.log(N(" [dry-run] Prompt only \u2014 no AI call made")),console.log();return}const d=l.split(" ");let p=" ";const f=[];for(const o of d)p.length+o.length>82?(f.push(p),p=" "+o):p+=(p===" "?"":" ")+o;p.trim()&&f.push(p);for(const o of f)console.log(o);console.log(),a?console.log(y(` \u2500\u2500 via ${a}`)):(console.log(y(" \u2500\u2500 structural summary (no AI provider configured)")),console.log(` ${N("\u{1F4A1}")} ${y("For richer AI narratives:")} ${W("infernoflow ai setup")}`)),console.log()}async function H(n){const e=(n||[]).slice(1),l=e.includes("--dry-run"),a=e.includes("--json"),i=e.find(t=>!t.startsWith("--"));i||(console.error(C("\u2717 Usage: infernoflow explain <capability-id|file-path> [--dry-run] [--json]")),console.error(y(" Examples:")),console.error(y(" infernoflow explain CreateTask")),console.error(y(" infernoflow explain src/components/TaskComposer.jsx")),process.exit(1));const r=process.cwd(),c=b.join(r,"inferno");let u=[];const d=T(b.join(c,"capabilities.json"));d&&(u=Array.isArray(d)?d:d.capabilities||[]);const p=i.includes("/")||i.includes("\\")||/\.\w+$/.test(i);let f=[];if(p){const m=T(b.join(c,"scan.json"))?.capabilities||[],j=b.relative(r,b.resolve(r,i));for(const v of m)(v.codeAnalysis?.sourceFiles||[]).some(g=>g===j||g.endsWith(j)||j.endsWith(g))&&f.push(v.id);if(f.length===0){const v=T(b.join(c,"capability-map.json"));if(v){const s=j.replace(/\\/g,"/");for(const[h,g]of Object.entries(v))(Array.isArray(g)?g:[g]).some(w=>w===s||w.endsWith(s)||s.endsWith(w))&&f.push(h)}}f.length===0&&(console.error(C(`\u2717 No capabilities found mapped to "${i}"`)),console.error(y(" Run: infernoflow scan \u2014 to build the source-file \u2192 capability map")),console.error(y(" Then retry: infernoflow explain "+i)),process.exit(1)),a||(console.log(y(`
|
|
5
|
+
infernoflow explain \u2192 ${I(i)}`)),console.log(y(` Found ${f.length} mapped ${f.length>1?"capabilities":"capability"}: ${f.join(", ")}`)),console.log())}else f=[i];const o=[];for(const t of f){const m=u.find($=>$.id===t);if(!m){a||(console.error(C(`\u2717 Capability "${t}" not found in capabilities.json`)),console.error(y(" Run: infernoflow stability \u2014 to list all capability IDs")));continue}const j=T(b.join(c,"scan.json")),v=T(b.join(c,"graph.json")),s=j?.capabilities?.find($=>$.id===t),h=s?.codeAnalysis?.sourceFiles||[],g=J(h[0],r),A=B(h[0],r),w=M(t,c),O=_(t,m,s,v,u,w,g,A);if(l&&!a){console.log(y(` \u2500\u2500 ${I(t)} \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`)),E(t,m,"",null,!0),f.length===1&&(console.log(I(" Prompt that would be sent to AI:")),console.log(),console.log(O.split(`
|
|
6
|
+
`).map($=>" "+$).join(`
|
|
7
|
+
`))),console.log();continue}let F=null,D=null;if(!l)try{const $=await V(O,r);$?.text&&(F=$.text.trim(),D=$.provider)}catch{}F||(F=Z(t,m,s,v,u,w),D=null),a?o.push({capId:t,name:m.name||m.title,stability:x(m),narrative:F,provider:D||"fallback",sourceFiles:h,scenarios:w.map($=>$.scenarioId||$.description),firstCommit:g}):(p||(console.log(y(`
|
|
8
|
+
infernoflow explain \u2192 ${I(t)}`)),console.log(y(" \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"))),E(t,m,F,D,!1))}if(a){const t=f.length===1?o[0]:o;console.log(JSON.stringify(t,null,2));return}}export{H as explainCommand};
|