@ijfw/install 1.5.6 → 1.6.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/uninstall.js CHANGED
@@ -1,10 +1,11 @@
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 } 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 } 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
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
9
 
9
10
  // src/marketplace.js
10
11
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
@@ -94,10 +95,57 @@ function unmergeMarketplace(settingsPath = claudeSettingsPath()) {
94
95
  return settings;
95
96
  }
96
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
+
97
121
  // src/uninstall.js
122
+ var __filename = fileURLToPath(import.meta.url);
123
+ var __dirname = dirname2(__filename);
124
+ var REPO_ROOT = resolve2(__dirname, "..", "..");
125
+ function resolveAiderTemplate(name, repoRoot) {
126
+ const root = repoRoot || REPO_ROOT;
127
+ const candidates = [
128
+ // (a) git clone: top-level aider/ under the (injected) repo root.
129
+ join3(root, "aider", name),
130
+ // (a') repo root with staged templates under installer/.
131
+ join3(root, "installer", "templates", "aider", name),
132
+ // (b) tarball/dist fallback: templates staged next to the package root.
133
+ // dist/uninstall.js -> __dirname=<pkg>/dist -> <pkg>/templates/aider/<name>
134
+ // src/uninstall.js -> __dirname=<pkg>/src -> <pkg>/templates/aider/<name>
135
+ // (__dirname's parent is the package root in both layouts)
136
+ resolve2(__dirname, "..", "templates", "aider", name)
137
+ ];
138
+ for (const c of candidates) {
139
+ try {
140
+ if (existsSync3(c)) return c;
141
+ } catch {
142
+ }
143
+ }
144
+ return "";
145
+ }
98
146
  function writeAtomic(target, content) {
99
147
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
100
- writeFileSync2(tmp, content);
148
+ writeFileSync3(tmp, content);
101
149
  try {
102
150
  renameSync2(tmp, target);
103
151
  } catch (err) {
@@ -153,17 +201,65 @@ function confirm(question) {
153
201
  var HOME = homedir2();
154
202
  var TS = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
155
203
  function backupFile(p) {
156
- if (existsSync2(p)) {
204
+ if (existsSync3(p)) {
157
205
  const bak = p + ".bak." + TS;
158
206
  cpSync(p, bak);
159
207
  return bak;
160
208
  }
161
209
  return null;
162
210
  }
211
+ function stripIjfwRegions(src) {
212
+ if (typeof src !== "string") return { text: src, changed: false };
213
+ const before = src;
214
+ let out = src;
215
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
216
+ const regions = [
217
+ ["<!-- IJFW-MEMORY-START", "<!-- IJFW-MEMORY-END -->"],
218
+ ["<!-- IJFW-ROUTING-START", "<!-- IJFW-ROUTING-END -->"],
219
+ ["<!-- IJFW-AGENTS-START", "<!-- IJFW-AGENTS-END -->"],
220
+ ["<!-- IJFW-BLACKBOARD-START", "<!-- IJFW-BLACKBOARD-END -->"],
221
+ ["<!-- IJFW-DISCIPLINE-START", "<!-- IJFW-DISCIPLINE-END -->"]
222
+ ];
223
+ for (const [start, end] of regions) {
224
+ const re = new RegExp("\\n*" + esc(start) + "[\\s\\S]*?" + esc(end) + "[^\\n]*", "g");
225
+ out = out.replace(re, "");
226
+ }
227
+ out = out.replace(/\n*<!-- Auto-generated by IJFW from repo scan\.[^\n]*-->/g, "");
228
+ out = out.replace(/\n*(?:Four|Five) IJFW-managed regions live in this file\.[\s\S]*?IJFW will never touch it\./g, "");
229
+ out = out.replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
230
+ if (out.length) out += "\n";
231
+ return { text: out, changed: out !== before };
232
+ }
233
+ function hasIjfwMarker(text) {
234
+ return /IJFW-MEMORY-START|IJFW-ROUTING-START|IJFW-AGENTS-START|IJFW-BLACKBOARD-START|IJFW-DISCIPLINE-START/.test(text);
235
+ }
236
+ function stripMarkerFile(p, opts = {}) {
237
+ try {
238
+ if (!existsSync3(p)) return null;
239
+ let text;
240
+ try {
241
+ text = readFileSync3(p, "utf8");
242
+ } catch {
243
+ return null;
244
+ }
245
+ if (!hasIjfwMarker(text)) return null;
246
+ const { text: stripped, changed } = stripIjfwRegions(text);
247
+ if (!changed) return null;
248
+ backupFile(p);
249
+ if (opts.deleteIfEmpty && stripped.trim() === "") {
250
+ rmSync(p, { force: true });
251
+ return `${opts.label || p} (removed -- became empty after IJFW region strip)`;
252
+ }
253
+ writeAtomic(p, stripped);
254
+ return `${opts.label || p} (stripped IJFW marker regions, user content preserved)`;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
163
259
  function removeTomlSection(p) {
164
- if (!existsSync2(p)) return false;
260
+ if (!existsSync3(p)) return false;
165
261
  backupFile(p);
166
- const lines = readFileSync2(p, "utf8").split("\n");
262
+ const lines = readFileSync3(p, "utf8").split("\n");
167
263
  const out = [];
168
264
  let skip = false;
169
265
  for (const line of lines) {
@@ -178,10 +274,10 @@ function removeTomlSection(p) {
178
274
  return true;
179
275
  }
180
276
  function removeJsonMcpEntry(p) {
181
- if (!existsSync2(p)) return false;
277
+ if (!existsSync3(p)) return false;
182
278
  let doc;
183
279
  try {
184
- doc = JSON.parse(readFileSync2(p, "utf8"));
280
+ doc = JSON.parse(readFileSync3(p, "utf8"));
185
281
  } catch {
186
282
  return false;
187
283
  }
@@ -195,11 +291,87 @@ function removeJsonMcpEntry(p) {
195
291
  }
196
292
  return changed;
197
293
  }
294
+ function removeNestedMcpEntry(p, keyPath) {
295
+ try {
296
+ if (!existsSync3(p)) return false;
297
+ let doc;
298
+ try {
299
+ doc = JSON.parse(readFileSync3(p, "utf8"));
300
+ } catch {
301
+ return false;
302
+ }
303
+ if (!doc || typeof doc !== "object") return false;
304
+ let node = doc;
305
+ for (const k of keyPath) {
306
+ if (!node[k] || typeof node[k] !== "object") return false;
307
+ node = node[k];
308
+ }
309
+ if (!node["ijfw-memory"]) return false;
310
+ backupFile(p);
311
+ delete node["ijfw-memory"];
312
+ writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+ function resolveClineSettingsPath(home) {
319
+ const H = home;
320
+ const APPDATA = process.env.APPDATA || join3(H, "AppData", "Roaming");
321
+ const ext = "saoudrizwan.claude-dev";
322
+ let userDirs;
323
+ if (process.platform === "darwin") {
324
+ userDirs = [
325
+ join3(H, "Library", "Application Support", "Code", "User"),
326
+ join3(H, "Library", "Application Support", "Code - Insiders", "User"),
327
+ join3(H, "Library", "Application Support", "VSCodium", "User")
328
+ ];
329
+ } else if (process.platform === "win32") {
330
+ userDirs = [
331
+ join3(APPDATA, "Code", "User"),
332
+ join3(APPDATA, "Code - Insiders", "User"),
333
+ join3(APPDATA, "VSCodium", "User")
334
+ ];
335
+ } else {
336
+ userDirs = [
337
+ join3(H, ".config", "Code", "User"),
338
+ join3(H, ".config", "VSCodium", "User"),
339
+ join3(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
340
+ join3(H, "snap", "code", "current", ".config", "Code", "User")
341
+ ];
342
+ }
343
+ for (const d of userDirs) {
344
+ const settings = join3(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
345
+ if (existsSync3(settings)) return settings;
346
+ }
347
+ return null;
348
+ }
349
+ function removeAiderFileIfPristine(installedPath, templatePath) {
350
+ try {
351
+ if (!existsSync3(installedPath)) return "absent";
352
+ if (!existsSync3(templatePath)) return "kept-modified";
353
+ let a, b;
354
+ try {
355
+ a = readFileSync3(installedPath);
356
+ b = readFileSync3(templatePath);
357
+ } catch {
358
+ return "kept-modified";
359
+ }
360
+ if (a.equals(b)) {
361
+ backupFile(installedPath);
362
+ rmSync(installedPath, { force: true });
363
+ return "removed";
364
+ }
365
+ return "kept-modified";
366
+ } catch {
367
+ return "kept-modified";
368
+ }
369
+ }
198
370
  function removeCodexHooks(p) {
199
- if (!existsSync2(p)) return false;
371
+ if (!existsSync3(p)) return false;
200
372
  let doc;
201
373
  try {
202
- doc = JSON.parse(readFileSync2(p, "utf8"));
374
+ doc = JSON.parse(readFileSync3(p, "utf8"));
203
375
  } catch {
204
376
  return false;
205
377
  }
@@ -240,8 +412,8 @@ function removeCodexHooks(p) {
240
412
  return false;
241
413
  }
242
414
  function removeYamlMcpEntry(p) {
243
- if (!existsSync2(p)) return false;
244
- const raw = readFileSync2(p, "utf8");
415
+ if (!existsSync3(p)) return false;
416
+ const raw = readFileSync3(p, "utf8");
245
417
  if (!/\bijfw-memory\b/.test(raw)) return false;
246
418
  const py = spawnSync("python3", ["-c", `
247
419
  import sys, yaml
@@ -275,12 +447,74 @@ import os; os.replace(p + ".tmp", p)
275
447
  writeAtomic(p, stripped);
276
448
  return true;
277
449
  }
450
+ function resolveShippedTemplate(rel, repoRoot) {
451
+ const root = repoRoot || REPO_ROOT;
452
+ const candidates = [
453
+ join3(root, rel),
454
+ join3(root, "installer", "templates", rel),
455
+ resolve2(__dirname, "..", "templates", rel)
456
+ ];
457
+ for (const c of candidates) {
458
+ try {
459
+ if (existsSync3(c)) return c;
460
+ } catch {
461
+ }
462
+ }
463
+ return "";
464
+ }
465
+ function removeHermesIjfwWiring(p) {
466
+ if (!existsSync3(p)) return false;
467
+ const raw = readFileSync3(p, "utf8");
468
+ if (!/\bijfw\b/i.test(raw)) return false;
469
+ const py = spawnSync("python3", ["-c", `
470
+ import sys, yaml
471
+ p = sys.argv[1]
472
+ with open(p) as f: raw = f.read()
473
+ doc = yaml.safe_load(raw) if raw.strip() else {}
474
+ if not isinstance(doc, dict): sys.exit(2)
475
+ changed = False
476
+ srv = doc.get('mcp_servers')
477
+ if isinstance(srv, dict) and 'ijfw-memory' in srv:
478
+ del srv['ijfw-memory']; changed = True
479
+ if not srv: del doc['mcp_servers']
480
+ pl = doc.get('plugins')
481
+ if isinstance(pl, dict) and isinstance(pl.get('enabled'), list) and 'ijfw' in pl['enabled']:
482
+ pl['enabled'] = [x for x in pl['enabled'] if x != 'ijfw']; changed = True
483
+ if not pl['enabled']: del pl['enabled']
484
+ if not pl: del doc['plugins']
485
+ hk = doc.get('hooks')
486
+ if isinstance(hk, dict):
487
+ for ev in list(hk.keys()):
488
+ items = hk[ev]
489
+ if isinstance(items, list):
490
+ new = [it for it in items if not (isinstance(it, dict) and 'ijfw' in str(it.get('script','')))]
491
+ if len(new) != len(items):
492
+ changed = True
493
+ if new: hk[ev] = new
494
+ else: del hk[ev]
495
+ if isinstance(doc.get('hooks'), dict) and not doc['hooks']: del doc['hooks']
496
+ if not changed: sys.exit(3)
497
+ with open(p + '.tmp', 'w') as f:
498
+ if doc: yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
499
+ else: f.write('')
500
+ import os; os.replace(p + '.tmp', p)
501
+ `, p], { encoding: "utf8" });
502
+ if (py.status === 0) {
503
+ backupFile(p);
504
+ return true;
505
+ }
506
+ 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, "");
507
+ if (out === raw) return false;
508
+ backupFile(p);
509
+ writeAtomic(p, out);
510
+ return true;
511
+ }
278
512
  function removeIjfwSkills(dir) {
279
- if (!existsSync2(dir)) return 0;
513
+ if (!existsSync3(dir)) return 0;
280
514
  let count = 0;
281
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
515
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
282
516
  if (entry.isDirectory() && entry.name.startsWith("ijfw-")) {
283
- rmSync(join2(dir, entry.name), { recursive: true, force: true });
517
+ rmSync(join3(dir, entry.name), { recursive: true, force: true });
284
518
  count++;
285
519
  }
286
520
  }
@@ -311,79 +545,308 @@ var CODEX_COMMAND_FILES = [
311
545
  "workflow.md"
312
546
  ];
313
547
  function removeCodexCommands(dir) {
314
- if (!existsSync2(dir)) return 0;
548
+ if (!existsSync3(dir)) return 0;
315
549
  let count = 0;
316
550
  for (const name of CODEX_COMMAND_FILES) {
317
- const path = join2(dir, name);
318
- if (existsSync2(path)) {
551
+ const path = join3(dir, name);
552
+ if (existsSync3(path)) {
319
553
  rmSync(path, { force: true });
320
554
  count++;
321
555
  }
322
556
  }
323
557
  return count;
324
558
  }
325
- function cleanPlatforms() {
559
+ function removeCodexHookFiles(hooksDir) {
560
+ if (!existsSync3(hooksDir)) return 0;
561
+ let count = 0;
562
+ const scriptsDir = join3(hooksDir, "scripts");
563
+ if (existsSync3(scriptsDir)) {
564
+ rmSync(scriptsDir, { recursive: true, force: true });
565
+ count++;
566
+ }
567
+ let entries = [];
568
+ try {
569
+ entries = readdirSync2(hooksDir, { withFileTypes: true });
570
+ } catch {
571
+ return count;
572
+ }
573
+ for (const e of entries) {
574
+ if (!e.isFile() || !e.name.endsWith(".sh")) continue;
575
+ const p = join3(hooksDir, e.name);
576
+ let body = "";
577
+ try {
578
+ body = readFileSync3(p, "utf8");
579
+ } catch {
580
+ continue;
581
+ }
582
+ if (/\bIJFW\b/.test(body) || /\bijfw\b/.test(body)) {
583
+ rmSync(p, { force: true });
584
+ count++;
585
+ }
586
+ }
587
+ return count;
588
+ }
589
+ function removeKnownMarketplacesEntry(p) {
590
+ if (!existsSync3(p)) return false;
591
+ let doc;
592
+ try {
593
+ doc = JSON.parse(readFileSync3(p, "utf8"));
594
+ } catch {
595
+ return false;
596
+ }
597
+ if (!doc || typeof doc !== "object") return false;
598
+ let changed = false;
599
+ if (doc.ijfw) {
600
+ delete doc.ijfw;
601
+ changed = true;
602
+ }
603
+ if (doc.extraKnownMarketplaces && typeof doc.extraKnownMarketplaces === "object" && doc.extraKnownMarketplaces.ijfw) {
604
+ delete doc.extraKnownMarketplaces.ijfw;
605
+ if (Object.keys(doc.extraKnownMarketplaces).length === 0) delete doc.extraKnownMarketplaces;
606
+ changed = true;
607
+ }
608
+ if (!changed) return false;
609
+ backupFile(p);
610
+ writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
611
+ return true;
612
+ }
613
+ function cleanPlatforms(opts = {}) {
614
+ const home = opts.home || HOME;
615
+ const cwd = opts.cwd || process.cwd();
616
+ const repoRoot = opts.repoRoot || REPO_ROOT;
326
617
  const removed = [];
327
- if (removeTomlSection(join2(HOME, ".codex", "config.toml"))) {
618
+ if (removeJsonMcpEntry(join3(home, ".claude", "settings.json"))) {
619
+ removed.push("~/.claude/settings.json (removed ijfw-memory mcp entry)");
620
+ }
621
+ if (removeKnownMarketplacesEntry(join3(home, ".claude", "plugins", "known_marketplaces.json"))) {
622
+ removed.push("~/.claude/plugins/known_marketplaces.json (removed ijfw entry)");
623
+ }
624
+ if (removeTomlSection(join3(home, ".codex", "config.toml"))) {
328
625
  removed.push("~/.codex/config.toml (removed [mcp_servers.ijfw-memory])");
329
626
  }
330
- if (removeCodexHooks(join2(HOME, ".codex", "hooks.json"))) {
627
+ if (removeCodexHooks(join3(home, ".codex", "hooks.json"))) {
331
628
  removed.push("~/.codex/hooks.json (removed IJFW hook entries)");
332
629
  }
333
- const codexSkills = removeIjfwSkills(join2(HOME, ".codex", "skills"));
630
+ const codexSkills = removeIjfwSkills(join3(home, ".codex", "skills"));
334
631
  if (codexSkills > 0) removed.push(`~/.codex/skills/ijfw-* (removed ${codexSkills} skill dirs)`);
335
- const codexCommands = removeCodexCommands(join2(HOME, ".codex", "commands"));
632
+ const codexCommands = removeCodexCommands(join3(home, ".codex", "commands"));
336
633
  if (codexCommands > 0) removed.push(`~/.codex/commands (removed ${codexCommands} IJFW command aliases)`);
337
- const codexMd = join2(HOME, ".codex", "IJFW.md");
338
- if (existsSync2(codexMd)) {
634
+ const codexMd = join3(home, ".codex", "IJFW.md");
635
+ if (existsSync3(codexMd)) {
339
636
  rmSync(codexMd, { force: true });
340
637
  removed.push("~/.codex/IJFW.md");
341
638
  }
342
- if (removeJsonMcpEntry(join2(HOME, ".gemini", "settings.json"))) {
639
+ const codexHookFiles = removeCodexHookFiles(join3(home, ".codex", "hooks"));
640
+ if (codexHookFiles > 0) removed.push(`~/.codex/hooks/ (removed ${codexHookFiles} IJFW hook scripts)`);
641
+ if (removeJsonMcpEntry(join3(home, ".gemini", "settings.json"))) {
343
642
  removed.push("~/.gemini/settings.json (removed ijfw-memory)");
344
643
  }
345
- const geminiExt = join2(HOME, ".gemini", "extensions", "ijfw");
346
- if (existsSync2(geminiExt)) {
644
+ const geminiExt = join3(home, ".gemini", "extensions", "ijfw");
645
+ if (existsSync3(geminiExt)) {
347
646
  rmSync(geminiExt, { recursive: true, force: true });
348
647
  removed.push("~/.gemini/extensions/ijfw/");
349
648
  }
350
- const cursorMcp = join2(".cursor", "mcp.json");
649
+ const cursorMcp = join3(cwd, ".cursor", "mcp.json");
351
650
  if (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
352
- if (removeJsonMcpEntry(join2(HOME, ".codeium", "windsurf", "mcp_config.json"))) {
651
+ if (removeJsonMcpEntry(join3(home, ".codeium", "windsurf", "mcp_config.json"))) {
353
652
  removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
354
653
  }
355
- const vscodeMcp = join2(".vscode", "mcp.json");
654
+ const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
356
655
  if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
357
- if (removeYamlMcpEntry(join2(HOME, ".hermes", "config.yaml"))) {
358
- removed.push("~/.hermes/config.yaml (removed ijfw-memory)");
656
+ if (removeHermesIjfwWiring(join3(home, ".hermes", "config.yaml"))) {
657
+ removed.push("~/.hermes/config.yaml (removed ijfw-memory + plugin + hook wiring)");
359
658
  }
360
- const hermesSkills = removeIjfwSkills(join2(HOME, ".hermes", "skills"));
659
+ const hermesSkills = removeIjfwSkills(join3(home, ".hermes", "skills"));
361
660
  if (hermesSkills > 0) removed.push(`~/.hermes/skills/ijfw-* (removed ${hermesSkills} skill dirs)`);
362
- const hermesMd = join2(HOME, ".hermes", "HERMES.md");
363
- if (existsSync2(hermesMd)) {
661
+ const hermesMd = join3(home, ".hermes", "HERMES.md");
662
+ if (existsSync3(hermesMd)) {
364
663
  rmSync(hermesMd, { force: true });
365
664
  removed.push("~/.hermes/HERMES.md");
366
665
  }
367
- if (removeYamlMcpEntry(join2(HOME, ".wayland", "config.yaml"))) {
368
- removed.push("~/.wayland/config.yaml (removed ijfw-memory)");
666
+ const hermesPlugin = join3(home, ".hermes", "plugins", "ijfw");
667
+ if (existsSync3(hermesPlugin)) {
668
+ rmSync(hermesPlugin, { recursive: true, force: true });
669
+ removed.push("~/.hermes/plugins/ijfw/ (removed plugin tree)");
670
+ }
671
+ const waylandPluginDir = join3(home, ".wayland", "plugins", "ijfw");
672
+ if (existsSync3(waylandPluginDir)) {
673
+ rmSync(waylandPluginDir, { recursive: true, force: true });
674
+ removed.push("~/.wayland/plugins/ijfw/ (removed plugin.toml + hooks + MCP)");
369
675
  }
370
- const waylandSkills = removeIjfwSkills(join2(HOME, ".wayland", "skills"));
676
+ if (removeYamlMcpEntry(join3(home, ".wayland", "config.yaml"))) {
677
+ removed.push("~/.wayland/config.yaml (removed legacy ijfw-memory)");
678
+ }
679
+ const waylandSkills = removeIjfwSkills(join3(home, ".wayland", "skills"));
371
680
  if (waylandSkills > 0) removed.push(`~/.wayland/skills/ijfw-* (removed ${waylandSkills} skill dirs)`);
372
- const waylandMd = join2(HOME, ".wayland", "WAYLAND.md");
373
- if (existsSync2(waylandMd)) {
681
+ const waylandMd = join3(home, ".wayland", "WAYLAND.md");
682
+ if (existsSync3(waylandMd)) {
374
683
  rmSync(waylandMd, { force: true });
375
684
  removed.push("~/.wayland/WAYLAND.md");
376
685
  }
686
+ if (removeJsonMcpEntry(join3(home, ".qwen", "settings.json"))) {
687
+ removed.push("~/.qwen/settings.json (removed ijfw-memory)");
688
+ }
689
+ if (removeJsonMcpEntry(join3(home, ".kimi", "mcp.json"))) {
690
+ removed.push("~/.kimi/mcp.json (removed ijfw-memory)");
691
+ }
692
+ if (removeJsonMcpEntry(join3(home, ".gemini", "antigravity", "mcp_config.json"))) {
693
+ removed.push("~/.gemini/antigravity/mcp_config.json (removed ijfw-memory)");
694
+ }
695
+ if (removeJsonMcpEntry(join3(home, ".gemini", "config", "mcp_config.json"))) {
696
+ removed.push("~/.gemini/config/mcp_config.json (removed ijfw-memory)");
697
+ }
698
+ if (removeNestedMcpEntry(join3(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
699
+ removed.push("~/.config/opencode/opencode.json (removed mcp.ijfw-memory)");
700
+ }
701
+ if (removeNestedMcpEntry(join3(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
702
+ removed.push("~/.openclaw/openclaw.json (removed mcp.servers.ijfw-memory)");
703
+ }
704
+ const piPath = join3(home, ".pi", "agent", "AGENTS.md");
705
+ const piStatus = stripMarkerFile(piPath, { label: "~/.pi/agent/AGENTS.md" });
706
+ if (piStatus) {
707
+ removed.push(piStatus);
708
+ } else {
709
+ const piPristine = removeAiderFileIfPristine(
710
+ piPath,
711
+ resolveShippedTemplate(join3("pi", "AGENTS.md"), repoRoot)
712
+ );
713
+ if (piPristine === "removed") {
714
+ removed.push("~/.pi/agent/AGENTS.md (removed -- matched shipped template)");
715
+ } else if (piPristine === "kept-modified") {
716
+ removed.push("~/.pi/agent/AGENTS.md (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
717
+ }
718
+ }
719
+ const clineSettings = resolveClineSettingsPath(home);
720
+ if (clineSettings) {
721
+ if (removeJsonMcpEntry(clineSettings)) {
722
+ removed.push(`${clineSettings} (removed ijfw-memory)`);
723
+ }
724
+ } else {
725
+ removed.push("Cline: no globalStorage found -- if you use Cline, remove the ijfw-memory MCP entry manually.");
726
+ }
727
+ const confResult = removeAiderFileIfPristine(
728
+ join3(home, ".aider.conf.yml"),
729
+ resolveAiderTemplate("aider.conf.yml", repoRoot)
730
+ );
731
+ if (confResult === "removed") {
732
+ removed.push("~/.aider.conf.yml (removed -- matched shipped template)");
733
+ } else if (confResult === "kept-modified") {
734
+ removed.push("~/.aider.conf.yml (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
735
+ }
736
+ const convResult = removeAiderFileIfPristine(
737
+ join3(home, "CONVENTIONS.md"),
738
+ resolveAiderTemplate("CONVENTIONS.md", repoRoot)
739
+ );
740
+ if (convResult === "removed") {
741
+ removed.push("~/CONVENTIONS.md (removed -- matched shipped template)");
742
+ } else if (convResult === "kept-modified") {
743
+ removed.push("~/CONVENTIONS.md (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
744
+ }
377
745
  return removed;
378
746
  }
747
+ function parseRegistryPaths(registryPath) {
748
+ try {
749
+ if (!existsSync3(registryPath)) return [];
750
+ const raw = readFileSync3(registryPath, "utf8");
751
+ const paths = [];
752
+ for (const line of raw.split("\n")) {
753
+ const trimmed = line.trim();
754
+ if (!trimmed) continue;
755
+ const first = trimmed.split("|")[0].trim();
756
+ if (!first) continue;
757
+ if (!first.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(first)) continue;
758
+ paths.push(first);
759
+ }
760
+ return paths;
761
+ } catch {
762
+ return [];
763
+ }
764
+ }
765
+ function stripRegisteredProjectBlocks(opts = {}) {
766
+ const home = opts.home || HOME;
767
+ const cwd = opts.cwd || process.cwd();
768
+ const registryPath = opts.registryPath || join3(home, ".ijfw", "registry.md");
769
+ const results = [];
770
+ for (const projPath of parseRegistryPaths(registryPath)) {
771
+ let dirExists = false;
772
+ try {
773
+ dirExists = existsSync3(projPath);
774
+ } catch {
775
+ dirExists = false;
776
+ }
777
+ if (!dirExists) continue;
778
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
779
+ const filePath = join3(projPath, name);
780
+ const status = stripMarkerFile(filePath, { label: join3(projPath, name) });
781
+ if (status) results.push(status);
782
+ }
783
+ }
784
+ try {
785
+ const cursorRule = join3(cwd, ".cursor", "rules", "ijfw.mdc");
786
+ if (existsSync3(cursorRule)) {
787
+ backupFile(cursorRule);
788
+ rmSync(cursorRule, { force: true });
789
+ results.push(".cursor/rules/ijfw.mdc (removed -- wholly IJFW-authored)");
790
+ }
791
+ } catch {
792
+ }
793
+ const windsurfStatus = stripMarkerFile(join3(cwd, ".windsurfrules"), {
794
+ label: ".windsurfrules",
795
+ deleteIfEmpty: true
796
+ });
797
+ if (windsurfStatus) results.push(windsurfStatus);
798
+ const copilotStatus = stripMarkerFile(join3(cwd, ".github", "copilot-instructions.md"), {
799
+ label: ".github/copilot-instructions.md",
800
+ deleteIfEmpty: true
801
+ });
802
+ if (copilotStatus) results.push(copilotStatus);
803
+ return results;
804
+ }
379
805
  function resolveTarget(opt) {
380
806
  if (opt.dir) return resolve2(opt.dir);
381
807
  if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
382
- return join2(homedir2(), ".ijfw");
808
+ return join3(homedir2(), ".ijfw");
809
+ }
810
+ function assertSafePurgeTarget(target) {
811
+ let real = target;
812
+ try {
813
+ real = realpathSync(target);
814
+ } catch {
815
+ }
816
+ let home = homedir2();
817
+ try {
818
+ home = realpathSync(home);
819
+ } catch {
820
+ }
821
+ if (!real || real === "/" || real === home) {
822
+ throw new Error(`refusing to delete '${target}': it resolves to the home or filesystem root.`);
823
+ }
824
+ if (real.split("/").filter(Boolean).length < 2) {
825
+ throw new Error(`refusing to delete shallow path '${real}'.`);
826
+ }
827
+ const looksIjfw = basename(real) === ".ijfw" || existsSync3(join3(real, "state.json")) || existsSync3(join3(real, "install-method")) || existsSync3(join3(real, "install-ledger.json")) || existsSync3(join3(real, "mcp-server")) || existsSync3(join3(real, "memory"));
828
+ if (!looksIjfw) {
829
+ throw new Error(`refusing to delete '${target}': it does not look like an IJFW install (no state.json / install-method / mcp-server). Aborting.`);
830
+ }
831
+ }
832
+ function removeCreatedDirs(home, createdDirs) {
833
+ const removed = [];
834
+ for (const rel of createdDirs || []) {
835
+ const abs = join3(home, rel);
836
+ if (isEmptyDir(abs)) {
837
+ try {
838
+ rmSync(abs, { recursive: false, force: true });
839
+ removed.push(`~/${rel} (IJFW-created, now empty)`);
840
+ } catch {
841
+ }
842
+ }
843
+ }
844
+ return removed;
383
845
  }
384
846
  async function main() {
385
847
  const opts = parseArgs(process.argv);
386
848
  const target = resolveTarget(opts);
849
+ const ledgerCreatedDirs = existsSync3(target) ? readLedger(target).createdDirs : [];
387
850
  console.log("This will remove IJFW configuration. Your memory at ~/.ijfw/memory/ will be preserved. Delete manually if desired.");
388
851
  if (opts.purge) {
389
852
  console.log("WARNING: --purge will also DELETE ~/.ijfw/memory/ (project memory cannot be recovered).");
@@ -397,16 +860,18 @@ async function main() {
397
860
  }
398
861
  console.log("");
399
862
  }
400
- if (!existsSync2(target)) {
863
+ if (!existsSync3(target)) {
401
864
  console.log(`IJFW directory absent (${target}); platform cleanup only.`);
402
865
  } else if (opts.purge) {
866
+ assertSafePurgeTarget(target);
403
867
  rmSync(target, { recursive: true, force: true });
404
868
  console.log(` removed ${target} (purged).`);
405
869
  } else {
406
- const memDir = join2(target, "memory");
870
+ assertSafePurgeTarget(target);
871
+ const memDir = join3(target, "memory");
407
872
  let stash = null;
408
- if (existsSync2(memDir)) {
409
- stash = mkdtempSync(join2(tmpdir(), "ijfw-memory-"));
873
+ if (existsSync3(memDir)) {
874
+ stash = mkdtempSync(join3(tmpdir(), "ijfw-memory-"));
410
875
  cpSync(memDir, stash, { recursive: true });
411
876
  }
412
877
  rmSync(target, { recursive: true, force: true });
@@ -418,11 +883,11 @@ async function main() {
418
883
  console.log(" memory/ was not present; nothing to preserve");
419
884
  }
420
885
  }
421
- const canonicalDir = join2(HOME, ".ijfw");
886
+ const canonicalDir = join3(HOME, ".ijfw");
422
887
  const isCanonical = target === canonicalDir;
423
888
  if (isCanonical && !opts.noMarketplace) {
424
889
  const settingsPath = claudeSettingsPath();
425
- if (existsSync2(settingsPath)) {
890
+ if (existsSync3(settingsPath)) {
426
891
  unmergeMarketplace(settingsPath);
427
892
  console.log(` marketplace removed from ${settingsPath}`);
428
893
  }
@@ -433,13 +898,39 @@ async function main() {
433
898
  console.log(" platform configs cleaned:");
434
899
  for (const line of cleaned) console.log(` ${line}`);
435
900
  }
901
+ const projectCleaned = stripRegisteredProjectBlocks();
902
+ if (projectCleaned.length > 0) {
903
+ console.log(" project blocks cleaned:");
904
+ for (const line of projectCleaned) console.log(` ${line}`);
905
+ }
906
+ if (opts.purge) {
907
+ const dirsRemoved = removeCreatedDirs(HOME, ledgerCreatedDirs);
908
+ if (dirsRemoved.length > 0) {
909
+ console.log(" IJFW-created dirs removed:");
910
+ for (const line of dirsRemoved) console.log(` ${line}`);
911
+ }
912
+ }
436
913
  } else {
437
914
  console.log(` custom-dir uninstall (${target}) -- platform configs in your real home left untouched.`);
438
915
  }
439
916
  console.log("\nIJFW uninstalled. Thanks for trying it.");
440
917
  process.exit(0);
441
918
  }
442
- main().catch((e) => {
443
- console.error(e.message || String(e));
444
- process.exit(1);
445
- });
919
+ var isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
920
+ if (isDirectRun) {
921
+ main().catch((e) => {
922
+ console.error(e.message || String(e));
923
+ process.exit(1);
924
+ });
925
+ }
926
+ export {
927
+ cleanPlatforms,
928
+ parseRegistryPaths,
929
+ removeAiderFileIfPristine,
930
+ removeNestedMcpEntry,
931
+ resolveAiderTemplate,
932
+ resolveClineSettingsPath,
933
+ stripIjfwRegions,
934
+ stripMarkerFile,
935
+ stripRegisteredProjectBlocks
936
+ };