@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/ijfw.js +134 -46
- package/dist/install.js +296 -79
- package/dist/uninstall.js +543 -52
- package/docs/GUIDE.md +20 -1
- package/docs/guide/assets/ferrox-hero.png +0 -0
- package/package.json +5 -2
- package/src/install.ps1 +2 -2
- package/templates/aider/CONVENTIONS.md +54 -0
- package/templates/aider/aider.conf.yml +23 -0
- package/templates/pi/AGENTS.md +55 -0
- package/dist/hub-index-snippet.json +0 -50
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
|
|
5
|
-
import { resolve as resolve2, join as
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
260
|
+
if (!existsSync3(p)) return false;
|
|
165
261
|
backupFile(p);
|
|
166
|
-
const lines =
|
|
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 (!
|
|
277
|
+
if (!existsSync3(p)) return false;
|
|
182
278
|
let doc;
|
|
183
279
|
try {
|
|
184
|
-
doc = JSON.parse(
|
|
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 (!
|
|
371
|
+
if (!existsSync3(p)) return false;
|
|
200
372
|
let doc;
|
|
201
373
|
try {
|
|
202
|
-
doc = JSON.parse(
|
|
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 (!
|
|
244
|
-
const raw =
|
|
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 (!
|
|
513
|
+
if (!existsSync3(dir)) return 0;
|
|
280
514
|
let count = 0;
|
|
281
|
-
for (const entry of
|
|
515
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
282
516
|
if (entry.isDirectory() && entry.name.startsWith("ijfw-")) {
|
|
283
|
-
rmSync(
|
|
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 (!
|
|
548
|
+
if (!existsSync3(dir)) return 0;
|
|
315
549
|
let count = 0;
|
|
316
550
|
for (const name of CODEX_COMMAND_FILES) {
|
|
317
|
-
const path =
|
|
318
|
-
if (
|
|
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
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
338
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
346
|
-
if (
|
|
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 =
|
|
649
|
+
const cursorMcp = join3(cwd, ".cursor", "mcp.json");
|
|
351
650
|
if (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
|
|
352
|
-
if (removeJsonMcpEntry(
|
|
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 =
|
|
654
|
+
const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
|
|
356
655
|
if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
|
|
357
|
-
if (
|
|
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(
|
|
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 =
|
|
363
|
-
if (
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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 =
|
|
373
|
-
if (
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
870
|
+
assertSafePurgeTarget(target);
|
|
871
|
+
const memDir = join3(target, "memory");
|
|
407
872
|
let stash = null;
|
|
408
|
-
if (
|
|
409
|
-
stash = mkdtempSync(
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
};
|