@ijfw/install 1.6.0 → 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 +196 -178
- package/dist/install.js +493 -120
- package/dist/uninstall.js +392 -111
- package/package.json +3 -2
- package/src/install.ps1 +1 -1
- package/templates/pi/AGENTS.md +55 -0
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
|
}
|
|
@@ -1388,6 +1540,152 @@ var init_install_targets_8_14 = __esm({
|
|
|
1388
1540
|
}
|
|
1389
1541
|
});
|
|
1390
1542
|
|
|
1543
|
+
// src/install-ledger.js
|
|
1544
|
+
var install_ledger_exports = {};
|
|
1545
|
+
__export(install_ledger_exports, {
|
|
1546
|
+
INSTALL_PLAN: () => INSTALL_PLAN,
|
|
1547
|
+
PLATFORM_OWNED_DIRS: () => PLATFORM_OWNED_DIRS,
|
|
1548
|
+
allOwnedDirs: () => allOwnedDirs,
|
|
1549
|
+
isEmptyDir: () => isEmptyDir,
|
|
1550
|
+
ledgerPath: () => ledgerPath,
|
|
1551
|
+
readLedger: () => readLedger,
|
|
1552
|
+
renderPlan: () => renderPlan,
|
|
1553
|
+
snapshotPreExistingDirs: () => snapshotPreExistingDirs,
|
|
1554
|
+
writeLedger: () => writeLedger
|
|
1555
|
+
});
|
|
1556
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
1557
|
+
import { join as join5 } from "node:path";
|
|
1558
|
+
function ledgerPath(ijfwHome) {
|
|
1559
|
+
return join5(ijfwHome, "install-ledger.json");
|
|
1560
|
+
}
|
|
1561
|
+
function allOwnedDirs() {
|
|
1562
|
+
const set = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const dirs of Object.values(PLATFORM_OWNED_DIRS)) {
|
|
1564
|
+
for (const d of dirs) set.add(d);
|
|
1565
|
+
}
|
|
1566
|
+
return [...set];
|
|
1567
|
+
}
|
|
1568
|
+
function snapshotPreExistingDirs(home) {
|
|
1569
|
+
const pre = [];
|
|
1570
|
+
for (const rel of allOwnedDirs()) {
|
|
1571
|
+
if (existsSync5(join5(home, rel))) pre.push(rel);
|
|
1572
|
+
}
|
|
1573
|
+
return pre;
|
|
1574
|
+
}
|
|
1575
|
+
function writeLedger({ home, ijfwHome, preExisting }) {
|
|
1576
|
+
const preSet = new Set(preExisting || []);
|
|
1577
|
+
const owned = allOwnedDirs();
|
|
1578
|
+
const created = [];
|
|
1579
|
+
for (const rel of owned) {
|
|
1580
|
+
if (!preSet.has(rel) && existsSync5(join5(home, rel))) created.push(rel);
|
|
1581
|
+
}
|
|
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])] };
|
|
1586
|
+
try {
|
|
1587
|
+
mkdirSync4(ijfwHome, { recursive: true, mode: 448 });
|
|
1588
|
+
writeFileSync4(ledgerPath(ijfwHome), JSON.stringify(ledger, null, 2) + "\n", { mode: 384 });
|
|
1589
|
+
} catch {
|
|
1590
|
+
}
|
|
1591
|
+
return ledger;
|
|
1592
|
+
}
|
|
1593
|
+
function readLedger(ijfwHome) {
|
|
1594
|
+
try {
|
|
1595
|
+
const raw = readFileSync4(ledgerPath(ijfwHome), "utf8");
|
|
1596
|
+
const doc = JSON.parse(raw);
|
|
1597
|
+
if (doc && Array.isArray(doc.createdDirs)) return doc;
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
return { version: 1, createdDirs: [] };
|
|
1601
|
+
}
|
|
1602
|
+
function isEmptyDir(p) {
|
|
1603
|
+
try {
|
|
1604
|
+
return existsSync5(p) && readdirSync2(p).length === 0;
|
|
1605
|
+
} catch {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function renderPlan(targetList) {
|
|
1610
|
+
const lines = [];
|
|
1611
|
+
lines.push("IJFW install plan (dry run) -- nothing will be written.");
|
|
1612
|
+
lines.push(" legend: [m]=merge into existing file [c]=create [./]=project-scoped");
|
|
1613
|
+
lines.push("");
|
|
1614
|
+
const emit = (title, rows) => {
|
|
1615
|
+
if (!rows || rows.length === 0) return;
|
|
1616
|
+
lines.push(title);
|
|
1617
|
+
for (const [path3, kind, note] of rows) lines.push(` [${kind}] ${path3} -- ${note}`);
|
|
1618
|
+
};
|
|
1619
|
+
emit(" shared:", INSTALL_PLAN.shared);
|
|
1620
|
+
for (const t of targetList) {
|
|
1621
|
+
if (INSTALL_PLAN[t]) emit(` ${t}:`, INSTALL_PLAN[t]);
|
|
1622
|
+
}
|
|
1623
|
+
lines.push("");
|
|
1624
|
+
lines.push(" Run without --dry-run to apply. Use `ijfw-uninstall --purge` to reverse.");
|
|
1625
|
+
return lines.join("\n");
|
|
1626
|
+
}
|
|
1627
|
+
var PLATFORM_OWNED_DIRS, INSTALL_PLAN;
|
|
1628
|
+
var init_install_ledger = __esm({
|
|
1629
|
+
"src/install-ledger.js"() {
|
|
1630
|
+
PLATFORM_OWNED_DIRS = {
|
|
1631
|
+
codex: [".codex"],
|
|
1632
|
+
gemini: [".gemini"],
|
|
1633
|
+
hermes: [".hermes"],
|
|
1634
|
+
wayland: [".wayland"],
|
|
1635
|
+
openclaw: [".openclaw"],
|
|
1636
|
+
qwen: [".qwen"],
|
|
1637
|
+
kimi: [".kimi"],
|
|
1638
|
+
opencode: [".config/opencode"],
|
|
1639
|
+
pi: [".pi"]
|
|
1640
|
+
};
|
|
1641
|
+
INSTALL_PLAN = {
|
|
1642
|
+
shared: [
|
|
1643
|
+
["~/.ijfw/", "c", "IJFW home (server symlink, scripts, index, logs, settings, ledger)"]
|
|
1644
|
+
],
|
|
1645
|
+
claude: [
|
|
1646
|
+
["~/.claude/settings.json", "m", "mcpServers.ijfw-memory + enabledPlugins + marketplace"],
|
|
1647
|
+
["~/.claude/plugins/known_marketplaces.json", "m", "ijfw marketplace entry"]
|
|
1648
|
+
],
|
|
1649
|
+
codex: [
|
|
1650
|
+
["~/.codex/config.toml", "m", "[mcp_servers.ijfw-memory]"],
|
|
1651
|
+
["~/.codex/hooks.json", "m", "IJFW hook entries"],
|
|
1652
|
+
["~/.codex/hooks/*.sh", "c", "lifecycle hook scripts"],
|
|
1653
|
+
["~/.codex/IJFW.md", "c", "context file"],
|
|
1654
|
+
["~/.codex/skills/, commands/", "c", "skills + command aliases"]
|
|
1655
|
+
],
|
|
1656
|
+
gemini: [
|
|
1657
|
+
["~/.gemini/settings.json", "m", "mcpServers.ijfw-memory"],
|
|
1658
|
+
["~/.gemini/extensions/ijfw/", "c", "extension (hooks, skills, commands, agents)"]
|
|
1659
|
+
],
|
|
1660
|
+
hermes: [
|
|
1661
|
+
["~/.hermes/config.yaml", "m", "mcp_servers.ijfw-memory + plugin + hook"],
|
|
1662
|
+
["~/.hermes/HERMES.md, skills/, plugins/ijfw/", "c", "context + skills + plugin tree"]
|
|
1663
|
+
],
|
|
1664
|
+
wayland: [
|
|
1665
|
+
["~/.wayland/plugins/ijfw/", "c", "plugin.toml (MCP + hooks)"],
|
|
1666
|
+
["~/.wayland/WAYLAND.md, skills/", "c", "context + skills"]
|
|
1667
|
+
],
|
|
1668
|
+
openclaw: [["~/.openclaw/openclaw.json", "m", "mcp.servers.ijfw-memory"]],
|
|
1669
|
+
qwen: [["~/.qwen/settings.json", "m", "mcpServers.ijfw-memory"]],
|
|
1670
|
+
kimi: [["~/.kimi/mcp.json", "m", "mcpServers.ijfw-memory"]],
|
|
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)"]],
|
|
1678
|
+
cursor: [["./.cursor/mcp.json + rules/ijfw.mdc", "mc", "project-scoped MCP + rule"]],
|
|
1679
|
+
windsurf: [
|
|
1680
|
+
["~/.codeium/windsurf/mcp_config.json", "m", "mcpServers.ijfw-memory"],
|
|
1681
|
+
["./.windsurfrules", "c", "project-scoped rules"]
|
|
1682
|
+
],
|
|
1683
|
+
copilot: [["./.vscode/mcp.json + .github/copilot-instructions.md", "mc", "project-scoped"]],
|
|
1684
|
+
aider: [["~/.aider.conf.yml + ~/CONVENTIONS.md", "c", "home-level rule files"]]
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1391
1689
|
// src/install-flow.js
|
|
1392
1690
|
var install_flow_exports = {};
|
|
1393
1691
|
__export(install_flow_exports, {
|
|
@@ -1927,6 +2225,7 @@ async function runInstall({
|
|
|
1927
2225
|
});
|
|
1928
2226
|
}
|
|
1929
2227
|
pruneBackups({ home });
|
|
2228
|
+
const preExistingDirs = snapshotPreExistingDirs(home);
|
|
1930
2229
|
const live = [];
|
|
1931
2230
|
const standby = [];
|
|
1932
2231
|
const failed = [];
|
|
@@ -1981,6 +2280,7 @@ async function runInstall({
|
|
|
1981
2280
|
standby.push(display);
|
|
1982
2281
|
}
|
|
1983
2282
|
}
|
|
2283
|
+
writeLedger({ home, ijfwHome: resolvedIjfwHome, preExisting: preExistingDirs });
|
|
1984
2284
|
printSummary({
|
|
1985
2285
|
live,
|
|
1986
2286
|
standby,
|
|
@@ -1996,6 +2296,7 @@ var init_install_flow = __esm({
|
|
|
1996
2296
|
init_install_helpers();
|
|
1997
2297
|
init_install_targets_1_7();
|
|
1998
2298
|
init_install_targets_8_14();
|
|
2299
|
+
init_install_ledger();
|
|
1999
2300
|
CANONICAL_ORDER = [
|
|
2000
2301
|
"claude",
|
|
2001
2302
|
"codex",
|
|
@@ -2038,8 +2339,8 @@ var init_install_flow = __esm({
|
|
|
2038
2339
|
|
|
2039
2340
|
// src/install.js
|
|
2040
2341
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2041
|
-
import { existsSync as
|
|
2042
|
-
import { resolve as resolve4, join as
|
|
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";
|
|
2343
|
+
import { resolve as resolve4, join as join6, dirname as dirname5, basename as basename2 } from "node:path";
|
|
2043
2344
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
2044
2345
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2045
2346
|
|
|
@@ -2201,16 +2502,17 @@ function triggerColdScan(projectRoot, options = {}) {
|
|
|
2201
2502
|
var DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git";
|
|
2202
2503
|
var DEFAULT_BRANCH = "main";
|
|
2203
2504
|
function parseArgs(argv) {
|
|
2204
|
-
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false };
|
|
2505
|
+
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false, dryRun: false };
|
|
2205
2506
|
for (let i = 2; i < argv.length; i++) {
|
|
2206
2507
|
const a = argv[i];
|
|
2207
2508
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
2208
|
-
else if (a === "--dir") out.dir = argv[++i];
|
|
2509
|
+
else if (a === "--dir") out.dir = requireFlagValue("--dir", "a path", argv[++i]);
|
|
2209
2510
|
else if (a === "--no-marketplace") out.noMarketplace = true;
|
|
2210
2511
|
else if (a === "--branch") {
|
|
2211
|
-
out.branch = argv[++i];
|
|
2512
|
+
out.branch = requireFlagValue("--branch", "a name", argv[++i]);
|
|
2212
2513
|
out.branchExplicit = true;
|
|
2213
2514
|
} else if (a === "--purge") out.purge = true;
|
|
2515
|
+
else if (a === "--dry-run" || a === "--print-plan") out.dryRun = true;
|
|
2214
2516
|
else if (a === "--help" || a === "-h") {
|
|
2215
2517
|
printHelp();
|
|
2216
2518
|
process.exit(0);
|
|
@@ -2218,6 +2520,13 @@ function parseArgs(argv) {
|
|
|
2218
2520
|
}
|
|
2219
2521
|
return out;
|
|
2220
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
|
+
}
|
|
2221
2530
|
function skipNetwork() {
|
|
2222
2531
|
return process.env.IJFW_SKIP_NETWORK === "1";
|
|
2223
2532
|
}
|
|
@@ -2257,10 +2566,11 @@ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup, _logger } = {}
|
|
|
2257
2566
|
}
|
|
2258
2567
|
function printHelp() {
|
|
2259
2568
|
console.log(`ijfw-install -- IJFW installer
|
|
2260
|
-
Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--yes]
|
|
2569
|
+
Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--yes] [--dry-run]
|
|
2261
2570
|
--dir install location (default: $IJFW_HOME or ~/.ijfw)
|
|
2262
2571
|
--branch git branch or tag (default: latest released tag)
|
|
2263
2572
|
--no-marketplace skip merging ~/.claude/settings.json
|
|
2573
|
+
--dry-run print every file/dir the install would touch, write nothing
|
|
2264
2574
|
--yes non-interactive
|
|
2265
2575
|
`);
|
|
2266
2576
|
}
|
|
@@ -2291,15 +2601,15 @@ function findBash() {
|
|
|
2291
2601
|
const whereGit = spawnSync2("where", ["git"], { encoding: "utf8" });
|
|
2292
2602
|
if (whereGit.status === 0) {
|
|
2293
2603
|
const gitPath = (whereGit.stdout || "").split(/\r?\n/)[0].trim();
|
|
2294
|
-
if (gitPath &&
|
|
2604
|
+
if (gitPath && existsSync6(gitPath)) {
|
|
2295
2605
|
const gitDir = dirname5(gitPath);
|
|
2296
2606
|
const gitRoot = dirname5(gitDir);
|
|
2297
2607
|
const candidates = [
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2608
|
+
join6(gitDir, "bash.exe"),
|
|
2609
|
+
join6(gitRoot, "bin", "bash.exe"),
|
|
2610
|
+
join6(gitRoot, "usr", "bin", "bash.exe")
|
|
2301
2611
|
];
|
|
2302
|
-
for (const c of candidates) if (
|
|
2612
|
+
for (const c of candidates) if (existsSync6(c)) return c;
|
|
2303
2613
|
}
|
|
2304
2614
|
}
|
|
2305
2615
|
for (const c of [
|
|
@@ -2307,35 +2617,45 @@ function findBash() {
|
|
|
2307
2617
|
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
2308
2618
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
2309
2619
|
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
2310
|
-
]) if (
|
|
2620
|
+
]) if (existsSync6(c)) return c;
|
|
2311
2621
|
if (hasBin2("bash")) return "bash";
|
|
2312
2622
|
return null;
|
|
2313
2623
|
}
|
|
2314
2624
|
function resolveTarget(opt) {
|
|
2315
2625
|
if (opt.dir) return resolve4(opt.dir);
|
|
2316
2626
|
if (process.env.IJFW_HOME) return resolve4(process.env.IJFW_HOME);
|
|
2317
|
-
return
|
|
2627
|
+
return join6(homedir3(), ".ijfw");
|
|
2318
2628
|
}
|
|
2319
2629
|
function runCheck(cmd, args, opts) {
|
|
2320
2630
|
const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
|
|
2321
2631
|
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
|
|
2322
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
|
+
}
|
|
2323
2643
|
function cloneOrPull(dir, branch) {
|
|
2324
2644
|
if (skipNetwork()) {
|
|
2325
|
-
if (
|
|
2645
|
+
if (existsSync6(dir)) {
|
|
2326
2646
|
return "skipped-network";
|
|
2327
2647
|
}
|
|
2328
2648
|
throw new Error(
|
|
2329
2649
|
`IJFW_SKIP_NETWORK=1 set but cloneOrPull needs network: target directory ${dir} does not exist. Pre-seed the directory before setting IJFW_SKIP_NETWORK, or unset the env var.`
|
|
2330
2650
|
);
|
|
2331
2651
|
}
|
|
2332
|
-
if (!
|
|
2333
|
-
|
|
2652
|
+
if (!existsSync6(dir)) {
|
|
2653
|
+
mkdirSync5(dir, { recursive: true });
|
|
2334
2654
|
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
2335
2655
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
2336
2656
|
return "cloned";
|
|
2337
2657
|
}
|
|
2338
|
-
const hasGit =
|
|
2658
|
+
const hasGit = existsSync6(join6(dir, ".git"));
|
|
2339
2659
|
if (hasGit) {
|
|
2340
2660
|
const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
2341
2661
|
if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
|
|
@@ -2359,6 +2679,13 @@ function cloneOrPull(dir, branch) {
|
|
|
2359
2679
|
console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
|
|
2360
2680
|
}
|
|
2361
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
|
+
}
|
|
2362
2689
|
const fetch = spawnSync2("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
|
|
2363
2690
|
if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
|
|
2364
2691
|
const co = spawnSync2("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
|
|
@@ -2385,32 +2712,53 @@ function cloneOrPull(dir, branch) {
|
|
|
2385
2712
|
".ijfw"
|
|
2386
2713
|
// internal — recall counter, indexes, layout version
|
|
2387
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
|
+
}
|
|
2388
2730
|
const backupDir = dir + ".bak." + Date.now();
|
|
2389
2731
|
renameSync3(dir, backupDir);
|
|
2390
2732
|
try {
|
|
2391
2733
|
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
2392
2734
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
2393
|
-
|
|
2735
|
+
const restoredItems = [];
|
|
2394
2736
|
for (const item of RESTORE_ALLOWLIST) {
|
|
2395
|
-
const src =
|
|
2396
|
-
if (
|
|
2397
|
-
const dst =
|
|
2398
|
-
if (existsSync5(dst)) rmSync2(dst, { recursive: true, force: true });
|
|
2737
|
+
const src = join6(backupDir, item);
|
|
2738
|
+
if (existsSync6(src)) {
|
|
2739
|
+
const dst = join6(dir, item);
|
|
2399
2740
|
try {
|
|
2741
|
+
if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
|
|
2400
2742
|
cpSync2(src, dst, { recursive: true, dereference: false });
|
|
2401
|
-
|
|
2402
|
-
restoredCount++;
|
|
2743
|
+
restoredItems.push(item);
|
|
2403
2744
|
} catch (cpErr) {
|
|
2404
2745
|
const msg = cpErr && cpErr.message ? cpErr.message : String(cpErr);
|
|
2405
2746
|
throw new Error(
|
|
2406
|
-
`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.`
|
|
2407
2748
|
);
|
|
2408
2749
|
}
|
|
2409
2750
|
}
|
|
2410
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
|
+
}
|
|
2411
2759
|
let backupResidual = [];
|
|
2412
2760
|
try {
|
|
2413
|
-
backupResidual =
|
|
2761
|
+
backupResidual = readdirSync3(backupDir);
|
|
2414
2762
|
} catch {
|
|
2415
2763
|
}
|
|
2416
2764
|
if (backupResidual.length === 0) {
|
|
@@ -2422,13 +2770,20 @@ function cloneOrPull(dir, branch) {
|
|
|
2422
2770
|
}
|
|
2423
2771
|
return "updated";
|
|
2424
2772
|
} catch (err) {
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
+
}
|
|
2427
2782
|
throw err;
|
|
2428
2783
|
}
|
|
2429
2784
|
}
|
|
2430
2785
|
async function runInstallScript(dir) {
|
|
2431
|
-
const canonicalDir =
|
|
2786
|
+
const canonicalDir = join6(homedir3(), ".ijfw");
|
|
2432
2787
|
const isCustomDir = resolve4(dir) !== canonicalDir;
|
|
2433
2788
|
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
|
|
2434
2789
|
await runInstall2({
|
|
@@ -2449,9 +2804,25 @@ async function main() {
|
|
|
2449
2804
|
process.exit(1);
|
|
2450
2805
|
}
|
|
2451
2806
|
const target = resolveTarget(opts);
|
|
2452
|
-
|
|
2807
|
+
if (opts.dryRun) {
|
|
2808
|
+
const { CANONICAL_ORDER: CANONICAL_ORDER2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
|
|
2809
|
+
const { renderPlan: renderPlan2 } = await Promise.resolve().then(() => (init_install_ledger(), install_ledger_exports));
|
|
2810
|
+
console.log(`IJFW install target: ${target}`);
|
|
2811
|
+
console.log("");
|
|
2812
|
+
console.log(renderPlan2(CANONICAL_ORDER2));
|
|
2813
|
+
process.exit(0);
|
|
2814
|
+
}
|
|
2815
|
+
const createdThisRun = !existsSync6(target);
|
|
2816
|
+
let platformConfigPhase = false;
|
|
2453
2817
|
const sigint = () => {
|
|
2454
|
-
if (
|
|
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
|
+
}
|
|
2825
|
+
if (createdThisRun && existsSync6(target)) {
|
|
2455
2826
|
try {
|
|
2456
2827
|
rmSync2(target, { recursive: true, force: true });
|
|
2457
2828
|
} catch (err) {
|
|
@@ -2470,9 +2841,10 @@ async function main() {
|
|
|
2470
2841
|
console.log(` version: ${ref}`);
|
|
2471
2842
|
const action = cloneOrPull(target, ref);
|
|
2472
2843
|
console.log(` repo ${action}`);
|
|
2844
|
+
platformConfigPhase = true;
|
|
2473
2845
|
await runInstallScript(target);
|
|
2474
2846
|
console.log(" platform configs applied");
|
|
2475
|
-
const canonicalDir =
|
|
2847
|
+
const canonicalDir = join6(homedir3(), ".ijfw");
|
|
2476
2848
|
const isCustomDir = process.env.IJFW_CUSTOM_DIR === "1" || resolve4(target) !== canonicalDir;
|
|
2477
2849
|
if (!opts.noMarketplace && !isCustomDir) {
|
|
2478
2850
|
const settingsPath = claudeSettingsPath();
|
|
@@ -2510,5 +2882,6 @@ if (isDirectRun()) {
|
|
|
2510
2882
|
}
|
|
2511
2883
|
export {
|
|
2512
2884
|
findBash,
|
|
2885
|
+
looksLikeIjfwInstall,
|
|
2513
2886
|
resolveBranchOrTag
|
|
2514
2887
|
};
|