@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/ijfw.js +186 -178
- package/dist/install.js +324 -100
- package/dist/uninstall.js +112 -40
- package/package.json +2 -2
- package/src/install.ps1 +1 -1
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
"
|
|
927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1579
|
+
for (const rel of owned) {
|
|
1427
1580
|
if (!preSet.has(rel) && existsSync5(join5(home, rel))) created.push(rel);
|
|
1428
1581
|
}
|
|
1429
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2567
|
-
|
|
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
|
};
|