@ijfw/install 1.6.1 → 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,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 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)`;
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
- 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);
274
288
  return true;
275
289
  }
276
290
  function removeJsonMcpEntry(p) {
277
291
  if (!existsSync3(p)) return false;
278
- let doc;
292
+ let raw;
279
293
  try {
280
- doc = JSON.parse(readFileSync3(p, "utf8"));
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; 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)
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) return false;
446
- backupFile(p);
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; os.replace(p + '.tmp', p)
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) return false;
508
- backupFile(p);
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
- if (/\bIJFW\b/.test(body) || /\bijfw\b/.test(body)) {
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
- if (removeJsonMcpEntry(join3(home, ".claude", "settings.json"))) {
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 (removeJsonMcpEntry(join3(home, ".gemini", "settings.json"))) {
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 (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
651
- if (removeJsonMcpEntry(join3(home, ".codeium", "windsurf", "mcp_config.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")) {
652
696
  removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
653
697
  }
654
698
  const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
655
- if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
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 (removeJsonMcpEntry(join3(home, ".qwen", "settings.json"))) {
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 (removeJsonMcpEntry(join3(home, ".kimi", "mcp.json"))) {
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 (removeJsonMcpEntry(join3(home, ".gemini", "antigravity", "mcp_config.json"))) {
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 (removeJsonMcpEntry(join3(home, ".gemini", "config", "mcp_config.json"))) {
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 (removeJsonMcpEntry(clineSettings)) {
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
- if (!real || real === "/" || real === home) {
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
- if (real.split("/").filter(Boolean).length < 2) {
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 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"));
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 state.json / install-method / mcp-server). Aborting.`);
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 && 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
+ }
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.1",
3
+ "version": "1.6.2",
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/uninstall-completeness.test.mjs test/project-write-guard.test.mjs",
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",
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 anyone
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()