infernoflow 0.34.0 → 0.34.1

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.
@@ -73,6 +73,7 @@ const COMMAND_DESCRIPTIONS = {
73
73
  stats: "Value dashboard — session memory, tokens injected per session, coverage %, estimated savings",
74
74
  ask: "Query session memory — search gotchas, decisions, and failed attempts by keyword or type",
75
75
  recap: "End-of-session summary — what was captured, what git changes weren't logged, session health score",
76
+ uninstall: "Remove infernoflow from a project — inferno/, CLAUDE.md, MCP server, git hooks (--dry-run to preview)",
76
77
  };
77
78
 
78
79
  const COMMAND_HANDLERS = {
@@ -139,6 +140,7 @@ const COMMAND_HANDLERS = {
139
140
  stats: async (args) => (await import("../lib/commands/stats.mjs")).statsCommand(args),
140
141
  ask: async (args) => (await import("../lib/commands/ask.mjs")).askCommand(args),
141
142
  recap: async (args) => (await import("../lib/commands/recap.mjs")).recapCommand(args),
143
+ uninstall: async (args) => (await import("../lib/commands/uninstall.mjs")).uninstallCommand(args),
142
144
  };
143
145
 
144
146
  function formatCommandsHelp() {
@@ -0,0 +1,375 @@
1
+ /**
2
+ * infernoflow uninstall
3
+ *
4
+ * Removes everything infernoflow installed from a project.
5
+ * The inverse of `infernoflow setup`.
6
+ *
7
+ * What it removes:
8
+ * - inferno/ — contract, capabilities, session memory, HANDOFF.md
9
+ * - CLAUDE.md — auto-behavior instruction file
10
+ * - .claude/ — settings.json with pre-approved tools
11
+ * - .cursor/inferno-mcp-server.mjs — MCP server file
12
+ * - .cursor/mcp.json — infernoflow entry (other entries preserved)
13
+ * - ~/.claude.json — infernoflow mcpServers entry (other entries preserved)
14
+ * - .git/hooks/post-commit / pre-push — infernoflow sections (other hooks preserved)
15
+ *
16
+ * Flags:
17
+ * --dry-run Preview what would be removed without touching anything
18
+ * --keep-memory Preserve inferno/sessions.jsonl (your session logs)
19
+ * --keep-inferno Preserve the entire inferno/ folder
20
+ * --yes / -y Skip confirmation prompt
21
+ * --json Machine-readable output
22
+ *
23
+ * Usage:
24
+ * infernoflow uninstall Interactive — shows plan, asks to confirm
25
+ * infernoflow uninstall --dry-run Show what would be removed
26
+ * infernoflow uninstall --yes Remove without prompting
27
+ * infernoflow uninstall --keep-memory Remove setup but keep session logs
28
+ */
29
+
30
+ import * as fs from "node:fs";
31
+ import * as path from "node:path";
32
+ import * as os from "node:os";
33
+ import * as readline from "node:readline";
34
+ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
35
+
36
+ const INFERNO_DIR = "inferno";
37
+ const CLAUDE_MD = "CLAUDE.md";
38
+ const CLAUDE_DIR = ".claude";
39
+ const CURSOR_DIR = ".cursor";
40
+ const MCP_SERVER = path.join(CURSOR_DIR, "inferno-mcp-server.mjs");
41
+ const CURSOR_MCP = path.join(CURSOR_DIR, "mcp.json");
42
+ const CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
43
+ const GIT_HOOKS = [".git/hooks/post-commit", ".git/hooks/pre-push"];
44
+ const INFERNO_MARKER = "# infernoflow";
45
+
46
+ // ── helpers ──────────────────────────────────────────────────────────────────
47
+
48
+ function exists(p) { return fs.existsSync(p); }
49
+ function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
50
+
51
+ function readFile(f) {
52
+ try { return fs.readFileSync(f, "utf8"); } catch { return null; }
53
+ }
54
+
55
+ function prompt(question) {
56
+ return new Promise(resolve => {
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
+ rl.question(question, ans => { rl.close(); resolve(ans); });
59
+ });
60
+ }
61
+
62
+ // ── planners — compute what would be removed ─────────────────────────────────
63
+
64
+ function planInfernoDir(cwd, keepMemory, keepInferno) {
65
+ const items = [];
66
+ const dir = path.join(cwd, INFERNO_DIR);
67
+ if (!exists(dir)) return items;
68
+
69
+ if (keepInferno) {
70
+ items.push({ type: "skip", path: INFERNO_DIR, reason: "--keep-inferno" });
71
+ return items;
72
+ }
73
+
74
+ if (keepMemory) {
75
+ // Remove everything except sessions.jsonl
76
+ const files = fs.readdirSync(dir);
77
+ for (const f of files) {
78
+ if (f === "sessions.jsonl") {
79
+ items.push({ type: "skip", path: path.join(INFERNO_DIR, f), reason: "--keep-memory" });
80
+ } else {
81
+ const full = path.join(dir, f);
82
+ const stat = fs.statSync(full);
83
+ if (stat.isDirectory()) {
84
+ items.push({ type: "rmdir", path: path.join(INFERNO_DIR, f) });
85
+ } else {
86
+ items.push({ type: "rm", path: path.join(INFERNO_DIR, f) });
87
+ }
88
+ }
89
+ }
90
+ } else {
91
+ items.push({ type: "rmdir", path: INFERNO_DIR });
92
+ }
93
+
94
+ return items;
95
+ }
96
+
97
+ function planClaudeMd(cwd) {
98
+ const p = path.join(cwd, CLAUDE_MD);
99
+ if (!exists(p)) return [];
100
+ return [{ type: "rm", path: CLAUDE_MD }];
101
+ }
102
+
103
+ function planClaudeDir(cwd) {
104
+ const items = [];
105
+ const settingsFile = path.join(cwd, CLAUDE_DIR, "settings.json");
106
+ if (!exists(settingsFile)) return items;
107
+
108
+ const settings = readJSON(settingsFile);
109
+ const hasInfernoTools = settings?.tools?.some?.(t => t.startsWith?.("mcp__infernoflow"));
110
+ const hasOtherContent = settings && Object.keys(settings).some(k => {
111
+ if (k === "tools") {
112
+ return (settings.tools || []).some(t => !t.startsWith("mcp__infernoflow"));
113
+ }
114
+ return k !== "tools";
115
+ });
116
+
117
+ if (hasInfernoTools && !hasOtherContent) {
118
+ items.push({ type: "rm", path: path.join(CLAUDE_DIR, "settings.json"), desc: "auto-approved tools" });
119
+ } else if (hasInfernoTools) {
120
+ items.push({ type: "edit", path: path.join(CLAUDE_DIR, "settings.json"), desc: "remove infernoflow tools (preserve other content)" });
121
+ }
122
+ return items;
123
+ }
124
+
125
+ function planCursorMcpServer(cwd) {
126
+ const p = path.join(cwd, MCP_SERVER);
127
+ if (!exists(p)) return [];
128
+ return [{ type: "rm", path: MCP_SERVER }];
129
+ }
130
+
131
+ function planCursorMcpJson(cwd) {
132
+ const p = path.join(cwd, CURSOR_MCP);
133
+ if (!exists(p)) return [];
134
+ const cfg = readJSON(p);
135
+ if (!cfg?.mcpServers?.infernoflow) return [];
136
+ const otherKeys = Object.keys(cfg.mcpServers || {}).filter(k => k !== "infernoflow");
137
+ if (otherKeys.length === 0 && Object.keys(cfg).length === 1) {
138
+ return [{ type: "rm", path: CURSOR_MCP, desc: "infernoflow-only file" }];
139
+ }
140
+ return [{ type: "edit", path: CURSOR_MCP, desc: 'remove "infernoflow" key (preserve other servers)' }];
141
+ }
142
+
143
+ function planClaudeJson() {
144
+ if (!exists(CLAUDE_JSON)) return [];
145
+ const cfg = readJSON(CLAUDE_JSON);
146
+ if (!cfg?.mcpServers?.infernoflow) return [];
147
+ return [{ type: "edit", path: "~/.claude.json", desc: 'remove "infernoflow" MCP entry (preserve other entries)', _realPath: CLAUDE_JSON }];
148
+ }
149
+
150
+ function planGitHooks(cwd) {
151
+ const items = [];
152
+ for (const hookRel of GIT_HOOKS) {
153
+ const hookPath = path.join(cwd, hookRel);
154
+ if (!exists(hookPath)) continue;
155
+ const content = readFile(hookPath);
156
+ if (!content?.includes(INFERNO_MARKER)) continue;
157
+
158
+ const lines = content.split("\n");
159
+ const markerIdx = lines.findIndex(l => l.includes(INFERNO_MARKER));
160
+ const beforeMarker = lines.slice(0, markerIdx).join("\n").trim();
161
+
162
+ if (!beforeMarker || beforeMarker === "#!/bin/sh" || beforeMarker === "#!/bin/bash") {
163
+ items.push({ type: "rm", path: hookRel, desc: "infernoflow-only hook" });
164
+ } else {
165
+ items.push({ type: "edit", path: hookRel, desc: "remove infernoflow section (preserve existing hooks)" });
166
+ }
167
+ }
168
+ return items;
169
+ }
170
+
171
+ // ── executors ─────────────────────────────────────────────────────────────────
172
+
173
+ function removeInfernoDir(cwd, plan, dryRun) {
174
+ for (const item of plan) {
175
+ if (item.type === "skip") continue;
176
+ const full = path.join(cwd, item.path);
177
+ if (dryRun) continue;
178
+ try {
179
+ if (item.type === "rmdir") {
180
+ fs.rmSync(full, { recursive: true, force: true });
181
+ } else {
182
+ fs.unlinkSync(full);
183
+ }
184
+ } catch {}
185
+ }
186
+ }
187
+
188
+ function removeClaudeMd(cwd, dryRun) {
189
+ if (dryRun) return;
190
+ try { fs.unlinkSync(path.join(cwd, CLAUDE_MD)); } catch {}
191
+ }
192
+
193
+ function removeClaudeDir(cwd, plan, dryRun) {
194
+ const settingsPath = path.join(cwd, CLAUDE_DIR, "settings.json");
195
+ for (const item of plan) {
196
+ if (dryRun) continue;
197
+ if (item.type === "rm") {
198
+ try { fs.unlinkSync(path.join(cwd, item.path)); } catch {}
199
+ } else if (item.type === "edit") {
200
+ try {
201
+ const cfg = readJSON(settingsPath);
202
+ if (cfg?.tools) {
203
+ cfg.tools = cfg.tools.filter(t => !t.startsWith("mcp__infernoflow"));
204
+ }
205
+ fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
206
+ } catch {}
207
+ }
208
+ }
209
+ }
210
+
211
+ function removeCursorMcpServer(cwd, plan, dryRun) {
212
+ if (dryRun) return;
213
+ for (const item of plan) {
214
+ if (item.type === "rm") {
215
+ try { fs.unlinkSync(path.join(cwd, MCP_SERVER)); } catch {}
216
+ }
217
+ }
218
+ }
219
+
220
+ function removeCursorMcpJson(cwd, plan, dryRun) {
221
+ const mcpPath = path.join(cwd, CURSOR_MCP);
222
+ for (const item of plan) {
223
+ if (dryRun) continue;
224
+ if (item.type === "rm") {
225
+ try { fs.unlinkSync(mcpPath); } catch {}
226
+ } else if (item.type === "edit") {
227
+ try {
228
+ const cfg = readJSON(mcpPath);
229
+ if (cfg?.mcpServers?.infernoflow) delete cfg.mcpServers.infernoflow;
230
+ fs.writeFileSync(mcpPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
231
+ } catch {}
232
+ }
233
+ }
234
+ }
235
+
236
+ function removeClaudeJson(plan, dryRun) {
237
+ for (const item of plan) {
238
+ if (dryRun) continue;
239
+ const p = item._realPath || CLAUDE_JSON;
240
+ if (item.type === "edit") {
241
+ try {
242
+ const cfg = readJSON(p);
243
+ if (cfg?.mcpServers?.infernoflow) delete cfg.mcpServers.infernoflow;
244
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
245
+ } catch {}
246
+ }
247
+ }
248
+ }
249
+
250
+ function removeGitHooks(cwd, plan, dryRun) {
251
+ for (const item of plan) {
252
+ const hookPath = path.join(cwd, item.path);
253
+ if (dryRun) continue;
254
+ if (item.type === "rm") {
255
+ try { fs.unlinkSync(hookPath); } catch {}
256
+ } else if (item.type === "edit") {
257
+ try {
258
+ const content = readFile(hookPath);
259
+ const lines = content.split("\n");
260
+ const markerIdx = lines.findIndex(l => l.includes(INFERNO_MARKER));
261
+ const preserved = lines.slice(0, markerIdx).join("\n").trimEnd();
262
+ fs.writeFileSync(hookPath, preserved + "\n", "utf8");
263
+ } catch {}
264
+ }
265
+ }
266
+ }
267
+
268
+ // ── entry point ───────────────────────────────────────────────────────────────
269
+
270
+ export async function uninstallCommand(args = []) {
271
+ const has = f => args.includes(f);
272
+ const dryRun = has("--dry-run") || has("--dry");
273
+ const keepMem = has("--keep-memory");
274
+ const keepInf = has("--keep-inferno");
275
+ const skipPrompt= has("--yes") || has("-y");
276
+ const jsonMode = has("--json");
277
+
278
+ const cwd = process.cwd();
279
+
280
+ // ── Build the full removal plan ──────────────────────────────────────────
281
+ const plan = {
282
+ infernoDir: planInfernoDir(cwd, keepMem, keepInf),
283
+ claudeMd: planClaudeMd(cwd),
284
+ claudeDir: planClaudeDir(cwd),
285
+ cursorMcpServer: planCursorMcpServer(cwd),
286
+ cursorMcpJson: planCursorMcpJson(cwd),
287
+ claudeJson: planClaudeJson(),
288
+ gitHooks: planGitHooks(cwd),
289
+ };
290
+
291
+ const allItems = Object.values(plan).flat();
292
+ const actionItems = allItems.filter(i => i.type !== "skip");
293
+
294
+ if (jsonMode) {
295
+ console.log(JSON.stringify({ dryRun, keepMemory: keepMem, keepInferno: keepInf, plan, actionCount: actionItems.length }, null, 2));
296
+ return;
297
+ }
298
+
299
+ const SEP = gray(" " + "─".repeat(52));
300
+
301
+ console.log();
302
+ console.log(" " + bold("🔥 infernoflow uninstall"));
303
+ if (dryRun) console.log(yellow(" DRY RUN — nothing will be changed"));
304
+ console.log(SEP);
305
+
306
+ if (actionItems.length === 0) {
307
+ console.log();
308
+ console.log(green(" ✔ Nothing to remove — infernoflow is not installed in this project"));
309
+ console.log();
310
+ return;
311
+ }
312
+
313
+ // ── Print the plan ───────────────────────────────────────────────────────
314
+ console.log();
315
+ console.log(" " + bold("Will remove:"));
316
+ console.log();
317
+
318
+ const typeIcon = { rm: red(" ✖"), rmdir: red(" ✖"), edit: yellow(" ~"), skip: gray(" ·") };
319
+
320
+ for (const item of allItems) {
321
+ const icon = typeIcon[item.type] || " ?";
322
+ const label = item.desc ? gray(` (${item.desc})`) : "";
323
+ console.log(`${icon} ${item.path}${label}`);
324
+ }
325
+
326
+ if (keepMem) {
327
+ console.log();
328
+ console.log(gray(" ℹ inferno/sessions.jsonl will be preserved (--keep-memory)"));
329
+ }
330
+
331
+ console.log();
332
+
333
+ // ── Confirm ──────────────────────────────────────────────────────────────
334
+ if (!dryRun && !skipPrompt) {
335
+ const ans = await prompt(" Continue? " + gray("[y/N] ") );
336
+ if (!ans.trim().toLowerCase().startsWith("y")) {
337
+ console.log(gray("\n Aborted — nothing changed.\n"));
338
+ return;
339
+ }
340
+ console.log();
341
+ }
342
+
343
+ if (dryRun) {
344
+ console.log(gray(" ↑ Dry run complete — run without --dry-run to apply\n"));
345
+ return;
346
+ }
347
+
348
+ // ── Execute ──────────────────────────────────────────────────────────────
349
+ removeInfernoDir(cwd, plan.infernoDir, false);
350
+ if (plan.claudeMd.length) removeClaudeMd(cwd, false);
351
+ if (plan.claudeDir.length) removeClaudeDir(cwd, plan.claudeDir, false);
352
+ if (plan.cursorMcpServer.length) removeCursorMcpServer(cwd, plan.cursorMcpServer, false);
353
+ if (plan.cursorMcpJson.length) removeCursorMcpJson(cwd, plan.cursorMcpJson, false);
354
+ if (plan.claudeJson.length) removeClaudeJson(plan.claudeJson, false);
355
+ if (plan.gitHooks.length) removeGitHooks(cwd, plan.gitHooks, false);
356
+
357
+ // ── Summary ──────────────────────────────────────────────────────────────
358
+ console.log(SEP);
359
+ console.log();
360
+ console.log(green(" ✔ infernoflow removed from this project"));
361
+ console.log();
362
+
363
+ const edited = actionItems.filter(i => i.type === "edit");
364
+ const removed = actionItems.filter(i => i.type === "rm" || i.type === "rmdir");
365
+
366
+ if (removed.length) console.log(gray(` Deleted: `) + removed.map(i => i.path).join(", "));
367
+ if (edited.length) console.log(gray(` Edited: `) + edited.map(i => i.path).join(", "));
368
+
369
+ if (keepMem) {
370
+ console.log();
371
+ console.log(gray(" Session memory kept → inferno/sessions.jsonl"));
372
+ console.log(gray(" Re-run infernoflow init to restore the rest."));
373
+ }
374
+ console.log();
375
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {