infernoflow 0.10.6 → 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 +21 -2
- package/bin/infernoflow.mjs +6 -0
- package/lib/commands/adopt.mjs +61 -5
- package/lib/commands/init.mjs +11 -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
|
@@ -166,6 +166,8 @@ infernoflow doc-gate --json
|
|
|
166
166
|
| `infernoflow status` | At-a-glance health of your contract |
|
|
167
167
|
| `infernoflow suggest` | Generate an AI prompt, apply capability updates |
|
|
168
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) |
|
|
169
171
|
| `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
|
|
170
172
|
| `infernoflow doc-gate` | Fails if code changed but docs weren't updated |
|
|
171
173
|
| `infernoflow context` | Build/persist AI session context for this project |
|
|
@@ -183,6 +185,11 @@ infernoflow implement "..." --mode both
|
|
|
183
185
|
infernoflow implement "..." --mode cursor
|
|
184
186
|
infernoflow implement "..." --mode generic
|
|
185
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)
|
|
186
193
|
infernoflow check --json # machine-readable output for CI
|
|
187
194
|
infernoflow check --skip-doc-gate
|
|
188
195
|
infernoflow status --json # machine-readable status summary
|
|
@@ -272,13 +279,25 @@ Recommended chain:
|
|
|
272
279
|
|
|
273
280
|
```yaml
|
|
274
281
|
# .github/workflows/ci.yml
|
|
275
|
-
- name: infernoflow check
|
|
276
|
-
run:
|
|
282
|
+
- name: infernoflow impact + check
|
|
283
|
+
run: |
|
|
284
|
+
npx infernoflow pr-impact --json
|
|
285
|
+
npx infernoflow check --json
|
|
277
286
|
env:
|
|
278
287
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
279
288
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
280
289
|
```
|
|
281
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
|
+
|
|
282
301
|
## Release Checklist
|
|
283
302
|
|
|
284
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
|
@@ -300,11 +300,25 @@ function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {})
|
|
|
300
300
|
function detectApiCalls(cwd, files) {
|
|
301
301
|
const calls = [];
|
|
302
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
|
+
};
|
|
303
315
|
const addCall = (call) => {
|
|
304
|
-
const
|
|
316
|
+
const endpointPattern = normalizeEndpointPattern(call.endpointPattern);
|
|
317
|
+
if (!endpointPattern) return;
|
|
318
|
+
const key = `${call.method}|${endpointPattern}|${call.sourceFile}|${call.style}`;
|
|
305
319
|
if (seen.has(key)) return;
|
|
306
320
|
seen.add(key);
|
|
307
|
-
calls.push(call);
|
|
321
|
+
calls.push({ ...call, endpointPattern });
|
|
308
322
|
};
|
|
309
323
|
|
|
310
324
|
for (const filePath of files) {
|
|
@@ -393,7 +407,7 @@ function detectApiCalls(cwd, files) {
|
|
|
393
407
|
if (!raw || !isLikelyEndpoint(raw)) continue;
|
|
394
408
|
addCall({
|
|
395
409
|
method,
|
|
396
|
-
endpointPattern:
|
|
410
|
+
endpointPattern: raw,
|
|
397
411
|
style: "httpClient",
|
|
398
412
|
sourceFile: rel,
|
|
399
413
|
});
|
|
@@ -409,12 +423,54 @@ function detectApiCalls(cwd, files) {
|
|
|
409
423
|
const method = (methodMatch?.[1] || "GET").toUpperCase();
|
|
410
424
|
addCall({
|
|
411
425
|
method,
|
|
412
|
-
endpointPattern:
|
|
426
|
+
endpointPattern: firstArg,
|
|
413
427
|
style: "fetch",
|
|
414
428
|
sourceFile: rel,
|
|
415
429
|
});
|
|
416
430
|
}
|
|
417
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
|
+
|
|
418
474
|
if (/\.cs$/i.test(filePath)) {
|
|
419
475
|
const mapPattern = /\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;
|
|
420
476
|
for (const m of normalized.matchAll(mapPattern)) {
|
|
@@ -432,7 +488,7 @@ function detectApiCalls(cwd, files) {
|
|
|
432
488
|
for (const m of normalized.matchAll(attrPattern)) {
|
|
433
489
|
const method = m[1].replace("Http", "").toUpperCase();
|
|
434
490
|
const attrRoute = m[2] || "";
|
|
435
|
-
const endpoint = [classRoute, attrRoute].filter(Boolean).join("/").replace(/\/+/g, "/");
|
|
491
|
+
const endpoint = [classRoute, attrRoute].filter(Boolean).join("/").replace(/\/+/g, "/").replace(/\[controller\]/gi, "{controller}");
|
|
436
492
|
addCall({
|
|
437
493
|
method,
|
|
438
494
|
endpointPattern: endpoint || classRoute || "{controller-route}",
|
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));
|
|
@@ -303,6 +307,12 @@ export async function initCommand(args) {
|
|
|
303
307
|
const srcScript = path.join(templates, "scripts", "inferno-doc-gate.mjs");
|
|
304
308
|
const dstScript = path.join(cwd, "scripts", "inferno-doc-gate.mjs");
|
|
305
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);
|
|
306
316
|
|
|
307
317
|
upsertScripts(cwd, silent);
|
|
308
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
|
+
|