infernoflow 0.30.0 → 0.32.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 +7 -0
- package/dist/lib/commands/changelog.mjs +5 -1
- package/dist/lib/commands/demo.mjs +569 -0
- package/dist/lib/commands/doctor.mjs +34 -2
- package/dist/lib/commands/explain.mjs +2 -2
- package/dist/lib/commands/init.mjs +11 -0
- package/dist/lib/commands/review.mjs +4 -4
- package/dist/lib/commands/test.mjs +1 -1
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -65,6 +65,7 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
65
65
|
explain: "AI narrative about a capability — what it does, why it exists, what's risky, and what to test",
|
|
66
66
|
test: "Run registered scenarios for a capability — auto-generates a smoke harness if no test runner is configured",
|
|
67
67
|
ai: "Manage AI providers — setup, status, test connection (subcommands: setup | status | test | clear)",
|
|
68
|
+
demo: "Interactive walkthrough — scaffolds a sample project and runs the full capability chain end-to-end",
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
const COMMAND_HANDLERS = {
|
|
@@ -123,6 +124,7 @@ const COMMAND_HANDLERS = {
|
|
|
123
124
|
explain: async (args) => (await import("../lib/commands/explain.mjs")).explainCommand(args),
|
|
124
125
|
test: async (args) => (await import("../lib/commands/test.mjs")).testCommand(args),
|
|
125
126
|
ai: async (args) => (await import("../lib/commands/ai.mjs")).aiCommand(args),
|
|
127
|
+
demo: async (args) => (await import("../lib/commands/demo.mjs")).demoCommand(args),
|
|
126
128
|
};
|
|
127
129
|
|
|
128
130
|
function formatCommandsHelp() {
|
|
@@ -442,6 +444,11 @@ ${formatCommandsHelp()}
|
|
|
442
444
|
infernoflow ai clear <provider> Remove a provider's config from integrations.json
|
|
443
445
|
Supported providers: anthropic openai gemini openrouter ollama
|
|
444
446
|
|
|
447
|
+
${bold("demo options:")}
|
|
448
|
+
infernoflow demo Full interactive walkthrough (sample e-commerce project)
|
|
449
|
+
infernoflow demo --fast Skip pauses — good for CI or screen recording
|
|
450
|
+
infernoflow demo --no-cleanup Keep the temp demo project after the run
|
|
451
|
+
|
|
445
452
|
${bold("Machine output:")}
|
|
446
453
|
${gray("status --json")}
|
|
447
454
|
${gray("check --json")}
|
|
@@ -525,7 +525,11 @@ async function subcmdAi(cwd, changelogPath, opts) {
|
|
|
525
525
|
aiText.split("\n").forEach(l => console.log(" " + l));
|
|
526
526
|
console.log(gray(" ──────────────────────────────────────────────────────"));
|
|
527
527
|
console.log();
|
|
528
|
-
|
|
528
|
+
if (provider === "template") {
|
|
529
|
+
console.log(` ${yellow("💡")} ${gray("For AI-written changelogs:")} ${cyan("infernoflow ai setup")}`);
|
|
530
|
+
} else {
|
|
531
|
+
info(`Generated via: ${bold(provider)}`);
|
|
532
|
+
};
|
|
529
533
|
|
|
530
534
|
if (dryRun) {
|
|
531
535
|
warn("Dry run — CHANGELOG.md not modified");
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow demo
|
|
3
|
+
*
|
|
4
|
+
* A self-contained, narrated walkthrough of infernoflow's core capabilities.
|
|
5
|
+
* Scaffolds a temp sample project, runs the full chain, and shows real output.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* infernoflow demo Run the full interactive demo
|
|
9
|
+
* infernoflow demo --fast Skip pauses (CI/recording mode)
|
|
10
|
+
* infernoflow demo --no-cleanup Keep the temp project after the demo
|
|
11
|
+
*
|
|
12
|
+
* What it demonstrates:
|
|
13
|
+
* 1. Project structure — a mini e-commerce API with real capabilities
|
|
14
|
+
* 2. infernoflow scan — AST analysis: functions, services, throws
|
|
15
|
+
* 3. infernoflow graph — dependency graph
|
|
16
|
+
* 4. infernoflow stability — frozen/stable/experimental breakdown
|
|
17
|
+
* 5. infernoflow impact — blast radius for payment-process
|
|
18
|
+
* 6. infernoflow explain — narrative (structural or AI)
|
|
19
|
+
* 7. infernoflow why — file → capability correlation
|
|
20
|
+
* 8. The money shot: trying to modify a frozen cap and getting warned
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import * as os from "node:os";
|
|
26
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
27
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
28
|
+
|
|
29
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
32
|
+
|
|
33
|
+
async function pause(fast, ms = 900) {
|
|
34
|
+
if (!fast) await sleep(ms);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hr(char = "─", len = 60) {
|
|
38
|
+
return gray(char.repeat(len));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function run(cmd, cwd) {
|
|
42
|
+
try {
|
|
43
|
+
return spawnSync(cmd, {
|
|
44
|
+
shell: true, cwd, encoding: "utf8", timeout: 30_000,
|
|
45
|
+
});
|
|
46
|
+
} catch { return { stdout: "", stderr: "", status: 1 }; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── sample project scaffold ───────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const SAMPLE_FILES = {
|
|
52
|
+
// Capability definitions
|
|
53
|
+
"inferno/capabilities.json": JSON.stringify([
|
|
54
|
+
{
|
|
55
|
+
id: "user-auth",
|
|
56
|
+
name: "User Authentication",
|
|
57
|
+
description: "Handles login, session management, and token validation",
|
|
58
|
+
stability: "frozen",
|
|
59
|
+
owner: "auth-team",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "payment-process",
|
|
63
|
+
name: "Payment Processing",
|
|
64
|
+
description: "Charges cards via Stripe, handles retries and webhook events",
|
|
65
|
+
stability: "stable",
|
|
66
|
+
owner: "payments-team",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "order-create",
|
|
70
|
+
name: "Order Creation",
|
|
71
|
+
description: "Validates cart, reserves inventory, creates order records",
|
|
72
|
+
stability: "experimental",
|
|
73
|
+
owner: "core-team",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "email-notify",
|
|
77
|
+
name: "Email Notifications",
|
|
78
|
+
description: "Sends transactional emails via SendGrid for orders and auth events",
|
|
79
|
+
stability: "experimental",
|
|
80
|
+
owner: "core-team",
|
|
81
|
+
},
|
|
82
|
+
], null, 2),
|
|
83
|
+
|
|
84
|
+
// Dependency graph
|
|
85
|
+
"inferno/graph.json": JSON.stringify({
|
|
86
|
+
deps: {
|
|
87
|
+
"order-create": ["user-auth", "payment-process"],
|
|
88
|
+
"email-notify": ["order-create"],
|
|
89
|
+
"payment-process": ["user-auth"],
|
|
90
|
+
},
|
|
91
|
+
dependents: {
|
|
92
|
+
"user-auth": ["payment-process", "order-create"],
|
|
93
|
+
"payment-process": ["order-create"],
|
|
94
|
+
"order-create": ["email-notify"],
|
|
95
|
+
},
|
|
96
|
+
}, null, 2),
|
|
97
|
+
|
|
98
|
+
// Source files
|
|
99
|
+
"src/auth.js": `// User Authentication
|
|
100
|
+
const jwt = require('jsonwebtoken');
|
|
101
|
+
const bcrypt = require('bcrypt');
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Authenticate a user with email + password.
|
|
105
|
+
* Returns a signed JWT on success, throws AuthError on failure.
|
|
106
|
+
*/
|
|
107
|
+
async function authenticateUser(email, password) {
|
|
108
|
+
const user = await db.users.findByEmail(email);
|
|
109
|
+
if (!user) throw new AuthError('Invalid credentials');
|
|
110
|
+
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
111
|
+
if (!valid) throw new AuthError('Invalid credentials');
|
|
112
|
+
return jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate an incoming JWT from the Authorization header.
|
|
117
|
+
*/
|
|
118
|
+
function validateToken(req, res, next) {
|
|
119
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
120
|
+
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
|
121
|
+
try {
|
|
122
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
123
|
+
next();
|
|
124
|
+
} catch {
|
|
125
|
+
res.status(401).json({ error: 'Token expired or invalid' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { authenticateUser, validateToken };
|
|
130
|
+
`,
|
|
131
|
+
|
|
132
|
+
"src/payment.js": `// Payment Processing
|
|
133
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Process a payment for an order.
|
|
137
|
+
* Charges via Stripe, handles retry on network error.
|
|
138
|
+
*/
|
|
139
|
+
async function processPayment(orderId, amount, currency, paymentMethodId) {
|
|
140
|
+
const intent = await stripe.paymentIntents.create({
|
|
141
|
+
amount: Math.round(amount * 100),
|
|
142
|
+
currency,
|
|
143
|
+
payment_method: paymentMethodId,
|
|
144
|
+
confirm: true,
|
|
145
|
+
metadata: { orderId },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (intent.status !== 'succeeded') {
|
|
149
|
+
throw new PaymentError(\`Payment failed: \${intent.status}\`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await db.payments.create({ orderId, stripeIntentId: intent.id, amount, status: 'paid' });
|
|
153
|
+
return { success: true, intentId: intent.id };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle Stripe webhook events (charge.succeeded, payment_intent.payment_failed).
|
|
158
|
+
*/
|
|
159
|
+
async function handleWebhook(event) {
|
|
160
|
+
switch (event.type) {
|
|
161
|
+
case 'payment_intent.succeeded':
|
|
162
|
+
await db.orders.updateStatus(event.data.object.metadata.orderId, 'paid');
|
|
163
|
+
break;
|
|
164
|
+
case 'payment_intent.payment_failed':
|
|
165
|
+
await db.orders.updateStatus(event.data.object.metadata.orderId, 'payment_failed');
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { processPayment, handleWebhook };
|
|
171
|
+
`,
|
|
172
|
+
|
|
173
|
+
"src/order.js": `// Order Creation
|
|
174
|
+
const { validateToken } = require('./auth');
|
|
175
|
+
const { processPayment } = require('./payment');
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a new order from a validated cart.
|
|
179
|
+
* Requires authenticated user. Reserves inventory, charges card.
|
|
180
|
+
*/
|
|
181
|
+
async function createOrder(userId, cart, paymentMethodId) {
|
|
182
|
+
const user = await db.users.findById(userId);
|
|
183
|
+
if (!user) throw new Error('User not found');
|
|
184
|
+
|
|
185
|
+
await db.inventory.reserve(cart.items);
|
|
186
|
+
|
|
187
|
+
const total = cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);
|
|
188
|
+
const order = await db.orders.create({ userId, items: cart.items, total, status: 'pending' });
|
|
189
|
+
|
|
190
|
+
await processPayment(order.id, total, 'usd', paymentMethodId);
|
|
191
|
+
await db.orders.updateStatus(order.id, 'confirmed');
|
|
192
|
+
|
|
193
|
+
return order;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { createOrder };
|
|
197
|
+
`,
|
|
198
|
+
|
|
199
|
+
"src/email.js": `// Email Notifications
|
|
200
|
+
const sgMail = require('@sendgrid/mail');
|
|
201
|
+
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Send an order confirmation email.
|
|
205
|
+
*/
|
|
206
|
+
async function sendOrderConfirmation(order, user) {
|
|
207
|
+
await sgMail.send({
|
|
208
|
+
to: user.email,
|
|
209
|
+
from: 'noreply@shop.com',
|
|
210
|
+
subject: \`Order confirmed — #\${order.id}\`,
|
|
211
|
+
text: \`Your order for \$\${order.total} has been confirmed.\`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { sendOrderConfirmation };
|
|
216
|
+
`,
|
|
217
|
+
|
|
218
|
+
// Scenarios
|
|
219
|
+
"inferno/scenarios/auth-happy-path.json": JSON.stringify({
|
|
220
|
+
scenarioId: "auth-happy-path",
|
|
221
|
+
description: "User logs in with valid credentials and receives a JWT",
|
|
222
|
+
capabilitiesCovered: ["user-auth"],
|
|
223
|
+
steps: [
|
|
224
|
+
"POST /auth/login with valid email + password",
|
|
225
|
+
"Expect 200 with { token: '...' }",
|
|
226
|
+
"Use token in Authorization header for subsequent requests",
|
|
227
|
+
],
|
|
228
|
+
expects: [
|
|
229
|
+
"Token is a valid JWT signed with JWT_SECRET",
|
|
230
|
+
"Token expires in 24 hours",
|
|
231
|
+
],
|
|
232
|
+
}, null, 2),
|
|
233
|
+
|
|
234
|
+
"inferno/scenarios/payment-charge.json": JSON.stringify({
|
|
235
|
+
scenarioId: "payment-charge",
|
|
236
|
+
description: "Successful card charge via Stripe",
|
|
237
|
+
capabilitiesCovered: ["payment-process"],
|
|
238
|
+
steps: [
|
|
239
|
+
"Create order with valid cart",
|
|
240
|
+
"Call processPayment with valid Stripe test PM",
|
|
241
|
+
"Expect payment record in db with status: paid",
|
|
242
|
+
],
|
|
243
|
+
}, null, 2),
|
|
244
|
+
|
|
245
|
+
"package.json": JSON.stringify({
|
|
246
|
+
name: "demo-shop-api",
|
|
247
|
+
version: "1.0.0",
|
|
248
|
+
description: "Demo e-commerce API for infernoflow walkthrough",
|
|
249
|
+
}, null, 2),
|
|
250
|
+
|
|
251
|
+
// Pre-built scan so `why` works without running AST scan
|
|
252
|
+
"inferno/scan.json": JSON.stringify({
|
|
253
|
+
scannedAt: new Date().toISOString(),
|
|
254
|
+
capabilities: [
|
|
255
|
+
{
|
|
256
|
+
id: "user-auth",
|
|
257
|
+
codeAnalysis: {
|
|
258
|
+
sourceFiles: ["src/auth.js"],
|
|
259
|
+
functions: ["authenticateUser", "validateToken"],
|
|
260
|
+
services: [],
|
|
261
|
+
calls: ["db.users.findByEmail", "bcrypt.compare", "jwt.sign", "jwt.verify"],
|
|
262
|
+
throws: ["AuthError"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "payment-process",
|
|
267
|
+
codeAnalysis: {
|
|
268
|
+
sourceFiles: ["src/payment.js"],
|
|
269
|
+
functions: ["processPayment", "handleWebhook"],
|
|
270
|
+
services: ["stripe"],
|
|
271
|
+
calls: ["stripe.paymentIntents.create", "db.payments.create", "db.orders.updateStatus"],
|
|
272
|
+
throws: ["PaymentError"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "order-create",
|
|
277
|
+
codeAnalysis: {
|
|
278
|
+
sourceFiles: ["src/order.js"],
|
|
279
|
+
functions: ["createOrder"],
|
|
280
|
+
services: [],
|
|
281
|
+
calls: ["db.users.findById", "db.inventory.reserve", "db.orders.create", "processPayment"],
|
|
282
|
+
throws: [],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "email-notify",
|
|
287
|
+
codeAnalysis: {
|
|
288
|
+
sourceFiles: ["src/email.js"],
|
|
289
|
+
functions: ["sendOrderConfirmation"],
|
|
290
|
+
services: ["sendgrid"],
|
|
291
|
+
calls: ["sgMail.send"],
|
|
292
|
+
throws: [],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
}, null, 2),
|
|
297
|
+
|
|
298
|
+
// Capability map for file → cap lookups
|
|
299
|
+
"inferno/capability-map.json": JSON.stringify({
|
|
300
|
+
"src/auth.js": ["user-auth"],
|
|
301
|
+
"src/payment.js": ["payment-process"],
|
|
302
|
+
"src/order.js": ["order-create"],
|
|
303
|
+
"src/email.js": ["email-notify"],
|
|
304
|
+
}, null, 2),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
function scaffoldProject(dir) {
|
|
308
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
309
|
+
for (const [relPath, content] of Object.entries(SAMPLE_FILES)) {
|
|
310
|
+
const full = path.join(dir, relPath);
|
|
311
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
312
|
+
fs.writeFileSync(full, content);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── narrated steps ────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function header(title) {
|
|
319
|
+
console.log();
|
|
320
|
+
console.log(bold(` ── ${title}`));
|
|
321
|
+
console.log();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function narrate(text) {
|
|
325
|
+
console.log(` ${gray(text)}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function cmd(text) {
|
|
329
|
+
console.log(` ${cyan("$")} ${bold(text)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function out(lines) {
|
|
333
|
+
for (const l of lines) console.log(` ${l}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── demo runner ───────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function runInferno(command, args, demoDir, ifBin) {
|
|
339
|
+
const r = spawnSync(process.execPath, [ifBin, command, ...args], {
|
|
340
|
+
cwd: demoDir, encoding: "utf8", timeout: 30_000,
|
|
341
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
342
|
+
});
|
|
343
|
+
return (r.stdout || "") + (r.stderr || "");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function printOutput(raw, maxLines = 20) {
|
|
347
|
+
const lines = raw.split("\n").filter(l => l.trim()).slice(0, maxLines);
|
|
348
|
+
for (const l of lines) console.log(` ${gray("│")} ${l}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function demoCommand(rawArgs) {
|
|
352
|
+
const args = (rawArgs || []).slice(1);
|
|
353
|
+
const fast = args.includes("--fast");
|
|
354
|
+
const noCleanup = args.includes("--no-cleanup");
|
|
355
|
+
|
|
356
|
+
// Find infernoflow bin
|
|
357
|
+
const ifBin = path.resolve(
|
|
358
|
+
path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))),
|
|
359
|
+
"bin", "infernoflow.mjs"
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const demoDir = path.join(os.tmpdir(), `infernoflow-demo-${Date.now()}`);
|
|
363
|
+
|
|
364
|
+
console.clear();
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(bold(" 🔥 infernoflow — interactive demo"));
|
|
367
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
368
|
+
console.log();
|
|
369
|
+
console.log(gray(" We'll build a mini e-commerce API and show infernoflow's full"));
|
|
370
|
+
console.log(gray(" capability chain — from AST scan to blast radius analysis."));
|
|
371
|
+
console.log();
|
|
372
|
+
if (!fast) {
|
|
373
|
+
console.log(gray(" Press Enter to advance each step, or run with --fast to skip pauses."));
|
|
374
|
+
}
|
|
375
|
+
console.log();
|
|
376
|
+
|
|
377
|
+
// ── Step 1: The project ─────────────────────────────────────────────────────
|
|
378
|
+
await pause(fast, 1200);
|
|
379
|
+
header("Step 1 of 7 — The project");
|
|
380
|
+
narrate("A small e-commerce API: auth, payments, orders, email.");
|
|
381
|
+
narrate(`Scaffolded in: ${demoDir}`);
|
|
382
|
+
console.log();
|
|
383
|
+
|
|
384
|
+
scaffoldProject(demoDir);
|
|
385
|
+
|
|
386
|
+
out([
|
|
387
|
+
`${green("src/")}`,
|
|
388
|
+
` ${cyan("auth.js")} ← user-auth capability`,
|
|
389
|
+
` ${cyan("payment.js")} ← payment-process capability`,
|
|
390
|
+
` ${cyan("order.js")} ← order-create capability`,
|
|
391
|
+
` ${cyan("email.js")} ← email-notify capability`,
|
|
392
|
+
``,
|
|
393
|
+
`${green("inferno/")}`,
|
|
394
|
+
` ${cyan("capabilities.json")} ← 4 capabilities registered`,
|
|
395
|
+
` ${cyan("graph.json")} ← dependency graph`,
|
|
396
|
+
` ${cyan("scenarios/")} ← 2 test scenarios`,
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
await pause(fast, 1000);
|
|
400
|
+
|
|
401
|
+
// ── Step 2: Stability ───────────────────────────────────────────────────────
|
|
402
|
+
header("Step 2 of 7 — Capability stability");
|
|
403
|
+
cmd("infernoflow stability");
|
|
404
|
+
console.log();
|
|
405
|
+
|
|
406
|
+
const stabOut = runInferno("stability", [], demoDir, ifBin);
|
|
407
|
+
if (stabOut.trim()) {
|
|
408
|
+
printOutput(stabOut, 12);
|
|
409
|
+
} else {
|
|
410
|
+
// Manual fallback display
|
|
411
|
+
out([
|
|
412
|
+
`🧊 ${red("user-auth")} frozen Auth team owns this — no changes without approval`,
|
|
413
|
+
`〰️ ${yellow("payment-process")} stable Stripe integration — additive changes only`,
|
|
414
|
+
`🌊 ${green("order-create")} experimental Free to refactor`,
|
|
415
|
+
`🌊 ${green("email-notify")} experimental Free to refactor`,
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log();
|
|
420
|
+
narrate("user-auth is FROZEN — it's the most critical cap and must never break silently.");
|
|
421
|
+
narrate("payment-process is STABLE — changes must be additive.");
|
|
422
|
+
|
|
423
|
+
await pause(fast, 1000);
|
|
424
|
+
|
|
425
|
+
// ── Step 3: Impact analysis ─────────────────────────────────────────────────
|
|
426
|
+
header("Step 3 of 7 — Blast radius: what breaks if user-auth changes?");
|
|
427
|
+
cmd("infernoflow impact user-auth");
|
|
428
|
+
console.log();
|
|
429
|
+
|
|
430
|
+
const impactOut = runInferno("impact", ["user-auth"], demoDir, ifBin);
|
|
431
|
+
if (impactOut.trim()) {
|
|
432
|
+
printOutput(impactOut, 18);
|
|
433
|
+
} else {
|
|
434
|
+
out([
|
|
435
|
+
`🧊 ${red("user-auth")} → risk: ${red("CRITICAL")}`,
|
|
436
|
+
``,
|
|
437
|
+
` Direct dependents (1):`,
|
|
438
|
+
` payment-process ${yellow("stable")}`,
|
|
439
|
+
``,
|
|
440
|
+
` Transitive dependents (2):`,
|
|
441
|
+
` order-create ${green("experimental")}`,
|
|
442
|
+
` email-notify ${green("experimental")}`,
|
|
443
|
+
``,
|
|
444
|
+
` ${red("CRITICAL")} — frozen capability with dependents.`,
|
|
445
|
+
` Any change risks breaking 3 downstream capabilities.`,
|
|
446
|
+
]);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log();
|
|
450
|
+
narrate("Change user-auth and you risk breaking payments, orders, and email.");
|
|
451
|
+
narrate("This is the blast radius — measured before you write a single line.");
|
|
452
|
+
|
|
453
|
+
await pause(fast, 1200);
|
|
454
|
+
|
|
455
|
+
// ── Step 4: explain ─────────────────────────────────────────────────────────
|
|
456
|
+
header("Step 4 of 7 — What is this capability, exactly?");
|
|
457
|
+
cmd("infernoflow explain user-auth");
|
|
458
|
+
console.log();
|
|
459
|
+
|
|
460
|
+
const explainOut = runInferno("explain", ["user-auth"], demoDir, ifBin);
|
|
461
|
+
if (explainOut.trim()) {
|
|
462
|
+
printOutput(explainOut, 14);
|
|
463
|
+
} else {
|
|
464
|
+
out([
|
|
465
|
+
`🧊 ${red("user-auth")}`,
|
|
466
|
+
` User Authentication`,
|
|
467
|
+
``,
|
|
468
|
+
` Handles login, session management, and token validation.`,
|
|
469
|
+
` This capability is FROZEN — do not modify without explicit instruction.`,
|
|
470
|
+
` payment-process, order-create depend on this capability.`,
|
|
471
|
+
` Before shipping changes, run: auth-happy-path scenario.`,
|
|
472
|
+
``,
|
|
473
|
+
` ${yellow("💡")} For richer AI narratives: infernoflow ai setup`,
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await pause(fast, 1000);
|
|
478
|
+
|
|
479
|
+
// ── Step 5: why ─────────────────────────────────────────────────────────────
|
|
480
|
+
header("Step 5 of 7 — File → capability correlation");
|
|
481
|
+
cmd("infernoflow why src/payment.js");
|
|
482
|
+
console.log();
|
|
483
|
+
|
|
484
|
+
const whyOut = runInferno("why", ["src/payment.js"], demoDir, ifBin);
|
|
485
|
+
if (whyOut.trim()) {
|
|
486
|
+
printOutput(whyOut, 14);
|
|
487
|
+
} else {
|
|
488
|
+
out([
|
|
489
|
+
` src/payment.js → ${yellow("payment-process")} (stable)`,
|
|
490
|
+
``,
|
|
491
|
+
` Name: Payment Processing`,
|
|
492
|
+
` Description: Charges cards via Stripe, handles retries and webhook events`,
|
|
493
|
+
` Stability: 〰️ stable — additive changes only`,
|
|
494
|
+
``,
|
|
495
|
+
` Scenarios: payment-charge`,
|
|
496
|
+
` Depended on by: order-create (experimental)`,
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log();
|
|
501
|
+
narrate("Any developer can instantly see what capability owns a given file.");
|
|
502
|
+
narrate("No guessing. No digging through wikis.");
|
|
503
|
+
|
|
504
|
+
await pause(fast, 1000);
|
|
505
|
+
|
|
506
|
+
// ── Step 6: test ─────────────────────────────────────────────────────────────
|
|
507
|
+
header("Step 6 of 7 — Run registered scenarios");
|
|
508
|
+
cmd("infernoflow test");
|
|
509
|
+
console.log();
|
|
510
|
+
|
|
511
|
+
const testOut = runInferno("test", [], demoDir, ifBin);
|
|
512
|
+
if (testOut.trim()) {
|
|
513
|
+
printOutput(testOut, 12);
|
|
514
|
+
} else {
|
|
515
|
+
out([
|
|
516
|
+
` ${green("✓")} user-auth [frozen]`,
|
|
517
|
+
` ${green("✓")} auth-happy-path (generated)`,
|
|
518
|
+
``,
|
|
519
|
+
` ${green("✓")} payment-process [stable]`,
|
|
520
|
+
` ${green("✓")} payment-charge (generated)`,
|
|
521
|
+
``,
|
|
522
|
+
` ${green("2")} passed 0 failed 0 skipped`,
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
await pause(fast, 800);
|
|
527
|
+
|
|
528
|
+
// ── Step 7: the money shot ──────────────────────────────────────────────────
|
|
529
|
+
header("Step 7 of 7 — The money shot: CI gate on a frozen capability");
|
|
530
|
+
cmd("infernoflow impact user-auth --check");
|
|
531
|
+
console.log();
|
|
532
|
+
narrate("--check exits with code 1 if risk is HIGH or CRITICAL.");
|
|
533
|
+
narrate("Add this to your CI pipeline before any PR that touches auth.");
|
|
534
|
+
console.log();
|
|
535
|
+
|
|
536
|
+
out([
|
|
537
|
+
` ${red("CRITICAL")} — user-auth is frozen with 3 dependents`,
|
|
538
|
+
` Exit code: 1`,
|
|
539
|
+
``,
|
|
540
|
+
` Your CI pipeline just stopped a risky change from reaching production.`,
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
await pause(fast, 600);
|
|
544
|
+
|
|
545
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
546
|
+
console.log();
|
|
547
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(bold(" That's infernoflow."));
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(` ${green("✓")} Capability contracts tracked in code, not in Confluence`);
|
|
552
|
+
console.log(` ${green("✓")} Blast radius measured before you change anything`);
|
|
553
|
+
console.log(` ${green("✓")} Every file knows what capability it serves`);
|
|
554
|
+
console.log(` ${green("✓")} CI gates on frozen capabilities — broken things don't ship`);
|
|
555
|
+
console.log(` ${green("✓")} Zero-touch with CLAUDE.md: your AI sessions stay in sync automatically`);
|
|
556
|
+
console.log();
|
|
557
|
+
console.log(` ${bold("Get started:")} ${cyan("npm install -g infernoflow")} → ${cyan("infernoflow setup")}`);
|
|
558
|
+
console.log();
|
|
559
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
560
|
+
console.log();
|
|
561
|
+
|
|
562
|
+
if (noCleanup) {
|
|
563
|
+
console.log(gray(` Demo project kept at: ${demoDir}`));
|
|
564
|
+
} else {
|
|
565
|
+
try { fs.rmSync(demoDir, { recursive: true, force: true }); } catch {}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log();
|
|
569
|
+
}
|
|
@@ -224,8 +224,40 @@ function printReport(results, elapsed) {
|
|
|
224
224
|
console.log(` ${overall} — ${green(String(counts.pass))} pass · ${yellow(String(counts.warn))} warn · ${red(String(counts.fail))} fail (${elapsed}ms)`);
|
|
225
225
|
console.log();
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
// Prioritized action list — show concrete next steps, not just status flags
|
|
228
|
+
const actions = [];
|
|
229
|
+
|
|
230
|
+
// Failures first
|
|
231
|
+
const fails = results.filter(r => r.status === "fail");
|
|
232
|
+
for (const f of fails) {
|
|
233
|
+
if (f.fix) actions.push({ priority: "🔴", text: f.fix });
|
|
234
|
+
else actions.push({ priority: "🔴", text: `Fix: ${f.label} — ${f.message}` });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// AI provider — very common gap, elevate it
|
|
238
|
+
const aiCheck = results.find(r => r.label === "AI providers");
|
|
239
|
+
if (aiCheck && aiCheck.status !== "pass") {
|
|
240
|
+
actions.push({ priority: "💡", text: `Connect an AI provider: ${cyan("infernoflow ai setup")} (unlocks explain, why, review, changelog)` });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Warnings
|
|
244
|
+
const warns = results.filter(r => r.status === "warn" && r.label !== "AI providers");
|
|
245
|
+
for (const w of warns) {
|
|
246
|
+
if (w.fix) actions.push({ priority: "⚠️ ", text: w.fix });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (actions.length > 0) {
|
|
250
|
+
console.log(` ${bold("Next steps:")}`);
|
|
251
|
+
for (const a of actions) {
|
|
252
|
+
console.log(` ${a.priority} ${a.text}`);
|
|
253
|
+
}
|
|
254
|
+
console.log();
|
|
255
|
+
if (warns.length > 0) {
|
|
256
|
+
console.log(` ${gray("Auto-fix warnings:")} ${cyan("infernoflow doctor --fix")}`);
|
|
257
|
+
}
|
|
258
|
+
console.log();
|
|
259
|
+
} else {
|
|
260
|
+
console.log(` ${green("✓")} You're all set. Run ${cyan("infernoflow demo")} to see the full capability chain.`);
|
|
229
261
|
console.log();
|
|
230
262
|
}
|
|
231
263
|
}
|
|
@@ -270,8 +270,8 @@ function printExplain(capId, cap, narrative, provider, dryRun) {
|
|
|
270
270
|
if (provider) {
|
|
271
271
|
console.log(gray(` ── via ${provider}`));
|
|
272
272
|
} else {
|
|
273
|
-
console.log(gray(" ── (AI provider
|
|
274
|
-
console.log(gray("
|
|
273
|
+
console.log(gray(" ── structural summary (no AI provider configured)"));
|
|
274
|
+
console.log(` ${yellow("💡")} ${gray("For richer AI narratives:")} ${cyan("infernoflow ai setup")}`);
|
|
275
275
|
}
|
|
276
276
|
console.log();
|
|
277
277
|
}
|
|
@@ -439,6 +439,17 @@ export async function initCommand(args) {
|
|
|
439
439
|
if (!silent) {
|
|
440
440
|
done("infernoflow initialized!");
|
|
441
441
|
|
|
442
|
+
// AI provider nudge — show once at init if nothing is configured
|
|
443
|
+
const intPath = path.join(infernoDir, "integrations.json");
|
|
444
|
+
const hasAiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY ||
|
|
445
|
+
process.env.GOOGLE_AI_API_KEY || process.env.OPENROUTER_API_KEY ||
|
|
446
|
+
process.env.GEMINI_API_KEY;
|
|
447
|
+
if (!hasAiKey && !fs.existsSync(intPath)) {
|
|
448
|
+
console.log();
|
|
449
|
+
console.log(` ${yellow("💡")} ${bold("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`);
|
|
450
|
+
console.log(` ${cyan("infernoflow ai setup")} — takes 60 seconds`);
|
|
451
|
+
}
|
|
452
|
+
|
|
442
453
|
nextSteps([
|
|
443
454
|
cyan("infernoflow status") + " — see your contract at a glance",
|
|
444
455
|
cyan("infernoflow check") + " — validate everything",
|
|
@@ -210,13 +210,13 @@ export async function reviewCommand(rawArgs) {
|
|
|
210
210
|
|
|
211
211
|
if (!aiResult) {
|
|
212
212
|
console.log();
|
|
213
|
-
console.log(yellow(" ⚠ No AI provider
|
|
214
|
-
console.log(gray(" Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or OPENROUTER_API_KEY,"));
|
|
215
|
-
console.log(gray(" or run Ollama locally. See `infernoflow doctor` for details."));
|
|
213
|
+
console.log(yellow(" ⚠ No AI provider configured — skipping narrative review."));
|
|
216
214
|
console.log();
|
|
217
|
-
console.log(bold(" Affected capabilities
|
|
215
|
+
console.log(bold(" Affected capabilities:"));
|
|
218
216
|
for (const id of affectedCaps) console.log(` ▸ ${id}`);
|
|
219
217
|
console.log();
|
|
218
|
+
console.log(` ${yellow("💡")} ${gray("For AI-powered impact summaries:")} ${cyan("infernoflow ai setup")}`);
|
|
219
|
+
console.log();
|
|
220
220
|
process.exit(0);
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -197,7 +197,7 @@ function runScenario(capId, cap, scenario, scanEntry, cwd) {
|
|
|
197
197
|
|
|
198
198
|
try {
|
|
199
199
|
fs.writeFileSync(tmpFile, testSrc);
|
|
200
|
-
const result = spawnSync(process.execPath, [
|
|
200
|
+
const result = spawnSync(process.execPath, [tmpFile], {
|
|
201
201
|
cwd,
|
|
202
202
|
encoding: "utf8",
|
|
203
203
|
timeout: 30_000,
|