@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/install.js CHANGED
@@ -292,21 +292,42 @@ function prettyName(targetId) {
292
292
  };
293
293
  return map[targetId] || String(targetId);
294
294
  }
295
+ function backupTimestamp() {
296
+ const d = /* @__PURE__ */ new Date();
297
+ const p = (n) => String(n).padStart(2, "0");
298
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
299
+ }
295
300
  function readJsonOrEmpty(path3) {
296
301
  if (!existsSync3(path3)) return {};
302
+ let raw;
303
+ try {
304
+ raw = readFileSync2(path3, "utf8");
305
+ } catch (err) {
306
+ const msg = err && err.message ? err.message : String(err);
307
+ throw new Error(`cannot read existing config at ${path3} (${msg}) -- fix permissions and re-run.`);
308
+ }
309
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
310
+ if (!raw || raw.trim() === "") return {};
297
311
  try {
298
- const raw = readFileSync2(path3, "utf8");
299
- if (!raw || raw.trim() === "") return {};
300
312
  const parsed = JSON.parse(raw);
301
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
302
- return parsed;
313
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
303
314
  } catch {
304
- return {};
305
315
  }
316
+ const bak = `${path3}.corrupt-${backupTimestamp()}.bak`;
317
+ try {
318
+ copyFileSync(path3, bak);
319
+ } catch (err) {
320
+ const msg = err && err.message ? err.message : String(err);
321
+ throw new Error(
322
+ `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.`
323
+ );
324
+ }
325
+ printWarn(`existing config at ${path3} is not valid JSON -- original saved to ${bak}; starting fresh with the IJFW entry only`);
326
+ return {};
306
327
  }
307
328
  function mergeJson(dst, serverJs, ts) {
308
329
  mkdirSync2(dirname3(dst), { recursive: true });
309
- requireBackup(dst, ts);
330
+ requireBackup(dst, ts || backupTimestamp());
310
331
  const doc = readJsonOrEmpty(dst);
311
332
  if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
312
333
  const isWin = IS_WIN;
@@ -334,7 +355,7 @@ function mergeJson(dst, serverJs, ts) {
334
355
  }
335
356
  function mergeToml(dst, serverJs, ts) {
336
357
  mkdirSync2(dirname3(dst), { recursive: true });
337
- requireBackup(dst, ts);
358
+ requireBackup(dst, ts || backupTimestamp());
338
359
  let text = "";
339
360
  try {
340
361
  text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
@@ -385,32 +406,6 @@ function stripTomlSection(text, sectionName) {
385
406
  }
386
407
  return out.join("\n");
387
408
  }
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
409
  function stripSentinelBlock(text, beginMark, endMark) {
415
410
  const lines = text.split("\n");
416
411
  const out = [];
@@ -500,7 +495,7 @@ function isIndentedEnabledLine(line) {
500
495
  }
501
496
  function opencodeMerge(dst, serverJs, ts) {
502
497
  mkdirSync2(dirname3(dst), { recursive: true });
503
- if (ts) backup(dst, ts);
498
+ backup(dst, ts || backupTimestamp());
504
499
  const doc = readJsonOrEmpty(dst);
505
500
  if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
506
501
  doc.mcp["ijfw-memory"] = { type: "local", command: ["node", serverJs] };
@@ -508,7 +503,7 @@ function opencodeMerge(dst, serverJs, ts) {
508
503
  }
509
504
  function openclawMerge(dst, serverJs, ts) {
510
505
  mkdirSync2(dirname3(dst), { recursive: true });
511
- if (ts) backup(dst, ts);
506
+ backup(dst, ts || backupTimestamp());
512
507
  const doc = readJsonOrEmpty(dst);
513
508
  if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
514
509
  if (!doc.mcp.servers || typeof doc.mcp.servers !== "object") doc.mcp.servers = {};
@@ -554,7 +549,7 @@ function clineMerge(serverJs, home, ts) {
554
549
  if (!userDir) userDir = osDefault;
555
550
  const dst = join3(userDir, "globalStorage", ext, "settings", "cline_mcp_settings.json");
556
551
  mkdirSync2(dirname3(dst), { recursive: true });
557
- if (ts) backup(dst, ts);
552
+ backup(dst, ts || backupTimestamp());
558
553
  const doc = readJsonOrEmpty(dst);
559
554
  if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
560
555
  doc.mcpServers["ijfw-memory"] = {
@@ -633,6 +628,9 @@ function ensureDir(p) {
633
628
  } catch {
634
629
  }
635
630
  }
631
+ function stripBom(s) {
632
+ return s && s.charCodeAt(0) === 65279 ? s.slice(1) : s;
633
+ }
636
634
  function copyIfAbsent(src, dst) {
637
635
  if (!existsSync4(src)) return false;
638
636
  if (existsSync4(dst)) return false;
@@ -716,13 +714,18 @@ async function installClaude(ctx) {
716
714
  "known_marketplaces.json"
717
715
  );
718
716
  ensureDir(join4(ctx.home, ".claude", "plugins"));
719
- backup(claudeSettings, ctx.ts);
717
+ const settingsBak = requireBackup(claudeSettings, ctx.ts);
720
718
  let settings = {};
721
719
  if (existsSync4(claudeSettings)) {
722
720
  try {
723
- settings = JSON.parse(readFileSync3(claudeSettings, "utf8") || "{}");
721
+ settings = JSON.parse(stripBom(readFileSync3(claudeSettings, "utf8")) || "{}");
724
722
  } catch {
725
- settings = {};
723
+ ctx.log.warn("~/.claude/settings.json could not be parsed as JSON -- IJFW will not modify it.");
724
+ if (settingsBak) {
725
+ ctx.log.warn(`A copy of the current file was preserved at ${settingsBak}.`);
726
+ }
727
+ ctx.log.warn("Fix the JSON syntax error and re-run `ijfw install`.");
728
+ return { status: "noop" };
726
729
  }
727
730
  }
728
731
  if (!settings || typeof settings !== "object") settings = {};
@@ -736,7 +739,7 @@ async function installClaude(ctx) {
736
739
  let mp = {};
737
740
  if (existsSync4(claudeMarketplaces)) {
738
741
  try {
739
- mp = JSON.parse(readFileSync3(claudeMarketplaces, "utf8") || "{}");
742
+ mp = JSON.parse(stripBom(readFileSync3(claudeMarketplaces, "utf8")) || "{}");
740
743
  } catch {
741
744
  mp = {};
742
745
  }
@@ -785,7 +788,7 @@ async function installCodex(ctx) {
785
788
  }
786
789
  const configToml = join4(ctx.home, ".codex", "config.toml");
787
790
  ensureDir(dirname4(configToml));
788
- mergeToml(configToml, ctx.serverJsNative);
791
+ mergeToml(configToml, ctx.serverJsNative, ctx.ts);
789
792
  const hooksDst = join4(ctx.home, ".codex", "hooks.json");
790
793
  const hooksSrc = join4(ctx.repoRoot, "codex", ".codex", "hooks.json");
791
794
  const hooksBase = join4(ctx.home, ".codex", "hooks");
@@ -914,18 +917,41 @@ async function installGemini(ctx) {
914
917
  }
915
918
  const dst = join4(ctx.home, ".gemini", "settings.json");
916
919
  ensureDir(dirname4(dst));
917
- mergeJson(dst, ctx.serverJsNative);
920
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
918
921
  const extDst = join4(ctx.home, ".gemini", "extensions", "ijfw");
919
922
  const extSrc = join4(ctx.repoRoot, "gemini", "extensions", "ijfw");
920
923
  for (const sub of ["hooks", "skills", "commands", "agents", "policies"]) {
921
924
  ensureDir(join4(extDst, sub));
922
925
  }
923
- for (const rel of [
924
- "gemini-extension.json",
925
- "IJFW.md",
926
- "hooks/hooks.json",
927
- "policies/ijfw.toml"
928
- ]) {
926
+ for (const rel of ["gemini-extension.json", "hooks/hooks.json"]) {
927
+ const srcFile = join4(extSrc, rel);
928
+ if (!existsSync4(srcFile)) continue;
929
+ let desired = "";
930
+ try {
931
+ desired = readFileSync3(srcFile, "utf8");
932
+ } catch {
933
+ continue;
934
+ }
935
+ desired = desired.split("{{extensionPath}}").join(extDst);
936
+ const dstFile = join4(extDst, rel);
937
+ let current = null;
938
+ try {
939
+ current = existsSync4(dstFile) ? readFileSync3(dstFile, "utf8") : null;
940
+ } catch {
941
+ current = null;
942
+ }
943
+ if (current === desired) continue;
944
+ if (current !== null) {
945
+ try {
946
+ copyFileSync2(dstFile, `${dstFile}.bak.${ctx.ts}`);
947
+ } catch {
948
+ }
949
+ ctx.log.note(`Updated ${rel} (previous copy backed up to ${rel}.bak.${ctx.ts})`);
950
+ }
951
+ ensureDir(dirname4(dstFile));
952
+ writeAtomic(dstFile, desired);
953
+ }
954
+ for (const rel of ["IJFW.md", "policies/ijfw.toml"]) {
929
955
  const dstFile = join4(extDst, rel);
930
956
  if (!existsSync4(dstFile)) {
931
957
  ensureDir(dirname4(dstFile));
@@ -1051,6 +1077,101 @@ function renderWaylandPluginToml(ctx) {
1051
1077
  ""
1052
1078
  ].join("\n");
1053
1079
  }
1080
+ function stripSentinelLines(text, beginMark, endMark) {
1081
+ const lines = text.split("\n");
1082
+ const out = [];
1083
+ let skip = false;
1084
+ for (const line of lines) {
1085
+ if (line === beginMark) {
1086
+ skip = true;
1087
+ continue;
1088
+ }
1089
+ if (line === endMark) {
1090
+ skip = false;
1091
+ continue;
1092
+ }
1093
+ if (skip) continue;
1094
+ out.push(line);
1095
+ }
1096
+ return out.join("\n");
1097
+ }
1098
+ function hermesMergeYamlMcp(ctx, dst, serverJs) {
1099
+ ensureDir(dirname4(dst));
1100
+ requireBackup(dst, ctx.ts);
1101
+ let text = "";
1102
+ try {
1103
+ text = existsSync4(dst) ? stripBom(readFileSync3(dst, "utf8")) : "";
1104
+ } catch {
1105
+ text = "";
1106
+ }
1107
+ text = stripSentinelLines(text, HERMES_MCP_BEGIN, HERMES_MCP_END);
1108
+ const escaped = String(serverJs).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1109
+ const block = [
1110
+ HERMES_MCP_BEGIN,
1111
+ " ijfw-memory:",
1112
+ ' command: "node"',
1113
+ ` args: ["${escaped}"]`,
1114
+ " enabled: true",
1115
+ HERMES_MCP_END
1116
+ ];
1117
+ const lines = text.split("\n");
1118
+ let anchorIdx = -1;
1119
+ for (let i = 0; i < lines.length; i++) {
1120
+ const line = lines[i];
1121
+ if (line.startsWith("mcp_servers:")) {
1122
+ const rest = line.slice("mcp_servers:".length).trim();
1123
+ if (rest === "" || rest.startsWith("#")) {
1124
+ anchorIdx = i;
1125
+ break;
1126
+ }
1127
+ const beforeComment = rest.split("#")[0].replace(/\s/g, "");
1128
+ if (beforeComment === "{}") {
1129
+ lines[i] = "mcp_servers:";
1130
+ anchorIdx = i;
1131
+ break;
1132
+ }
1133
+ }
1134
+ }
1135
+ let merged;
1136
+ if (anchorIdx >= 0) {
1137
+ lines.splice(anchorIdx + 1, 0, ...block);
1138
+ merged = lines.join("\n");
1139
+ } else if (/^mcp_servers:/m.test(text)) {
1140
+ ctx.log.warn("Hermes config.yaml uses an inline mcp_servers map -- cannot merge safely.");
1141
+ ctx.log.warn('Add an "ijfw-memory" entry to mcp_servers manually (command: node, args: [server.js path]).');
1142
+ return false;
1143
+ } else {
1144
+ const prefix = text.trim() === "" ? "" : (text.endsWith("\n") ? text : `${text}
1145
+ `) + "\n";
1146
+ merged = `${prefix}mcp_servers:
1147
+ ${block.join("\n")}`;
1148
+ }
1149
+ if (!merged.endsWith("\n")) merged += "\n";
1150
+ writeAtomic(dst, merged, { mode: 384 });
1151
+ return true;
1152
+ }
1153
+ function hermesInlineEnabledHas(dst, pluginName) {
1154
+ let text = "";
1155
+ try {
1156
+ text = existsSync4(dst) ? readFileSync3(dst, "utf8") : "";
1157
+ } catch {
1158
+ return false;
1159
+ }
1160
+ const esc = pluginName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1161
+ const nameRe = new RegExp(`[\\[,]\\s*["']?${esc}["']?\\s*[,\\]]`);
1162
+ let inPlugins = false;
1163
+ for (const line of text.split("\n")) {
1164
+ if (/^plugins:\s*$/.test(line)) {
1165
+ inPlugins = true;
1166
+ continue;
1167
+ }
1168
+ if (inPlugins && /^\S/.test(line) && line.trim() !== "") inPlugins = false;
1169
+ if (inPlugins && /^\s+enabled:\s*\[.+\]\s*$/.test(line) && nameRe.test(line)) {
1170
+ return true;
1171
+ }
1172
+ }
1173
+ return false;
1174
+ }
1054
1175
  async function installHermes(ctx) {
1055
1176
  if (ctx.ijfwCustomDir) {
1056
1177
  return customDirNoop(
@@ -1062,7 +1183,7 @@ async function installHermes(ctx) {
1062
1183
  }
1063
1184
  const dst = join4(ctx.home, ".hermes", "config.yaml");
1064
1185
  ensureDir(dirname4(dst));
1065
- mergeYamlMcp(dst, ctx.serverJsNative);
1186
+ hermesMergeYamlMcp(ctx, dst, ctx.serverJsNative);
1066
1187
  ensureDir(join4(ctx.home, ".hermes"));
1067
1188
  copyIfAbsent(
1068
1189
  join4(ctx.repoRoot, "hermes", "HERMES.md"),
@@ -1087,40 +1208,44 @@ async function installHermes(ctx) {
1087
1208
  }
1088
1209
  if (readdirErr) {
1089
1210
  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}`);
1211
+ ctx.log.warn("Leaving the installed Hermes plugin tree untouched.");
1212
+ } else {
1213
+ const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
1214
+ let dstEntries = [];
1215
+ try {
1216
+ dstEntries = readdirSync(pluginDst);
1217
+ } catch {
1218
+ }
1219
+ for (const name of dstEntries) {
1220
+ if (name === "__pycache__") continue;
1221
+ if (!srcNames.has(name)) {
1222
+ try {
1223
+ rmSync(join4(pluginDst, name), { recursive: true, force: true });
1224
+ } catch (err) {
1225
+ ctx.log.warn(`Hermes plugin: could not remove stale ${name}: ${err.message || err}`);
1226
+ }
1104
1227
  }
1105
1228
  }
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);
1229
+ for (const name of entries) {
1230
+ if (name === "__pycache__") continue;
1231
+ const src = join4(pluginSrc, name);
1232
+ const dstEntry = join4(pluginDst, name);
1233
+ try {
1234
+ const st = statSync2(src);
1235
+ if (st.isDirectory()) {
1236
+ cpSync(src, dstEntry, { recursive: true, force: true });
1237
+ } else if (st.isFile()) {
1238
+ copyFileSync2(src, dstEntry);
1239
+ }
1240
+ } catch {
1117
1241
  }
1118
- } catch {
1119
1242
  }
1120
1243
  }
1121
1244
  }
1122
- mergeYamlPluginsEnabled(dst, "ijfw");
1123
- mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py", ctx.ts);
1245
+ if (!hermesInlineEnabledHas(dst, "ijfw")) {
1246
+ mergeYamlPluginsEnabled(dst, "ijfw");
1247
+ }
1248
+ mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py");
1124
1249
  ctx.log.ok("Installed Hermes bundle: MCP + HERMES.md + skills + plugin + tier-2 hook");
1125
1250
  return { status: "ok" };
1126
1251
  }
@@ -1148,13 +1273,13 @@ async function installCursor(ctx) {
1148
1273
  }
1149
1274
  const dst = join4(cwd, ".cursor", "mcp.json");
1150
1275
  ensureDir(dirname4(dst));
1151
- mergeJson(dst, ctx.serverJsNative);
1276
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
1152
1277
  const rulesDir = join4(cwd, ".cursor", "rules");
1153
1278
  ensureDir(rulesDir);
1154
1279
  const ruleSrc = join4(ctx.repoRoot, "cursor", ".cursor", "rules", "ijfw.mdc");
1155
1280
  if (existsSync4(ruleSrc)) {
1156
1281
  try {
1157
- copyFileSync2(ruleSrc, join4(rulesDir, "ijfw.mdc"));
1282
+ installHook(ruleSrc, join4(rulesDir, "ijfw.mdc"), ctx.ts);
1158
1283
  } catch {
1159
1284
  }
1160
1285
  }
@@ -1172,7 +1297,7 @@ async function installWindsurf(ctx) {
1172
1297
  }
1173
1298
  const dst = join4(ctx.home, ".codeium", "windsurf", "mcp_config.json");
1174
1299
  ensureDir(dirname4(dst));
1175
- mergeJson(dst, ctx.serverJsNative);
1300
+ mergeJson(dst, ctx.serverJsNative, ctx.ts);
1176
1301
  const cwd = ctx.cwd || process.cwd();
1177
1302
  if (!guardProjectWrite(cwd, ctx.home, {
1178
1303
  platformLabel: "Windsurf project rules (.windsurfrules)",
@@ -1198,9 +1323,12 @@ async function installWindsurf(ctx) {
1198
1323
  }
1199
1324
  return { status: "ok" };
1200
1325
  }
1326
+ var HERMES_MCP_BEGIN, HERMES_MCP_END;
1201
1327
  var init_install_targets_1_7 = __esm({
1202
1328
  "src/install-targets-1-7.js"() {
1203
1329
  init_install_helpers();
1330
+ HERMES_MCP_BEGIN = "# IJFW-MCP-BEGIN ijfw-memory";
1331
+ HERMES_MCP_END = "# IJFW-MCP-END ijfw-memory";
1204
1332
  }
1205
1333
  });
1206
1334
 
@@ -1230,6 +1358,30 @@ function commandExists(name) {
1230
1358
  const r = spawnSync(probeCmd, [name], { stdio: "ignore" });
1231
1359
  return r.status === 0;
1232
1360
  }
1361
+ function vscodeMcpMerge(dst, serverJs, ts) {
1362
+ ensureDir2(path.dirname(dst));
1363
+ requireBackup(dst, ts);
1364
+ let doc = {};
1365
+ try {
1366
+ if (fs.existsSync(dst)) {
1367
+ let raw = fs.readFileSync(dst, "utf8");
1368
+ if (raw && raw.charCodeAt(0) === 65279) raw = raw.slice(1);
1369
+ if (raw.trim() !== "") {
1370
+ const parsed = JSON.parse(raw);
1371
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) doc = parsed;
1372
+ }
1373
+ }
1374
+ } catch {
1375
+ doc = {};
1376
+ }
1377
+ if (!doc.servers || typeof doc.servers !== "object") doc.servers = {};
1378
+ if (doc.mcpServers && typeof doc.mcpServers === "object") {
1379
+ delete doc.mcpServers["ijfw-memory"];
1380
+ if (Object.keys(doc.mcpServers).length === 0) delete doc.mcpServers;
1381
+ }
1382
+ doc.servers["ijfw-memory"] = { type: "stdio", command: "node", args: [serverJs] };
1383
+ writeAtomic(dst, JSON.stringify(doc, null, 2) + "\n", { mode: 384 });
1384
+ }
1233
1385
  function installCopilot(ctx) {
1234
1386
  if (ctx.ijfwCustomDir) {
1235
1387
  printInfo("Custom-dir install -- skipping Copilot project writes.");
@@ -1251,7 +1403,7 @@ function installCopilot(ctx) {
1251
1403
  }
1252
1404
  const dst = path.join(cwd, ".vscode", "mcp.json");
1253
1405
  ensureDir2(path.dirname(dst));
1254
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1406
+ vscodeMcpMerge(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1255
1407
  const rulesDst = path.join(cwd, ".github", "copilot-instructions.md");
1256
1408
  const rulesSrc = path.join(ctx.repoRoot, "copilot", "copilot-instructions.md");
1257
1409
  const wroteRules = copyIfMissing(rulesSrc, rulesDst);
@@ -1270,7 +1422,7 @@ function installOpencode(ctx) {
1270
1422
  }
1271
1423
  const dst = path.join(ctx.home, ".config", "opencode", "opencode.json");
1272
1424
  ensureDir2(path.dirname(dst));
1273
- opencodeMerge(dst, ctx.serverJsNative || ctx.serverJs);
1425
+ opencodeMerge(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1274
1426
  printOk(`Merged MCP into ${dst} (opencode mcp.local schema)`);
1275
1427
  return { status: "ok" };
1276
1428
  }
@@ -1282,7 +1434,7 @@ function installQwen(ctx) {
1282
1434
  }
1283
1435
  const dst = path.join(ctx.home, ".qwen", "settings.json");
1284
1436
  ensureDir2(path.dirname(dst));
1285
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1437
+ mergeJson(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1286
1438
  printOk(`Merged MCP into ${dst}`);
1287
1439
  return { status: "ok" };
1288
1440
  }
@@ -1292,7 +1444,7 @@ function installCline(ctx) {
1292
1444
  printOk("Cline: real platform config left untouched.");
1293
1445
  return { status: "noop" };
1294
1446
  }
1295
- const dst = clineMerge(ctx.serverJsNative || ctx.serverJs, ctx.home);
1447
+ const dst = clineMerge(ctx.serverJsNative || ctx.serverJs, ctx.home, ctx.ts);
1296
1448
  printOk(`Merged MCP into ${dst} (cline globalStorage schema)`);
1297
1449
  return { status: "ok" };
1298
1450
  }
@@ -1304,7 +1456,7 @@ function installKimi(ctx) {
1304
1456
  }
1305
1457
  const dst = path.join(ctx.home, ".kimi", "mcp.json");
1306
1458
  ensureDir2(path.dirname(dst));
1307
- mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
1459
+ mergeJson(dst, ctx.serverJsNative || ctx.serverJs, ctx.ts);
1308
1460
  printOk(`Merged MCP into ${dst}`);
1309
1461
  return { status: "ok" };
1310
1462
  }
@@ -1331,7 +1483,7 @@ function installOpenclaw(ctx) {
1331
1483
  }
1332
1484
  }
1333
1485
  ensureDir2(path.dirname(dst));
1334
- openclawMerge(dst, serverJs);
1486
+ openclawMerge(dst, serverJs, ctx.ts);
1335
1487
  if (cliRegistered) {
1336
1488
  printOk(`Registered ijfw-memory via 'openclaw mcp set' AND file-write merge (${dst})`);
1337
1489
  } else {
@@ -1363,10 +1515,10 @@ function installAntigravity(ctx) {
1363
1515
  const serverJs = ctx.serverJsNative || ctx.serverJs;
1364
1516
  const ideDst = path.join(ctx.home, ".gemini", "antigravity", "mcp_config.json");
1365
1517
  ensureDir2(path.dirname(ideDst));
1366
- mergeJson(ideDst, serverJs);
1518
+ mergeJson(ideDst, serverJs, ctx.ts);
1367
1519
  const cliDst = path.join(ctx.home, ".gemini", "config", "mcp_config.json");
1368
1520
  ensureDir2(path.dirname(cliDst));
1369
- mergeJson(cliDst, serverJs);
1521
+ mergeJson(cliDst, serverJs, ctx.ts);
1370
1522
  printOk(`Merged MCP into ${ideDst} + ${cliDst} (Antigravity IDE + CLI)`);
1371
1523
  return { status: "ok" };
1372
1524
  }
@@ -1422,11 +1574,15 @@ function snapshotPreExistingDirs(home) {
1422
1574
  }
1423
1575
  function writeLedger({ home, ijfwHome, preExisting }) {
1424
1576
  const preSet = new Set(preExisting || []);
1577
+ const owned = allOwnedDirs();
1425
1578
  const created = [];
1426
- for (const rel of allOwnedDirs()) {
1579
+ for (const rel of owned) {
1427
1580
  if (!preSet.has(rel) && existsSync5(join5(home, rel))) created.push(rel);
1428
1581
  }
1429
- const ledger = { version: 1, createdDirs: created };
1582
+ const prev = readLedger(ijfwHome).createdDirs.filter(
1583
+ (rel) => owned.includes(rel) && existsSync5(join5(home, rel))
1584
+ );
1585
+ const ledger = { version: 1, createdDirs: [.../* @__PURE__ */ new Set([...prev, ...created])] };
1430
1586
  try {
1431
1587
  mkdirSync4(ijfwHome, { recursive: true, mode: 448 });
1432
1588
  writeFileSync4(ledgerPath(ijfwHome), JSON.stringify(ledger, null, 2) + "\n", { mode: 384 });
@@ -1513,6 +1669,12 @@ var init_install_ledger = __esm({
1513
1669
  qwen: [["~/.qwen/settings.json", "m", "mcpServers.ijfw-memory"]],
1514
1670
  kimi: [["~/.kimi/mcp.json", "m", "mcpServers.ijfw-memory"]],
1515
1671
  opencode: [["~/.config/opencode/opencode.json", "m", "mcp.ijfw-memory"]],
1672
+ cline: [["VS Code globalStorage <Code|VSCodium>/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", "m", "mcpServers.ijfw-memory"]],
1673
+ antigravity: [
1674
+ ["~/.gemini/antigravity/mcp_config.json", "m", "mcpServers.ijfw-memory (IDE)"],
1675
+ ["~/.gemini/config/mcp_config.json", "m", "mcpServers.ijfw-memory (CLI agy)"]
1676
+ ],
1677
+ pi: [["~/.pi/agent/AGENTS.md", "c", "context file (rules-only, no MCP)"]],
1516
1678
  cursor: [["./.cursor/mcp.json + rules/ijfw.mdc", "mc", "project-scoped MCP + rule"]],
1517
1679
  windsurf: [
1518
1680
  ["~/.codeium/windsurf/mcp_config.json", "m", "mcpServers.ijfw-memory"],
@@ -2178,7 +2340,7 @@ var init_install_flow = __esm({
2178
2340
  // src/install.js
2179
2341
  import { spawnSync as spawnSync2 } from "node:child_process";
2180
2342
  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";
2343
+ import { resolve as resolve4, join as join6, dirname as dirname5, basename as basename2 } from "node:path";
2182
2344
  import { homedir as homedir3, platform as platform2 } from "node:os";
2183
2345
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2184
2346
 
@@ -2344,10 +2506,10 @@ function parseArgs(argv) {
2344
2506
  for (let i = 2; i < argv.length; i++) {
2345
2507
  const a = argv[i];
2346
2508
  if (a === "--yes" || a === "-y") out.yes = true;
2347
- else if (a === "--dir") out.dir = argv[++i];
2509
+ else if (a === "--dir") out.dir = requireFlagValue("--dir", "a path", argv[++i]);
2348
2510
  else if (a === "--no-marketplace") out.noMarketplace = true;
2349
2511
  else if (a === "--branch") {
2350
- out.branch = argv[++i];
2512
+ out.branch = requireFlagValue("--branch", "a name", argv[++i]);
2351
2513
  out.branchExplicit = true;
2352
2514
  } else if (a === "--purge") out.purge = true;
2353
2515
  else if (a === "--dry-run" || a === "--print-plan") out.dryRun = true;
@@ -2358,6 +2520,13 @@ function parseArgs(argv) {
2358
2520
  }
2359
2521
  return out;
2360
2522
  }
2523
+ function requireFlagValue(flag, what, value) {
2524
+ if (value == null || value.startsWith("-")) {
2525
+ console.error(`${flag} requires ${what} argument`);
2526
+ process.exit(1);
2527
+ }
2528
+ return value;
2529
+ }
2361
2530
  function skipNetwork() {
2362
2531
  return process.env.IJFW_SKIP_NETWORK === "1";
2363
2532
  }
@@ -2461,6 +2630,16 @@ function runCheck(cmd, args, opts) {
2461
2630
  const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
2462
2631
  return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
2463
2632
  }
2633
+ function looksLikeIjfwInstall(dir) {
2634
+ try {
2635
+ if (basename2(resolve4(dir)) === ".ijfw") return true;
2636
+ if (existsSync6(join6(dir, "install-ledger.json"))) return true;
2637
+ if (existsSync6(join6(dir, "install-method"))) return true;
2638
+ if (existsSync6(join6(dir, "mcp-server", "src", "server.js")) && existsSync6(join6(dir, "claude"))) return true;
2639
+ } catch {
2640
+ }
2641
+ return false;
2642
+ }
2464
2643
  function cloneOrPull(dir, branch) {
2465
2644
  if (skipNetwork()) {
2466
2645
  if (existsSync6(dir)) {
@@ -2500,6 +2679,13 @@ function cloneOrPull(dir, branch) {
2500
2679
  console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
2501
2680
  }
2502
2681
  }
2682
+ const CANONICAL_PATTERN = /^https:\/\/github\.com\/ferroxlabs\/ijfw(\.git)?\/?$/i;
2683
+ const isIjfwOrigin = CANONICAL_PATTERN.test(currentOrigin) || STALE_PATTERNS.some((re) => re.test(currentOrigin));
2684
+ if (!isIjfwOrigin && !looksLikeIjfwInstall(dir)) {
2685
+ throw new Error(
2686
+ `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.`
2687
+ );
2688
+ }
2503
2689
  const fetch = spawnSync2("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
2504
2690
  if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
2505
2691
  const co = spawnSync2("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
@@ -2526,29 +2712,50 @@ function cloneOrPull(dir, branch) {
2526
2712
  ".ijfw"
2527
2713
  // internal — recall counter, indexes, layout version
2528
2714
  ];
2715
+ if (!looksLikeIjfwInstall(dir)) {
2716
+ let entries = null;
2717
+ try {
2718
+ entries = readdirSync3(dir);
2719
+ } catch {
2720
+ }
2721
+ if (entries && entries.length === 0) {
2722
+ const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2723
+ if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2724
+ return "cloned";
2725
+ }
2726
+ throw new Error(
2727
+ `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.`
2728
+ );
2729
+ }
2529
2730
  const backupDir = dir + ".bak." + Date.now();
2530
2731
  renameSync3(dir, backupDir);
2531
2732
  try {
2532
2733
  const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2533
2734
  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;
2735
+ const restoredItems = [];
2535
2736
  for (const item of RESTORE_ALLOWLIST) {
2536
2737
  const src = join6(backupDir, item);
2537
2738
  if (existsSync6(src)) {
2538
2739
  const dst = join6(dir, item);
2539
- if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
2540
2740
  try {
2741
+ if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
2541
2742
  cpSync2(src, dst, { recursive: true, dereference: false });
2542
- rmSync2(src, { recursive: true, force: true });
2543
- restoredCount++;
2743
+ restoredItems.push(item);
2544
2744
  } catch (cpErr) {
2545
2745
  const msg = cpErr && cpErr.message ? cpErr.message : String(cpErr);
2546
2746
  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.`
2747
+ `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
2748
  );
2549
2749
  }
2550
2750
  }
2551
2751
  }
2752
+ const restoredCount = restoredItems.length;
2753
+ for (const item of restoredItems) {
2754
+ try {
2755
+ rmSync2(join6(backupDir, item), { recursive: true, force: true });
2756
+ } catch {
2757
+ }
2758
+ }
2552
2759
  let backupResidual = [];
2553
2760
  try {
2554
2761
  backupResidual = readdirSync3(backupDir);
@@ -2563,8 +2770,15 @@ function cloneOrPull(dir, branch) {
2563
2770
  }
2564
2771
  return "updated";
2565
2772
  } catch (err) {
2566
- if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
2567
- renameSync3(backupDir, dir);
2773
+ try {
2774
+ if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
2775
+ renameSync3(backupDir, dir);
2776
+ } catch (rollbackErr) {
2777
+ const msg = rollbackErr && rollbackErr.message ? rollbackErr.message : String(rollbackErr);
2778
+ console.error(
2779
+ ` [!] rollback failed (${msg}). Your original data is preserved at: ${backupDir}. Move it back to ${dir} manually.`
2780
+ );
2781
+ }
2568
2782
  throw err;
2569
2783
  }
2570
2784
  }
@@ -2599,7 +2813,15 @@ async function main() {
2599
2813
  process.exit(0);
2600
2814
  }
2601
2815
  const createdThisRun = !existsSync6(target);
2816
+ let platformConfigPhase = false;
2602
2817
  const sigint = () => {
2818
+ if (platformConfigPhase) {
2819
+ console.warn(
2820
+ `
2821
+ [!] 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.`
2822
+ );
2823
+ process.exit(130);
2824
+ }
2603
2825
  if (createdThisRun && existsSync6(target)) {
2604
2826
  try {
2605
2827
  rmSync2(target, { recursive: true, force: true });
@@ -2619,6 +2841,7 @@ async function main() {
2619
2841
  console.log(` version: ${ref}`);
2620
2842
  const action = cloneOrPull(target, ref);
2621
2843
  console.log(` repo ${action}`);
2844
+ platformConfigPhase = true;
2622
2845
  await runInstallScript(target);
2623
2846
  console.log(" platform configs applied");
2624
2847
  const canonicalDir = join6(homedir3(), ".ijfw");
@@ -2659,5 +2882,6 @@ if (isDirectRun()) {
2659
2882
  }
2660
2883
  export {
2661
2884
  findBash,
2885
+ looksLikeIjfwInstall,
2662
2886
  resolveBranchOrTag
2663
2887
  };