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.
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -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
|
+
}
|