@ijfw/install 1.5.6 → 1.6.1
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 +134 -46
- package/dist/install.js +296 -79
- package/dist/uninstall.js +543 -52
- package/docs/GUIDE.md +20 -1
- package/docs/guide/assets/ferrox-hero.png +0 -0
- package/package.json +5 -2
- package/src/install.ps1 +2 -2
- package/templates/aider/CONVENTIONS.md +54 -0
- package/templates/aider/aider.conf.yml +23 -0
- package/templates/pi/AGENTS.md +55 -0
- package/dist/hub-index-snippet.json +0 -50
package/dist/install.js
CHANGED
|
@@ -61,6 +61,44 @@ function nativePath(p) {
|
|
|
61
61
|
if (p == null) return p;
|
|
62
62
|
return normalize(String(p));
|
|
63
63
|
}
|
|
64
|
+
function isProjectWritable(cwd, home) {
|
|
65
|
+
if (!cwd || typeof cwd !== "string") return false;
|
|
66
|
+
let candReal;
|
|
67
|
+
try {
|
|
68
|
+
candReal = realpathSync(cwd);
|
|
69
|
+
} catch {
|
|
70
|
+
try {
|
|
71
|
+
candReal = resolve3(cwd);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!candReal || candReal === "/" || candReal === sep) return false;
|
|
77
|
+
const rawHome = home && typeof home === "string" && home.length > 0 ? home : homedir2();
|
|
78
|
+
if (!rawHome) return false;
|
|
79
|
+
let homeRealPath;
|
|
80
|
+
try {
|
|
81
|
+
homeRealPath = realpathSync(rawHome);
|
|
82
|
+
} catch {
|
|
83
|
+
try {
|
|
84
|
+
homeRealPath = resolve3(rawHome);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!homeRealPath) return false;
|
|
90
|
+
if (candReal === homeRealPath) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function guardProjectWrite(cwd, home, opts = {}) {
|
|
94
|
+
if (isProjectWritable(cwd, home)) return true;
|
|
95
|
+
const label = opts.platformLabel || "project rules";
|
|
96
|
+
const info = opts.log && typeof opts.log.info === "function" ? opts.log.info : printInfo;
|
|
97
|
+
info(
|
|
98
|
+
`Run \`ijfw install\` from a project directory to install ${label}; skipped (cwd is your home directory).`
|
|
99
|
+
);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
64
102
|
function writeAtomic(path3, contents, opts = {}) {
|
|
65
103
|
const mode = opts.mode ?? 384;
|
|
66
104
|
mkdirSync2(dirname3(path3), { recursive: true });
|
|
@@ -847,7 +885,10 @@ async function installCodex(ctx) {
|
|
|
847
885
|
copyIfAbsent(f.path, join4(userCommands, f.name));
|
|
848
886
|
}
|
|
849
887
|
const cwd = ctx.cwd || process.cwd();
|
|
850
|
-
if (
|
|
888
|
+
if (guardProjectWrite(cwd, ctx.home, {
|
|
889
|
+
platformLabel: "Codex project skills/commands",
|
|
890
|
+
log: ctx.log
|
|
891
|
+
}) && (existsSync4(join4(cwd, ".codex", "config.toml")) || existsSync4(join4(cwd, ".ijfw")))) {
|
|
851
892
|
const projSkills = join4(cwd, ".codex", "skills");
|
|
852
893
|
ensureDir(projSkills);
|
|
853
894
|
for (const sd of listSubdirs(repoSkills)) {
|
|
@@ -942,9 +983,9 @@ async function installWayland(ctx) {
|
|
|
942
983
|
"Custom-dir install -- skipping ~/.wayland/ merges."
|
|
943
984
|
);
|
|
944
985
|
}
|
|
945
|
-
const
|
|
946
|
-
ensureDir(dirname4(
|
|
947
|
-
|
|
986
|
+
const pluginToml = join4(ctx.home, ".wayland", "plugins", "ijfw", "plugin.toml");
|
|
987
|
+
ensureDir(dirname4(pluginToml));
|
|
988
|
+
writeFileSync3(pluginToml, renderWaylandPluginToml(ctx), { encoding: "utf8" });
|
|
948
989
|
ensureDir(join4(ctx.home, ".wayland"));
|
|
949
990
|
copyIfAbsent(
|
|
950
991
|
join4(ctx.repoRoot, "wayland", "WAYLAND.md"),
|
|
@@ -955,56 +996,61 @@ async function installWayland(ctx) {
|
|
|
955
996
|
for (const sd of listSubdirs(sharedSkills)) {
|
|
956
997
|
copyDirIfAbsent(sd.path, join4(ctx.home, ".wayland", "skills", sd.name));
|
|
957
998
|
}
|
|
958
|
-
|
|
959
|
-
if (existsSync4(pluginSrc)) {
|
|
960
|
-
const pluginDst = join4(ctx.home, ".wayland", "plugins", "ijfw");
|
|
961
|
-
ensureDir(pluginDst);
|
|
962
|
-
let entries;
|
|
963
|
-
let readdirErr = null;
|
|
964
|
-
try {
|
|
965
|
-
entries = readdirSync(pluginSrc);
|
|
966
|
-
} catch (err) {
|
|
967
|
-
entries = [];
|
|
968
|
-
readdirErr = err;
|
|
969
|
-
}
|
|
970
|
-
if (readdirErr) {
|
|
971
|
-
ctx.log.warn(`Wayland plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
|
|
972
|
-
}
|
|
973
|
-
const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
|
|
974
|
-
let dstEntries = [];
|
|
975
|
-
try {
|
|
976
|
-
dstEntries = readdirSync(pluginDst);
|
|
977
|
-
} catch {
|
|
978
|
-
}
|
|
979
|
-
for (const name of dstEntries) {
|
|
980
|
-
if (name === "__pycache__") continue;
|
|
981
|
-
if (!srcNames.has(name)) {
|
|
982
|
-
try {
|
|
983
|
-
rmSync(join4(pluginDst, name), { recursive: true, force: true });
|
|
984
|
-
} catch (err) {
|
|
985
|
-
ctx.log.warn(`Wayland plugin: could not remove stale ${name}: ${err.message || err}`);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
for (const name of entries) {
|
|
990
|
-
if (name === "__pycache__") continue;
|
|
991
|
-
const src = join4(pluginSrc, name);
|
|
992
|
-
const dstEntry = join4(pluginDst, name);
|
|
993
|
-
try {
|
|
994
|
-
const st = statSync2(src);
|
|
995
|
-
if (st.isDirectory()) {
|
|
996
|
-
cpSync(src, dstEntry, { recursive: true, force: true });
|
|
997
|
-
} else if (st.isFile()) {
|
|
998
|
-
copyFileSync2(src, dstEntry);
|
|
999
|
-
}
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py", ctx.ts);
|
|
1005
|
-
ctx.log.ok("Installed Wayland bundle: MCP + WAYLAND.md + skills + plugin + tier-2 hook");
|
|
999
|
+
ctx.log.ok("Installed Wayland bundle: declarative plugin.toml + WAYLAND.md + skills");
|
|
1006
1000
|
return { status: "ok" };
|
|
1007
1001
|
}
|
|
1002
|
+
function ijfwVersion(ctx) {
|
|
1003
|
+
try {
|
|
1004
|
+
const pkg = JSON.parse(
|
|
1005
|
+
readFileSync3(join4(ctx.repoRoot, "installer", "package.json"), "utf8")
|
|
1006
|
+
);
|
|
1007
|
+
if (pkg && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
return "0.0.0";
|
|
1011
|
+
}
|
|
1012
|
+
function tomlBasicString(value) {
|
|
1013
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1014
|
+
}
|
|
1015
|
+
function renderWaylandPluginToml(ctx) {
|
|
1016
|
+
const serverJs = tomlBasicString(ctx.serverJsNative);
|
|
1017
|
+
const version = tomlBasicString(ijfwVersion(ctx));
|
|
1018
|
+
return [
|
|
1019
|
+
"[plugin]",
|
|
1020
|
+
'name = "wayland-ijfw"',
|
|
1021
|
+
`version = "${version}"`,
|
|
1022
|
+
'description = "IJFW memory + lifecycle hooks for Wayland Core"',
|
|
1023
|
+
'license = "MIT"',
|
|
1024
|
+
"",
|
|
1025
|
+
"[permissions]",
|
|
1026
|
+
"register_hooks = true",
|
|
1027
|
+
"register_mcp_server = true",
|
|
1028
|
+
"",
|
|
1029
|
+
"[runtime]",
|
|
1030
|
+
'kind = "declarative"',
|
|
1031
|
+
"",
|
|
1032
|
+
"[mcp_server]",
|
|
1033
|
+
'name = "ijfw-memory"',
|
|
1034
|
+
"",
|
|
1035
|
+
"[mcp_server.transport]",
|
|
1036
|
+
'kind = "stdio"',
|
|
1037
|
+
'command = "node"',
|
|
1038
|
+
`args = ["${serverJs}"]`,
|
|
1039
|
+
"",
|
|
1040
|
+
// Only session_start and pre_prompt dispatch in Wayland today; the
|
|
1041
|
+
// post_tool_use / session_end / pre_compact phases are registered-but-
|
|
1042
|
+
// log-only on the Wayland side, so they are intentionally omitted. Add
|
|
1043
|
+
// them here once Wayland wires those phases.
|
|
1044
|
+
"[[hooks]]",
|
|
1045
|
+
'phase = "session_start"',
|
|
1046
|
+
'tool = "ijfw_memory_prelude"',
|
|
1047
|
+
"",
|
|
1048
|
+
"[[hooks]]",
|
|
1049
|
+
'phase = "pre_prompt"',
|
|
1050
|
+
'tool = "ijfw_memory_recall"',
|
|
1051
|
+
""
|
|
1052
|
+
].join("\n");
|
|
1053
|
+
}
|
|
1008
1054
|
async function installHermes(ctx) {
|
|
1009
1055
|
if (ctx.ijfwCustomDir) {
|
|
1010
1056
|
return customDirNoop(
|
|
@@ -1093,6 +1139,13 @@ async function installCursor(ctx) {
|
|
|
1093
1139
|
);
|
|
1094
1140
|
}
|
|
1095
1141
|
const cwd = ctx.cwd || process.cwd();
|
|
1142
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1143
|
+
platformLabel: "Cursor project rules",
|
|
1144
|
+
log: ctx.log
|
|
1145
|
+
})) {
|
|
1146
|
+
ctx.log.ok("Cursor: real platform config left untouched.");
|
|
1147
|
+
return { status: "noop" };
|
|
1148
|
+
}
|
|
1096
1149
|
const dst = join4(cwd, ".cursor", "mcp.json");
|
|
1097
1150
|
ensureDir(dirname4(dst));
|
|
1098
1151
|
mergeJson(dst, ctx.serverJsNative);
|
|
@@ -1121,6 +1174,13 @@ async function installWindsurf(ctx) {
|
|
|
1121
1174
|
ensureDir(dirname4(dst));
|
|
1122
1175
|
mergeJson(dst, ctx.serverJsNative);
|
|
1123
1176
|
const cwd = ctx.cwd || process.cwd();
|
|
1177
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1178
|
+
platformLabel: "Windsurf project rules (.windsurfrules)",
|
|
1179
|
+
log: ctx.log
|
|
1180
|
+
})) {
|
|
1181
|
+
ctx.log.ok(`Merged MCP into ${dst}`);
|
|
1182
|
+
return { status: "ok" };
|
|
1183
|
+
}
|
|
1124
1184
|
const projectRules = join4(cwd, ".windsurfrules");
|
|
1125
1185
|
const repoRules = join4(ctx.repoRoot, "windsurf", ".windsurfrules");
|
|
1126
1186
|
let installedRules = false;
|
|
@@ -1181,10 +1241,18 @@ function installCopilot(ctx) {
|
|
|
1181
1241
|
printOk("Copilot: source tree left untouched.");
|
|
1182
1242
|
return { status: "noop" };
|
|
1183
1243
|
}
|
|
1184
|
-
const
|
|
1244
|
+
const cwd = ctx.cwd || process.cwd();
|
|
1245
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1246
|
+
platformLabel: "Copilot project rules",
|
|
1247
|
+
log: ctx.log
|
|
1248
|
+
})) {
|
|
1249
|
+
printOk("Copilot: real platform config left untouched.");
|
|
1250
|
+
return { status: "noop" };
|
|
1251
|
+
}
|
|
1252
|
+
const dst = path.join(cwd, ".vscode", "mcp.json");
|
|
1185
1253
|
ensureDir2(path.dirname(dst));
|
|
1186
1254
|
mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1187
|
-
const rulesDst = path.join(
|
|
1255
|
+
const rulesDst = path.join(cwd, ".github", "copilot-instructions.md");
|
|
1188
1256
|
const rulesSrc = path.join(ctx.repoRoot, "copilot", "copilot-instructions.md");
|
|
1189
1257
|
const wroteRules = copyIfMissing(rulesSrc, rulesDst);
|
|
1190
1258
|
if (wroteRules) {
|
|
@@ -1320,6 +1388,142 @@ var init_install_targets_8_14 = __esm({
|
|
|
1320
1388
|
}
|
|
1321
1389
|
});
|
|
1322
1390
|
|
|
1391
|
+
// src/install-ledger.js
|
|
1392
|
+
var install_ledger_exports = {};
|
|
1393
|
+
__export(install_ledger_exports, {
|
|
1394
|
+
INSTALL_PLAN: () => INSTALL_PLAN,
|
|
1395
|
+
PLATFORM_OWNED_DIRS: () => PLATFORM_OWNED_DIRS,
|
|
1396
|
+
allOwnedDirs: () => allOwnedDirs,
|
|
1397
|
+
isEmptyDir: () => isEmptyDir,
|
|
1398
|
+
ledgerPath: () => ledgerPath,
|
|
1399
|
+
readLedger: () => readLedger,
|
|
1400
|
+
renderPlan: () => renderPlan,
|
|
1401
|
+
snapshotPreExistingDirs: () => snapshotPreExistingDirs,
|
|
1402
|
+
writeLedger: () => writeLedger
|
|
1403
|
+
});
|
|
1404
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
1405
|
+
import { join as join5 } from "node:path";
|
|
1406
|
+
function ledgerPath(ijfwHome) {
|
|
1407
|
+
return join5(ijfwHome, "install-ledger.json");
|
|
1408
|
+
}
|
|
1409
|
+
function allOwnedDirs() {
|
|
1410
|
+
const set = /* @__PURE__ */ new Set();
|
|
1411
|
+
for (const dirs of Object.values(PLATFORM_OWNED_DIRS)) {
|
|
1412
|
+
for (const d of dirs) set.add(d);
|
|
1413
|
+
}
|
|
1414
|
+
return [...set];
|
|
1415
|
+
}
|
|
1416
|
+
function snapshotPreExistingDirs(home) {
|
|
1417
|
+
const pre = [];
|
|
1418
|
+
for (const rel of allOwnedDirs()) {
|
|
1419
|
+
if (existsSync5(join5(home, rel))) pre.push(rel);
|
|
1420
|
+
}
|
|
1421
|
+
return pre;
|
|
1422
|
+
}
|
|
1423
|
+
function writeLedger({ home, ijfwHome, preExisting }) {
|
|
1424
|
+
const preSet = new Set(preExisting || []);
|
|
1425
|
+
const created = [];
|
|
1426
|
+
for (const rel of allOwnedDirs()) {
|
|
1427
|
+
if (!preSet.has(rel) && existsSync5(join5(home, rel))) created.push(rel);
|
|
1428
|
+
}
|
|
1429
|
+
const ledger = { version: 1, createdDirs: created };
|
|
1430
|
+
try {
|
|
1431
|
+
mkdirSync4(ijfwHome, { recursive: true, mode: 448 });
|
|
1432
|
+
writeFileSync4(ledgerPath(ijfwHome), JSON.stringify(ledger, null, 2) + "\n", { mode: 384 });
|
|
1433
|
+
} catch {
|
|
1434
|
+
}
|
|
1435
|
+
return ledger;
|
|
1436
|
+
}
|
|
1437
|
+
function readLedger(ijfwHome) {
|
|
1438
|
+
try {
|
|
1439
|
+
const raw = readFileSync4(ledgerPath(ijfwHome), "utf8");
|
|
1440
|
+
const doc = JSON.parse(raw);
|
|
1441
|
+
if (doc && Array.isArray(doc.createdDirs)) return doc;
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
return { version: 1, createdDirs: [] };
|
|
1445
|
+
}
|
|
1446
|
+
function isEmptyDir(p) {
|
|
1447
|
+
try {
|
|
1448
|
+
return existsSync5(p) && readdirSync2(p).length === 0;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function renderPlan(targetList) {
|
|
1454
|
+
const lines = [];
|
|
1455
|
+
lines.push("IJFW install plan (dry run) -- nothing will be written.");
|
|
1456
|
+
lines.push(" legend: [m]=merge into existing file [c]=create [./]=project-scoped");
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
const emit = (title, rows) => {
|
|
1459
|
+
if (!rows || rows.length === 0) return;
|
|
1460
|
+
lines.push(title);
|
|
1461
|
+
for (const [path3, kind, note] of rows) lines.push(` [${kind}] ${path3} -- ${note}`);
|
|
1462
|
+
};
|
|
1463
|
+
emit(" shared:", INSTALL_PLAN.shared);
|
|
1464
|
+
for (const t of targetList) {
|
|
1465
|
+
if (INSTALL_PLAN[t]) emit(` ${t}:`, INSTALL_PLAN[t]);
|
|
1466
|
+
}
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
lines.push(" Run without --dry-run to apply. Use `ijfw-uninstall --purge` to reverse.");
|
|
1469
|
+
return lines.join("\n");
|
|
1470
|
+
}
|
|
1471
|
+
var PLATFORM_OWNED_DIRS, INSTALL_PLAN;
|
|
1472
|
+
var init_install_ledger = __esm({
|
|
1473
|
+
"src/install-ledger.js"() {
|
|
1474
|
+
PLATFORM_OWNED_DIRS = {
|
|
1475
|
+
codex: [".codex"],
|
|
1476
|
+
gemini: [".gemini"],
|
|
1477
|
+
hermes: [".hermes"],
|
|
1478
|
+
wayland: [".wayland"],
|
|
1479
|
+
openclaw: [".openclaw"],
|
|
1480
|
+
qwen: [".qwen"],
|
|
1481
|
+
kimi: [".kimi"],
|
|
1482
|
+
opencode: [".config/opencode"],
|
|
1483
|
+
pi: [".pi"]
|
|
1484
|
+
};
|
|
1485
|
+
INSTALL_PLAN = {
|
|
1486
|
+
shared: [
|
|
1487
|
+
["~/.ijfw/", "c", "IJFW home (server symlink, scripts, index, logs, settings, ledger)"]
|
|
1488
|
+
],
|
|
1489
|
+
claude: [
|
|
1490
|
+
["~/.claude/settings.json", "m", "mcpServers.ijfw-memory + enabledPlugins + marketplace"],
|
|
1491
|
+
["~/.claude/plugins/known_marketplaces.json", "m", "ijfw marketplace entry"]
|
|
1492
|
+
],
|
|
1493
|
+
codex: [
|
|
1494
|
+
["~/.codex/config.toml", "m", "[mcp_servers.ijfw-memory]"],
|
|
1495
|
+
["~/.codex/hooks.json", "m", "IJFW hook entries"],
|
|
1496
|
+
["~/.codex/hooks/*.sh", "c", "lifecycle hook scripts"],
|
|
1497
|
+
["~/.codex/IJFW.md", "c", "context file"],
|
|
1498
|
+
["~/.codex/skills/, commands/", "c", "skills + command aliases"]
|
|
1499
|
+
],
|
|
1500
|
+
gemini: [
|
|
1501
|
+
["~/.gemini/settings.json", "m", "mcpServers.ijfw-memory"],
|
|
1502
|
+
["~/.gemini/extensions/ijfw/", "c", "extension (hooks, skills, commands, agents)"]
|
|
1503
|
+
],
|
|
1504
|
+
hermes: [
|
|
1505
|
+
["~/.hermes/config.yaml", "m", "mcp_servers.ijfw-memory + plugin + hook"],
|
|
1506
|
+
["~/.hermes/HERMES.md, skills/, plugins/ijfw/", "c", "context + skills + plugin tree"]
|
|
1507
|
+
],
|
|
1508
|
+
wayland: [
|
|
1509
|
+
["~/.wayland/plugins/ijfw/", "c", "plugin.toml (MCP + hooks)"],
|
|
1510
|
+
["~/.wayland/WAYLAND.md, skills/", "c", "context + skills"]
|
|
1511
|
+
],
|
|
1512
|
+
openclaw: [["~/.openclaw/openclaw.json", "m", "mcp.servers.ijfw-memory"]],
|
|
1513
|
+
qwen: [["~/.qwen/settings.json", "m", "mcpServers.ijfw-memory"]],
|
|
1514
|
+
kimi: [["~/.kimi/mcp.json", "m", "mcpServers.ijfw-memory"]],
|
|
1515
|
+
opencode: [["~/.config/opencode/opencode.json", "m", "mcp.ijfw-memory"]],
|
|
1516
|
+
cursor: [["./.cursor/mcp.json + rules/ijfw.mdc", "mc", "project-scoped MCP + rule"]],
|
|
1517
|
+
windsurf: [
|
|
1518
|
+
["~/.codeium/windsurf/mcp_config.json", "m", "mcpServers.ijfw-memory"],
|
|
1519
|
+
["./.windsurfrules", "c", "project-scoped rules"]
|
|
1520
|
+
],
|
|
1521
|
+
copilot: [["./.vscode/mcp.json + .github/copilot-instructions.md", "mc", "project-scoped"]],
|
|
1522
|
+
aider: [["~/.aider.conf.yml + ~/CONVENTIONS.md", "c", "home-level rule files"]]
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1323
1527
|
// src/install-flow.js
|
|
1324
1528
|
var install_flow_exports = {};
|
|
1325
1529
|
__export(install_flow_exports, {
|
|
@@ -1859,6 +2063,7 @@ async function runInstall({
|
|
|
1859
2063
|
});
|
|
1860
2064
|
}
|
|
1861
2065
|
pruneBackups({ home });
|
|
2066
|
+
const preExistingDirs = snapshotPreExistingDirs(home);
|
|
1862
2067
|
const live = [];
|
|
1863
2068
|
const standby = [];
|
|
1864
2069
|
const failed = [];
|
|
@@ -1913,6 +2118,7 @@ async function runInstall({
|
|
|
1913
2118
|
standby.push(display);
|
|
1914
2119
|
}
|
|
1915
2120
|
}
|
|
2121
|
+
writeLedger({ home, ijfwHome: resolvedIjfwHome, preExisting: preExistingDirs });
|
|
1916
2122
|
printSummary({
|
|
1917
2123
|
live,
|
|
1918
2124
|
standby,
|
|
@@ -1928,6 +2134,7 @@ var init_install_flow = __esm({
|
|
|
1928
2134
|
init_install_helpers();
|
|
1929
2135
|
init_install_targets_1_7();
|
|
1930
2136
|
init_install_targets_8_14();
|
|
2137
|
+
init_install_ledger();
|
|
1931
2138
|
CANONICAL_ORDER = [
|
|
1932
2139
|
"claude",
|
|
1933
2140
|
"codex",
|
|
@@ -1970,8 +2177,8 @@ var init_install_flow = __esm({
|
|
|
1970
2177
|
|
|
1971
2178
|
// src/install.js
|
|
1972
2179
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1973
|
-
import { existsSync as
|
|
1974
|
-
import { resolve as resolve4, join as
|
|
2180
|
+
import { existsSync as existsSync6, rmSync as rmSync2, mkdirSync as mkdirSync5, realpathSync as realpathSync2, renameSync as renameSync3, readdirSync as readdirSync3, cpSync as cpSync2 } from "node:fs";
|
|
2181
|
+
import { resolve as resolve4, join as join6, dirname as dirname5 } from "node:path";
|
|
1975
2182
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
1976
2183
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1977
2184
|
|
|
@@ -2133,7 +2340,7 @@ function triggerColdScan(projectRoot, options = {}) {
|
|
|
2133
2340
|
var DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git";
|
|
2134
2341
|
var DEFAULT_BRANCH = "main";
|
|
2135
2342
|
function parseArgs(argv) {
|
|
2136
|
-
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false };
|
|
2343
|
+
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false, dryRun: false };
|
|
2137
2344
|
for (let i = 2; i < argv.length; i++) {
|
|
2138
2345
|
const a = argv[i];
|
|
2139
2346
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
@@ -2143,6 +2350,7 @@ function parseArgs(argv) {
|
|
|
2143
2350
|
out.branch = argv[++i];
|
|
2144
2351
|
out.branchExplicit = true;
|
|
2145
2352
|
} else if (a === "--purge") out.purge = true;
|
|
2353
|
+
else if (a === "--dry-run" || a === "--print-plan") out.dryRun = true;
|
|
2146
2354
|
else if (a === "--help" || a === "-h") {
|
|
2147
2355
|
printHelp();
|
|
2148
2356
|
process.exit(0);
|
|
@@ -2189,10 +2397,11 @@ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup, _logger } = {}
|
|
|
2189
2397
|
}
|
|
2190
2398
|
function printHelp() {
|
|
2191
2399
|
console.log(`ijfw-install -- IJFW installer
|
|
2192
|
-
Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--yes]
|
|
2400
|
+
Usage: npx @ijfw/install [--dir <path>] [--branch <name>] [--no-marketplace] [--yes] [--dry-run]
|
|
2193
2401
|
--dir install location (default: $IJFW_HOME or ~/.ijfw)
|
|
2194
2402
|
--branch git branch or tag (default: latest released tag)
|
|
2195
2403
|
--no-marketplace skip merging ~/.claude/settings.json
|
|
2404
|
+
--dry-run print every file/dir the install would touch, write nothing
|
|
2196
2405
|
--yes non-interactive
|
|
2197
2406
|
`);
|
|
2198
2407
|
}
|
|
@@ -2223,15 +2432,15 @@ function findBash() {
|
|
|
2223
2432
|
const whereGit = spawnSync2("where", ["git"], { encoding: "utf8" });
|
|
2224
2433
|
if (whereGit.status === 0) {
|
|
2225
2434
|
const gitPath = (whereGit.stdout || "").split(/\r?\n/)[0].trim();
|
|
2226
|
-
if (gitPath &&
|
|
2435
|
+
if (gitPath && existsSync6(gitPath)) {
|
|
2227
2436
|
const gitDir = dirname5(gitPath);
|
|
2228
2437
|
const gitRoot = dirname5(gitDir);
|
|
2229
2438
|
const candidates = [
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2439
|
+
join6(gitDir, "bash.exe"),
|
|
2440
|
+
join6(gitRoot, "bin", "bash.exe"),
|
|
2441
|
+
join6(gitRoot, "usr", "bin", "bash.exe")
|
|
2233
2442
|
];
|
|
2234
|
-
for (const c of candidates) if (
|
|
2443
|
+
for (const c of candidates) if (existsSync6(c)) return c;
|
|
2235
2444
|
}
|
|
2236
2445
|
}
|
|
2237
2446
|
for (const c of [
|
|
@@ -2239,14 +2448,14 @@ function findBash() {
|
|
|
2239
2448
|
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
2240
2449
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
2241
2450
|
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
2242
|
-
]) if (
|
|
2451
|
+
]) if (existsSync6(c)) return c;
|
|
2243
2452
|
if (hasBin2("bash")) return "bash";
|
|
2244
2453
|
return null;
|
|
2245
2454
|
}
|
|
2246
2455
|
function resolveTarget(opt) {
|
|
2247
2456
|
if (opt.dir) return resolve4(opt.dir);
|
|
2248
2457
|
if (process.env.IJFW_HOME) return resolve4(process.env.IJFW_HOME);
|
|
2249
|
-
return
|
|
2458
|
+
return join6(homedir3(), ".ijfw");
|
|
2250
2459
|
}
|
|
2251
2460
|
function runCheck(cmd, args, opts) {
|
|
2252
2461
|
const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
|
|
@@ -2254,20 +2463,20 @@ function runCheck(cmd, args, opts) {
|
|
|
2254
2463
|
}
|
|
2255
2464
|
function cloneOrPull(dir, branch) {
|
|
2256
2465
|
if (skipNetwork()) {
|
|
2257
|
-
if (
|
|
2466
|
+
if (existsSync6(dir)) {
|
|
2258
2467
|
return "skipped-network";
|
|
2259
2468
|
}
|
|
2260
2469
|
throw new Error(
|
|
2261
2470
|
`IJFW_SKIP_NETWORK=1 set but cloneOrPull needs network: target directory ${dir} does not exist. Pre-seed the directory before setting IJFW_SKIP_NETWORK, or unset the env var.`
|
|
2262
2471
|
);
|
|
2263
2472
|
}
|
|
2264
|
-
if (!
|
|
2265
|
-
|
|
2473
|
+
if (!existsSync6(dir)) {
|
|
2474
|
+
mkdirSync5(dir, { recursive: true });
|
|
2266
2475
|
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
2267
2476
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
2268
2477
|
return "cloned";
|
|
2269
2478
|
}
|
|
2270
|
-
const hasGit =
|
|
2479
|
+
const hasGit = existsSync6(join6(dir, ".git"));
|
|
2271
2480
|
if (hasGit) {
|
|
2272
2481
|
const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
2273
2482
|
if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
|
|
@@ -2324,10 +2533,10 @@ function cloneOrPull(dir, branch) {
|
|
|
2324
2533
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
2325
2534
|
let restoredCount = 0;
|
|
2326
2535
|
for (const item of RESTORE_ALLOWLIST) {
|
|
2327
|
-
const src =
|
|
2328
|
-
if (
|
|
2329
|
-
const dst =
|
|
2330
|
-
if (
|
|
2536
|
+
const src = join6(backupDir, item);
|
|
2537
|
+
if (existsSync6(src)) {
|
|
2538
|
+
const dst = join6(dir, item);
|
|
2539
|
+
if (existsSync6(dst)) rmSync2(dst, { recursive: true, force: true });
|
|
2331
2540
|
try {
|
|
2332
2541
|
cpSync2(src, dst, { recursive: true, dereference: false });
|
|
2333
2542
|
rmSync2(src, { recursive: true, force: true });
|
|
@@ -2342,7 +2551,7 @@ function cloneOrPull(dir, branch) {
|
|
|
2342
2551
|
}
|
|
2343
2552
|
let backupResidual = [];
|
|
2344
2553
|
try {
|
|
2345
|
-
backupResidual =
|
|
2554
|
+
backupResidual = readdirSync3(backupDir);
|
|
2346
2555
|
} catch {
|
|
2347
2556
|
}
|
|
2348
2557
|
if (backupResidual.length === 0) {
|
|
@@ -2354,13 +2563,13 @@ function cloneOrPull(dir, branch) {
|
|
|
2354
2563
|
}
|
|
2355
2564
|
return "updated";
|
|
2356
2565
|
} catch (err) {
|
|
2357
|
-
if (
|
|
2566
|
+
if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
|
|
2358
2567
|
renameSync3(backupDir, dir);
|
|
2359
2568
|
throw err;
|
|
2360
2569
|
}
|
|
2361
2570
|
}
|
|
2362
2571
|
async function runInstallScript(dir) {
|
|
2363
|
-
const canonicalDir =
|
|
2572
|
+
const canonicalDir = join6(homedir3(), ".ijfw");
|
|
2364
2573
|
const isCustomDir = resolve4(dir) !== canonicalDir;
|
|
2365
2574
|
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
|
|
2366
2575
|
await runInstall2({
|
|
@@ -2381,9 +2590,17 @@ async function main() {
|
|
|
2381
2590
|
process.exit(1);
|
|
2382
2591
|
}
|
|
2383
2592
|
const target = resolveTarget(opts);
|
|
2384
|
-
|
|
2593
|
+
if (opts.dryRun) {
|
|
2594
|
+
const { CANONICAL_ORDER: CANONICAL_ORDER2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
|
|
2595
|
+
const { renderPlan: renderPlan2 } = await Promise.resolve().then(() => (init_install_ledger(), install_ledger_exports));
|
|
2596
|
+
console.log(`IJFW install target: ${target}`);
|
|
2597
|
+
console.log("");
|
|
2598
|
+
console.log(renderPlan2(CANONICAL_ORDER2));
|
|
2599
|
+
process.exit(0);
|
|
2600
|
+
}
|
|
2601
|
+
const createdThisRun = !existsSync6(target);
|
|
2385
2602
|
const sigint = () => {
|
|
2386
|
-
if (createdThisRun &&
|
|
2603
|
+
if (createdThisRun && existsSync6(target)) {
|
|
2387
2604
|
try {
|
|
2388
2605
|
rmSync2(target, { recursive: true, force: true });
|
|
2389
2606
|
} catch (err) {
|
|
@@ -2404,7 +2621,7 @@ async function main() {
|
|
|
2404
2621
|
console.log(` repo ${action}`);
|
|
2405
2622
|
await runInstallScript(target);
|
|
2406
2623
|
console.log(" platform configs applied");
|
|
2407
|
-
const canonicalDir =
|
|
2624
|
+
const canonicalDir = join6(homedir3(), ".ijfw");
|
|
2408
2625
|
const isCustomDir = process.env.IJFW_CUSTOM_DIR === "1" || resolve4(target) !== canonicalDir;
|
|
2409
2626
|
if (!opts.noMarketplace && !isCustomDir) {
|
|
2410
2627
|
const settingsPath = claudeSettingsPath();
|