@ijfw/install 1.6.0 → 1.6.2

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/uninstall.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/uninstall.js
4
- import { existsSync as existsSync2, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, readdirSync } from "node:fs";
5
- import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
4
+ import { existsSync as existsSync3, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync2, readdirSync as readdirSync2, realpathSync, statSync, chmodSync } from "node:fs";
5
+ import { resolve as resolve2, join as join3, dirname as dirname2, basename } from "node:path";
6
6
  import { homedir as homedir2, tmpdir } from "node:os";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -95,6 +95,29 @@ function unmergeMarketplace(settingsPath = claudeSettingsPath()) {
95
95
  return settings;
96
96
  }
97
97
 
98
+ // src/install-ledger.js
99
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync } from "node:fs";
100
+ import { join as join2 } from "node:path";
101
+ function ledgerPath(ijfwHome) {
102
+ return join2(ijfwHome, "install-ledger.json");
103
+ }
104
+ function readLedger(ijfwHome) {
105
+ try {
106
+ const raw = readFileSync2(ledgerPath(ijfwHome), "utf8");
107
+ const doc = JSON.parse(raw);
108
+ if (doc && Array.isArray(doc.createdDirs)) return doc;
109
+ } catch {
110
+ }
111
+ return { version: 1, createdDirs: [] };
112
+ }
113
+ function isEmptyDir(p) {
114
+ try {
115
+ return existsSync2(p) && readdirSync(p).length === 0;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
98
121
  // src/uninstall.js
99
122
  var __filename = fileURLToPath(import.meta.url);
100
123
  var __dirname = dirname2(__filename);
@@ -103,9 +126,9 @@ function resolveAiderTemplate(name, repoRoot) {
103
126
  const root = repoRoot || REPO_ROOT;
104
127
  const candidates = [
105
128
  // (a) git clone: top-level aider/ under the (injected) repo root.
106
- join2(root, "aider", name),
129
+ join3(root, "aider", name),
107
130
  // (a') repo root with staged templates under installer/.
108
- join2(root, "installer", "templates", "aider", name),
131
+ join3(root, "installer", "templates", "aider", name),
109
132
  // (b) tarball/dist fallback: templates staged next to the package root.
110
133
  // dist/uninstall.js -> __dirname=<pkg>/dist -> <pkg>/templates/aider/<name>
111
134
  // src/uninstall.js -> __dirname=<pkg>/src -> <pkg>/templates/aider/<name>
@@ -114,17 +137,26 @@ function resolveAiderTemplate(name, repoRoot) {
114
137
  ];
115
138
  for (const c of candidates) {
116
139
  try {
117
- if (existsSync2(c)) return c;
140
+ if (existsSync3(c)) return c;
118
141
  } catch {
119
142
  }
120
143
  }
121
144
  return "";
122
145
  }
123
146
  function writeAtomic(target, content) {
147
+ let mode = 384;
148
+ try {
149
+ mode = statSync(target).mode & 511;
150
+ } catch {
151
+ }
124
152
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
125
- writeFileSync2(tmp, content);
153
+ writeFileSync3(tmp, content, { mode });
126
154
  try {
127
155
  renameSync2(tmp, target);
156
+ try {
157
+ chmodSync(target, mode);
158
+ } catch {
159
+ }
128
160
  } catch (err) {
129
161
  try {
130
162
  unlinkSync2(tmp);
@@ -178,7 +210,7 @@ function confirm(question) {
178
210
  var HOME = homedir2();
179
211
  var TS = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
180
212
  function backupFile(p) {
181
- if (existsSync2(p)) {
213
+ if (existsSync3(p)) {
182
214
  const bak = p + ".bak." + TS;
183
215
  cpSync(p, bak);
184
216
  return bak;
@@ -212,10 +244,10 @@ function hasIjfwMarker(text) {
212
244
  }
213
245
  function stripMarkerFile(p, opts = {}) {
214
246
  try {
215
- if (!existsSync2(p)) return null;
247
+ if (!existsSync3(p)) return null;
216
248
  let text;
217
249
  try {
218
- text = readFileSync2(p, "utf8");
250
+ text = readFileSync3(p, "utf8");
219
251
  } catch {
220
252
  return null;
221
253
  }
@@ -229,35 +261,46 @@ function stripMarkerFile(p, opts = {}) {
229
261
  }
230
262
  writeAtomic(p, stripped);
231
263
  return `${opts.label || p} (stripped IJFW marker regions, user content preserved)`;
232
- } catch {
233
- return null;
264
+ } catch (err) {
265
+ return `${opts.label || p} (KEPT -- IJFW region strip FAILED: ${err && err.message ? err.message : err}; remove the IJFW marker regions manually)`;
234
266
  }
235
267
  }
236
268
  function removeTomlSection(p) {
237
- if (!existsSync2(p)) return false;
238
- backupFile(p);
239
- const lines = readFileSync2(p, "utf8").split("\n");
269
+ if (!existsSync3(p)) return false;
270
+ const lines = readFileSync3(p, "utf8").split("\n");
240
271
  const out = [];
241
272
  let skip = false;
273
+ let changed = false;
242
274
  for (const line of lines) {
243
275
  if (/^\[mcp_servers\.ijfw-memory\]\s*$/.test(line)) {
244
276
  skip = true;
277
+ changed = true;
245
278
  continue;
246
279
  }
247
280
  if (skip && line.startsWith("[") && !line.startsWith("[mcp_servers.ijfw-memory]")) skip = false;
248
281
  if (!skip) out.push(line);
249
282
  }
250
- writeAtomic(p, out.join("\n") + "\n");
283
+ if (!changed) return false;
284
+ backupFile(p);
285
+ let text = out.join("\n");
286
+ if (!text.endsWith("\n")) text += "\n";
287
+ writeAtomic(p, text);
251
288
  return true;
252
289
  }
253
290
  function removeJsonMcpEntry(p) {
254
- if (!existsSync2(p)) return false;
255
- let doc;
291
+ if (!existsSync3(p)) return false;
292
+ let raw;
256
293
  try {
257
- doc = JSON.parse(readFileSync2(p, "utf8"));
294
+ raw = readFileSync3(p, "utf8");
258
295
  } catch {
259
296
  return false;
260
297
  }
298
+ let doc;
299
+ try {
300
+ doc = JSON.parse(raw);
301
+ } catch {
302
+ return /\bijfw-memory\b/.test(raw) ? "parse-failed" : false;
303
+ }
261
304
  if (!doc || typeof doc !== "object") return false;
262
305
  let changed = false;
263
306
  if (doc.mcpServers && doc.mcpServers["ijfw-memory"]) {
@@ -270,10 +313,10 @@ function removeJsonMcpEntry(p) {
270
313
  }
271
314
  function removeNestedMcpEntry(p, keyPath) {
272
315
  try {
273
- if (!existsSync2(p)) return false;
316
+ if (!existsSync3(p)) return false;
274
317
  let doc;
275
318
  try {
276
- doc = JSON.parse(readFileSync2(p, "utf8"));
319
+ doc = JSON.parse(readFileSync3(p, "utf8"));
277
320
  } catch {
278
321
  return false;
279
322
  }
@@ -294,43 +337,43 @@ function removeNestedMcpEntry(p, keyPath) {
294
337
  }
295
338
  function resolveClineSettingsPath(home) {
296
339
  const H = home;
297
- const APPDATA = process.env.APPDATA || join2(H, "AppData", "Roaming");
340
+ const APPDATA = process.env.APPDATA || join3(H, "AppData", "Roaming");
298
341
  const ext = "saoudrizwan.claude-dev";
299
342
  let userDirs;
300
343
  if (process.platform === "darwin") {
301
344
  userDirs = [
302
- join2(H, "Library", "Application Support", "Code", "User"),
303
- join2(H, "Library", "Application Support", "Code - Insiders", "User"),
304
- join2(H, "Library", "Application Support", "VSCodium", "User")
345
+ join3(H, "Library", "Application Support", "Code", "User"),
346
+ join3(H, "Library", "Application Support", "Code - Insiders", "User"),
347
+ join3(H, "Library", "Application Support", "VSCodium", "User")
305
348
  ];
306
349
  } else if (process.platform === "win32") {
307
350
  userDirs = [
308
- join2(APPDATA, "Code", "User"),
309
- join2(APPDATA, "Code - Insiders", "User"),
310
- join2(APPDATA, "VSCodium", "User")
351
+ join3(APPDATA, "Code", "User"),
352
+ join3(APPDATA, "Code - Insiders", "User"),
353
+ join3(APPDATA, "VSCodium", "User")
311
354
  ];
312
355
  } else {
313
356
  userDirs = [
314
- join2(H, ".config", "Code", "User"),
315
- join2(H, ".config", "VSCodium", "User"),
316
- join2(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
317
- join2(H, "snap", "code", "current", ".config", "Code", "User")
357
+ join3(H, ".config", "Code", "User"),
358
+ join3(H, ".config", "VSCodium", "User"),
359
+ join3(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
360
+ join3(H, "snap", "code", "current", ".config", "Code", "User")
318
361
  ];
319
362
  }
320
363
  for (const d of userDirs) {
321
- const settings = join2(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
322
- if (existsSync2(settings)) return settings;
364
+ const settings = join3(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
365
+ if (existsSync3(settings)) return settings;
323
366
  }
324
367
  return null;
325
368
  }
326
369
  function removeAiderFileIfPristine(installedPath, templatePath) {
327
370
  try {
328
- if (!existsSync2(installedPath)) return "absent";
329
- if (!existsSync2(templatePath)) return "kept-modified";
371
+ if (!existsSync3(installedPath)) return "absent";
372
+ if (!existsSync3(templatePath)) return "kept-modified";
330
373
  let a, b;
331
374
  try {
332
- a = readFileSync2(installedPath);
333
- b = readFileSync2(templatePath);
375
+ a = readFileSync3(installedPath);
376
+ b = readFileSync3(templatePath);
334
377
  } catch {
335
378
  return "kept-modified";
336
379
  }
@@ -345,10 +388,10 @@ function removeAiderFileIfPristine(installedPath, templatePath) {
345
388
  }
346
389
  }
347
390
  function removeCodexHooks(p) {
348
- if (!existsSync2(p)) return false;
391
+ if (!existsSync3(p)) return false;
349
392
  let doc;
350
393
  try {
351
- doc = JSON.parse(readFileSync2(p, "utf8"));
394
+ doc = JSON.parse(readFileSync3(p, "utf8"));
352
395
  } catch {
353
396
  return false;
354
397
  }
@@ -389,9 +432,10 @@ function removeCodexHooks(p) {
389
432
  return false;
390
433
  }
391
434
  function removeYamlMcpEntry(p) {
392
- if (!existsSync2(p)) return false;
393
- const raw = readFileSync2(p, "utf8");
435
+ if (!existsSync3(p)) return false;
436
+ const raw = readFileSync3(p, "utf8");
394
437
  if (!/\bijfw-memory\b/.test(raw)) return false;
438
+ const bak = backupFile(p);
395
439
  const py = spawnSync("python3", ["-c", `
396
440
  import sys, yaml
397
441
  p = sys.argv[1]
@@ -404,12 +448,11 @@ del srv["ijfw-memory"]
404
448
  if not srv: del doc["mcp_servers"]
405
449
  with open(p + ".tmp", "w") as f:
406
450
  yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
407
- import os; os.replace(p + ".tmp", p)
451
+ import os, stat
452
+ os.chmod(p + ".tmp", stat.S_IMODE(os.stat(p).st_mode))
453
+ os.replace(p + ".tmp", p)
408
454
  `, p], { encoding: "utf8" });
409
- if (py.status === 0) {
410
- backupFile(p);
411
- return true;
412
- }
455
+ if (py.status === 0) return true;
413
456
  const stripped = raw.replace(
414
457
  // eslint-disable-next-line security/detect-unsafe-regex -- raw is a small local YAML config file; pattern is line-anchored to the IJFW-owned block.
415
458
  /^ ijfw-memory:\n(?: .*\n)*(?:# IJFW-MCP-END ijfw-memory\n)?/m,
@@ -419,17 +462,93 @@ import os; os.replace(p + ".tmp", p)
419
462
  /# IJFW-MCP-BEGIN ijfw-memory\n(?:.*\n)*?# IJFW-MCP-END ijfw-memory\n/,
420
463
  ""
421
464
  );
422
- if (stripped === raw) return false;
423
- backupFile(p);
465
+ if (stripped === raw) {
466
+ if (bak) {
467
+ try {
468
+ rmSync(bak, { force: true });
469
+ } catch {
470
+ }
471
+ }
472
+ return false;
473
+ }
424
474
  writeAtomic(p, stripped);
425
475
  return true;
426
476
  }
477
+ function resolveShippedTemplate(rel, repoRoot) {
478
+ const root = repoRoot || REPO_ROOT;
479
+ const candidates = [
480
+ join3(root, rel),
481
+ join3(root, "installer", "templates", rel),
482
+ resolve2(__dirname, "..", "templates", rel)
483
+ ];
484
+ for (const c of candidates) {
485
+ try {
486
+ if (existsSync3(c)) return c;
487
+ } catch {
488
+ }
489
+ }
490
+ return "";
491
+ }
492
+ function removeHermesIjfwWiring(p) {
493
+ if (!existsSync3(p)) return false;
494
+ const raw = readFileSync3(p, "utf8");
495
+ if (!/\bijfw\b/i.test(raw)) return false;
496
+ const bak = backupFile(p);
497
+ const py = spawnSync("python3", ["-c", `
498
+ import sys, yaml
499
+ p = sys.argv[1]
500
+ with open(p) as f: raw = f.read()
501
+ doc = yaml.safe_load(raw) if raw.strip() else {}
502
+ if not isinstance(doc, dict): sys.exit(2)
503
+ changed = False
504
+ srv = doc.get('mcp_servers')
505
+ if isinstance(srv, dict) and 'ijfw-memory' in srv:
506
+ del srv['ijfw-memory']; changed = True
507
+ if not srv: del doc['mcp_servers']
508
+ pl = doc.get('plugins')
509
+ if isinstance(pl, dict) and isinstance(pl.get('enabled'), list) and 'ijfw' in pl['enabled']:
510
+ pl['enabled'] = [x for x in pl['enabled'] if x != 'ijfw']; changed = True
511
+ if not pl['enabled']: del pl['enabled']
512
+ if not pl: del doc['plugins']
513
+ hk = doc.get('hooks')
514
+ if isinstance(hk, dict):
515
+ for ev in list(hk.keys()):
516
+ items = hk[ev]
517
+ if isinstance(items, list):
518
+ new = [it for it in items if not (isinstance(it, dict) and 'ijfw' in str(it.get('script','')))]
519
+ if len(new) != len(items):
520
+ changed = True
521
+ if new: hk[ev] = new
522
+ else: del hk[ev]
523
+ if isinstance(doc.get('hooks'), dict) and not doc['hooks']: del doc['hooks']
524
+ if not changed: sys.exit(3)
525
+ with open(p + '.tmp', 'w') as f:
526
+ if doc: yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
527
+ else: f.write('')
528
+ import os, stat
529
+ os.chmod(p + '.tmp', stat.S_IMODE(os.stat(p).st_mode))
530
+ os.replace(p + '.tmp', p)
531
+ `, p], { encoding: "utf8" });
532
+ if (py.status === 0) return true;
533
+ const out = raw.replace(/# IJFW-MCP-BEGIN ijfw-memory\n[\s\S]*?# IJFW-MCP-END ijfw-memory\n/g, "").replace(/# IJFW-PLUGINS-BEGIN\n[\s\S]*?# IJFW-PLUGINS-END\n/g, "").replace(/# IJFW-HOOK-BEGIN pre_tool_use\n[\s\S]*?# IJFW-HOOK-END pre_tool_use\n/g, "").replace(/^[ \t]*-[ \t]+ijfw[ \t]*\n/gm, "").replace(/^[ \t]*-[ \t]+script:[ \t]*["']?plugins\/ijfw\/[^\n]*\n/gm, "");
534
+ if (out === raw) {
535
+ if (bak) {
536
+ try {
537
+ rmSync(bak, { force: true });
538
+ } catch {
539
+ }
540
+ }
541
+ return false;
542
+ }
543
+ writeAtomic(p, out);
544
+ return true;
545
+ }
427
546
  function removeIjfwSkills(dir) {
428
- if (!existsSync2(dir)) return 0;
547
+ if (!existsSync3(dir)) return 0;
429
548
  let count = 0;
430
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
549
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
431
550
  if (entry.isDirectory() && entry.name.startsWith("ijfw-")) {
432
- rmSync(join2(dir, entry.name), { recursive: true, force: true });
551
+ rmSync(join3(dir, entry.name), { recursive: true, force: true });
433
552
  count++;
434
553
  }
435
554
  }
@@ -460,109 +579,199 @@ var CODEX_COMMAND_FILES = [
460
579
  "workflow.md"
461
580
  ];
462
581
  function removeCodexCommands(dir) {
463
- if (!existsSync2(dir)) return 0;
582
+ if (!existsSync3(dir)) return 0;
464
583
  let count = 0;
465
584
  for (const name of CODEX_COMMAND_FILES) {
466
- const path = join2(dir, name);
467
- if (existsSync2(path)) {
585
+ const path = join3(dir, name);
586
+ if (existsSync3(path)) {
468
587
  rmSync(path, { force: true });
469
588
  count++;
470
589
  }
471
590
  }
472
591
  return count;
473
592
  }
593
+ function removeCodexHookFiles(hooksDir) {
594
+ if (!existsSync3(hooksDir)) return 0;
595
+ let count = 0;
596
+ const scriptsDir = join3(hooksDir, "scripts");
597
+ if (existsSync3(scriptsDir)) {
598
+ rmSync(scriptsDir, { recursive: true, force: true });
599
+ count++;
600
+ }
601
+ let entries = [];
602
+ try {
603
+ entries = readdirSync2(hooksDir, { withFileTypes: true });
604
+ } catch {
605
+ return count;
606
+ }
607
+ for (const e of entries) {
608
+ if (!e.isFile() || !e.name.endsWith(".sh")) continue;
609
+ const p = join3(hooksDir, e.name);
610
+ let body = "";
611
+ try {
612
+ body = readFileSync3(p, "utf8");
613
+ } catch {
614
+ continue;
615
+ }
616
+ const header = body.split("\n", 4).slice(0, 4);
617
+ if (header.some((l) => /^#\s*IJFW\b/.test(l))) {
618
+ backupFile(p);
619
+ rmSync(p, { force: true });
620
+ count++;
621
+ }
622
+ }
623
+ return count;
624
+ }
625
+ function removeKnownMarketplacesEntry(p) {
626
+ if (!existsSync3(p)) return false;
627
+ let doc;
628
+ try {
629
+ doc = JSON.parse(readFileSync3(p, "utf8"));
630
+ } catch {
631
+ return false;
632
+ }
633
+ if (!doc || typeof doc !== "object") return false;
634
+ let changed = false;
635
+ if (doc.ijfw) {
636
+ delete doc.ijfw;
637
+ changed = true;
638
+ }
639
+ if (doc.extraKnownMarketplaces && typeof doc.extraKnownMarketplaces === "object" && doc.extraKnownMarketplaces.ijfw) {
640
+ delete doc.extraKnownMarketplaces.ijfw;
641
+ if (Object.keys(doc.extraKnownMarketplaces).length === 0) delete doc.extraKnownMarketplaces;
642
+ changed = true;
643
+ }
644
+ if (!changed) return false;
645
+ backupFile(p);
646
+ writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
647
+ return true;
648
+ }
474
649
  function cleanPlatforms(opts = {}) {
475
650
  const home = opts.home || HOME;
476
651
  const cwd = opts.cwd || process.cwd();
477
652
  const repoRoot = opts.repoRoot || REPO_ROOT;
478
653
  const removed = [];
479
- if (removeTomlSection(join2(home, ".codex", "config.toml"))) {
654
+ const rmJsonEntry = (p, label) => {
655
+ const r = removeJsonMcpEntry(p);
656
+ if (r === "parse-failed") {
657
+ removed.push(`${label} (KEPT -- file is not valid JSON but still references ijfw-memory; remove the entry manually)`);
658
+ return false;
659
+ }
660
+ return r === true;
661
+ };
662
+ if (rmJsonEntry(join3(home, ".claude", "settings.json"), "~/.claude/settings.json")) {
663
+ removed.push("~/.claude/settings.json (removed ijfw-memory mcp entry)");
664
+ }
665
+ if (removeKnownMarketplacesEntry(join3(home, ".claude", "plugins", "known_marketplaces.json"))) {
666
+ removed.push("~/.claude/plugins/known_marketplaces.json (removed ijfw entry)");
667
+ }
668
+ if (removeTomlSection(join3(home, ".codex", "config.toml"))) {
480
669
  removed.push("~/.codex/config.toml (removed [mcp_servers.ijfw-memory])");
481
670
  }
482
- if (removeCodexHooks(join2(home, ".codex", "hooks.json"))) {
671
+ if (removeCodexHooks(join3(home, ".codex", "hooks.json"))) {
483
672
  removed.push("~/.codex/hooks.json (removed IJFW hook entries)");
484
673
  }
485
- const codexSkills = removeIjfwSkills(join2(home, ".codex", "skills"));
674
+ const codexSkills = removeIjfwSkills(join3(home, ".codex", "skills"));
486
675
  if (codexSkills > 0) removed.push(`~/.codex/skills/ijfw-* (removed ${codexSkills} skill dirs)`);
487
- const codexCommands = removeCodexCommands(join2(home, ".codex", "commands"));
676
+ const codexCommands = removeCodexCommands(join3(home, ".codex", "commands"));
488
677
  if (codexCommands > 0) removed.push(`~/.codex/commands (removed ${codexCommands} IJFW command aliases)`);
489
- const codexMd = join2(home, ".codex", "IJFW.md");
490
- if (existsSync2(codexMd)) {
678
+ const codexMd = join3(home, ".codex", "IJFW.md");
679
+ if (existsSync3(codexMd)) {
491
680
  rmSync(codexMd, { force: true });
492
681
  removed.push("~/.codex/IJFW.md");
493
682
  }
494
- if (removeJsonMcpEntry(join2(home, ".gemini", "settings.json"))) {
683
+ const codexHookFiles = removeCodexHookFiles(join3(home, ".codex", "hooks"));
684
+ if (codexHookFiles > 0) removed.push(`~/.codex/hooks/ (removed ${codexHookFiles} IJFW hook scripts)`);
685
+ if (rmJsonEntry(join3(home, ".gemini", "settings.json"), "~/.gemini/settings.json")) {
495
686
  removed.push("~/.gemini/settings.json (removed ijfw-memory)");
496
687
  }
497
- const geminiExt = join2(home, ".gemini", "extensions", "ijfw");
498
- if (existsSync2(geminiExt)) {
688
+ const geminiExt = join3(home, ".gemini", "extensions", "ijfw");
689
+ if (existsSync3(geminiExt)) {
499
690
  rmSync(geminiExt, { recursive: true, force: true });
500
691
  removed.push("~/.gemini/extensions/ijfw/");
501
692
  }
502
- const cursorMcp = join2(cwd, ".cursor", "mcp.json");
503
- if (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
504
- if (removeJsonMcpEntry(join2(home, ".codeium", "windsurf", "mcp_config.json"))) {
693
+ const cursorMcp = join3(cwd, ".cursor", "mcp.json");
694
+ if (rmJsonEntry(cursorMcp, ".cursor/mcp.json")) removed.push(".cursor/mcp.json (removed ijfw-memory)");
695
+ if (rmJsonEntry(join3(home, ".codeium", "windsurf", "mcp_config.json"), "~/.codeium/windsurf/mcp_config.json")) {
505
696
  removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
506
697
  }
507
- const vscodeMcp = join2(cwd, ".vscode", "mcp.json");
508
- if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
509
- if (removeYamlMcpEntry(join2(home, ".hermes", "config.yaml"))) {
510
- removed.push("~/.hermes/config.yaml (removed ijfw-memory)");
698
+ const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
699
+ const vscodeLegacy = rmJsonEntry(vscodeMcp, ".vscode/mcp.json");
700
+ const vscodeServers = removeNestedMcpEntry(vscodeMcp, ["servers"]);
701
+ if (vscodeLegacy || vscodeServers) removed.push(".vscode/mcp.json (removed ijfw-memory)");
702
+ if (removeHermesIjfwWiring(join3(home, ".hermes", "config.yaml"))) {
703
+ removed.push("~/.hermes/config.yaml (removed ijfw-memory + plugin + hook wiring)");
511
704
  }
512
- const hermesSkills = removeIjfwSkills(join2(home, ".hermes", "skills"));
705
+ const hermesSkills = removeIjfwSkills(join3(home, ".hermes", "skills"));
513
706
  if (hermesSkills > 0) removed.push(`~/.hermes/skills/ijfw-* (removed ${hermesSkills} skill dirs)`);
514
- const hermesMd = join2(home, ".hermes", "HERMES.md");
515
- if (existsSync2(hermesMd)) {
707
+ const hermesMd = join3(home, ".hermes", "HERMES.md");
708
+ if (existsSync3(hermesMd)) {
516
709
  rmSync(hermesMd, { force: true });
517
710
  removed.push("~/.hermes/HERMES.md");
518
711
  }
519
- const waylandPluginDir = join2(home, ".wayland", "plugins", "ijfw");
520
- if (existsSync2(waylandPluginDir)) {
712
+ const hermesPlugin = join3(home, ".hermes", "plugins", "ijfw");
713
+ if (existsSync3(hermesPlugin)) {
714
+ rmSync(hermesPlugin, { recursive: true, force: true });
715
+ removed.push("~/.hermes/plugins/ijfw/ (removed plugin tree)");
716
+ }
717
+ const waylandPluginDir = join3(home, ".wayland", "plugins", "ijfw");
718
+ if (existsSync3(waylandPluginDir)) {
521
719
  rmSync(waylandPluginDir, { recursive: true, force: true });
522
720
  removed.push("~/.wayland/plugins/ijfw/ (removed plugin.toml + hooks + MCP)");
523
721
  }
524
- if (removeYamlMcpEntry(join2(home, ".wayland", "config.yaml"))) {
722
+ if (removeYamlMcpEntry(join3(home, ".wayland", "config.yaml"))) {
525
723
  removed.push("~/.wayland/config.yaml (removed legacy ijfw-memory)");
526
724
  }
527
- const waylandSkills = removeIjfwSkills(join2(home, ".wayland", "skills"));
725
+ const waylandSkills = removeIjfwSkills(join3(home, ".wayland", "skills"));
528
726
  if (waylandSkills > 0) removed.push(`~/.wayland/skills/ijfw-* (removed ${waylandSkills} skill dirs)`);
529
- const waylandMd = join2(home, ".wayland", "WAYLAND.md");
530
- if (existsSync2(waylandMd)) {
727
+ const waylandMd = join3(home, ".wayland", "WAYLAND.md");
728
+ if (existsSync3(waylandMd)) {
531
729
  rmSync(waylandMd, { force: true });
532
730
  removed.push("~/.wayland/WAYLAND.md");
533
731
  }
534
- if (removeJsonMcpEntry(join2(home, ".qwen", "settings.json"))) {
732
+ if (rmJsonEntry(join3(home, ".qwen", "settings.json"), "~/.qwen/settings.json")) {
535
733
  removed.push("~/.qwen/settings.json (removed ijfw-memory)");
536
734
  }
537
- if (removeJsonMcpEntry(join2(home, ".kimi", "mcp.json"))) {
735
+ if (rmJsonEntry(join3(home, ".kimi", "mcp.json"), "~/.kimi/mcp.json")) {
538
736
  removed.push("~/.kimi/mcp.json (removed ijfw-memory)");
539
737
  }
540
- if (removeJsonMcpEntry(join2(home, ".gemini", "antigravity", "mcp_config.json"))) {
738
+ if (rmJsonEntry(join3(home, ".gemini", "antigravity", "mcp_config.json"), "~/.gemini/antigravity/mcp_config.json")) {
541
739
  removed.push("~/.gemini/antigravity/mcp_config.json (removed ijfw-memory)");
542
740
  }
543
- if (removeJsonMcpEntry(join2(home, ".gemini", "config", "mcp_config.json"))) {
741
+ if (rmJsonEntry(join3(home, ".gemini", "config", "mcp_config.json"), "~/.gemini/config/mcp_config.json")) {
544
742
  removed.push("~/.gemini/config/mcp_config.json (removed ijfw-memory)");
545
743
  }
546
- if (removeNestedMcpEntry(join2(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
744
+ if (removeNestedMcpEntry(join3(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
547
745
  removed.push("~/.config/opencode/opencode.json (removed mcp.ijfw-memory)");
548
746
  }
549
- if (removeNestedMcpEntry(join2(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
747
+ if (removeNestedMcpEntry(join3(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
550
748
  removed.push("~/.openclaw/openclaw.json (removed mcp.servers.ijfw-memory)");
551
749
  }
552
- const piStatus = stripMarkerFile(join2(home, ".pi", "agent", "AGENTS.md"), {
553
- label: "~/.pi/agent/AGENTS.md"
554
- });
555
- if (piStatus) removed.push(piStatus);
750
+ const piPath = join3(home, ".pi", "agent", "AGENTS.md");
751
+ const piStatus = stripMarkerFile(piPath, { label: "~/.pi/agent/AGENTS.md" });
752
+ if (piStatus) {
753
+ removed.push(piStatus);
754
+ } else {
755
+ const piPristine = removeAiderFileIfPristine(
756
+ piPath,
757
+ resolveShippedTemplate(join3("pi", "AGENTS.md"), repoRoot)
758
+ );
759
+ if (piPristine === "removed") {
760
+ removed.push("~/.pi/agent/AGENTS.md (removed -- matched shipped template)");
761
+ } else if (piPristine === "kept-modified") {
762
+ removed.push("~/.pi/agent/AGENTS.md (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
763
+ }
764
+ }
556
765
  const clineSettings = resolveClineSettingsPath(home);
557
766
  if (clineSettings) {
558
- if (removeJsonMcpEntry(clineSettings)) {
767
+ if (rmJsonEntry(clineSettings, clineSettings)) {
559
768
  removed.push(`${clineSettings} (removed ijfw-memory)`);
560
769
  }
561
770
  } else {
562
771
  removed.push("Cline: no globalStorage found -- if you use Cline, remove the ijfw-memory MCP entry manually.");
563
772
  }
564
773
  const confResult = removeAiderFileIfPristine(
565
- join2(home, ".aider.conf.yml"),
774
+ join3(home, ".aider.conf.yml"),
566
775
  resolveAiderTemplate("aider.conf.yml", repoRoot)
567
776
  );
568
777
  if (confResult === "removed") {
@@ -571,7 +780,7 @@ function cleanPlatforms(opts = {}) {
571
780
  removed.push("~/.aider.conf.yml (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
572
781
  }
573
782
  const convResult = removeAiderFileIfPristine(
574
- join2(home, "CONVENTIONS.md"),
783
+ join3(home, "CONVENTIONS.md"),
575
784
  resolveAiderTemplate("CONVENTIONS.md", repoRoot)
576
785
  );
577
786
  if (convResult === "removed") {
@@ -583,8 +792,8 @@ function cleanPlatforms(opts = {}) {
583
792
  }
584
793
  function parseRegistryPaths(registryPath) {
585
794
  try {
586
- if (!existsSync2(registryPath)) return [];
587
- const raw = readFileSync2(registryPath, "utf8");
795
+ if (!existsSync3(registryPath)) return [];
796
+ const raw = readFileSync3(registryPath, "utf8");
588
797
  const paths = [];
589
798
  for (const line of raw.split("\n")) {
590
799
  const trimmed = line.trim();
@@ -602,37 +811,37 @@ function parseRegistryPaths(registryPath) {
602
811
  function stripRegisteredProjectBlocks(opts = {}) {
603
812
  const home = opts.home || HOME;
604
813
  const cwd = opts.cwd || process.cwd();
605
- const registryPath = opts.registryPath || join2(home, ".ijfw", "registry.md");
814
+ const registryPath = opts.registryPath || join3(home, ".ijfw", "registry.md");
606
815
  const results = [];
607
816
  for (const projPath of parseRegistryPaths(registryPath)) {
608
817
  let dirExists = false;
609
818
  try {
610
- dirExists = existsSync2(projPath);
819
+ dirExists = existsSync3(projPath);
611
820
  } catch {
612
821
  dirExists = false;
613
822
  }
614
823
  if (!dirExists) continue;
615
824
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
616
- const filePath = join2(projPath, name);
617
- const status = stripMarkerFile(filePath, { label: join2(projPath, name) });
825
+ const filePath = join3(projPath, name);
826
+ const status = stripMarkerFile(filePath, { label: join3(projPath, name) });
618
827
  if (status) results.push(status);
619
828
  }
620
829
  }
621
830
  try {
622
- const cursorRule = join2(cwd, ".cursor", "rules", "ijfw.mdc");
623
- if (existsSync2(cursorRule)) {
831
+ const cursorRule = join3(cwd, ".cursor", "rules", "ijfw.mdc");
832
+ if (existsSync3(cursorRule)) {
624
833
  backupFile(cursorRule);
625
834
  rmSync(cursorRule, { force: true });
626
835
  results.push(".cursor/rules/ijfw.mdc (removed -- wholly IJFW-authored)");
627
836
  }
628
837
  } catch {
629
838
  }
630
- const windsurfStatus = stripMarkerFile(join2(cwd, ".windsurfrules"), {
839
+ const windsurfStatus = stripMarkerFile(join3(cwd, ".windsurfrules"), {
631
840
  label: ".windsurfrules",
632
841
  deleteIfEmpty: true
633
842
  });
634
843
  if (windsurfStatus) results.push(windsurfStatus);
635
- const copilotStatus = stripMarkerFile(join2(cwd, ".github", "copilot-instructions.md"), {
844
+ const copilotStatus = stripMarkerFile(join3(cwd, ".github", "copilot-instructions.md"), {
636
845
  label: ".github/copilot-instructions.md",
637
846
  deleteIfEmpty: true
638
847
  });
@@ -642,17 +851,77 @@ function stripRegisteredProjectBlocks(opts = {}) {
642
851
  function resolveTarget(opt) {
643
852
  if (opt.dir) return resolve2(opt.dir);
644
853
  if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
645
- return join2(homedir2(), ".ijfw");
854
+ return join3(homedir2(), ".ijfw");
855
+ }
856
+ function assertSafePurgeTarget(target) {
857
+ let real = target;
858
+ try {
859
+ real = realpathSync(target);
860
+ } catch {
861
+ }
862
+ let home = homedir2();
863
+ try {
864
+ home = realpathSync(home);
865
+ } catch {
866
+ }
867
+ const isFsRoot = (p) => p === "/" || /^[A-Za-z]:[\\/]?$/.test(p);
868
+ if (!real || isFsRoot(real) || real === home) {
869
+ throw new Error(`refusing to delete '${target}': it resolves to the home or filesystem root.`);
870
+ }
871
+ const segs = real.split(/[\\/]+/).filter((s) => s && !/^[A-Za-z]:$/.test(s));
872
+ if (segs.length < 2) {
873
+ throw new Error(`refusing to delete shallow path '${real}'.`);
874
+ }
875
+ const hasIjfwState = () => {
876
+ try {
877
+ const doc = JSON.parse(readFileSync3(join3(real, "state.json"), "utf8"));
878
+ return !!doc && typeof doc === "object" && ("install_method" in doc || "installed_version" in doc);
879
+ } catch {
880
+ return false;
881
+ }
882
+ };
883
+ const looksIjfw = basename(real) === ".ijfw" || existsSync3(join3(real, "install-method")) || existsSync3(join3(real, "install-ledger.json")) || hasIjfwState();
884
+ if (!looksIjfw) {
885
+ throw new Error(`refusing to delete '${target}': it does not look like an IJFW install (no .ijfw basename / install-method / install-ledger.json / IJFW state.json). Aborting.`);
886
+ }
887
+ }
888
+ function removeCreatedDirs(home, createdDirs) {
889
+ const removed = [];
890
+ for (const rel of createdDirs || []) {
891
+ const abs = join3(home, rel);
892
+ if (isEmptyDir(abs)) {
893
+ try {
894
+ rmSync(abs, { recursive: false, force: true });
895
+ removed.push(`~/${rel} (IJFW-created, now empty)`);
896
+ } catch {
897
+ }
898
+ }
899
+ }
900
+ return removed;
646
901
  }
647
902
  async function main() {
648
903
  const opts = parseArgs(process.argv);
649
904
  const target = resolveTarget(opts);
905
+ const realOrSelf = (p) => {
906
+ try {
907
+ return realpathSync(p);
908
+ } catch {
909
+ return p;
910
+ }
911
+ };
912
+ const isCanonical = realOrSelf(target) === realOrSelf(join3(HOME, ".ijfw"));
913
+ const ledgerCreatedDirs = existsSync3(target) ? readLedger(target).createdDirs : [];
650
914
  console.log("This will remove IJFW configuration. Your memory at ~/.ijfw/memory/ will be preserved. Delete manually if desired.");
651
915
  if (opts.purge) {
652
916
  console.log("WARNING: --purge will also DELETE ~/.ijfw/memory/ (project memory cannot be recovered).");
653
917
  }
654
918
  console.log("");
655
- if (!opts.yes && process.stdin.isTTY) {
919
+ if (!opts.yes) {
920
+ if (!process.stdin.isTTY) {
921
+ console.error("Refusing to proceed: stdin is not a TTY, so the confirmation prompt cannot be answered.");
922
+ console.error("Non-interactive uninstall requires an explicit --yes (or -y). Nothing was changed.");
923
+ process.exit(1);
924
+ }
656
925
  const ok = await confirm("Proceed with IJFW uninstall? [y/N] ");
657
926
  if (!ok) {
658
927
  console.log("Uninstall cancelled. Nothing was changed.");
@@ -660,16 +929,18 @@ async function main() {
660
929
  }
661
930
  console.log("");
662
931
  }
663
- if (!existsSync2(target)) {
932
+ if (!existsSync3(target)) {
664
933
  console.log(`IJFW directory absent (${target}); platform cleanup only.`);
665
934
  } else if (opts.purge) {
935
+ assertSafePurgeTarget(target);
666
936
  rmSync(target, { recursive: true, force: true });
667
937
  console.log(` removed ${target} (purged).`);
668
938
  } else {
669
- const memDir = join2(target, "memory");
939
+ assertSafePurgeTarget(target);
940
+ const memDir = join3(target, "memory");
670
941
  let stash = null;
671
- if (existsSync2(memDir)) {
672
- stash = mkdtempSync(join2(tmpdir(), "ijfw-memory-"));
942
+ if (existsSync3(memDir)) {
943
+ stash = mkdtempSync(join3(tmpdir(), "ijfw-memory-"));
673
944
  cpSync(memDir, stash, { recursive: true });
674
945
  }
675
946
  rmSync(target, { recursive: true, force: true });
@@ -681,11 +952,9 @@ async function main() {
681
952
  console.log(" memory/ was not present; nothing to preserve");
682
953
  }
683
954
  }
684
- const canonicalDir = join2(HOME, ".ijfw");
685
- const isCanonical = target === canonicalDir;
686
955
  if (isCanonical && !opts.noMarketplace) {
687
956
  const settingsPath = claudeSettingsPath();
688
- if (existsSync2(settingsPath)) {
957
+ if (existsSync3(settingsPath)) {
689
958
  unmergeMarketplace(settingsPath);
690
959
  console.log(` marketplace removed from ${settingsPath}`);
691
960
  }
@@ -701,6 +970,13 @@ async function main() {
701
970
  console.log(" project blocks cleaned:");
702
971
  for (const line of projectCleaned) console.log(` ${line}`);
703
972
  }
973
+ if (opts.purge) {
974
+ const dirsRemoved = removeCreatedDirs(HOME, ledgerCreatedDirs);
975
+ if (dirsRemoved.length > 0) {
976
+ console.log(" IJFW-created dirs removed:");
977
+ for (const line of dirsRemoved) console.log(` ${line}`);
978
+ }
979
+ }
704
980
  } else {
705
981
  console.log(` custom-dir uninstall (${target}) -- platform configs in your real home left untouched.`);
706
982
  }
@@ -715,10 +991,15 @@ if (isDirectRun) {
715
991
  });
716
992
  }
717
993
  export {
994
+ assertSafePurgeTarget,
718
995
  cleanPlatforms,
719
996
  parseRegistryPaths,
720
997
  removeAiderFileIfPristine,
998
+ removeCodexHookFiles,
999
+ removeJsonMcpEntry,
721
1000
  removeNestedMcpEntry,
1001
+ removeTomlSection,
1002
+ removeYamlMcpEntry,
722
1003
  resolveAiderTemplate,
723
1004
  resolveClineSettingsPath,
724
1005
  stripIjfwRegions,