@ijfw/install 1.2.9 → 1.3.0
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/README.md +5 -3
- package/dist/ijfw.js +28 -28
- package/dist/install.js +1916 -85
- package/dist/uninstall.js +36 -11
- package/package.json +1 -1
- package/src/install.ps1 +51 -22
package/dist/install.js
CHANGED
|
@@ -1,16 +1,1792 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/install-helpers.js
|
|
13
|
+
import {
|
|
14
|
+
existsSync as existsSync3,
|
|
15
|
+
readFileSync as readFileSync2,
|
|
16
|
+
writeFileSync as writeFileSync2,
|
|
17
|
+
renameSync as renameSync2,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
chmodSync,
|
|
20
|
+
mkdirSync as mkdirSync2,
|
|
21
|
+
realpathSync,
|
|
22
|
+
statSync
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { dirname as dirname3, basename, join as join3, normalize, delimiter } from "node:path";
|
|
25
|
+
import { homedir as homedir2 } from "node:os";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
function printOk(msg) {
|
|
28
|
+
process.stdout.write(` [ok] ${msg}
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function printNote(msg) {
|
|
32
|
+
process.stdout.write(` [--] ${msg}
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
function printInfo(msg) {
|
|
36
|
+
process.stdout.write(` -- ${msg}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
function printWarn(msg) {
|
|
40
|
+
process.stdout.write(` [!] ${msg}
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
function printSection(name) {
|
|
44
|
+
process.stdout.write(`
|
|
45
|
+
[${name}]
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
function homeReal() {
|
|
49
|
+
const h = homedir2();
|
|
50
|
+
try {
|
|
51
|
+
return realpathSync(h);
|
|
52
|
+
} catch {
|
|
53
|
+
return h;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function nativePath(p) {
|
|
57
|
+
if (p == null) return p;
|
|
58
|
+
return normalize(String(p));
|
|
59
|
+
}
|
|
60
|
+
function writeAtomic(path3, contents, opts = {}) {
|
|
61
|
+
const mode = opts.mode ?? 384;
|
|
62
|
+
mkdirSync2(dirname3(path3), { recursive: true });
|
|
63
|
+
const tmp = `${path3}.tmp.${process.pid}`;
|
|
64
|
+
writeFileSync2(tmp, contents, { mode });
|
|
65
|
+
renameSync2(tmp, path3);
|
|
66
|
+
try {
|
|
67
|
+
chmodSync(path3, mode);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function backup(path3, ts) {
|
|
72
|
+
try {
|
|
73
|
+
const st = statSync(path3);
|
|
74
|
+
if (!st.isFile()) return null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const dst = `${path3}.bak.${ts}`;
|
|
79
|
+
try {
|
|
80
|
+
copyFileSync(path3, dst);
|
|
81
|
+
printInfo(`backup: ${basename(path3)}.bak.${ts}`);
|
|
82
|
+
return dst;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function safeChecksum(path3) {
|
|
88
|
+
try {
|
|
89
|
+
return createHash("sha1").update(readFileSync2(path3)).digest("hex");
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function installHook(src, dst, ts) {
|
|
95
|
+
if (!existsSync3(src)) return;
|
|
96
|
+
if (existsSync3(dst)) {
|
|
97
|
+
const srcSum = safeChecksum(src);
|
|
98
|
+
const dstSum = safeChecksum(dst);
|
|
99
|
+
if (!srcSum || !dstSum) {
|
|
100
|
+
try {
|
|
101
|
+
copyFileSync(dst, `${dst}.bak.${ts}`);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
printNote(`Updated ${basename(dst)} (no checksum util on host -- precautionary backup)`);
|
|
105
|
+
} else if (srcSum === dstSum) {
|
|
106
|
+
return;
|
|
107
|
+
} else {
|
|
108
|
+
try {
|
|
109
|
+
copyFileSync(dst, `${dst}.bak.${ts}`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
printNote(`Updated ${basename(dst)} (your custom version backed up to ${basename(dst)}.bak.${ts})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
116
|
+
copyFileSync(src, dst);
|
|
117
|
+
if (!IS_WIN) {
|
|
118
|
+
try {
|
|
119
|
+
chmodSync(dst, 493);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function which(bin) {
|
|
125
|
+
if (!bin) return null;
|
|
126
|
+
const path3 = process.env.PATH || "";
|
|
127
|
+
const parts = path3.split(delimiter).filter(Boolean);
|
|
128
|
+
const exts = IS_WIN ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()) : [""];
|
|
129
|
+
for (const dir of parts) {
|
|
130
|
+
for (const ext of exts) {
|
|
131
|
+
const candidate = join3(dir, bin + ext);
|
|
132
|
+
try {
|
|
133
|
+
if (existsSync3(candidate)) return candidate;
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function hasBin(bin) {
|
|
141
|
+
return which(bin) != null;
|
|
142
|
+
}
|
|
143
|
+
function isLive(targetId, home) {
|
|
144
|
+
const H = home || homeReal();
|
|
145
|
+
const APPDATA = process.env.APPDATA || "";
|
|
146
|
+
const appdataOr = (rel) => APPDATA ? join3(APPDATA, rel) : null;
|
|
147
|
+
switch (targetId) {
|
|
148
|
+
case "claude":
|
|
149
|
+
return hasBin("claude") || existsSync3(join3(H, ".claude"));
|
|
150
|
+
case "codex":
|
|
151
|
+
return hasBin("codex") || existsSync3(join3(H, ".codex"));
|
|
152
|
+
case "gemini":
|
|
153
|
+
return hasBin("gemini") || existsSync3(join3(H, ".gemini"));
|
|
154
|
+
case "cursor":
|
|
155
|
+
return hasBin("cursor");
|
|
156
|
+
case "windsurf":
|
|
157
|
+
return hasBin("windsurf") || existsSync3(join3(H, ".codeium", "windsurf"));
|
|
158
|
+
case "copilot": {
|
|
159
|
+
if (hasBin("code")) return true;
|
|
160
|
+
const candidates = [
|
|
161
|
+
join3(H, ".vscode"),
|
|
162
|
+
join3(H, ".config", "Code"),
|
|
163
|
+
join3(H, "Library", "Application Support", "Code"),
|
|
164
|
+
appdataOr("Code")
|
|
165
|
+
].filter(Boolean);
|
|
166
|
+
return candidates.some((p) => existsSync3(p));
|
|
167
|
+
}
|
|
168
|
+
case "hermes":
|
|
169
|
+
return hasBin("hermes") || existsSync3(join3(H, ".hermes"));
|
|
170
|
+
case "wayland":
|
|
171
|
+
return hasBin("wayland") || existsSync3(join3(H, ".wayland"));
|
|
172
|
+
case "opencode":
|
|
173
|
+
return hasBin("opencode") || existsSync3(join3(H, ".config", "opencode"));
|
|
174
|
+
case "qwen":
|
|
175
|
+
return hasBin("qwen") || existsSync3(join3(H, ".qwen"));
|
|
176
|
+
case "cline": {
|
|
177
|
+
const ext = "saoudrizwan.claude-dev";
|
|
178
|
+
const candidates = [
|
|
179
|
+
// macOS
|
|
180
|
+
join3(H, "Library", "Application Support", "Code", "User", "globalStorage", ext),
|
|
181
|
+
join3(H, "Library", "Application Support", "Code - Insiders", "User", "globalStorage", ext),
|
|
182
|
+
join3(H, "Library", "Application Support", "VSCodium", "User", "globalStorage", ext),
|
|
183
|
+
// Linux
|
|
184
|
+
join3(H, ".config", "Code", "User", "globalStorage", ext),
|
|
185
|
+
join3(H, ".config", "VSCodium", "User", "globalStorage", ext),
|
|
186
|
+
join3(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User", "globalStorage", ext),
|
|
187
|
+
join3(H, "snap", "code", "current", ".config", "Code", "User", "globalStorage", ext),
|
|
188
|
+
// Windows
|
|
189
|
+
appdataOr(join3("Code", "User", "globalStorage", ext)),
|
|
190
|
+
appdataOr(join3("Code - Insiders", "User", "globalStorage", ext)),
|
|
191
|
+
appdataOr(join3("VSCodium", "User", "globalStorage", ext)),
|
|
192
|
+
// Catch-all extensions dir
|
|
193
|
+
join3(H, ".vscode", "extensions")
|
|
194
|
+
].filter(Boolean);
|
|
195
|
+
return candidates.some((p) => existsSync3(p));
|
|
196
|
+
}
|
|
197
|
+
case "kimi":
|
|
198
|
+
return hasBin("kimi") || existsSync3(join3(H, ".kimi"));
|
|
199
|
+
case "openclaw":
|
|
200
|
+
return hasBin("openclaw") || existsSync3(join3(H, ".openclaw"));
|
|
201
|
+
case "aider":
|
|
202
|
+
return hasBin("aider") || existsSync3(join3(H, ".aider.conf.yml"));
|
|
203
|
+
default:
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function prettyName(targetId) {
|
|
208
|
+
const map = {
|
|
209
|
+
claude: "Claude Code",
|
|
210
|
+
codex: "Codex",
|
|
211
|
+
gemini: "Gemini",
|
|
212
|
+
cursor: "Cursor",
|
|
213
|
+
windsurf: "Windsurf",
|
|
214
|
+
copilot: "Copilot",
|
|
215
|
+
hermes: "Hermes",
|
|
216
|
+
wayland: "Wayland",
|
|
217
|
+
opencode: "OpenCode",
|
|
218
|
+
qwen: "Qwen Code",
|
|
219
|
+
cline: "Cline",
|
|
220
|
+
kimi: "Kimi Code",
|
|
221
|
+
openclaw: "OpenClaw",
|
|
222
|
+
aider: "Aider"
|
|
223
|
+
};
|
|
224
|
+
return map[targetId] || String(targetId);
|
|
225
|
+
}
|
|
226
|
+
function readJsonOrEmpty(path3) {
|
|
227
|
+
if (!existsSync3(path3)) return {};
|
|
228
|
+
try {
|
|
229
|
+
const raw = readFileSync2(path3, "utf8") || "{}";
|
|
230
|
+
const parsed = JSON.parse(raw);
|
|
231
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
232
|
+
return parsed;
|
|
233
|
+
} catch {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function mergeJson(dst, serverJs, ts) {
|
|
238
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
239
|
+
if (ts) backup(dst, ts);
|
|
240
|
+
const doc = readJsonOrEmpty(dst);
|
|
241
|
+
if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
|
|
242
|
+
const isWin = IS_WIN;
|
|
243
|
+
const nodeDir = dirname3(process.execPath);
|
|
244
|
+
const home = homedir2();
|
|
245
|
+
const candidatePaths = isWin ? [] : [
|
|
246
|
+
nodeDir,
|
|
247
|
+
"/opt/homebrew/bin",
|
|
248
|
+
"/usr/local/bin",
|
|
249
|
+
`${home}/.nvm/versions/node/${process.version}/bin`,
|
|
250
|
+
"/usr/bin",
|
|
251
|
+
"/bin"
|
|
252
|
+
];
|
|
253
|
+
const envPath = candidatePaths.filter((d) => {
|
|
254
|
+
try {
|
|
255
|
+
return typeof d === "string" && d.length > 0 && existsSync3(d);
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}).join(":");
|
|
260
|
+
const entry = { command: "node", args: [serverJs] };
|
|
261
|
+
if (envPath) entry.env = { PATH: envPath };
|
|
262
|
+
doc.mcpServers["ijfw-memory"] = entry;
|
|
263
|
+
writeAtomic(dst, JSON.stringify(doc, null, 2) + "\n", { mode: 384 });
|
|
264
|
+
}
|
|
265
|
+
function mergeToml(dst, serverJs, ts) {
|
|
266
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
267
|
+
if (ts) backup(dst, ts);
|
|
268
|
+
let text = "";
|
|
269
|
+
try {
|
|
270
|
+
text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
|
|
271
|
+
} catch {
|
|
272
|
+
text = "";
|
|
273
|
+
}
|
|
274
|
+
text = stripTomlSection(text, "mcp_servers.ijfw-memory");
|
|
275
|
+
if (/^\[features\]/m.test(text)) {
|
|
276
|
+
if (!/^codex_hooks\s*=/m.test(text)) {
|
|
277
|
+
text = text.replace(/^(\[features\][^\n]*\n)/m, "$1codex_hooks = true\n");
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
text = text.replace(/\n+$/, "") + "\n\n[features]\ncodex_hooks = true\n";
|
|
281
|
+
}
|
|
282
|
+
if (!/^suppress_unstable_features_warning\s*=/m.test(text)) {
|
|
283
|
+
if (/^\[/m.test(text)) {
|
|
284
|
+
text = text.replace(/^(\[)/m, "suppress_unstable_features_warning = true\n\n$1");
|
|
285
|
+
} else {
|
|
286
|
+
text = text.replace(/\n+$/, "") + "\nsuppress_unstable_features_warning = true\n";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const escaped = String(serverJs).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
290
|
+
let block = "";
|
|
291
|
+
if (text && !text.endsWith("\n")) block += "\n";
|
|
292
|
+
block += "\n[mcp_servers.ijfw-memory]\n";
|
|
293
|
+
block += 'command = "node"\n';
|
|
294
|
+
block += `args = ["${escaped}"]
|
|
295
|
+
`;
|
|
296
|
+
block += "enabled = true\n";
|
|
297
|
+
block += "startup_timeout_sec = 10\n";
|
|
298
|
+
block += "tool_timeout_sec = 30\n";
|
|
299
|
+
writeAtomic(dst, text + block, { mode: 384 });
|
|
300
|
+
}
|
|
301
|
+
function stripTomlSection(text, sectionName) {
|
|
302
|
+
const safe = sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303
|
+
const lines = text.split("\n");
|
|
304
|
+
const out = [];
|
|
305
|
+
let skip = false;
|
|
306
|
+
const headerRe = new RegExp(`^\\[${safe}\\][\\s]*$`);
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
if (headerRe.test(line)) {
|
|
309
|
+
skip = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (skip && line.startsWith("[") && !headerRe.test(line)) skip = false;
|
|
313
|
+
if (skip) continue;
|
|
314
|
+
out.push(line);
|
|
315
|
+
}
|
|
316
|
+
return out.join("\n");
|
|
317
|
+
}
|
|
318
|
+
function mergeYamlMcp(dst, serverJs, ts) {
|
|
319
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
320
|
+
if (ts) backup(dst, ts);
|
|
321
|
+
let text = "";
|
|
322
|
+
try {
|
|
323
|
+
text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
|
|
324
|
+
} catch {
|
|
325
|
+
text = "";
|
|
326
|
+
}
|
|
327
|
+
text = stripSentinelBlock(text, "# IJFW-MCP-BEGIN ijfw-memory", "# IJFW-MCP-END ijfw-memory");
|
|
328
|
+
if (!/^mcp_servers:/m.test(text)) {
|
|
329
|
+
if (text && !text.endsWith("\n")) text += "\n";
|
|
330
|
+
text += "\nmcp_servers:\n";
|
|
331
|
+
}
|
|
332
|
+
const escaped = String(serverJs).replace(/"/g, '\\"');
|
|
333
|
+
let block = "";
|
|
334
|
+
if (!text.endsWith("\n")) block += "\n";
|
|
335
|
+
block += "# IJFW-MCP-BEGIN ijfw-memory\n";
|
|
336
|
+
block += " ijfw-memory:\n";
|
|
337
|
+
block += ' command: "node"\n';
|
|
338
|
+
block += ` args: ["${escaped}"]
|
|
339
|
+
`;
|
|
340
|
+
block += " enabled: true\n";
|
|
341
|
+
block += "# IJFW-MCP-END ijfw-memory\n";
|
|
342
|
+
writeAtomic(dst, text + block, { mode: 384 });
|
|
343
|
+
}
|
|
344
|
+
function stripSentinelBlock(text, beginMark, endMark) {
|
|
345
|
+
const lines = text.split("\n");
|
|
346
|
+
const out = [];
|
|
347
|
+
let skip = false;
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
if (line === beginMark) {
|
|
350
|
+
skip = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (line === endMark) {
|
|
354
|
+
skip = false;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (skip) continue;
|
|
358
|
+
out.push(line);
|
|
359
|
+
}
|
|
360
|
+
return out.join("\n");
|
|
361
|
+
}
|
|
362
|
+
function mergeYamlPluginsEnabled(dst, pluginName, ts) {
|
|
363
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
364
|
+
if (ts) backup(dst, ts);
|
|
365
|
+
let text = "";
|
|
366
|
+
try {
|
|
367
|
+
text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
|
|
368
|
+
} catch {
|
|
369
|
+
text = "";
|
|
370
|
+
}
|
|
371
|
+
text = stripSentinelBlock(text, "# IJFW-PLUGINS-BEGIN", "# IJFW-PLUGINS-END");
|
|
372
|
+
const lines = text.split("\n");
|
|
373
|
+
let pluginsLineIdx = -1;
|
|
374
|
+
let enabledLineIdx = -1;
|
|
375
|
+
let inPluginsBlock = false;
|
|
376
|
+
let alreadyListed = false;
|
|
377
|
+
const itemRe = new RegExp(`^\\s+-\\s+${pluginName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`);
|
|
378
|
+
for (let i = 0; i < lines.length; i++) {
|
|
379
|
+
const line = lines[i];
|
|
380
|
+
if (/^plugins:\s*$/.test(line)) {
|
|
381
|
+
pluginsLineIdx = i;
|
|
382
|
+
inPluginsBlock = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (inPluginsBlock) {
|
|
386
|
+
if (/^\S/.test(line) && line.trim() !== "") {
|
|
387
|
+
inPluginsBlock = false;
|
|
388
|
+
} else if (/^\s+enabled:\s*(\[\s*\])?\s*$/.test(line) || /^\s+enabled:\s*\[.*\]\s*$/.test(line)) {
|
|
389
|
+
enabledLineIdx = i;
|
|
390
|
+
} else if (enabledLineIdx >= 0 && itemRe.test(line)) {
|
|
391
|
+
alreadyListed = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (pluginsLineIdx < 0) {
|
|
396
|
+
if (text && !text.endsWith("\n")) text += "\n";
|
|
397
|
+
text += "\nplugins:\n";
|
|
398
|
+
const newLines = text.split("\n");
|
|
399
|
+
pluginsLineIdx = newLines.length - 2;
|
|
400
|
+
enabledLineIdx = -1;
|
|
401
|
+
}
|
|
402
|
+
let cur = text.split("\n");
|
|
403
|
+
if (enabledLineIdx < 0) {
|
|
404
|
+
cur.splice(pluginsLineIdx + 1, 0, " enabled: []");
|
|
405
|
+
enabledLineIdx = pluginsLineIdx + 1;
|
|
406
|
+
}
|
|
407
|
+
if (!alreadyListed) {
|
|
408
|
+
const enabledLine = cur[enabledLineIdx];
|
|
409
|
+
if (/^\s+enabled:\s*\[\s*\]\s*$/.test(enabledLine)) {
|
|
410
|
+
cur[enabledLineIdx] = " enabled:";
|
|
411
|
+
cur.splice(enabledLineIdx + 1, 0, ` - ${pluginName}`);
|
|
412
|
+
} else if (/^\s+enabled:\s*\[.+\]\s*$/.test(enabledLine)) {
|
|
413
|
+
cur[enabledLineIdx] = enabledLine.replace(/\]\s*$/, `, ${pluginName}]`);
|
|
414
|
+
} else {
|
|
415
|
+
cur.splice(enabledLineIdx + 1, 0, ` - ${pluginName}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
let outText = cur.join("\n");
|
|
419
|
+
if (outText && !outText.endsWith("\n")) outText += "\n";
|
|
420
|
+
outText += "# IJFW-PLUGINS-BEGIN\n";
|
|
421
|
+
outText += `# plugin ${pluginName} registered by IJFW installer
|
|
422
|
+
`;
|
|
423
|
+
outText += "# IJFW-PLUGINS-END\n";
|
|
424
|
+
writeAtomic(dst, outText, { mode: 384 });
|
|
425
|
+
}
|
|
426
|
+
function opencodeMerge(dst, serverJs, ts) {
|
|
427
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
428
|
+
if (ts) backup(dst, ts);
|
|
429
|
+
const doc = readJsonOrEmpty(dst);
|
|
430
|
+
if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
|
|
431
|
+
doc.mcp["ijfw-memory"] = { type: "local", command: ["node", serverJs] };
|
|
432
|
+
writeAtomic(dst, JSON.stringify(doc, null, 2), { mode: 384 });
|
|
433
|
+
}
|
|
434
|
+
function openclawMerge(dst, serverJs, ts) {
|
|
435
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
436
|
+
if (ts) backup(dst, ts);
|
|
437
|
+
const doc = readJsonOrEmpty(dst);
|
|
438
|
+
if (!doc.mcp || typeof doc.mcp !== "object") doc.mcp = {};
|
|
439
|
+
if (!doc.mcp.servers || typeof doc.mcp.servers !== "object") doc.mcp.servers = {};
|
|
440
|
+
doc.mcp.servers["ijfw-memory"] = { command: "node", args: [serverJs] };
|
|
441
|
+
writeAtomic(dst, JSON.stringify(doc, null, 2), { mode: 384 });
|
|
442
|
+
}
|
|
443
|
+
function clineMerge(serverJs, home, ts) {
|
|
444
|
+
const H = home || homeReal();
|
|
445
|
+
const APPDATA = process.env.APPDATA || join3(H, "AppData", "Roaming");
|
|
446
|
+
const ext = "saoudrizwan.claude-dev";
|
|
447
|
+
let candidates;
|
|
448
|
+
let osDefault;
|
|
449
|
+
if (process.platform === "darwin") {
|
|
450
|
+
candidates = [
|
|
451
|
+
join3(H, "Library", "Application Support", "Code", "User"),
|
|
452
|
+
join3(H, "Library", "Application Support", "Code - Insiders", "User"),
|
|
453
|
+
join3(H, "Library", "Application Support", "VSCodium", "User")
|
|
454
|
+
];
|
|
455
|
+
osDefault = join3(H, "Library", "Application Support", "Code", "User");
|
|
456
|
+
} else if (IS_WIN) {
|
|
457
|
+
candidates = [
|
|
458
|
+
join3(APPDATA, "Code", "User"),
|
|
459
|
+
join3(APPDATA, "Code - Insiders", "User"),
|
|
460
|
+
join3(APPDATA, "VSCodium", "User")
|
|
461
|
+
];
|
|
462
|
+
osDefault = join3(APPDATA, "Code", "User");
|
|
463
|
+
} else {
|
|
464
|
+
candidates = [
|
|
465
|
+
join3(H, ".config", "Code", "User"),
|
|
466
|
+
join3(H, ".config", "VSCodium", "User"),
|
|
467
|
+
join3(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
|
|
468
|
+
join3(H, "snap", "code", "current", ".config", "Code", "User")
|
|
469
|
+
];
|
|
470
|
+
osDefault = join3(H, ".config", "Code", "User");
|
|
471
|
+
}
|
|
472
|
+
let userDir = "";
|
|
473
|
+
for (const c of candidates) {
|
|
474
|
+
if (existsSync3(join3(c, "globalStorage", ext))) {
|
|
475
|
+
userDir = c;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!userDir) userDir = osDefault;
|
|
480
|
+
const dst = join3(userDir, "globalStorage", ext, "settings", "cline_mcp_settings.json");
|
|
481
|
+
mkdirSync2(dirname3(dst), { recursive: true });
|
|
482
|
+
if (ts) backup(dst, ts);
|
|
483
|
+
const doc = readJsonOrEmpty(dst);
|
|
484
|
+
if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
|
|
485
|
+
doc.mcpServers["ijfw-memory"] = {
|
|
486
|
+
type: "stdio",
|
|
487
|
+
command: "node",
|
|
488
|
+
args: [serverJs],
|
|
489
|
+
disabled: false,
|
|
490
|
+
autoApprove: [],
|
|
491
|
+
timeout: 60
|
|
492
|
+
};
|
|
493
|
+
writeAtomic(dst, JSON.stringify(doc, null, 2), { mode: 384 });
|
|
494
|
+
return dst;
|
|
495
|
+
}
|
|
496
|
+
var IS_WIN;
|
|
497
|
+
var init_install_helpers = __esm({
|
|
498
|
+
"src/install-helpers.js"() {
|
|
499
|
+
IS_WIN = process.platform === "win32";
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// src/install-targets-1-7.js
|
|
504
|
+
import {
|
|
505
|
+
existsSync as existsSync4,
|
|
506
|
+
mkdirSync as mkdirSync3,
|
|
507
|
+
readFileSync as readFileSync3,
|
|
508
|
+
writeFileSync as writeFileSync3,
|
|
509
|
+
readdirSync,
|
|
510
|
+
statSync as statSync2,
|
|
511
|
+
copyFileSync as copyFileSync2,
|
|
512
|
+
cpSync,
|
|
513
|
+
chmodSync as chmodSync2
|
|
514
|
+
} from "node:fs";
|
|
515
|
+
import { join as join4, dirname as dirname4, isAbsolute } from "node:path";
|
|
516
|
+
import { platform } from "node:os";
|
|
517
|
+
function ensureDir(p) {
|
|
518
|
+
try {
|
|
519
|
+
mkdirSync3(p, { recursive: true });
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function copyIfAbsent(src, dst) {
|
|
524
|
+
if (!existsSync4(src)) return false;
|
|
525
|
+
if (existsSync4(dst)) return false;
|
|
526
|
+
ensureDir(dirname4(dst));
|
|
527
|
+
try {
|
|
528
|
+
copyFileSync2(src, dst);
|
|
529
|
+
return true;
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function copyDirIfAbsent(src, dst) {
|
|
535
|
+
if (!existsSync4(src)) return false;
|
|
536
|
+
if (existsSync4(dst)) return false;
|
|
537
|
+
ensureDir(dirname4(dst));
|
|
538
|
+
try {
|
|
539
|
+
cpSync(src, dst, { recursive: true });
|
|
540
|
+
return true;
|
|
541
|
+
} catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function listSubdirs(parent) {
|
|
546
|
+
if (!existsSync4(parent)) return [];
|
|
547
|
+
let names;
|
|
548
|
+
try {
|
|
549
|
+
names = readdirSync(parent);
|
|
550
|
+
} catch {
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
const out = [];
|
|
554
|
+
for (const name of names) {
|
|
555
|
+
if (name === "__pycache__") continue;
|
|
556
|
+
const path3 = join4(parent, name);
|
|
557
|
+
try {
|
|
558
|
+
if (statSync2(path3).isDirectory()) out.push({ name, path: path3 });
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
564
|
+
function listFiles(parent, extFilter) {
|
|
565
|
+
if (!existsSync4(parent)) return [];
|
|
566
|
+
let names;
|
|
567
|
+
try {
|
|
568
|
+
names = readdirSync(parent);
|
|
569
|
+
} catch {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
const out = [];
|
|
573
|
+
for (const name of names) {
|
|
574
|
+
if (extFilter && !name.endsWith(extFilter)) continue;
|
|
575
|
+
const path3 = join4(parent, name);
|
|
576
|
+
try {
|
|
577
|
+
if (statSync2(path3).isFile()) out.push({ name, path: path3 });
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
function customDirNoop(ctx, targetId, displayName, reason) {
|
|
584
|
+
ctx.log.info(reason);
|
|
585
|
+
ctx.log.ok(`${displayName}: real platform config left untouched.`);
|
|
586
|
+
return { status: "noop" };
|
|
587
|
+
}
|
|
588
|
+
async function installClaude(ctx) {
|
|
589
|
+
if (ctx.ijfwCustomDir) {
|
|
590
|
+
return customDirNoop(
|
|
591
|
+
ctx,
|
|
592
|
+
"claude",
|
|
593
|
+
"Claude Code",
|
|
594
|
+
"Custom-dir install -- skipping ~/.claude/settings.json merge."
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const claudePluginPath = join4(ctx.home, ".ijfw", "claude");
|
|
598
|
+
const claudeSettings = join4(ctx.home, ".claude", "settings.json");
|
|
599
|
+
const claudeMarketplaces = join4(
|
|
600
|
+
ctx.home,
|
|
601
|
+
".claude",
|
|
602
|
+
"plugins",
|
|
603
|
+
"known_marketplaces.json"
|
|
604
|
+
);
|
|
605
|
+
ensureDir(join4(ctx.home, ".claude", "plugins"));
|
|
606
|
+
backup(claudeSettings, ctx.ts);
|
|
607
|
+
let settings = {};
|
|
608
|
+
if (existsSync4(claudeSettings)) {
|
|
609
|
+
try {
|
|
610
|
+
settings = JSON.parse(readFileSync3(claudeSettings, "utf8") || "{}");
|
|
611
|
+
} catch {
|
|
612
|
+
settings = {};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (!settings || typeof settings !== "object") settings = {};
|
|
616
|
+
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
617
|
+
settings.enabledPlugins["ijfw@ijfw"] = true;
|
|
618
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
619
|
+
settings.extraKnownMarketplaces["ijfw"] = {
|
|
620
|
+
source: { source: "directory", path: claudePluginPath }
|
|
621
|
+
};
|
|
622
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
623
|
+
let mp = {};
|
|
624
|
+
if (existsSync4(claudeMarketplaces)) {
|
|
625
|
+
try {
|
|
626
|
+
mp = JSON.parse(readFileSync3(claudeMarketplaces, "utf8") || "{}");
|
|
627
|
+
} catch {
|
|
628
|
+
mp = {};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!mp || typeof mp !== "object") mp = {};
|
|
632
|
+
mp["ijfw"] = {
|
|
633
|
+
source: { source: "directory", path: claudePluginPath },
|
|
634
|
+
installLocation: claudePluginPath,
|
|
635
|
+
lastUpdated: now
|
|
636
|
+
};
|
|
637
|
+
writeAtomic(claudeMarketplaces, JSON.stringify(mp, null, 2) + "\n");
|
|
638
|
+
const existing = settings.mcpServers && settings.mcpServers["ijfw-memory"];
|
|
639
|
+
if (existing && existing.command) {
|
|
640
|
+
const cmd = existing.command;
|
|
641
|
+
if (isAbsolute(cmd) && !existsSync4(cmd)) {
|
|
642
|
+
delete settings.mcpServers["ijfw-memory"];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
646
|
+
settings.mcpServers["ijfw-memory"] = {
|
|
647
|
+
command: "node",
|
|
648
|
+
args: [ctx.serverJsNative],
|
|
649
|
+
env: {}
|
|
650
|
+
};
|
|
651
|
+
writeAtomic(claudeSettings, JSON.stringify(settings, null, 2) + "\n");
|
|
652
|
+
const launcherPath = join4(ctx.repoRoot, "mcp-server", "bin", "ijfw-memory");
|
|
653
|
+
if (platform() !== "win32" && existsSync4(launcherPath)) {
|
|
654
|
+
try {
|
|
655
|
+
chmodSync2(launcherPath, 493);
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
ctx.log.ok("Claude Code ready.");
|
|
660
|
+
ctx.log.note(`.claudeignore template at ${ctx.repoRoot}/claude/.claudeignore`);
|
|
661
|
+
ctx.log.note(" Copy to your project root for instant context savings.");
|
|
662
|
+
return { status: "ok", restart: false };
|
|
663
|
+
}
|
|
664
|
+
async function installCodex(ctx) {
|
|
665
|
+
if (ctx.ijfwCustomDir) {
|
|
666
|
+
return customDirNoop(
|
|
667
|
+
ctx,
|
|
668
|
+
"codex",
|
|
669
|
+
"Codex",
|
|
670
|
+
"Custom-dir install -- skipping ~/.codex/ merges."
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const configToml = join4(ctx.home, ".codex", "config.toml");
|
|
674
|
+
ensureDir(dirname4(configToml));
|
|
675
|
+
mergeToml(configToml, ctx.serverJsNative);
|
|
676
|
+
const hooksDst = join4(ctx.home, ".codex", "hooks.json");
|
|
677
|
+
const hooksSrc = join4(ctx.repoRoot, "codex", ".codex", "hooks.json");
|
|
678
|
+
const hooksBase = join4(ctx.home, ".codex", "hooks");
|
|
679
|
+
ensureDir(hooksBase);
|
|
680
|
+
if (existsSync4(hooksSrc)) {
|
|
681
|
+
let doc = {};
|
|
682
|
+
let rawForBackup = null;
|
|
683
|
+
if (existsSync4(hooksDst)) {
|
|
684
|
+
rawForBackup = readFileSync3(hooksDst, "utf8");
|
|
685
|
+
try {
|
|
686
|
+
doc = JSON.parse(rawForBackup || "{}");
|
|
687
|
+
} catch {
|
|
688
|
+
doc = {};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const isLegacyShape = Array.isArray(doc) || doc && typeof doc === "object" && doc.hooks && (Array.isArray(doc.hooks) || typeof doc.hooks !== "object");
|
|
692
|
+
if (isLegacyShape && rawForBackup) {
|
|
693
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
694
|
+
const bk = `${hooksDst}.legacy.bak.${ts}`;
|
|
695
|
+
try {
|
|
696
|
+
writeFileSync3(bk, rawForBackup);
|
|
697
|
+
ctx.log.note(`preserved legacy hooks.json at ${bk}`);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) doc = {};
|
|
702
|
+
if (!doc.hooks || typeof doc.hooks !== "object" || Array.isArray(doc.hooks)) {
|
|
703
|
+
doc.hooks = {};
|
|
704
|
+
}
|
|
705
|
+
const VALID_EVENTS = [
|
|
706
|
+
"SessionStart",
|
|
707
|
+
"UserPromptSubmit",
|
|
708
|
+
"PreToolUse",
|
|
709
|
+
"PostToolUse",
|
|
710
|
+
"Stop",
|
|
711
|
+
"PermissionRequest"
|
|
712
|
+
];
|
|
713
|
+
for (const ev of VALID_EVENTS) {
|
|
714
|
+
if (!Array.isArray(doc.hooks[ev])) doc.hooks[ev] = [];
|
|
715
|
+
doc.hooks[ev] = doc.hooks[ev].filter((g) => {
|
|
716
|
+
if (!g || !Array.isArray(g.hooks)) return true;
|
|
717
|
+
return !g.hooks.some((h) => h && h._ijfw);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const shellQuote = (p) => {
|
|
721
|
+
if (/^[A-Za-z0-9_./@-]+$/.test(p)) return p;
|
|
722
|
+
return "'" + p.replace(/'/g, "'\\''") + "'";
|
|
723
|
+
};
|
|
724
|
+
let ijfw = {};
|
|
725
|
+
try {
|
|
726
|
+
ijfw = JSON.parse(readFileSync3(hooksSrc, "utf8"));
|
|
727
|
+
} catch {
|
|
728
|
+
ijfw = {};
|
|
729
|
+
}
|
|
730
|
+
const srcHooks = ijfw && ijfw.hooks ? ijfw.hooks : {};
|
|
731
|
+
for (const [ev, groups] of Object.entries(srcHooks)) {
|
|
732
|
+
if (!VALID_EVENTS.includes(ev)) continue;
|
|
733
|
+
if (!Array.isArray(groups)) continue;
|
|
734
|
+
for (const g of groups) {
|
|
735
|
+
if (!g || !Array.isArray(g.hooks)) continue;
|
|
736
|
+
const rewritten = g.hooks.map((h) => {
|
|
737
|
+
if (!h || h.type !== "command" || !h.command) return h;
|
|
738
|
+
const rel = String(h.command).replace(/^hooks\//, "");
|
|
739
|
+
const cmd = shellQuote(`${hooksBase}/${rel}`);
|
|
740
|
+
return { ...h, command: cmd };
|
|
741
|
+
});
|
|
742
|
+
doc.hooks[ev].push({
|
|
743
|
+
...g.matcher ? { matcher: g.matcher } : {},
|
|
744
|
+
hooks: rewritten
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
writeAtomic(hooksDst, JSON.stringify(doc, null, 2) + "\n");
|
|
749
|
+
}
|
|
750
|
+
const hookScriptsDir = join4(ctx.repoRoot, "codex", ".codex", "hooks");
|
|
751
|
+
for (const f of listFiles(hookScriptsDir, ".sh")) {
|
|
752
|
+
installHook(f.path, join4(hooksBase, f.name), ctx.ts);
|
|
753
|
+
}
|
|
754
|
+
const codexCtx = join4(ctx.home, ".codex", "IJFW.md");
|
|
755
|
+
copyIfAbsent(join4(ctx.repoRoot, "codex", ".codex", "IJFW.md"), codexCtx);
|
|
756
|
+
const userSkills = join4(ctx.home, ".codex", "skills");
|
|
757
|
+
ensureDir(userSkills);
|
|
758
|
+
const repoSkills = join4(ctx.repoRoot, "codex", "skills");
|
|
759
|
+
for (const sd of listSubdirs(repoSkills)) {
|
|
760
|
+
copyDirIfAbsent(sd.path, join4(userSkills, sd.name));
|
|
761
|
+
}
|
|
762
|
+
const cwd = ctx.cwd || process.cwd();
|
|
763
|
+
if (existsSync4(join4(cwd, ".codex", "config.toml")) || existsSync4(join4(cwd, ".ijfw"))) {
|
|
764
|
+
const projSkills = join4(cwd, ".codex", "skills");
|
|
765
|
+
ensureDir(projSkills);
|
|
766
|
+
for (const sd of listSubdirs(repoSkills)) {
|
|
767
|
+
copyDirIfAbsent(sd.path, join4(projSkills, sd.name));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
ctx.log.ok("Installed Codex bundle: MCP + hooks + 15 skills + context");
|
|
771
|
+
return { status: "ok" };
|
|
772
|
+
}
|
|
773
|
+
async function installGemini(ctx) {
|
|
774
|
+
if (ctx.ijfwCustomDir) {
|
|
775
|
+
return customDirNoop(
|
|
776
|
+
ctx,
|
|
777
|
+
"gemini",
|
|
778
|
+
"Gemini",
|
|
779
|
+
"Custom-dir install -- skipping ~/.gemini/ merges."
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const dst = join4(ctx.home, ".gemini", "settings.json");
|
|
783
|
+
ensureDir(dirname4(dst));
|
|
784
|
+
mergeJson(dst, ctx.serverJsNative);
|
|
785
|
+
const extDst = join4(ctx.home, ".gemini", "extensions", "ijfw");
|
|
786
|
+
const extSrc = join4(ctx.repoRoot, "gemini", "extensions", "ijfw");
|
|
787
|
+
for (const sub of ["hooks", "skills", "commands", "agents", "policies"]) {
|
|
788
|
+
ensureDir(join4(extDst, sub));
|
|
789
|
+
}
|
|
790
|
+
for (const rel of [
|
|
791
|
+
"gemini-extension.json",
|
|
792
|
+
"IJFW.md",
|
|
793
|
+
"hooks/hooks.json",
|
|
794
|
+
"policies/ijfw.toml"
|
|
795
|
+
]) {
|
|
796
|
+
const dstFile = join4(extDst, rel);
|
|
797
|
+
if (!existsSync4(dstFile)) {
|
|
798
|
+
ensureDir(dirname4(dstFile));
|
|
799
|
+
const srcFile = join4(extSrc, rel);
|
|
800
|
+
try {
|
|
801
|
+
if (existsSync4(srcFile)) copyFileSync2(srcFile, dstFile);
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const hooksJson = join4(extDst, "hooks", "hooks.json");
|
|
807
|
+
if (existsSync4(hooksJson)) {
|
|
808
|
+
let raw = "";
|
|
809
|
+
try {
|
|
810
|
+
raw = readFileSync3(hooksJson, "utf8");
|
|
811
|
+
} catch {
|
|
812
|
+
raw = "";
|
|
813
|
+
}
|
|
814
|
+
if (raw.includes("{{extensionPath}}")) {
|
|
815
|
+
const replaced = raw.split("{{extensionPath}}").join(extDst);
|
|
816
|
+
writeAtomic(hooksJson, replaced);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const hookScriptsDir = join4(extSrc, "hooks");
|
|
820
|
+
for (const f of listFiles(hookScriptsDir, ".sh")) {
|
|
821
|
+
installHook(f.path, join4(extDst, "hooks", f.name), ctx.ts);
|
|
822
|
+
}
|
|
823
|
+
const skillsSrc = join4(extSrc, "skills");
|
|
824
|
+
for (const sd of listSubdirs(skillsSrc)) {
|
|
825
|
+
copyDirIfAbsent(sd.path, join4(extDst, "skills", sd.name));
|
|
826
|
+
}
|
|
827
|
+
const cmdSrc = join4(extSrc, "commands");
|
|
828
|
+
for (const f of listFiles(cmdSrc, ".toml")) {
|
|
829
|
+
copyIfAbsent(f.path, join4(extDst, "commands", f.name));
|
|
830
|
+
}
|
|
831
|
+
const agentSrc = join4(extSrc, "agents");
|
|
832
|
+
for (const f of listFiles(agentSrc, ".md")) {
|
|
833
|
+
copyIfAbsent(f.path, join4(extDst, "agents", f.name));
|
|
834
|
+
}
|
|
835
|
+
ctx.log.ok("Installed Gemini bundle: MCP + extension + 15 skills + 11 hooks + policy");
|
|
836
|
+
return { status: "ok" };
|
|
837
|
+
}
|
|
838
|
+
async function installWayland(ctx) {
|
|
839
|
+
if (ctx.ijfwCustomDir) {
|
|
840
|
+
return customDirNoop(
|
|
841
|
+
ctx,
|
|
842
|
+
"wayland",
|
|
843
|
+
"Wayland",
|
|
844
|
+
"Custom-dir install -- skipping ~/.wayland/ merges."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const dst = join4(ctx.home, ".wayland", "config.yaml");
|
|
848
|
+
ensureDir(dirname4(dst));
|
|
849
|
+
mergeYamlMcp(dst, ctx.serverJsNative);
|
|
850
|
+
ensureDir(join4(ctx.home, ".wayland"));
|
|
851
|
+
copyIfAbsent(
|
|
852
|
+
join4(ctx.repoRoot, "wayland", "WAYLAND.md"),
|
|
853
|
+
join4(ctx.home, ".wayland", "WAYLAND.md")
|
|
854
|
+
);
|
|
855
|
+
ensureDir(join4(ctx.home, ".wayland", "skills"));
|
|
856
|
+
const sharedSkills = join4(ctx.repoRoot, "shared", "skills");
|
|
857
|
+
for (const sd of listSubdirs(sharedSkills)) {
|
|
858
|
+
copyDirIfAbsent(sd.path, join4(ctx.home, ".wayland", "skills", sd.name));
|
|
859
|
+
}
|
|
860
|
+
const pluginSrc = join4(ctx.repoRoot, "wayland", "plugins", "ijfw");
|
|
861
|
+
if (existsSync4(pluginSrc)) {
|
|
862
|
+
const pluginDst = join4(ctx.home, ".wayland", "plugins", "ijfw");
|
|
863
|
+
ensureDir(pluginDst);
|
|
864
|
+
let entries;
|
|
865
|
+
try {
|
|
866
|
+
entries = readdirSync(pluginSrc);
|
|
867
|
+
} catch {
|
|
868
|
+
entries = [];
|
|
869
|
+
}
|
|
870
|
+
for (const name of entries) {
|
|
871
|
+
if (name === "__pycache__") continue;
|
|
872
|
+
const src = join4(pluginSrc, name);
|
|
873
|
+
const dstEntry = join4(pluginDst, name);
|
|
874
|
+
try {
|
|
875
|
+
const st = statSync2(src);
|
|
876
|
+
if (st.isDirectory()) {
|
|
877
|
+
cpSync(src, dstEntry, { recursive: true, force: true });
|
|
878
|
+
} else if (st.isFile()) {
|
|
879
|
+
copyFileSync2(src, dstEntry);
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
ctx.log.ok("Installed Wayland bundle: MCP + WAYLAND.md + skills + plugin");
|
|
886
|
+
return { status: "ok" };
|
|
887
|
+
}
|
|
888
|
+
async function installHermes(ctx) {
|
|
889
|
+
if (ctx.ijfwCustomDir) {
|
|
890
|
+
return customDirNoop(
|
|
891
|
+
ctx,
|
|
892
|
+
"hermes",
|
|
893
|
+
"Hermes",
|
|
894
|
+
"Custom-dir install -- skipping ~/.hermes/ merges."
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
const dst = join4(ctx.home, ".hermes", "config.yaml");
|
|
898
|
+
ensureDir(dirname4(dst));
|
|
899
|
+
mergeYamlMcp(dst, ctx.serverJsNative);
|
|
900
|
+
ensureDir(join4(ctx.home, ".hermes"));
|
|
901
|
+
copyIfAbsent(
|
|
902
|
+
join4(ctx.repoRoot, "hermes", "HERMES.md"),
|
|
903
|
+
join4(ctx.home, ".hermes", "HERMES.md")
|
|
904
|
+
);
|
|
905
|
+
ensureDir(join4(ctx.home, ".hermes", "skills"));
|
|
906
|
+
const sharedSkills = join4(ctx.repoRoot, "shared", "skills");
|
|
907
|
+
for (const sd of listSubdirs(sharedSkills)) {
|
|
908
|
+
copyDirIfAbsent(sd.path, join4(ctx.home, ".hermes", "skills", sd.name));
|
|
909
|
+
}
|
|
910
|
+
const pluginSrc = join4(ctx.repoRoot, "hermes", "plugins", "ijfw");
|
|
911
|
+
if (existsSync4(pluginSrc)) {
|
|
912
|
+
const pluginDst = join4(ctx.home, ".hermes", "plugins", "ijfw");
|
|
913
|
+
ensureDir(pluginDst);
|
|
914
|
+
let entries;
|
|
915
|
+
try {
|
|
916
|
+
entries = readdirSync(pluginSrc);
|
|
917
|
+
} catch {
|
|
918
|
+
entries = [];
|
|
919
|
+
}
|
|
920
|
+
for (const name of entries) {
|
|
921
|
+
if (name === "__pycache__") continue;
|
|
922
|
+
const src = join4(pluginSrc, name);
|
|
923
|
+
const dstEntry = join4(pluginDst, name);
|
|
924
|
+
try {
|
|
925
|
+
const st = statSync2(src);
|
|
926
|
+
if (st.isDirectory()) {
|
|
927
|
+
cpSync(src, dstEntry, { recursive: true, force: true });
|
|
928
|
+
} else if (st.isFile()) {
|
|
929
|
+
copyFileSync2(src, dstEntry);
|
|
930
|
+
}
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
mergeYamlPluginsEnabled(dst, "ijfw");
|
|
936
|
+
ctx.log.ok("Installed Hermes bundle: MCP + HERMES.md + skills + plugin");
|
|
937
|
+
return { status: "ok" };
|
|
938
|
+
}
|
|
939
|
+
async function installCursor(ctx) {
|
|
940
|
+
if (ctx.isIjfwSource) {
|
|
941
|
+
ctx.log.info("IJFW source tree detected -- skipping Cursor project writes (would litter source).");
|
|
942
|
+
ctx.log.ok("Cursor: source tree left untouched.");
|
|
943
|
+
return { status: "noop" };
|
|
944
|
+
}
|
|
945
|
+
if (ctx.ijfwCustomDir) {
|
|
946
|
+
return customDirNoop(
|
|
947
|
+
ctx,
|
|
948
|
+
"cursor",
|
|
949
|
+
"Cursor",
|
|
950
|
+
"Custom-dir install -- skipping Cursor project writes."
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const cwd = ctx.cwd || process.cwd();
|
|
954
|
+
const dst = join4(cwd, ".cursor", "mcp.json");
|
|
955
|
+
ensureDir(dirname4(dst));
|
|
956
|
+
mergeJson(dst, ctx.serverJsNative);
|
|
957
|
+
const rulesDir = join4(cwd, ".cursor", "rules");
|
|
958
|
+
ensureDir(rulesDir);
|
|
959
|
+
const ruleSrc = join4(ctx.repoRoot, "cursor", ".cursor", "rules", "ijfw.mdc");
|
|
960
|
+
if (existsSync4(ruleSrc)) {
|
|
961
|
+
try {
|
|
962
|
+
copyFileSync2(ruleSrc, join4(rulesDir, "ijfw.mdc"));
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
ctx.log.ok("Merged MCP + installed rule to project ./.cursor/");
|
|
967
|
+
return { status: "ok" };
|
|
968
|
+
}
|
|
969
|
+
async function installWindsurf(ctx) {
|
|
970
|
+
if (ctx.ijfwCustomDir || ctx.isIjfwSource) {
|
|
971
|
+
return customDirNoop(
|
|
972
|
+
ctx,
|
|
973
|
+
"windsurf",
|
|
974
|
+
"Windsurf",
|
|
975
|
+
"Skipping Windsurf platform writes (custom-dir or IJFW source tree)."
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
const dst = join4(ctx.home, ".codeium", "windsurf", "mcp_config.json");
|
|
979
|
+
ensureDir(dirname4(dst));
|
|
980
|
+
mergeJson(dst, ctx.serverJsNative);
|
|
981
|
+
const cwd = ctx.cwd || process.cwd();
|
|
982
|
+
const projectRules = join4(cwd, ".windsurfrules");
|
|
983
|
+
const repoRules = join4(ctx.repoRoot, "windsurf", ".windsurfrules");
|
|
984
|
+
let installedRules = false;
|
|
985
|
+
if (!existsSync4(projectRules) && existsSync4(repoRules)) {
|
|
986
|
+
try {
|
|
987
|
+
copyFileSync2(repoRules, projectRules);
|
|
988
|
+
installedRules = true;
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (installedRules) {
|
|
993
|
+
ctx.log.ok("Merged MCP + installed .windsurfrules");
|
|
994
|
+
} else {
|
|
995
|
+
ctx.log.ok(`Merged MCP into ${dst}`);
|
|
996
|
+
}
|
|
997
|
+
return { status: "ok" };
|
|
998
|
+
}
|
|
999
|
+
var init_install_targets_1_7 = __esm({
|
|
1000
|
+
"src/install-targets-1-7.js"() {
|
|
1001
|
+
init_install_helpers();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// src/install-targets-8-14.js
|
|
1006
|
+
import fs from "node:fs";
|
|
1007
|
+
import path from "node:path";
|
|
1008
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
1009
|
+
function ensureDir2(dir) {
|
|
1010
|
+
try {
|
|
1011
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function copyIfMissing(src, dst) {
|
|
1016
|
+
try {
|
|
1017
|
+
if (fs.existsSync(dst)) return false;
|
|
1018
|
+
if (!fs.existsSync(src)) return false;
|
|
1019
|
+
ensureDir2(path.dirname(dst));
|
|
1020
|
+
fs.copyFileSync(src, dst);
|
|
1021
|
+
return true;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function commandExists(name) {
|
|
1027
|
+
const probeCmd = process.platform === "win32" ? "where" : "which";
|
|
1028
|
+
const r = spawnSync(probeCmd, [name], { stdio: "ignore" });
|
|
1029
|
+
return r.status === 0;
|
|
1030
|
+
}
|
|
1031
|
+
function installCopilot(ctx) {
|
|
1032
|
+
if (ctx.ijfwCustomDir) {
|
|
1033
|
+
printInfo("Custom-dir install -- skipping Copilot project writes.");
|
|
1034
|
+
printOk("Copilot: real platform config left untouched.");
|
|
1035
|
+
return { status: "noop" };
|
|
1036
|
+
}
|
|
1037
|
+
if (ctx.isIjfwSource) {
|
|
1038
|
+
printInfo("IJFW source tree detected -- skipping Copilot project writes (would litter source).");
|
|
1039
|
+
printOk("Copilot: source tree left untouched.");
|
|
1040
|
+
return { status: "noop" };
|
|
1041
|
+
}
|
|
1042
|
+
const dst = path.join(process.cwd(), ".vscode", "mcp.json");
|
|
1043
|
+
ensureDir2(path.dirname(dst));
|
|
1044
|
+
mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1045
|
+
const rulesDst = path.join(process.cwd(), ".github", "copilot-instructions.md");
|
|
1046
|
+
const rulesSrc = path.join(ctx.repoRoot, "copilot", "copilot-instructions.md");
|
|
1047
|
+
const wroteRules = copyIfMissing(rulesSrc, rulesDst);
|
|
1048
|
+
if (wroteRules) {
|
|
1049
|
+
printOk("Merged MCP + installed .github/copilot-instructions.md");
|
|
1050
|
+
} else {
|
|
1051
|
+
printOk("Merged MCP into project ./.vscode/mcp.json");
|
|
1052
|
+
}
|
|
1053
|
+
return { status: "ok" };
|
|
1054
|
+
}
|
|
1055
|
+
function installOpencode(ctx) {
|
|
1056
|
+
if (ctx.ijfwCustomDir) {
|
|
1057
|
+
printInfo("Custom-dir install -- skipping ~/.config/opencode/ merge.");
|
|
1058
|
+
printOk("OpenCode: real platform config left untouched.");
|
|
1059
|
+
return { status: "noop" };
|
|
1060
|
+
}
|
|
1061
|
+
const dst = path.join(ctx.home, ".config", "opencode", "opencode.json");
|
|
1062
|
+
ensureDir2(path.dirname(dst));
|
|
1063
|
+
opencodeMerge(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1064
|
+
printOk(`Merged MCP into ${dst} (opencode mcp.local schema)`);
|
|
1065
|
+
return { status: "ok" };
|
|
1066
|
+
}
|
|
1067
|
+
function installQwen(ctx) {
|
|
1068
|
+
if (ctx.ijfwCustomDir) {
|
|
1069
|
+
printInfo("Custom-dir install -- skipping ~/.qwen/ merge.");
|
|
1070
|
+
printOk("Qwen Code: real platform config left untouched.");
|
|
1071
|
+
return { status: "noop" };
|
|
1072
|
+
}
|
|
1073
|
+
const dst = path.join(ctx.home, ".qwen", "settings.json");
|
|
1074
|
+
ensureDir2(path.dirname(dst));
|
|
1075
|
+
mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1076
|
+
printOk(`Merged MCP into ${dst}`);
|
|
1077
|
+
return { status: "ok" };
|
|
1078
|
+
}
|
|
1079
|
+
function installCline(ctx) {
|
|
1080
|
+
if (ctx.ijfwCustomDir) {
|
|
1081
|
+
printInfo("Custom-dir install -- skipping Cline merges.");
|
|
1082
|
+
printOk("Cline: real platform config left untouched.");
|
|
1083
|
+
return { status: "noop" };
|
|
1084
|
+
}
|
|
1085
|
+
const dst = clineMerge(ctx.serverJsNative || ctx.serverJs, ctx.home);
|
|
1086
|
+
printOk(`Merged MCP into ${dst} (cline globalStorage schema)`);
|
|
1087
|
+
return { status: "ok" };
|
|
1088
|
+
}
|
|
1089
|
+
function installKimi(ctx) {
|
|
1090
|
+
if (ctx.ijfwCustomDir) {
|
|
1091
|
+
printInfo("Custom-dir install -- skipping ~/.kimi/ merge.");
|
|
1092
|
+
printOk("Kimi Code: real platform config left untouched.");
|
|
1093
|
+
return { status: "noop" };
|
|
1094
|
+
}
|
|
1095
|
+
const dst = path.join(ctx.home, ".kimi", "mcp.json");
|
|
1096
|
+
ensureDir2(path.dirname(dst));
|
|
1097
|
+
mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1098
|
+
printOk(`Merged MCP into ${dst}`);
|
|
1099
|
+
return { status: "ok" };
|
|
1100
|
+
}
|
|
1101
|
+
function installOpenclaw(ctx) {
|
|
1102
|
+
if (ctx.ijfwCustomDir) {
|
|
1103
|
+
printInfo("Custom-dir install -- skipping OpenClaw merges.");
|
|
1104
|
+
printOk("OpenClaw: real platform config left untouched.");
|
|
1105
|
+
return { status: "noop" };
|
|
1106
|
+
}
|
|
1107
|
+
const dst = path.join(ctx.home, ".openclaw", "openclaw.json");
|
|
1108
|
+
const serverJs = ctx.serverJsNative || ctx.serverJs;
|
|
1109
|
+
if (commandExists("openclaw")) {
|
|
1110
|
+
try {
|
|
1111
|
+
const payload = JSON.stringify({ command: "node", args: [serverJs] });
|
|
1112
|
+
execFileSync("openclaw", ["mcp", "set", "ijfw-memory", payload], { stdio: "ignore" });
|
|
1113
|
+
printOk(`Registered ijfw-memory via 'openclaw mcp set' (${dst})`);
|
|
1114
|
+
return { status: "ok" };
|
|
1115
|
+
} catch {
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
ensureDir2(path.dirname(dst));
|
|
1119
|
+
openclawMerge(dst, serverJs);
|
|
1120
|
+
printOk(`Merged MCP into ${dst} (openclaw mcp.servers schema)`);
|
|
1121
|
+
return { status: "ok" };
|
|
1122
|
+
}
|
|
1123
|
+
function installAider(ctx) {
|
|
1124
|
+
if (ctx.ijfwCustomDir) {
|
|
1125
|
+
printInfo("Custom-dir install -- skipping Aider merges.");
|
|
1126
|
+
printOk("Aider: real platform config left untouched.");
|
|
1127
|
+
return { status: "noop" };
|
|
1128
|
+
}
|
|
1129
|
+
const confSrc = path.join(ctx.repoRoot, "aider", "aider.conf.yml");
|
|
1130
|
+
const confDst = path.join(ctx.home, ".aider.conf.yml");
|
|
1131
|
+
copyIfMissing(confSrc, confDst);
|
|
1132
|
+
const convSrc = path.join(ctx.repoRoot, "aider", "CONVENTIONS.md");
|
|
1133
|
+
const convDst = path.join(ctx.home, "CONVENTIONS.md");
|
|
1134
|
+
copyIfMissing(convSrc, convDst);
|
|
1135
|
+
printOk("Aider: rules-only install (~/.aider.conf.yml + ~/CONVENTIONS.md). No MCP -- Aider lacks a native MCP client.");
|
|
1136
|
+
return { status: "ok" };
|
|
1137
|
+
}
|
|
1138
|
+
var init_install_targets_8_14 = __esm({
|
|
1139
|
+
"src/install-targets-8-14.js"() {
|
|
1140
|
+
init_install_helpers();
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// src/install-flow.js
|
|
1145
|
+
var install_flow_exports = {};
|
|
1146
|
+
__export(install_flow_exports, {
|
|
1147
|
+
CANONICAL_ORDER: () => CANONICAL_ORDER,
|
|
1148
|
+
default: () => install_flow_default,
|
|
1149
|
+
runInstall: () => runInstall
|
|
1150
|
+
});
|
|
1151
|
+
import fs2 from "node:fs";
|
|
1152
|
+
import path2 from "node:path";
|
|
1153
|
+
function timestamp() {
|
|
1154
|
+
const d = /* @__PURE__ */ new Date();
|
|
1155
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
1156
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
1157
|
+
}
|
|
1158
|
+
function isWindows() {
|
|
1159
|
+
return process.platform === "win32";
|
|
1160
|
+
}
|
|
1161
|
+
function realpathOrSelf(p) {
|
|
1162
|
+
try {
|
|
1163
|
+
return fs2.realpathSync(p);
|
|
1164
|
+
} catch {
|
|
1165
|
+
return p;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function preflight({ repoRoot, serverJs }) {
|
|
1169
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1170
|
+
if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`Preflight: Node.js ${process.versions.node} is too old (need 18+).`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
1176
|
+
throw new Error("Preflight: repoRoot is required and must be a string.");
|
|
1177
|
+
}
|
|
1178
|
+
if (!fs2.existsSync(repoRoot)) {
|
|
1179
|
+
throw new Error(`Preflight: repoRoot does not exist at ${repoRoot}`);
|
|
1180
|
+
}
|
|
1181
|
+
if (!fs2.existsSync(serverJs)) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`Preflight: MCP server.js missing at ${serverJs} -- re-clone the IJFW source tree.`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function linkPlugin({ repoRoot, ijfwHome, ts }) {
|
|
1188
|
+
const pluginSrc = path2.join(repoRoot, "claude");
|
|
1189
|
+
const pluginDst = path2.join(ijfwHome, "claude");
|
|
1190
|
+
const pluginSrcReal = realpathOrSelf(pluginSrc);
|
|
1191
|
+
const pluginDstReal = realpathOrSelf(pluginDst);
|
|
1192
|
+
if (pluginSrcReal === pluginDstReal && fs2.existsSync(pluginDst)) {
|
|
1193
|
+
printOk("Plugin source already at canonical path -- symlink not needed.");
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
fs2.mkdirSync(ijfwHome, { recursive: true });
|
|
1197
|
+
if (isWindows()) {
|
|
1198
|
+
if (fs2.existsSync(pluginDst)) {
|
|
1199
|
+
const st = fs2.lstatSync(pluginDst);
|
|
1200
|
+
if (st.isSymbolicLink()) {
|
|
1201
|
+
try {
|
|
1202
|
+
fs2.unlinkSync(pluginDst);
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
} else if (st.isDirectory()) {
|
|
1206
|
+
fs2.cpSync(pluginSrc, pluginDst, { recursive: true });
|
|
1207
|
+
printOk(`Plugin tree mirrored to ${pluginDst}`);
|
|
1208
|
+
return;
|
|
1209
|
+
} else {
|
|
1210
|
+
try {
|
|
1211
|
+
fs2.rmSync(pluginDst, { recursive: true, force: true });
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
fs2.cpSync(pluginSrc, pluginDst, { recursive: true });
|
|
1217
|
+
printOk(`Plugin tree copied to ${pluginDst}`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
let dstStat = null;
|
|
1221
|
+
try {
|
|
1222
|
+
dstStat = fs2.lstatSync(pluginDst);
|
|
1223
|
+
} catch {
|
|
1224
|
+
dstStat = null;
|
|
1225
|
+
}
|
|
1226
|
+
if (dstStat && dstStat.isSymbolicLink()) {
|
|
1227
|
+
let cur = "";
|
|
1228
|
+
try {
|
|
1229
|
+
cur = fs2.readlinkSync(pluginDst);
|
|
1230
|
+
} catch {
|
|
1231
|
+
cur = "";
|
|
1232
|
+
}
|
|
1233
|
+
if (cur !== pluginSrc) {
|
|
1234
|
+
try {
|
|
1235
|
+
fs2.unlinkSync(pluginDst);
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
fs2.symlinkSync(pluginSrc, pluginDst, "dir");
|
|
1239
|
+
printOk(`Plugin link retargeted: ${pluginDst} -> ${pluginSrc}`);
|
|
1240
|
+
} else {
|
|
1241
|
+
printOk(`Plugin link already correct at ${pluginDst}`);
|
|
1242
|
+
}
|
|
1243
|
+
} else if (dstStat) {
|
|
1244
|
+
const backup2 = `${pluginDst}.backup.${ts}`;
|
|
1245
|
+
fs2.renameSync(pluginDst, backup2);
|
|
1246
|
+
fs2.symlinkSync(pluginSrc, pluginDst, "dir");
|
|
1247
|
+
printOk(`Plugin link created (existing dir preserved at ${backup2}).`);
|
|
1248
|
+
} else {
|
|
1249
|
+
fs2.symlinkSync(pluginSrc, pluginDst, "dir");
|
|
1250
|
+
printOk(`Plugin link created: ${pluginDst} -> ${pluginSrc}`);
|
|
1251
|
+
}
|
|
1252
|
+
const manifest = path2.join(pluginDst, ".claude-plugin", "plugin.json");
|
|
1253
|
+
if (!fs2.existsSync(manifest)) {
|
|
1254
|
+
printWarn(
|
|
1255
|
+
`Plugin at ${pluginDst} is missing .claude-plugin/plugin.json -- install may be incomplete.`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function seedState({ ijfwHome, repoRoot, nodeBin: _nodeBin }) {
|
|
1260
|
+
const cacheDir = path2.join(ijfwHome, "cache");
|
|
1261
|
+
const runDir = path2.join(ijfwHome, "run");
|
|
1262
|
+
const logsDir = path2.join(ijfwHome, "logs");
|
|
1263
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
1264
|
+
fs2.mkdirSync(runDir, { recursive: true });
|
|
1265
|
+
fs2.mkdirSync(logsDir, { recursive: true });
|
|
1266
|
+
try {
|
|
1267
|
+
fs2.chmodSync(ijfwHome, 448);
|
|
1268
|
+
} catch {
|
|
1269
|
+
}
|
|
1270
|
+
let installMethod = "manual";
|
|
1271
|
+
if (fs2.existsSync(path2.join(repoRoot, ".git"))) {
|
|
1272
|
+
installMethod = "git-clone";
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
const npmGlobalRoot = process.env.npm_config_prefix ? path2.join(process.env.npm_config_prefix, "lib", "node_modules") : null;
|
|
1276
|
+
if (npmGlobalRoot && repoRoot.startsWith(npmGlobalRoot) && repoRoot !== npmGlobalRoot) {
|
|
1277
|
+
installMethod = "npm-global";
|
|
1278
|
+
}
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
let installedVer = "0.0.0";
|
|
1282
|
+
try {
|
|
1283
|
+
const pkgPath = path2.join(repoRoot, "installer", "package.json");
|
|
1284
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
1285
|
+
installedVer = pkg.version || "0.0.0";
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
const nowTs = Math.floor(Date.now() / 1e3);
|
|
1289
|
+
const state = {
|
|
1290
|
+
schema_version: 1,
|
|
1291
|
+
install_method: installMethod,
|
|
1292
|
+
installed_version: installedVer,
|
|
1293
|
+
last_applied_version: installedVer,
|
|
1294
|
+
last_good_shasum: null,
|
|
1295
|
+
settings_reseeded_at: null,
|
|
1296
|
+
installed_at: nowTs
|
|
1297
|
+
};
|
|
1298
|
+
writeAtomic(
|
|
1299
|
+
path2.join(ijfwHome, "state.json"),
|
|
1300
|
+
JSON.stringify(state, null, 2) + "\n",
|
|
1301
|
+
{ mode: 384 }
|
|
1302
|
+
);
|
|
1303
|
+
const settingsPath = path2.join(ijfwHome, "settings.json");
|
|
1304
|
+
if (!fs2.existsSync(settingsPath)) {
|
|
1305
|
+
const seedPath = path2.join(
|
|
1306
|
+
repoRoot,
|
|
1307
|
+
"installer",
|
|
1308
|
+
"src",
|
|
1309
|
+
"settings-seed.json"
|
|
1310
|
+
);
|
|
1311
|
+
if (fs2.existsSync(seedPath)) {
|
|
1312
|
+
const seed = fs2.readFileSync(seedPath, "utf8");
|
|
1313
|
+
writeAtomic(settingsPath, seed, { mode: 384 });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
writeAtomic(
|
|
1317
|
+
path2.join(ijfwHome, "install-method"),
|
|
1318
|
+
`${installMethod}
|
|
1319
|
+
`,
|
|
1320
|
+
{ mode: 384 }
|
|
1321
|
+
);
|
|
1322
|
+
printOk(`State seeded (${installMethod}, v${installedVer})`);
|
|
1323
|
+
return { installMethod, installedVer };
|
|
1324
|
+
}
|
|
1325
|
+
function detectStatusline({ home, ijfwHome }) {
|
|
1326
|
+
const claudeSettingsPath2 = path2.join(home, ".claude", "settings.json");
|
|
1327
|
+
if (!fs2.existsSync(claudeSettingsPath2)) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
let claudeSettings;
|
|
1331
|
+
try {
|
|
1332
|
+
claudeSettings = JSON.parse(fs2.readFileSync(claudeSettingsPath2, "utf8"));
|
|
1333
|
+
} catch {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const existingCmd = claudeSettings && claudeSettings.statusLine && claudeSettings.statusLine.command ? String(claudeSettings.statusLine.command) : "";
|
|
1337
|
+
if (!existingCmd) {
|
|
1338
|
+
printOk(
|
|
1339
|
+
"statusLine off by default. Run 'ijfw statusline --install' to enable."
|
|
1340
|
+
);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const allowed = existingCmd.includes("/.claude/") || existingCmd.includes("/.gsd/") || existingCmd.includes("/.ijfw/claude/") || existingCmd.includes("/.cursor/");
|
|
1344
|
+
if (!allowed) {
|
|
1345
|
+
printWarn(
|
|
1346
|
+
`Existing statusLine at ${existingCmd} -- not composing for security.`
|
|
1347
|
+
);
|
|
1348
|
+
printNote(
|
|
1349
|
+
"Run 'ijfw statusline --install' to replace, or '--compose' if trusted."
|
|
1350
|
+
);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const settingsPath = path2.join(ijfwHome, "settings.json");
|
|
1354
|
+
let s = {};
|
|
1355
|
+
try {
|
|
1356
|
+
s = JSON.parse(fs2.readFileSync(settingsPath, "utf8"));
|
|
1357
|
+
} catch {
|
|
1358
|
+
s = {};
|
|
1359
|
+
}
|
|
1360
|
+
if (!s.schema_version) s.schema_version = 1;
|
|
1361
|
+
if (!s.statusline) s.statusline = {};
|
|
1362
|
+
s.statusline.composed_command = existingCmd;
|
|
1363
|
+
s.statusline.mode = "compose";
|
|
1364
|
+
s.statusline.enabled = "auto";
|
|
1365
|
+
try {
|
|
1366
|
+
writeAtomic(settingsPath, JSON.stringify(s, null, 2) + "\n", {
|
|
1367
|
+
mode: 384
|
|
1368
|
+
});
|
|
1369
|
+
printOk(
|
|
1370
|
+
"Composed alongside existing statusLine. Run 'ijfw statusline --disable' to opt out."
|
|
1371
|
+
);
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
printWarn(`Statusline compose write failed: ${err.message}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function patchPluginMcpJson({ ijfwHome, repoRoot, nodeBin, serverJs }) {
|
|
1377
|
+
const pluginDst = path2.join(ijfwHome, "claude");
|
|
1378
|
+
const mcpJsonPath = path2.join(pluginDst, ".mcp.json");
|
|
1379
|
+
if (!fs2.existsSync(mcpJsonPath)) return;
|
|
1380
|
+
const pluginDstReal = realpathOrSelf(pluginDst);
|
|
1381
|
+
const repoRootReal = realpathOrSelf(repoRoot);
|
|
1382
|
+
if (fs2.existsSync(path2.join(repoRoot, ".git")) && pluginDstReal.startsWith(repoRootReal) && pluginDstReal !== repoRootReal) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
let d;
|
|
1386
|
+
try {
|
|
1387
|
+
d = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf8"));
|
|
1388
|
+
} catch {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (!d || !d.mcpServers || !d.mcpServers["ijfw-memory"]) return;
|
|
1392
|
+
const nodeDir = path2.dirname(nodeBin);
|
|
1393
|
+
d.mcpServers["ijfw-memory"].command = nodeBin;
|
|
1394
|
+
d.mcpServers["ijfw-memory"].args = [serverJs];
|
|
1395
|
+
const envSep = process.platform === "win32" ? ";" : ":";
|
|
1396
|
+
const commonPaths = process.platform === "win32" ? [nodeDir, "C:\\Windows\\System32"] : [nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
1397
|
+
const dedup = [...new Set(commonPaths.filter((x) => x && fs2.existsSync(x)))];
|
|
1398
|
+
d.mcpServers["ijfw-memory"].env = { PATH: dedup.join(envSep) };
|
|
1399
|
+
try {
|
|
1400
|
+
writeAtomic(mcpJsonPath, JSON.stringify(d, null, 2) + "\n");
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
printWarn(`Plugin .mcp.json patch failed: ${err.message}`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function nukePluginCache({ home }) {
|
|
1406
|
+
const cacheDir = path2.join(home, ".claude", "plugins", "cache", "ijfw");
|
|
1407
|
+
if (fs2.existsSync(cacheDir)) {
|
|
1408
|
+
try {
|
|
1409
|
+
fs2.rmSync(cacheDir, { recursive: true, force: true });
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function linkMcpSibling({ repoRoot, ijfwHome, ts }) {
|
|
1415
|
+
const mcpSrc = path2.join(repoRoot, "mcp-server");
|
|
1416
|
+
const mcpDst = path2.join(ijfwHome, "mcp-server");
|
|
1417
|
+
const mcpSrcReal = realpathOrSelf(mcpSrc);
|
|
1418
|
+
const mcpDstReal = realpathOrSelf(mcpDst);
|
|
1419
|
+
if (mcpSrcReal === mcpDstReal && fs2.existsSync(mcpDst)) {
|
|
1420
|
+
printOk("MCP source already at canonical path -- symlink not needed.");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
fs2.mkdirSync(ijfwHome, { recursive: true });
|
|
1424
|
+
if (isWindows()) {
|
|
1425
|
+
if (fs2.existsSync(mcpDst)) {
|
|
1426
|
+
const st = fs2.lstatSync(mcpDst);
|
|
1427
|
+
if (st.isSymbolicLink()) {
|
|
1428
|
+
try {
|
|
1429
|
+
fs2.unlinkSync(mcpDst);
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
} else if (st.isDirectory()) {
|
|
1433
|
+
fs2.cpSync(mcpSrc, mcpDst, { recursive: true });
|
|
1434
|
+
return;
|
|
1435
|
+
} else {
|
|
1436
|
+
try {
|
|
1437
|
+
fs2.rmSync(mcpDst, { recursive: true, force: true });
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
fs2.cpSync(mcpSrc, mcpDst, { recursive: true });
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
let dstStat = null;
|
|
1446
|
+
try {
|
|
1447
|
+
dstStat = fs2.lstatSync(mcpDst);
|
|
1448
|
+
} catch {
|
|
1449
|
+
dstStat = null;
|
|
1450
|
+
}
|
|
1451
|
+
if (dstStat && dstStat.isSymbolicLink()) {
|
|
1452
|
+
let cur = "";
|
|
1453
|
+
try {
|
|
1454
|
+
cur = fs2.readlinkSync(mcpDst);
|
|
1455
|
+
} catch {
|
|
1456
|
+
cur = "";
|
|
1457
|
+
}
|
|
1458
|
+
if (cur !== mcpSrc) {
|
|
1459
|
+
try {
|
|
1460
|
+
fs2.unlinkSync(mcpDst);
|
|
1461
|
+
} catch {
|
|
1462
|
+
}
|
|
1463
|
+
fs2.symlinkSync(mcpSrc, mcpDst, "dir");
|
|
1464
|
+
}
|
|
1465
|
+
} else if (dstStat) {
|
|
1466
|
+
const backup2 = `${mcpDst}.backup.${ts}`;
|
|
1467
|
+
fs2.renameSync(mcpDst, backup2);
|
|
1468
|
+
fs2.symlinkSync(mcpSrc, mcpDst, "dir");
|
|
1469
|
+
} else {
|
|
1470
|
+
fs2.symlinkSync(mcpSrc, mcpDst, "dir");
|
|
1471
|
+
}
|
|
1472
|
+
if (!fs2.existsSync(path2.join(mcpDst, "src", "server.js"))) {
|
|
1473
|
+
printWarn(
|
|
1474
|
+
`MCP server at ${mcpDst} is missing src/server.js -- install may be incomplete.`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function pruneBackups({ home }) {
|
|
1479
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1480
|
+
const cwd = process.cwd();
|
|
1481
|
+
const dirs = [
|
|
1482
|
+
path2.join(home, ".codex"),
|
|
1483
|
+
path2.join(home, ".gemini"),
|
|
1484
|
+
path2.join(home, ".codeium", "windsurf"),
|
|
1485
|
+
path2.join(home, ".hermes"),
|
|
1486
|
+
path2.join(home, ".wayland"),
|
|
1487
|
+
path2.join(cwd, ".vscode"),
|
|
1488
|
+
path2.join(cwd, ".cursor")
|
|
1489
|
+
];
|
|
1490
|
+
const cutoff = Date.now() - THIRTY_DAYS_MS;
|
|
1491
|
+
for (const d of dirs) {
|
|
1492
|
+
if (!fs2.existsSync(d)) continue;
|
|
1493
|
+
walkAndPrune(d, 2, cutoff);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function walkAndPrune(dir, depth, cutoff) {
|
|
1497
|
+
if (depth < 0) return;
|
|
1498
|
+
let entries = [];
|
|
1499
|
+
try {
|
|
1500
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1501
|
+
} catch {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
for (const entry of entries) {
|
|
1505
|
+
const full = path2.join(dir, entry.name);
|
|
1506
|
+
if (entry.isDirectory()) {
|
|
1507
|
+
walkAndPrune(full, depth - 1, cutoff);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (!entry.isFile()) continue;
|
|
1511
|
+
if (!entry.name.includes(".bak.")) continue;
|
|
1512
|
+
let st;
|
|
1513
|
+
try {
|
|
1514
|
+
st = fs2.statSync(full);
|
|
1515
|
+
} catch {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (st.mtimeMs < cutoff) {
|
|
1519
|
+
try {
|
|
1520
|
+
fs2.unlinkSync(full);
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function printSummary({ live, standby, claudeNeedsRestart, repoRoot }) {
|
|
1527
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1528
|
+
const c = useColor ? {
|
|
1529
|
+
reset: "\x1B[0m",
|
|
1530
|
+
bold: "\x1B[1m",
|
|
1531
|
+
dim: "\x1B[2m",
|
|
1532
|
+
cyan: "\x1B[36m",
|
|
1533
|
+
green: "\x1B[32m",
|
|
1534
|
+
yellow: "\x1B[33m"
|
|
1535
|
+
} : { reset: "", bold: "", dim: "", cyan: "", green: "", yellow: "" };
|
|
1536
|
+
const nativeRepo = nativePath(repoRoot);
|
|
1537
|
+
process.stdout.write("\n");
|
|
1538
|
+
process.stdout.write(
|
|
1539
|
+
` ${c.bold}${c.cyan}+----------------------------------------+${c.reset}
|
|
1540
|
+
`
|
|
1541
|
+
);
|
|
1542
|
+
process.stdout.write(
|
|
1543
|
+
` ${c.bold}${c.cyan}|${c.reset} ${c.bold}${c.cyan}|${c.reset}
|
|
1544
|
+
`
|
|
1545
|
+
);
|
|
1546
|
+
process.stdout.write(
|
|
1547
|
+
` ${c.bold}${c.cyan}|${c.reset} ${c.bold}${c.cyan}IJFW${c.reset} ${c.dim}It just f*cking works.${c.reset} ${c.bold}${c.cyan}|${c.reset}
|
|
1548
|
+
`
|
|
1549
|
+
);
|
|
1550
|
+
process.stdout.write(
|
|
1551
|
+
` ${c.bold}${c.cyan}|${c.reset} ${c.bold}${c.cyan}|${c.reset}
|
|
1552
|
+
`
|
|
1553
|
+
);
|
|
1554
|
+
process.stdout.write(
|
|
1555
|
+
` ${c.bold}${c.cyan}+----------------------------------------+${c.reset}
|
|
1556
|
+
`
|
|
1557
|
+
);
|
|
1558
|
+
process.stdout.write("\n");
|
|
1559
|
+
process.stdout.write(
|
|
1560
|
+
` ${c.dim}Installed at${c.reset} ${nativeRepo}
|
|
1561
|
+
|
|
1562
|
+
`
|
|
1563
|
+
);
|
|
1564
|
+
if (live.length) {
|
|
1565
|
+
process.stdout.write(
|
|
1566
|
+
` ${c.bold}${c.green}==> LIVE NOW (${live.length})${c.reset}
|
|
1567
|
+
`
|
|
1568
|
+
);
|
|
1569
|
+
for (const p of live) {
|
|
1570
|
+
process.stdout.write(` ${c.green}o${c.reset} ${p}
|
|
1571
|
+
`);
|
|
1572
|
+
}
|
|
1573
|
+
process.stdout.write("\n");
|
|
1574
|
+
}
|
|
1575
|
+
if (standby.length) {
|
|
1576
|
+
process.stdout.write(
|
|
1577
|
+
` ${c.bold}${c.yellow}==> STANDING BY (${standby.length})${c.reset} ${c.dim}auto-activate on install${c.reset}
|
|
1578
|
+
`
|
|
1579
|
+
);
|
|
1580
|
+
for (const p of standby) {
|
|
1581
|
+
process.stdout.write(` ${c.yellow}o${c.reset} ${p}
|
|
1582
|
+
`);
|
|
1583
|
+
}
|
|
1584
|
+
process.stdout.write("\n");
|
|
1585
|
+
}
|
|
1586
|
+
if (live.length === 0 && standby.length === 0) {
|
|
1587
|
+
process.stdout.write(
|
|
1588
|
+
` ${c.yellow}Ready to configure${c.reset} -- pass a platform name to get started: ${c.bold}ijfw install claude${c.reset}
|
|
1589
|
+
|
|
1590
|
+
`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (claudeNeedsRestart) {
|
|
1594
|
+
process.stdout.write(
|
|
1595
|
+
` ${c.bold}${c.yellow}==> RESTART REQUIRED${c.reset} Claude Code is running -- ${c.bold}restart your sessions now to activate IJFW.${c.reset}
|
|
1596
|
+
|
|
1597
|
+
`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function maybeInstallPostCommitHook({ noninteractive }) {
|
|
1602
|
+
if (process.env.CI || noninteractive) return;
|
|
1603
|
+
if (!fs2.existsSync(".git")) return;
|
|
1604
|
+
printNote(
|
|
1605
|
+
"Tip: background Trident critique on every commit -- pass --post-commit-hook to enable."
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
async function runInstall({
|
|
1609
|
+
targets,
|
|
1610
|
+
ijfwHome,
|
|
1611
|
+
ijfwCustomDir,
|
|
1612
|
+
repoRoot,
|
|
1613
|
+
noninteractive
|
|
1614
|
+
} = {}) {
|
|
1615
|
+
const home = homeReal();
|
|
1616
|
+
const resolvedIjfwHome = ijfwHome && typeof ijfwHome === "string" && ijfwHome.length > 0 ? path2.resolve(ijfwHome) : path2.join(home, ".ijfw");
|
|
1617
|
+
const customDir = !!ijfwCustomDir;
|
|
1618
|
+
const resolvedRepoRoot = repoRoot && typeof repoRoot === "string" ? path2.resolve(repoRoot) : process.cwd();
|
|
1619
|
+
let targetList = Array.isArray(targets) && targets.length > 0 ? targets.filter((t) => CANONICAL_ORDER.includes(t)) : CANONICAL_ORDER.slice();
|
|
1620
|
+
targetList = CANONICAL_ORDER.filter((t) => targetList.includes(t));
|
|
1621
|
+
const serverJs = path2.join(resolvedRepoRoot, "mcp-server", "src", "server.js");
|
|
1622
|
+
const serverJsNative = nativePath(serverJs);
|
|
1623
|
+
const nodeBin = process.execPath;
|
|
1624
|
+
const ts = timestamp();
|
|
1625
|
+
preflight({ repoRoot: resolvedRepoRoot, serverJs });
|
|
1626
|
+
if (customDir) {
|
|
1627
|
+
printOk(
|
|
1628
|
+
"Custom-dir install -- skipping ~/.ijfw/ sibling links (canonical-dir feature)."
|
|
1629
|
+
);
|
|
1630
|
+
} else {
|
|
1631
|
+
linkPlugin({ repoRoot: resolvedRepoRoot, ijfwHome: resolvedIjfwHome, ts });
|
|
1632
|
+
}
|
|
1633
|
+
seedState({
|
|
1634
|
+
ijfwHome: resolvedIjfwHome,
|
|
1635
|
+
repoRoot: resolvedRepoRoot,
|
|
1636
|
+
nodeBin
|
|
1637
|
+
});
|
|
1638
|
+
if (!customDir) {
|
|
1639
|
+
detectStatusline({ home, ijfwHome: resolvedIjfwHome });
|
|
1640
|
+
}
|
|
1641
|
+
if (!customDir) {
|
|
1642
|
+
patchPluginMcpJson({
|
|
1643
|
+
ijfwHome: resolvedIjfwHome,
|
|
1644
|
+
repoRoot: resolvedRepoRoot,
|
|
1645
|
+
nodeBin,
|
|
1646
|
+
serverJs
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
if (!customDir) {
|
|
1650
|
+
nukePluginCache({ home });
|
|
1651
|
+
}
|
|
1652
|
+
if (!customDir) {
|
|
1653
|
+
linkMcpSibling({
|
|
1654
|
+
repoRoot: resolvedRepoRoot,
|
|
1655
|
+
ijfwHome: resolvedIjfwHome,
|
|
1656
|
+
ts
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
pruneBackups({ home });
|
|
1660
|
+
const live = [];
|
|
1661
|
+
const standby = [];
|
|
1662
|
+
const failed = [];
|
|
1663
|
+
let claudeNeedsRestart = false;
|
|
1664
|
+
for (const target of targetList) {
|
|
1665
|
+
const fn = TARGET_FNS[target];
|
|
1666
|
+
if (typeof fn !== "function") {
|
|
1667
|
+
printWarn(`No installer registered for target '${target}' -- skipped.`);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
printSection(`[${prettyName(target)}]`);
|
|
1671
|
+
const ctx = {
|
|
1672
|
+
home,
|
|
1673
|
+
homeReal: home,
|
|
1674
|
+
ijfwHome: resolvedIjfwHome,
|
|
1675
|
+
ijfwCustomDir: customDir,
|
|
1676
|
+
repoRoot: resolvedRepoRoot,
|
|
1677
|
+
serverJs,
|
|
1678
|
+
serverJsNative,
|
|
1679
|
+
nodeBin,
|
|
1680
|
+
ts,
|
|
1681
|
+
log: {
|
|
1682
|
+
ok: printOk,
|
|
1683
|
+
note: printNote,
|
|
1684
|
+
info: printInfo,
|
|
1685
|
+
warn: printWarn
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
let outcome;
|
|
1689
|
+
try {
|
|
1690
|
+
outcome = await fn(ctx);
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
failed.push(prettyName(target));
|
|
1693
|
+
printWarn(
|
|
1694
|
+
`${prettyName(target)}: install failed -- ${err && err.message ? err.message : String(err)}`
|
|
1695
|
+
);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (outcome && outcome.restart === true && target === "claude") {
|
|
1699
|
+
claudeNeedsRestart = true;
|
|
1700
|
+
}
|
|
1701
|
+
let liveNow = false;
|
|
1702
|
+
try {
|
|
1703
|
+
liveNow = !!isLive(target, home);
|
|
1704
|
+
} catch {
|
|
1705
|
+
liveNow = false;
|
|
1706
|
+
}
|
|
1707
|
+
const display = prettyName(target);
|
|
1708
|
+
if (liveNow) {
|
|
1709
|
+
live.push(display);
|
|
1710
|
+
} else {
|
|
1711
|
+
standby.push(display);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
printSummary({
|
|
1715
|
+
live,
|
|
1716
|
+
standby,
|
|
1717
|
+
claudeNeedsRestart,
|
|
1718
|
+
repoRoot: resolvedRepoRoot
|
|
1719
|
+
});
|
|
1720
|
+
maybeInstallPostCommitHook({ noninteractive: !!noninteractive });
|
|
1721
|
+
return { live, standby, failed, claudeNeedsRestart };
|
|
1722
|
+
}
|
|
1723
|
+
var CANONICAL_ORDER, TARGET_FNS, install_flow_default;
|
|
1724
|
+
var init_install_flow = __esm({
|
|
1725
|
+
"src/install-flow.js"() {
|
|
1726
|
+
init_install_helpers();
|
|
1727
|
+
init_install_targets_1_7();
|
|
1728
|
+
init_install_targets_8_14();
|
|
1729
|
+
CANONICAL_ORDER = [
|
|
1730
|
+
"claude",
|
|
1731
|
+
"codex",
|
|
1732
|
+
"gemini",
|
|
1733
|
+
"wayland",
|
|
1734
|
+
"hermes",
|
|
1735
|
+
"cursor",
|
|
1736
|
+
"windsurf",
|
|
1737
|
+
"copilot",
|
|
1738
|
+
"opencode",
|
|
1739
|
+
"qwen",
|
|
1740
|
+
"cline",
|
|
1741
|
+
"kimi",
|
|
1742
|
+
"openclaw",
|
|
1743
|
+
"aider"
|
|
1744
|
+
];
|
|
1745
|
+
TARGET_FNS = {
|
|
1746
|
+
claude: installClaude,
|
|
1747
|
+
codex: installCodex,
|
|
1748
|
+
gemini: installGemini,
|
|
1749
|
+
wayland: installWayland,
|
|
1750
|
+
hermes: installHermes,
|
|
1751
|
+
cursor: installCursor,
|
|
1752
|
+
windsurf: installWindsurf,
|
|
1753
|
+
copilot: installCopilot,
|
|
1754
|
+
opencode: installOpencode,
|
|
1755
|
+
qwen: installQwen,
|
|
1756
|
+
cline: installCline,
|
|
1757
|
+
kimi: installKimi,
|
|
1758
|
+
openclaw: installOpenclaw,
|
|
1759
|
+
aider: installAider
|
|
1760
|
+
};
|
|
1761
|
+
install_flow_default = { runInstall, CANONICAL_ORDER };
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
2
1764
|
|
|
3
1765
|
// src/install.js
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { resolve as
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
1766
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1767
|
+
import { existsSync as existsSync5, rmSync, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3 } from "node:fs";
|
|
1768
|
+
import { resolve as resolve3, join as join5, dirname as dirname5 } from "node:path";
|
|
1769
|
+
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
1770
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
1771
|
|
|
10
1772
|
// src/marketplace.js
|
|
11
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
1773
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
1774
|
+
import { randomBytes } from "node:crypto";
|
|
12
1775
|
import { dirname, join, resolve } from "node:path";
|
|
13
1776
|
import { homedir } from "node:os";
|
|
1777
|
+
function atomicWriteJson(path3, data) {
|
|
1778
|
+
const tmp = `${path3}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
1779
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
1780
|
+
try {
|
|
1781
|
+
renameSync(tmp, path3);
|
|
1782
|
+
} catch (err) {
|
|
1783
|
+
try {
|
|
1784
|
+
unlinkSync(tmp);
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
throw new Error(`atomic write failed for ${path3}: ${err.message}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
14
1790
|
function claudeSettingsPath() {
|
|
15
1791
|
return join(homedir(), ".claude", "settings.json");
|
|
16
1792
|
}
|
|
@@ -92,12 +1868,61 @@ function mergeMarketplace(settingsPath = claudeSettingsPath(), options = {}) {
|
|
|
92
1868
|
delete settings.enabledPlugins["ijfw-core@ijfw"];
|
|
93
1869
|
}
|
|
94
1870
|
settings.enabledPlugins["ijfw@ijfw"] = true;
|
|
95
|
-
|
|
96
|
-
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
97
|
-
renameSync(tmp, settingsPath);
|
|
1871
|
+
atomicWriteJson(settingsPath, settings);
|
|
98
1872
|
return settings;
|
|
99
1873
|
}
|
|
100
1874
|
|
|
1875
|
+
// src/post-install/cold-scan.js
|
|
1876
|
+
import { spawn } from "node:child_process";
|
|
1877
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1878
|
+
import { join as join2, dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
1879
|
+
import { fileURLToPath } from "node:url";
|
|
1880
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1881
|
+
var __dirname = dirname2(__filename);
|
|
1882
|
+
var RUNNER_REL_FROM_HERE = "../../../mcp-server/src/cold-scan-runner.mjs";
|
|
1883
|
+
function triggerColdScan(projectRoot, options = {}) {
|
|
1884
|
+
const root = String(projectRoot || process.cwd());
|
|
1885
|
+
if (!existsSync2(root)) {
|
|
1886
|
+
return { spawned: false, reason: "project root not present" };
|
|
1887
|
+
}
|
|
1888
|
+
const ijfwHome = options.ijfwHome || process.env.IJFW_HOME || "";
|
|
1889
|
+
const candidates = [
|
|
1890
|
+
resolve2(__dirname, RUNNER_REL_FROM_HERE),
|
|
1891
|
+
// repo root from installer/src/post-install/
|
|
1892
|
+
resolve2(__dirname, "../../mcp-server/src/cold-scan-runner.mjs"),
|
|
1893
|
+
// legacy guess (kept for compat)
|
|
1894
|
+
ijfwHome ? join2(ijfwHome, "mcp-server/src/cold-scan-runner.mjs") : "",
|
|
1895
|
+
// explicit IJFW_HOME
|
|
1896
|
+
resolve2(projectRoot, "mcp-server/src/cold-scan-runner.mjs")
|
|
1897
|
+
// projectRoot itself is an IJFW checkout
|
|
1898
|
+
].filter(Boolean);
|
|
1899
|
+
let runnerPath = null;
|
|
1900
|
+
for (const c of candidates) {
|
|
1901
|
+
if (c && existsSync2(c)) {
|
|
1902
|
+
runnerPath = c;
|
|
1903
|
+
break;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (!runnerPath) {
|
|
1907
|
+
return { spawned: false, reason: "cold-scan runner not present" };
|
|
1908
|
+
}
|
|
1909
|
+
const nodeBin = options.nodeBin || process.execPath;
|
|
1910
|
+
const args = [runnerPath, "--project-root", root];
|
|
1911
|
+
if (options.c9Available === false) args.push("--no-c9");
|
|
1912
|
+
if (options.maxFiles) args.push("--max-files", String(options.maxFiles));
|
|
1913
|
+
try {
|
|
1914
|
+
const child = spawn(nodeBin, args, {
|
|
1915
|
+
detached: true,
|
|
1916
|
+
stdio: "ignore",
|
|
1917
|
+
env: { ...process.env, IJFW_COLD_SCAN: "1" }
|
|
1918
|
+
});
|
|
1919
|
+
child.unref();
|
|
1920
|
+
return { spawned: true, pid: child.pid };
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
return { spawned: false, reason: String(err && err.message ? err.message : err) };
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
101
1926
|
// src/install.js
|
|
102
1927
|
var DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git";
|
|
103
1928
|
var DEFAULT_BRANCH = "main";
|
|
@@ -121,7 +1946,7 @@ function parseArgs(argv) {
|
|
|
121
1946
|
}
|
|
122
1947
|
function latestTagFromGithub() {
|
|
123
1948
|
try {
|
|
124
|
-
const res =
|
|
1949
|
+
const res = spawnSync2("git", ["ls-remote", "--tags", "--refs", "--sort=-v:refname", DEFAULT_REPO], {
|
|
125
1950
|
encoding: "utf8",
|
|
126
1951
|
timeout: 1e4
|
|
127
1952
|
});
|
|
@@ -153,12 +1978,12 @@ Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--
|
|
|
153
1978
|
--yes non-interactive
|
|
154
1979
|
`);
|
|
155
1980
|
}
|
|
156
|
-
function
|
|
1981
|
+
function preflight2() {
|
|
157
1982
|
const issues = [];
|
|
158
1983
|
const [major] = process.versions.node.split(".").map(Number);
|
|
159
1984
|
if (major < 18) issues.push(`IJFW needs Node >=18 -- current: ${process.versions.node}. Upgrade Node, then retry.`);
|
|
160
|
-
if (!
|
|
161
|
-
if (
|
|
1985
|
+
if (!hasBin2("git")) {
|
|
1986
|
+
if (platform2() === "win32") {
|
|
162
1987
|
issues.push(
|
|
163
1988
|
"IJFW needs Git for Windows (it bundles git + bash). One command:\n winget install --id Git.Git -e --source winget --accept-source-agreements --accept-package-agreements\n Then close this PowerShell window, open a fresh one, and rerun:\n npx -p @ijfw/install ijfw-install"
|
|
164
1989
|
);
|
|
@@ -166,36 +1991,29 @@ function preflight() {
|
|
|
166
1991
|
issues.push("IJFW needs git on PATH -- install git (https://git-scm.com), then retry.");
|
|
167
1992
|
}
|
|
168
1993
|
}
|
|
169
|
-
if (!findBash()) {
|
|
170
|
-
if (platform() === "win32") {
|
|
171
|
-
issues.push(
|
|
172
|
-
"IJFW could not locate bash.exe. Git for Windows installs it at\n C:\\Program Files\\Git\\bin\\bash.exe\n If you installed Git elsewhere, add its bin\\ to PATH and rerun.\n Missing Git entirely? winget install --id Git.Git -e --source winget"
|
|
173
|
-
);
|
|
174
|
-
} else {
|
|
175
|
-
issues.push("IJFW needs bash on PATH -- install bash, then retry.");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
1994
|
return issues;
|
|
179
1995
|
}
|
|
180
|
-
function
|
|
181
|
-
const res =
|
|
182
|
-
|
|
1996
|
+
function hasBin2(bin) {
|
|
1997
|
+
const res = spawnSync2(bin, ["--version"], { stdio: "ignore", timeout: 3e3 });
|
|
1998
|
+
if (res.error && res.error.code === "ENOENT") return false;
|
|
1999
|
+
if (res.status === 0) return true;
|
|
2000
|
+
return res.error == null;
|
|
183
2001
|
}
|
|
184
2002
|
function findBash() {
|
|
185
|
-
if (
|
|
186
|
-
if (
|
|
187
|
-
const whereGit =
|
|
2003
|
+
if (hasBin2("bash") && platform2() !== "win32") return "bash";
|
|
2004
|
+
if (platform2() !== "win32") return hasBin2("bash") ? "bash" : null;
|
|
2005
|
+
const whereGit = spawnSync2("where", ["git"], { encoding: "utf8" });
|
|
188
2006
|
if (whereGit.status === 0) {
|
|
189
2007
|
const gitPath = (whereGit.stdout || "").split(/\r?\n/)[0].trim();
|
|
190
|
-
if (gitPath &&
|
|
191
|
-
const gitDir =
|
|
192
|
-
const gitRoot =
|
|
2008
|
+
if (gitPath && existsSync5(gitPath)) {
|
|
2009
|
+
const gitDir = dirname5(gitPath);
|
|
2010
|
+
const gitRoot = dirname5(gitDir);
|
|
193
2011
|
const candidates = [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
2012
|
+
join5(gitDir, "bash.exe"),
|
|
2013
|
+
join5(gitRoot, "bin", "bash.exe"),
|
|
2014
|
+
join5(gitRoot, "usr", "bin", "bash.exe")
|
|
197
2015
|
];
|
|
198
|
-
for (const c of candidates) if (
|
|
2016
|
+
for (const c of candidates) if (existsSync5(c)) return c;
|
|
199
2017
|
}
|
|
200
2018
|
}
|
|
201
2019
|
for (const c of [
|
|
@@ -203,93 +2021,101 @@ function findBash() {
|
|
|
203
2021
|
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
204
2022
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
205
2023
|
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
206
|
-
]) if (
|
|
207
|
-
if (
|
|
2024
|
+
]) if (existsSync5(c)) return c;
|
|
2025
|
+
if (hasBin2("bash")) return "bash";
|
|
208
2026
|
return null;
|
|
209
2027
|
}
|
|
210
2028
|
function resolveTarget(opt) {
|
|
211
|
-
if (opt.dir) return
|
|
212
|
-
if (process.env.IJFW_HOME) return
|
|
213
|
-
return
|
|
2029
|
+
if (opt.dir) return resolve3(opt.dir);
|
|
2030
|
+
if (process.env.IJFW_HOME) return resolve3(process.env.IJFW_HOME);
|
|
2031
|
+
return join5(homedir3(), ".ijfw");
|
|
214
2032
|
}
|
|
215
2033
|
function runCheck(cmd, args, opts) {
|
|
216
|
-
const r =
|
|
217
|
-
return { status: r.status, stdout: r.stdout || "" };
|
|
2034
|
+
const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
|
|
2035
|
+
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
|
|
218
2036
|
}
|
|
219
2037
|
function cloneOrPull(dir, branch) {
|
|
220
|
-
if (!
|
|
221
|
-
|
|
222
|
-
const r =
|
|
2038
|
+
if (!existsSync5(dir)) {
|
|
2039
|
+
mkdirSync4(dir, { recursive: true });
|
|
2040
|
+
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
223
2041
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
224
2042
|
return "cloned";
|
|
225
2043
|
}
|
|
226
|
-
const hasGit =
|
|
2044
|
+
const hasGit = existsSync5(join5(dir, ".git"));
|
|
227
2045
|
if (hasGit) {
|
|
228
|
-
const { status: remoteStatus, stdout } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
2046
|
+
const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
2047
|
+
if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
|
|
2048
|
+
else if (remoteSignal) console.warn(` git exited on signal ${remoteSignal}`);
|
|
2049
|
+
else if (remoteStatus !== 0 && remoteStderr) console.warn(` git remote get-url: ${remoteStderr.slice(0, 120).trim()}`);
|
|
229
2050
|
if (remoteStatus === 0) {
|
|
2051
|
+
const STALE_ORIGINS = [
|
|
2052
|
+
"https://github.com/seandonahoe/ijfw.git",
|
|
2053
|
+
"https://github.com/seandonahoe/ijfw",
|
|
2054
|
+
"https://github.com/seandonahoe/ijfw/",
|
|
2055
|
+
"https://github.com/seandonahoe/ijfw.git/"
|
|
2056
|
+
];
|
|
230
2057
|
const currentOrigin = (stdout || "").trim();
|
|
231
|
-
if (currentOrigin
|
|
232
|
-
|
|
233
|
-
|
|
2058
|
+
if (STALE_ORIGINS.includes(currentOrigin)) {
|
|
2059
|
+
const setUrl = spawnSync2("git", ["-C", dir, "remote", "set-url", "origin", DEFAULT_REPO], { stdio: "inherit" });
|
|
2060
|
+
if (setUrl.status !== 0) {
|
|
2061
|
+
console.warn(` [!] origin migration failed -- could not repoint ${currentOrigin} to ${DEFAULT_REPO}`);
|
|
2062
|
+
} else {
|
|
2063
|
+
console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
|
|
2064
|
+
}
|
|
234
2065
|
}
|
|
235
|
-
const fetch =
|
|
2066
|
+
const fetch = spawnSync2("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
|
|
236
2067
|
if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
|
|
237
|
-
const co =
|
|
2068
|
+
const co = spawnSync2("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
|
|
238
2069
|
if (co.status !== 0) throw new Error(`IJFW checkout did not complete (exit ${co.status}) -- run ijfw doctor to check prerequisites.`);
|
|
239
2070
|
return "updated";
|
|
240
2071
|
}
|
|
241
2072
|
}
|
|
242
2073
|
const backupDir = dir + ".bak." + Date.now();
|
|
243
|
-
|
|
2074
|
+
renameSync3(dir, backupDir);
|
|
244
2075
|
try {
|
|
245
|
-
const r =
|
|
2076
|
+
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
246
2077
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
247
2078
|
for (const item of ["memory", "sessions", "install.log", ".session-counter"]) {
|
|
248
|
-
const src =
|
|
249
|
-
if (
|
|
250
|
-
const dst =
|
|
251
|
-
if (
|
|
252
|
-
|
|
2079
|
+
const src = join5(backupDir, item);
|
|
2080
|
+
if (existsSync5(src)) {
|
|
2081
|
+
const dst = join5(dir, item);
|
|
2082
|
+
if (existsSync5(dst)) rmSync(dst, { recursive: true, force: true });
|
|
2083
|
+
renameSync3(src, dst);
|
|
253
2084
|
}
|
|
254
2085
|
}
|
|
255
2086
|
rmSync(backupDir, { recursive: true, force: true });
|
|
256
2087
|
return "updated";
|
|
257
2088
|
} catch (err) {
|
|
258
|
-
if (
|
|
259
|
-
|
|
2089
|
+
if (existsSync5(dir)) rmSync(dir, { recursive: true, force: true });
|
|
2090
|
+
renameSync3(backupDir, dir);
|
|
260
2091
|
throw err;
|
|
261
2092
|
}
|
|
262
2093
|
}
|
|
263
|
-
function runInstallScript(dir) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!bashExe) {
|
|
276
|
-
throw new Error("IJFW could not locate bash (preflight should have caught this -- file an issue).");
|
|
277
|
-
}
|
|
278
|
-
const r = spawnSync(bashExe, ["scripts/install.sh"], { cwd: dir, stdio: "inherit", env });
|
|
279
|
-
if (r.status !== 0) throw new Error(`IJFW platform config step did not complete (exit ${r.status}) -- run ijfw doctor to see what to fix.`);
|
|
2094
|
+
async function runInstallScript(dir) {
|
|
2095
|
+
const canonicalDir = join5(homedir3(), ".ijfw");
|
|
2096
|
+
const isCustomDir = resolve3(dir) !== canonicalDir;
|
|
2097
|
+
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
|
|
2098
|
+
await runInstall2({
|
|
2099
|
+
targets: void 0,
|
|
2100
|
+
// undefined = canonical 14
|
|
2101
|
+
ijfwHome: dir,
|
|
2102
|
+
ijfwCustomDir: isCustomDir,
|
|
2103
|
+
repoRoot: dir,
|
|
2104
|
+
noninteractive: !!process.env.CI || process.env.IJFW_NONINTERACTIVE === "1"
|
|
2105
|
+
});
|
|
280
2106
|
}
|
|
281
2107
|
async function main() {
|
|
282
2108
|
const opts = parseArgs(process.argv);
|
|
283
|
-
const issues =
|
|
2109
|
+
const issues = preflight2();
|
|
284
2110
|
if (issues.length) {
|
|
285
2111
|
console.error("IJFW needs a couple of things first -- fix these and re-run:");
|
|
286
2112
|
for (const i of issues) console.error(" - " + i);
|
|
287
2113
|
process.exit(1);
|
|
288
2114
|
}
|
|
289
2115
|
const target = resolveTarget(opts);
|
|
290
|
-
const createdThisRun = !
|
|
2116
|
+
const createdThisRun = !existsSync5(target);
|
|
291
2117
|
const sigint = () => {
|
|
292
|
-
if (createdThisRun &&
|
|
2118
|
+
if (createdThisRun && existsSync5(target)) {
|
|
293
2119
|
try {
|
|
294
2120
|
rmSync(target, { recursive: true, force: true });
|
|
295
2121
|
} catch {
|
|
@@ -303,15 +2129,20 @@ async function main() {
|
|
|
303
2129
|
console.log(` version: ${ref}`);
|
|
304
2130
|
const action = cloneOrPull(target, ref);
|
|
305
2131
|
console.log(` repo ${action}`);
|
|
306
|
-
runInstallScript(target);
|
|
2132
|
+
await runInstallScript(target);
|
|
307
2133
|
console.log(" platform configs applied");
|
|
308
2134
|
if (!opts.noMarketplace) {
|
|
309
2135
|
const settingsPath = claudeSettingsPath();
|
|
310
2136
|
mergeMarketplace(settingsPath, { rootDir: target });
|
|
311
2137
|
console.log(` marketplace registered in ${settingsPath}`);
|
|
312
2138
|
}
|
|
2139
|
+
try {
|
|
2140
|
+
const coldScanRoot = process.env.IJFW_PROJECT_DIR || process.cwd();
|
|
2141
|
+
triggerColdScan(coldScanRoot, { ijfwHome: target });
|
|
2142
|
+
} catch {
|
|
2143
|
+
}
|
|
313
2144
|
console.log("");
|
|
314
|
-
console.log("IJFW now active across
|
|
2145
|
+
console.log("IJFW now active across 14 platforms -- one memory layer, all your models, zero config.");
|
|
315
2146
|
console.log(" Run `ijfw demo` to see the Trident in action.");
|
|
316
2147
|
console.log(" Run `ijfw doctor` to confirm which auditors are reachable.");
|
|
317
2148
|
console.log(" Privacy: everything stays local. See NO_TELEMETRY.md.");
|
|
@@ -319,8 +2150,8 @@ async function main() {
|
|
|
319
2150
|
}
|
|
320
2151
|
function isDirectRun() {
|
|
321
2152
|
try {
|
|
322
|
-
const entry = process.argv[1] &&
|
|
323
|
-
const self =
|
|
2153
|
+
const entry = process.argv[1] && realpathSync2(process.argv[1]);
|
|
2154
|
+
const self = fileURLToPath2(import.meta.url);
|
|
324
2155
|
return entry === self;
|
|
325
2156
|
} catch {
|
|
326
2157
|
return false;
|