@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/install.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
6
11
  };
7
12
  var __export = (target, all) => {
8
13
  for (var name in all)
@@ -292,21 +297,42 @@ function prettyName(targetId) {
292
297
  };
293
298
  return map[targetId] || String(targetId);
294
299
  }
300
+ function backupTimestamp() {
301
+ const d = /* @__PURE__ */ new Date();
302
+ const p = (n) => String(n).padStart(2, "0");
303
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
304
+ }
295
305
  function readJsonOrEmpty(path3) {
296
306
  if (!existsSync3(path3)) return {};
307
+ let raw;
308
+ try {
309
+ raw = readFileSync2(path3, "utf8");
310
+ } catch (err) {
311
+ const msg = err && err.message ? err.message : String(err);
312
+ throw new Error(`cannot read existing config at ${path3} (${msg}) -- fix permissions and re-run.`);
313
+ }
314
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
315
+ if (!raw || raw.trim() === "") return {};
297
316
  try {
298
- const raw = readFileSync2(path3, "utf8");
299
- if (!raw || raw.trim() === "") return {};
300
317
  const parsed = JSON.parse(raw);
301
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
302
- return parsed;
318
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
303
319
  } catch {
304
- return {};
305
320
  }
321
+ const bak = `${path3}.corrupt-${backupTimestamp()}.bak`;
322
+ try {
323
+ copyFileSync(path3, bak);
324
+ } catch (err) {
325
+ const msg = err && err.message ? err.message : String(err);
326
+ throw new Error(
327
+ `existing config at ${path3} is not valid JSON and a safety copy could not be written (${msg}) -- fix or move the file, then re-run.`
328
+ );
329
+ }
330
+ printWarn(`existing config at ${path3} is not valid JSON -- original saved to ${bak}; starting fresh with the IJFW entry only`);
331
+ return {};
306
332
  }
307
333
  function mergeJson(dst, serverJs, ts) {
308
334
  mkdirSync2(dirname3(dst), { recursive: true });
309
- requireBackup(dst, ts);
335
+ requireBackup(dst, ts || backupTimestamp());
310
336
  const doc = readJsonOrEmpty(dst);
311
337
  if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
312
338
  const isWin = IS_WIN;
@@ -334,7 +360,7 @@ function mergeJson(dst, serverJs, ts) {
334
360
  }
335
361
  function mergeToml(dst, serverJs, ts) {
336
362
  mkdirSync2(dirname3(dst), { recursive: true });
337
- requireBackup(dst, ts);
363
+ requireBackup(dst, ts || backupTimestamp());
338
364
  let text = "";
339
365
  try {
340
366
  text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
@@ -385,32 +411,6 @@ function stripTomlSection(text, sectionName) {
385
411
  }
386
412
  return out.join("\n");
387
413
  }
388
- function mergeYamlMcp(dst, serverJs, ts) {
389
- mkdirSync2(dirname3(dst), { recursive: true });
390
- if (ts) backup(dst, ts);
391
- let text = "";
392
- try {
393
- text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
394
- } catch {
395
- text = "";
396
- }
397
- text = stripSentinelBlock(text, "# IJFW-MCP-BEGIN ijfw-memory", "# IJFW-MCP-END ijfw-memory");
398
- if (!/^mcp_servers:/m.test(text)) {
399
- if (text && !text.endsWith("\n")) text += "\n";
400
- text += "\nmcp_servers:\n";
401
- }
402
- const escaped = String(serverJs).replace(/"/g, '\\"');
403
- let block = "";
404
- if (!text.endsWith("\n")) block += "\n";
405
- block += "# IJFW-MCP-BEGIN ijfw-memory\n";
406
- block += " ijfw-memory:\n";
407
- block += ' command: "node"\n';
408
- block += ` args: ["${escaped}"]
409
- `;
410
- block += " enabled: true\n";
411
- block += "# IJFW-MCP-END ijfw-memory\n";
412
- writeAtomic(dst, text + block, { mode: 384 });
413
- }
414
414
  function stripSentinelBlock(text, beginMark, endMark) {
415
415
  const lines = text.split("\n");
416
416
  const out = [];
@@ -500,7 +500,7 @@ function isIndentedEnabledLine(line) {
500
500
  }
501
501
  function opencodeMerge(dst, serverJs, ts) {
502
502
  mkdirSync2(dirname3(dst), { recursive: true });
503
- if (ts) backup(dst, ts);
503
+ backup(dst, ts || backupTimestamp());
504
504
  const doc = readJsonOrEmpty(dst);
505
505
  if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
506
506
  doc.mcp["ijfw-memory"] = { type: "local", command: ["node", serverJs] };
@@ -508,7 +508,7 @@ function opencodeMerge(dst, serverJs, ts) {
508
508
  }
509
509
  function openclawMerge(dst, serverJs, ts) {
510
510
  mkdirSync2(dirname3(dst), { recursive: true });
511
- if (ts) backup(dst, ts);
511
+ backup(dst, ts || backupTimestamp());
512
512
  const doc = readJsonOrEmpty(dst);
513
513
  if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
514
514
  if (!doc.mcp.servers || typeof doc.mcp.servers !== "object") doc.mcp.servers = {};
@@ -554,7 +554,7 @@ function clineMerge(serverJs, home, ts) {
554
554
  if (!userDir) userDir = osDefault;
555
555
  const dst = join3(userDir, "globalStorage", ext, "settings", "cline_mcp_settings.json");
556
556
  mkdirSync2(dirname3(dst), { recursive: true });
557
- if (ts) backup(dst, ts);
557
+ backup(dst, ts || backupTimestamp());
558
558
  const doc = readJsonOrEmpty(dst);
559
559
  if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
560
560
  doc.mcpServers["ijfw-memory"] = {
@@ -633,6 +633,9 @@ function ensureDir(p) {
633
633
  } catch {
634
634
  }
635
635
  }
636
+ function stripBom(s) {
637
+ return s && s.charCodeAt(0) === 65279 ? s.slice(1) : s;
638
+ }
636
639
  function copyIfAbsent(src, dst) {
637
640
  if (!existsSync4(src)) return false;
638
641
  if (existsSync4(dst)) return false;
@@ -716,13 +719,18 @@ async function installClaude(ctx) {
716
719
  "known_marketplaces.json"
717
720
  );
718
721
  ensureDir(join4(ctx.home, ".claude", "plugins"));
719
- backup(claudeSettings, ctx.ts);
722
+ const settingsBak = requireBackup(claudeSettings, ctx.ts);
720
723
  let settings = {};
721
724
  if (existsSync4(claudeSettings)) {
722
725
  try {
723
- settings = JSON.parse(readFileSync3(claudeSettings, "utf8") || "{}");
726
+ settings = JSON.parse(stripBom(readFileSync3(claudeSettings, "utf8")) || "{}");
724
727
  } catch {
725
- settings = {};
728
+ ctx.log.warn("~/.claude/settings.json could not be parsed as JSON -- IJFW will not modify it.");
729
+ if (settingsBak) {
730
+ ctx.log.warn(`A copy of the current file was preserved at ${settingsBak}.`);
731
+ }
732
+ ctx.log.warn("Fix the JSON syntax error and re-run `ijfw install`.");
733
+ return { status: "noop" };
726
734
  }
727
735
  }
728
736
  if (!settings || typeof settings !== "object") settings = {};
@@ -736,7 +744,7 @@ async function installClaude(ctx) {
736
744
  let mp = {};
737
745
  if (existsSync4(claudeMarketplaces)) {
738
746
  try {
739
- mp = JSON.parse(readFileSync3(claudeMarketplaces, "utf8") || "{}");
747
+ mp = JSON.parse(stripBom(readFileSync3(claudeMarketplaces, "utf8")) || "{}");
740
748
  } catch {
741
749
  mp = {};
742
750
  }
@@ -785,7 +793,7 @@ async function installCodex(ctx) {
785
793
  }
786
794
  const configToml = join4(ctx.home, ".codex", "config.toml");
787
795
  ensureDir(dirname4(configToml));
788
- mergeToml(configToml, ctx.serverJsNative);
796
+ mergeToml(configToml, ctx.serverJsNative, ctx.ts);
789
797
  const hooksDst = join4(ctx.home, ".codex", "hooks.json");
790
798
  const hooksSrc = join4(ctx.repoRoot, "codex", ".codex", "hooks.json");
791
799
  const hooksBase = join4(ctx.home, ".codex", "hooks");
@@ -914,18 +922,41 @@ async function installGemini(ctx) {
914
922
  }
915
923
  const dst = join4(ctx.home, ".gemini", "settings.json");
916
924
  ensureDir(dirname4(dst));
917
- mergeJson(dst, ctx.serverJsNative);
925
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
918
926
  const extDst = join4(ctx.home, ".gemini", "extensions", "ijfw");
919
927
  const extSrc = join4(ctx.repoRoot, "gemini", "extensions", "ijfw");
920
928
  for (const sub of ["hooks", "skills", "commands", "agents", "policies"]) {
921
929
  ensureDir(join4(extDst, sub));
922
930
  }
923
- for (const rel of [
924
- "gemini-extension.json",
925
- "IJFW.md",
926
- "hooks/hooks.json",
927
- "policies/ijfw.toml"
928
- ]) {
931
+ for (const rel of ["gemini-extension.json", "hooks/hooks.json"]) {
932
+ const srcFile = join4(extSrc, rel);
933
+ if (!existsSync4(srcFile)) continue;
934
+ let desired = "";
935
+ try {
936
+ desired = readFileSync3(srcFile, "utf8");
937
+ } catch {
938
+ continue;
939
+ }
940
+ desired = desired.split("{{extensionPath}}").join(extDst);
941
+ const dstFile = join4(extDst, rel);
942
+ let current = null;
943
+ try {
944
+ current = existsSync4(dstFile) ? readFileSync3(dstFile, "utf8") : null;
945
+ } catch {
946
+ current = null;
947
+ }
948
+ if (current === desired) continue;
949
+ if (current !== null) {
950
+ try {
951
+ copyFileSync2(dstFile, `${dstFile}.bak.${ctx.ts}`);
952
+ } catch {
953
+ }
954
+ ctx.log.note(`Updated ${rel} (previous copy backed up to ${rel}.bak.${ctx.ts})`);
955
+ }
956
+ ensureDir(dirname4(dstFile));
957
+ writeAtomic(dstFile, desired);
958
+ }
959
+ for (const rel of ["IJFW.md", "policies/ijfw.toml"]) {
929
960
  const dstFile = join4(extDst, rel);
930
961
  if (!existsSync4(dstFile)) {
931
962
  ensureDir(dirname4(dstFile));
@@ -1051,6 +1082,101 @@ function renderWaylandPluginToml(ctx) {
1051
1082
  ""
1052
1083
  ].join("\n");
1053
1084
  }
1085
+ function stripSentinelLines(text, beginMark, endMark) {
1086
+ const lines = text.split("\n");
1087
+ const out = [];
1088
+ let skip = false;
1089
+ for (const line of lines) {
1090
+ if (line === beginMark) {
1091
+ skip = true;
1092
+ continue;
1093
+ }
1094
+ if (line === endMark) {
1095
+ skip = false;
1096
+ continue;
1097
+ }
1098
+ if (skip) continue;
1099
+ out.push(line);
1100
+ }
1101
+ return out.join("\n");
1102
+ }
1103
+ function hermesMergeYamlMcp(ctx, dst, serverJs) {
1104
+ ensureDir(dirname4(dst));
1105
+ requireBackup(dst, ctx.ts);
1106
+ let text = "";
1107
+ try {
1108
+ text = existsSync4(dst) ? stripBom(readFileSync3(dst, "utf8")) : "";
1109
+ } catch {
1110
+ text = "";
1111
+ }
1112
+ text = stripSentinelLines(text, HERMES_MCP_BEGIN, HERMES_MCP_END);
1113
+ const escaped = String(serverJs).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1114
+ const block = [
1115
+ HERMES_MCP_BEGIN,
1116
+ " ijfw-memory:",
1117
+ ' command: "node"',
1118
+ ` args: ["${escaped}"]`,
1119
+ " enabled: true",
1120
+ HERMES_MCP_END
1121
+ ];
1122
+ const lines = text.split("\n");
1123
+ let anchorIdx = -1;
1124
+ for (let i = 0; i < lines.length; i++) {
1125
+ const line = lines[i];
1126
+ if (line.startsWith("mcp_servers:")) {
1127
+ const rest = line.slice("mcp_servers:".length).trim();
1128
+ if (rest === "" || rest.startsWith("#")) {
1129
+ anchorIdx = i;
1130
+ break;
1131
+ }
1132
+ const beforeComment = rest.split("#")[0].replace(/\s/g, "");
1133
+ if (beforeComment === "{}") {
1134
+ lines[i] = "mcp_servers:";
1135
+ anchorIdx = i;
1136
+ break;
1137
+ }
1138
+ }
1139
+ }
1140
+ let merged;
1141
+ if (anchorIdx >= 0) {
1142
+ lines.splice(anchorIdx + 1, 0, ...block);
1143
+ merged = lines.join("\n");
1144
+ } else if (/^mcp_servers:/m.test(text)) {
1145
+ ctx.log.warn("Hermes config.yaml uses an inline mcp_servers map -- cannot merge safely.");
1146
+ ctx.log.warn('Add an "ijfw-memory" entry to mcp_servers manually (command: node, args: [server.js path]).');
1147
+ return false;
1148
+ } else {
1149
+ const prefix = text.trim() === "" ? "" : (text.endsWith("\n") ? text : `${text}
1150
+ `) + "\n";
1151
+ merged = `${prefix}mcp_servers:
1152
+ ${block.join("\n")}`;
1153
+ }
1154
+ if (!merged.endsWith("\n")) merged += "\n";
1155
+ writeAtomic(dst, merged, { mode: 384 });
1156
+ return true;
1157
+ }
1158
+ function hermesInlineEnabledHas(dst, pluginName) {
1159
+ let text = "";
1160
+ try {
1161
+ text = existsSync4(dst) ? readFileSync3(dst, "utf8") : "";
1162
+ } catch {
1163
+ return false;
1164
+ }
1165
+ const esc = pluginName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1166
+ const nameRe = new RegExp(`[\\[,]\\s*["']?${esc}["']?\\s*[,\\]]`);
1167
+ let inPlugins = false;
1168
+ for (const line of text.split("\n")) {
1169
+ if (/^plugins:\s*$/.test(line)) {
1170
+ inPlugins = true;
1171
+ continue;
1172
+ }
1173
+ if (inPlugins && /^\S/.test(line) && line.trim() !== "") inPlugins = false;
1174
+ if (inPlugins && /^\s+enabled:\s*\[.+\]\s*$/.test(line) && nameRe.test(line)) {
1175
+ return true;
1176
+ }
1177
+ }
1178
+ return false;
1179
+ }
1054
1180
  async function installHermes(ctx) {
1055
1181
  if (ctx.ijfwCustomDir) {
1056
1182
  return customDirNoop(
@@ -1062,7 +1188,7 @@ async function installHermes(ctx) {
1062
1188
  }
1063
1189
  const dst = join4(ctx.home, ".hermes", "config.yaml");
1064
1190
  ensureDir(dirname4(dst));
1065
- mergeYamlMcp(dst, ctx.serverJsNative);
1191
+ hermesMergeYamlMcp(ctx, dst, ctx.serverJsNative);
1066
1192
  ensureDir(join4(ctx.home, ".hermes"));
1067
1193
  copyIfAbsent(
1068
1194
  join4(ctx.repoRoot, "hermes", "HERMES.md"),
@@ -1087,40 +1213,44 @@ async function installHermes(ctx) {
1087
1213
  }
1088
1214
  if (readdirErr) {
1089
1215
  ctx.log.warn(`Hermes plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
1090
- }
1091
- const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
1092
- let dstEntries = [];
1093
- try {
1094
- dstEntries = readdirSync(pluginDst);
1095
- } catch {
1096
- }
1097
- for (const name of dstEntries) {
1098
- if (name === "__pycache__") continue;
1099
- if (!srcNames.has(name)) {
1100
- try {
1101
- rmSync(join4(pluginDst, name), { recursive: true, force: true });
1102
- } catch (err) {
1103
- ctx.log.warn(`Hermes plugin: could not remove stale ${name}: ${err.message || err}`);
1216
+ ctx.log.warn("Leaving the installed Hermes plugin tree untouched.");
1217
+ } else {
1218
+ const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
1219
+ let dstEntries = [];
1220
+ try {
1221
+ dstEntries = readdirSync(pluginDst);
1222
+ } catch {
1223
+ }
1224
+ for (const name of dstEntries) {
1225
+ if (name === "__pycache__") continue;
1226
+ if (!srcNames.has(name)) {
1227
+ try {
1228
+ rmSync(join4(pluginDst, name), { recursive: true, force: true });
1229
+ } catch (err) {
1230
+ ctx.log.warn(`Hermes plugin: could not remove stale ${name}: ${err.message || err}`);
1231
+ }
1104
1232
  }
1105
1233
  }
1106
- }
1107
- for (const name of entries) {
1108
- if (name === "__pycache__") continue;
1109
- const src = join4(pluginSrc, name);
1110
- const dstEntry = join4(pluginDst, name);
1111
- try {
1112
- const st = statSync2(src);
1113
- if (st.isDirectory()) {
1114
- cpSync(src, dstEntry, { recursive: true, force: true });
1115
- } else if (st.isFile()) {
1116
- copyFileSync2(src, dstEntry);
1234
+ for (const name of entries) {
1235
+ if (name === "__pycache__") continue;
1236
+ const src = join4(pluginSrc, name);
1237
+ const dstEntry = join4(pluginDst, name);
1238
+ try {
1239
+ const st = statSync2(src);
1240
+ if (st.isDirectory()) {
1241
+ cpSync(src, dstEntry, { recursive: true, force: true });
1242
+ } else if (st.isFile()) {
1243
+ copyFileSync2(src, dstEntry);
1244
+ }
1245
+ } catch {
1117
1246
  }
1118
- } catch {
1119
1247
  }
1120
1248
  }
1121
1249
  }
1122
- mergeYamlPluginsEnabled(dst, "ijfw");
1123
- mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py", ctx.ts);
1250
+ if (!hermesInlineEnabledHas(dst, "ijfw")) {
1251
+ mergeYamlPluginsEnabled(dst, "ijfw");
1252
+ }
1253
+ mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py");
1124
1254
  ctx.log.ok("Installed Hermes bundle: MCP + HERMES.md + skills + plugin + tier-2 hook");
1125
1255
  return { status: "ok" };
1126
1256
  }
@@ -1148,13 +1278,13 @@ async function installCursor(ctx) {
1148
1278
  }
1149
1279
  const dst = join4(cwd, ".cursor", "mcp.json");
1150
1280
  ensureDir(dirname4(dst));
1151
- mergeJson(dst, ctx.serverJsNative);
1281
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
1152
1282
  const rulesDir = join4(cwd, ".cursor", "rules");
1153
1283
  ensureDir(rulesDir);
1154
1284
  const ruleSrc = join4(ctx.repoRoot, "cursor", ".cursor", "rules", "ijfw.mdc");
1155
1285
  if (existsSync4(ruleSrc)) {
1156
1286
  try {
1157
- copyFileSync2(ruleSrc, join4(rulesDir, "ijfw.mdc"));
1287
+ installHook(ruleSrc, join4(rulesDir, "ijfw.mdc"), ctx.ts);
1158
1288
  } catch {
1159
1289
  }
1160
1290
  }
@@ -1172,7 +1302,7 @@ async function installWindsurf(ctx) {
1172
1302
  }
1173
1303
  const dst = join4(ctx.home, ".codeium", "windsurf", "mcp_config.json");
1174
1304
  ensureDir(dirname4(dst));
1175
- mergeJson(dst, ctx.serverJsNative);
1305
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
1176
1306
  const cwd = ctx.cwd || process.cwd();
1177
1307
  if (!guardProjectWrite(cwd, ctx.home, {
1178
1308
  platformLabel: "Windsurf project rules (.windsurfrules)",
@@ -1198,9 +1328,12 @@ async function installWindsurf(ctx) {
1198
1328
  }
1199
1329
  return { status: "ok" };
1200
1330
  }
1331
+ var HERMES_MCP_BEGIN, HERMES_MCP_END;
1201
1332
  var init_install_targets_1_7 = __esm({
1202
1333
  "src/install-targets-1-7.js"() {
1203
1334
  init_install_helpers();
1335
+ HERMES_MCP_BEGIN = "# IJFW-MCP-BEGIN ijfw-memory";
1336
+ HERMES_MCP_END = "# IJFW-MCP-END ijfw-memory";
1204
1337
  }
1205
1338
  });
1206
1339
 
@@ -1230,6 +1363,30 @@ function commandExists(name) {
1230
1363
  const r = spawnSync(probeCmd, [name], { stdio: "ignore" });
1231
1364
  return r.status === 0;
1232
1365
  }
1366
+ function vscodeMcpMerge(dst, serverJs, ts) {
1367
+ ensureDir2(path.dirname(dst));
1368
+ requireBackup(dst, ts);
1369
+ let doc = {};
1370
+ try {
1371
+ if (fs.existsSync(dst)) {
1372
+ let raw = fs.readFileSync(dst, "utf8");
1373
+ if (raw && raw.charCodeAt(0) === 65279) raw = raw.slice(1);
1374
+ if (raw.trim() !== "") {
1375
+ const parsed = JSON.parse(raw);
1376
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) doc = parsed;
1377
+ }
1378
+ }
1379
+ } catch {
1380
+ doc = {};
1381
+ }
1382
+ if (!doc.servers || typeof doc.servers !== "object") doc.servers = {};
1383
+ if (doc.mcpServers && typeof doc.mcpServers === "object") {
1384
+ delete doc.mcpServers["ijfw-memory"];
1385
+ if (Object.keys(doc.mcpServers).length === 0) delete doc.mcpServers;
1386
+ }
1387
+ doc.servers["ijfw-memory"] = { type: "stdio", command: "node", args: [serverJs] };
1388
+ writeAtomic(dst, JSON.stringify(doc, null, 2) + "\n", { mode: 384 });
1389
+ }
1233
1390
  function installCopilot(ctx) {
1234
1391
  if (ctx.ijfwCustomDir) {
1235
1392
  printInfo("Custom-dir install -- skipping Copilot project writes.");
@@ -1251,7 +1408,7 @@ function installCopilot(ctx) {
1251
1408
  }
1252
1409
  const dst = path.join(cwd, ".vscode", "mcp.json");
1253
1410
  ensureDir2(path.dirname(dst));
1254
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1411
+ vscodeMcpMerge(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1255
1412
  const rulesDst = path.join(cwd, ".github", "copilot-instructions.md");
1256
1413
  const rulesSrc = path.join(ctx.repoRoot, "copilot", "copilot-instructions.md");
1257
1414
  const wroteRules = copyIfMissing(rulesSrc, rulesDst);
@@ -1270,7 +1427,7 @@ function installOpencode(ctx) {
1270
1427
  }
1271
1428
  const dst = path.join(ctx.home, ".config", "opencode", "opencode.json");
1272
1429
  ensureDir2(path.dirname(dst));
1273
- opencodeMerge(dst, ctx.serverJsNative || ctx.serverJs);
1430
+ opencodeMerge(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1274
1431
  printOk(`Merged MCP into ${dst} (opencode mcp.local schema)`);
1275
1432
  return { status: "ok" };
1276
1433
  }
@@ -1282,7 +1439,7 @@ function installQwen(ctx) {
1282
1439
  }
1283
1440
  const dst = path.join(ctx.home, ".qwen", "settings.json");
1284
1441
  ensureDir2(path.dirname(dst));
1285
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1442
+ mergeJson(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1286
1443
  printOk(`Merged MCP into ${dst}`);
1287
1444
  return { status: "ok" };
1288
1445
  }
@@ -1292,7 +1449,7 @@ function installCline(ctx) {
1292
1449
  printOk("Cline: real platform config left untouched.");
1293
1450
  return { status: "noop" };
1294
1451
  }
1295
- const dst = clineMerge(ctx.serverJsNative || ctx.serverJs, ctx.home);
1452
+ const dst = clineMerge(ctx.serverJsNative || ctx.serverJs, ctx.home, ctx.ts);
1296
1453
  printOk(`Merged MCP into ${dst} (cline globalStorage schema)`);
1297
1454
  return { status: "ok" };
1298
1455
  }
@@ -1304,7 +1461,7 @@ function installKimi(ctx) {
1304
1461
  }
1305
1462
  const dst = path.join(ctx.home, ".kimi", "mcp.json");
1306
1463
  ensureDir2(path.dirname(dst));
1307
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1464
+ mergeJson(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1308
1465
  printOk(`Merged MCP into ${dst}`);
1309
1466
  return { status: "ok" };
1310
1467
  }
@@ -1331,7 +1488,7 @@ function installOpenclaw(ctx) {
1331
1488
  }
1332
1489
  }
1333
1490
  ensureDir2(path.dirname(dst));
1334
- openclawMerge(dst, serverJs);
1491
+ openclawMerge(dst, serverJs, ctx.ts);
1335
1492
  if (cliRegistered) {
1336
1493
  printOk(`Registered ijfw-memory via 'openclaw mcp set' AND file-write merge (${dst})`);
1337
1494
  } else {
@@ -1363,10 +1520,10 @@ function installAntigravity(ctx) {
1363
1520
  const serverJs = ctx.serverJsNative || ctx.serverJs;
1364
1521
  const ideDst = path.join(ctx.home, ".gemini", "antigravity", "mcp_config.json");
1365
1522
  ensureDir2(path.dirname(ideDst));
1366
- mergeJson(ideDst, serverJs);
1523
+ mergeJson(ideDst, serverJs, ctx.ts);
1367
1524
  const cliDst = path.join(ctx.home, ".gemini", "config", "mcp_config.json");
1368
1525
  ensureDir2(path.dirname(cliDst));
1369
- mergeJson(cliDst, serverJs);
1526
+ mergeJson(cliDst, serverJs, ctx.ts);
1370
1527
  printOk(`Merged MCP into ${ideDst} + ${cliDst} (Antigravity IDE + CLI)`);
1371
1528
  return { status: "ok" };
1372
1529
  }
@@ -1422,11 +1579,15 @@ function snapshotPreExistingDirs(home) {
1422
1579
  }
1423
1580
  function writeLedger({ home, ijfwHome, preExisting }) {
1424
1581
  const preSet = new Set(preExisting || []);
1582
+ const owned = allOwnedDirs();
1425
1583
  const created = [];
1426
- for (const rel of allOwnedDirs()) {
1584
+ for (const rel of owned) {
1427
1585
  if (!preSet.has(rel) && existsSync5(join5(home, rel))) created.push(rel);
1428
1586
  }
1429
- const ledger = { version: 1, createdDirs: created };
1587
+ const prev = readLedger(ijfwHome).createdDirs.filter(
1588
+ (rel) => owned.includes(rel) && existsSync5(join5(home, rel))
1589
+ );
1590
+ const ledger = { version: 1, createdDirs: [.../* @__PURE__ */ new Set([...prev, ...created])] };
1430
1591
  try {
1431
1592
  mkdirSync4(ijfwHome, { recursive: true, mode: 448 });
1432
1593
  writeFileSync4(ledgerPath(ijfwHome), JSON.stringify(ledger, null, 2) + "\n", { mode: 384 });
@@ -1513,6 +1674,12 @@ var init_install_ledger = __esm({
1513
1674
  qwen: [["~/.qwen/settings.json", "m", "mcpServers.ijfw-memory"]],
1514
1675
  kimi: [["~/.kimi/mcp.json", "m", "mcpServers.ijfw-memory"]],
1515
1676
  opencode: [["~/.config/opencode/opencode.json", "m", "mcp.ijfw-memory"]],
1677
+ cline: [["VS Code globalStorage <Code|VSCodium>/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", "m", "mcpServers.ijfw-memory"]],
1678
+ antigravity: [
1679
+ ["~/.gemini/antigravity/mcp_config.json", "m", "mcpServers.ijfw-memory (IDE)"],
1680
+ ["~/.gemini/config/mcp_config.json", "m", "mcpServers.ijfw-memory (CLI agy)"]
1681
+ ],
1682
+ pi: [["~/.pi/agent/AGENTS.md", "c", "context file (rules-only, no MCP)"]],
1516
1683
  cursor: [["./.cursor/mcp.json + rules/ijfw.mdc", "mc", "project-scoped MCP + rule"]],
1517
1684
  windsurf: [
1518
1685
  ["~/.codeium/windsurf/mcp_config.json", "m", "mcpServers.ijfw-memory"],
@@ -2178,7 +2345,7 @@ var init_install_flow = __esm({
2178
2345
  // src/install.js
2179
2346
  import { spawnSync as spawnSync2 } from "node:child_process";
2180
2347
  import { existsSync as existsSync6, rmSync as rmSync2, mkdirSync as mkdirSync5, realpathSync as realpathSync2, renameSync as renameSync3, readdirSync as readdirSync3, cpSync as cpSync2 } from "node:fs";
2181
- import { resolve as resolve4, join as join6, dirname as dirname5 } from "node:path";
2348
+ import { resolve as resolve4, join as join6, dirname as dirname5, basename as basename2 } from "node:path";
2182
2349
  import { homedir as homedir3, platform as platform2 } from "node:os";
2183
2350
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2184
2351
 
@@ -2344,10 +2511,10 @@ function parseArgs(argv) {
2344
2511
  for (let i = 2; i < argv.length; i++) {
2345
2512
  const a = argv[i];
2346
2513
  if (a === "--yes" || a === "-y") out.yes = true;
2347
- else if (a === "--dir") out.dir = argv[++i];
2514
+ else if (a === "--dir") out.dir = requireFlagValue("--dir", "a path", argv[++i]);
2348
2515
  else if (a === "--no-marketplace") out.noMarketplace = true;
2349
2516
  else if (a === "--branch") {
2350
- out.branch = argv[++i];
2517
+ out.branch = requireFlagValue("--branch", "a name", argv[++i]);
2351
2518
  out.branchExplicit = true;
2352
2519
  } else if (a === "--purge") out.purge = true;
2353
2520
  else if (a === "--dry-run" || a === "--print-plan") out.dryRun = true;
@@ -2358,6 +2525,13 @@ function parseArgs(argv) {
2358
2525
  }
2359
2526
  return out;
2360
2527
  }
2528
+ function requireFlagValue(flag, what, value) {
2529
+ if (value == null || value.startsWith("-")) {
2530
+ console.error(`${flag} requires ${what} argument`);
2531
+ process.exit(1);
2532
+ }
2533
+ return value;
2534
+ }
2361
2535
  function skipNetwork() {
2362
2536
  return process.env.IJFW_SKIP_NETWORK === "1";
2363
2537
  }
@@ -2461,6 +2635,16 @@ function runCheck(cmd, args, opts) {
2461
2635
  const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
2462
2636
  return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
2463
2637
  }
2638
+ function looksLikeIjfwInstall(dir) {
2639
+ try {
2640
+ if (basename2(resolve4(dir)) === ".ijfw") return true;
2641
+ if (existsSync6(join6(dir, "install-ledger.json"))) return true;
2642
+ if (existsSync6(join6(dir, "install-method"))) return true;
2643
+ if (existsSync6(join6(dir, "mcp-server", "src", "server.js")) && existsSync6(join6(dir, "claude"))) return true;
2644
+ } catch {
2645
+ }
2646
+ return false;
2647
+ }
2464
2648
  function cloneOrPull(dir, branch) {
2465
2649
  if (skipNetwork()) {
2466
2650
  if (existsSync6(dir)) {
@@ -2500,6 +2684,13 @@ function cloneOrPull(dir, branch) {
2500
2684
  console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
2501
2685
  }
2502
2686
  }
2687
+ const CANONICAL_PATTERN = /^https:\/\/github\.com\/ferroxlabs\/ijfw(\.git)?\/?$/i;
2688
+ const isIjfwOrigin = CANONICAL_PATTERN.test(currentOrigin) || STALE_PATTERNS.some((re) => re.test(currentOrigin));
2689
+ if (!isIjfwOrigin && !looksLikeIjfwInstall(dir)) {
2690
+ throw new Error(
2691
+ `Refusing to update ${dir}: it is a git checkout of "${currentOrigin}", not an IJFW install. Check your --dir / IJFW_HOME setting, or remove the directory and retry.`
2692
+ );
2693
+ }
2503
2694
  const fetch = spawnSync2("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
2504
2695
  if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
2505
2696
  const co = spawnSync2("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
@@ -2526,29 +2717,50 @@ function cloneOrPull(dir, branch) {
2526
2717
  ".ijfw"
2527
2718
  // internal — recall counter, indexes, layout version
2528
2719
  ];
2720
+ if (!looksLikeIjfwInstall(dir)) {
2721
+ let entries = null;
2722
+ try {
2723
+ entries = readdirSync3(dir);
2724
+ } catch {
2725
+ }
2726
+ if (entries && entries.length === 0) {
2727
+ const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2728
+ if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2729
+ return "cloned";
2730
+ }
2731
+ throw new Error(
2732
+ `Refusing to replace ${dir}: it exists but does not look like an IJFW install (no install ledger, install-method file, or IJFW checkout markers). Check your --dir / IJFW_HOME setting, or move the directory aside and retry.`
2733
+ );
2734
+ }
2529
2735
  const backupDir = dir + ".bak." + Date.now();
2530
2736
  renameSync3(dir, backupDir);
2531
2737
  try {
2532
2738
  const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2533
2739
  if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2534
- let restoredCount = 0;
2740
+ const restoredItems = [];
2535
2741
  for (const item of RESTORE_ALLOWLIST) {
2536
2742
  const src = join6(backupDir, item);
2537
2743
  if (existsSync6(src)) {
2538
2744
  const dst = join6(dir, item);
2539
- if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
2540
2745
  try {
2746
+ if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
2541
2747
  cpSync2(src, dst, { recursive: true, dereference: false });
2542
- rmSync2(src, { recursive: true, force: true });
2543
- restoredCount++;
2748
+ restoredItems.push(item);
2544
2749
  } catch (cpErr) {
2545
2750
  const msg = cpErr && cpErr.message ? cpErr.message : String(cpErr);
2546
2751
  throw new Error(
2547
- `IJFW restore: cpSync failed for "${item}" (${msg}). Your data is still intact under: ${backupDir}. Move it back into ${dir} manually after diagnosing the copy failure.`
2752
+ `IJFW restore: copy failed for "${item}" (${msg}). Your data is still intact under: ${backupDir}. The previous state of ${dir} will be restored from it.`
2548
2753
  );
2549
2754
  }
2550
2755
  }
2551
2756
  }
2757
+ const restoredCount = restoredItems.length;
2758
+ for (const item of restoredItems) {
2759
+ try {
2760
+ rmSync2(join6(backupDir, item), { recursive: true, force: true });
2761
+ } catch {
2762
+ }
2763
+ }
2552
2764
  let backupResidual = [];
2553
2765
  try {
2554
2766
  backupResidual = readdirSync3(backupDir);
@@ -2563,8 +2775,15 @@ function cloneOrPull(dir, branch) {
2563
2775
  }
2564
2776
  return "updated";
2565
2777
  } catch (err) {
2566
- if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
2567
- renameSync3(backupDir, dir);
2778
+ try {
2779
+ if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
2780
+ renameSync3(backupDir, dir);
2781
+ } catch (rollbackErr) {
2782
+ const msg = rollbackErr && rollbackErr.message ? rollbackErr.message : String(rollbackErr);
2783
+ console.error(
2784
+ ` [!] rollback failed (${msg}). Your original data is preserved at: ${backupDir}. Move it back to ${dir} manually.`
2785
+ );
2786
+ }
2568
2787
  throw err;
2569
2788
  }
2570
2789
  }
@@ -2599,7 +2818,15 @@ async function main() {
2599
2818
  process.exit(0);
2600
2819
  }
2601
2820
  const createdThisRun = !existsSync6(target);
2821
+ let platformConfigPhase = false;
2602
2822
  const sigint = () => {
2823
+ if (platformConfigPhase) {
2824
+ console.warn(
2825
+ `
2826
+ [!] install interrupted while platform configs were being written. Some platform configs may already reference ${target} -- the partial install was kept so they keep working. Rerun \`npx -p @ijfw/install ijfw-install\` to complete it, or \`ijfw-uninstall\` to remove IJFW from all platform configs.`
2827
+ );
2828
+ process.exit(130);
2829
+ }
2603
2830
  if (createdThisRun && existsSync6(target)) {
2604
2831
  try {
2605
2832
  rmSync2(target, { recursive: true, force: true });
@@ -2619,6 +2846,7 @@ async function main() {
2619
2846
  console.log(` version: ${ref}`);
2620
2847
  const action = cloneOrPull(target, ref);
2621
2848
  console.log(` repo ${action}`);
2849
+ platformConfigPhase = true;
2622
2850
  await runInstallScript(target);
2623
2851
  console.log(" platform configs applied");
2624
2852
  const canonicalDir = join6(homedir3(), ".ijfw");
@@ -2659,5 +2887,6 @@ if (isDirectRun()) {
2659
2887
  }
2660
2888
  export {
2661
2889
  findBash,
2890
+ looksLikeIjfwInstall,
2662
2891
  resolveBranchOrTag
2663
2892
  };