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 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: npx infernoflow check --json
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
@@ -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;
@@ -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 key = `${call.method}|${call.endpointPattern}|${call.sourceFile}|${call.style}`;
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: normalizeExpr(raw),
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: normalizeExpr(firstArg),
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}",
@@ -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.6",
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
+