@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/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 (existsSync4(join4(cwd, ".codex", "config.toml")) || existsSync4(join4(cwd, ".ijfw"))) {
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 dst = join4(ctx.home, ".wayland", "config.yaml");
946
- ensureDir(dirname4(dst));
947
- mergeYamlMcp(dst, ctx.serverJsNative);
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
- const pluginSrc = join4(ctx.repoRoot, "wayland", "plugins", "ijfw");
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 dst = path.join(process.cwd(), ".vscode", "mcp.json");
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(process.cwd(), ".github", "copilot-instructions.md");
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 existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3, readdirSync as readdirSync2, cpSync as cpSync2 } from "node:fs";
1974
- import { resolve as resolve4, join as join5, dirname as dirname5 } from "node:path";
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 && existsSync5(gitPath)) {
2435
+ if (gitPath && existsSync6(gitPath)) {
2227
2436
  const gitDir = dirname5(gitPath);
2228
2437
  const gitRoot = dirname5(gitDir);
2229
2438
  const candidates = [
2230
- join5(gitDir, "bash.exe"),
2231
- join5(gitRoot, "bin", "bash.exe"),
2232
- join5(gitRoot, "usr", "bin", "bash.exe")
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 (existsSync5(c)) return c;
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 (existsSync5(c)) return c;
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 join5(homedir3(), ".ijfw");
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 (existsSync5(dir)) {
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 (!existsSync5(dir)) {
2265
- mkdirSync4(dir, { recursive: true });
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 = existsSync5(join5(dir, ".git"));
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 = join5(backupDir, item);
2328
- if (existsSync5(src)) {
2329
- const dst = join5(dir, item);
2330
- if (existsSync5(dst)) rmSync2(dst, { recursive: true, force: true });
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 = readdirSync2(backupDir);
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 (existsSync5(dir)) rmSync2(dir, { recursive: true, force: true });
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 = join5(homedir3(), ".ijfw");
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
- const createdThisRun = !existsSync5(target);
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 && existsSync5(target)) {
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 = join5(homedir3(), ".ijfw");
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();