@ijfw/install 1.2.10 → 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/install.js +1884 -80
- package/package.json +1 -1
- package/src/install.ps1 +8 -15
package/dist/install.js
CHANGED
|
@@ -1,28 +1,1790 @@
|
|
|
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
1773
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
12
1774
|
import { randomBytes } from "node:crypto";
|
|
13
1775
|
import { dirname, join, resolve } from "node:path";
|
|
14
1776
|
import { homedir } from "node:os";
|
|
15
|
-
function atomicWriteJson(
|
|
16
|
-
const tmp = `${
|
|
1777
|
+
function atomicWriteJson(path3, data) {
|
|
1778
|
+
const tmp = `${path3}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
17
1779
|
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
18
1780
|
try {
|
|
19
|
-
renameSync(tmp,
|
|
1781
|
+
renameSync(tmp, path3);
|
|
20
1782
|
} catch (err) {
|
|
21
1783
|
try {
|
|
22
1784
|
unlinkSync(tmp);
|
|
23
1785
|
} catch {
|
|
24
1786
|
}
|
|
25
|
-
throw new Error(`atomic write failed for ${
|
|
1787
|
+
throw new Error(`atomic write failed for ${path3}: ${err.message}`);
|
|
26
1788
|
}
|
|
27
1789
|
}
|
|
28
1790
|
function claudeSettingsPath() {
|
|
@@ -110,6 +1872,57 @@ function mergeMarketplace(settingsPath = claudeSettingsPath(), options = {}) {
|
|
|
110
1872
|
return settings;
|
|
111
1873
|
}
|
|
112
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
|
+
|
|
113
1926
|
// src/install.js
|
|
114
1927
|
var DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git";
|
|
115
1928
|
var DEFAULT_BRANCH = "main";
|
|
@@ -133,7 +1946,7 @@ function parseArgs(argv) {
|
|
|
133
1946
|
}
|
|
134
1947
|
function latestTagFromGithub() {
|
|
135
1948
|
try {
|
|
136
|
-
const res =
|
|
1949
|
+
const res = spawnSync2("git", ["ls-remote", "--tags", "--refs", "--sort=-v:refname", DEFAULT_REPO], {
|
|
137
1950
|
encoding: "utf8",
|
|
138
1951
|
timeout: 1e4
|
|
139
1952
|
});
|
|
@@ -165,12 +1978,12 @@ Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--
|
|
|
165
1978
|
--yes non-interactive
|
|
166
1979
|
`);
|
|
167
1980
|
}
|
|
168
|
-
function
|
|
1981
|
+
function preflight2() {
|
|
169
1982
|
const issues = [];
|
|
170
1983
|
const [major] = process.versions.node.split(".").map(Number);
|
|
171
1984
|
if (major < 18) issues.push(`IJFW needs Node >=18 -- current: ${process.versions.node}. Upgrade Node, then retry.`);
|
|
172
|
-
if (!
|
|
173
|
-
if (
|
|
1985
|
+
if (!hasBin2("git")) {
|
|
1986
|
+
if (platform2() === "win32") {
|
|
174
1987
|
issues.push(
|
|
175
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"
|
|
176
1989
|
);
|
|
@@ -178,38 +1991,29 @@ function preflight() {
|
|
|
178
1991
|
issues.push("IJFW needs git on PATH -- install git (https://git-scm.com), then retry.");
|
|
179
1992
|
}
|
|
180
1993
|
}
|
|
181
|
-
if (!findBash()) {
|
|
182
|
-
if (platform() === "win32") {
|
|
183
|
-
issues.push(
|
|
184
|
-
"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"
|
|
185
|
-
);
|
|
186
|
-
} else {
|
|
187
|
-
issues.push("IJFW needs bash on PATH -- install bash, then retry.");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
1994
|
return issues;
|
|
191
1995
|
}
|
|
192
|
-
function
|
|
193
|
-
const res =
|
|
1996
|
+
function hasBin2(bin) {
|
|
1997
|
+
const res = spawnSync2(bin, ["--version"], { stdio: "ignore", timeout: 3e3 });
|
|
194
1998
|
if (res.error && res.error.code === "ENOENT") return false;
|
|
195
1999
|
if (res.status === 0) return true;
|
|
196
2000
|
return res.error == null;
|
|
197
2001
|
}
|
|
198
2002
|
function findBash() {
|
|
199
|
-
if (
|
|
200
|
-
if (
|
|
201
|
-
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" });
|
|
202
2006
|
if (whereGit.status === 0) {
|
|
203
2007
|
const gitPath = (whereGit.stdout || "").split(/\r?\n/)[0].trim();
|
|
204
|
-
if (gitPath &&
|
|
205
|
-
const gitDir =
|
|
206
|
-
const gitRoot =
|
|
2008
|
+
if (gitPath && existsSync5(gitPath)) {
|
|
2009
|
+
const gitDir = dirname5(gitPath);
|
|
2010
|
+
const gitRoot = dirname5(gitDir);
|
|
207
2011
|
const candidates = [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
2012
|
+
join5(gitDir, "bash.exe"),
|
|
2013
|
+
join5(gitRoot, "bin", "bash.exe"),
|
|
2014
|
+
join5(gitRoot, "usr", "bin", "bash.exe")
|
|
211
2015
|
];
|
|
212
|
-
for (const c of candidates) if (
|
|
2016
|
+
for (const c of candidates) if (existsSync5(c)) return c;
|
|
213
2017
|
}
|
|
214
2018
|
}
|
|
215
2019
|
for (const c of [
|
|
@@ -217,27 +2021,27 @@ function findBash() {
|
|
|
217
2021
|
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
218
2022
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
219
2023
|
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
220
|
-
]) if (
|
|
221
|
-
if (
|
|
2024
|
+
]) if (existsSync5(c)) return c;
|
|
2025
|
+
if (hasBin2("bash")) return "bash";
|
|
222
2026
|
return null;
|
|
223
2027
|
}
|
|
224
2028
|
function resolveTarget(opt) {
|
|
225
|
-
if (opt.dir) return
|
|
226
|
-
if (process.env.IJFW_HOME) return
|
|
227
|
-
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");
|
|
228
2032
|
}
|
|
229
2033
|
function runCheck(cmd, args, opts) {
|
|
230
|
-
const r =
|
|
2034
|
+
const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
|
|
231
2035
|
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
|
|
232
2036
|
}
|
|
233
2037
|
function cloneOrPull(dir, branch) {
|
|
234
|
-
if (!
|
|
235
|
-
|
|
236
|
-
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" });
|
|
237
2041
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
238
2042
|
return "cloned";
|
|
239
2043
|
}
|
|
240
|
-
const hasGit =
|
|
2044
|
+
const hasGit = existsSync5(join5(dir, ".git"));
|
|
241
2045
|
if (hasGit) {
|
|
242
2046
|
const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
243
2047
|
if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
|
|
@@ -252,71 +2056,66 @@ function cloneOrPull(dir, branch) {
|
|
|
252
2056
|
];
|
|
253
2057
|
const currentOrigin = (stdout || "").trim();
|
|
254
2058
|
if (STALE_ORIGINS.includes(currentOrigin)) {
|
|
255
|
-
const setUrl =
|
|
2059
|
+
const setUrl = spawnSync2("git", ["-C", dir, "remote", "set-url", "origin", DEFAULT_REPO], { stdio: "inherit" });
|
|
256
2060
|
if (setUrl.status !== 0) {
|
|
257
2061
|
console.warn(` [!] origin migration failed -- could not repoint ${currentOrigin} to ${DEFAULT_REPO}`);
|
|
258
2062
|
} else {
|
|
259
2063
|
console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
|
|
260
2064
|
}
|
|
261
2065
|
}
|
|
262
|
-
const fetch =
|
|
2066
|
+
const fetch = spawnSync2("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
|
|
263
2067
|
if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
|
|
264
|
-
const co =
|
|
2068
|
+
const co = spawnSync2("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
|
|
265
2069
|
if (co.status !== 0) throw new Error(`IJFW checkout did not complete (exit ${co.status}) -- run ijfw doctor to check prerequisites.`);
|
|
266
2070
|
return "updated";
|
|
267
2071
|
}
|
|
268
2072
|
}
|
|
269
2073
|
const backupDir = dir + ".bak." + Date.now();
|
|
270
|
-
|
|
2074
|
+
renameSync3(dir, backupDir);
|
|
271
2075
|
try {
|
|
272
|
-
const r =
|
|
2076
|
+
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
273
2077
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
274
2078
|
for (const item of ["memory", "sessions", "install.log", ".session-counter"]) {
|
|
275
|
-
const src =
|
|
276
|
-
if (
|
|
277
|
-
const dst =
|
|
278
|
-
if (
|
|
279
|
-
|
|
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);
|
|
280
2084
|
}
|
|
281
2085
|
}
|
|
282
2086
|
rmSync(backupDir, { recursive: true, force: true });
|
|
283
2087
|
return "updated";
|
|
284
2088
|
} catch (err) {
|
|
285
|
-
if (
|
|
286
|
-
|
|
2089
|
+
if (existsSync5(dir)) rmSync(dir, { recursive: true, force: true });
|
|
2090
|
+
renameSync3(backupDir, dir);
|
|
287
2091
|
throw err;
|
|
288
2092
|
}
|
|
289
2093
|
}
|
|
290
|
-
function runInstallScript(dir) {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (!bashExe) {
|
|
303
|
-
throw new Error("IJFW could not locate bash (preflight should have caught this -- file an issue).");
|
|
304
|
-
}
|
|
305
|
-
const r = spawnSync(bashExe, ["scripts/install.sh"], { cwd: dir, stdio: "inherit", env });
|
|
306
|
-
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
|
+
});
|
|
307
2106
|
}
|
|
308
2107
|
async function main() {
|
|
309
2108
|
const opts = parseArgs(process.argv);
|
|
310
|
-
const issues =
|
|
2109
|
+
const issues = preflight2();
|
|
311
2110
|
if (issues.length) {
|
|
312
2111
|
console.error("IJFW needs a couple of things first -- fix these and re-run:");
|
|
313
2112
|
for (const i of issues) console.error(" - " + i);
|
|
314
2113
|
process.exit(1);
|
|
315
2114
|
}
|
|
316
2115
|
const target = resolveTarget(opts);
|
|
317
|
-
const createdThisRun = !
|
|
2116
|
+
const createdThisRun = !existsSync5(target);
|
|
318
2117
|
const sigint = () => {
|
|
319
|
-
if (createdThisRun &&
|
|
2118
|
+
if (createdThisRun && existsSync5(target)) {
|
|
320
2119
|
try {
|
|
321
2120
|
rmSync(target, { recursive: true, force: true });
|
|
322
2121
|
} catch {
|
|
@@ -330,15 +2129,20 @@ async function main() {
|
|
|
330
2129
|
console.log(` version: ${ref}`);
|
|
331
2130
|
const action = cloneOrPull(target, ref);
|
|
332
2131
|
console.log(` repo ${action}`);
|
|
333
|
-
runInstallScript(target);
|
|
2132
|
+
await runInstallScript(target);
|
|
334
2133
|
console.log(" platform configs applied");
|
|
335
2134
|
if (!opts.noMarketplace) {
|
|
336
2135
|
const settingsPath = claudeSettingsPath();
|
|
337
2136
|
mergeMarketplace(settingsPath, { rootDir: target });
|
|
338
2137
|
console.log(` marketplace registered in ${settingsPath}`);
|
|
339
2138
|
}
|
|
2139
|
+
try {
|
|
2140
|
+
const coldScanRoot = process.env.IJFW_PROJECT_DIR || process.cwd();
|
|
2141
|
+
triggerColdScan(coldScanRoot, { ijfwHome: target });
|
|
2142
|
+
} catch {
|
|
2143
|
+
}
|
|
340
2144
|
console.log("");
|
|
341
|
-
console.log("IJFW now active across
|
|
2145
|
+
console.log("IJFW now active across 14 platforms -- one memory layer, all your models, zero config.");
|
|
342
2146
|
console.log(" Run `ijfw demo` to see the Trident in action.");
|
|
343
2147
|
console.log(" Run `ijfw doctor` to confirm which auditors are reachable.");
|
|
344
2148
|
console.log(" Privacy: everything stays local. See NO_TELEMETRY.md.");
|
|
@@ -346,8 +2150,8 @@ async function main() {
|
|
|
346
2150
|
}
|
|
347
2151
|
function isDirectRun() {
|
|
348
2152
|
try {
|
|
349
|
-
const entry = process.argv[1] &&
|
|
350
|
-
const self =
|
|
2153
|
+
const entry = process.argv[1] && realpathSync2(process.argv[1]);
|
|
2154
|
+
const self = fileURLToPath2(import.meta.url);
|
|
351
2155
|
return entry === self;
|
|
352
2156
|
} catch {
|
|
353
2157
|
return false;
|