infernoflow 0.32.7 → 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.
- package/dist/bin/infernoflow.mjs +84 -255
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -320
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +23 -475
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +5 -558
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,61 +1,28 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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: ${
|
|
48
|
-
policyVersion: ${
|
|
49
|
-
capabilities: [${
|
|
14
|
+
policyId: ${e.policyId}
|
|
15
|
+
policyVersion: ${e.policyVersion}
|
|
16
|
+
capabilities: [${b.join(", ")}]
|
|
50
17
|
|
|
51
18
|
## Current capabilities registry
|
|
52
|
-
${
|
|
19
|
+
${u||" (none)"}
|
|
53
20
|
|
|
54
21
|
## Current scenarios
|
|
55
|
-
${
|
|
22
|
+
${s||" (none)"}
|
|
56
23
|
|
|
57
24
|
## Developer's description of what changed
|
|
58
|
-
"${
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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};
|