infernoflow 0.24.0 → 0.28.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 +33 -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/scaffold.mjs +489 -0
- package/dist/lib/commands/scan.mjs +45 -26
- package/dist/lib/commands/why.mjs +358 -0
- package/package.json +1 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow scaffold
|
|
3
|
+
*
|
|
4
|
+
* Generate a new capability — source file skeleton + contract registration
|
|
5
|
+
* + placeholder scenario — pre-wired to the project's detected patterns.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* infernoflow scaffold payment-refund
|
|
9
|
+
* infernoflow scaffold payment-refund --dir src/payments
|
|
10
|
+
* infernoflow scaffold payment-refund --lang ts --dry-run
|
|
11
|
+
* infernoflow scaffold payment-refund --json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
17
|
+
|
|
18
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function loadJson(p) {
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
22
|
+
catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function saveJson(p, data) {
|
|
26
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── name derivers ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** payment-refund → PaymentRefund */
|
|
32
|
+
function toPascalCase(id) {
|
|
33
|
+
return id.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** payment-refund → paymentRefund */
|
|
37
|
+
function toCamelCase(id) {
|
|
38
|
+
const p = toPascalCase(id);
|
|
39
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** payment-refund → "Payment Refund" */
|
|
43
|
+
function toTitle(id) {
|
|
44
|
+
return id.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Derive a primary function name from the cap ID.
|
|
49
|
+
* payment-refund → refundPayment, user-auth → authenticateUser
|
|
50
|
+
*/
|
|
51
|
+
function primaryFnName(id) {
|
|
52
|
+
const parts = id.split(/[-_]/);
|
|
53
|
+
if (parts.length === 1) return toCamelCase(id);
|
|
54
|
+
|
|
55
|
+
// Common verb patterns: if second word is a verb-like term, flip order
|
|
56
|
+
const VERBS = ["auth", "login", "logout", "register", "refresh", "validate",
|
|
57
|
+
"verify", "process", "refund", "charge", "send", "fetch",
|
|
58
|
+
"create", "update", "delete", "get", "list", "search",
|
|
59
|
+
"sync", "import", "export", "scan", "check", "notify"];
|
|
60
|
+
|
|
61
|
+
const last = parts[parts.length - 1];
|
|
62
|
+
const first = parts[0];
|
|
63
|
+
|
|
64
|
+
// If last part looks like a noun and first part looks verb-like, use as-is
|
|
65
|
+
if (VERBS.includes(first)) {
|
|
66
|
+
// auth-user → authenticateUser style (expand verb)
|
|
67
|
+
const verbExpand = { auth: "authenticate", get: "get", list: "list",
|
|
68
|
+
send: "send", check: "check", notify: "notify" };
|
|
69
|
+
const verb = verbExpand[first] || first;
|
|
70
|
+
const rest = parts.slice(1).map((w, i) =>
|
|
71
|
+
i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w
|
|
72
|
+
).join("");
|
|
73
|
+
return verb + rest.charAt(0).toUpperCase() + rest.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Default: flip last+first — payment-refund → refundPayment
|
|
77
|
+
if (VERBS.includes(last)) {
|
|
78
|
+
const noun = parts.slice(0, -1).map((w, i) =>
|
|
79
|
+
i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w
|
|
80
|
+
).join("");
|
|
81
|
+
return last + noun;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return toCamelCase(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── language detector ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function detectLang(scan, profile, cwd) {
|
|
90
|
+
// 1. From scan source files
|
|
91
|
+
if (scan?.capabilities?.length) {
|
|
92
|
+
const files = scan.capabilities.flatMap(c => c.codeAnalysis?.sourceFiles || []);
|
|
93
|
+
const exts = files.map(f => path.extname(f));
|
|
94
|
+
if (exts.filter(e => e === ".ts").length > exts.filter(e => e === ".js").length) return "ts";
|
|
95
|
+
if (exts.includes(".py")) return "py";
|
|
96
|
+
if (exts.includes(".go")) return "go";
|
|
97
|
+
if (exts.some(e => e === ".js" || e === ".mjs")) return "js";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. From profile
|
|
101
|
+
const lang = profile?.language || profile?.lang;
|
|
102
|
+
if (lang) return lang.toLowerCase().replace("javascript", "js").replace("typescript", "ts");
|
|
103
|
+
|
|
104
|
+
// 3. From project files
|
|
105
|
+
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) return "ts";
|
|
106
|
+
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) return "py";
|
|
107
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) return "go";
|
|
108
|
+
|
|
109
|
+
return "js";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function detectSrcDir(scan, cwd) {
|
|
113
|
+
if (!scan?.capabilities?.length) return null;
|
|
114
|
+
const files = scan.capabilities.flatMap(c => c.codeAnalysis?.sourceFiles || []);
|
|
115
|
+
if (!files.length) return null;
|
|
116
|
+
|
|
117
|
+
// Count dir prefixes
|
|
118
|
+
const dirCount = {};
|
|
119
|
+
for (const f of files) {
|
|
120
|
+
const dir = path.dirname(f).split("/")[0];
|
|
121
|
+
dirCount[dir] = (dirCount[dir] || 0) + 1;
|
|
122
|
+
}
|
|
123
|
+
const top = Object.entries(dirCount).sort((a, b) => b[1] - a[1])[0];
|
|
124
|
+
return top ? top[0] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function detectServices(scan) {
|
|
128
|
+
if (!scan?.capabilities?.length) return [];
|
|
129
|
+
const all = scan.capabilities.flatMap(c => c.codeAnalysis?.services || []);
|
|
130
|
+
return [...new Set(all)];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── code generators ───────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function generateTs(id, name, description, fn, services) {
|
|
136
|
+
const pascal = toPascalCase(id);
|
|
137
|
+
const errorName = `${pascal}Error`;
|
|
138
|
+
const imports = buildImports("ts", services);
|
|
139
|
+
|
|
140
|
+
return `/**
|
|
141
|
+
* ${name}
|
|
142
|
+
*
|
|
143
|
+
* ${description}
|
|
144
|
+
*
|
|
145
|
+
* @capability ${id}
|
|
146
|
+
* @stability experimental
|
|
147
|
+
*/
|
|
148
|
+
${imports}
|
|
149
|
+
|
|
150
|
+
// ── errors ────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export class ${errorName} extends Error {
|
|
153
|
+
constructor(message: string, public readonly code?: string) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.name = "${errorName}";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── types ─────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export interface ${pascal}Input {
|
|
162
|
+
// TODO: define input fields
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ${pascal}Result {
|
|
166
|
+
// TODO: define result fields
|
|
167
|
+
success: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── implementation ────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* ${fn} — primary entry point for ${name}.
|
|
174
|
+
* TODO: implement this function.
|
|
175
|
+
*/
|
|
176
|
+
export async function ${fn}(input: ${pascal}Input): Promise<${pascal}Result> {
|
|
177
|
+
// TODO: implement
|
|
178
|
+
throw new ${errorName}("Not implemented yet");
|
|
179
|
+
}
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function generateJs(id, name, description, fn, services) {
|
|
184
|
+
const pascal = toPascalCase(id);
|
|
185
|
+
const errorName = `${pascal}Error`;
|
|
186
|
+
const imports = buildImports("js", services);
|
|
187
|
+
|
|
188
|
+
return `/**
|
|
189
|
+
* ${name}
|
|
190
|
+
*
|
|
191
|
+
* ${description}
|
|
192
|
+
*
|
|
193
|
+
* @capability ${id}
|
|
194
|
+
* @stability experimental
|
|
195
|
+
*/
|
|
196
|
+
${imports}
|
|
197
|
+
|
|
198
|
+
// ── errors ────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
export class ${errorName} extends Error {
|
|
201
|
+
constructor(message, code) {
|
|
202
|
+
super(message);
|
|
203
|
+
this.name = "${errorName}";
|
|
204
|
+
this.code = code;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── implementation ────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* ${fn} — primary entry point for ${name}.
|
|
212
|
+
* TODO: implement this function.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} input
|
|
215
|
+
* @returns {Promise<object>}
|
|
216
|
+
*/
|
|
217
|
+
export async function ${fn}(input = {}) {
|
|
218
|
+
// TODO: implement
|
|
219
|
+
throw new ${errorName}("Not implemented yet");
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function generatePy(id, name, description, fn) {
|
|
225
|
+
const cls = toPascalCase(id);
|
|
226
|
+
|
|
227
|
+
return `"""
|
|
228
|
+
${name}
|
|
229
|
+
|
|
230
|
+
${description}
|
|
231
|
+
|
|
232
|
+
capability: ${id}
|
|
233
|
+
stability: experimental
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
from typing import Any
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ${cls}Error(Exception):
|
|
240
|
+
"""Raised when ${name} operations fail."""
|
|
241
|
+
def __init__(self, message: str, code: str | None = None):
|
|
242
|
+
super().__init__(message)
|
|
243
|
+
self.code = code
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def ${fn.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '')}(input: dict[str, Any]) -> dict[str, Any]:
|
|
247
|
+
"""Primary entry point for ${name}.
|
|
248
|
+
|
|
249
|
+
TODO: implement this function.
|
|
250
|
+
"""
|
|
251
|
+
raise ${cls}Error("Not implemented yet")
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateGo(id, name, description, fn) {
|
|
256
|
+
const pkg = id.split("-")[0];
|
|
257
|
+
|
|
258
|
+
return `// Package ${pkg} implements ${name}.
|
|
259
|
+
//
|
|
260
|
+
// ${description}
|
|
261
|
+
//
|
|
262
|
+
// capability: ${id}
|
|
263
|
+
// stability: experimental
|
|
264
|
+
package ${pkg}
|
|
265
|
+
|
|
266
|
+
import "errors"
|
|
267
|
+
|
|
268
|
+
// Err${toPascalCase(id)} is returned when ${name} operations fail.
|
|
269
|
+
var Err${toPascalCase(id)} = errors.New("${id}: operation failed")
|
|
270
|
+
|
|
271
|
+
// ${toPascalCase(fn)} is the primary entry point for ${name}.
|
|
272
|
+
// TODO: implement this function.
|
|
273
|
+
func ${toPascalCase(fn)}(input map[string]any) (map[string]any, error) {
|
|
274
|
+
\treturn nil, Err${toPascalCase(id)}
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildImports(lang, services) {
|
|
280
|
+
if (!services.length) return "";
|
|
281
|
+
const lines = [];
|
|
282
|
+
if (lang === "ts" || lang === "js") {
|
|
283
|
+
// Suggest common client imports for detected services
|
|
284
|
+
const serviceImports = {
|
|
285
|
+
stripe: `// import Stripe from 'stripe';`,
|
|
286
|
+
postgres: `// import { Pool } from 'pg';`,
|
|
287
|
+
mysql: `// import mysql from 'mysql2/promise';`,
|
|
288
|
+
redis: `// import { createClient } from 'redis';`,
|
|
289
|
+
s3: `// import { S3Client } from '@aws-sdk/client-s3';`,
|
|
290
|
+
sendgrid: `// import sgMail from '@sendgrid/mail';`,
|
|
291
|
+
twilio: `// import twilio from 'twilio';`,
|
|
292
|
+
openai: `// import OpenAI from 'openai';`,
|
|
293
|
+
};
|
|
294
|
+
for (const svc of services) {
|
|
295
|
+
const imp = serviceImports[svc.toLowerCase()];
|
|
296
|
+
if (imp) lines.push(imp);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return lines.length ? lines.join("\n") + "\n" : "";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── scenario generator ────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
function generateScenario(id, name, fn) {
|
|
305
|
+
return {
|
|
306
|
+
scenarioId: `${id}-happy-path`,
|
|
307
|
+
description: `Happy path for ${name}`,
|
|
308
|
+
capabilitiesCovered: [id],
|
|
309
|
+
createdAt: new Date().toISOString(),
|
|
310
|
+
steps: [
|
|
311
|
+
{ step: 1, action: `Call ${fn} with valid input`, expected: "Returns success result" },
|
|
312
|
+
{ step: 2, action: `Call ${fn} with invalid input`, expected: "Throws appropriate error" },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── printer ───────────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function printResult({ id, filePath, scenarioPath, lang, fn, dryRun }) {
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(bold(` 🌊 ${green(id)}`));
|
|
322
|
+
console.log(gray(" stability: experimental — free to evolve"));
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(gray(" Generated:"));
|
|
325
|
+
console.log(` ${green("+")} ${cyan(filePath)} ${gray(`(${lang} source skeleton)`)}`);
|
|
326
|
+
console.log(` ${green("+")} ${cyan("inferno/capabilities.json")} ${gray("(capability registered)")}`);
|
|
327
|
+
console.log(` ${green("+")} ${cyan(scenarioPath)} ${gray("(placeholder scenario)")}`);
|
|
328
|
+
console.log();
|
|
329
|
+
if (dryRun) {
|
|
330
|
+
console.log(yellow(" [dry-run] — no files were written"));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(gray(" Next steps:"));
|
|
333
|
+
console.log(gray(` 1. Implement ${fn}() in ${filePath}`));
|
|
334
|
+
console.log(gray(` 2. Run: infernoflow scan — to extract call graph`));
|
|
335
|
+
console.log(gray(` 3. Run: infernoflow graph — to see dependencies`));
|
|
336
|
+
console.log(gray(` 4. Run: infernoflow check — to validate contract`));
|
|
337
|
+
}
|
|
338
|
+
console.log();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
export async function scaffoldCommand(rawArgs) {
|
|
344
|
+
const args = (rawArgs || []).slice(1);
|
|
345
|
+
const dryRun = args.includes("--dry-run");
|
|
346
|
+
const jsonMode = args.includes("--json");
|
|
347
|
+
|
|
348
|
+
const langIdx = args.indexOf("--lang");
|
|
349
|
+
const langArg = langIdx !== -1 ? args[langIdx + 1] : null;
|
|
350
|
+
|
|
351
|
+
const dirIdx = args.indexOf("--dir");
|
|
352
|
+
const dirArg = dirIdx !== -1 ? args[dirIdx + 1] : null;
|
|
353
|
+
|
|
354
|
+
const descIdx = args.indexOf("--description");
|
|
355
|
+
const descArg = descIdx !== -1 ? args[descIdx + 1] : null;
|
|
356
|
+
|
|
357
|
+
// Cap ID: first non-flag arg (skip values after --lang, --dir, --description)
|
|
358
|
+
const skipIdxs = new Set([langIdx + 1, dirIdx + 1, descIdx + 1].filter(i => i > 0));
|
|
359
|
+
const capId = args.find((a, i) => !a.startsWith("--") && !skipIdxs.has(i));
|
|
360
|
+
|
|
361
|
+
if (!capId) {
|
|
362
|
+
console.error(red("✗ Usage: infernoflow scaffold <capability-id> [--dir <src>] [--lang ts|js|py|go] [--dry-run] [--json]"));
|
|
363
|
+
console.error(gray(" Example: infernoflow scaffold payment-refund"));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Validate cap ID format
|
|
368
|
+
if (!/^[a-z][a-z0-9-]*$/.test(capId)) {
|
|
369
|
+
console.error(red(`✗ Invalid capability ID: "${capId}"`));
|
|
370
|
+
console.error(gray(" Use lowercase kebab-case: payment-refund, user-auth, etc."));
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const cwd = process.cwd();
|
|
375
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
376
|
+
|
|
377
|
+
// Load context
|
|
378
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
379
|
+
if (!fs.existsSync(capsPath)) {
|
|
380
|
+
console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let allCaps = [];
|
|
385
|
+
const rawCaps = loadJson(capsPath);
|
|
386
|
+
if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
|
|
387
|
+
|
|
388
|
+
// Check duplicate
|
|
389
|
+
if (allCaps.some(c => c.id === capId)) {
|
|
390
|
+
console.error(red(`✗ Capability "${capId}" already exists in capabilities.json`));
|
|
391
|
+
console.error(gray(" Use a different ID, or run: infernoflow why " + capId));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const scan = loadJson(path.join(infernoDir, "scan.json"));
|
|
396
|
+
const profile = loadJson(path.join(infernoDir, "developer-profile.json"));
|
|
397
|
+
|
|
398
|
+
// Detect language
|
|
399
|
+
const lang = langArg || detectLang(scan, profile, cwd);
|
|
400
|
+
|
|
401
|
+
// Detect output directory
|
|
402
|
+
const srcDir = dirArg || detectSrcDir(scan, cwd) || "src";
|
|
403
|
+
const ext = { ts: ".ts", js: ".js", py: ".py", go: ".go" }[lang] || ".js";
|
|
404
|
+
|
|
405
|
+
// Derive names
|
|
406
|
+
const name = toTitle(capId);
|
|
407
|
+
const description = descArg || `TODO: describe ${name}`;
|
|
408
|
+
const fn = primaryFnName(capId);
|
|
409
|
+
const services = detectServices(scan);
|
|
410
|
+
|
|
411
|
+
// Generate code
|
|
412
|
+
let code;
|
|
413
|
+
if (lang === "ts") code = generateTs(capId, name, description, fn, services);
|
|
414
|
+
else if (lang === "py") code = generatePy(capId, name, description, fn);
|
|
415
|
+
else if (lang === "go") code = generateGo(capId, name, description, fn);
|
|
416
|
+
else code = generateJs(capId, name, description, fn, services);
|
|
417
|
+
|
|
418
|
+
// File path: srcDir/capId (replace dashes with nothing) + ext
|
|
419
|
+
// e.g. payment-refund → src/paymentRefund.ts
|
|
420
|
+
const fileName = toCamelCase(capId) + ext;
|
|
421
|
+
const filePath = path.join(srcDir, fileName);
|
|
422
|
+
const absFile = path.join(cwd, filePath);
|
|
423
|
+
|
|
424
|
+
// Scenario path
|
|
425
|
+
const scenarioPath = path.join("inferno", "scenarios", `${capId}.json`);
|
|
426
|
+
const absScenario = path.join(cwd, scenarioPath);
|
|
427
|
+
const scenario = generateScenario(capId, name, fn);
|
|
428
|
+
|
|
429
|
+
// New capability entry
|
|
430
|
+
const newCap = {
|
|
431
|
+
id: capId,
|
|
432
|
+
name,
|
|
433
|
+
description,
|
|
434
|
+
stability: "experimental",
|
|
435
|
+
since: new Date().toISOString().slice(0, 10),
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (jsonMode) {
|
|
439
|
+
const out = {
|
|
440
|
+
capId,
|
|
441
|
+
name,
|
|
442
|
+
stability: "experimental",
|
|
443
|
+
lang,
|
|
444
|
+
filePath,
|
|
445
|
+
scenarioPath,
|
|
446
|
+
primaryFn: fn,
|
|
447
|
+
dryRun,
|
|
448
|
+
code,
|
|
449
|
+
};
|
|
450
|
+
console.log(JSON.stringify(out, null, 2));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log(gray(`\n infernoflow scaffold → ${bold(capId)}`));
|
|
455
|
+
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
456
|
+
|
|
457
|
+
if (!dryRun) {
|
|
458
|
+
// Create source directory if needed
|
|
459
|
+
const absDir = path.dirname(absFile);
|
|
460
|
+
if (!fs.existsSync(absDir)) fs.mkdirSync(absDir, { recursive: true });
|
|
461
|
+
|
|
462
|
+
// Write source file
|
|
463
|
+
if (fs.existsSync(absFile)) {
|
|
464
|
+
console.error(red(` ✗ File already exists: ${filePath}`));
|
|
465
|
+
console.error(gray(" Delete it first or choose a different --dir"));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
fs.writeFileSync(absFile, code, "utf8");
|
|
469
|
+
|
|
470
|
+
// Register capability
|
|
471
|
+
allCaps.push(newCap);
|
|
472
|
+
saveJson(capsPath, allCaps);
|
|
473
|
+
|
|
474
|
+
// Write scenario
|
|
475
|
+
const scenDir = path.join(cwd, "inferno", "scenarios");
|
|
476
|
+
if (!fs.existsSync(scenDir)) fs.mkdirSync(scenDir, { recursive: true });
|
|
477
|
+
if (!fs.existsSync(absScenario)) {
|
|
478
|
+
saveJson(absScenario, scenario);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Show code preview
|
|
483
|
+
const previewLines = code.split("\n").slice(0, 12).map(l => " " + l).join("\n");
|
|
484
|
+
console.log(gray("\n Preview:"));
|
|
485
|
+
console.log(gray(previewLines));
|
|
486
|
+
console.log(gray(" ..."));
|
|
487
|
+
|
|
488
|
+
printResult({ id: capId, filePath, scenarioPath, lang, fn, dryRun });
|
|
489
|
+
}
|
|
@@ -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);
|