infernoflow 0.10.5 → 0.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/bin/infernoflow.mjs +6 -0
- package/lib/commands/adopt.mjs +264 -3
- package/lib/commands/init.mjs +13 -1
- package/lib/commands/prImpact.mjs +157 -0
- package/lib/commands/syncAuto.mjs +96 -0
- package/package.json +2 -2
- package/templates/ci/github-inferno-check.yml +36 -0
- package/templates/scripts/inferno-install-hooks.mjs +36 -0
package/README.md
CHANGED
|
@@ -62,6 +62,12 @@ Override detected stack during adoption:
|
|
|
62
62
|
infernoflow init --adopt --lang ts --framework angular --project-type frontend
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
C# / ASP.NET example:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
infernoflow init --adopt --lang cs --framework aspnet --project-type backend --report-human-only
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
JSON report for CI/logging:
|
|
66
72
|
|
|
67
73
|
```bash
|
|
@@ -85,6 +91,7 @@ What adoption creates:
|
|
|
85
91
|
- `inferno/capabilities.json` (inferred registry)
|
|
86
92
|
- `inferno/scenarios/adoption_baseline.json` (coverage baseline)
|
|
87
93
|
- `inferno/adoption_profile.json` (detected components, display fields, external libraries, UI layout, styling hints)
|
|
94
|
+
- `inferno/adoption_profile.json` (detected components, display fields, external libraries, UI layout, styling hints, API call map)
|
|
88
95
|
- `inferno/context-state.json` (saved development profile: language/framework/project type)
|
|
89
96
|
- `inferno/CHANGELOG.md` (adoption entry)
|
|
90
97
|
|
|
@@ -159,6 +166,8 @@ infernoflow doc-gate --json
|
|
|
159
166
|
| `infernoflow status` | At-a-glance health of your contract |
|
|
160
167
|
| `infernoflow suggest` | Generate an AI prompt, apply capability updates |
|
|
161
168
|
| `infernoflow implement` | Generate implementation prompts for coding agents |
|
|
169
|
+
| `infernoflow pr-impact` | Analyze changed files and infer capability/doc drift |
|
|
170
|
+
| `infernoflow sync --auto` | Deterministic sync flow for agents (skeleton) |
|
|
162
171
|
| `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
|
|
163
172
|
| `infernoflow doc-gate` | Fails if code changed but docs weren't updated |
|
|
164
173
|
| `infernoflow context` | Build/persist AI session context for this project |
|
|
@@ -176,6 +185,11 @@ infernoflow implement "..." --mode both
|
|
|
176
185
|
infernoflow implement "..." --mode cursor
|
|
177
186
|
infernoflow implement "..." --mode generic
|
|
178
187
|
infernoflow implement "..." --mode both --copy
|
|
188
|
+
infernoflow pr-impact
|
|
189
|
+
infernoflow pr-impact --json
|
|
190
|
+
infernoflow sync --auto
|
|
191
|
+
infernoflow sync --auto --json
|
|
192
|
+
npm run inferno:hooks # install local git hooks (after init)
|
|
179
193
|
infernoflow check --json # machine-readable output for CI
|
|
180
194
|
infernoflow check --skip-doc-gate
|
|
181
195
|
infernoflow status --json # machine-readable status summary
|
|
@@ -265,13 +279,25 @@ Recommended chain:
|
|
|
265
279
|
|
|
266
280
|
```yaml
|
|
267
281
|
# .github/workflows/ci.yml
|
|
268
|
-
- name: infernoflow check
|
|
269
|
-
run:
|
|
282
|
+
- name: infernoflow impact + check
|
|
283
|
+
run: |
|
|
284
|
+
npx infernoflow pr-impact --json
|
|
285
|
+
npx infernoflow check --json
|
|
270
286
|
env:
|
|
271
287
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
272
288
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
273
289
|
```
|
|
274
290
|
|
|
291
|
+
When you run `infernoflow init`, it now scaffolds:
|
|
292
|
+
- `scripts/inferno-install-hooks.mjs`
|
|
293
|
+
- `.github/workflows/infernoflow-check.yml`
|
|
294
|
+
|
|
295
|
+
Install local hooks once per clone:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run inferno:hooks
|
|
299
|
+
```
|
|
300
|
+
|
|
275
301
|
## Release Checklist
|
|
276
302
|
|
|
277
303
|
```bash
|
package/bin/infernoflow.mjs
CHANGED
|
@@ -11,6 +11,8 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
11
11
|
init: "Scaffold inferno/ in your project (or adopt existing project)",
|
|
12
12
|
check: "Validate contract, capabilities, scenarios, changelog",
|
|
13
13
|
status: "Show contract health at a glance",
|
|
14
|
+
"pr-impact": "Summarize PR impact on capabilities and docs",
|
|
15
|
+
sync: "Run deterministic inferno sync flow",
|
|
14
16
|
"doc-gate": "Fail if code changed but docs were not updated",
|
|
15
17
|
suggest: "Generate AI prompt + apply capability updates",
|
|
16
18
|
implement: "Generate code-agent implementation prompt(s)",
|
|
@@ -21,6 +23,8 @@ const COMMAND_HANDLERS = {
|
|
|
21
23
|
init: async (args) => (await import("../lib/commands/init.mjs")).initCommand(args),
|
|
22
24
|
check: async (args) => (await import("../lib/commands/check.mjs")).checkCommand(args),
|
|
23
25
|
status: async (args) => (await import("../lib/commands/status.mjs")).statusCommand(args),
|
|
26
|
+
"pr-impact": async (args) => (await import("../lib/commands/prImpact.mjs")).prImpactCommand(args),
|
|
27
|
+
sync: async (args) => (await import("../lib/commands/syncAuto.mjs")).syncCommand(args),
|
|
24
28
|
suggest: async (args) => (await import("../lib/commands/suggest.mjs")).suggestCommand(args),
|
|
25
29
|
implement: async (args) => (await import("../lib/commands/implement.mjs")).implementCommand(args),
|
|
26
30
|
context: async (args) => (await import("../lib/commands/context.mjs")).contextCommand(args),
|
|
@@ -77,6 +81,8 @@ ${formatCommandsHelp()}
|
|
|
77
81
|
${gray("status --json")}
|
|
78
82
|
${gray("check --json")}
|
|
79
83
|
${gray("doc-gate --json")}
|
|
84
|
+
${gray("pr-impact --json")}
|
|
85
|
+
${gray("sync --auto --json")}
|
|
80
86
|
`;
|
|
81
87
|
|
|
82
88
|
const [, , cmd, ...rest] = process.argv;
|
package/lib/commands/adopt.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export function discoverCapabilities(cwd) {
|
|
|
43
43
|
|
|
44
44
|
function collectCodeFiles(cwd) {
|
|
45
45
|
const files = [];
|
|
46
|
-
const roots = ["src", "server", "app", "backend", "frontend", "api"];
|
|
46
|
+
const roots = ["src", "server", "app", "backend", "frontend", "api", "Controllers"];
|
|
47
47
|
for (const r of roots) {
|
|
48
48
|
const root = path.join(cwd, r);
|
|
49
49
|
if (!fs.existsSync(root)) continue;
|
|
@@ -55,12 +55,19 @@ function collectCodeFiles(cwd) {
|
|
|
55
55
|
if (entry.isDirectory()) {
|
|
56
56
|
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
57
57
|
stack.push(p);
|
|
58
|
-
} else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm)$/.test(entry.name)) {
|
|
58
|
+
} else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(entry.name)) {
|
|
59
59
|
files.push(p);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// Include common root-level .NET entry files without scanning the whole repo.
|
|
65
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isFile()) continue;
|
|
67
|
+
if (/^(Program\.cs|.+\.csproj)$/i.test(entry.name)) {
|
|
68
|
+
files.push(path.join(cwd, entry.name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
64
71
|
return files;
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -237,6 +244,21 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
|
|
|
237
244
|
const autoLanguage = sortedLang[0]?.[1] > 0 ? sortedLang[0][0] : "unknown";
|
|
238
245
|
|
|
239
246
|
let autoFramework = "unknown";
|
|
247
|
+
let hasDotnetWebSdk = false;
|
|
248
|
+
let hasMinimalApi = false;
|
|
249
|
+
let hasBlazor = false;
|
|
250
|
+
for (const filePath of files) {
|
|
251
|
+
const base = path.basename(filePath).toLowerCase();
|
|
252
|
+
if (base.endsWith(".csproj")) {
|
|
253
|
+
const text = safeRead(filePath);
|
|
254
|
+
if (/Microsoft\.NET\.Sdk\.Web/i.test(text)) hasDotnetWebSdk = true;
|
|
255
|
+
if (/Blazor/i.test(text) || /Microsoft\.AspNetCore\.Components/i.test(text)) hasBlazor = true;
|
|
256
|
+
}
|
|
257
|
+
if (base === "program.cs") {
|
|
258
|
+
const text = safeRead(filePath);
|
|
259
|
+
if (/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(text)) hasMinimalApi = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
240
262
|
const hasDep = (name) => externalLibraries.includes(name);
|
|
241
263
|
if (externalLibraries.some((d) => d.startsWith("@angular/"))) autoFramework = "angular";
|
|
242
264
|
else if (hasDep("react")) autoFramework = "react";
|
|
@@ -250,14 +272,18 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
|
|
|
250
272
|
else if (hasDep("flask")) autoFramework = "flask";
|
|
251
273
|
else if (hasDep("django")) autoFramework = "django";
|
|
252
274
|
else if (hasDep("spring-boot")) autoFramework = "spring";
|
|
275
|
+
else if (hasBlazor) autoFramework = "blazor";
|
|
276
|
+
else if (hasMinimalApi) autoFramework = "minimalapi";
|
|
277
|
+
else if (hasDotnetWebSdk || extCount.cs > 0) autoFramework = "aspnet";
|
|
253
278
|
|
|
254
279
|
let autoProjectType = "fullstack";
|
|
255
280
|
const hasClientRoots = ["src", "frontend", "app"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
256
281
|
const hasServerRoots = ["server", "backend", "api"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
257
282
|
if (["react", "angular", "vue", "svelte", "nextjs", "nuxt"].includes(autoFramework)) autoProjectType = "frontend";
|
|
258
|
-
if (["express", "nestjs", "fastify", "flask", "django", "spring"].includes(autoFramework)) autoProjectType = "backend";
|
|
283
|
+
if (["express", "nestjs", "fastify", "flask", "django", "spring", "aspnet", "minimalapi"].includes(autoFramework)) autoProjectType = "backend";
|
|
259
284
|
if (hasClientRoots && hasServerRoots) autoProjectType = "fullstack";
|
|
260
285
|
if (!hasClientRoots && !hasServerRoots) autoProjectType = "library";
|
|
286
|
+
if (autoFramework === "blazor") autoProjectType = "frontend";
|
|
261
287
|
|
|
262
288
|
return {
|
|
263
289
|
language: overrides.language || autoLanguage,
|
|
@@ -271,6 +297,231 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
|
|
|
271
297
|
};
|
|
272
298
|
}
|
|
273
299
|
|
|
300
|
+
function detectApiCalls(cwd, files) {
|
|
301
|
+
const calls = [];
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
const normalizeEndpointPattern = (value) => {
|
|
304
|
+
let out = String(value || "").trim();
|
|
305
|
+
if (!out) return "";
|
|
306
|
+
out = out.replace(/https?:\/\/[^/]+/gi, "");
|
|
307
|
+
out = out.replace(/\$\{[^}]+\}/g, "{var}");
|
|
308
|
+
out = out.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g, "{var}");
|
|
309
|
+
out = out.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, "{var}");
|
|
310
|
+
out = out.replace(/\/\d+(?=\/|$)/g, "/{id}");
|
|
311
|
+
out = out.replace(/=[^&\s]+/g, "={value}");
|
|
312
|
+
out = out.replace(/\/+/g, "/");
|
|
313
|
+
return out;
|
|
314
|
+
};
|
|
315
|
+
const addCall = (call) => {
|
|
316
|
+
const endpointPattern = normalizeEndpointPattern(call.endpointPattern);
|
|
317
|
+
if (!endpointPattern) return;
|
|
318
|
+
const key = `${call.method}|${endpointPattern}|${call.sourceFile}|${call.style}`;
|
|
319
|
+
if (seen.has(key)) return;
|
|
320
|
+
seen.add(key);
|
|
321
|
+
calls.push({ ...call, endpointPattern });
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (const filePath of files) {
|
|
325
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(filePath)) continue;
|
|
326
|
+
const rel = path.relative(cwd, filePath);
|
|
327
|
+
const text = safeRead(filePath);
|
|
328
|
+
const looksLikeService = /service|api|client|controller|program\.cs/i.test(rel) || /HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(text);
|
|
329
|
+
if (!looksLikeService) continue;
|
|
330
|
+
|
|
331
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
332
|
+
|
|
333
|
+
const constStrings = {};
|
|
334
|
+
const normalizeExpr = (expr) => {
|
|
335
|
+
let out = String(expr || "").trim();
|
|
336
|
+
if (!out) return "";
|
|
337
|
+
out = out.replace(/;+$/, "").trim();
|
|
338
|
+
out = out.replace(/\(\s*$/, ""); // e.g. "this._nextPage("
|
|
339
|
+
// unwrap surrounding quotes/templates
|
|
340
|
+
while ((out.startsWith("'") && out.endsWith("'")) || (out.startsWith('"') && out.endsWith('"')) || (out.startsWith("`") && out.endsWith("`"))) {
|
|
341
|
+
out = out.slice(1, -1).trim();
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
};
|
|
345
|
+
const isLikelyEndpoint = (value) => {
|
|
346
|
+
const v = String(value || "").trim();
|
|
347
|
+
if (!v) return false;
|
|
348
|
+
if (/^https?:\/\//i.test(v)) return true;
|
|
349
|
+
if (v.startsWith("/")) return true;
|
|
350
|
+
if (/\bapi\b/i.test(v)) return true;
|
|
351
|
+
if (/\$\{[^}]+\}/.test(v)) return true;
|
|
352
|
+
if (/\?[^=\s]+=?/.test(v)) return true;
|
|
353
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(v)) return true;
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
356
|
+
const storeConst = (name, raw) => {
|
|
357
|
+
if (!name || !raw) return;
|
|
358
|
+
constStrings[name] = normalizeExpr(raw);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const constPattern = /(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
|
|
362
|
+
for (const m of normalized.matchAll(constPattern)) {
|
|
363
|
+
storeConst(m[1], m[2]);
|
|
364
|
+
}
|
|
365
|
+
const readonlyPattern =
|
|
366
|
+
/(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
|
|
367
|
+
for (const m of normalized.matchAll(readonlyPattern)) {
|
|
368
|
+
storeConst(m[1], m[2]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const resolveExpr = (expr) => {
|
|
372
|
+
const trimmed = normalizeExpr(expr);
|
|
373
|
+
if (!trimmed) return "";
|
|
374
|
+
if (/^['"`][\s\S]*['"`]$/.test(trimmed)) {
|
|
375
|
+
return trimmed.replace(/^['"`]|['"`]$/g, "");
|
|
376
|
+
}
|
|
377
|
+
if (constStrings[trimmed] && isLikelyEndpoint(constStrings[trimmed])) return constStrings[trimmed];
|
|
378
|
+
const thisRef = trimmed.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
379
|
+
if (thisRef && constStrings[thisRef[1]] && isLikelyEndpoint(constStrings[thisRef[1]])) return constStrings[thisRef[1]];
|
|
380
|
+
const parts = trimmed.split("+").map((s) => s.trim()).filter(Boolean);
|
|
381
|
+
if (parts.length > 1) {
|
|
382
|
+
const rebuilt = parts
|
|
383
|
+
.map((p) => {
|
|
384
|
+
if (/^['"`][\s\S]*['"`]$/.test(p)) return p.replace(/^['"`]|['"`]$/g, "");
|
|
385
|
+
if (constStrings[p] && isLikelyEndpoint(constStrings[p])) return constStrings[p];
|
|
386
|
+
const thisP = p.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
387
|
+
if (thisP && constStrings[thisP[1]] && isLikelyEndpoint(constStrings[thisP[1]])) return constStrings[thisP[1]];
|
|
388
|
+
return `{${p}}`;
|
|
389
|
+
})
|
|
390
|
+
.join("");
|
|
391
|
+
if (rebuilt) return rebuilt;
|
|
392
|
+
}
|
|
393
|
+
const ternary = trimmed.match(/^(.+?)\?(.+?):(.+)$/s);
|
|
394
|
+
if (ternary) {
|
|
395
|
+
const left = resolveExpr(ternary[2]);
|
|
396
|
+
const right = resolveExpr(ternary[3]);
|
|
397
|
+
if (left || right) return `${left || "{optionA}"} | ${right || "{optionB}"}`;
|
|
398
|
+
}
|
|
399
|
+
return trimmed;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const httpClientPattern =
|
|
403
|
+
/\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
404
|
+
for (const m of normalized.matchAll(httpClientPattern)) {
|
|
405
|
+
const method = m[1].toUpperCase();
|
|
406
|
+
const raw = resolveExpr(m[2]);
|
|
407
|
+
if (!raw || !isLikelyEndpoint(raw)) continue;
|
|
408
|
+
addCall({
|
|
409
|
+
method,
|
|
410
|
+
endpointPattern: raw,
|
|
411
|
+
style: "httpClient",
|
|
412
|
+
sourceFile: rel,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const fetchPattern = /\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
417
|
+
for (const m of normalized.matchAll(fetchPattern)) {
|
|
418
|
+
const firstArg = resolveExpr(m[1]);
|
|
419
|
+
if (!firstArg || !isLikelyEndpoint(firstArg)) continue;
|
|
420
|
+
const fromIdx = m.index || 0;
|
|
421
|
+
const lookahead = normalized.slice(fromIdx, fromIdx + 260);
|
|
422
|
+
const methodMatch = /method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec(lookahead);
|
|
423
|
+
const method = (methodMatch?.[1] || "GET").toUpperCase();
|
|
424
|
+
addCall({
|
|
425
|
+
method,
|
|
426
|
+
endpointPattern: firstArg,
|
|
427
|
+
style: "fetch",
|
|
428
|
+
sourceFile: rel,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const axiosPattern = /\baxios\.(get|post|put|patch|delete)\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
433
|
+
for (const m of normalized.matchAll(axiosPattern)) {
|
|
434
|
+
const method = m[1].toUpperCase();
|
|
435
|
+
const endpoint = resolveExpr(m[2]);
|
|
436
|
+
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
437
|
+
addCall({
|
|
438
|
+
method,
|
|
439
|
+
endpointPattern: endpoint,
|
|
440
|
+
style: "axios",
|
|
441
|
+
sourceFile: rel,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const axiosObjPattern = /\baxios\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;
|
|
446
|
+
for (const m of normalized.matchAll(axiosObjPattern)) {
|
|
447
|
+
const body = m[1];
|
|
448
|
+
const methodMatch = /\bmethod\s*:\s*["']?(get|post|put|patch|delete)["']?/i.exec(body);
|
|
449
|
+
const urlMatch = /\burl\s*:\s*([^,\n]+)/i.exec(body);
|
|
450
|
+
const method = (methodMatch?.[1] || "get").toUpperCase();
|
|
451
|
+
const endpoint = resolveExpr(urlMatch?.[1] || "");
|
|
452
|
+
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
453
|
+
addCall({
|
|
454
|
+
method,
|
|
455
|
+
endpointPattern: endpoint,
|
|
456
|
+
style: "axios-config",
|
|
457
|
+
sourceFile: rel,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const requestPattern = /\.\s*request\s*\(\s*["'](GET|POST|PUT|PATCH|DELETE)["']\s*,\s*([\s\S]*?)(?:,|\))/gi;
|
|
462
|
+
for (const m of normalized.matchAll(requestPattern)) {
|
|
463
|
+
const method = m[1].toUpperCase();
|
|
464
|
+
const endpoint = resolveExpr(m[2]);
|
|
465
|
+
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
466
|
+
addCall({
|
|
467
|
+
method,
|
|
468
|
+
endpointPattern: endpoint,
|
|
469
|
+
style: "request",
|
|
470
|
+
sourceFile: rel,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (/\.cs$/i.test(filePath)) {
|
|
475
|
+
const mapPattern = /\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;
|
|
476
|
+
for (const m of normalized.matchAll(mapPattern)) {
|
|
477
|
+
addCall({
|
|
478
|
+
method: m[1].toUpperCase(),
|
|
479
|
+
endpointPattern: m[2],
|
|
480
|
+
style: "csharp-map",
|
|
481
|
+
sourceFile: rel,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const classRouteMatch = /\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(normalized);
|
|
486
|
+
const classRoute = classRouteMatch ? classRouteMatch[1] : "";
|
|
487
|
+
const attrPattern = /\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;
|
|
488
|
+
for (const m of normalized.matchAll(attrPattern)) {
|
|
489
|
+
const method = m[1].replace("Http", "").toUpperCase();
|
|
490
|
+
const attrRoute = m[2] || "";
|
|
491
|
+
const endpoint = [classRoute, attrRoute].filter(Boolean).join("/").replace(/\/+/g, "/").replace(/\[controller\]/gi, "{controller}");
|
|
492
|
+
addCall({
|
|
493
|
+
method,
|
|
494
|
+
endpointPattern: endpoint || classRoute || "{controller-route}",
|
|
495
|
+
style: "csharp-controller",
|
|
496
|
+
sourceFile: rel,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const httpClientPattern = /\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;
|
|
501
|
+
for (const m of normalized.matchAll(httpClientPattern)) {
|
|
502
|
+
const method = m[1].replace("Async", "").replace("Send", "SEND").toUpperCase();
|
|
503
|
+
addCall({
|
|
504
|
+
method,
|
|
505
|
+
endpointPattern: m[2],
|
|
506
|
+
style: "csharp-httpclient",
|
|
507
|
+
sourceFile: rel,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const byMethod = calls.reduce((acc, c) => {
|
|
514
|
+
acc[c.method] = (acc[c.method] || 0) + 1;
|
|
515
|
+
return acc;
|
|
516
|
+
}, {});
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
totalCalls: calls.length,
|
|
520
|
+
byMethod,
|
|
521
|
+
calls: calls.slice(0, 80),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
274
525
|
export function discoverProjectSignals(cwd, profileOverrides = {}) {
|
|
275
526
|
const files = collectCodeFiles(cwd);
|
|
276
527
|
const inferred = new Map();
|
|
@@ -324,6 +575,7 @@ export function discoverProjectSignals(cwd, profileOverrides = {}) {
|
|
|
324
575
|
uiLayout: detectUiLayout(files),
|
|
325
576
|
styling: detectStyling(cwd, files, externalLibraries),
|
|
326
577
|
developmentProfile: detectDevelopmentProfile(cwd, files, externalLibraries, profileOverrides),
|
|
578
|
+
apiCalls: detectApiCalls(cwd, files),
|
|
327
579
|
};
|
|
328
580
|
}
|
|
329
581
|
|
|
@@ -425,6 +677,14 @@ export function buildSignalsReport(signals) {
|
|
|
425
677
|
` - language : ${signals.developmentProfile?.language || "unknown"} (auto: ${signals.developmentProfile?.detected?.language || "unknown"})`,
|
|
426
678
|
` - framework : ${signals.developmentProfile?.framework || "unknown"} (auto: ${signals.developmentProfile?.detected?.framework || "unknown"})`,
|
|
427
679
|
` - project type: ${signals.developmentProfile?.projectType || "unknown"} (auto: ${signals.developmentProfile?.detected?.projectType || "unknown"})`,
|
|
680
|
+
"API calls",
|
|
681
|
+
"-".repeat(56),
|
|
682
|
+
` - total calls : ${signals.apiCalls?.totalCalls ?? 0}`,
|
|
683
|
+
` - by method : ${Object.entries(signals.apiCalls?.byMethod || {}).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}`,
|
|
684
|
+
...(signals.apiCalls?.calls || []).slice(0, 6).map((c) => ` - ${c.method} ${c.endpointPattern} [${c.style}] (${c.sourceFile})`),
|
|
685
|
+
...((signals.apiCalls?.calls || []).length > 6
|
|
686
|
+
? [` - ... +${(signals.apiCalls?.calls || []).length - 6} more`]
|
|
687
|
+
: []),
|
|
428
688
|
"=".repeat(56),
|
|
429
689
|
].join("\n");
|
|
430
690
|
}
|
|
@@ -488,6 +748,7 @@ export function writeAdoptionBaseline(infernoDir, policyId, capabilities, signal
|
|
|
488
748
|
projectType: "unknown",
|
|
489
749
|
detected: { language: "unknown", framework: "unknown", projectType: "unknown" },
|
|
490
750
|
},
|
|
751
|
+
apiCalls: signals.apiCalls || { totalCalls: 0, byMethod: {}, calls: [] },
|
|
491
752
|
};
|
|
492
753
|
fs.writeFileSync(path.join(infernoDir, "adoption_profile.json"), JSON.stringify(profile, null, 2) + "\n");
|
|
493
754
|
}
|
package/lib/commands/init.mjs
CHANGED
|
@@ -65,7 +65,10 @@ function upsertScripts(cwd, silent = false) {
|
|
|
65
65
|
const toAdd = {
|
|
66
66
|
"inferno:check": "infernoflow check",
|
|
67
67
|
"inferno:status": "infernoflow status",
|
|
68
|
-
"inferno:gate": "infernoflow doc-gate"
|
|
68
|
+
"inferno:gate": "infernoflow doc-gate",
|
|
69
|
+
"inferno:impact": "infernoflow pr-impact --json",
|
|
70
|
+
"inferno:sync": "infernoflow sync --auto --json",
|
|
71
|
+
"inferno:hooks": "node scripts/inferno-install-hooks.mjs"
|
|
69
72
|
};
|
|
70
73
|
for (const [k, v] of Object.entries(toAdd)) {
|
|
71
74
|
if (!pkg.scripts[k]) { pkg.scripts[k] = v; changed = true; }
|
|
@@ -167,6 +170,7 @@ export async function initCommand(args) {
|
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
const infernoDir = path.join(cwd, "inferno");
|
|
173
|
+
const workflowsDir = path.join(cwd, ".github", "workflows");
|
|
170
174
|
if (fs.existsSync(infernoDir) && !force) {
|
|
171
175
|
if (silent) {
|
|
172
176
|
console.log(JSON.stringify({ ok: false, error: "inferno_exists", hint: "Use --force to overwrite" }, null, 2));
|
|
@@ -216,6 +220,7 @@ export async function initCommand(args) {
|
|
|
216
220
|
uiLayout: signals.uiLayout,
|
|
217
221
|
styling: signals.styling,
|
|
218
222
|
developmentProfile: signals.developmentProfile,
|
|
223
|
+
apiCalls: signals.apiCalls,
|
|
219
224
|
},
|
|
220
225
|
null,
|
|
221
226
|
2
|
|
@@ -240,6 +245,7 @@ export async function initCommand(args) {
|
|
|
240
245
|
uiLayout: signals.uiLayout,
|
|
241
246
|
styling: signals.styling,
|
|
242
247
|
developmentProfile: signals.developmentProfile,
|
|
248
|
+
apiCalls: signals.apiCalls,
|
|
243
249
|
},
|
|
244
250
|
null,
|
|
245
251
|
2
|
|
@@ -301,6 +307,12 @@ export async function initCommand(args) {
|
|
|
301
307
|
const srcScript = path.join(templates, "scripts", "inferno-doc-gate.mjs");
|
|
302
308
|
const dstScript = path.join(cwd, "scripts", "inferno-doc-gate.mjs");
|
|
303
309
|
copyFile(srcScript, dstScript, force, silent);
|
|
310
|
+
const srcHookScript = path.join(templates, "scripts", "inferno-install-hooks.mjs");
|
|
311
|
+
const dstHookScript = path.join(cwd, "scripts", "inferno-install-hooks.mjs");
|
|
312
|
+
copyFile(srcHookScript, dstHookScript, force, silent);
|
|
313
|
+
const srcWorkflow = path.join(templates, "ci", "github-inferno-check.yml");
|
|
314
|
+
const dstWorkflow = path.join(workflowsDir, "infernoflow-check.yml");
|
|
315
|
+
copyFile(srcWorkflow, dstWorkflow, force, silent);
|
|
304
316
|
|
|
305
317
|
upsertScripts(cwd, silent);
|
|
306
318
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { header, section, ok, warn, fail, gray, cyan, yellow } from "../ui/output.mjs";
|
|
5
|
+
|
|
6
|
+
const CODE_PREFIXES = ["src/", "frontend/", "backend/", "app/", "pages/", "components/", "lib/", "api/", "server/", "Controllers/"];
|
|
7
|
+
|
|
8
|
+
function sh(cmd) {
|
|
9
|
+
return execSync(cmd, { stdio: ["ignore", "pipe", "pipe"] }).toString("utf8").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readJson(filePath, fallback = null) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readFile(filePath, fallback = "") {
|
|
21
|
+
try {
|
|
22
|
+
return fs.readFileSync(filePath, "utf8");
|
|
23
|
+
} catch {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getChangedFiles(base, head) {
|
|
29
|
+
const out = base && head
|
|
30
|
+
? sh(`git diff --name-only ${base}..${head}`)
|
|
31
|
+
: sh("git diff --name-only HEAD");
|
|
32
|
+
return out ? out.split("\n").map((s) => s.trim()).filter(Boolean) : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildCapabilityHints(cwd) {
|
|
36
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
37
|
+
const contract = readJson(path.join(infernoDir, "contract.json"), { capabilities: [] });
|
|
38
|
+
const registry = readJson(path.join(infernoDir, "capabilities.json"), { capabilities: [] });
|
|
39
|
+
const titleById = new Map((registry.capabilities || []).map((c) => [c.id, c.title || c.id]));
|
|
40
|
+
return (contract.capabilities || []).map((id) => {
|
|
41
|
+
const title = titleById.get(id) || id;
|
|
42
|
+
const keywords = new Set(
|
|
43
|
+
`${id} ${title}`
|
|
44
|
+
.replace(/([A-Z])/g, " $1")
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.split(/[^a-z0-9]+/)
|
|
47
|
+
.filter((k) => k.length >= 4)
|
|
48
|
+
);
|
|
49
|
+
return { id, title, keywords: Array.from(keywords) };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inferImpactedCapabilities(cwd, changedCodeFiles) {
|
|
54
|
+
const hints = buildCapabilityHints(cwd);
|
|
55
|
+
const impacted = [];
|
|
56
|
+
for (const hint of hints) {
|
|
57
|
+
const matched = [];
|
|
58
|
+
for (const rel of changedCodeFiles) {
|
|
59
|
+
const abs = path.join(cwd, rel);
|
|
60
|
+
const text = readFile(abs, "").toLowerCase();
|
|
61
|
+
if (!text) continue;
|
|
62
|
+
if (hint.keywords.some((k) => text.includes(k))) {
|
|
63
|
+
matched.push(rel);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (matched.length) {
|
|
67
|
+
impacted.push({ id: hint.id, title: hint.title, matchedFiles: matched.slice(0, 5) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return impacted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function prImpactCommand(args = []) {
|
|
74
|
+
const asJson = args.includes("--json");
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const base = process.env.BASE_SHA || null;
|
|
77
|
+
const head = process.env.HEAD_SHA || null;
|
|
78
|
+
|
|
79
|
+
let changedFiles = [];
|
|
80
|
+
try {
|
|
81
|
+
changedFiles = getChangedFiles(base, head);
|
|
82
|
+
} catch {
|
|
83
|
+
const payload = { ok: true, skipped: true, reason: "no_git_available" };
|
|
84
|
+
if (asJson) {
|
|
85
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
header("pr-impact");
|
|
89
|
+
warn("git not available; cannot compute PR impact");
|
|
90
|
+
console.log();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const changedCodeFiles = changedFiles.filter((f) => CODE_PREFIXES.some((p) => f.startsWith(p)));
|
|
95
|
+
const changedInfernoFiles = changedFiles.filter((f) => f.startsWith("inferno/"));
|
|
96
|
+
const impactedCapabilities = inferImpactedCapabilities(cwd, changedCodeFiles);
|
|
97
|
+
const inferredBehaviorChange = changedCodeFiles.length > 0;
|
|
98
|
+
const missingInfernoUpdate = inferredBehaviorChange && changedInfernoFiles.length === 0;
|
|
99
|
+
const confidence = impactedCapabilities.length > 0 ? "high" : inferredBehaviorChange ? "medium" : "low";
|
|
100
|
+
const reasonCodes = [];
|
|
101
|
+
if (inferredBehaviorChange) reasonCodes.push("CODE_CHANGED");
|
|
102
|
+
if (missingInfernoUpdate) reasonCodes.push("INFERNO_NOT_UPDATED");
|
|
103
|
+
if (impactedCapabilities.length > 0) reasonCodes.push("CAPABILITY_HINT_MATCH");
|
|
104
|
+
if (!reasonCodes.length) reasonCodes.push("NO_BEHAVIOR_SIGNAL");
|
|
105
|
+
|
|
106
|
+
const payload = {
|
|
107
|
+
ok: !missingInfernoUpdate,
|
|
108
|
+
base: base || "HEAD",
|
|
109
|
+
head: head || "WORKTREE",
|
|
110
|
+
changedFiles,
|
|
111
|
+
changedCodeFiles,
|
|
112
|
+
changedInfernoFiles,
|
|
113
|
+
inferredBehaviorChange,
|
|
114
|
+
impactedCapabilities,
|
|
115
|
+
confidence,
|
|
116
|
+
reasonCodes,
|
|
117
|
+
recommendations: missingInfernoUpdate
|
|
118
|
+
? ["Run infernoflow suggest \"describe behavior change\" and update inferno/", "Run infernoflow check --json"]
|
|
119
|
+
: ["Run infernoflow check --json to validate final state"],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (asJson) {
|
|
123
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
124
|
+
process.exit(payload.ok ? 0 : 1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
header("pr-impact");
|
|
128
|
+
|
|
129
|
+
section("Diff Scope");
|
|
130
|
+
ok(`Changed files: ${cyan(String(changedFiles.length))}`);
|
|
131
|
+
ok(`Code files: ${cyan(String(changedCodeFiles.length))}`);
|
|
132
|
+
ok(`Inferno files: ${cyan(String(changedInfernoFiles.length))}`);
|
|
133
|
+
|
|
134
|
+
section("Capability Impact");
|
|
135
|
+
if (impactedCapabilities.length === 0) {
|
|
136
|
+
warn("No capability hints matched changed code files");
|
|
137
|
+
} else {
|
|
138
|
+
impactedCapabilities.forEach((c) => {
|
|
139
|
+
console.log(` ${cyan("•")} ${c.id} ${gray(`(${c.title})`)}`);
|
|
140
|
+
c.matchedFiles.slice(0, 3).forEach((f) => console.log(` ${gray("- " + f)}`));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
section("Doc Sync");
|
|
145
|
+
if (missingInfernoUpdate) {
|
|
146
|
+
fail("Code changed but inferno/ was not updated", "Run infernoflow suggest and then infernoflow check");
|
|
147
|
+
} else {
|
|
148
|
+
ok("No immediate inferno drift signal from changed files");
|
|
149
|
+
}
|
|
150
|
+
ok(`Confidence: ${cyan(confidence)}`);
|
|
151
|
+
|
|
152
|
+
section("Suggested Next");
|
|
153
|
+
payload.recommendations.forEach((r) => console.log(` ${yellow("→")} ${r}`));
|
|
154
|
+
console.log();
|
|
155
|
+
process.exit(payload.ok ? 0 : 1);
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { header, section, ok, warn, yellow, gray } from "../ui/output.mjs";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
|
|
9
|
+
|
|
10
|
+
function runCliJson(args) {
|
|
11
|
+
const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
12
|
+
return JSON.parse(out);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tryRunCliJson(args) {
|
|
16
|
+
try {
|
|
17
|
+
return { ok: true, data: runCliJson(args) };
|
|
18
|
+
} catch (err) {
|
|
19
|
+
const stdout = err?.stdout?.toString?.() || "";
|
|
20
|
+
try {
|
|
21
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
22
|
+
} catch {
|
|
23
|
+
return { ok: false, data: { ok: false, errors: ["command_failed"] } };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function syncCommand(args = []) {
|
|
29
|
+
const auto = args.includes("--auto");
|
|
30
|
+
const asJson = args.includes("--json");
|
|
31
|
+
const dryRun = args.includes("--dry-run");
|
|
32
|
+
|
|
33
|
+
if (!auto) {
|
|
34
|
+
const payload = { ok: false, error: "missing_required_flag", hint: "Use: infernoflow sync --auto" };
|
|
35
|
+
if (asJson) {
|
|
36
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
header("sync");
|
|
40
|
+
warn("missing --auto flag");
|
|
41
|
+
console.log(` ${yellow("→")} infernoflow sync --auto`);
|
|
42
|
+
console.log();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const impact = tryRunCliJson(["pr-impact", "--json"]);
|
|
47
|
+
const needsSync = !impact.data?.ok;
|
|
48
|
+
const confidence = impact.data?.confidence || "low";
|
|
49
|
+
const policyDecision = confidence === "high" ? "auto" : confidence === "medium" ? "ask" : "block";
|
|
50
|
+
const actions = needsSync
|
|
51
|
+
? ["Generate inferno update proposal (suggest)", "Review changes", "Validate with check --json"]
|
|
52
|
+
: ["No inferno drift detected", "Validate with check --json"];
|
|
53
|
+
|
|
54
|
+
const check = tryRunCliJson(["check", "--json"]);
|
|
55
|
+
const payload = {
|
|
56
|
+
ok: impact.ok && check.ok && !!check.data?.ok,
|
|
57
|
+
mode: "auto-skeleton",
|
|
58
|
+
dryRun,
|
|
59
|
+
needsSync,
|
|
60
|
+
didApply: false,
|
|
61
|
+
confidence,
|
|
62
|
+
policyDecision,
|
|
63
|
+
actions,
|
|
64
|
+
prImpact: impact.data,
|
|
65
|
+
postCheck: check.data,
|
|
66
|
+
reasonCodes: [
|
|
67
|
+
...(needsSync ? ["DRIFT_DETECTED"] : ["NO_DRIFT"]),
|
|
68
|
+
`POLICY_${policyDecision.toUpperCase()}`,
|
|
69
|
+
...(policyDecision === "auto" ? ["AUTO_APPLY_DISABLED_IN_SKELETON"] : []),
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (asJson) {
|
|
74
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
75
|
+
process.exit(payload.ok ? 0 : 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
header("sync --auto");
|
|
79
|
+
section("State");
|
|
80
|
+
if (needsSync) warn("Inferno drift detected");
|
|
81
|
+
else ok("No inferno drift detected");
|
|
82
|
+
ok(`Confidence: ${gray(confidence)}`);
|
|
83
|
+
ok(`Policy decision: ${gray(policyDecision)}`);
|
|
84
|
+
ok(`Apply mode: ${gray("skeleton (no file writes)")}`);
|
|
85
|
+
if (dryRun) ok("Dry run enabled");
|
|
86
|
+
|
|
87
|
+
section("Plan");
|
|
88
|
+
actions.forEach((a) => console.log(` ${yellow("→")} ${a}`));
|
|
89
|
+
|
|
90
|
+
section("Validation");
|
|
91
|
+
if (check.ok && check.data?.ok) ok("Post-check passed");
|
|
92
|
+
else warn("Post-check failed; see infernoflow check --json");
|
|
93
|
+
console.log();
|
|
94
|
+
process.exit(payload.ok ? 0 : 1);
|
|
95
|
+
}
|
|
96
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs",
|
|
19
|
+
"test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs",
|
|
20
20
|
"test:help": "node bin/infernoflow.mjs --help"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: infernoflow-check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main, master]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
inferno:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
|
|
17
|
+
- name: Setup Node
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: "20"
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci --ignore-scripts || npm install --ignore-scripts
|
|
24
|
+
|
|
25
|
+
- name: Inferno PR impact
|
|
26
|
+
run: npx infernoflow pr-impact --json
|
|
27
|
+
env:
|
|
28
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
29
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
30
|
+
|
|
31
|
+
- name: Inferno check
|
|
32
|
+
run: npx infernoflow check --json
|
|
33
|
+
env:
|
|
34
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
35
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
36
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const gitDir = path.join(cwd, ".git");
|
|
7
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(gitDir)) {
|
|
10
|
+
console.error("[inferno hooks] .git not found. Run inside a git repository.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const preCommit = `#!/bin/sh
|
|
17
|
+
echo "[inferno hooks] pre-commit: infernoflow check --skip-doc-gate"
|
|
18
|
+
npx infernoflow check --skip-doc-gate
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const prePush = `#!/bin/sh
|
|
22
|
+
echo "[inferno hooks] pre-push: infernoflow doc-gate"
|
|
23
|
+
npx infernoflow doc-gate
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const writeHook = (name, content) => {
|
|
27
|
+
const filePath = path.join(hooksDir, name);
|
|
28
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
29
|
+
fs.chmodSync(filePath, 0o755);
|
|
30
|
+
console.log(`[inferno hooks] installed ${name}`);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
writeHook("pre-commit", preCommit);
|
|
34
|
+
writeHook("pre-push", prePush);
|
|
35
|
+
console.log("[inferno hooks] done");
|
|
36
|
+
|