infernoflow 0.10.6 → 0.10.8
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 +63 -2
- package/bin/infernoflow.mjs +14 -0
- package/lib/ai/localProvider.mjs +88 -0
- package/lib/commands/adopt.mjs +768 -712
- package/lib/commands/implement.mjs +103 -103
- package/lib/commands/init.mjs +12 -1
- package/lib/commands/prImpact.mjs +157 -0
- package/lib/commands/run.mjs +227 -0
- package/lib/commands/suggest.mjs +42 -12
- package/lib/commands/syncAuto.mjs +96 -0
- package/package.json +2 -2
- package/templates/ci/github-inferno-check.yml +33 -0
- package/templates/scripts/inferno-install-hooks.mjs +36 -0
package/lib/commands/suggest.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { header, ok, fail, warn, info, done, section, nextSteps, bold, cyan, gra
|
|
|
5
5
|
|
|
6
6
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
|
-
function readJson(filePath) {
|
|
8
|
+
export function readJson(filePath) {
|
|
9
9
|
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
10
10
|
catch { return null; }
|
|
11
11
|
}
|
|
@@ -25,7 +25,7 @@ function toCapabilityId(str) {
|
|
|
25
25
|
.join("");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function buildPrompt({ description, contract, capabilities, scenarios }) {
|
|
28
|
+
export function buildPrompt({ description, contract, capabilities, scenarios }) {
|
|
29
29
|
const capsIds = contract.capabilities || [];
|
|
30
30
|
const capsDetail = (capabilities?.capabilities || [])
|
|
31
31
|
.map(c => ` - ${c.id}: ${c.title || c.id}`)
|
|
@@ -87,7 +87,7 @@ Rules:
|
|
|
87
87
|
- Keep it minimal and accurate`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function validateSuggestion(suggestion) {
|
|
90
|
+
export function validateSuggestion(suggestion) {
|
|
91
91
|
const errors = [];
|
|
92
92
|
if (!suggestion || typeof suggestion !== "object") {
|
|
93
93
|
return ["AI response must be a JSON object."];
|
|
@@ -146,7 +146,7 @@ function validateSuggestion(suggestion) {
|
|
|
146
146
|
return errors;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function detectSuggestionConflicts(contract, suggestion) {
|
|
149
|
+
export function detectSuggestionConflicts(contract, suggestion) {
|
|
150
150
|
const issues = [];
|
|
151
151
|
const existing = new Set(contract.capabilities || []);
|
|
152
152
|
const newIds = new Set((suggestion.newCapabilities || []).map((c) => c.id));
|
|
@@ -170,7 +170,7 @@ function detectSuggestionConflicts(contract, suggestion) {
|
|
|
170
170
|
return issues;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
173
|
+
export function applyChanges({ cwd, contract, capabilities, suggestion, version, quiet = false }) {
|
|
174
174
|
const infernoDir = path.join(cwd, "inferno");
|
|
175
175
|
const contractPath = path.join(infernoDir, "contract.json");
|
|
176
176
|
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
@@ -195,7 +195,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
195
195
|
const nextVersion = Number(contract.policyVersion || 1) + 1;
|
|
196
196
|
const contractUpdated = { ...contract, capabilities: updatedCaps, policyVersion: nextVersion };
|
|
197
197
|
queueWrite(contractPath, JSON.stringify(contractUpdated, null, 2) + "\n");
|
|
198
|
-
ok(`contract.json updated → policyVersion: v${nextVersion}`);
|
|
198
|
+
if (!quiet) ok(`contract.json updated → policyVersion: v${nextVersion}`);
|
|
199
199
|
changed = true;
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -209,7 +209,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
queueWrite(capsPath, JSON.stringify(reg, null, 2) + "\n");
|
|
212
|
-
ok(`capabilities.json updated`);
|
|
212
|
+
if (!quiet) ok(`capabilities.json updated`);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// ── scenarios ─────────────────────────────────────────────────────────────
|
|
@@ -225,7 +225,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
225
225
|
steps: us.stepsToAdd || []
|
|
226
226
|
};
|
|
227
227
|
queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
|
|
228
|
-
ok(`Created scenario: ${cyan(us.file)}`);
|
|
228
|
+
if (!quiet) ok(`Created scenario: ${cyan(us.file)}`);
|
|
229
229
|
} else {
|
|
230
230
|
scenario = readJson(filePath);
|
|
231
231
|
const existingCaps = new Set(scenario.capabilitiesCovered || []);
|
|
@@ -233,7 +233,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
233
233
|
scenario.capabilitiesCovered = [...existingCaps];
|
|
234
234
|
scenario.steps = [...(scenario.steps || []), ...(us.stepsToAdd || [])];
|
|
235
235
|
queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
|
|
236
|
-
ok(`Updated scenario: ${cyan(us.file)}`);
|
|
236
|
+
if (!quiet) ok(`Updated scenario: ${cyan(us.file)}`);
|
|
237
237
|
}
|
|
238
238
|
changed = true;
|
|
239
239
|
}
|
|
@@ -244,7 +244,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
244
244
|
if (/##\s+Unreleased/i.test(txt)) {
|
|
245
245
|
txt = txt.replace(/(##\s+Unreleased[^\n]*\n)/i, `$1\n${changelogEntry}\n`);
|
|
246
246
|
queueWrite(changelogPath, txt);
|
|
247
|
-
ok(`CHANGELOG.md updated`);
|
|
247
|
+
if (!quiet) ok(`CHANGELOG.md updated`);
|
|
248
248
|
changed = true;
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -275,6 +275,37 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
|
|
|
275
275
|
return changed;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
export function parseSuggestionJson(rawInput) {
|
|
279
|
+
const clean = String(rawInput || "").trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
|
|
280
|
+
return JSON.parse(clean);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function loadSuggestContext(cwd) {
|
|
284
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
285
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
286
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
287
|
+
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
288
|
+
|
|
289
|
+
const contract = readJson(contractPath);
|
|
290
|
+
const capabilities = readJson(capsPath);
|
|
291
|
+
const scenarios = [];
|
|
292
|
+
if (fs.existsSync(scenariosDir)) {
|
|
293
|
+
for (const f of fs.readdirSync(scenariosDir).filter((name) => name.endsWith(".json"))) {
|
|
294
|
+
const s = readJson(path.join(scenariosDir, f));
|
|
295
|
+
if (s) scenarios.push({ ...s, _file: f });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let version = "0.1.0";
|
|
300
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
301
|
+
if (fs.existsSync(pkgPath)) {
|
|
302
|
+
const pkg = readJson(pkgPath);
|
|
303
|
+
if (pkg?.version) version = pkg.version;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { contract, capabilities, scenarios, version };
|
|
307
|
+
}
|
|
308
|
+
|
|
278
309
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
279
310
|
|
|
280
311
|
export async function suggestCommand(args) {
|
|
@@ -375,8 +406,7 @@ export async function suggestCommand(args) {
|
|
|
375
406
|
// ── Parse response ────────────────────────────────────────────────────────
|
|
376
407
|
let suggestion;
|
|
377
408
|
try {
|
|
378
|
-
|
|
379
|
-
suggestion = JSON.parse(clean);
|
|
409
|
+
suggestion = parseSuggestionJson(jsonInput);
|
|
380
410
|
} catch {
|
|
381
411
|
errorAndExit(
|
|
382
412
|
"Could not parse the AI response as JSON",
|
|
@@ -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.8",
|
|
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 && node scripts/run-smoke.mjs",
|
|
20
20
|
"test:help": "node bin/infernoflow.mjs --help"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
@@ -0,0 +1,33 @@
|
|
|
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 one-command run
|
|
26
|
+
run: npx infernoflow run "sync check" --json
|
|
27
|
+
env:
|
|
28
|
+
INFERNO_LOCAL_PROVIDER: ollama
|
|
29
|
+
INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
|
|
30
|
+
INFERNO_LOCAL_MODEL: llama3.1:8b
|
|
31
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
32
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
33
|
+
|
|
@@ -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 run --dry-run"
|
|
18
|
+
npx infernoflow run "sync check" --dry-run
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const prePush = `#!/bin/sh
|
|
22
|
+
echo "[inferno hooks] pre-push: infernoflow run --json"
|
|
23
|
+
npx infernoflow run "sync check" --json
|
|
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
|
+
|