@ijfw/install 1.2.9 → 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 +28 -28
- package/dist/install.js +38 -11
- package/dist/uninstall.js +36 -11
- package/package.json +1 -1
- package/src/install.ps1 +43 -7
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() {
|
|
@@ -2519,7 +2516,7 @@ async function main() {
|
|
|
2519
2516
|
const ijfwHome = join9(homedir(), ".ijfw");
|
|
2520
2517
|
const findInTree = (...rel) => {
|
|
2521
2518
|
const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
|
|
2522
|
-
return candidates.find((p) =>
|
|
2519
|
+
return candidates.find((p) => existsSync4(p)) || null;
|
|
2523
2520
|
};
|
|
2524
2521
|
if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
|
|
2525
2522
|
const dashBin = findInTree("mcp-server", "bin", "ijfw-dashboard");
|
|
@@ -2530,7 +2527,7 @@ async function main() {
|
|
|
2530
2527
|
const serverJs = findInTree("mcp-server", "src", "dashboard-server.js");
|
|
2531
2528
|
if (dashSub === "start" && serverJs) {
|
|
2532
2529
|
const { spawn } = await import("node:child_process");
|
|
2533
|
-
const child = spawn(process.execPath, [serverJs, "--daemon"], {
|
|
2530
|
+
const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
|
|
2534
2531
|
detached: true,
|
|
2535
2532
|
stdio: "ignore"
|
|
2536
2533
|
});
|
|
@@ -2567,7 +2564,7 @@ async function main() {
|
|
|
2567
2564
|
process.exit(1);
|
|
2568
2565
|
}
|
|
2569
2566
|
const abs = resolve3(filePath);
|
|
2570
|
-
if (!
|
|
2567
|
+
if (!existsSync4(abs)) {
|
|
2571
2568
|
console.error(`File not found: ${abs}`);
|
|
2572
2569
|
process.exit(1);
|
|
2573
2570
|
}
|
|
@@ -2593,7 +2590,7 @@ async function main() {
|
|
|
2593
2590
|
resolve3(__dirname2, "..", "docs", "GUIDE.md"),
|
|
2594
2591
|
join9(homedir(), ".ijfw", "docs", "GUIDE.md")
|
|
2595
2592
|
];
|
|
2596
|
-
const guidePath = candidates.find((p) =>
|
|
2593
|
+
const guidePath = candidates.find((p) => existsSync4(p));
|
|
2597
2594
|
if (!guidePath) {
|
|
2598
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");
|
|
2599
2596
|
process.exit(1);
|
|
@@ -2603,7 +2600,7 @@ async function main() {
|
|
|
2603
2600
|
const assetsSrc = join9(dirname2(guidePath), "guide", "assets");
|
|
2604
2601
|
const outDir = join9(homedir(), ".ijfw", "guide");
|
|
2605
2602
|
mkdirSync3(join9(outDir, "assets"), { recursive: true });
|
|
2606
|
-
if (
|
|
2603
|
+
if (existsSync4(assetsSrc)) {
|
|
2607
2604
|
for (const f of readdirSync4(assetsSrc)) {
|
|
2608
2605
|
copyFileSync(join9(assetsSrc, f), join9(outDir, "assets", f));
|
|
2609
2606
|
}
|
|
@@ -2635,7 +2632,10 @@ async function main() {
|
|
|
2635
2632
|
}
|
|
2636
2633
|
const hasLess = spawnSync12("less", ["-V"], { stdio: "ignore" }).status === 0;
|
|
2637
2634
|
if (hasLess) {
|
|
2638
|
-
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
|
+
}
|
|
2639
2639
|
} else {
|
|
2640
2640
|
process.stdout.write(readFileSync3(guidePath, "utf8"));
|
|
2641
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
|
|
|
@@ -271,7 +278,29 @@ function Provision-Plugin {
|
|
|
271
278
|
return
|
|
272
279
|
}
|
|
273
280
|
New-Item -ItemType Directory -Force -Path $Dst | Out-Null
|
|
274
|
-
|
|
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
|
+
}
|
|
275
304
|
}
|
|
276
305
|
|
|
277
306
|
function Merge-PluginsEnabled {
|
|
@@ -298,6 +327,9 @@ function Merge-PluginsEnabled {
|
|
|
298
327
|
# Strategy: try python3 first (has real YAML support); fall back to sentinel-anchored
|
|
299
328
|
# regex that appends to a plugins.enabled: block or creates one.
|
|
300
329
|
|
|
330
|
+
# Sentinel: only run the regex fallback when Python was unavailable or failed.
|
|
331
|
+
$pythonAllSucceeded = $false
|
|
332
|
+
|
|
301
333
|
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
302
334
|
if ($python) {
|
|
303
335
|
$pyScript = @"
|
|
@@ -348,14 +380,15 @@ sys.exit(0)
|
|
|
348
380
|
[System.IO.File]::WriteAllText($tmp, $pyScript)
|
|
349
381
|
try {
|
|
350
382
|
& python3 $tmp $ConfigPath $pluginName
|
|
351
|
-
$
|
|
383
|
+
$pythonAllSucceeded = ($LASTEXITCODE -eq 0)
|
|
352
384
|
} finally {
|
|
353
385
|
Remove-Item -LiteralPath $tmp -ErrorAction SilentlyContinue
|
|
354
386
|
}
|
|
355
|
-
if ($
|
|
387
|
+
if ($pythonAllSucceeded) { return $true }
|
|
356
388
|
}
|
|
357
389
|
|
|
358
390
|
# Fallback: pure PowerShell sentinel-anchored regex approach.
|
|
391
|
+
# Only runs when Python was unavailable or exited non-zero.
|
|
359
392
|
# If the file has a plugins.enabled: [...] line, splice in the name.
|
|
360
393
|
# Otherwise append a plugins.enabled block.
|
|
361
394
|
$enabledRe = [regex]'(?m)^([ \t]+enabled\s*:\s*\[)([^\]]*)\]'
|
|
@@ -398,7 +431,10 @@ function Merge-Marketplace {
|
|
|
398
431
|
$raw = Get-Content -Raw -LiteralPath $settingsPath
|
|
399
432
|
$cleaned = ConvertFrom-Jsonc $raw
|
|
400
433
|
try {
|
|
401
|
-
|
|
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
|
|
402
438
|
$settings = ConvertTo-Hashtable $parsed
|
|
403
439
|
if ($null -eq $settings) { $settings = @{} }
|
|
404
440
|
} catch {
|