infernoflow 0.10.13 → 0.10.14
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 +68 -0
- package/dist/lib/ai/ideDetection.mjs +1 -0
- package/dist/lib/ai/localProvider.mjs +1 -0
- package/dist/lib/ai/providerRouter.mjs +1 -0
- package/dist/lib/commands/adopt.mjs +20 -0
- package/dist/lib/commands/check.mjs +3 -0
- package/dist/lib/commands/context.mjs +20 -0
- package/dist/lib/commands/docGate.mjs +2 -0
- package/dist/lib/commands/implement.mjs +7 -0
- package/dist/lib/commands/init.mjs +17 -0
- package/dist/lib/commands/installCursorHooks.mjs +1 -0
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -0
- package/dist/lib/commands/prImpact.mjs +2 -0
- package/dist/lib/commands/run.mjs +10 -0
- package/dist/lib/commands/status.mjs +4 -0
- package/dist/lib/commands/suggest.mjs +62 -0
- package/dist/lib/commands/syncAuto.mjs +1 -0
- package/dist/lib/cursorHooksInstall.mjs +1 -0
- package/dist/lib/draftToolingInstall.mjs +8 -0
- package/dist/lib/ui/output.mjs +6 -0
- package/dist/lib/ui/prompts.mjs +6 -0
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -0
- package/package.json +48 -44
- package/bin/infernoflow.mjs +0 -138
- package/lib/ai/ideDetection.mjs +0 -31
- package/lib/ai/localProvider.mjs +0 -88
- package/lib/ai/providerRouter.mjs +0 -73
- package/lib/commands/adopt.mjs +0 -768
- package/lib/commands/check.mjs +0 -179
- package/lib/commands/context.mjs +0 -164
- package/lib/commands/docGate.mjs +0 -81
- package/lib/commands/implement.mjs +0 -103
- package/lib/commands/init.mjs +0 -401
- package/lib/commands/installCursorHooks.mjs +0 -36
- package/lib/commands/installVsCodeCopilotHooks.mjs +0 -37
- package/lib/commands/prImpact.mjs +0 -157
- package/lib/commands/run.mjs +0 -338
- package/lib/commands/status.mjs +0 -172
- package/lib/commands/suggest.mjs +0 -501
- package/lib/commands/syncAuto.mjs +0 -96
- package/lib/cursorHooksInstall.mjs +0 -39
- package/lib/draftToolingInstall.mjs +0 -69
- package/lib/ui/output.mjs +0 -72
- package/lib/ui/prompts.mjs +0 -147
- package/lib/vsCodeCopilotHooksInstall.mjs +0 -42
- /package/{templates → dist/templates}/ci/github-inferno-check.yml +0 -0
- /package/{templates → dist/templates}/cursor/hooks/inferno-session-draft.mjs +0 -0
- /package/{templates → dist/templates}/cursor/hooks.json +0 -0
- /package/{templates → dist/templates}/github-hooks/infernoflow-drafts.json +0 -0
- /package/{templates → dist/templates}/inferno/CHANGELOG.md +0 -0
- /package/{templates → dist/templates}/inferno/capabilities.json +0 -0
- /package/{templates → dist/templates}/inferno/contract.json +0 -0
- /package/{templates → dist/templates}/inferno/scenarios/happy_path.json +0 -0
- /package/{templates → dist/templates}/scripts/inferno-doc-gate.mjs +0 -0
- /package/{templates → dist/templates}/scripts/inferno-install-hooks.mjs +0 -0
- /package/{templates → dist/templates}/scripts/inferno-promote-draft.mjs +0 -0
- /package/{templates → dist/templates}/scripts/inferno-vscode-copilot-hook.mjs +0 -0
package/lib/commands/suggest.mjs
DELETED
|
@@ -1,501 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
export function readJson(filePath) {
|
|
9
|
-
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
10
|
-
catch { return null; }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function ask(rl, question) {
|
|
14
|
-
return new Promise(resolve => {
|
|
15
|
-
rl.question(question, answer => resolve(answer.trim()));
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function toCapabilityId(str) {
|
|
20
|
-
// "send email" → "SendEmail", "send-email" → "SendEmail"
|
|
21
|
-
return str
|
|
22
|
-
.replace(/[-_]+/g, " ")
|
|
23
|
-
.split(" ")
|
|
24
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
25
|
-
.join("");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function buildPrompt({ description, contract, capabilities, scenarios }) {
|
|
29
|
-
const capsIds = contract.capabilities || [];
|
|
30
|
-
const capsDetail = (capabilities?.capabilities || [])
|
|
31
|
-
.map(c => ` - ${c.id}: ${c.title || c.id}`)
|
|
32
|
-
.join("\n");
|
|
33
|
-
|
|
34
|
-
const scenarioFiles = scenarios.map(s => {
|
|
35
|
-
const covered = (s.capabilitiesCovered || []).join(", ");
|
|
36
|
-
const steps = (s.steps || []).map(st => ` {action: "${st.action}", expect: "${st.expect}"}`).join("\n");
|
|
37
|
-
return ` File: ${s._file}\n capabilitiesCovered: [${covered}]\n steps:\n${steps}`;
|
|
38
|
-
}).join("\n\n");
|
|
39
|
-
|
|
40
|
-
return `You are a developer assistant for the infernoflow CLI tool.
|
|
41
|
-
|
|
42
|
-
Your job is to analyze a code change description and suggest updates to the infernoflow contract files.
|
|
43
|
-
|
|
44
|
-
## Current contract state
|
|
45
|
-
|
|
46
|
-
policyId: ${contract.policyId}
|
|
47
|
-
policyVersion: ${contract.policyVersion}
|
|
48
|
-
capabilities: [${capsIds.join(", ")}]
|
|
49
|
-
|
|
50
|
-
## Current capabilities registry
|
|
51
|
-
${capsDetail || " (none)"}
|
|
52
|
-
|
|
53
|
-
## Current scenarios
|
|
54
|
-
${scenarioFiles || " (none)"}
|
|
55
|
-
|
|
56
|
-
## Developer's description of what changed
|
|
57
|
-
"${description}"
|
|
58
|
-
|
|
59
|
-
## Your task
|
|
60
|
-
|
|
61
|
-
Respond with ONLY a valid JSON object (no markdown, no explanation) in this exact format:
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
"summary": "one-line summary of what changed",
|
|
65
|
-
"newCapabilities": [
|
|
66
|
-
{ "id": "CapabilityName", "title": "Human readable title", "reason": "why this is a new capability" }
|
|
67
|
-
],
|
|
68
|
-
"removedCapabilities": ["CapabilityId"],
|
|
69
|
-
"updatedScenarios": [
|
|
70
|
-
{
|
|
71
|
-
"file": "existing_scenario_filename.json or new_scenario_name.json",
|
|
72
|
-
"isNew": false,
|
|
73
|
-
"capabilitiesCovered": ["CapabilityId1", "CapabilityId2"],
|
|
74
|
-
"stepsToAdd": [
|
|
75
|
-
{ "action": "CapabilityId", "expect": "what should happen" }
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
],
|
|
79
|
-
"changelogEntry": "- Short description of the change for CHANGELOG.md"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
Rules:
|
|
83
|
-
- Only suggest capabilities that are genuinely new behaviors the system gains
|
|
84
|
-
- Capability IDs must be PascalCase (e.g. SendEmail, not send_email)
|
|
85
|
-
- If nothing changed capability-wise, return empty arrays
|
|
86
|
-
- changelogEntry should start with "- "
|
|
87
|
-
- Keep it minimal and accurate`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function validateSuggestion(suggestion) {
|
|
91
|
-
const errors = [];
|
|
92
|
-
if (!suggestion || typeof suggestion !== "object") {
|
|
93
|
-
return ["AI response must be a JSON object."];
|
|
94
|
-
}
|
|
95
|
-
if (suggestion.summary != null && typeof suggestion.summary !== "string") {
|
|
96
|
-
errors.push(`"summary" must be a string.`);
|
|
97
|
-
}
|
|
98
|
-
if (!Array.isArray(suggestion.newCapabilities)) {
|
|
99
|
-
errors.push(`"newCapabilities" must be an array.`);
|
|
100
|
-
}
|
|
101
|
-
if (!Array.isArray(suggestion.removedCapabilities)) {
|
|
102
|
-
errors.push(`"removedCapabilities" must be an array.`);
|
|
103
|
-
}
|
|
104
|
-
if (!Array.isArray(suggestion.updatedScenarios)) {
|
|
105
|
-
errors.push(`"updatedScenarios" must be an array.`);
|
|
106
|
-
}
|
|
107
|
-
if (suggestion.changelogEntry != null && typeof suggestion.changelogEntry !== "string") {
|
|
108
|
-
errors.push(`"changelogEntry" must be a string.`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const c of suggestion.newCapabilities || []) {
|
|
112
|
-
if (!c || typeof c !== "object") {
|
|
113
|
-
errors.push(`Each item in "newCapabilities" must be an object.`);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (typeof c.id !== "string" || !/^[A-Z][A-Za-z0-9]*$/.test(c.id)) {
|
|
117
|
-
errors.push(`newCapabilities[].id must be PascalCase (example: SendEmail).`);
|
|
118
|
-
}
|
|
119
|
-
if (typeof c.title !== "string" || !c.title.trim()) {
|
|
120
|
-
errors.push(`newCapabilities[].title must be a non-empty string.`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const id of suggestion.removedCapabilities || []) {
|
|
125
|
-
if (typeof id !== "string" || !id.trim()) {
|
|
126
|
-
errors.push(`removedCapabilities[] must contain non-empty strings.`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (const s of suggestion.updatedScenarios || []) {
|
|
131
|
-
if (!s || typeof s !== "object") {
|
|
132
|
-
errors.push(`Each item in "updatedScenarios" must be an object.`);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (typeof s.file !== "string" || !s.file.endsWith(".json")) {
|
|
136
|
-
errors.push(`updatedScenarios[].file must be a .json filename.`);
|
|
137
|
-
}
|
|
138
|
-
if (typeof s.isNew !== "boolean") {
|
|
139
|
-
errors.push(`updatedScenarios[].isNew must be boolean.`);
|
|
140
|
-
}
|
|
141
|
-
if (!Array.isArray(s.capabilitiesCovered) || !Array.isArray(s.stepsToAdd)) {
|
|
142
|
-
errors.push(`updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return errors;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function detectSuggestionConflicts(contract, suggestion) {
|
|
150
|
-
const issues = [];
|
|
151
|
-
const existing = new Set(contract.capabilities || []);
|
|
152
|
-
const newIds = new Set((suggestion.newCapabilities || []).map((c) => c.id));
|
|
153
|
-
const removed = new Set(suggestion.removedCapabilities || []);
|
|
154
|
-
|
|
155
|
-
for (const id of newIds) {
|
|
156
|
-
if (removed.has(id)) {
|
|
157
|
-
issues.push(`Capability "${id}" appears in both newCapabilities and removedCapabilities.`);
|
|
158
|
-
}
|
|
159
|
-
if (existing.has(id)) {
|
|
160
|
-
issues.push(`Capability "${id}" already exists in contract capabilities.`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
for (const id of removed) {
|
|
165
|
-
if (!existing.has(id)) {
|
|
166
|
-
issues.push(`Capability "${id}" cannot be removed because it does not exist in contract.`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return issues;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function applyChanges({ cwd, contract, capabilities, suggestion, version, quiet = false }) {
|
|
174
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
175
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
176
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
177
|
-
const changelogPath = path.join(infernoDir, "CHANGELOG.md");
|
|
178
|
-
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
179
|
-
|
|
180
|
-
const newCaps = suggestion.newCapabilities || [];
|
|
181
|
-
const removedCaps = suggestion.removedCapabilities || [];
|
|
182
|
-
const updatedScenarios = suggestion.updatedScenarios || [];
|
|
183
|
-
const changelogEntry = suggestion.changelogEntry || "";
|
|
184
|
-
|
|
185
|
-
let changed = false;
|
|
186
|
-
const writes = [];
|
|
187
|
-
const queueWrite = (filePath, content) => writes.push({ filePath, content });
|
|
188
|
-
|
|
189
|
-
// ── contract.json ─────────────────────────────────────────────────────────
|
|
190
|
-
if (newCaps.length > 0 || removedCaps.length > 0) {
|
|
191
|
-
const updatedCaps = [
|
|
192
|
-
...contract.capabilities.filter(c => !removedCaps.includes(c)),
|
|
193
|
-
...newCaps.map(c => c.id)
|
|
194
|
-
];
|
|
195
|
-
const nextVersion = Number(contract.policyVersion || 1) + 1;
|
|
196
|
-
const contractUpdated = { ...contract, capabilities: updatedCaps, policyVersion: nextVersion };
|
|
197
|
-
queueWrite(contractPath, JSON.stringify(contractUpdated, null, 2) + "\n");
|
|
198
|
-
if (!quiet) ok(`contract.json updated → policyVersion: v${nextVersion}`);
|
|
199
|
-
changed = true;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ── capabilities.json ─────────────────────────────────────────────────────
|
|
203
|
-
if (newCaps.length > 0 || removedCaps.length > 0) {
|
|
204
|
-
const reg = capabilities ? { ...capabilities } : { schemaVersion: 1, capabilities: [] };
|
|
205
|
-
reg.capabilities = (reg.capabilities || []).filter(c => !removedCaps.includes(c.id));
|
|
206
|
-
for (const nc of newCaps) {
|
|
207
|
-
if (!reg.capabilities.find(c => c.id === nc.id)) {
|
|
208
|
-
reg.capabilities.push({ id: nc.id, title: nc.title, since: version });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
queueWrite(capsPath, JSON.stringify(reg, null, 2) + "\n");
|
|
212
|
-
if (!quiet) ok(`capabilities.json updated`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ── scenarios ─────────────────────────────────────────────────────────────
|
|
216
|
-
for (const us of updatedScenarios) {
|
|
217
|
-
const filePath = path.join(scenariosDir, us.file);
|
|
218
|
-
let scenario;
|
|
219
|
-
|
|
220
|
-
if (us.isNew || !fs.existsSync(filePath)) {
|
|
221
|
-
scenario = {
|
|
222
|
-
scenarioId: us.file.replace(".json", ""),
|
|
223
|
-
description: suggestion.summary || "",
|
|
224
|
-
capabilitiesCovered: us.capabilitiesCovered || [],
|
|
225
|
-
steps: us.stepsToAdd || []
|
|
226
|
-
};
|
|
227
|
-
queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
|
|
228
|
-
if (!quiet) ok(`Created scenario: ${cyan(us.file)}`);
|
|
229
|
-
} else {
|
|
230
|
-
scenario = readJson(filePath);
|
|
231
|
-
const existingCaps = new Set(scenario.capabilitiesCovered || []);
|
|
232
|
-
(us.capabilitiesCovered || []).forEach(c => existingCaps.add(c));
|
|
233
|
-
scenario.capabilitiesCovered = [...existingCaps];
|
|
234
|
-
scenario.steps = [...(scenario.steps || []), ...(us.stepsToAdd || [])];
|
|
235
|
-
queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
|
|
236
|
-
if (!quiet) ok(`Updated scenario: ${cyan(us.file)}`);
|
|
237
|
-
}
|
|
238
|
-
changed = true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── CHANGELOG.md ──────────────────────────────────────────────────────────
|
|
242
|
-
if (changelogEntry && fs.existsSync(changelogPath)) {
|
|
243
|
-
let txt = fs.readFileSync(changelogPath, "utf8");
|
|
244
|
-
if (/##\s+Unreleased/i.test(txt)) {
|
|
245
|
-
txt = txt.replace(/(##\s+Unreleased[^\n]*\n)/i, `$1\n${changelogEntry}\n`);
|
|
246
|
-
queueWrite(changelogPath, txt);
|
|
247
|
-
if (!quiet) ok(`CHANGELOG.md updated`);
|
|
248
|
-
changed = true;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const backups = new Map();
|
|
253
|
-
try {
|
|
254
|
-
for (const write of writes) {
|
|
255
|
-
if (fs.existsSync(write.filePath)) {
|
|
256
|
-
backups.set(write.filePath, fs.readFileSync(write.filePath, "utf8"));
|
|
257
|
-
} else {
|
|
258
|
-
backups.set(write.filePath, null);
|
|
259
|
-
}
|
|
260
|
-
const tmpPath = `${write.filePath}.tmp`;
|
|
261
|
-
fs.writeFileSync(tmpPath, write.content);
|
|
262
|
-
fs.renameSync(tmpPath, write.filePath);
|
|
263
|
-
}
|
|
264
|
-
} catch (err) {
|
|
265
|
-
for (const [filePath, content] of backups.entries()) {
|
|
266
|
-
if (content === null) {
|
|
267
|
-
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
268
|
-
} else {
|
|
269
|
-
fs.writeFileSync(filePath, content);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
throw new Error(`Failed applying changes. Rolled back. Details: ${err.message}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return changed;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function parseSuggestionJson(rawInput) {
|
|
279
|
-
const clean = String(rawInput || "").trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
|
|
280
|
-
return JSON.parse(clean);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function loadSuggestContext(cwd) {
|
|
284
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
285
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
286
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
287
|
-
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
288
|
-
|
|
289
|
-
const contract = readJson(contractPath);
|
|
290
|
-
const capabilities = readJson(capsPath);
|
|
291
|
-
const scenarios = [];
|
|
292
|
-
if (fs.existsSync(scenariosDir)) {
|
|
293
|
-
for (const f of fs.readdirSync(scenariosDir).filter((name) => name.endsWith(".json"))) {
|
|
294
|
-
const s = readJson(path.join(scenariosDir, f));
|
|
295
|
-
if (s) scenarios.push({ ...s, _file: f });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let version = "0.1.0";
|
|
300
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
301
|
-
if (fs.existsSync(pkgPath)) {
|
|
302
|
-
const pkg = readJson(pkgPath);
|
|
303
|
-
if (pkg?.version) version = pkg.version;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return { contract, capabilities, scenarios, version };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
310
|
-
|
|
311
|
-
export async function suggestCommand(args) {
|
|
312
|
-
const cwd = process.cwd();
|
|
313
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
314
|
-
|
|
315
|
-
header("suggest");
|
|
316
|
-
|
|
317
|
-
// ── Check inferno/ exists ─────────────────────────────────────────────────
|
|
318
|
-
if (!fs.existsSync(infernoDir)) {
|
|
319
|
-
errorAndExit("inferno/ not found", "Run: infernoflow init");
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
323
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
324
|
-
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
325
|
-
|
|
326
|
-
const contract = readJson(contractPath);
|
|
327
|
-
if (!contract) errorAndExit("contract.json not found or invalid");
|
|
328
|
-
|
|
329
|
-
const capabilities = readJson(capsPath);
|
|
330
|
-
|
|
331
|
-
// Load scenarios
|
|
332
|
-
const scenarios = [];
|
|
333
|
-
if (fs.existsSync(scenariosDir)) {
|
|
334
|
-
for (const f of fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"))) {
|
|
335
|
-
const s = readJson(path.join(scenariosDir, f));
|
|
336
|
-
if (s) scenarios.push({ ...s, _file: f });
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Get version from package.json
|
|
341
|
-
let version = "0.1.0";
|
|
342
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
343
|
-
if (fs.existsSync(pkgPath)) {
|
|
344
|
-
const pkg = readJson(pkgPath);
|
|
345
|
-
if (pkg?.version) version = pkg.version;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ── Get description from args or prompt ───────────────────────────────────
|
|
349
|
-
const descArg = args.filter(a => !a.startsWith("-")).slice(1).join(" ");
|
|
350
|
-
let description = descArg;
|
|
351
|
-
|
|
352
|
-
if (!description) {
|
|
353
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
354
|
-
console.log(gray(" Describe what changed in your code (e.g. 'added email notifications'):"));
|
|
355
|
-
description = await ask(rl, ` ${cyan(">")} `);
|
|
356
|
-
rl.close();
|
|
357
|
-
console.log();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!description) {
|
|
361
|
-
errorAndExit("No description provided", "Usage: infernoflow suggest \"what changed\"");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// ── Build prompt ──────────────────────────────────────────────────────────
|
|
365
|
-
const prompt = buildPrompt({ description, contract, capabilities, scenarios });
|
|
366
|
-
|
|
367
|
-
// ── Show prompt + instructions ────────────────────────────────────────────
|
|
368
|
-
section("Generated Prompt");
|
|
369
|
-
console.log();
|
|
370
|
-
console.log(gray("─".repeat(50)));
|
|
371
|
-
console.log(prompt);
|
|
372
|
-
console.log(gray("─".repeat(50)));
|
|
373
|
-
console.log();
|
|
374
|
-
|
|
375
|
-
info("Copy the prompt above and paste it into:");
|
|
376
|
-
console.log(` ${cyan("•")} Claude → https://claude.ai`);
|
|
377
|
-
console.log(` ${cyan("•")} ChatGPT → https://chatgpt.com`);
|
|
378
|
-
console.log(` ${cyan("•")} Copilot, Cursor, or any AI you use`);
|
|
379
|
-
console.log();
|
|
380
|
-
warn("The AI will respond with a JSON object.");
|
|
381
|
-
console.log();
|
|
382
|
-
|
|
383
|
-
// ── Get AI response ───────────────────────────────────────────────────────
|
|
384
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
385
|
-
console.log(gray(" Paste the AI's JSON response below, then press Enter twice:"));
|
|
386
|
-
console.log();
|
|
387
|
-
|
|
388
|
-
let jsonInput = "";
|
|
389
|
-
let emptyLines = 0;
|
|
390
|
-
|
|
391
|
-
await new Promise(resolve => {
|
|
392
|
-
rl2.on("line", line => {
|
|
393
|
-
if (line.trim() === "") {
|
|
394
|
-
emptyLines++;
|
|
395
|
-
if (emptyLines >= 2 && jsonInput.trim()) resolve();
|
|
396
|
-
} else {
|
|
397
|
-
emptyLines = 0;
|
|
398
|
-
jsonInput += line + "\n";
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
rl2.on("close", resolve);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
rl2.close();
|
|
405
|
-
|
|
406
|
-
// ── Parse response ────────────────────────────────────────────────────────
|
|
407
|
-
let suggestion;
|
|
408
|
-
try {
|
|
409
|
-
suggestion = parseSuggestionJson(jsonInput);
|
|
410
|
-
} catch {
|
|
411
|
-
errorAndExit(
|
|
412
|
-
"Could not parse the AI response as JSON",
|
|
413
|
-
"Make sure you copied the full JSON response from the AI"
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const validationErrors = validateSuggestion(suggestion);
|
|
418
|
-
if (validationErrors.length > 0) {
|
|
419
|
-
errorAndExit(
|
|
420
|
-
"AI response schema is invalid",
|
|
421
|
-
validationErrors[0] + (validationErrors.length > 1 ? ` (+${validationErrors.length - 1} more)` : "")
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
const conflictErrors = detectSuggestionConflicts(contract, suggestion);
|
|
425
|
-
if (conflictErrors.length > 0) {
|
|
426
|
-
errorAndExit(
|
|
427
|
-
"AI response contains conflicting capability operations",
|
|
428
|
-
conflictErrors[0] + (conflictErrors.length > 1 ? ` (+${conflictErrors.length - 1} more)` : "")
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// ── Preview ───────────────────────────────────────────────────────────────
|
|
433
|
-
section("Proposed Changes");
|
|
434
|
-
console.log();
|
|
435
|
-
|
|
436
|
-
if (suggestion.summary) {
|
|
437
|
-
console.log(` ${bold("Summary:")} ${suggestion.summary}`);
|
|
438
|
-
console.log();
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const newCaps = suggestion.newCapabilities || [];
|
|
442
|
-
const removedCaps = suggestion.removedCapabilities || [];
|
|
443
|
-
const updatedScenarios = suggestion.updatedScenarios || [];
|
|
444
|
-
|
|
445
|
-
if (newCaps.length === 0 && removedCaps.length === 0 && updatedScenarios.length === 0) {
|
|
446
|
-
ok("No capability changes detected — nothing to apply.");
|
|
447
|
-
console.log();
|
|
448
|
-
process.exit(0);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (newCaps.length > 0) {
|
|
452
|
-
console.log(` ${green("+")} New capabilities:`);
|
|
453
|
-
newCaps.forEach(c => console.log(` ${green(c.id)} — ${gray(c.title)}`));
|
|
454
|
-
console.log();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (removedCaps.length > 0) {
|
|
458
|
-
console.log(` ${red("-")} Removed capabilities:`);
|
|
459
|
-
removedCaps.forEach(c => console.log(` ${red(c)}`));
|
|
460
|
-
console.log();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (updatedScenarios.length > 0) {
|
|
464
|
-
console.log(` ${cyan("~")} Scenario updates:`);
|
|
465
|
-
updatedScenarios.forEach(s => {
|
|
466
|
-
const tag = s.isNew ? green("[new]") : cyan("[update]");
|
|
467
|
-
console.log(` ${tag} ${s.file}`);
|
|
468
|
-
});
|
|
469
|
-
console.log();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (suggestion.changelogEntry) {
|
|
473
|
-
console.log(` ${yellow("📝")} Changelog: ${gray(suggestion.changelogEntry)}`);
|
|
474
|
-
console.log();
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ── Confirm ───────────────────────────────────────────────────────────────
|
|
478
|
-
const rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
479
|
-
const answer = await ask(rl3, ` Apply these changes? ${gray("(y/n)")} `);
|
|
480
|
-
rl3.close();
|
|
481
|
-
console.log();
|
|
482
|
-
|
|
483
|
-
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
484
|
-
warn("Cancelled — no changes made.");
|
|
485
|
-
console.log();
|
|
486
|
-
process.exit(0);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ── Apply ─────────────────────────────────────────────────────────────────
|
|
490
|
-
section("Applying Changes");
|
|
491
|
-
console.log();
|
|
492
|
-
|
|
493
|
-
applyChanges({ cwd, contract, capabilities, suggestion, version });
|
|
494
|
-
|
|
495
|
-
done("suggest complete!");
|
|
496
|
-
|
|
497
|
-
nextSteps([
|
|
498
|
-
cyan("infernoflow status") + " — verify the updated contract",
|
|
499
|
-
cyan("infernoflow check") + " — validate everything",
|
|
500
|
-
]);
|
|
501
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { header, section, ok, warn, yellow, gray } from "../ui/output.mjs";
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
|
|
9
|
-
|
|
10
|
-
function runCliJson(args) {
|
|
11
|
-
const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
12
|
-
return JSON.parse(out);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function tryRunCliJson(args) {
|
|
16
|
-
try {
|
|
17
|
-
return { ok: true, data: runCliJson(args) };
|
|
18
|
-
} catch (err) {
|
|
19
|
-
const stdout = err?.stdout?.toString?.() || "";
|
|
20
|
-
try {
|
|
21
|
-
return { ok: false, data: JSON.parse(stdout) };
|
|
22
|
-
} catch {
|
|
23
|
-
return { ok: false, data: { ok: false, errors: ["command_failed"] } };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function syncCommand(args = []) {
|
|
29
|
-
const auto = args.includes("--auto");
|
|
30
|
-
const asJson = args.includes("--json");
|
|
31
|
-
const dryRun = args.includes("--dry-run");
|
|
32
|
-
|
|
33
|
-
if (!auto) {
|
|
34
|
-
const payload = { ok: false, error: "missing_required_flag", hint: "Use: infernoflow sync --auto" };
|
|
35
|
-
if (asJson) {
|
|
36
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
header("sync");
|
|
40
|
-
warn("missing --auto flag");
|
|
41
|
-
console.log(` ${yellow("→")} infernoflow sync --auto`);
|
|
42
|
-
console.log();
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const impact = tryRunCliJson(["pr-impact", "--json"]);
|
|
47
|
-
const needsSync = !impact.data?.ok;
|
|
48
|
-
const confidence = impact.data?.confidence || "low";
|
|
49
|
-
const policyDecision = confidence === "high" ? "auto" : confidence === "medium" ? "ask" : "block";
|
|
50
|
-
const actions = needsSync
|
|
51
|
-
? ["Generate inferno update proposal (suggest)", "Review changes", "Validate with check --json"]
|
|
52
|
-
: ["No inferno drift detected", "Validate with check --json"];
|
|
53
|
-
|
|
54
|
-
const check = tryRunCliJson(["check", "--json"]);
|
|
55
|
-
const payload = {
|
|
56
|
-
ok: impact.ok && check.ok && !!check.data?.ok,
|
|
57
|
-
mode: "auto-skeleton",
|
|
58
|
-
dryRun,
|
|
59
|
-
needsSync,
|
|
60
|
-
didApply: false,
|
|
61
|
-
confidence,
|
|
62
|
-
policyDecision,
|
|
63
|
-
actions,
|
|
64
|
-
prImpact: impact.data,
|
|
65
|
-
postCheck: check.data,
|
|
66
|
-
reasonCodes: [
|
|
67
|
-
...(needsSync ? ["DRIFT_DETECTED"] : ["NO_DRIFT"]),
|
|
68
|
-
`POLICY_${policyDecision.toUpperCase()}`,
|
|
69
|
-
...(policyDecision === "auto" ? ["AUTO_APPLY_DISABLED_IN_SKELETON"] : []),
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
if (asJson) {
|
|
74
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
75
|
-
process.exit(payload.ok ? 0 : 1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
header("sync --auto");
|
|
79
|
-
section("State");
|
|
80
|
-
if (needsSync) warn("Inferno drift detected");
|
|
81
|
-
else ok("No inferno drift detected");
|
|
82
|
-
ok(`Confidence: ${gray(confidence)}`);
|
|
83
|
-
ok(`Policy decision: ${gray(policyDecision)}`);
|
|
84
|
-
ok(`Apply mode: ${gray("skeleton (no file writes)")}`);
|
|
85
|
-
if (dryRun) ok("Dry run enabled");
|
|
86
|
-
|
|
87
|
-
section("Plan");
|
|
88
|
-
actions.forEach((a) => console.log(` ${yellow("→")} ${a}`));
|
|
89
|
-
|
|
90
|
-
section("Validation");
|
|
91
|
-
if (check.ok && check.data?.ok) ok("Post-check passed");
|
|
92
|
-
else warn("Post-check failed; see infernoflow check --json");
|
|
93
|
-
console.log();
|
|
94
|
-
process.exit(payload.ok ? 0 : 1);
|
|
95
|
-
}
|
|
96
|
-
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { installInfernoDraftTooling } from "./draftToolingInstall.mjs";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @param {object} opts
|
|
7
|
-
* @param {string} opts.cwd
|
|
8
|
-
* @param {string} opts.templatesRoot
|
|
9
|
-
* @param {boolean} opts.force
|
|
10
|
-
* @param {boolean} opts.silent
|
|
11
|
-
* @param {(msg: string) => void} [opts.logOk]
|
|
12
|
-
* @param {(msg: string) => void} [opts.logWarn]
|
|
13
|
-
*/
|
|
14
|
-
export function installCursorHooksArtifacts(opts) {
|
|
15
|
-
const { cwd, templatesRoot, force, silent } = opts;
|
|
16
|
-
const logOk = opts.logOk || (() => {});
|
|
17
|
-
const logWarn = opts.logWarn || (() => {});
|
|
18
|
-
|
|
19
|
-
function copyFile(src, dst) {
|
|
20
|
-
if (fs.existsSync(dst) && !force) {
|
|
21
|
-
if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
25
|
-
fs.copyFileSync(src, dst);
|
|
26
|
-
if (!silent) logOk("Created: " + path.relative(cwd, dst));
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
installInfernoDraftTooling({ cwd, templatesRoot, force, silent, logOk, logWarn });
|
|
31
|
-
|
|
32
|
-
const srcHooksJson = path.join(templatesRoot, "cursor", "hooks.json");
|
|
33
|
-
const dstHooksJson = path.join(cwd, ".cursor", "hooks.json");
|
|
34
|
-
const srcHook = path.join(templatesRoot, "cursor", "hooks", "inferno-session-draft.mjs");
|
|
35
|
-
const dstHook = path.join(cwd, ".cursor", "hooks", "inferno-session-draft.mjs");
|
|
36
|
-
|
|
37
|
-
copyFile(srcHooksJson, dstHooksJson);
|
|
38
|
-
copyFile(srcHook, dstHook);
|
|
39
|
-
}
|