infernoflow 0.32.8 → 0.32.9

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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -321
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,61 +1,28 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as readline from "node:readline";
4
- import { header, ok, fail, warn, info, done, section, nextSteps, bold, cyan, gray, yellow, green, red, errorAndExit } from "../ui/output.mjs";
5
- import { personalisePrompt } from "../learning/adapt.mjs";
1
+ import*as p from"node:fs";import*as f from"node:path";import*as R from"node:readline";import{header as ee,ok as O,warn as M,info as ie,done as te,section as U,nextSteps as ne,bold as oe,cyan as S,gray as E,yellow as se,green as W,red as Z,errorAndExit as _}from"../ui/output.mjs";import{personalisePrompt as ae}from"../learning/adapt.mjs";function j(i){try{return JSON.parse(p.readFileSync(i,"utf8"))}catch{return null}}function K(i,e){return new Promise(t=>{i.question(e,r=>t(r.trim()))})}function fe(i){return i.replace(/[-_]+/g," ").split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function re({description:i,contract:e,capabilities:t,scenarios:r}){const b=e.capabilities||[],u=(t?.capabilities||[]).map(h=>` - ${h.id}: ${h.title||h.id}`).join(`
2
+ `),s=r.map(h=>{const v=(h.capabilitiesCovered||[]).join(", "),m=(h.steps||[]).map(c=>` {action: "${c.action}", expect: "${c.expect}"}`).join(`
3
+ `);return` File: ${h._file}
4
+ capabilitiesCovered: [${v}]
5
+ steps:
6
+ ${m}`}).join(`
6
7
 
7
- // ── Helpers ──────────────────────────────────────────────────────────────────
8
-
9
- export function readJson(filePath) {
10
- try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
11
- catch { return null; }
12
- }
13
-
14
- function ask(rl, question) {
15
- return new Promise(resolve => {
16
- rl.question(question, answer => resolve(answer.trim()));
17
- });
18
- }
19
-
20
- function toCapabilityId(str) {
21
- // "send email" → "SendEmail", "send-email" → "SendEmail"
22
- return str
23
- .replace(/[-_]+/g, " ")
24
- .split(" ")
25
- .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
26
- .join("");
27
- }
28
-
29
- export function buildPrompt({ description, contract, capabilities, scenarios }) {
30
- const capsIds = contract.capabilities || [];
31
- const capsDetail = (capabilities?.capabilities || [])
32
- .map(c => ` - ${c.id}: ${c.title || c.id}`)
33
- .join("\n");
34
-
35
- const scenarioFiles = scenarios.map(s => {
36
- const covered = (s.capabilitiesCovered || []).join(", ");
37
- const steps = (s.steps || []).map(st => ` {action: "${st.action}", expect: "${st.expect}"}`).join("\n");
38
- return ` File: ${s._file}\n capabilitiesCovered: [${covered}]\n steps:\n${steps}`;
39
- }).join("\n\n");
40
-
41
- return `You are a developer assistant for the infernoflow CLI tool.
8
+ `);return`You are a developer assistant for the infernoflow CLI tool.
42
9
 
43
10
  Your job is to analyze a code change description and suggest updates to the infernoflow contract files.
44
11
 
45
12
  ## Current contract state
46
13
 
47
- policyId: ${contract.policyId}
48
- policyVersion: ${contract.policyVersion}
49
- capabilities: [${capsIds.join(", ")}]
14
+ policyId: ${e.policyId}
15
+ policyVersion: ${e.policyVersion}
16
+ capabilities: [${b.join(", ")}]
50
17
 
51
18
  ## Current capabilities registry
52
- ${capsDetail || " (none)"}
19
+ ${u||" (none)"}
53
20
 
54
21
  ## Current scenarios
55
- ${scenarioFiles || " (none)"}
22
+ ${s||" (none)"}
56
23
 
57
24
  ## Developer's description of what changed
58
- "${description}"
25
+ "${i}"
59
26
 
60
27
  ## Your task
61
28
 
@@ -85,520 +52,11 @@ Rules:
85
52
  - Capability IDs must be PascalCase (e.g. SendEmail, not send_email)
86
53
  - If nothing changed capability-wise, return empty arrays
87
54
  - changelogEntry should start with "- "
88
- - Keep it minimal and accurate`;
89
- }
90
-
91
- export function validateSuggestion(suggestion) {
92
- const errors = [];
93
- if (!suggestion || typeof suggestion !== "object") {
94
- return ["AI response must be a JSON object."];
95
- }
96
- if (suggestion.summary != null && typeof suggestion.summary !== "string") {
97
- errors.push(`"summary" must be a string.`);
98
- }
99
- if (!Array.isArray(suggestion.newCapabilities)) {
100
- errors.push(`"newCapabilities" must be an array.`);
101
- }
102
- if (!Array.isArray(suggestion.removedCapabilities)) {
103
- errors.push(`"removedCapabilities" must be an array.`);
104
- }
105
- if (!Array.isArray(suggestion.updatedScenarios)) {
106
- errors.push(`"updatedScenarios" must be an array.`);
107
- }
108
- if (suggestion.changelogEntry != null && typeof suggestion.changelogEntry !== "string") {
109
- errors.push(`"changelogEntry" must be a string.`);
110
- }
111
-
112
- for (const c of suggestion.newCapabilities || []) {
113
- if (!c || typeof c !== "object") {
114
- errors.push(`Each item in "newCapabilities" must be an object.`);
115
- continue;
116
- }
117
- if (typeof c.id !== "string" || !/^[A-Z][A-Za-z0-9]*$/.test(c.id)) {
118
- errors.push(`newCapabilities[].id must be PascalCase (example: SendEmail).`);
119
- }
120
- if (typeof c.title !== "string" || !c.title.trim()) {
121
- errors.push(`newCapabilities[].title must be a non-empty string.`);
122
- }
123
- }
124
-
125
- for (const id of suggestion.removedCapabilities || []) {
126
- if (typeof id !== "string" || !id.trim()) {
127
- errors.push(`removedCapabilities[] must contain non-empty strings.`);
128
- }
129
- }
130
-
131
- for (const s of suggestion.updatedScenarios || []) {
132
- if (!s || typeof s !== "object") {
133
- errors.push(`Each item in "updatedScenarios" must be an object.`);
134
- continue;
135
- }
136
- if (typeof s.file !== "string" || !s.file.endsWith(".json")) {
137
- errors.push(`updatedScenarios[].file must be a .json filename.`);
138
- }
139
- if (typeof s.isNew !== "boolean") {
140
- errors.push(`updatedScenarios[].isNew must be boolean.`);
141
- }
142
- if (!Array.isArray(s.capabilitiesCovered) || !Array.isArray(s.stepsToAdd)) {
143
- errors.push(`updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.`);
144
- }
145
- }
146
-
147
- return errors;
148
- }
149
-
150
- export function detectSuggestionConflicts(contract, suggestion) {
151
- const issues = [];
152
- const existing = new Set(contract.capabilities || []);
153
- const newIds = new Set((suggestion.newCapabilities || []).map((c) => c.id));
154
- const removed = new Set(suggestion.removedCapabilities || []);
155
-
156
- for (const id of newIds) {
157
- if (removed.has(id)) {
158
- issues.push(`Capability "${id}" appears in both newCapabilities and removedCapabilities.`);
159
- }
160
- if (existing.has(id)) {
161
- issues.push(`Capability "${id}" already exists in contract capabilities.`);
162
- }
163
- }
164
-
165
- for (const id of removed) {
166
- if (!existing.has(id)) {
167
- issues.push(`Capability "${id}" cannot be removed because it does not exist in contract.`);
168
- }
169
- }
170
-
171
- return issues;
172
- }
173
-
174
- export function applyChanges({ cwd, contract, capabilities, suggestion, version, quiet = false }) {
175
- const infernoDir = path.join(cwd, "inferno");
176
- const contractPath = path.join(infernoDir, "contract.json");
177
- const capsPath = path.join(infernoDir, "capabilities.json");
178
- const changelogPath = path.join(infernoDir, "CHANGELOG.md");
179
- const scenariosDir = path.join(infernoDir, "scenarios");
180
-
181
- const newCaps = suggestion.newCapabilities || [];
182
- const removedCaps = suggestion.removedCapabilities || [];
183
- const updatedScenarios = suggestion.updatedScenarios || [];
184
- const changelogEntry = suggestion.changelogEntry || "";
185
-
186
- let changed = false;
187
- const writes = [];
188
- const queueWrite = (filePath, content) => writes.push({ filePath, content });
189
-
190
- // ── contract.json ─────────────────────────────────────────────────────────
191
- if (newCaps.length > 0 || removedCaps.length > 0) {
192
- const updatedCaps = [
193
- ...contract.capabilities.filter(c => !removedCaps.includes(c)),
194
- ...newCaps.map(c => c.id)
195
- ];
196
- const nextVersion = Number(contract.policyVersion || 1) + 1;
197
- const contractUpdated = { ...contract, capabilities: updatedCaps, policyVersion: nextVersion };
198
- queueWrite(contractPath, JSON.stringify(contractUpdated, null, 2) + "\n");
199
- if (!quiet) ok(`contract.json updated → policyVersion: v${nextVersion}`);
200
- changed = true;
201
- }
202
-
203
- // ── capabilities.json ─────────────────────────────────────────────────────
204
- if (newCaps.length > 0 || removedCaps.length > 0) {
205
- const reg = capabilities ? { ...capabilities } : { schemaVersion: 1, capabilities: [] };
206
- reg.capabilities = (reg.capabilities || []).filter(c => !removedCaps.includes(c.id));
207
- for (const nc of newCaps) {
208
- if (!reg.capabilities.find(c => c.id === nc.id)) {
209
- reg.capabilities.push({ id: nc.id, title: nc.title, since: version });
210
- }
211
- }
212
- queueWrite(capsPath, JSON.stringify(reg, null, 2) + "\n");
213
- if (!quiet) ok(`capabilities.json updated`);
214
- }
215
-
216
- // ── scenarios ─────────────────────────────────────────────────────────────
217
- for (const us of updatedScenarios) {
218
- const filePath = path.join(scenariosDir, us.file);
219
- let scenario;
220
-
221
- if (us.isNew || !fs.existsSync(filePath)) {
222
- scenario = {
223
- scenarioId: us.file.replace(".json", ""),
224
- description: suggestion.summary || "",
225
- capabilitiesCovered: us.capabilitiesCovered || [],
226
- steps: us.stepsToAdd || []
227
- };
228
- queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
229
- if (!quiet) ok(`Created scenario: ${cyan(us.file)}`);
230
- } else {
231
- scenario = readJson(filePath);
232
- const existingCaps = new Set(scenario.capabilitiesCovered || []);
233
- (us.capabilitiesCovered || []).forEach(c => existingCaps.add(c));
234
- scenario.capabilitiesCovered = [...existingCaps];
235
- scenario.steps = [...(scenario.steps || []), ...(us.stepsToAdd || [])];
236
- queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
237
- if (!quiet) ok(`Updated scenario: ${cyan(us.file)}`);
238
- }
239
- changed = true;
240
- }
241
-
242
- // ── CHANGELOG.md ──────────────────────────────────────────────────────────
243
- if (changelogEntry && fs.existsSync(changelogPath)) {
244
- let txt = fs.readFileSync(changelogPath, "utf8");
245
- if (/##\s+Unreleased/i.test(txt)) {
246
- txt = txt.replace(/(##\s+Unreleased[^\n]*\n)/i, `$1\n${changelogEntry}\n`);
247
- queueWrite(changelogPath, txt);
248
- if (!quiet) ok(`CHANGELOG.md updated`);
249
- changed = true;
250
- }
251
- }
252
-
253
- const backups = new Map();
254
- try {
255
- for (const write of writes) {
256
- if (fs.existsSync(write.filePath)) {
257
- backups.set(write.filePath, fs.readFileSync(write.filePath, "utf8"));
258
- } else {
259
- backups.set(write.filePath, null);
260
- }
261
- const tmpPath = `${write.filePath}.tmp`;
262
- fs.writeFileSync(tmpPath, write.content);
263
- fs.renameSync(tmpPath, write.filePath);
264
- }
265
- } catch (err) {
266
- for (const [filePath, content] of backups.entries()) {
267
- if (content === null) {
268
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
269
- } else {
270
- fs.writeFileSync(filePath, content);
271
- }
272
- }
273
- throw new Error(`Failed applying changes. Rolled back. Details: ${err.message}`);
274
- }
275
-
276
- return changed;
277
- }
278
-
279
- export function parseSuggestionJson(rawInput) {
280
- const clean = String(rawInput || "").trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
281
- return JSON.parse(clean);
282
- }
283
-
284
- export function loadSuggestContext(cwd) {
285
- const infernoDir = path.join(cwd, "inferno");
286
- const contractPath = path.join(infernoDir, "contract.json");
287
- const capsPath = path.join(infernoDir, "capabilities.json");
288
- const scenariosDir = path.join(infernoDir, "scenarios");
289
-
290
- const contract = readJson(contractPath);
291
- const capabilities = readJson(capsPath);
292
- const scenarios = [];
293
- if (fs.existsSync(scenariosDir)) {
294
- for (const f of fs.readdirSync(scenariosDir).filter((name) => name.endsWith(".json"))) {
295
- const s = readJson(path.join(scenariosDir, f));
296
- if (s) scenarios.push({ ...s, _file: f });
297
- }
298
- }
299
-
300
- let version = "0.1.0";
301
- const pkgPath = path.join(cwd, "package.json");
302
- if (fs.existsSync(pkgPath)) {
303
- const pkg = readJson(pkgPath);
304
- if (pkg?.version) version = pkg.version;
305
- }
306
-
307
- return { contract, capabilities, scenarios, version };
308
- }
309
-
310
- // ── JSON mode helpers ─────────────────────────────────────────────────────────
311
-
312
- function jsonOut(obj) {
313
- console.log(JSON.stringify(obj, null, 2));
314
- }
315
-
316
- function jsonErr(code, message, hint) {
317
- jsonOut({ ok: false, error: code, message, hint });
318
- process.exit(1);
319
- }
320
-
321
- async function readStdin() {
322
- return new Promise(resolve => {
323
- let data = "";
324
- process.stdin.setEncoding("utf8");
325
- process.stdin.on("data", chunk => { data += chunk; });
326
- process.stdin.on("end", () => resolve(data.trim()));
327
- // Timeout after 100ms if nothing arrives (not piped)
328
- setTimeout(() => resolve(""), 100);
329
- });
330
- }
331
-
332
- // ── Main ─────────────────────────────────────────────────────────────────────
333
-
334
- export async function suggestCommand(args) {
335
- const cwd = process.cwd();
336
- const infernoDir = path.join(cwd, "inferno");
337
-
338
- const asJson = args.includes("--json");
339
- const applyFlag = args.includes("--apply");
340
-
341
- // --response <json-string|@file>
342
- const respIdx = args.indexOf("--response");
343
- let responseRaw = respIdx !== -1 ? args[respIdx + 1] : null;
344
-
345
- if (!asJson) header("suggest");
346
-
347
- // ── Check inferno/ exists ─────────────────────────────────────────────────
348
- if (!fs.existsSync(infernoDir)) {
349
- if (asJson) jsonErr("inferno_not_found", "inferno/ not found", "Run: infernoflow init");
350
- errorAndExit("inferno/ not found", "Run: infernoflow init");
351
- }
352
-
353
- const contractPath = path.join(infernoDir, "contract.json");
354
- const capsPath = path.join(infernoDir, "capabilities.json");
355
- const scenariosDir = path.join(infernoDir, "scenarios");
356
-
357
- const contract = readJson(contractPath);
358
- if (!contract) {
359
- if (asJson) jsonErr("contract_not_found", "contract.json not found or invalid");
360
- errorAndExit("contract.json not found or invalid");
361
- }
362
-
363
- const capabilities = readJson(capsPath);
364
-
365
- // Load scenarios
366
- const scenarios = [];
367
- if (fs.existsSync(scenariosDir)) {
368
- for (const f of fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"))) {
369
- const s = readJson(path.join(scenariosDir, f));
370
- if (s) scenarios.push({ ...s, _file: f });
371
- }
372
- }
373
-
374
- // Get version from package.json
375
- let version = "0.1.0";
376
- const pkgPath = path.join(cwd, "package.json");
377
- if (fs.existsSync(pkgPath)) {
378
- const pkg = readJson(pkgPath);
379
- if (pkg?.version) version = pkg.version;
380
- }
381
-
382
- // ── Get description ───────────────────────────────────────────────────────
383
- const descArg = args.filter(a => !a.startsWith("-")).slice(1).join(" ");
384
- let description = descArg;
385
-
386
- if (!description && !asJson) {
387
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
388
- console.log(gray(" Describe what changed in your code (e.g. 'added email notifications'):"));
389
- description = await ask(rl, ` ${cyan(">")} `);
390
- rl.close();
391
- console.log();
392
- }
393
-
394
- if (!description) {
395
- if (asJson) jsonErr("no_description", "No description provided", "Usage: infernoflow suggest \"what changed\" --json");
396
- errorAndExit("No description provided", "Usage: infernoflow suggest \"what changed\"");
397
- }
398
-
399
- // ── Build prompt (personalised to developer profile) ─────────────────────
400
- const rawPrompt = buildPrompt({ description, contract, capabilities, scenarios });
401
- const prompt = personalisePrompt(rawPrompt, infernoDir);
402
-
403
- // ── JSON mode: emit prompt + context, then optionally apply response ──────
404
- if (asJson) {
405
- // If no --response given, check stdin for piped JSON
406
- if (!responseRaw) {
407
- const piped = await readStdin();
408
- if (piped) responseRaw = piped;
409
- }
410
-
411
- // If still no response → just emit the prompt payload and exit
412
- if (!responseRaw) {
413
- jsonOut({
414
- ok: true,
415
- mode: "prompt",
416
- description,
417
- prompt,
418
- context: {
419
- policyId: contract.policyId,
420
- policyVersion: contract.policyVersion,
421
- capabilities: contract.capabilities || [],
422
- scenarios: scenarios.map(s => s._file),
423
- version,
424
- },
425
- });
426
- return;
427
- }
428
-
429
- // Response provided — parse, validate, optionally apply
430
- // Support @file syntax
431
- if (responseRaw.startsWith("@")) {
432
- const filePath = responseRaw.slice(1);
433
- try { responseRaw = fs.readFileSync(filePath, "utf8"); }
434
- catch { jsonErr("file_not_found", `Cannot read response file: ${filePath}`); }
435
- }
436
-
437
- let suggestion;
438
- try { suggestion = parseSuggestionJson(responseRaw); }
439
- catch (e) { jsonErr("parse_error", "Could not parse AI response as JSON", e.message); }
440
-
441
- const validationErrors = validateSuggestion(suggestion);
442
- if (validationErrors.length > 0) {
443
- jsonErr("validation_error", validationErrors[0], validationErrors.join("; "));
444
- }
445
-
446
- const conflictErrors = detectSuggestionConflicts(contract, suggestion);
447
- if (conflictErrors.length > 0) {
448
- jsonErr("conflict_error", conflictErrors[0], conflictErrors.join("; "));
449
- }
450
-
451
- const changes = {
452
- summary: suggestion.summary || "",
453
- newCapabilities: suggestion.newCapabilities || [],
454
- removedCapabilities: suggestion.removedCapabilities || [],
455
- updatedScenarios: suggestion.updatedScenarios || [],
456
- changelogEntry: suggestion.changelogEntry || "",
457
- };
458
-
459
- if (!applyFlag) {
460
- // Validate-only: return the parsed changes without writing
461
- jsonOut({ ok: true, mode: "validate", description, changes, applied: false });
462
- return;
463
- }
464
-
465
- // Apply changes
466
- try {
467
- applyChanges({ cwd, contract, capabilities, suggestion, version, quiet: true });
468
- jsonOut({ ok: true, mode: "apply", description, changes, applied: true });
469
- } catch (e) {
470
- jsonErr("apply_error", e.message);
471
- }
472
- return;
473
- }
474
-
475
- // ── Interactive mode (unchanged) ──────────────────────────────────────────
476
- section("Generated Prompt");
477
- console.log();
478
- console.log(gray("─".repeat(50)));
479
- console.log(prompt);
480
- console.log(gray("─".repeat(50)));
481
- console.log();
482
-
483
- info("Copy the prompt above and paste it into:");
484
- console.log(` ${cyan("•")} Claude → https://claude.ai`);
485
- console.log(` ${cyan("•")} ChatGPT → https://chatgpt.com`);
486
- console.log(` ${cyan("•")} Copilot, Cursor, or any AI you use`);
487
- console.log();
488
- warn("The AI will respond with a JSON object.");
489
- console.log();
490
-
491
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
492
- console.log(gray(" Paste the AI's JSON response below, then press Enter twice:"));
493
- console.log();
494
-
495
- let jsonInput = "";
496
- let emptyLines = 0;
497
-
498
- await new Promise(resolve => {
499
- rl2.on("line", line => {
500
- if (line.trim() === "") {
501
- emptyLines++;
502
- if (emptyLines >= 2 && jsonInput.trim()) resolve();
503
- } else {
504
- emptyLines = 0;
505
- jsonInput += line + "\n";
506
- }
507
- });
508
- rl2.on("close", resolve);
509
- });
510
-
511
- rl2.close();
512
-
513
- let suggestion;
514
- try {
515
- suggestion = parseSuggestionJson(jsonInput);
516
- } catch {
517
- errorAndExit(
518
- "Could not parse the AI response as JSON",
519
- "Make sure you copied the full JSON response from the AI"
520
- );
521
- }
522
-
523
- const validationErrors = validateSuggestion(suggestion);
524
- if (validationErrors.length > 0) {
525
- errorAndExit(
526
- "AI response schema is invalid",
527
- validationErrors[0] + (validationErrors.length > 1 ? ` (+${validationErrors.length - 1} more)` : "")
528
- );
529
- }
530
- const conflictErrors = detectSuggestionConflicts(contract, suggestion);
531
- if (conflictErrors.length > 0) {
532
- errorAndExit(
533
- "AI response contains conflicting capability operations",
534
- conflictErrors[0] + (conflictErrors.length > 1 ? ` (+${conflictErrors.length - 1} more)` : "")
535
- );
536
- }
537
-
538
- section("Proposed Changes");
539
- console.log();
540
-
541
- if (suggestion.summary) {
542
- console.log(` ${bold("Summary:")} ${suggestion.summary}`);
543
- console.log();
544
- }
545
-
546
- const newCaps = suggestion.newCapabilities || [];
547
- const removedCaps = suggestion.removedCapabilities || [];
548
- const updatedScenarios = suggestion.updatedScenarios || [];
549
-
550
- if (newCaps.length === 0 && removedCaps.length === 0 && updatedScenarios.length === 0) {
551
- ok("No capability changes detected — nothing to apply.");
552
- console.log();
553
- process.exit(0);
554
- }
555
-
556
- if (newCaps.length > 0) {
557
- console.log(` ${green("+")} New capabilities:`);
558
- newCaps.forEach(c => console.log(` ${green(c.id)} — ${gray(c.title)}`));
559
- console.log();
560
- }
561
-
562
- if (removedCaps.length > 0) {
563
- console.log(` ${red("-")} Removed capabilities:`);
564
- removedCaps.forEach(c => console.log(` ${red(c)}`));
565
- console.log();
566
- }
567
-
568
- if (updatedScenarios.length > 0) {
569
- console.log(` ${cyan("~")} Scenario updates:`);
570
- updatedScenarios.forEach(s => {
571
- const tag = s.isNew ? green("[new]") : cyan("[update]");
572
- console.log(` ${tag} ${s.file}`);
573
- });
574
- console.log();
575
- }
576
-
577
- if (suggestion.changelogEntry) {
578
- console.log(` ${yellow("📝")} Changelog: ${gray(suggestion.changelogEntry)}`);
579
- console.log();
580
- }
581
-
582
- const rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
583
- const answer = await ask(rl3, ` Apply these changes? ${gray("(y/n)")} `);
584
- rl3.close();
585
- console.log();
586
-
587
- if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
588
- warn("Cancelled — no changes made.");
589
- console.log();
590
- process.exit(0);
591
- }
592
-
593
- section("Applying Changes");
594
- console.log();
595
-
596
- applyChanges({ cwd, contract, capabilities, suggestion, version });
597
-
598
- done("suggest complete!");
599
-
600
- nextSteps([
601
- cyan("infernoflow status") + " — verify the updated contract",
602
- cyan("infernoflow check") + " — validate everything",
603
- ]);
604
- }
55
+ - Keep it minimal and accurate`}function B(i){const e=[];if(!i||typeof i!="object")return["AI response must be a JSON object."];i.summary!=null&&typeof i.summary!="string"&&e.push('"summary" must be a string.'),Array.isArray(i.newCapabilities)||e.push('"newCapabilities" must be an array.'),Array.isArray(i.removedCapabilities)||e.push('"removedCapabilities" must be an array.'),Array.isArray(i.updatedScenarios)||e.push('"updatedScenarios" must be an array.'),i.changelogEntry!=null&&typeof i.changelogEntry!="string"&&e.push('"changelogEntry" must be a string.');for(const t of i.newCapabilities||[]){if(!t||typeof t!="object"){e.push('Each item in "newCapabilities" must be an object.');continue}(typeof t.id!="string"||!/^[A-Z][A-Za-z0-9]*$/.test(t.id))&&e.push("newCapabilities[].id must be PascalCase (example: SendEmail)."),(typeof t.title!="string"||!t.title.trim())&&e.push("newCapabilities[].title must be a non-empty string.")}for(const t of i.removedCapabilities||[])(typeof t!="string"||!t.trim())&&e.push("removedCapabilities[] must contain non-empty strings.");for(const t of i.updatedScenarios||[]){if(!t||typeof t!="object"){e.push('Each item in "updatedScenarios" must be an object.');continue}(typeof t.file!="string"||!t.file.endsWith(".json"))&&e.push("updatedScenarios[].file must be a .json filename."),typeof t.isNew!="boolean"&&e.push("updatedScenarios[].isNew must be boolean."),(!Array.isArray(t.capabilitiesCovered)||!Array.isArray(t.stepsToAdd))&&e.push("updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.")}return e}function Q(i,e){const t=[],r=new Set(i.capabilities||[]),b=new Set((e.newCapabilities||[]).map(s=>s.id)),u=new Set(e.removedCapabilities||[]);for(const s of b)u.has(s)&&t.push(`Capability "${s}" appears in both newCapabilities and removedCapabilities.`),r.has(s)&&t.push(`Capability "${s}" already exists in contract capabilities.`);for(const s of u)r.has(s)||t.push(`Capability "${s}" cannot be removed because it does not exist in contract.`);return t}function X({cwd:i,contract:e,capabilities:t,suggestion:r,version:b,quiet:u=!1}){const s=f.join(i,"inferno"),h=f.join(s,"contract.json"),v=f.join(s,"capabilities.json"),m=f.join(s,"CHANGELOG.md"),c=f.join(s,"scenarios"),y=r.newCapabilities||[],$=r.removedCapabilities||[],P=r.updatedScenarios||[],k=r.changelogEntry||"";let J=!1;const w=[],A=(n,a)=>w.push({filePath:n,content:a});if(y.length>0||$.length>0){const n=[...e.capabilities.filter(d=>!$.includes(d)),...y.map(d=>d.id)],a=Number(e.policyVersion||1)+1,l={...e,capabilities:n,policyVersion:a};A(h,JSON.stringify(l,null,2)+`
56
+ `),u||O(`contract.json updated \u2192 policyVersion: v${a}`),J=!0}if(y.length>0||$.length>0){const n=t?{...t}:{schemaVersion:1,capabilities:[]};n.capabilities=(n.capabilities||[]).filter(a=>!$.includes(a.id));for(const a of y)n.capabilities.find(l=>l.id===a.id)||n.capabilities.push({id:a.id,title:a.title,since:b});A(v,JSON.stringify(n,null,2)+`
57
+ `),u||O("capabilities.json updated")}for(const n of P){const a=f.join(c,n.file);let l;if(n.isNew||!p.existsSync(a))l={scenarioId:n.file.replace(".json",""),description:r.summary||"",capabilitiesCovered:n.capabilitiesCovered||[],steps:n.stepsToAdd||[]},A(a,JSON.stringify(l,null,2)+`
58
+ `),u||O(`Created scenario: ${S(n.file)}`);else{l=j(a);const d=new Set(l.capabilitiesCovered||[]);(n.capabilitiesCovered||[]).forEach(N=>d.add(N)),l.capabilitiesCovered=[...d],l.steps=[...l.steps||[],...n.stepsToAdd||[]],A(a,JSON.stringify(l,null,2)+`
59
+ `),u||O(`Updated scenario: ${S(n.file)}`)}J=!0}if(k&&p.existsSync(m)){let n=p.readFileSync(m,"utf8");/##\s+Unreleased/i.test(n)&&(n=n.replace(/(##\s+Unreleased[^\n]*\n)/i,`$1
60
+ ${k}
61
+ `),A(m,n),u||O("CHANGELOG.md updated"),J=!0)}const I=new Map;try{for(const n of w){p.existsSync(n.filePath)?I.set(n.filePath,p.readFileSync(n.filePath,"utf8")):I.set(n.filePath,null);const a=`${n.filePath}.tmp`;p.writeFileSync(a,n.content),p.renameSync(a,n.filePath)}}catch(n){for(const[a,l]of I.entries())l===null?p.existsSync(a)&&p.unlinkSync(a):p.writeFileSync(a,l);throw new Error(`Failed applying changes. Rolled back. Details: ${n.message}`)}return J}function q(i){const e=String(i||"").trim().replace(/^```json?\n?/,"").replace(/\n?```$/,"");return JSON.parse(e)}function ue(i){const e=f.join(i,"inferno"),t=f.join(e,"contract.json"),r=f.join(e,"capabilities.json"),b=f.join(e,"scenarios"),u=j(t),s=j(r),h=[];if(p.existsSync(b))for(const c of p.readdirSync(b).filter(y=>y.endsWith(".json"))){const y=j(f.join(b,c));y&&h.push({...y,_file:c})}let v="0.1.0";const m=f.join(i,"package.json");if(p.existsSync(m)){const c=j(m);c?.version&&(v=c.version)}return{contract:u,capabilities:s,scenarios:h,version:v}}function F(i){console.log(JSON.stringify(i,null,2))}function x(i,e,t){F({ok:!1,error:i,message:e,hint:t}),process.exit(1)}async function ce(){return new Promise(i=>{let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>i(e.trim())),setTimeout(()=>i(""),100)})}async function he(i){const e=process.cwd(),t=f.join(e,"inferno"),r=i.includes("--json"),b=i.includes("--apply"),u=i.indexOf("--response");let s=u!==-1?i[u+1]:null;r||ee("suggest"),p.existsSync(t)||(r&&x("inferno_not_found","inferno/ not found","Run: infernoflow init"),_("inferno/ not found","Run: infernoflow init"));const h=f.join(t,"contract.json"),v=f.join(t,"capabilities.json"),m=f.join(t,"scenarios"),c=j(h);c||(r&&x("contract_not_found","contract.json not found or invalid"),_("contract.json not found or invalid"));const y=j(v),$=[];if(p.existsSync(m))for(const o of p.readdirSync(m).filter(g=>g.endsWith(".json"))){const g=j(f.join(m,o));g&&$.push({...g,_file:o})}let P="0.1.0";const k=f.join(e,"package.json");if(p.existsSync(k)){const o=j(k);o?.version&&(P=o.version)}let w=i.filter(o=>!o.startsWith("-")).slice(1).join(" ");if(!w&&!r){const o=R.createInterface({input:process.stdin,output:process.stdout});console.log(E(" Describe what changed in your code (e.g. 'added email notifications'):")),w=await K(o,` ${S(">")} `),o.close(),console.log()}w||(r&&x("no_description","No description provided",'Usage: infernoflow suggest "what changed" --json'),_("No description provided",'Usage: infernoflow suggest "what changed"'));const A=re({description:w,contract:c,capabilities:y,scenarios:$}),I=ae(A,t);if(r){if(!s){const C=await ce();C&&(s=C)}if(!s){F({ok:!0,mode:"prompt",description:w,prompt:I,context:{policyId:c.policyId,policyVersion:c.policyVersion,capabilities:c.capabilities||[],scenarios:$.map(C=>C._file),version:P}});return}if(s.startsWith("@")){const C=s.slice(1);try{s=p.readFileSync(C,"utf8")}catch{x("file_not_found",`Cannot read response file: ${C}`)}}let o;try{o=q(s)}catch(C){x("parse_error","Could not parse AI response as JSON",C.message)}const g=B(o);g.length>0&&x("validation_error",g[0],g.join("; "));const T=Q(c,o);T.length>0&&x("conflict_error",T[0],T.join("; "));const z={summary:o.summary||"",newCapabilities:o.newCapabilities||[],removedCapabilities:o.removedCapabilities||[],updatedScenarios:o.updatedScenarios||[],changelogEntry:o.changelogEntry||""};if(!b){F({ok:!0,mode:"validate",description:w,changes:z,applied:!1});return}try{X({cwd:e,contract:c,capabilities:y,suggestion:o,version:P,quiet:!0}),F({ok:!0,mode:"apply",description:w,changes:z,applied:!0})}catch(C){x("apply_error",C.message)}return}U("Generated Prompt"),console.log(),console.log(E("\u2500".repeat(50))),console.log(I),console.log(E("\u2500".repeat(50))),console.log(),ie("Copy the prompt above and paste it into:"),console.log(` ${S("\u2022")} Claude \u2192 https://claude.ai`),console.log(` ${S("\u2022")} ChatGPT \u2192 https://chatgpt.com`),console.log(` ${S("\u2022")} Copilot, Cursor, or any AI you use`),console.log(),M("The AI will respond with a JSON object."),console.log();const n=R.createInterface({input:process.stdin,output:process.stdout});console.log(E(" Paste the AI's JSON response below, then press Enter twice:")),console.log();let a="",l=0;await new Promise(o=>{n.on("line",g=>{g.trim()===""?(l++,l>=2&&a.trim()&&o()):(l=0,a+=g+`
62
+ `)}),n.on("close",o)}),n.close();let d;try{d=q(a)}catch{_("Could not parse the AI response as JSON","Make sure you copied the full JSON response from the AI")}const N=B(d);N.length>0&&_("AI response schema is invalid",N[0]+(N.length>1?` (+${N.length-1} more)`:""));const D=Q(c,d);D.length>0&&_("AI response contains conflicting capability operations",D[0]+(D.length>1?` (+${D.length-1} more)`:"")),U("Proposed Changes"),console.log(),d.summary&&(console.log(` ${oe("Summary:")} ${d.summary}`),console.log());const L=d.newCapabilities||[],V=d.removedCapabilities||[],G=d.updatedScenarios||[];L.length===0&&V.length===0&&G.length===0&&(O("No capability changes detected \u2014 nothing to apply."),console.log(),process.exit(0)),L.length>0&&(console.log(` ${W("+")} New capabilities:`),L.forEach(o=>console.log(` ${W(o.id)} \u2014 ${E(o.title)}`)),console.log()),V.length>0&&(console.log(` ${Z("-")} Removed capabilities:`),V.forEach(o=>console.log(` ${Z(o)}`)),console.log()),G.length>0&&(console.log(` ${S("~")} Scenario updates:`),G.forEach(o=>{const g=o.isNew?W("[new]"):S("[update]");console.log(` ${g} ${o.file}`)}),console.log()),d.changelogEntry&&(console.log(` ${se("\u{1F4DD}")} Changelog: ${E(d.changelogEntry)}`),console.log());const H=R.createInterface({input:process.stdin,output:process.stdout}),Y=await K(H,` Apply these changes? ${E("(y/n)")} `);H.close(),console.log(),Y.toLowerCase()!=="y"&&Y.toLowerCase()!=="yes"&&(M("Cancelled \u2014 no changes made."),console.log(),process.exit(0)),U("Applying Changes"),console.log(),X({cwd:e,contract:c,capabilities:y,suggestion:d,version:P}),te("suggest complete!"),ne([S("infernoflow status")+" \u2014 verify the updated contract",S("infernoflow check")+" \u2014 validate everything"])}export{X as applyChanges,re as buildPrompt,Q as detectSuggestionConflicts,ue as loadSuggestContext,q as parseSuggestionJson,j as readJson,he as suggestCommand,B as validateSuggestion};