infernoflow 0.10.7 → 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 +46 -4
- package/bin/infernoflow.mjs +8 -0
- package/lib/ai/localProvider.mjs +88 -0
- package/lib/commands/adopt.mjs +768 -768
- package/lib/commands/implement.mjs +103 -103
- package/lib/commands/init.mjs +1 -0
- package/lib/commands/prImpact.mjs +157 -157
- package/lib/commands/run.mjs +227 -0
- package/lib/commands/suggest.mjs +42 -12
- package/lib/commands/syncAuto.mjs +96 -96
- package/package.json +2 -2
- package/templates/ci/github-inferno-check.yml +33 -36
- package/templates/scripts/inferno-install-hooks.mjs +36 -36
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { generateWithLocalModel } from "../ai/localProvider.mjs";
|
|
6
|
+
import {
|
|
7
|
+
buildPrompt,
|
|
8
|
+
loadSuggestContext,
|
|
9
|
+
parseSuggestionJson,
|
|
10
|
+
validateSuggestion,
|
|
11
|
+
detectSuggestionConflicts,
|
|
12
|
+
applyChanges,
|
|
13
|
+
} from "./suggest.mjs";
|
|
14
|
+
import { header, section, ok, warn, fail, info, gray } from "../ui/output.mjs";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
|
|
19
|
+
|
|
20
|
+
function runCliJson(args) {
|
|
21
|
+
try {
|
|
22
|
+
const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
23
|
+
return { ok: true, data: JSON.parse(out) };
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const stdout = err?.stdout?.toString?.() || "";
|
|
26
|
+
try {
|
|
27
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
28
|
+
} catch {
|
|
29
|
+
return { ok: false, data: { ok: false, errors: ["command_failed"] } };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stageEvent(asJson, events, stage, status, details = {}) {
|
|
35
|
+
const ev = { ts: new Date().toISOString(), stage, status, ...details };
|
|
36
|
+
events.push(ev);
|
|
37
|
+
if (asJson) return;
|
|
38
|
+
const text = `${stage}: ${status}`;
|
|
39
|
+
if (status === "ok") ok(text);
|
|
40
|
+
else if (status === "warn") warn(text);
|
|
41
|
+
else if (status === "fail") fail(text);
|
|
42
|
+
else info(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snapshotInferno(cwd) {
|
|
46
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
47
|
+
const targets = [];
|
|
48
|
+
const walk = (dir) => {
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const p = path.join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) walk(p);
|
|
52
|
+
else targets.push(p);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
if (fs.existsSync(infernoDir)) walk(infernoDir);
|
|
56
|
+
const snapshot = new Map();
|
|
57
|
+
targets.forEach((filePath) => snapshot.set(filePath, fs.readFileSync(filePath, "utf8")));
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function restoreSnapshot(cwd, snapshot) {
|
|
62
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
63
|
+
if (fs.existsSync(infernoDir)) {
|
|
64
|
+
const existing = [];
|
|
65
|
+
const walk = (dir) => {
|
|
66
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
67
|
+
const p = path.join(dir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) walk(p);
|
|
69
|
+
else existing.push(p);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(infernoDir);
|
|
73
|
+
existing.forEach((filePath) => {
|
|
74
|
+
if (!snapshot.has(filePath)) fs.unlinkSync(filePath);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const [filePath, content] of snapshot.entries()) {
|
|
78
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeRunArtifact(cwd, artifact) {
|
|
84
|
+
const runsDir = path.join(cwd, "inferno", "runs");
|
|
85
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
86
|
+
const filePath = path.join(runsDir, `${Date.now()}.json`);
|
|
87
|
+
fs.writeFileSync(filePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
88
|
+
return path.relative(cwd, filePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runCommand(args = []) {
|
|
92
|
+
const asJson = args.includes("--json");
|
|
93
|
+
const dryRun = args.includes("--dry-run");
|
|
94
|
+
const noRollback = args.includes("--no-rollback");
|
|
95
|
+
const task = args.filter((a) => !a.startsWith("-")).slice(1).join(" ").trim() || "sync check";
|
|
96
|
+
const cwd = process.cwd();
|
|
97
|
+
const events = [];
|
|
98
|
+
|
|
99
|
+
if (!asJson) header("run");
|
|
100
|
+
stageEvent(asJson, events, "init", "info", { task, dryRun, noRollback });
|
|
101
|
+
|
|
102
|
+
// detect
|
|
103
|
+
const impact = runCliJson(["pr-impact", "--json"]);
|
|
104
|
+
stageEvent(asJson, events, "detect", impact.data?.ok ? "ok" : "warn", { confidence: impact.data?.confidence || "low" });
|
|
105
|
+
|
|
106
|
+
const ctx = loadSuggestContext(cwd);
|
|
107
|
+
if (!ctx?.contract) {
|
|
108
|
+
const payload = { ok: false, error: "inferno_missing", events };
|
|
109
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
110
|
+
else fail("inferno/ missing or invalid");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// propose
|
|
115
|
+
const prompt = buildPrompt({
|
|
116
|
+
description: task,
|
|
117
|
+
contract: ctx.contract,
|
|
118
|
+
capabilities: ctx.capabilities,
|
|
119
|
+
scenarios: ctx.scenarios,
|
|
120
|
+
});
|
|
121
|
+
let suggestion;
|
|
122
|
+
try {
|
|
123
|
+
const raw = await generateWithLocalModel(prompt);
|
|
124
|
+
suggestion = parseSuggestionJson(raw);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const payload = { ok: false, error: "local_model_failed", reason: String(err.message || err), events };
|
|
127
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
128
|
+
else fail(`local model failed`, err.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
stageEvent(asJson, events, "propose", "ok", {
|
|
132
|
+
newCapabilities: (suggestion.newCapabilities || []).length,
|
|
133
|
+
removedCapabilities: (suggestion.removedCapabilities || []).length,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const schemaErrors = validateSuggestion(suggestion);
|
|
137
|
+
const conflictErrors = detectSuggestionConflicts(ctx.contract, suggestion);
|
|
138
|
+
if (schemaErrors.length || conflictErrors.length) {
|
|
139
|
+
const payload = {
|
|
140
|
+
ok: false,
|
|
141
|
+
error: "invalid_suggestion",
|
|
142
|
+
issues: [...schemaErrors, ...conflictErrors],
|
|
143
|
+
events,
|
|
144
|
+
};
|
|
145
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
146
|
+
else fail("suggestion invalid", payload.issues[0]);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const snapshot = snapshotInferno(cwd);
|
|
151
|
+
let rolledBack = false;
|
|
152
|
+
let applyChanged = false;
|
|
153
|
+
let validationPassed = false;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
stageEvent(asJson, events, "apply", "info", { dryRun: true });
|
|
158
|
+
} else {
|
|
159
|
+
applyChanged = applyChanges({
|
|
160
|
+
cwd,
|
|
161
|
+
contract: ctx.contract,
|
|
162
|
+
capabilities: ctx.capabilities,
|
|
163
|
+
suggestion,
|
|
164
|
+
version: ctx.version,
|
|
165
|
+
quiet: asJson,
|
|
166
|
+
});
|
|
167
|
+
stageEvent(asJson, events, "apply", "ok", { changed: applyChanged });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let check = runCliJson(["check", "--json"]);
|
|
171
|
+
if (process.env.INFERNO_TEST_FORCE_VALIDATE_FAIL === "1") {
|
|
172
|
+
check = { ok: false, data: { ok: false, errors: ["forced_validation_failure"] } };
|
|
173
|
+
}
|
|
174
|
+
if (!check.ok || !check.data?.ok) {
|
|
175
|
+
throw new Error(`validation_failed:${(check.data?.errors || []).join(",")}`);
|
|
176
|
+
}
|
|
177
|
+
validationPassed = true;
|
|
178
|
+
stageEvent(asJson, events, "validate", "ok");
|
|
179
|
+
} catch (err) {
|
|
180
|
+
stageEvent(asJson, events, "validate", "fail", { reason: String(err.message || err) });
|
|
181
|
+
if (!dryRun && !noRollback) {
|
|
182
|
+
restoreSnapshot(cwd, snapshot);
|
|
183
|
+
rolledBack = true;
|
|
184
|
+
stageEvent(asJson, events, "rollback", "ok");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const artifact = {
|
|
189
|
+
task,
|
|
190
|
+
dryRun,
|
|
191
|
+
noRollback,
|
|
192
|
+
rolledBack,
|
|
193
|
+
applyChanged,
|
|
194
|
+
suggestionSummary: suggestion.summary || "",
|
|
195
|
+
touchedCapabilities: [
|
|
196
|
+
...(suggestion.newCapabilities || []).map((c) => c.id),
|
|
197
|
+
...(suggestion.removedCapabilities || []),
|
|
198
|
+
],
|
|
199
|
+
events,
|
|
200
|
+
};
|
|
201
|
+
const artifactPath = writeRunArtifact(cwd, artifact);
|
|
202
|
+
|
|
203
|
+
const payload = {
|
|
204
|
+
ok: validationPassed,
|
|
205
|
+
mode: "run",
|
|
206
|
+
task,
|
|
207
|
+
dryRun,
|
|
208
|
+
rolledBack,
|
|
209
|
+
applyChanged,
|
|
210
|
+
artifactPath,
|
|
211
|
+
events,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (asJson) {
|
|
215
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
216
|
+
process.exit(payload.ok ? 0 : 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
section("Result");
|
|
220
|
+
info(`task: ${gray(task)}`);
|
|
221
|
+
info(`artifact: ${gray(artifactPath)}`);
|
|
222
|
+
if (payload.ok) ok("run completed");
|
|
223
|
+
else warn("run rolled back after failed validation");
|
|
224
|
+
console.log();
|
|
225
|
+
process.exit(payload.ok ? 0 : 1);
|
|
226
|
+
}
|
|
227
|
+
|
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",
|
|
@@ -1,96 +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
|
-
|
|
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 && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-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": [
|
|
@@ -1,36 +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
|
|
26
|
-
run: npx infernoflow
|
|
27
|
-
env:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
35
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
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 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
|
+
|