infernoflow 0.10.23 → 0.10.26
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/CHANGELOG.md +13 -6
- package/dist/bin/infernoflow.mjs +135 -41
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +73 -1
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/changelog.mjs +343 -21
- package/dist/lib/commands/check.mjs +179 -3
- package/dist/lib/commands/context.mjs +287 -31
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +394 -10
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +293 -15
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/setup.mjs +234 -4
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +114 -1
- package/dist/lib/learning/profile.mjs +212 -2
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +47 -47
|
@@ -1,21 +1,299 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow publish
|
|
3
|
+
*
|
|
4
|
+
* One command to release:
|
|
5
|
+
* 1. Bump version in package.json (--bump patch|minor|major, default: patch)
|
|
6
|
+
* 2. Move CHANGELOG ## Unreleased → ## <new-version> — <date>
|
|
7
|
+
* 3. Run build (node build.mjs)
|
|
8
|
+
* 4. Run smoke tests (skippable with --skip-tests)
|
|
9
|
+
* 5. npm publish
|
|
10
|
+
* 6. git add + commit + push
|
|
11
|
+
*
|
|
12
|
+
* Flags:
|
|
13
|
+
* --bump patch|minor|major Version bump type (default: patch)
|
|
14
|
+
* --skip-build Skip build step
|
|
15
|
+
* --skip-tests Skip smoke-test step
|
|
16
|
+
* --skip-push Publish + commit but don't git push
|
|
17
|
+
* --dry-run Print every step without executing
|
|
18
|
+
* --yes, -y Non-interactive (skip confirmation prompt)
|
|
19
|
+
* --tag Also create a git tag vX.Y.Z
|
|
20
|
+
*/
|
|
2
21
|
|
|
3
|
-
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { execSync } from "node:child_process";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { header, ok, fail, warn, info, done, bold, cyan, gray, yellow, green } from "../ui/output.mjs";
|
|
4
27
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`),!0;let e=c.readFileSync(r,"utf8");if(/^## Unreleased/im.test(e))return e=e.replace(/^## Unreleased.*$/im,`## ${i} \u2014 ${h()}`),c.writeFileSync(r,e),!0;const p=/^# .+$/im;return p.test(e)?(e=e.replace(p,y=>`${y}
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PKG_ROOT = path.resolve(__dirname, "../..");
|
|
8
30
|
|
|
9
|
-
|
|
31
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
10
32
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
function run(cmd, opts = {}) {
|
|
34
|
+
return execSync(cmd, {
|
|
35
|
+
cwd: PKG_ROOT,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: opts.silent ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
38
|
+
...opts,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
|
|
42
|
+
function runCapture(cmd) {
|
|
43
|
+
return execSync(cmd, { cwd: PKG_ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
44
|
+
}
|
|
17
45
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
function bumpVersion(current, type) {
|
|
47
|
+
const parts = current.split(".").map(Number);
|
|
48
|
+
if (type === "major") { parts[0]++; parts[1] = 0; parts[2] = 0; }
|
|
49
|
+
else if (type === "minor") { parts[1]++; parts[2] = 0; }
|
|
50
|
+
else { parts[2]++; }
|
|
51
|
+
return parts.join(".");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function todayISO() {
|
|
55
|
+
return new Date().toISOString().slice(0, 10);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function updateChangelog(changelogPath, newVersion) {
|
|
59
|
+
if (!fs.existsSync(changelogPath)) {
|
|
60
|
+
// create a minimal changelog if none exists
|
|
61
|
+
fs.writeFileSync(changelogPath,
|
|
62
|
+
`# Changelog — infernoflow\n\n## ${newVersion} — ${todayISO()}\n\n### Added\n- Release ${newVersion}\n`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let text = fs.readFileSync(changelogPath, "utf8");
|
|
67
|
+
|
|
68
|
+
// If there's an ## Unreleased section, rename it
|
|
69
|
+
if (/^## Unreleased/im.test(text)) {
|
|
70
|
+
text = text.replace(
|
|
71
|
+
/^## Unreleased.*$/im,
|
|
72
|
+
`## ${newVersion} — ${todayISO()}`
|
|
73
|
+
);
|
|
74
|
+
fs.writeFileSync(changelogPath, text);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// No Unreleased section — insert a new version heading after the first heading
|
|
79
|
+
const insertAfter = /^# .+$/im;
|
|
80
|
+
if (insertAfter.test(text)) {
|
|
81
|
+
text = text.replace(
|
|
82
|
+
insertAfter,
|
|
83
|
+
(m) => `${m}\n\n## ${newVersion} — ${todayISO()}\n\n### Added\n- Release ${newVersion}\n`
|
|
84
|
+
);
|
|
85
|
+
fs.writeFileSync(changelogPath, text);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback: prepend
|
|
90
|
+
fs.writeFileSync(changelogPath, `## ${newVersion} — ${todayISO()}\n\n### Added\n- Release ${newVersion}\n\n${text}`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasUncommittedChanges() {
|
|
95
|
+
try {
|
|
96
|
+
const out = runCapture("git status --porcelain");
|
|
97
|
+
return out.length > 0;
|
|
98
|
+
} catch { return false; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function gitUserConfigured() {
|
|
102
|
+
try {
|
|
103
|
+
runCapture("git config user.email");
|
|
104
|
+
return true;
|
|
105
|
+
} catch { return false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export async function publishCommand(rawArgs) {
|
|
112
|
+
const args = rawArgs.slice(1); // drop command name
|
|
113
|
+
|
|
114
|
+
const dryRun = args.includes("--dry-run");
|
|
115
|
+
const skipBuild = args.includes("--skip-build");
|
|
116
|
+
const skipTests = args.includes("--skip-tests");
|
|
117
|
+
const skipPush = args.includes("--skip-push");
|
|
118
|
+
const createTag = args.includes("--tag");
|
|
119
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
120
|
+
|
|
121
|
+
const bumpIdx = args.indexOf("--bump");
|
|
122
|
+
const bumpType = bumpIdx !== -1 ? (args[bumpIdx + 1] || "patch") : "patch";
|
|
123
|
+
|
|
124
|
+
if (!["patch", "minor", "major"].includes(bumpType)) {
|
|
125
|
+
console.error(` Invalid --bump value: ${bumpType}. Must be patch, minor, or major.`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
header("infernoflow publish");
|
|
130
|
+
|
|
131
|
+
if (dryRun) {
|
|
132
|
+
warn("DRY RUN — no files will be written, no commands executed");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 1. Read current version ───────────────────────────────────────────────
|
|
136
|
+
const pkgPath = path.join(PKG_ROOT, "package.json");
|
|
137
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
138
|
+
const oldVersion = pkg.version;
|
|
139
|
+
const newVersion = bumpVersion(oldVersion, bumpType);
|
|
140
|
+
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(` ${gray("current")} ${bold(oldVersion)}`);
|
|
143
|
+
console.log(` ${gray("new ")} ${bold(green(newVersion))} ${gray("(" + bumpType + " bump)")}`);
|
|
144
|
+
console.log();
|
|
145
|
+
|
|
146
|
+
// ── 2. Confirm ────────────────────────────────────────────────────────────
|
|
147
|
+
if (!yes && !dryRun) {
|
|
148
|
+
process.stdout.write(` Publish ${bold(cyan("infernoflow@" + newVersion))} to npm? [y/N] `);
|
|
149
|
+
let confirmed = false;
|
|
150
|
+
try {
|
|
151
|
+
const answer = execSync("bash -c 'read -r ans </dev/tty; echo $ans'", {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
154
|
+
}).trim().toLowerCase();
|
|
155
|
+
confirmed = answer === "y" || answer === "yes";
|
|
156
|
+
} catch {
|
|
157
|
+
confirmed = false;
|
|
158
|
+
}
|
|
159
|
+
console.log();
|
|
160
|
+
if (!confirmed) {
|
|
161
|
+
console.log(gray(" Aborted.\n"));
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 3. Bump package.json ──────────────────────────────────────────────────
|
|
167
|
+
info(`Bumping package.json ${gray(oldVersion + " → " + newVersion)}`);
|
|
168
|
+
if (!dryRun) {
|
|
169
|
+
pkg.version = newVersion;
|
|
170
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n");
|
|
171
|
+
ok("package.json updated");
|
|
172
|
+
} else {
|
|
173
|
+
ok(gray("[dry] would write package.json"));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 4. Update CHANGELOG ───────────────────────────────────────────────────
|
|
177
|
+
const changelogPath = path.join(PKG_ROOT, "CHANGELOG.md");
|
|
178
|
+
info("Updating CHANGELOG.md");
|
|
179
|
+
if (!dryRun) {
|
|
180
|
+
updateChangelog(changelogPath, newVersion);
|
|
181
|
+
ok("CHANGELOG.md updated");
|
|
182
|
+
} else {
|
|
183
|
+
ok(gray("[dry] would update CHANGELOG.md"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── 5. Build ──────────────────────────────────────────────────────────────
|
|
187
|
+
if (!skipBuild) {
|
|
188
|
+
info("Running build " + gray("node build.mjs"));
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
try {
|
|
191
|
+
run("node build.mjs", { silent: false });
|
|
192
|
+
ok("Build succeeded");
|
|
193
|
+
} catch (err) {
|
|
194
|
+
fail("Build failed", err.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
ok(gray("[dry] would run: node build.mjs"));
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
warn("Skipping build (--skip-build)");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── 6. Smoke tests ────────────────────────────────────────────────────────
|
|
205
|
+
if (!skipTests) {
|
|
206
|
+
info("Running smoke tests");
|
|
207
|
+
if (!dryRun) {
|
|
208
|
+
try {
|
|
209
|
+
run("npm test", { silent: false });
|
|
210
|
+
ok("All smoke tests passed");
|
|
211
|
+
} catch (err) {
|
|
212
|
+
fail("Smoke tests failed", "Fix tests or re-run with --skip-tests");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
ok(gray("[dry] would run: npm test"));
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
warn("Skipping tests (--skip-tests)");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── 7. npm publish ────────────────────────────────────────────────────────
|
|
223
|
+
info(`Publishing to npm ${gray("infernoflow@" + newVersion)}`);
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
try {
|
|
226
|
+
run("npm publish", { silent: false });
|
|
227
|
+
ok(`Published infernoflow@${newVersion}`);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
fail("npm publish failed", err.message || "Check npm credentials");
|
|
230
|
+
// Don't exit — still commit the version bump even if publish fails
|
|
231
|
+
warn("Continuing to git commit despite publish failure");
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
ok(gray("[dry] would run: npm publish"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── 8. Git commit ─────────────────────────────────────────────────────────
|
|
238
|
+
info("Committing version bump");
|
|
239
|
+
if (!dryRun) {
|
|
240
|
+
try {
|
|
241
|
+
// Stage changed files
|
|
242
|
+
const filesToStage = ["package.json", "CHANGELOG.md"];
|
|
243
|
+
run(`git add ${filesToStage.join(" ")}`, { silent: false });
|
|
244
|
+
|
|
245
|
+
const commitMsg = `chore: release ${newVersion}`;
|
|
246
|
+
run(`git commit -m "${commitMsg}"`, { silent: false });
|
|
247
|
+
ok(`Committed: ${gray(commitMsg)}`);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
warn(`Git commit failed: ${err.message}`);
|
|
250
|
+
warn("You can commit manually: git add package.json CHANGELOG.md && git commit -m \"chore: release " + newVersion + "\"");
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
ok(gray(`[dry] would commit: chore: release ${newVersion}`));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── 9. Git tag ────────────────────────────────────────────────────────────
|
|
257
|
+
if (createTag) {
|
|
258
|
+
info(`Creating git tag ${gray("v" + newVersion)}`);
|
|
259
|
+
if (!dryRun) {
|
|
260
|
+
try {
|
|
261
|
+
run(`git tag v${newVersion}`, { silent: false });
|
|
262
|
+
ok(`Tagged v${newVersion}`);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
warn(`Git tag failed: ${err.message}`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
ok(gray(`[dry] would tag: v${newVersion}`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── 10. Git push ──────────────────────────────────────────────────────────
|
|
272
|
+
if (!skipPush) {
|
|
273
|
+
info("Pushing to origin");
|
|
274
|
+
if (!dryRun) {
|
|
275
|
+
try {
|
|
276
|
+
const pushCmd = createTag ? `git push && git push origin v${newVersion}` : "git push";
|
|
277
|
+
run(pushCmd, { silent: false });
|
|
278
|
+
ok("Pushed to origin");
|
|
279
|
+
} catch (err) {
|
|
280
|
+
warn(`Git push failed: ${err.message}`);
|
|
281
|
+
warn("Push manually: git push");
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
ok(gray("[dry] would run: git push"));
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
warn("Skipping push (--skip-push)");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
291
|
+
console.log();
|
|
292
|
+
if (dryRun) {
|
|
293
|
+
done(`Dry run complete — would have published infernoflow@${newVersion}`);
|
|
294
|
+
} else {
|
|
295
|
+
done(`infernoflow@${newVersion} published, committed, and pushed`);
|
|
296
|
+
console.log(` ${cyan("npm:")} https://www.npmjs.com/package/infernoflow`);
|
|
297
|
+
console.log(` ${cyan("git:")} ${gray("chore: release " + newVersion)}\n`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -1,10 +1,338 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 { resolveProvider } from "../ai/providerRouter.mjs";
|
|
7
|
+
import {
|
|
8
|
+
buildPrompt,
|
|
9
|
+
loadSuggestContext,
|
|
10
|
+
parseSuggestionJson,
|
|
11
|
+
validateSuggestion,
|
|
12
|
+
detectSuggestionConflicts,
|
|
13
|
+
applyChanges,
|
|
14
|
+
} from "./suggest.mjs";
|
|
15
|
+
import { header, section, ok, warn, fail, info, gray } from "../ui/output.mjs";
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
|
|
20
|
+
|
|
21
|
+
function runCliJson(args) {
|
|
22
|
+
try {
|
|
23
|
+
const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
24
|
+
return { ok: true, data: JSON.parse(out) };
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const stdout = err?.stdout?.toString?.() || "";
|
|
27
|
+
try {
|
|
28
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
29
|
+
} catch {
|
|
30
|
+
return { ok: false, data: { ok: false, errors: ["command_failed"] } };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stageEvent(asJson, events, stage, status, details = {}) {
|
|
36
|
+
const ev = { ts: new Date().toISOString(), stage, status, ...details };
|
|
37
|
+
events.push(ev);
|
|
38
|
+
if (asJson) return;
|
|
39
|
+
const text = `${stage}: ${status}`;
|
|
40
|
+
if (status === "ok") ok(text);
|
|
41
|
+
else if (status === "warn") warn(text);
|
|
42
|
+
else if (status === "fail") fail(text);
|
|
43
|
+
else info(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function snapshotInferno(cwd) {
|
|
47
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
48
|
+
const targets = [];
|
|
49
|
+
const walk = (dir) => {
|
|
50
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const p = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) walk(p);
|
|
53
|
+
else targets.push(p);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (fs.existsSync(infernoDir)) walk(infernoDir);
|
|
57
|
+
const snapshot = new Map();
|
|
58
|
+
targets.forEach((filePath) => snapshot.set(filePath, fs.readFileSync(filePath, "utf8")));
|
|
59
|
+
return snapshot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function restoreSnapshot(cwd, snapshot) {
|
|
63
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
64
|
+
if (fs.existsSync(infernoDir)) {
|
|
65
|
+
const existing = [];
|
|
66
|
+
const walk = (dir) => {
|
|
67
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
const p = path.join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) walk(p);
|
|
70
|
+
else existing.push(p);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(infernoDir);
|
|
74
|
+
existing.forEach((filePath) => {
|
|
75
|
+
if (!snapshot.has(filePath)) fs.unlinkSync(filePath);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const [filePath, content] of snapshot.entries()) {
|
|
79
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
80
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeRunArtifact(cwd, artifact) {
|
|
85
|
+
const runsDir = path.join(cwd, "inferno", "runs");
|
|
86
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
87
|
+
const filePath = path.join(runsDir, `${Date.now()}.json`);
|
|
88
|
+
fs.writeFileSync(filePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
89
|
+
return path.relative(cwd, filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getOptionValue(args, flag, fallback = null) {
|
|
93
|
+
const idx = args.indexOf(flag);
|
|
94
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTask(args) {
|
|
99
|
+
const takesValue = new Set(["--provider", "--ide"]);
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 1; i < args.length; i++) {
|
|
102
|
+
const token = args[i];
|
|
103
|
+
if (token.startsWith("-")) {
|
|
104
|
+
if (takesValue.has(token)) i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out.push(token);
|
|
108
|
+
}
|
|
109
|
+
return out.join(" ").trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPromptFallbackSuggestion(task, contract) {
|
|
113
|
+
return {
|
|
114
|
+
summary: `Prompt fallback only: ${task}`,
|
|
115
|
+
newCapabilities: [],
|
|
116
|
+
removedCapabilities: [],
|
|
117
|
+
updatedScenarios: [],
|
|
118
|
+
changelogEntry: `- Prompt fallback mode for task: ${task} (no automatic contract mutation).`,
|
|
119
|
+
_meta: {
|
|
120
|
+
actionRequired: true,
|
|
121
|
+
nextStep: "Run infernoflow suggest or provide an agent bridge for automatic apply.",
|
|
122
|
+
capabilitiesCount: (contract?.capabilities || []).length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function generateWithIdeAgent(prompt) {
|
|
128
|
+
if (process.env.INFERNO_AGENT_MOCK_RESPONSE) return process.env.INFERNO_AGENT_MOCK_RESPONSE;
|
|
129
|
+
const responseFile = process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
130
|
+
? process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
131
|
+
: path.join(process.cwd(), "inferno", "agent-response.json");
|
|
132
|
+
if (fs.existsSync(responseFile)) {
|
|
133
|
+
const data = fs.readFileSync(responseFile, "utf8");
|
|
134
|
+
fs.unlinkSync(responseFile);
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
const infernoDir = path.join(process.cwd(), "inferno");
|
|
138
|
+
const promptFile = path.join(infernoDir, "agent-prompt.md");
|
|
139
|
+
if (fs.existsSync(promptFile)) fs.unlinkSync(promptFile);
|
|
140
|
+
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
141
|
+
process.stderr.write("\n \u2139 Prompt written to inferno/agent-prompt.md\n");
|
|
142
|
+
process.stderr.write(" \u2192 Open it, paste into Cursor or Claude\n");
|
|
143
|
+
process.stderr.write(" \u2192 Save the JSON reply to: inferno/agent-response.json\n");
|
|
144
|
+
process.stderr.write(" Waiting up to 5 minutes...\n\n");
|
|
145
|
+
const deadline = Date.now() + 300_000;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
148
|
+
if (fs.existsSync(responseFile)) {
|
|
149
|
+
const data = fs.readFileSync(responseFile, "utf8");
|
|
150
|
+
fs.unlinkSync(responseFile);
|
|
151
|
+
process.stderr.write(" \u2714 Response received\n\n");
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error("ide_agent_bridge_timeout");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runCommand(args = []) {
|
|
159
|
+
const asJson = args.includes("--json");
|
|
160
|
+
const dryRun = args.includes("--dry-run");
|
|
161
|
+
const noRollback = args.includes("--no-rollback");
|
|
162
|
+
const providerRequested = (getOptionValue(args, "--provider", "auto") || "auto").toLowerCase();
|
|
163
|
+
const ideRequested = (getOptionValue(args, "--ide", "auto") || "auto").toLowerCase();
|
|
164
|
+
const task = extractTask(args) || "sync check";
|
|
165
|
+
const cwd = process.cwd();
|
|
166
|
+
const events = [];
|
|
167
|
+
const reasonCodes = [];
|
|
168
|
+
|
|
169
|
+
if (!asJson) header("run");
|
|
170
|
+
stageEvent(asJson, events, "init", "info", { task, dryRun, noRollback });
|
|
171
|
+
|
|
172
|
+
// detect
|
|
173
|
+
const impact = runCliJson(["pr-impact", "--json"]);
|
|
174
|
+
stageEvent(asJson, events, "detect", impact.data?.ok ? "ok" : "warn", { confidence: impact.data?.confidence || "low" });
|
|
175
|
+
|
|
176
|
+
const routed = await resolveProvider(providerRequested, ideRequested);
|
|
177
|
+
reasonCodes.push(...(routed.reasonCodes || []));
|
|
178
|
+
if (routed.error === "agent_unavailable") {
|
|
179
|
+
const payload = {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: "agent_unavailable",
|
|
182
|
+
providerRequested,
|
|
183
|
+
providerResolved: routed.providerResolved,
|
|
184
|
+
ideDetected: routed.ideDetected,
|
|
185
|
+
agentAvailable: routed.agentAvailable,
|
|
186
|
+
reasonCodes,
|
|
187
|
+
events,
|
|
188
|
+
};
|
|
189
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
190
|
+
else fail("provider agent unavailable", "Use --provider auto|local|prompt");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
stageEvent(asJson, events, "route", "ok", {
|
|
194
|
+
providerRequested,
|
|
195
|
+
providerResolved: routed.providerResolved,
|
|
196
|
+
ideDetected: routed.ideDetected,
|
|
197
|
+
agentAvailable: routed.agentAvailable,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const ctx = loadSuggestContext(cwd);
|
|
201
|
+
if (!ctx?.contract) {
|
|
202
|
+
const payload = { ok: false, error: "inferno_missing", events };
|
|
203
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
204
|
+
else fail("inferno/ missing or invalid");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// propose
|
|
209
|
+
const prompt = buildPrompt({
|
|
210
|
+
description: task,
|
|
211
|
+
contract: ctx.contract,
|
|
212
|
+
capabilities: ctx.capabilities,
|
|
213
|
+
scenarios: ctx.scenarios,
|
|
214
|
+
});
|
|
215
|
+
let suggestion;
|
|
216
|
+
try {
|
|
217
|
+
if (routed.providerResolved === "local") {
|
|
218
|
+
const raw = await generateWithLocalModel(prompt);
|
|
219
|
+
suggestion = parseSuggestionJson(raw);
|
|
220
|
+
} else if (routed.providerResolved === "agent") {
|
|
221
|
+
const raw = await generateWithIdeAgent(prompt);
|
|
222
|
+
suggestion = parseSuggestionJson(raw);
|
|
223
|
+
} else {
|
|
224
|
+
suggestion = buildPromptFallbackSuggestion(task, ctx.contract);
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const payload = { ok: false, error: "proposal_failed", reason: String(err.message || err), reasonCodes, events };
|
|
228
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
229
|
+
else fail("proposal generation failed", err.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
stageEvent(asJson, events, "propose", "ok", {
|
|
233
|
+
newCapabilities: (suggestion.newCapabilities || []).length,
|
|
234
|
+
removedCapabilities: (suggestion.removedCapabilities || []).length,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const schemaErrors = routed.providerResolved === "prompt" ? [] : validateSuggestion(suggestion);
|
|
238
|
+
const conflictErrors = routed.providerResolved === "prompt" ? [] : detectSuggestionConflicts(ctx.contract, suggestion);
|
|
239
|
+
if (schemaErrors.length || conflictErrors.length) {
|
|
240
|
+
const payload = {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "invalid_suggestion",
|
|
243
|
+
issues: [...schemaErrors, ...conflictErrors],
|
|
244
|
+
events,
|
|
245
|
+
};
|
|
246
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
247
|
+
else fail("suggestion invalid", payload.issues[0]);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const snapshot = snapshotInferno(cwd);
|
|
252
|
+
let rolledBack = false;
|
|
253
|
+
let applyChanged = false;
|
|
254
|
+
let validationPassed = false;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
if (dryRun) {
|
|
258
|
+
stageEvent(asJson, events, "apply", "info", { dryRun: true });
|
|
259
|
+
} else if (routed.providerResolved === "prompt") {
|
|
260
|
+
stageEvent(asJson, events, "apply", "warn", {
|
|
261
|
+
skipped: true,
|
|
262
|
+
reason: "prompt_fallback_requires_manual_step",
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
applyChanged = applyChanges({
|
|
266
|
+
cwd,
|
|
267
|
+
contract: ctx.contract,
|
|
268
|
+
capabilities: ctx.capabilities,
|
|
269
|
+
suggestion,
|
|
270
|
+
version: ctx.version,
|
|
271
|
+
quiet: asJson,
|
|
272
|
+
});
|
|
273
|
+
stageEvent(asJson, events, "apply", "ok", { changed: applyChanged });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let check = runCliJson(["check", "--json"]);
|
|
277
|
+
if (process.env.INFERNO_TEST_FORCE_VALIDATE_FAIL === "1") {
|
|
278
|
+
check = { ok: false, data: { ok: false, errors: ["forced_validation_failure"] } };
|
|
279
|
+
}
|
|
280
|
+
if (!check.ok || !check.data?.ok) {
|
|
281
|
+
throw new Error(`validation_failed:${(check.data?.errors || []).join(",")}`);
|
|
282
|
+
}
|
|
283
|
+
validationPassed = true;
|
|
284
|
+
stageEvent(asJson, events, "validate", "ok");
|
|
285
|
+
} catch (err) {
|
|
286
|
+
stageEvent(asJson, events, "validate", "fail", { reason: String(err.message || err) });
|
|
287
|
+
if (!dryRun && !noRollback) {
|
|
288
|
+
restoreSnapshot(cwd, snapshot);
|
|
289
|
+
rolledBack = true;
|
|
290
|
+
stageEvent(asJson, events, "rollback", "ok");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const artifact = {
|
|
295
|
+
task,
|
|
296
|
+
dryRun,
|
|
297
|
+
noRollback,
|
|
298
|
+
rolledBack,
|
|
299
|
+
applyChanged,
|
|
300
|
+
suggestionSummary: suggestion.summary || "",
|
|
301
|
+
touchedCapabilities: [
|
|
302
|
+
...(suggestion.newCapabilities || []).map((c) => c.id),
|
|
303
|
+
...(suggestion.removedCapabilities || []),
|
|
304
|
+
],
|
|
305
|
+
events,
|
|
306
|
+
};
|
|
307
|
+
const artifactPath = writeRunArtifact(cwd, artifact);
|
|
308
|
+
|
|
309
|
+
const payload = {
|
|
310
|
+
ok: validationPassed,
|
|
311
|
+
mode: "run",
|
|
312
|
+
task,
|
|
313
|
+
dryRun,
|
|
314
|
+
providerRequested,
|
|
315
|
+
providerResolved: routed.providerResolved,
|
|
316
|
+
ideDetected: routed.ideDetected,
|
|
317
|
+
agentAvailable: routed.agentAvailable,
|
|
318
|
+
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
319
|
+
rolledBack,
|
|
320
|
+
applyChanged,
|
|
321
|
+
artifactPath,
|
|
322
|
+
events,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (asJson) {
|
|
326
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
327
|
+
process.exit(payload.ok ? 0 : 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
section("Result");
|
|
331
|
+
info(`task: ${gray(task)}`);
|
|
332
|
+
info(`artifact: ${gray(artifactPath)}`);
|
|
333
|
+
if (payload.ok) ok("run completed");
|
|
334
|
+
else warn("run rolled back after failed validation");
|
|
335
|
+
console.log();
|
|
336
|
+
process.exit(payload.ok ? 0 : 1);
|
|
337
|
+
}
|
|
9
338
|
|
|
10
|
-
`),r}throw new Error("ide_agent_bridge_timeout")}async function le(t=[]){const e=t.includes("--json"),n=t.includes("--dry-run"),o=t.includes("--no-rollback"),s=(A(t,"--provider","auto")||"auto").toLowerCase(),r=(A(t,"--ide","auto")||"auto").toLowerCase(),c=ee(t)||"sync check",f=process.cwd(),l=[],y=[];e||U("run"),m(e,l,"init","info",{task:c,dryRun:n,noRollback:o});const b=D(["pr-impact","--json"]);m(e,l,"detect",b.data?.ok?"ok":"warn",{confidence:b.data?.confidence||"low"});const d=await q(s,r);if(y.push(...d.reasonCodes||[]),d.error==="agent_unavailable"){const i={ok:!1,error:"agent_unavailable",providerRequested:s,providerResolved:d.providerResolved,ideDetected:d.ideDetected,agentAvailable:d.agentAvailable,reasonCodes:y,events:l};e?console.log(JSON.stringify(i,null,2)):v("provider agent unavailable","Use --provider auto|local|prompt"),process.exit(1)}m(e,l,"route","ok",{providerRequested:s,providerResolved:d.providerResolved,ideDetected:d.ideDetected,agentAvailable:d.agentAvailable});const g=G(f);g?.contract||(e?console.log(JSON.stringify({ok:!1,error:"inferno_missing",events:l},null,2)):v("inferno/ missing or invalid"),process.exit(1));const _=W({description:c,contract:g.contract,capabilities:g.capabilities,scenarios:g.scenarios});let u;try{if(d.providerResolved==="local"){const i=await $(_);u=O(i)}else if(d.providerResolved==="agent"){const i=await te(_);u=O(i)}else u=oe(c,g.contract)}catch(i){const J={ok:!1,error:"proposal_failed",reason:String(i.message||i),reasonCodes:y,events:l};e?console.log(JSON.stringify(J,null,2)):v("proposal generation failed",i.message),process.exit(1)}m(e,l,"propose","ok",{newCapabilities:(u.newCapabilities||[]).length,removedCapabilities:(u.removedCapabilities||[]).length});const R=d.providerResolved==="prompt"?[]:M(u),E=d.providerResolved==="prompt"?[]:V(g.contract,u);if(R.length||E.length){const i={ok:!1,error:"invalid_suggestion",issues:[...R,...E],events:l};e?console.log(JSON.stringify(i,null,2)):v("suggestion invalid",i.issues[0]),process.exit(1)}const P=X(f);let w=!1,S=!1,N=!1;try{n?m(e,l,"apply","info",{dryRun:!0}):d.providerResolved==="prompt"?m(e,l,"apply","warn",{skipped:!0,reason:"prompt_fallback_requires_manual_step"}):(S=K({cwd:f,contract:g.contract,capabilities:g.capabilities,suggestion:u,version:g.version,quiet:e}),m(e,l,"apply","ok",{changed:S}));let i=D(["check","--json"]);if(process.env.INFERNO_TEST_FORCE_VALIDATE_FAIL==="1"&&(i={ok:!1,data:{ok:!1,errors:["forced_validation_failure"]}}),!i.ok||!i.data?.ok)throw new Error(`validation_failed:${(i.data?.errors||[]).join(",")}`);N=!0,m(e,l,"validate","ok")}catch(i){m(e,l,"validate","fail",{reason:String(i.message||i)}),!n&&!o&&(Y(f,P),w=!0,m(e,l,"rollback","ok"))}const I={task:c,dryRun:n,noRollback:o,rolledBack:w,applyChanged:S,suggestionSummary:u.summary||"",touchedCapabilities:[...(u.newCapabilities||[]).map(i=>i.id),...u.removedCapabilities||[]],events:l},C=Z(f,I),k={ok:N,mode:"run",task:c,dryRun:n,providerRequested:s,providerResolved:d.providerResolved,ideDetected:d.ideDetected,agentAvailable:d.agentAvailable,reasonCodes:Array.from(new Set(y)),rolledBack:w,applyChanged:S,artifactPath:C,events:l};e&&(console.log(JSON.stringify(k,null,2)),process.exit(k.ok?0:1)),B("Result"),h(`task: ${j(c)}`),h(`artifact: ${j(C)}`),k.ok?x("run completed"):F("run rolled back after failed validation"),console.log(),process.exit(k.ok?0:1)}export{le as runCommand};
|