@ijfw/install 1.6.1 → 1.6.3
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 +194 -181
- package/dist/install.js +331 -102
- package/dist/uninstall.js +112 -40
- package/package.json +3 -3
- package/src/install.ps1 +1 -1
package/dist/uninstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/uninstall.js
|
|
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";
|
|
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
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";
|
|
@@ -144,10 +144,19 @@ function resolveAiderTemplate(name, repoRoot) {
|
|
|
144
144
|
return "";
|
|
145
145
|
}
|
|
146
146
|
function writeAtomic(target, content) {
|
|
147
|
+
let mode = 384;
|
|
148
|
+
try {
|
|
149
|
+
mode = statSync(target).mode & 511;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
147
152
|
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
148
|
-
writeFileSync3(tmp, content);
|
|
153
|
+
writeFileSync3(tmp, content, { mode });
|
|
149
154
|
try {
|
|
150
155
|
renameSync2(tmp, target);
|
|
156
|
+
try {
|
|
157
|
+
chmodSync(target, mode);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
151
160
|
} catch (err) {
|
|
152
161
|
try {
|
|
153
162
|
unlinkSync2(tmp);
|
|
@@ -252,35 +261,46 @@ function stripMarkerFile(p, opts = {}) {
|
|
|
252
261
|
}
|
|
253
262
|
writeAtomic(p, stripped);
|
|
254
263
|
return `${opts.label || p} (stripped IJFW marker regions, user content preserved)`;
|
|
255
|
-
} catch {
|
|
256
|
-
return
|
|
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)`;
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
function removeTomlSection(p) {
|
|
260
269
|
if (!existsSync3(p)) return false;
|
|
261
|
-
backupFile(p);
|
|
262
270
|
const lines = readFileSync3(p, "utf8").split("\n");
|
|
263
271
|
const out = [];
|
|
264
272
|
let skip = false;
|
|
273
|
+
let changed = false;
|
|
265
274
|
for (const line of lines) {
|
|
266
275
|
if (/^\[mcp_servers\.ijfw-memory\]\s*$/.test(line)) {
|
|
267
276
|
skip = true;
|
|
277
|
+
changed = true;
|
|
268
278
|
continue;
|
|
269
279
|
}
|
|
270
280
|
if (skip && line.startsWith("[") && !line.startsWith("[mcp_servers.ijfw-memory]")) skip = false;
|
|
271
281
|
if (!skip) out.push(line);
|
|
272
282
|
}
|
|
273
|
-
|
|
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);
|
|
274
288
|
return true;
|
|
275
289
|
}
|
|
276
290
|
function removeJsonMcpEntry(p) {
|
|
277
291
|
if (!existsSync3(p)) return false;
|
|
278
|
-
let
|
|
292
|
+
let raw;
|
|
279
293
|
try {
|
|
280
|
-
|
|
294
|
+
raw = readFileSync3(p, "utf8");
|
|
281
295
|
} catch {
|
|
282
296
|
return false;
|
|
283
297
|
}
|
|
298
|
+
let doc;
|
|
299
|
+
try {
|
|
300
|
+
doc = JSON.parse(raw);
|
|
301
|
+
} catch {
|
|
302
|
+
return /\bijfw-memory\b/.test(raw) ? "parse-failed" : false;
|
|
303
|
+
}
|
|
284
304
|
if (!doc || typeof doc !== "object") return false;
|
|
285
305
|
let changed = false;
|
|
286
306
|
if (doc.mcpServers && doc.mcpServers["ijfw-memory"]) {
|
|
@@ -415,6 +435,7 @@ function removeYamlMcpEntry(p) {
|
|
|
415
435
|
if (!existsSync3(p)) return false;
|
|
416
436
|
const raw = readFileSync3(p, "utf8");
|
|
417
437
|
if (!/\bijfw-memory\b/.test(raw)) return false;
|
|
438
|
+
const bak = backupFile(p);
|
|
418
439
|
const py = spawnSync("python3", ["-c", `
|
|
419
440
|
import sys, yaml
|
|
420
441
|
p = sys.argv[1]
|
|
@@ -427,12 +448,11 @@ del srv["ijfw-memory"]
|
|
|
427
448
|
if not srv: del doc["mcp_servers"]
|
|
428
449
|
with open(p + ".tmp", "w") as f:
|
|
429
450
|
yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
|
|
430
|
-
import os
|
|
451
|
+
import os, stat
|
|
452
|
+
os.chmod(p + ".tmp", stat.S_IMODE(os.stat(p).st_mode))
|
|
453
|
+
os.replace(p + ".tmp", p)
|
|
431
454
|
`, p], { encoding: "utf8" });
|
|
432
|
-
if (py.status === 0)
|
|
433
|
-
backupFile(p);
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
455
|
+
if (py.status === 0) return true;
|
|
436
456
|
const stripped = raw.replace(
|
|
437
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.
|
|
438
458
|
/^ ijfw-memory:\n(?: .*\n)*(?:# IJFW-MCP-END ijfw-memory\n)?/m,
|
|
@@ -442,8 +462,15 @@ import os; os.replace(p + ".tmp", p)
|
|
|
442
462
|
/# IJFW-MCP-BEGIN ijfw-memory\n(?:.*\n)*?# IJFW-MCP-END ijfw-memory\n/,
|
|
443
463
|
""
|
|
444
464
|
);
|
|
445
|
-
if (stripped === raw)
|
|
446
|
-
|
|
465
|
+
if (stripped === raw) {
|
|
466
|
+
if (bak) {
|
|
467
|
+
try {
|
|
468
|
+
rmSync(bak, { force: true });
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
447
474
|
writeAtomic(p, stripped);
|
|
448
475
|
return true;
|
|
449
476
|
}
|
|
@@ -466,6 +493,7 @@ function removeHermesIjfwWiring(p) {
|
|
|
466
493
|
if (!existsSync3(p)) return false;
|
|
467
494
|
const raw = readFileSync3(p, "utf8");
|
|
468
495
|
if (!/\bijfw\b/i.test(raw)) return false;
|
|
496
|
+
const bak = backupFile(p);
|
|
469
497
|
const py = spawnSync("python3", ["-c", `
|
|
470
498
|
import sys, yaml
|
|
471
499
|
p = sys.argv[1]
|
|
@@ -497,15 +525,21 @@ if not changed: sys.exit(3)
|
|
|
497
525
|
with open(p + '.tmp', 'w') as f:
|
|
498
526
|
if doc: yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
|
|
499
527
|
else: f.write('')
|
|
500
|
-
import os
|
|
528
|
+
import os, stat
|
|
529
|
+
os.chmod(p + '.tmp', stat.S_IMODE(os.stat(p).st_mode))
|
|
530
|
+
os.replace(p + '.tmp', p)
|
|
501
531
|
`, p], { encoding: "utf8" });
|
|
502
|
-
if (py.status === 0)
|
|
503
|
-
backupFile(p);
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
532
|
+
if (py.status === 0) return true;
|
|
506
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, "");
|
|
507
|
-
if (out === raw)
|
|
508
|
-
|
|
534
|
+
if (out === raw) {
|
|
535
|
+
if (bak) {
|
|
536
|
+
try {
|
|
537
|
+
rmSync(bak, { force: true });
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
509
543
|
writeAtomic(p, out);
|
|
510
544
|
return true;
|
|
511
545
|
}
|
|
@@ -579,7 +613,9 @@ function removeCodexHookFiles(hooksDir) {
|
|
|
579
613
|
} catch {
|
|
580
614
|
continue;
|
|
581
615
|
}
|
|
582
|
-
|
|
616
|
+
const header = body.split("\n", 4).slice(0, 4);
|
|
617
|
+
if (header.some((l) => /^#\s*IJFW\b/.test(l))) {
|
|
618
|
+
backupFile(p);
|
|
583
619
|
rmSync(p, { force: true });
|
|
584
620
|
count++;
|
|
585
621
|
}
|
|
@@ -615,7 +651,15 @@ function cleanPlatforms(opts = {}) {
|
|
|
615
651
|
const cwd = opts.cwd || process.cwd();
|
|
616
652
|
const repoRoot = opts.repoRoot || REPO_ROOT;
|
|
617
653
|
const removed = [];
|
|
618
|
-
|
|
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")) {
|
|
619
663
|
removed.push("~/.claude/settings.json (removed ijfw-memory mcp entry)");
|
|
620
664
|
}
|
|
621
665
|
if (removeKnownMarketplacesEntry(join3(home, ".claude", "plugins", "known_marketplaces.json"))) {
|
|
@@ -638,7 +682,7 @@ function cleanPlatforms(opts = {}) {
|
|
|
638
682
|
}
|
|
639
683
|
const codexHookFiles = removeCodexHookFiles(join3(home, ".codex", "hooks"));
|
|
640
684
|
if (codexHookFiles > 0) removed.push(`~/.codex/hooks/ (removed ${codexHookFiles} IJFW hook scripts)`);
|
|
641
|
-
if (
|
|
685
|
+
if (rmJsonEntry(join3(home, ".gemini", "settings.json"), "~/.gemini/settings.json")) {
|
|
642
686
|
removed.push("~/.gemini/settings.json (removed ijfw-memory)");
|
|
643
687
|
}
|
|
644
688
|
const geminiExt = join3(home, ".gemini", "extensions", "ijfw");
|
|
@@ -647,12 +691,14 @@ function cleanPlatforms(opts = {}) {
|
|
|
647
691
|
removed.push("~/.gemini/extensions/ijfw/");
|
|
648
692
|
}
|
|
649
693
|
const cursorMcp = join3(cwd, ".cursor", "mcp.json");
|
|
650
|
-
if (
|
|
651
|
-
if (
|
|
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")) {
|
|
652
696
|
removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
|
|
653
697
|
}
|
|
654
698
|
const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
|
|
655
|
-
|
|
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)");
|
|
656
702
|
if (removeHermesIjfwWiring(join3(home, ".hermes", "config.yaml"))) {
|
|
657
703
|
removed.push("~/.hermes/config.yaml (removed ijfw-memory + plugin + hook wiring)");
|
|
658
704
|
}
|
|
@@ -683,16 +729,16 @@ function cleanPlatforms(opts = {}) {
|
|
|
683
729
|
rmSync(waylandMd, { force: true });
|
|
684
730
|
removed.push("~/.wayland/WAYLAND.md");
|
|
685
731
|
}
|
|
686
|
-
if (
|
|
732
|
+
if (rmJsonEntry(join3(home, ".qwen", "settings.json"), "~/.qwen/settings.json")) {
|
|
687
733
|
removed.push("~/.qwen/settings.json (removed ijfw-memory)");
|
|
688
734
|
}
|
|
689
|
-
if (
|
|
735
|
+
if (rmJsonEntry(join3(home, ".kimi", "mcp.json"), "~/.kimi/mcp.json")) {
|
|
690
736
|
removed.push("~/.kimi/mcp.json (removed ijfw-memory)");
|
|
691
737
|
}
|
|
692
|
-
if (
|
|
738
|
+
if (rmJsonEntry(join3(home, ".gemini", "antigravity", "mcp_config.json"), "~/.gemini/antigravity/mcp_config.json")) {
|
|
693
739
|
removed.push("~/.gemini/antigravity/mcp_config.json (removed ijfw-memory)");
|
|
694
740
|
}
|
|
695
|
-
if (
|
|
741
|
+
if (rmJsonEntry(join3(home, ".gemini", "config", "mcp_config.json"), "~/.gemini/config/mcp_config.json")) {
|
|
696
742
|
removed.push("~/.gemini/config/mcp_config.json (removed ijfw-memory)");
|
|
697
743
|
}
|
|
698
744
|
if (removeNestedMcpEntry(join3(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
|
|
@@ -718,7 +764,7 @@ function cleanPlatforms(opts = {}) {
|
|
|
718
764
|
}
|
|
719
765
|
const clineSettings = resolveClineSettingsPath(home);
|
|
720
766
|
if (clineSettings) {
|
|
721
|
-
if (
|
|
767
|
+
if (rmJsonEntry(clineSettings, clineSettings)) {
|
|
722
768
|
removed.push(`${clineSettings} (removed ijfw-memory)`);
|
|
723
769
|
}
|
|
724
770
|
} else {
|
|
@@ -818,15 +864,25 @@ function assertSafePurgeTarget(target) {
|
|
|
818
864
|
home = realpathSync(home);
|
|
819
865
|
} catch {
|
|
820
866
|
}
|
|
821
|
-
|
|
867
|
+
const isFsRoot = (p) => p === "/" || /^[A-Za-z]:[\\/]?$/.test(p);
|
|
868
|
+
if (!real || isFsRoot(real) || real === home) {
|
|
822
869
|
throw new Error(`refusing to delete '${target}': it resolves to the home or filesystem root.`);
|
|
823
870
|
}
|
|
824
|
-
|
|
871
|
+
const segs = real.split(/[\\/]+/).filter((s) => s && !/^[A-Za-z]:$/.test(s));
|
|
872
|
+
if (segs.length < 2) {
|
|
825
873
|
throw new Error(`refusing to delete shallow path '${real}'.`);
|
|
826
874
|
}
|
|
827
|
-
const
|
|
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();
|
|
828
884
|
if (!looksIjfw) {
|
|
829
|
-
throw new Error(`refusing to delete '${target}': it does not look like an IJFW install (no
|
|
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.`);
|
|
830
886
|
}
|
|
831
887
|
}
|
|
832
888
|
function removeCreatedDirs(home, createdDirs) {
|
|
@@ -846,13 +902,26 @@ function removeCreatedDirs(home, createdDirs) {
|
|
|
846
902
|
async function main() {
|
|
847
903
|
const opts = parseArgs(process.argv);
|
|
848
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"));
|
|
849
913
|
const ledgerCreatedDirs = existsSync3(target) ? readLedger(target).createdDirs : [];
|
|
850
914
|
console.log("This will remove IJFW configuration. Your memory at ~/.ijfw/memory/ will be preserved. Delete manually if desired.");
|
|
851
915
|
if (opts.purge) {
|
|
852
916
|
console.log("WARNING: --purge will also DELETE ~/.ijfw/memory/ (project memory cannot be recovered).");
|
|
853
917
|
}
|
|
854
918
|
console.log("");
|
|
855
|
-
if (!opts.yes
|
|
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
|
+
}
|
|
856
925
|
const ok = await confirm("Proceed with IJFW uninstall? [y/N] ");
|
|
857
926
|
if (!ok) {
|
|
858
927
|
console.log("Uninstall cancelled. Nothing was changed.");
|
|
@@ -883,8 +952,6 @@ async function main() {
|
|
|
883
952
|
console.log(" memory/ was not present; nothing to preserve");
|
|
884
953
|
}
|
|
885
954
|
}
|
|
886
|
-
const canonicalDir = join3(HOME, ".ijfw");
|
|
887
|
-
const isCanonical = target === canonicalDir;
|
|
888
955
|
if (isCanonical && !opts.noMarketplace) {
|
|
889
956
|
const settingsPath = claudeSettingsPath();
|
|
890
957
|
if (existsSync3(settingsPath)) {
|
|
@@ -924,10 +991,15 @@ if (isDirectRun) {
|
|
|
924
991
|
});
|
|
925
992
|
}
|
|
926
993
|
export {
|
|
994
|
+
assertSafePurgeTarget,
|
|
927
995
|
cleanPlatforms,
|
|
928
996
|
parseRegistryPaths,
|
|
929
997
|
removeAiderFileIfPristine,
|
|
998
|
+
removeCodexHookFiles,
|
|
999
|
+
removeJsonMcpEntry,
|
|
930
1000
|
removeNestedMcpEntry,
|
|
1001
|
+
removeTomlSection,
|
|
1002
|
+
removeYamlMcpEntry,
|
|
931
1003
|
resolveAiderTemplate,
|
|
932
1004
|
resolveClineSettingsPath,
|
|
933
1005
|
stripIjfwRegions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/install",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "One-command installer for IJFW -- the AI efficiency layer. One install, every AI coding agent, zero config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "node scripts/build.js",
|
|
26
|
-
"test": "node --test test.js test
|
|
26
|
+
"test": "node --test test.js test/*.test.mjs",
|
|
27
27
|
"test:hub-extension": "node --test test/test-pack-hub-extension.js",
|
|
28
28
|
"test:uninstall-completeness": "node --test test/uninstall-completeness.test.mjs",
|
|
29
29
|
"preflight": "node dist/ijfw.js preflight",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"prepublishOnly": "npm run build && npm run preflight && npm run pack:check"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"esbuild": "^0.28.
|
|
36
|
+
"esbuild": "^0.28.1",
|
|
37
37
|
"marked": "^18.0.2"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
package/src/install.ps1
CHANGED
|
@@ -95,7 +95,7 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
95
95
|
# Self-heal stale origin URLs across host migrations (1.2.9 parity with install.js).
|
|
96
96
|
# Without this, Windows users on the pre-GitLab origin still 404 on every upgrade.
|
|
97
97
|
# V155-012: only rewrite ORIGINS THAT MATCH KNOWN STALE PATTERNS. Previously
|
|
98
|
-
# this clobbered SSH remotes, forks, and any user-customized origin
|
|
98
|
+
# this clobbered SSH remotes, forks, and any user-customized origin -- anyone
|
|
99
99
|
# working on the IJFW source itself ended up silently retargeted to upstream.
|
|
100
100
|
# Port the install.js STALE_PATTERNS allowlist verbatim (case-insensitive).
|
|
101
101
|
$currentOrigin = ($currentOriginRaw | Out-String).Trim()
|