infernoflow 0.37.1 → 0.37.4

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 (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -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};