@ijfw/install 1.2.8 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ijfw.js +39 -34
- package/dist/install.js +38 -11
- package/dist/uninstall.js +36 -11
- package/package.json +1 -1
- package/src/install.ps1 +65 -11
package/dist/ijfw.js
CHANGED
|
@@ -483,7 +483,7 @@ async function run5(ctx) {
|
|
|
483
483
|
const res = spawnSync5(
|
|
484
484
|
"npx",
|
|
485
485
|
["--yes", `publint@${ver}`, "--strict"],
|
|
486
|
-
{ encoding: "utf8", cwd: ctx.repoRoot + "/installer", timeout:
|
|
486
|
+
{ encoding: "utf8", cwd: ctx.repoRoot + "/installer", timeout: 9e4 }
|
|
487
487
|
);
|
|
488
488
|
const durationMs = Date.now() - t0;
|
|
489
489
|
const output = (res.stdout || "") + (res.stderr || "");
|
|
@@ -727,7 +727,7 @@ __export(pack_smoke_exports, {
|
|
|
727
727
|
severity: () => severity10
|
|
728
728
|
});
|
|
729
729
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
730
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
730
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync as readdirSync3, existsSync } from "node:fs";
|
|
731
731
|
import { join as join6, resolve } from "node:path";
|
|
732
732
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
733
733
|
async function run10(ctx) {
|
|
@@ -803,13 +803,9 @@ async function run10(ctx) {
|
|
|
803
803
|
];
|
|
804
804
|
let binPath = null;
|
|
805
805
|
for (const c2 of binCandidates) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
binPath = c2;
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
} catch {
|
|
806
|
+
if (existsSync(c2)) {
|
|
807
|
+
binPath = c2;
|
|
808
|
+
break;
|
|
813
809
|
}
|
|
814
810
|
}
|
|
815
811
|
if (!binPath) {
|
|
@@ -883,7 +879,7 @@ __export(upgrade_smoke_exports, {
|
|
|
883
879
|
severity: () => severity11
|
|
884
880
|
});
|
|
885
881
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
886
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync, existsSync } from "node:fs";
|
|
882
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync, existsSync as existsSync2 } from "node:fs";
|
|
887
883
|
import { join as join7, resolve as resolve2 } from "node:path";
|
|
888
884
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
889
885
|
async function run11(ctx) {
|
|
@@ -952,8 +948,7 @@ async function run11(ctx) {
|
|
|
952
948
|
];
|
|
953
949
|
let installerBin = null;
|
|
954
950
|
for (const c2 of binCandidates) {
|
|
955
|
-
|
|
956
|
-
if (check.status === 0) {
|
|
951
|
+
if (existsSync2(c2)) {
|
|
957
952
|
installerBin = c2;
|
|
958
953
|
break;
|
|
959
954
|
}
|
|
@@ -968,7 +963,7 @@ async function run11(ctx) {
|
|
|
968
963
|
};
|
|
969
964
|
}
|
|
970
965
|
const settingsPath = join7(claudeDir, "settings.json");
|
|
971
|
-
if (
|
|
966
|
+
if (existsSync2(settingsPath)) {
|
|
972
967
|
let settings;
|
|
973
968
|
try {
|
|
974
969
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
@@ -993,7 +988,7 @@ async function run11(ctx) {
|
|
|
993
988
|
}
|
|
994
989
|
}
|
|
995
990
|
const marketplaceSrc = join7(installerDir, "src", "marketplace.js");
|
|
996
|
-
if (
|
|
991
|
+
if (existsSync2(marketplaceSrc)) {
|
|
997
992
|
const src = readFileSync(marketplaceSrc, "utf8");
|
|
998
993
|
const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
|
|
999
994
|
const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
|
|
@@ -1049,7 +1044,7 @@ var preflight_exports = {};
|
|
|
1049
1044
|
__export(preflight_exports, {
|
|
1050
1045
|
runPreflightCommand: () => runPreflightCommand
|
|
1051
1046
|
});
|
|
1052
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
1047
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
1053
1048
|
import { join as join8, dirname } from "node:path";
|
|
1054
1049
|
import { fileURLToPath } from "node:url";
|
|
1055
1050
|
function printHelp() {
|
|
@@ -1091,7 +1086,7 @@ function loadVersions(repoRoot2) {
|
|
|
1091
1086
|
join8(repoRoot2, ".ijfw", "preflight-versions.json")
|
|
1092
1087
|
];
|
|
1093
1088
|
for (const f of candidates) {
|
|
1094
|
-
if (
|
|
1089
|
+
if (existsSync3(f)) {
|
|
1095
1090
|
try {
|
|
1096
1091
|
return JSON.parse(readFileSync2(f, "utf8"));
|
|
1097
1092
|
} catch {
|
|
@@ -2419,14 +2414,14 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2419
2414
|
// src/ijfw.js
|
|
2420
2415
|
import { dirname as dirname2, join as join9, resolve as resolve3, basename } from "node:path";
|
|
2421
2416
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2422
|
-
import { existsSync as
|
|
2417
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2423
2418
|
import { homedir, platform as platform2 } from "node:os";
|
|
2424
2419
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
2425
2420
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
2426
2421
|
function repoRoot() {
|
|
2427
2422
|
let dir = __dirname2;
|
|
2428
2423
|
for (let i = 0; i < 6; i++) {
|
|
2429
|
-
if (
|
|
2424
|
+
if (existsSync4(join9(dir, "package.json")) && existsSync4(join9(dir, ".git"))) return dir;
|
|
2430
2425
|
dir = resolve3(dir, "..");
|
|
2431
2426
|
}
|
|
2432
2427
|
return process.cwd();
|
|
@@ -2442,7 +2437,7 @@ COMMANDS
|
|
|
2442
2437
|
install Install IJFW into your AI coding agents
|
|
2443
2438
|
uninstall Remove IJFW from your AI coding agents
|
|
2444
2439
|
help Open the full IJFW guide (terminal, or --browser for rendered)
|
|
2445
|
-
preflight Run
|
|
2440
|
+
preflight Run 12-gate quality pipeline before publishing
|
|
2446
2441
|
dashboard Start / stop / check the local observability dashboard
|
|
2447
2442
|
design Manage the visual design companion
|
|
2448
2443
|
doctor Diagnose IJFW installation health
|
|
@@ -2453,19 +2448,21 @@ COMMANDS
|
|
|
2453
2448
|
}
|
|
2454
2449
|
function doctorCheck(cmd, args) {
|
|
2455
2450
|
const r = spawnSync12(cmd, args, { encoding: "utf8" });
|
|
2456
|
-
|
|
2451
|
+
if (r.status === 0) return r.stdout.split("\n")[0].trim();
|
|
2452
|
+
if (r.status === 127 || r.error && r.error.code === "ENOENT") return "not found";
|
|
2453
|
+
return `exit ${r.status} (may be transient)`;
|
|
2457
2454
|
}
|
|
2458
2455
|
function findCli() {
|
|
2459
2456
|
const candidates = [
|
|
2460
2457
|
join9(repoRoot(), "mcp-server", "src", "cross-orchestrator-cli.js"),
|
|
2461
2458
|
join9(homedir(), ".ijfw", "mcp-server", "src", "cross-orchestrator-cli.js")
|
|
2462
2459
|
];
|
|
2463
|
-
return candidates.find((p) =>
|
|
2460
|
+
return candidates.find((p) => existsSync4(p)) || null;
|
|
2464
2461
|
}
|
|
2465
2462
|
function delegateToCli(argTail) {
|
|
2466
2463
|
const cli = findCli();
|
|
2467
2464
|
if (!cli) return false;
|
|
2468
|
-
const r = spawnSync12(
|
|
2465
|
+
const r = spawnSync12(process.execPath, [cli, ...argTail], { stdio: "inherit" });
|
|
2469
2466
|
process.exit(r.status ?? 1);
|
|
2470
2467
|
}
|
|
2471
2468
|
async function main() {
|
|
@@ -2516,16 +2513,21 @@ async function main() {
|
|
|
2516
2513
|
case "dashboard": {
|
|
2517
2514
|
const dashSub = argv[3];
|
|
2518
2515
|
const root = repoRoot();
|
|
2516
|
+
const ijfwHome = join9(homedir(), ".ijfw");
|
|
2517
|
+
const findInTree = (...rel) => {
|
|
2518
|
+
const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
|
|
2519
|
+
return candidates.find((p) => existsSync4(p)) || null;
|
|
2520
|
+
};
|
|
2519
2521
|
if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
|
|
2520
|
-
const dashBin =
|
|
2521
|
-
if (
|
|
2522
|
+
const dashBin = findInTree("mcp-server", "bin", "ijfw-dashboard");
|
|
2523
|
+
if (dashBin) {
|
|
2522
2524
|
const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
|
|
2523
2525
|
process.exit(r.status ?? 0);
|
|
2524
2526
|
} else {
|
|
2525
|
-
const serverJs =
|
|
2526
|
-
if (dashSub === "start" &&
|
|
2527
|
+
const serverJs = findInTree("mcp-server", "src", "dashboard-server.js");
|
|
2528
|
+
if (dashSub === "start" && serverJs) {
|
|
2527
2529
|
const { spawn } = await import("node:child_process");
|
|
2528
|
-
const child = spawn(process.execPath, [serverJs, "--daemon"], {
|
|
2530
|
+
const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
|
|
2529
2531
|
detached: true,
|
|
2530
2532
|
stdio: "ignore"
|
|
2531
2533
|
});
|
|
@@ -2533,12 +2535,12 @@ async function main() {
|
|
|
2533
2535
|
console.log("Dashboard starting... (check: ijfw dashboard status)");
|
|
2534
2536
|
process.exit(0);
|
|
2535
2537
|
}
|
|
2536
|
-
console.log("[ijfw] Dashboard
|
|
2538
|
+
console.log("[ijfw] Dashboard not found. Try `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
|
|
2537
2539
|
process.exit(1);
|
|
2538
2540
|
}
|
|
2539
2541
|
} else if (dashSub === "render" || !dashSub) {
|
|
2540
|
-
const binJs =
|
|
2541
|
-
if (
|
|
2542
|
+
const binJs = findInTree("scripts", "dashboard", "bin.js");
|
|
2543
|
+
if (binJs) {
|
|
2542
2544
|
const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
|
|
2543
2545
|
process.exit(r.status ?? 0);
|
|
2544
2546
|
} else {
|
|
@@ -2562,7 +2564,7 @@ async function main() {
|
|
|
2562
2564
|
process.exit(1);
|
|
2563
2565
|
}
|
|
2564
2566
|
const abs = resolve3(filePath);
|
|
2565
|
-
if (!
|
|
2567
|
+
if (!existsSync4(abs)) {
|
|
2566
2568
|
console.error(`File not found: ${abs}`);
|
|
2567
2569
|
process.exit(1);
|
|
2568
2570
|
}
|
|
@@ -2588,7 +2590,7 @@ async function main() {
|
|
|
2588
2590
|
resolve3(__dirname2, "..", "docs", "GUIDE.md"),
|
|
2589
2591
|
join9(homedir(), ".ijfw", "docs", "GUIDE.md")
|
|
2590
2592
|
];
|
|
2591
|
-
const guidePath = candidates.find((p) =>
|
|
2593
|
+
const guidePath = candidates.find((p) => existsSync4(p));
|
|
2592
2594
|
if (!guidePath) {
|
|
2593
2595
|
console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://gitlab.com/therealseandonahoe/ijfw/-/blob/main/docs/GUIDE.md");
|
|
2594
2596
|
process.exit(1);
|
|
@@ -2598,7 +2600,7 @@ async function main() {
|
|
|
2598
2600
|
const assetsSrc = join9(dirname2(guidePath), "guide", "assets");
|
|
2599
2601
|
const outDir = join9(homedir(), ".ijfw", "guide");
|
|
2600
2602
|
mkdirSync3(join9(outDir, "assets"), { recursive: true });
|
|
2601
|
-
if (
|
|
2603
|
+
if (existsSync4(assetsSrc)) {
|
|
2602
2604
|
for (const f of readdirSync4(assetsSrc)) {
|
|
2603
2605
|
copyFileSync(join9(assetsSrc, f), join9(outDir, "assets", f));
|
|
2604
2606
|
}
|
|
@@ -2630,7 +2632,10 @@ async function main() {
|
|
|
2630
2632
|
}
|
|
2631
2633
|
const hasLess = spawnSync12("less", ["-V"], { stdio: "ignore" }).status === 0;
|
|
2632
2634
|
if (hasLess) {
|
|
2633
|
-
spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
|
|
2635
|
+
const lessRes = spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
|
|
2636
|
+
if (lessRes.status !== 0 && lessRes.status !== null) {
|
|
2637
|
+
process.stdout.write(readFileSync3(guidePath, "utf8"));
|
|
2638
|
+
}
|
|
2634
2639
|
} else {
|
|
2635
2640
|
process.stdout.write(readFileSync3(guidePath, "utf8"));
|
|
2636
2641
|
}
|
package/dist/install.js
CHANGED
|
@@ -8,9 +8,23 @@ import { homedir as homedir2, platform } from "node:os";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/marketplace.js
|
|
11
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
12
13
|
import { dirname, join, resolve } from "node:path";
|
|
13
14
|
import { homedir } from "node:os";
|
|
15
|
+
function atomicWriteJson(path, data) {
|
|
16
|
+
const tmp = `${path}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
17
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
18
|
+
try {
|
|
19
|
+
renameSync(tmp, path);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(tmp);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`atomic write failed for ${path}: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
function claudeSettingsPath() {
|
|
15
29
|
return join(homedir(), ".claude", "settings.json");
|
|
16
30
|
}
|
|
@@ -92,9 +106,7 @@ function mergeMarketplace(settingsPath = claudeSettingsPath(), options = {}) {
|
|
|
92
106
|
delete settings.enabledPlugins["ijfw-core@ijfw"];
|
|
93
107
|
}
|
|
94
108
|
settings.enabledPlugins["ijfw@ijfw"] = true;
|
|
95
|
-
|
|
96
|
-
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
97
|
-
renameSync(tmp, settingsPath);
|
|
109
|
+
atomicWriteJson(settingsPath, settings);
|
|
98
110
|
return settings;
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -178,8 +190,10 @@ function preflight() {
|
|
|
178
190
|
return issues;
|
|
179
191
|
}
|
|
180
192
|
function hasBin(bin) {
|
|
181
|
-
const res = spawnSync(bin, ["--version"], { stdio: "ignore" });
|
|
182
|
-
|
|
193
|
+
const res = spawnSync(bin, ["--version"], { stdio: "ignore", timeout: 3e3 });
|
|
194
|
+
if (res.error && res.error.code === "ENOENT") return false;
|
|
195
|
+
if (res.status === 0) return true;
|
|
196
|
+
return res.error == null;
|
|
183
197
|
}
|
|
184
198
|
function findBash() {
|
|
185
199
|
if (hasBin("bash") && platform() !== "win32") return "bash";
|
|
@@ -214,7 +228,7 @@ function resolveTarget(opt) {
|
|
|
214
228
|
}
|
|
215
229
|
function runCheck(cmd, args, opts) {
|
|
216
230
|
const r = spawnSync(cmd, args, { encoding: "utf8", ...opts });
|
|
217
|
-
return { status: r.status, stdout: r.stdout || "" };
|
|
231
|
+
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
|
|
218
232
|
}
|
|
219
233
|
function cloneOrPull(dir, branch) {
|
|
220
234
|
if (!existsSync2(dir)) {
|
|
@@ -225,12 +239,25 @@ function cloneOrPull(dir, branch) {
|
|
|
225
239
|
}
|
|
226
240
|
const hasGit = existsSync2(join2(dir, ".git"));
|
|
227
241
|
if (hasGit) {
|
|
228
|
-
const { status: remoteStatus, stdout } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
242
|
+
const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
243
|
+
if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
|
|
244
|
+
else if (remoteSignal) console.warn(` git exited on signal ${remoteSignal}`);
|
|
245
|
+
else if (remoteStatus !== 0 && remoteStderr) console.warn(` git remote get-url: ${remoteStderr.slice(0, 120).trim()}`);
|
|
229
246
|
if (remoteStatus === 0) {
|
|
247
|
+
const STALE_ORIGINS = [
|
|
248
|
+
"https://github.com/seandonahoe/ijfw.git",
|
|
249
|
+
"https://github.com/seandonahoe/ijfw",
|
|
250
|
+
"https://github.com/seandonahoe/ijfw/",
|
|
251
|
+
"https://github.com/seandonahoe/ijfw.git/"
|
|
252
|
+
];
|
|
230
253
|
const currentOrigin = (stdout || "").trim();
|
|
231
|
-
if (currentOrigin
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
if (STALE_ORIGINS.includes(currentOrigin)) {
|
|
255
|
+
const setUrl = spawnSync("git", ["-C", dir, "remote", "set-url", "origin", DEFAULT_REPO], { stdio: "inherit" });
|
|
256
|
+
if (setUrl.status !== 0) {
|
|
257
|
+
console.warn(` [!] origin migration failed -- could not repoint ${currentOrigin} to ${DEFAULT_REPO}`);
|
|
258
|
+
} else {
|
|
259
|
+
console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
|
|
260
|
+
}
|
|
234
261
|
}
|
|
235
262
|
const fetch = spawnSync("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
|
|
236
263
|
if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
|
package/dist/uninstall.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/uninstall.js
|
|
4
|
-
import { existsSync as existsSync2, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync2, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, readdirSync } from "node:fs";
|
|
5
5
|
import { resolve as resolve2, join as join2 } from "node:path";
|
|
6
6
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
// src/marketplace.js
|
|
10
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
13
|
import { homedir } from "node:os";
|
|
14
|
+
function atomicWriteJson(path, data) {
|
|
15
|
+
const tmp = `${path}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
16
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
17
|
+
try {
|
|
18
|
+
renameSync(tmp, path);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
try {
|
|
21
|
+
unlinkSync(tmp);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`atomic write failed for ${path}: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
function claudeSettingsPath() {
|
|
14
28
|
return join(homedir(), ".claude", "settings.json");
|
|
15
29
|
}
|
|
@@ -76,13 +90,24 @@ function unmergeMarketplace(settingsPath = claudeSettingsPath()) {
|
|
|
76
90
|
if ("ijfw-core@ijfw" in settings.enabledPlugins) delete settings.enabledPlugins["ijfw-core@ijfw"];
|
|
77
91
|
if ("ijfw@ijfw" in settings.enabledPlugins) delete settings.enabledPlugins["ijfw@ijfw"];
|
|
78
92
|
}
|
|
79
|
-
|
|
80
|
-
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
81
|
-
renameSync(tmp, settingsPath);
|
|
93
|
+
atomicWriteJson(settingsPath, settings);
|
|
82
94
|
return settings;
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
// src/uninstall.js
|
|
98
|
+
function writeAtomic(target, content) {
|
|
99
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
100
|
+
writeFileSync2(tmp, content);
|
|
101
|
+
try {
|
|
102
|
+
renameSync2(tmp, target);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync2(tmp);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`atomic write failed for ${target}: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
86
111
|
function parseArgs(argv) {
|
|
87
112
|
const out = { dir: null, purge: false, noMarketplace: false };
|
|
88
113
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -128,7 +153,7 @@ function removeTomlSection(p) {
|
|
|
128
153
|
if (skip && line.startsWith("[") && !line.startsWith("[mcp_servers.ijfw-memory]")) skip = false;
|
|
129
154
|
if (!skip) out.push(line);
|
|
130
155
|
}
|
|
131
|
-
|
|
156
|
+
writeAtomic(p, out.join("\n") + "\n");
|
|
132
157
|
return true;
|
|
133
158
|
}
|
|
134
159
|
function removeJsonMcpEntry(p) {
|
|
@@ -144,7 +169,7 @@ function removeJsonMcpEntry(p) {
|
|
|
144
169
|
if (doc.mcpServers && doc.mcpServers["ijfw-memory"]) {
|
|
145
170
|
backupFile(p);
|
|
146
171
|
delete doc.mcpServers["ijfw-memory"];
|
|
147
|
-
|
|
172
|
+
writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
|
|
148
173
|
changed = true;
|
|
149
174
|
}
|
|
150
175
|
return changed;
|
|
@@ -162,7 +187,7 @@ function removeCodexHooks(p) {
|
|
|
162
187
|
const after = doc.filter((h) => !(h && h._ijfw));
|
|
163
188
|
if (after.length === before) return false;
|
|
164
189
|
backupFile(p);
|
|
165
|
-
|
|
190
|
+
writeAtomic(p, JSON.stringify(after, null, 2) + "\n");
|
|
166
191
|
return true;
|
|
167
192
|
}
|
|
168
193
|
if (!doc || typeof doc !== "object" || !doc.hooks) return false;
|
|
@@ -180,7 +205,7 @@ function removeCodexHooks(p) {
|
|
|
180
205
|
}
|
|
181
206
|
if (!changed) return false;
|
|
182
207
|
backupFile(p);
|
|
183
|
-
|
|
208
|
+
writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
|
|
184
209
|
return true;
|
|
185
210
|
}
|
|
186
211
|
if (Array.isArray(doc.hooks)) {
|
|
@@ -188,7 +213,7 @@ function removeCodexHooks(p) {
|
|
|
188
213
|
doc.hooks = doc.hooks.filter((h) => !(h && h._ijfw));
|
|
189
214
|
if (doc.hooks.length === before) return false;
|
|
190
215
|
backupFile(p);
|
|
191
|
-
|
|
216
|
+
writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
|
|
192
217
|
return true;
|
|
193
218
|
}
|
|
194
219
|
return false;
|
|
@@ -224,7 +249,7 @@ import os; os.replace(p + ".tmp", p)
|
|
|
224
249
|
);
|
|
225
250
|
if (stripped === raw) return false;
|
|
226
251
|
backupFile(p);
|
|
227
|
-
|
|
252
|
+
writeAtomic(p, stripped);
|
|
228
253
|
return true;
|
|
229
254
|
}
|
|
230
255
|
function removeIjfwSkills(dir) {
|
package/package.json
CHANGED
package/src/install.ps1
CHANGED
|
@@ -30,10 +30,17 @@ function Test-Command($cmd) {
|
|
|
30
30
|
|
|
31
31
|
function Get-Target {
|
|
32
32
|
if ($Dir) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# Expand %APPDATA%-style env vars and leading ~ before resolving.
|
|
34
|
+
$path = [System.Environment]::ExpandEnvironmentVariables($Dir)
|
|
35
|
+
if ($path -like '~*') { $path = $path -replace '^~', $env:USERPROFILE }
|
|
36
|
+
$resolved = Resolve-Path -LiteralPath $path -ErrorAction SilentlyContinue
|
|
37
|
+
if ($resolved) { return $resolved.Path } else { return $path }
|
|
38
|
+
}
|
|
39
|
+
if ($env:IJFW_HOME) {
|
|
40
|
+
$path = [System.Environment]::ExpandEnvironmentVariables($env:IJFW_HOME)
|
|
41
|
+
if ($path -like '~*') { $path = $path -replace '^~', $env:USERPROFILE }
|
|
42
|
+
return $path
|
|
35
43
|
}
|
|
36
|
-
if ($env:IJFW_HOME) { return $env:IJFW_HOME }
|
|
37
44
|
return Join-Path $env:USERPROFILE ".ijfw"
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -83,8 +90,15 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
83
90
|
# Upgrade path.
|
|
84
91
|
$hasGit = Test-Path (Join-Path $target ".git")
|
|
85
92
|
if ($hasGit) {
|
|
86
|
-
& git -C $target remote get-url origin 2>$null
|
|
93
|
+
$currentOriginRaw = & git -C $target remote get-url origin 2>$null
|
|
87
94
|
if ($LASTEXITCODE -eq 0) {
|
|
95
|
+
# Self-heal stale origin URLs across host migrations (1.2.9 parity with install.js).
|
|
96
|
+
# Without this, Windows users on the pre-GitLab origin still 404 on every upgrade.
|
|
97
|
+
$currentOrigin = ($currentOriginRaw | Out-String).Trim()
|
|
98
|
+
if ($currentOrigin -and $currentOrigin -ne $DEFAULT_REPO) {
|
|
99
|
+
Write-Host " origin migration: $currentOrigin -> $DEFAULT_REPO"
|
|
100
|
+
& git -C $target remote set-url origin $DEFAULT_REPO
|
|
101
|
+
}
|
|
88
102
|
# fetch + hard checkout avoids ff-only failures from local divergence.
|
|
89
103
|
& git -C $target fetch --depth 1 origin $branch
|
|
90
104
|
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
@@ -95,9 +109,12 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
# Broken repo or no origin: backup user data, re-clone, restore.
|
|
112
|
+
# Rename-Item -NewName requires a leaf name; Move-Item -Destination accepts an
|
|
113
|
+
# absolute path. Using Rename-Item with an absolute path silently lost user data
|
|
114
|
+
# on Windows pre-1.2.9 because PowerShell rejected the call.
|
|
98
115
|
$ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
99
116
|
$backupDir = "$target.bak.$ts"
|
|
100
|
-
|
|
117
|
+
Move-Item -LiteralPath $target -Destination $backupDir -Force
|
|
101
118
|
try {
|
|
102
119
|
& git clone --depth 1 --branch $branch $DEFAULT_REPO $target
|
|
103
120
|
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
@@ -106,14 +123,14 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
106
123
|
if (Test-Path $src) {
|
|
107
124
|
$dst = Join-Path $target $item
|
|
108
125
|
if (Test-Path $dst) { Remove-Item -Recurse -Force -LiteralPath $dst }
|
|
109
|
-
Move-Item -LiteralPath $src -Destination $dst
|
|
126
|
+
Move-Item -LiteralPath $src -Destination $dst -Force
|
|
110
127
|
}
|
|
111
128
|
}
|
|
112
129
|
Remove-Item -Recurse -Force -LiteralPath $backupDir
|
|
113
130
|
return "updated"
|
|
114
131
|
} catch {
|
|
115
132
|
if (Test-Path $target) { Remove-Item -Recurse -Force -LiteralPath $target }
|
|
116
|
-
|
|
133
|
+
Move-Item -LiteralPath $backupDir -Destination $target -Force
|
|
117
134
|
throw
|
|
118
135
|
}
|
|
119
136
|
}
|
|
@@ -123,6 +140,13 @@ function Invoke-InstallScript($target) {
|
|
|
123
140
|
if (-not (Test-Path $script)) { throw "The installer script is not at $script yet. Run the full install from a fresh clone." }
|
|
124
141
|
$gitBash = Resolve-GitBash
|
|
125
142
|
if (-not $gitBash) { throw "IJFW needs Git Bash to complete setup. Install Git for Windows (includes bash.exe) and rerun." }
|
|
143
|
+
# Propagate IJFW_HOME into the bash sub-call so a custom -Dir target lands
|
|
144
|
+
# platform configs / sentinels under the user's chosen tree instead of the
|
|
145
|
+
# default ~/.ijfw. Without this, .\install.ps1 -Dir D:\custom would scribble
|
|
146
|
+
# MCP entries into the user's real ~/.codex / ~/.gemini / ~/.claude pointing
|
|
147
|
+
# at the scratch dir.
|
|
148
|
+
$priorIjfwHome = $env:IJFW_HOME
|
|
149
|
+
$env:IJFW_HOME = $target
|
|
126
150
|
Push-Location $target
|
|
127
151
|
try {
|
|
128
152
|
$env:IJFW_NONINTERACTIVE = if ($env:CI -or $Yes) { "1" } else { "" }
|
|
@@ -134,6 +158,7 @@ function Invoke-InstallScript($target) {
|
|
|
134
158
|
} finally {
|
|
135
159
|
Pop-Location
|
|
136
160
|
Remove-Item Env:\IJFW_SKIP_CLOSER -ErrorAction SilentlyContinue
|
|
161
|
+
if ($priorIjfwHome) { $env:IJFW_HOME = $priorIjfwHome } else { Remove-Item Env:\IJFW_HOME -ErrorAction SilentlyContinue }
|
|
137
162
|
}
|
|
138
163
|
}
|
|
139
164
|
|
|
@@ -253,7 +278,29 @@ function Provision-Plugin {
|
|
|
253
278
|
return
|
|
254
279
|
}
|
|
255
280
|
New-Item -ItemType Directory -Force -Path $Dst | Out-Null
|
|
256
|
-
|
|
281
|
+
foreach ($srcItem in (Get-ChildItem -LiteralPath $srcPath -Recurse -File)) {
|
|
282
|
+
$rel = $srcItem.FullName.Substring($srcPath.Length).TrimStart('\','/')
|
|
283
|
+
$dstItem = Join-Path $Dst $rel
|
|
284
|
+
$dstDir = Split-Path -Parent $dstItem
|
|
285
|
+
if (-not (Test-Path $dstDir)) { New-Item -ItemType Directory -Force -Path $dstDir | Out-Null }
|
|
286
|
+
$srcMtime = $null
|
|
287
|
+
$dstMtime = $null
|
|
288
|
+
try {
|
|
289
|
+
if (Test-Path $srcItem.FullName) { $srcMtime = (Get-Item $srcItem.FullName -ErrorAction Stop).LastWriteTime }
|
|
290
|
+
if (Test-Path $dstItem) { $dstMtime = (Get-Item $dstItem -ErrorAction Stop).LastWriteTime }
|
|
291
|
+
} catch {
|
|
292
|
+
# File watcher race / lock / OneDrive offline -- treat as new install
|
|
293
|
+
$dstMtime = $null
|
|
294
|
+
}
|
|
295
|
+
if ($null -ne $dstMtime -and $null -ne $srcMtime -and $dstMtime -gt $srcMtime) {
|
|
296
|
+
# User has modified the destination; back it up before overwriting.
|
|
297
|
+
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
298
|
+
Copy-Item $dstItem "$dstItem.user-bak.$ts" -Force -ErrorAction SilentlyContinue
|
|
299
|
+
}
|
|
300
|
+
Copy-Item $srcItem.FullName $dstItem -Force
|
|
301
|
+
# Preserve source mtime so next install doesn't mistake our copy for a user edit.
|
|
302
|
+
try { (Get-Item $dstItem -ErrorAction Stop).LastWriteTime = $srcItem.LastWriteTime } catch {}
|
|
303
|
+
}
|
|
257
304
|
}
|
|
258
305
|
|
|
259
306
|
function Merge-PluginsEnabled {
|
|
@@ -280,6 +327,9 @@ function Merge-PluginsEnabled {
|
|
|
280
327
|
# Strategy: try python3 first (has real YAML support); fall back to sentinel-anchored
|
|
281
328
|
# regex that appends to a plugins.enabled: block or creates one.
|
|
282
329
|
|
|
330
|
+
# Sentinel: only run the regex fallback when Python was unavailable or failed.
|
|
331
|
+
$pythonAllSucceeded = $false
|
|
332
|
+
|
|
283
333
|
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
284
334
|
if ($python) {
|
|
285
335
|
$pyScript = @"
|
|
@@ -330,14 +380,15 @@ sys.exit(0)
|
|
|
330
380
|
[System.IO.File]::WriteAllText($tmp, $pyScript)
|
|
331
381
|
try {
|
|
332
382
|
& python3 $tmp $ConfigPath $pluginName
|
|
333
|
-
$
|
|
383
|
+
$pythonAllSucceeded = ($LASTEXITCODE -eq 0)
|
|
334
384
|
} finally {
|
|
335
385
|
Remove-Item -LiteralPath $tmp -ErrorAction SilentlyContinue
|
|
336
386
|
}
|
|
337
|
-
if ($
|
|
387
|
+
if ($pythonAllSucceeded) { return $true }
|
|
338
388
|
}
|
|
339
389
|
|
|
340
390
|
# Fallback: pure PowerShell sentinel-anchored regex approach.
|
|
391
|
+
# Only runs when Python was unavailable or exited non-zero.
|
|
341
392
|
# If the file has a plugins.enabled: [...] line, splice in the name.
|
|
342
393
|
# Otherwise append a plugins.enabled block.
|
|
343
394
|
$enabledRe = [regex]'(?m)^([ \t]+enabled\s*:\s*\[)([^\]]*)\]'
|
|
@@ -380,7 +431,10 @@ function Merge-Marketplace {
|
|
|
380
431
|
$raw = Get-Content -Raw -LiteralPath $settingsPath
|
|
381
432
|
$cleaned = ConvertFrom-Jsonc $raw
|
|
382
433
|
try {
|
|
383
|
-
|
|
434
|
+
# -Depth is PS 6+; PS 5.1 ignores it safely via splatting.
|
|
435
|
+
$cfjArgs = @{ InputObject = $cleaned; ErrorAction = 'Stop' }
|
|
436
|
+
if ($PSVersionTable.PSVersion.Major -ge 6) { $cfjArgs['Depth'] = 32 }
|
|
437
|
+
$parsed = ConvertFrom-Json @cfjArgs
|
|
384
438
|
$settings = ConvertTo-Hashtable $parsed
|
|
385
439
|
if ($null -eq $settings) { $settings = @{} }
|
|
386
440
|
} catch {
|