@ijfw/install 1.6.0 → 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 CHANGED
@@ -3769,6 +3769,16 @@ var COMMAND_REGISTRY = Object.freeze([
3769
3769
  status: "active",
3770
3770
  helpGroup: "GET STARTED"
3771
3771
  },
3772
+ {
3773
+ name: "init",
3774
+ tier: "primary",
3775
+ owner: "orchestrator",
3776
+ description: "Approve the current folder for codebase indexing",
3777
+ aliases: [],
3778
+ since: "1.6.1",
3779
+ status: "active",
3780
+ helpGroup: "GET STARTED"
3781
+ },
3772
3782
  {
3773
3783
  name: "update",
3774
3784
  tier: "primary",
package/dist/install.js CHANGED
@@ -1388,6 +1388,142 @@ var init_install_targets_8_14 = __esm({
1388
1388
  }
1389
1389
  });
1390
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
+
1391
1527
  // src/install-flow.js
1392
1528
  var install_flow_exports = {};
1393
1529
  __export(install_flow_exports, {
@@ -1927,6 +2063,7 @@ async function runInstall({
1927
2063
  });
1928
2064
  }
1929
2065
  pruneBackups({ home });
2066
+ const preExistingDirs = snapshotPreExistingDirs(home);
1930
2067
  const live = [];
1931
2068
  const standby = [];
1932
2069
  const failed = [];
@@ -1981,6 +2118,7 @@ async function runInstall({
1981
2118
  standby.push(display);
1982
2119
  }
1983
2120
  }
2121
+ writeLedger({ home, ijfwHome: resolvedIjfwHome, preExisting: preExistingDirs });
1984
2122
  printSummary({
1985
2123
  live,
1986
2124
  standby,
@@ -1996,6 +2134,7 @@ var init_install_flow = __esm({
1996
2134
  init_install_helpers();
1997
2135
  init_install_targets_1_7();
1998
2136
  init_install_targets_8_14();
2137
+ init_install_ledger();
1999
2138
  CANONICAL_ORDER = [
2000
2139
  "claude",
2001
2140
  "codex",
@@ -2038,8 +2177,8 @@ var init_install_flow = __esm({
2038
2177
 
2039
2178
  // src/install.js
2040
2179
  import { spawnSync as spawnSync2 } from "node:child_process";
2041
- 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";
2042
- 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";
2043
2182
  import { homedir as homedir3, platform as platform2 } from "node:os";
2044
2183
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2045
2184
 
@@ -2201,7 +2340,7 @@ function triggerColdScan(projectRoot, options = {}) {
2201
2340
  var DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git";
2202
2341
  var DEFAULT_BRANCH = "main";
2203
2342
  function parseArgs(argv) {
2204
- 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 };
2205
2344
  for (let i = 2; i < argv.length; i++) {
2206
2345
  const a = argv[i];
2207
2346
  if (a === "--yes" || a === "-y") out.yes = true;
@@ -2211,6 +2350,7 @@ function parseArgs(argv) {
2211
2350
  out.branch = argv[++i];
2212
2351
  out.branchExplicit = true;
2213
2352
  } else if (a === "--purge") out.purge = true;
2353
+ else if (a === "--dry-run" || a === "--print-plan") out.dryRun = true;
2214
2354
  else if (a === "--help" || a === "-h") {
2215
2355
  printHelp();
2216
2356
  process.exit(0);
@@ -2257,10 +2397,11 @@ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup, _logger } = {}
2257
2397
  }
2258
2398
  function printHelp() {
2259
2399
  console.log(`ijfw-install -- IJFW installer
2260
- 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]
2261
2401
  --dir install location (default: $IJFW_HOME or ~/.ijfw)
2262
2402
  --branch git branch or tag (default: latest released tag)
2263
2403
  --no-marketplace skip merging ~/.claude/settings.json
2404
+ --dry-run print every file/dir the install would touch, write nothing
2264
2405
  --yes non-interactive
2265
2406
  `);
2266
2407
  }
@@ -2291,15 +2432,15 @@ function findBash() {
2291
2432
  const whereGit = spawnSync2("where", ["git"], { encoding: "utf8" });
2292
2433
  if (whereGit.status === 0) {
2293
2434
  const gitPath = (whereGit.stdout || "").split(/\r?\n/)[0].trim();
2294
- if (gitPath && existsSync5(gitPath)) {
2435
+ if (gitPath && existsSync6(gitPath)) {
2295
2436
  const gitDir = dirname5(gitPath);
2296
2437
  const gitRoot = dirname5(gitDir);
2297
2438
  const candidates = [
2298
- join5(gitDir, "bash.exe"),
2299
- join5(gitRoot, "bin", "bash.exe"),
2300
- join5(gitRoot, "usr", "bin", "bash.exe")
2439
+ join6(gitDir, "bash.exe"),
2440
+ join6(gitRoot, "bin", "bash.exe"),
2441
+ join6(gitRoot, "usr", "bin", "bash.exe")
2301
2442
  ];
2302
- for (const c of candidates) if (existsSync5(c)) return c;
2443
+ for (const c of candidates) if (existsSync6(c)) return c;
2303
2444
  }
2304
2445
  }
2305
2446
  for (const c of [
@@ -2307,14 +2448,14 @@ function findBash() {
2307
2448
  "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
2308
2449
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
2309
2450
  "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
2310
- ]) if (existsSync5(c)) return c;
2451
+ ]) if (existsSync6(c)) return c;
2311
2452
  if (hasBin2("bash")) return "bash";
2312
2453
  return null;
2313
2454
  }
2314
2455
  function resolveTarget(opt) {
2315
2456
  if (opt.dir) return resolve4(opt.dir);
2316
2457
  if (process.env.IJFW_HOME) return resolve4(process.env.IJFW_HOME);
2317
- return join5(homedir3(), ".ijfw");
2458
+ return join6(homedir3(), ".ijfw");
2318
2459
  }
2319
2460
  function runCheck(cmd, args, opts) {
2320
2461
  const r = spawnSync2(cmd, args, { encoding: "utf8", ...opts });
@@ -2322,20 +2463,20 @@ function runCheck(cmd, args, opts) {
2322
2463
  }
2323
2464
  function cloneOrPull(dir, branch) {
2324
2465
  if (skipNetwork()) {
2325
- if (existsSync5(dir)) {
2466
+ if (existsSync6(dir)) {
2326
2467
  return "skipped-network";
2327
2468
  }
2328
2469
  throw new Error(
2329
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.`
2330
2471
  );
2331
2472
  }
2332
- if (!existsSync5(dir)) {
2333
- mkdirSync4(dir, { recursive: true });
2473
+ if (!existsSync6(dir)) {
2474
+ mkdirSync5(dir, { recursive: true });
2334
2475
  const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2335
2476
  if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2336
2477
  return "cloned";
2337
2478
  }
2338
- const hasGit = existsSync5(join5(dir, ".git"));
2479
+ const hasGit = existsSync6(join6(dir, ".git"));
2339
2480
  if (hasGit) {
2340
2481
  const { status: remoteStatus, stdout, stderr: remoteStderr, spawnError: remoteSpawnError, signal: remoteSignal } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
2341
2482
  if (remoteSpawnError) console.warn(` git spawn error (${remoteSpawnError}) -- check git is on PATH`);
@@ -2392,10 +2533,10 @@ function cloneOrPull(dir, branch) {
2392
2533
  if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2393
2534
  let restoredCount = 0;
2394
2535
  for (const item of RESTORE_ALLOWLIST) {
2395
- const src = join5(backupDir, item);
2396
- if (existsSync5(src)) {
2397
- const dst = join5(dir, item);
2398
- 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 });
2399
2540
  try {
2400
2541
  cpSync2(src, dst, { recursive: true, dereference: false });
2401
2542
  rmSync2(src, { recursive: true, force: true });
@@ -2410,7 +2551,7 @@ function cloneOrPull(dir, branch) {
2410
2551
  }
2411
2552
  let backupResidual = [];
2412
2553
  try {
2413
- backupResidual = readdirSync2(backupDir);
2554
+ backupResidual = readdirSync3(backupDir);
2414
2555
  } catch {
2415
2556
  }
2416
2557
  if (backupResidual.length === 0) {
@@ -2422,13 +2563,13 @@ function cloneOrPull(dir, branch) {
2422
2563
  }
2423
2564
  return "updated";
2424
2565
  } catch (err) {
2425
- if (existsSync5(dir)) rmSync2(dir, { recursive: true, force: true });
2566
+ if (existsSync6(dir)) rmSync2(dir, { recursive: true, force: true });
2426
2567
  renameSync3(backupDir, dir);
2427
2568
  throw err;
2428
2569
  }
2429
2570
  }
2430
2571
  async function runInstallScript(dir) {
2431
- const canonicalDir = join5(homedir3(), ".ijfw");
2572
+ const canonicalDir = join6(homedir3(), ".ijfw");
2432
2573
  const isCustomDir = resolve4(dir) !== canonicalDir;
2433
2574
  const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install_flow(), install_flow_exports));
2434
2575
  await runInstall2({
@@ -2449,9 +2590,17 @@ async function main() {
2449
2590
  process.exit(1);
2450
2591
  }
2451
2592
  const target = resolveTarget(opts);
2452
- 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);
2453
2602
  const sigint = () => {
2454
- if (createdThisRun && existsSync5(target)) {
2603
+ if (createdThisRun && existsSync6(target)) {
2455
2604
  try {
2456
2605
  rmSync2(target, { recursive: true, force: true });
2457
2606
  } catch (err) {
@@ -2472,7 +2621,7 @@ async function main() {
2472
2621
  console.log(` repo ${action}`);
2473
2622
  await runInstallScript(target);
2474
2623
  console.log(" platform configs applied");
2475
- const canonicalDir = join5(homedir3(), ".ijfw");
2624
+ const canonicalDir = join6(homedir3(), ".ijfw");
2476
2625
  const isCustomDir = process.env.IJFW_CUSTOM_DIR === "1" || resolve4(target) !== canonicalDir;
2477
2626
  if (!opts.noMarketplace && !isCustomDir) {
2478
2627
  const settingsPath = claudeSettingsPath();
package/dist/uninstall.js CHANGED
@@ -1,8 +1,8 @@
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, renameSync as renameSync2, unlinkSync as unlinkSync2, readdirSync } from "node:fs";
5
- import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
4
+ import { existsSync as existsSync3, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync2, readdirSync as readdirSync2, realpathSync } from "node:fs";
5
+ import { resolve as resolve2, join as join3, dirname as dirname2, basename } from "node:path";
6
6
  import { homedir as homedir2, tmpdir } from "node:os";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -95,6 +95,29 @@ function unmergeMarketplace(settingsPath = claudeSettingsPath()) {
95
95
  return settings;
96
96
  }
97
97
 
98
+ // src/install-ledger.js
99
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync } from "node:fs";
100
+ import { join as join2 } from "node:path";
101
+ function ledgerPath(ijfwHome) {
102
+ return join2(ijfwHome, "install-ledger.json");
103
+ }
104
+ function readLedger(ijfwHome) {
105
+ try {
106
+ const raw = readFileSync2(ledgerPath(ijfwHome), "utf8");
107
+ const doc = JSON.parse(raw);
108
+ if (doc && Array.isArray(doc.createdDirs)) return doc;
109
+ } catch {
110
+ }
111
+ return { version: 1, createdDirs: [] };
112
+ }
113
+ function isEmptyDir(p) {
114
+ try {
115
+ return existsSync2(p) && readdirSync(p).length === 0;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
98
121
  // src/uninstall.js
99
122
  var __filename = fileURLToPath(import.meta.url);
100
123
  var __dirname = dirname2(__filename);
@@ -103,9 +126,9 @@ function resolveAiderTemplate(name, repoRoot) {
103
126
  const root = repoRoot || REPO_ROOT;
104
127
  const candidates = [
105
128
  // (a) git clone: top-level aider/ under the (injected) repo root.
106
- join2(root, "aider", name),
129
+ join3(root, "aider", name),
107
130
  // (a') repo root with staged templates under installer/.
108
- join2(root, "installer", "templates", "aider", name),
131
+ join3(root, "installer", "templates", "aider", name),
109
132
  // (b) tarball/dist fallback: templates staged next to the package root.
110
133
  // dist/uninstall.js -> __dirname=<pkg>/dist -> <pkg>/templates/aider/<name>
111
134
  // src/uninstall.js -> __dirname=<pkg>/src -> <pkg>/templates/aider/<name>
@@ -114,7 +137,7 @@ function resolveAiderTemplate(name, repoRoot) {
114
137
  ];
115
138
  for (const c of candidates) {
116
139
  try {
117
- if (existsSync2(c)) return c;
140
+ if (existsSync3(c)) return c;
118
141
  } catch {
119
142
  }
120
143
  }
@@ -122,7 +145,7 @@ function resolveAiderTemplate(name, repoRoot) {
122
145
  }
123
146
  function writeAtomic(target, content) {
124
147
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
125
- writeFileSync2(tmp, content);
148
+ writeFileSync3(tmp, content);
126
149
  try {
127
150
  renameSync2(tmp, target);
128
151
  } catch (err) {
@@ -178,7 +201,7 @@ function confirm(question) {
178
201
  var HOME = homedir2();
179
202
  var TS = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
180
203
  function backupFile(p) {
181
- if (existsSync2(p)) {
204
+ if (existsSync3(p)) {
182
205
  const bak = p + ".bak." + TS;
183
206
  cpSync(p, bak);
184
207
  return bak;
@@ -212,10 +235,10 @@ function hasIjfwMarker(text) {
212
235
  }
213
236
  function stripMarkerFile(p, opts = {}) {
214
237
  try {
215
- if (!existsSync2(p)) return null;
238
+ if (!existsSync3(p)) return null;
216
239
  let text;
217
240
  try {
218
- text = readFileSync2(p, "utf8");
241
+ text = readFileSync3(p, "utf8");
219
242
  } catch {
220
243
  return null;
221
244
  }
@@ -234,9 +257,9 @@ function stripMarkerFile(p, opts = {}) {
234
257
  }
235
258
  }
236
259
  function removeTomlSection(p) {
237
- if (!existsSync2(p)) return false;
260
+ if (!existsSync3(p)) return false;
238
261
  backupFile(p);
239
- const lines = readFileSync2(p, "utf8").split("\n");
262
+ const lines = readFileSync3(p, "utf8").split("\n");
240
263
  const out = [];
241
264
  let skip = false;
242
265
  for (const line of lines) {
@@ -251,10 +274,10 @@ function removeTomlSection(p) {
251
274
  return true;
252
275
  }
253
276
  function removeJsonMcpEntry(p) {
254
- if (!existsSync2(p)) return false;
277
+ if (!existsSync3(p)) return false;
255
278
  let doc;
256
279
  try {
257
- doc = JSON.parse(readFileSync2(p, "utf8"));
280
+ doc = JSON.parse(readFileSync3(p, "utf8"));
258
281
  } catch {
259
282
  return false;
260
283
  }
@@ -270,10 +293,10 @@ function removeJsonMcpEntry(p) {
270
293
  }
271
294
  function removeNestedMcpEntry(p, keyPath) {
272
295
  try {
273
- if (!existsSync2(p)) return false;
296
+ if (!existsSync3(p)) return false;
274
297
  let doc;
275
298
  try {
276
- doc = JSON.parse(readFileSync2(p, "utf8"));
299
+ doc = JSON.parse(readFileSync3(p, "utf8"));
277
300
  } catch {
278
301
  return false;
279
302
  }
@@ -294,43 +317,43 @@ function removeNestedMcpEntry(p, keyPath) {
294
317
  }
295
318
  function resolveClineSettingsPath(home) {
296
319
  const H = home;
297
- const APPDATA = process.env.APPDATA || join2(H, "AppData", "Roaming");
320
+ const APPDATA = process.env.APPDATA || join3(H, "AppData", "Roaming");
298
321
  const ext = "saoudrizwan.claude-dev";
299
322
  let userDirs;
300
323
  if (process.platform === "darwin") {
301
324
  userDirs = [
302
- join2(H, "Library", "Application Support", "Code", "User"),
303
- join2(H, "Library", "Application Support", "Code - Insiders", "User"),
304
- join2(H, "Library", "Application Support", "VSCodium", "User")
325
+ join3(H, "Library", "Application Support", "Code", "User"),
326
+ join3(H, "Library", "Application Support", "Code - Insiders", "User"),
327
+ join3(H, "Library", "Application Support", "VSCodium", "User")
305
328
  ];
306
329
  } else if (process.platform === "win32") {
307
330
  userDirs = [
308
- join2(APPDATA, "Code", "User"),
309
- join2(APPDATA, "Code - Insiders", "User"),
310
- join2(APPDATA, "VSCodium", "User")
331
+ join3(APPDATA, "Code", "User"),
332
+ join3(APPDATA, "Code - Insiders", "User"),
333
+ join3(APPDATA, "VSCodium", "User")
311
334
  ];
312
335
  } else {
313
336
  userDirs = [
314
- join2(H, ".config", "Code", "User"),
315
- join2(H, ".config", "VSCodium", "User"),
316
- join2(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
317
- join2(H, "snap", "code", "current", ".config", "Code", "User")
337
+ join3(H, ".config", "Code", "User"),
338
+ join3(H, ".config", "VSCodium", "User"),
339
+ join3(H, ".var", "app", "com.visualstudio.code", "config", "Code", "User"),
340
+ join3(H, "snap", "code", "current", ".config", "Code", "User")
318
341
  ];
319
342
  }
320
343
  for (const d of userDirs) {
321
- const settings = join2(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
322
- if (existsSync2(settings)) return settings;
344
+ const settings = join3(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
345
+ if (existsSync3(settings)) return settings;
323
346
  }
324
347
  return null;
325
348
  }
326
349
  function removeAiderFileIfPristine(installedPath, templatePath) {
327
350
  try {
328
- if (!existsSync2(installedPath)) return "absent";
329
- if (!existsSync2(templatePath)) return "kept-modified";
351
+ if (!existsSync3(installedPath)) return "absent";
352
+ if (!existsSync3(templatePath)) return "kept-modified";
330
353
  let a, b;
331
354
  try {
332
- a = readFileSync2(installedPath);
333
- b = readFileSync2(templatePath);
355
+ a = readFileSync3(installedPath);
356
+ b = readFileSync3(templatePath);
334
357
  } catch {
335
358
  return "kept-modified";
336
359
  }
@@ -345,10 +368,10 @@ function removeAiderFileIfPristine(installedPath, templatePath) {
345
368
  }
346
369
  }
347
370
  function removeCodexHooks(p) {
348
- if (!existsSync2(p)) return false;
371
+ if (!existsSync3(p)) return false;
349
372
  let doc;
350
373
  try {
351
- doc = JSON.parse(readFileSync2(p, "utf8"));
374
+ doc = JSON.parse(readFileSync3(p, "utf8"));
352
375
  } catch {
353
376
  return false;
354
377
  }
@@ -389,8 +412,8 @@ function removeCodexHooks(p) {
389
412
  return false;
390
413
  }
391
414
  function removeYamlMcpEntry(p) {
392
- if (!existsSync2(p)) return false;
393
- const raw = readFileSync2(p, "utf8");
415
+ if (!existsSync3(p)) return false;
416
+ const raw = readFileSync3(p, "utf8");
394
417
  if (!/\bijfw-memory\b/.test(raw)) return false;
395
418
  const py = spawnSync("python3", ["-c", `
396
419
  import sys, yaml
@@ -424,12 +447,74 @@ import os; os.replace(p + ".tmp", p)
424
447
  writeAtomic(p, stripped);
425
448
  return true;
426
449
  }
450
+ function resolveShippedTemplate(rel, repoRoot) {
451
+ const root = repoRoot || REPO_ROOT;
452
+ const candidates = [
453
+ join3(root, rel),
454
+ join3(root, "installer", "templates", rel),
455
+ resolve2(__dirname, "..", "templates", rel)
456
+ ];
457
+ for (const c of candidates) {
458
+ try {
459
+ if (existsSync3(c)) return c;
460
+ } catch {
461
+ }
462
+ }
463
+ return "";
464
+ }
465
+ function removeHermesIjfwWiring(p) {
466
+ if (!existsSync3(p)) return false;
467
+ const raw = readFileSync3(p, "utf8");
468
+ if (!/\bijfw\b/i.test(raw)) return false;
469
+ const py = spawnSync("python3", ["-c", `
470
+ import sys, yaml
471
+ p = sys.argv[1]
472
+ with open(p) as f: raw = f.read()
473
+ doc = yaml.safe_load(raw) if raw.strip() else {}
474
+ if not isinstance(doc, dict): sys.exit(2)
475
+ changed = False
476
+ srv = doc.get('mcp_servers')
477
+ if isinstance(srv, dict) and 'ijfw-memory' in srv:
478
+ del srv['ijfw-memory']; changed = True
479
+ if not srv: del doc['mcp_servers']
480
+ pl = doc.get('plugins')
481
+ if isinstance(pl, dict) and isinstance(pl.get('enabled'), list) and 'ijfw' in pl['enabled']:
482
+ pl['enabled'] = [x for x in pl['enabled'] if x != 'ijfw']; changed = True
483
+ if not pl['enabled']: del pl['enabled']
484
+ if not pl: del doc['plugins']
485
+ hk = doc.get('hooks')
486
+ if isinstance(hk, dict):
487
+ for ev in list(hk.keys()):
488
+ items = hk[ev]
489
+ if isinstance(items, list):
490
+ new = [it for it in items if not (isinstance(it, dict) and 'ijfw' in str(it.get('script','')))]
491
+ if len(new) != len(items):
492
+ changed = True
493
+ if new: hk[ev] = new
494
+ else: del hk[ev]
495
+ if isinstance(doc.get('hooks'), dict) and not doc['hooks']: del doc['hooks']
496
+ if not changed: sys.exit(3)
497
+ with open(p + '.tmp', 'w') as f:
498
+ if doc: yaml.safe_dump(doc, f, sort_keys=False, default_flow_style=False)
499
+ else: f.write('')
500
+ import os; os.replace(p + '.tmp', p)
501
+ `, p], { encoding: "utf8" });
502
+ if (py.status === 0) {
503
+ backupFile(p);
504
+ return true;
505
+ }
506
+ const out = raw.replace(/# IJFW-MCP-BEGIN ijfw-memory\n[\s\S]*?# IJFW-MCP-END ijfw-memory\n/g, "").replace(/# IJFW-PLUGINS-BEGIN\n[\s\S]*?# IJFW-PLUGINS-END\n/g, "").replace(/# IJFW-HOOK-BEGIN pre_tool_use\n[\s\S]*?# IJFW-HOOK-END pre_tool_use\n/g, "").replace(/^[ \t]*-[ \t]+ijfw[ \t]*\n/gm, "").replace(/^[ \t]*-[ \t]+script:[ \t]*["']?plugins\/ijfw\/[^\n]*\n/gm, "");
507
+ if (out === raw) return false;
508
+ backupFile(p);
509
+ writeAtomic(p, out);
510
+ return true;
511
+ }
427
512
  function removeIjfwSkills(dir) {
428
- if (!existsSync2(dir)) return 0;
513
+ if (!existsSync3(dir)) return 0;
429
514
  let count = 0;
430
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
515
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
431
516
  if (entry.isDirectory() && entry.name.startsWith("ijfw-")) {
432
- rmSync(join2(dir, entry.name), { recursive: true, force: true });
517
+ rmSync(join3(dir, entry.name), { recursive: true, force: true });
433
518
  count++;
434
519
  }
435
520
  }
@@ -460,99 +545,177 @@ var CODEX_COMMAND_FILES = [
460
545
  "workflow.md"
461
546
  ];
462
547
  function removeCodexCommands(dir) {
463
- if (!existsSync2(dir)) return 0;
548
+ if (!existsSync3(dir)) return 0;
464
549
  let count = 0;
465
550
  for (const name of CODEX_COMMAND_FILES) {
466
- const path = join2(dir, name);
467
- if (existsSync2(path)) {
551
+ const path = join3(dir, name);
552
+ if (existsSync3(path)) {
468
553
  rmSync(path, { force: true });
469
554
  count++;
470
555
  }
471
556
  }
472
557
  return count;
473
558
  }
559
+ function removeCodexHookFiles(hooksDir) {
560
+ if (!existsSync3(hooksDir)) return 0;
561
+ let count = 0;
562
+ const scriptsDir = join3(hooksDir, "scripts");
563
+ if (existsSync3(scriptsDir)) {
564
+ rmSync(scriptsDir, { recursive: true, force: true });
565
+ count++;
566
+ }
567
+ let entries = [];
568
+ try {
569
+ entries = readdirSync2(hooksDir, { withFileTypes: true });
570
+ } catch {
571
+ return count;
572
+ }
573
+ for (const e of entries) {
574
+ if (!e.isFile() || !e.name.endsWith(".sh")) continue;
575
+ const p = join3(hooksDir, e.name);
576
+ let body = "";
577
+ try {
578
+ body = readFileSync3(p, "utf8");
579
+ } catch {
580
+ continue;
581
+ }
582
+ if (/\bIJFW\b/.test(body) || /\bijfw\b/.test(body)) {
583
+ rmSync(p, { force: true });
584
+ count++;
585
+ }
586
+ }
587
+ return count;
588
+ }
589
+ function removeKnownMarketplacesEntry(p) {
590
+ if (!existsSync3(p)) return false;
591
+ let doc;
592
+ try {
593
+ doc = JSON.parse(readFileSync3(p, "utf8"));
594
+ } catch {
595
+ return false;
596
+ }
597
+ if (!doc || typeof doc !== "object") return false;
598
+ let changed = false;
599
+ if (doc.ijfw) {
600
+ delete doc.ijfw;
601
+ changed = true;
602
+ }
603
+ if (doc.extraKnownMarketplaces && typeof doc.extraKnownMarketplaces === "object" && doc.extraKnownMarketplaces.ijfw) {
604
+ delete doc.extraKnownMarketplaces.ijfw;
605
+ if (Object.keys(doc.extraKnownMarketplaces).length === 0) delete doc.extraKnownMarketplaces;
606
+ changed = true;
607
+ }
608
+ if (!changed) return false;
609
+ backupFile(p);
610
+ writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
611
+ return true;
612
+ }
474
613
  function cleanPlatforms(opts = {}) {
475
614
  const home = opts.home || HOME;
476
615
  const cwd = opts.cwd || process.cwd();
477
616
  const repoRoot = opts.repoRoot || REPO_ROOT;
478
617
  const removed = [];
479
- if (removeTomlSection(join2(home, ".codex", "config.toml"))) {
618
+ if (removeJsonMcpEntry(join3(home, ".claude", "settings.json"))) {
619
+ removed.push("~/.claude/settings.json (removed ijfw-memory mcp entry)");
620
+ }
621
+ if (removeKnownMarketplacesEntry(join3(home, ".claude", "plugins", "known_marketplaces.json"))) {
622
+ removed.push("~/.claude/plugins/known_marketplaces.json (removed ijfw entry)");
623
+ }
624
+ if (removeTomlSection(join3(home, ".codex", "config.toml"))) {
480
625
  removed.push("~/.codex/config.toml (removed [mcp_servers.ijfw-memory])");
481
626
  }
482
- if (removeCodexHooks(join2(home, ".codex", "hooks.json"))) {
627
+ if (removeCodexHooks(join3(home, ".codex", "hooks.json"))) {
483
628
  removed.push("~/.codex/hooks.json (removed IJFW hook entries)");
484
629
  }
485
- const codexSkills = removeIjfwSkills(join2(home, ".codex", "skills"));
630
+ const codexSkills = removeIjfwSkills(join3(home, ".codex", "skills"));
486
631
  if (codexSkills > 0) removed.push(`~/.codex/skills/ijfw-* (removed ${codexSkills} skill dirs)`);
487
- const codexCommands = removeCodexCommands(join2(home, ".codex", "commands"));
632
+ const codexCommands = removeCodexCommands(join3(home, ".codex", "commands"));
488
633
  if (codexCommands > 0) removed.push(`~/.codex/commands (removed ${codexCommands} IJFW command aliases)`);
489
- const codexMd = join2(home, ".codex", "IJFW.md");
490
- if (existsSync2(codexMd)) {
634
+ const codexMd = join3(home, ".codex", "IJFW.md");
635
+ if (existsSync3(codexMd)) {
491
636
  rmSync(codexMd, { force: true });
492
637
  removed.push("~/.codex/IJFW.md");
493
638
  }
494
- if (removeJsonMcpEntry(join2(home, ".gemini", "settings.json"))) {
639
+ const codexHookFiles = removeCodexHookFiles(join3(home, ".codex", "hooks"));
640
+ if (codexHookFiles > 0) removed.push(`~/.codex/hooks/ (removed ${codexHookFiles} IJFW hook scripts)`);
641
+ if (removeJsonMcpEntry(join3(home, ".gemini", "settings.json"))) {
495
642
  removed.push("~/.gemini/settings.json (removed ijfw-memory)");
496
643
  }
497
- const geminiExt = join2(home, ".gemini", "extensions", "ijfw");
498
- if (existsSync2(geminiExt)) {
644
+ const geminiExt = join3(home, ".gemini", "extensions", "ijfw");
645
+ if (existsSync3(geminiExt)) {
499
646
  rmSync(geminiExt, { recursive: true, force: true });
500
647
  removed.push("~/.gemini/extensions/ijfw/");
501
648
  }
502
- const cursorMcp = join2(cwd, ".cursor", "mcp.json");
649
+ const cursorMcp = join3(cwd, ".cursor", "mcp.json");
503
650
  if (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
504
- if (removeJsonMcpEntry(join2(home, ".codeium", "windsurf", "mcp_config.json"))) {
651
+ if (removeJsonMcpEntry(join3(home, ".codeium", "windsurf", "mcp_config.json"))) {
505
652
  removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
506
653
  }
507
- const vscodeMcp = join2(cwd, ".vscode", "mcp.json");
654
+ const vscodeMcp = join3(cwd, ".vscode", "mcp.json");
508
655
  if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
509
- if (removeYamlMcpEntry(join2(home, ".hermes", "config.yaml"))) {
510
- removed.push("~/.hermes/config.yaml (removed ijfw-memory)");
656
+ if (removeHermesIjfwWiring(join3(home, ".hermes", "config.yaml"))) {
657
+ removed.push("~/.hermes/config.yaml (removed ijfw-memory + plugin + hook wiring)");
511
658
  }
512
- const hermesSkills = removeIjfwSkills(join2(home, ".hermes", "skills"));
659
+ const hermesSkills = removeIjfwSkills(join3(home, ".hermes", "skills"));
513
660
  if (hermesSkills > 0) removed.push(`~/.hermes/skills/ijfw-* (removed ${hermesSkills} skill dirs)`);
514
- const hermesMd = join2(home, ".hermes", "HERMES.md");
515
- if (existsSync2(hermesMd)) {
661
+ const hermesMd = join3(home, ".hermes", "HERMES.md");
662
+ if (existsSync3(hermesMd)) {
516
663
  rmSync(hermesMd, { force: true });
517
664
  removed.push("~/.hermes/HERMES.md");
518
665
  }
519
- const waylandPluginDir = join2(home, ".wayland", "plugins", "ijfw");
520
- if (existsSync2(waylandPluginDir)) {
666
+ const hermesPlugin = join3(home, ".hermes", "plugins", "ijfw");
667
+ if (existsSync3(hermesPlugin)) {
668
+ rmSync(hermesPlugin, { recursive: true, force: true });
669
+ removed.push("~/.hermes/plugins/ijfw/ (removed plugin tree)");
670
+ }
671
+ const waylandPluginDir = join3(home, ".wayland", "plugins", "ijfw");
672
+ if (existsSync3(waylandPluginDir)) {
521
673
  rmSync(waylandPluginDir, { recursive: true, force: true });
522
674
  removed.push("~/.wayland/plugins/ijfw/ (removed plugin.toml + hooks + MCP)");
523
675
  }
524
- if (removeYamlMcpEntry(join2(home, ".wayland", "config.yaml"))) {
676
+ if (removeYamlMcpEntry(join3(home, ".wayland", "config.yaml"))) {
525
677
  removed.push("~/.wayland/config.yaml (removed legacy ijfw-memory)");
526
678
  }
527
- const waylandSkills = removeIjfwSkills(join2(home, ".wayland", "skills"));
679
+ const waylandSkills = removeIjfwSkills(join3(home, ".wayland", "skills"));
528
680
  if (waylandSkills > 0) removed.push(`~/.wayland/skills/ijfw-* (removed ${waylandSkills} skill dirs)`);
529
- const waylandMd = join2(home, ".wayland", "WAYLAND.md");
530
- if (existsSync2(waylandMd)) {
681
+ const waylandMd = join3(home, ".wayland", "WAYLAND.md");
682
+ if (existsSync3(waylandMd)) {
531
683
  rmSync(waylandMd, { force: true });
532
684
  removed.push("~/.wayland/WAYLAND.md");
533
685
  }
534
- if (removeJsonMcpEntry(join2(home, ".qwen", "settings.json"))) {
686
+ if (removeJsonMcpEntry(join3(home, ".qwen", "settings.json"))) {
535
687
  removed.push("~/.qwen/settings.json (removed ijfw-memory)");
536
688
  }
537
- if (removeJsonMcpEntry(join2(home, ".kimi", "mcp.json"))) {
689
+ if (removeJsonMcpEntry(join3(home, ".kimi", "mcp.json"))) {
538
690
  removed.push("~/.kimi/mcp.json (removed ijfw-memory)");
539
691
  }
540
- if (removeJsonMcpEntry(join2(home, ".gemini", "antigravity", "mcp_config.json"))) {
692
+ if (removeJsonMcpEntry(join3(home, ".gemini", "antigravity", "mcp_config.json"))) {
541
693
  removed.push("~/.gemini/antigravity/mcp_config.json (removed ijfw-memory)");
542
694
  }
543
- if (removeJsonMcpEntry(join2(home, ".gemini", "config", "mcp_config.json"))) {
695
+ if (removeJsonMcpEntry(join3(home, ".gemini", "config", "mcp_config.json"))) {
544
696
  removed.push("~/.gemini/config/mcp_config.json (removed ijfw-memory)");
545
697
  }
546
- if (removeNestedMcpEntry(join2(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
698
+ if (removeNestedMcpEntry(join3(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
547
699
  removed.push("~/.config/opencode/opencode.json (removed mcp.ijfw-memory)");
548
700
  }
549
- if (removeNestedMcpEntry(join2(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
701
+ if (removeNestedMcpEntry(join3(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
550
702
  removed.push("~/.openclaw/openclaw.json (removed mcp.servers.ijfw-memory)");
551
703
  }
552
- const piStatus = stripMarkerFile(join2(home, ".pi", "agent", "AGENTS.md"), {
553
- label: "~/.pi/agent/AGENTS.md"
554
- });
555
- if (piStatus) removed.push(piStatus);
704
+ const piPath = join3(home, ".pi", "agent", "AGENTS.md");
705
+ const piStatus = stripMarkerFile(piPath, { label: "~/.pi/agent/AGENTS.md" });
706
+ if (piStatus) {
707
+ removed.push(piStatus);
708
+ } else {
709
+ const piPristine = removeAiderFileIfPristine(
710
+ piPath,
711
+ resolveShippedTemplate(join3("pi", "AGENTS.md"), repoRoot)
712
+ );
713
+ if (piPristine === "removed") {
714
+ removed.push("~/.pi/agent/AGENTS.md (removed -- matched shipped template)");
715
+ } else if (piPristine === "kept-modified") {
716
+ removed.push("~/.pi/agent/AGENTS.md (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
717
+ }
718
+ }
556
719
  const clineSettings = resolveClineSettingsPath(home);
557
720
  if (clineSettings) {
558
721
  if (removeJsonMcpEntry(clineSettings)) {
@@ -562,7 +725,7 @@ function cleanPlatforms(opts = {}) {
562
725
  removed.push("Cline: no globalStorage found -- if you use Cline, remove the ijfw-memory MCP entry manually.");
563
726
  }
564
727
  const confResult = removeAiderFileIfPristine(
565
- join2(home, ".aider.conf.yml"),
728
+ join3(home, ".aider.conf.yml"),
566
729
  resolveAiderTemplate("aider.conf.yml", repoRoot)
567
730
  );
568
731
  if (confResult === "removed") {
@@ -571,7 +734,7 @@ function cleanPlatforms(opts = {}) {
571
734
  removed.push("~/.aider.conf.yml (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
572
735
  }
573
736
  const convResult = removeAiderFileIfPristine(
574
- join2(home, "CONVENTIONS.md"),
737
+ join3(home, "CONVENTIONS.md"),
575
738
  resolveAiderTemplate("CONVENTIONS.md", repoRoot)
576
739
  );
577
740
  if (convResult === "removed") {
@@ -583,8 +746,8 @@ function cleanPlatforms(opts = {}) {
583
746
  }
584
747
  function parseRegistryPaths(registryPath) {
585
748
  try {
586
- if (!existsSync2(registryPath)) return [];
587
- const raw = readFileSync2(registryPath, "utf8");
749
+ if (!existsSync3(registryPath)) return [];
750
+ const raw = readFileSync3(registryPath, "utf8");
588
751
  const paths = [];
589
752
  for (const line of raw.split("\n")) {
590
753
  const trimmed = line.trim();
@@ -602,37 +765,37 @@ function parseRegistryPaths(registryPath) {
602
765
  function stripRegisteredProjectBlocks(opts = {}) {
603
766
  const home = opts.home || HOME;
604
767
  const cwd = opts.cwd || process.cwd();
605
- const registryPath = opts.registryPath || join2(home, ".ijfw", "registry.md");
768
+ const registryPath = opts.registryPath || join3(home, ".ijfw", "registry.md");
606
769
  const results = [];
607
770
  for (const projPath of parseRegistryPaths(registryPath)) {
608
771
  let dirExists = false;
609
772
  try {
610
- dirExists = existsSync2(projPath);
773
+ dirExists = existsSync3(projPath);
611
774
  } catch {
612
775
  dirExists = false;
613
776
  }
614
777
  if (!dirExists) continue;
615
778
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
616
- const filePath = join2(projPath, name);
617
- const status = stripMarkerFile(filePath, { label: join2(projPath, name) });
779
+ const filePath = join3(projPath, name);
780
+ const status = stripMarkerFile(filePath, { label: join3(projPath, name) });
618
781
  if (status) results.push(status);
619
782
  }
620
783
  }
621
784
  try {
622
- const cursorRule = join2(cwd, ".cursor", "rules", "ijfw.mdc");
623
- if (existsSync2(cursorRule)) {
785
+ const cursorRule = join3(cwd, ".cursor", "rules", "ijfw.mdc");
786
+ if (existsSync3(cursorRule)) {
624
787
  backupFile(cursorRule);
625
788
  rmSync(cursorRule, { force: true });
626
789
  results.push(".cursor/rules/ijfw.mdc (removed -- wholly IJFW-authored)");
627
790
  }
628
791
  } catch {
629
792
  }
630
- const windsurfStatus = stripMarkerFile(join2(cwd, ".windsurfrules"), {
793
+ const windsurfStatus = stripMarkerFile(join3(cwd, ".windsurfrules"), {
631
794
  label: ".windsurfrules",
632
795
  deleteIfEmpty: true
633
796
  });
634
797
  if (windsurfStatus) results.push(windsurfStatus);
635
- const copilotStatus = stripMarkerFile(join2(cwd, ".github", "copilot-instructions.md"), {
798
+ const copilotStatus = stripMarkerFile(join3(cwd, ".github", "copilot-instructions.md"), {
636
799
  label: ".github/copilot-instructions.md",
637
800
  deleteIfEmpty: true
638
801
  });
@@ -642,11 +805,48 @@ function stripRegisteredProjectBlocks(opts = {}) {
642
805
  function resolveTarget(opt) {
643
806
  if (opt.dir) return resolve2(opt.dir);
644
807
  if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
645
- return join2(homedir2(), ".ijfw");
808
+ return join3(homedir2(), ".ijfw");
809
+ }
810
+ function assertSafePurgeTarget(target) {
811
+ let real = target;
812
+ try {
813
+ real = realpathSync(target);
814
+ } catch {
815
+ }
816
+ let home = homedir2();
817
+ try {
818
+ home = realpathSync(home);
819
+ } catch {
820
+ }
821
+ if (!real || real === "/" || real === home) {
822
+ throw new Error(`refusing to delete '${target}': it resolves to the home or filesystem root.`);
823
+ }
824
+ if (real.split("/").filter(Boolean).length < 2) {
825
+ throw new Error(`refusing to delete shallow path '${real}'.`);
826
+ }
827
+ const looksIjfw = basename(real) === ".ijfw" || existsSync3(join3(real, "state.json")) || existsSync3(join3(real, "install-method")) || existsSync3(join3(real, "install-ledger.json")) || existsSync3(join3(real, "mcp-server")) || existsSync3(join3(real, "memory"));
828
+ if (!looksIjfw) {
829
+ throw new Error(`refusing to delete '${target}': it does not look like an IJFW install (no state.json / install-method / mcp-server). Aborting.`);
830
+ }
831
+ }
832
+ function removeCreatedDirs(home, createdDirs) {
833
+ const removed = [];
834
+ for (const rel of createdDirs || []) {
835
+ const abs = join3(home, rel);
836
+ if (isEmptyDir(abs)) {
837
+ try {
838
+ rmSync(abs, { recursive: false, force: true });
839
+ removed.push(`~/${rel} (IJFW-created, now empty)`);
840
+ } catch {
841
+ }
842
+ }
843
+ }
844
+ return removed;
646
845
  }
647
846
  async function main() {
648
847
  const opts = parseArgs(process.argv);
649
848
  const target = resolveTarget(opts);
849
+ const ledgerCreatedDirs = existsSync3(target) ? readLedger(target).createdDirs : [];
650
850
  console.log("This will remove IJFW configuration. Your memory at ~/.ijfw/memory/ will be preserved. Delete manually if desired.");
651
851
  if (opts.purge) {
652
852
  console.log("WARNING: --purge will also DELETE ~/.ijfw/memory/ (project memory cannot be recovered).");
@@ -660,16 +860,18 @@ async function main() {
660
860
  }
661
861
  console.log("");
662
862
  }
663
- if (!existsSync2(target)) {
863
+ if (!existsSync3(target)) {
664
864
  console.log(`IJFW directory absent (${target}); platform cleanup only.`);
665
865
  } else if (opts.purge) {
866
+ assertSafePurgeTarget(target);
666
867
  rmSync(target, { recursive: true, force: true });
667
868
  console.log(` removed ${target} (purged).`);
668
869
  } else {
669
- const memDir = join2(target, "memory");
870
+ assertSafePurgeTarget(target);
871
+ const memDir = join3(target, "memory");
670
872
  let stash = null;
671
- if (existsSync2(memDir)) {
672
- stash = mkdtempSync(join2(tmpdir(), "ijfw-memory-"));
873
+ if (existsSync3(memDir)) {
874
+ stash = mkdtempSync(join3(tmpdir(), "ijfw-memory-"));
673
875
  cpSync(memDir, stash, { recursive: true });
674
876
  }
675
877
  rmSync(target, { recursive: true, force: true });
@@ -681,11 +883,11 @@ async function main() {
681
883
  console.log(" memory/ was not present; nothing to preserve");
682
884
  }
683
885
  }
684
- const canonicalDir = join2(HOME, ".ijfw");
886
+ const canonicalDir = join3(HOME, ".ijfw");
685
887
  const isCanonical = target === canonicalDir;
686
888
  if (isCanonical && !opts.noMarketplace) {
687
889
  const settingsPath = claudeSettingsPath();
688
- if (existsSync2(settingsPath)) {
890
+ if (existsSync3(settingsPath)) {
689
891
  unmergeMarketplace(settingsPath);
690
892
  console.log(` marketplace removed from ${settingsPath}`);
691
893
  }
@@ -701,6 +903,13 @@ async function main() {
701
903
  console.log(" project blocks cleaned:");
702
904
  for (const line of projectCleaned) console.log(` ${line}`);
703
905
  }
906
+ if (opts.purge) {
907
+ const dirsRemoved = removeCreatedDirs(HOME, ledgerCreatedDirs);
908
+ if (dirsRemoved.length > 0) {
909
+ console.log(" IJFW-created dirs removed:");
910
+ for (const line of dirsRemoved) console.log(` ${line}`);
911
+ }
912
+ }
704
913
  } else {
705
914
  console.log(` custom-dir uninstall (${target}) -- platform configs in your real home left untouched.`);
706
915
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "One-command installer for IJFW -- the AI efficiency layer. One install, every AI coding agent, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "docs/GUIDE.md",
15
15
  "docs/guide/assets",
16
16
  "templates/aider/**",
17
+ "templates/pi/**",
17
18
  "scripts/pack-hub-extension.js",
18
19
  "scripts/hub-extension/**",
19
20
  "README.md",
@@ -0,0 +1,55 @@
1
+ # IJFW Conventions for Pi
2
+
3
+ <!-- Pi MCP support last verified: 2026-05-28 against https://pi.dev/.
4
+ Pi has no native MCP client (build as extension or skill). When Pi adds
5
+ native MCP, regenerate this file and consider promoting Pi from
6
+ rules-only to full-skill tier in installer/src/install-targets-8-14.js
7
+ and add the MCP wiring path. -->
8
+
9
+ Pi has no native MCP, so IJFW's memory + cross-audit tools aren't available
10
+ inside Pi sessions out of the box. These conventions carry the IJFW spirit
11
+ (disciplined workflow, terse output, no scope creep, no half-shipping) into
12
+ Pi's terminal harness. Pi loads this file at startup from `~/.pi/agent/`,
13
+ parent directories, and the current working directory -- exactly where IJFW
14
+ installs it.
15
+
16
+ ## Workflow
17
+
18
+ - One question at a time. Don't dump multi-step plans before the user signs off.
19
+ - Lead with the answer. No restating the question.
20
+ - For multi-file changes, propose the plan in chat FIRST. Wait for the user "go".
21
+ - Terse output. The diff is the deliverable, not your prose about the diff.
22
+
23
+ ## Code
24
+
25
+ - Match existing style. Don't refactor adjacent code that wasn't asked for.
26
+ - No speculative abstractions. Three similar lines beats a premature helper.
27
+ - No error handling for impossible scenarios. Trust internal code; validate only at system boundaries.
28
+ - Default to writing no comments. Only add when the WHY is non-obvious (a hidden constraint, a subtle invariant, a workaround for a specific bug).
29
+ - Never write multi-paragraph docstrings. One short line max.
30
+
31
+ ## Memory + cross-audit
32
+
33
+ Pi sessions don't see IJFW's persistent memory by default. After significant work:
34
+
35
+ - Run `ijfw cross audit <file>` in a separate terminal to get a Trident review across two model families.
36
+ - Use `ijfw_memory_store` from a Claude/Codex/Gemini session (where MCP is native) to persist decisions Pi makes -- they won't survive otherwise.
37
+ - Or build a Pi extension that bridges to the IJFW MCP memory server (`~/.ijfw/mcp-server/src/server.js`). Pi's extension API supports tool registration; the bridge is the cleanest path to native parity.
38
+
39
+ ## Scope
40
+
41
+ Stay in the lane the user asked for. If you spot adjacent issues, mention them in chat -- don't fix them silently. No drive-by refactors. No backwards-compatibility shims for code that isn't shipped yet.
42
+
43
+ ## DESIGN picker
44
+
45
+ If the user asks for a design contract and no `DESIGN.md` exists in the project root, suggest one of the 12 IJFW curated templates (alphabetical):
46
+
47
+ apple-glass, anthropic, bauhaus, brutalist, calm, dark-mode, document, editorial, glassmorphism, minimal, neo-brutalist, terminal
48
+
49
+ Show 3 options matching the project's vibe; let the user pick. Then write `DESIGN.md` to project root. Every IJFW-connected agent reads the same visual contract -- you keep them consistent.
50
+
51
+ ## Executing actions with care
52
+
53
+ Carefully consider blast radius before destructive ops. Local edits and tests are safe; check with the user before `rm -rf`, force pushes, dropping tables, sending messages, or anything visible to others or hard to reverse.
54
+
55
+ When you hit an obstacle, find the root cause rather than bypassing safety checks (no `--no-verify`, no force-push to main). If you encounter unfamiliar state, investigate before deleting -- it may be the user's in-progress work.