@ijfw/install 1.5.5 → 1.6.0

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
@@ -389,7 +389,7 @@ __export(psscriptanalyzer_exports, {
389
389
  severity: () => severity4
390
390
  });
391
391
  import { spawnSync as spawnSync4 } from "node:child_process";
392
- import { readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
392
+ import { readFileSync, readdirSync as readdirSync2, statSync as statSync2, existsSync } from "node:fs";
393
393
  import { join as join3 } from "node:path";
394
394
  function findPs1Files(dir, acc = []) {
395
395
  let entries;
@@ -539,11 +539,13 @@ async function run4(ctx) {
539
539
  if (moduleCheck.status !== 0) {
540
540
  return runFallback(files, t0, "PSScriptAnalyzer module unavailable");
541
541
  }
542
+ const psSettings = join3(ctx.repoRoot, "PSScriptAnalyzerSettings.psd1");
543
+ const invoke = existsSync(psSettings) ? `Invoke-ScriptAnalyzer -Path $f -Settings '${psSettings.replace(/'/g, "''")}' -ErrorAction SilentlyContinue` : "Invoke-ScriptAnalyzer -Path $f -Severity Warning -ErrorAction SilentlyContinue";
542
544
  const script = `
543
545
  $files = @(${files.map((f) => `'${f.replace(/'/g, "''")}'`).join(",")})
544
546
  $found = $false
545
547
  foreach ($f in $files) {
546
- $results = Invoke-ScriptAnalyzer -Path $f -Severity Warning -ErrorAction SilentlyContinue
548
+ $results = ${invoke}
547
549
  if ($results) {
548
550
  $found = $true
549
551
  foreach ($r in $results) {
@@ -861,14 +863,14 @@ var init_gate_result_schema = __esm({
861
863
  });
862
864
 
863
865
  // ../mcp-server/src/scan-resume.js
864
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync, copyFileSync } from "fs";
866
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync, copyFileSync } from "fs";
865
867
  import { join as join4 } from "path";
866
868
  function statePath(projectRoot) {
867
869
  return join4(String(projectRoot), ".ijfw", STATE_FILE);
868
870
  }
869
871
  function loadScanState(projectRoot) {
870
872
  const path = statePath(projectRoot);
871
- if (!existsSync(path)) return null;
873
+ if (!existsSync2(path)) return null;
872
874
  try {
873
875
  const raw = readFileSync2(path, "utf8");
874
876
  const parsed = JSON.parse(raw);
@@ -879,7 +881,7 @@ function loadScanState(projectRoot) {
879
881
  }
880
882
  function writeScanState(projectRoot, state) {
881
883
  const dir = join4(String(projectRoot), ".ijfw");
882
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
884
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
883
885
  const finalPath = statePath(projectRoot);
884
886
  const tmpPath = `${finalPath}.tmp.${process.pid}.${Date.now()}`;
885
887
  const safe = {
@@ -925,7 +927,7 @@ function isPidAlive(pid) {
925
927
  }
926
928
  }
927
929
  function reclaimIfStale(lp) {
928
- if (!existsSync(lp)) return;
930
+ if (!existsSync2(lp)) return;
929
931
  let raw;
930
932
  try {
931
933
  raw = readFileSync2(lp, "utf8");
@@ -944,7 +946,7 @@ function reclaimIfStale(lp) {
944
946
  }
945
947
  function acquireScanLock(projectRoot) {
946
948
  const dir = join4(String(projectRoot), ".ijfw");
947
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
949
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
948
950
  const lp = lockPath(projectRoot);
949
951
  reclaimIfStale(lp);
950
952
  const payload = String(process.pid) + "\n" + String(Date.now()) + "\n";
@@ -980,7 +982,7 @@ function shouldResume(state) {
980
982
  }
981
983
  function clearScanState(projectRoot) {
982
984
  const path = statePath(projectRoot);
983
- if (existsSync(path)) {
985
+ if (existsSync2(path)) {
984
986
  try {
985
987
  unlinkSync(path);
986
988
  } catch {
@@ -1002,7 +1004,7 @@ var init_scan_resume = __esm({
1002
1004
  import {
1003
1005
  readFileSync as readFileSync3,
1004
1006
  writeFileSync as writeFileSync3,
1005
- existsSync as existsSync2,
1007
+ existsSync as existsSync3,
1006
1008
  readdirSync as readdirSync3,
1007
1009
  statSync as statSync3,
1008
1010
  renameSync as renameSync2,
@@ -1154,7 +1156,7 @@ function finalize({ primary, secondary, score, signals, scanIncomplete, fallback
1154
1156
  return out;
1155
1157
  }
1156
1158
  function readFrontmatterType(path) {
1157
- if (!existsSync2(path)) return null;
1159
+ if (!existsSync3(path)) return null;
1158
1160
  let src;
1159
1161
  try {
1160
1162
  src = readFileSync3(path, "utf8");
@@ -1406,7 +1408,7 @@ function fileTreeHash(paths) {
1406
1408
  function branchHash(root) {
1407
1409
  try {
1408
1410
  const dotGit = join5(root, ".git");
1409
- if (!existsSync2(dotGit)) return "";
1411
+ if (!existsSync3(dotGit)) return "";
1410
1412
  let headPath = null;
1411
1413
  let st;
1412
1414
  try {
@@ -1426,7 +1428,7 @@ function branchHash(root) {
1426
1428
  } else {
1427
1429
  return "";
1428
1430
  }
1429
- if (!headPath || !existsSync2(headPath)) return "";
1431
+ if (!headPath || !existsSync3(headPath)) return "";
1430
1432
  const head = readFileSync3(headPath, "utf8").trim();
1431
1433
  const m2 = head.match(/^ref:\s*(.+)$/);
1432
1434
  const branch = m2 ? m2[1] : head;
@@ -1440,7 +1442,7 @@ function isC9AvailableSync() {
1440
1442
  try {
1441
1443
  const here = fileURLToPath(import.meta.url);
1442
1444
  const fts5Path = join5(dirname(here), "compute", "fts5.js");
1443
- _c9AvailableCache = existsSync2(fts5Path);
1445
+ _c9AvailableCache = existsSync3(fts5Path);
1444
1446
  } catch {
1445
1447
  _c9AvailableCache = false;
1446
1448
  }
@@ -1775,30 +1777,32 @@ async function run7(ctx) {
1775
1777
  timeout: 6e4
1776
1778
  }
1777
1779
  );
1778
- const output = (res.stdout || "") + (res.stderr || "");
1779
- const report = parseAuditReport(output);
1780
+ const report = parseAuditReport(res.stdout || "");
1780
1781
  const highCritical = highCriticalCount(report);
1781
- return { dir, status: res.status, output, report, highCritical };
1782
+ return { dir, status: res.status, output: (res.stdout || "") + (res.stderr || ""), report, highCritical };
1782
1783
  });
1783
1784
  const durationMs = Date.now() - t0;
1784
- const failed = runs.filter((r) => !r.report || r.highCritical > 0);
1785
- const status = failed.length === 0 ? "PASS" : "FAIL";
1786
- const message = status === "PASS" ? "audit-ci: no high/critical vulnerabilities in installer or mcp-server" : "audit-ci: high or critical vulnerabilities found";
1785
+ const realVulns = runs.filter((r) => r.report && r.highCritical > 0);
1786
+ const unparseable = runs.filter((r) => !r.report);
1787
+ const status = realVulns.length > 0 ? "FAIL" : unparseable.length > 0 ? "WARN" : "PASS";
1788
+ const message = status === "FAIL" ? "audit-ci: high or critical vulnerabilities found" : status === "WARN" ? "audit-ci: audit could not be completed for some package(s); no high/critical in the rest" : "audit-ci: no high/critical vulnerabilities in installer or mcp-server";
1787
1789
  let details;
1788
1790
  if (status === "PASS") {
1789
1791
  details = runs.map((r) => `${r.dir}: pass`);
1790
- } else {
1792
+ } else if (status === "FAIL") {
1791
1793
  const lines = [];
1792
- for (const r of failed) {
1793
- if (!r.report) {
1794
- lines.push(`${r.dir}: audit report unavailable`);
1795
- lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
1796
- continue;
1797
- }
1794
+ for (const r of realVulns) {
1798
1795
  lines.push(`${r.dir}: ${r.highCritical} high/critical advisory item(s)`);
1799
1796
  lines.push(...vulnerableNames(r.report).slice(0, 10));
1800
1797
  }
1801
1798
  details = lines.slice(0, 20);
1799
+ } else {
1800
+ const lines = [];
1801
+ for (const r of unparseable) {
1802
+ lines.push(`${r.dir}: audit report unavailable (non-blocking)`);
1803
+ lines.push(...r.output.split("\n").filter(Boolean).slice(0, 6));
1804
+ }
1805
+ details = lines.slice(0, 20);
1802
1806
  }
1803
1807
  try {
1804
1808
  const block = await emitGateResult(
@@ -1952,7 +1956,7 @@ __export(pack_smoke_exports, {
1952
1956
  severity: () => severity10
1953
1957
  });
1954
1958
  import { spawnSync as spawnSync10 } from "node:child_process";
1955
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readdirSync as readdirSync4, existsSync as existsSync3 } from "node:fs";
1959
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readdirSync as readdirSync4, existsSync as existsSync4 } from "node:fs";
1956
1960
  import { join as join9, resolve } from "node:path";
1957
1961
  import { tmpdir as tmpdir2 } from "node:os";
1958
1962
  async function run10(ctx) {
@@ -2034,7 +2038,7 @@ async function run10(ctx) {
2034
2038
  ];
2035
2039
  let binPath = null;
2036
2040
  for (const c2 of binCandidates) {
2037
- if (existsSync3(c2)) {
2041
+ if (existsSync4(c2)) {
2038
2042
  binPath = c2;
2039
2043
  break;
2040
2044
  }
@@ -2110,7 +2114,7 @@ __export(upgrade_smoke_exports, {
2110
2114
  severity: () => severity11
2111
2115
  });
2112
2116
  import { spawnSync as spawnSync11 } from "node:child_process";
2113
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync4, cpSync } from "node:fs";
2117
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync5, cpSync } from "node:fs";
2114
2118
  import { join as join10, resolve as resolve2 } from "node:path";
2115
2119
  import { tmpdir as tmpdir3 } from "node:os";
2116
2120
  async function run11(ctx) {
@@ -2185,7 +2189,7 @@ async function run11(ctx) {
2185
2189
  ];
2186
2190
  let installerBin = null;
2187
2191
  for (const c2 of binCandidates) {
2188
- if (existsSync4(c2)) {
2192
+ if (existsSync5(c2)) {
2189
2193
  installerBin = c2;
2190
2194
  break;
2191
2195
  }
@@ -2203,12 +2207,12 @@ async function run11(ctx) {
2203
2207
  mkdirSync4(targetIjfwHome, { recursive: true });
2204
2208
  for (const sub of ["claude", "mcp-server"]) {
2205
2209
  const src = join10(ctx.repoRoot, sub);
2206
- if (existsSync4(src)) {
2210
+ if (existsSync5(src)) {
2207
2211
  cpSync(src, join10(targetIjfwHome, sub), { recursive: true });
2208
2212
  }
2209
2213
  }
2210
2214
  const installerPkgSrc = join10(ctx.repoRoot, "installer", "package.json");
2211
- if (existsSync4(installerPkgSrc)) {
2215
+ if (existsSync5(installerPkgSrc)) {
2212
2216
  mkdirSync4(join10(targetIjfwHome, "installer"), { recursive: true });
2213
2217
  cpSync(installerPkgSrc, join10(targetIjfwHome, "installer", "package.json"));
2214
2218
  }
@@ -2252,7 +2256,7 @@ async function run11(ctx) {
2252
2256
  };
2253
2257
  }
2254
2258
  const settingsPath = join10(claudeDir, "settings.json");
2255
- if (!existsSync4(settingsPath)) {
2259
+ if (!existsSync5(settingsPath)) {
2256
2260
  return {
2257
2261
  name: "upgrade-smoke",
2258
2262
  status: "FAIL",
@@ -2289,7 +2293,7 @@ async function run11(ctx) {
2289
2293
  };
2290
2294
  }
2291
2295
  const marketplaceSrc = join10(installerDir, "src", "marketplace.js");
2292
- if (existsSync4(marketplaceSrc)) {
2296
+ if (existsSync5(marketplaceSrc)) {
2293
2297
  const src = readFileSync4(marketplaceSrc, "utf8");
2294
2298
  const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
2295
2299
  const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
@@ -2345,7 +2349,7 @@ var preflight_exports = {};
2345
2349
  __export(preflight_exports, {
2346
2350
  runPreflightCommand: () => runPreflightCommand
2347
2351
  });
2348
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "node:fs";
2352
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "node:fs";
2349
2353
  import { join as join11, dirname as dirname3, resolve as resolve3 } from "node:path";
2350
2354
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2351
2355
  function printHelp() {
@@ -2387,7 +2391,7 @@ function loadVersions(repoRoot2) {
2387
2391
  join11(repoRoot2, ".ijfw", "preflight-versions.json")
2388
2392
  ];
2389
2393
  for (const f of candidates) {
2390
- if (existsSync5(f)) {
2394
+ if (existsSync6(f)) {
2391
2395
  try {
2392
2396
  return JSON.parse(readFileSync5(f, "utf8"));
2393
2397
  } catch {
@@ -2448,7 +2452,7 @@ async function runPreflightCommand(argv, repoRoot2) {
2448
2452
  function defaultRepoRoot() {
2449
2453
  let dir = __dirname;
2450
2454
  for (let i = 0; i < 8; i++) {
2451
- if (existsSync5(join11(dir, "package.json")) && existsSync5(join11(dir, "mcp-server"))) return dir;
2455
+ if (existsSync6(join11(dir, "package.json")) && existsSync6(join11(dir, "mcp-server"))) return dir;
2452
2456
  const next = resolve3(dir, "..");
2453
2457
  if (next === dir) break;
2454
2458
  dir = next;
@@ -3728,7 +3732,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3728
3732
  // src/ijfw.js
3729
3733
  import { dirname as dirname4, join as join12, resolve as resolve4, basename as basename2 } from "node:path";
3730
3734
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3731
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, copyFileSync as copyFileSync3, readdirSync as readdirSync5, rmSync as rmSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
3735
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, copyFileSync as copyFileSync3, readdirSync as readdirSync5, rmSync as rmSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
3732
3736
  import { homedir, platform } from "node:os";
3733
3737
  import { spawnSync as spawnSync12 } from "node:child_process";
3734
3738
 
@@ -3929,6 +3933,26 @@ var COMMAND_REGISTRY = Object.freeze([
3929
3933
  since: "1.4.0",
3930
3934
  status: "active"
3931
3935
  },
3936
+ {
3937
+ name: "personalize",
3938
+ tier: "coordination",
3939
+ owner: "orchestrator",
3940
+ description: "Control the profile-bus learning feature (on|off|status|forget)",
3941
+ aliases: [],
3942
+ since: "1.6.0",
3943
+ status: "active",
3944
+ notes: 'Opt-in injection control. `on`/`off` toggle whether your learned style is added to prompts; `status` shows flags + what was inferred; `forget` deletes inferences. Capture is always local; injection defaults to "ask".'
3945
+ },
3946
+ {
3947
+ name: "checkpoint",
3948
+ tier: "coordination",
3949
+ owner: "orchestrator",
3950
+ description: "Snapshot swarm/memory state (alias for `memory checkpoint`)",
3951
+ aliases: [],
3952
+ since: "1.6.0",
3953
+ status: "active",
3954
+ notes: "Thin top-level alias forwarding to `ijfw memory checkpoint <label>`. Added so the documented bare verb routes instead of returning Unknown."
3955
+ },
3932
3956
  {
3933
3957
  name: "design",
3934
3958
  tier: "coordination",
@@ -4002,6 +4026,16 @@ var COMMAND_REGISTRY = Object.freeze([
4002
4026
  since: "1.5.0",
4003
4027
  status: "active"
4004
4028
  },
4029
+ {
4030
+ name: "worktree",
4031
+ tier: "plumbing",
4032
+ owner: "orchestrator",
4033
+ description: "Manage swarm task worktrees (alias for `swarm worktree`)",
4034
+ aliases: [],
4035
+ since: "1.6.0",
4036
+ status: "active",
4037
+ notes: "Thin top-level alias forwarding to `ijfw swarm worktree \u2026`. Added so the documented bare verb routes instead of returning Unknown."
4038
+ },
4005
4039
  {
4006
4040
  name: "--purge-receipts",
4007
4041
  tier: "plumbing",
@@ -4194,10 +4228,52 @@ function commandsByTier() {
4194
4228
 
4195
4229
  // src/ijfw.js
4196
4230
  var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
4231
+ var LAUNCHER_VERB_REDIRECTS = {
4232
+ on: "Did you mean: ijfw install \xB7 or ijfw personalize on",
4233
+ marketplace: "Not available in this release \u2014 install via `ijfw install` or your agent's native plugin marketplace UI.",
4234
+ cluster: "Multi-machine cluster mode is a design milestone, not shipped in this release.",
4235
+ "wave-status": "Did you mean: ijfw swarm status",
4236
+ "worktree-drain": "Did you mean: ijfw worktree cleanup <task-id>"
4237
+ };
4238
+ function launcherEditDistance(a, b2) {
4239
+ a = String(a);
4240
+ b2 = String(b2);
4241
+ const m2 = a.length, n = b2.length;
4242
+ if (!m2) return n;
4243
+ if (!n) return m2;
4244
+ let prev = Array.from({ length: n + 1 }, (_2, j2) => j2);
4245
+ const cur = Array.from({ length: n + 1 });
4246
+ for (let i = 1; i <= m2; i++) {
4247
+ cur[0] = i;
4248
+ for (let j2 = 1; j2 <= n; j2++) {
4249
+ const cost = a[i - 1] === b2[j2 - 1] ? 0 : 1;
4250
+ cur[j2] = Math.min(prev[j2] + 1, cur[j2 - 1] + 1, prev[j2 - 1] + cost);
4251
+ }
4252
+ prev = cur.slice();
4253
+ }
4254
+ return prev[n];
4255
+ }
4256
+ function launcherSuggest(raw) {
4257
+ const q2 = String(raw || "").toLowerCase();
4258
+ if (Object.prototype.hasOwnProperty.call(LAUNCHER_VERB_REDIRECTS, q2)) {
4259
+ return LAUNCHER_VERB_REDIRECTS[q2];
4260
+ }
4261
+ const candidates = [...ALL_COMMAND_NAMES, ...ORCHESTRATOR_COMMAND_NAMES, ...INSTALLER_DIRECT_COMMAND_NAMES].filter((c2) => c2 && !c2.startsWith("--"));
4262
+ let best = null, bestD = Infinity;
4263
+ for (const c2 of candidates) {
4264
+ const d = launcherEditDistance(q2, c2.toLowerCase());
4265
+ if (d < bestD) {
4266
+ bestD = d;
4267
+ best = c2;
4268
+ }
4269
+ }
4270
+ const tol = Math.max(2, Math.floor(0.4 * Math.max(q2.length, (best || "").length)));
4271
+ return best && bestD <= tol ? `Did you mean: ijfw ${best}` : null;
4272
+ }
4197
4273
  function repoRoot() {
4198
4274
  let dir = __dirname2;
4199
4275
  for (let i = 0; i < 6; i++) {
4200
- if (existsSync6(join12(dir, "package.json")) && existsSync6(join12(dir, ".git"))) return dir;
4276
+ if (existsSync7(join12(dir, "package.json")) && existsSync7(join12(dir, ".git"))) return dir;
4201
4277
  dir = resolve4(dir, "..");
4202
4278
  }
4203
4279
  return process.cwd();
@@ -4206,7 +4282,7 @@ function findInternalAsset(...rel) {
4206
4282
  const root = repoRoot();
4207
4283
  const ijfwHome = join12(homedir(), ".ijfw");
4208
4284
  const candidates = [join12(root, ...rel), join12(ijfwHome, ...rel)];
4209
- return candidates.find((p) => existsSync6(p)) || null;
4285
+ return candidates.find((p) => existsSync7(p)) || null;
4210
4286
  }
4211
4287
  function readDashboardPort() {
4212
4288
  const portFile = join12(homedir(), ".ijfw", "dashboard.port");
@@ -4266,7 +4342,7 @@ function findCli() {
4266
4342
  join12(repoRoot(), "mcp-server", "src", "cross-orchestrator-cli.js"),
4267
4343
  join12(homedir(), ".ijfw", "mcp-server", "src", "cross-orchestrator-cli.js")
4268
4344
  ];
4269
- return candidates.find((p) => existsSync6(p)) || null;
4345
+ return candidates.find((p) => existsSync7(p)) || null;
4270
4346
  }
4271
4347
  function delegateToCli(argTail) {
4272
4348
  const cli = findCli();
@@ -4425,7 +4501,7 @@ async function main() {
4425
4501
  console.error("Design companion accepts standalone .html files.");
4426
4502
  process.exit(1);
4427
4503
  }
4428
- if (!existsSync6(abs)) {
4504
+ if (!existsSync7(abs)) {
4429
4505
  console.error(`File not found: ${abs}`);
4430
4506
  process.exit(1);
4431
4507
  }
@@ -4453,9 +4529,9 @@ async function main() {
4453
4529
  resolve4(__dirname2, "..", "docs", "GUIDE.md"),
4454
4530
  join12(homedir(), ".ijfw", "docs", "GUIDE.md")
4455
4531
  ];
4456
- const guidePath = candidates.find((p) => existsSync6(p));
4532
+ const guidePath = candidates.find((p) => existsSync7(p));
4457
4533
  if (!guidePath) {
4458
- console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://gitlab.com/therealseandonahoe/ijfw/-/blob/main/docs/GUIDE.md");
4534
+ console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://github.com/FerroxLabs/ijfw/blob/main/docs/GUIDE.md");
4459
4535
  process.exit(1);
4460
4536
  }
4461
4537
  if (wantsBrowser) {
@@ -4463,7 +4539,7 @@ async function main() {
4463
4539
  const assetsSrc = join12(dirname4(guidePath), "guide", "assets");
4464
4540
  const outDir = join12(homedir(), ".ijfw", "guide");
4465
4541
  mkdirSync5(join12(outDir, "assets"), { recursive: true });
4466
- if (existsSync6(assetsSrc)) {
4542
+ if (existsSync7(assetsSrc)) {
4467
4543
  for (const f of readdirSync5(assetsSrc)) {
4468
4544
  copyFileSync3(join12(assetsSrc, f), join12(outDir, "assets", f));
4469
4545
  }
@@ -4516,8 +4592,10 @@ async function main() {
4516
4592
  break;
4517
4593
  }
4518
4594
  default: {
4519
- console.error(`Unknown subcommand: ${sub}`);
4520
- printHelp2();
4595
+ console.error(`Unknown command: ${sub}`);
4596
+ const hint = launcherSuggest(sub);
4597
+ if (hint) console.error(hint);
4598
+ console.error("Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.");
4521
4599
  process.exit(1);
4522
4600
  }
4523
4601
  }
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) {
@@ -1302,6 +1370,18 @@ function installAntigravity(ctx) {
1302
1370
  printOk(`Merged MCP into ${ideDst} + ${cliDst} (Antigravity IDE + CLI)`);
1303
1371
  return { status: "ok" };
1304
1372
  }
1373
+ function installPi(ctx) {
1374
+ if (ctx.ijfwCustomDir) {
1375
+ printInfo("Custom-dir install -- skipping Pi merges.");
1376
+ printOk("Pi: real platform config left untouched.");
1377
+ return { status: "noop" };
1378
+ }
1379
+ const agentsSrc = path.join(ctx.repoRoot, "pi", "AGENTS.md");
1380
+ const agentsDst = path.join(ctx.home, ".pi", "agent", "AGENTS.md");
1381
+ copyIfMissing(agentsSrc, agentsDst);
1382
+ printOk("Pi: rules-only install (~/.pi/agent/AGENTS.md). No MCP -- Pi has no native MCP client (extension bridge required for memory).");
1383
+ return { status: "ok" };
1384
+ }
1305
1385
  var init_install_targets_8_14 = __esm({
1306
1386
  "src/install-targets-8-14.js"() {
1307
1387
  init_install_helpers();
@@ -1931,7 +2011,8 @@ var init_install_flow = __esm({
1931
2011
  "kimi",
1932
2012
  "openclaw",
1933
2013
  "aider",
1934
- "antigravity"
2014
+ "antigravity",
2015
+ "pi"
1935
2016
  ];
1936
2017
  TARGET_FNS = {
1937
2018
  claude: installClaude,
@@ -1948,7 +2029,8 @@ var init_install_flow = __esm({
1948
2029
  kimi: installKimi,
1949
2030
  openclaw: installOpenclaw,
1950
2031
  aider: installAider,
1951
- antigravity: installAntigravity
2032
+ antigravity: installAntigravity,
2033
+ pi: installPi
1952
2034
  };
1953
2035
  install_flow_default = { runInstall, CANONICAL_ORDER };
1954
2036
  }
package/dist/uninstall.js CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  // src/uninstall.js
4
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 } from "node:path";
5
+ import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
6
6
  import { homedir as homedir2, tmpdir } from "node:os";
7
7
  import { spawnSync } from "node:child_process";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
9
 
9
10
  // src/marketplace.js
10
11
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
@@ -95,6 +96,30 @@ function unmergeMarketplace(settingsPath = claudeSettingsPath()) {
95
96
  }
96
97
 
97
98
  // src/uninstall.js
99
+ var __filename = fileURLToPath(import.meta.url);
100
+ var __dirname = dirname2(__filename);
101
+ var REPO_ROOT = resolve2(__dirname, "..", "..");
102
+ function resolveAiderTemplate(name, repoRoot) {
103
+ const root = repoRoot || REPO_ROOT;
104
+ const candidates = [
105
+ // (a) git clone: top-level aider/ under the (injected) repo root.
106
+ join2(root, "aider", name),
107
+ // (a') repo root with staged templates under installer/.
108
+ join2(root, "installer", "templates", "aider", name),
109
+ // (b) tarball/dist fallback: templates staged next to the package root.
110
+ // dist/uninstall.js -> __dirname=<pkg>/dist -> <pkg>/templates/aider/<name>
111
+ // src/uninstall.js -> __dirname=<pkg>/src -> <pkg>/templates/aider/<name>
112
+ // (__dirname's parent is the package root in both layouts)
113
+ resolve2(__dirname, "..", "templates", "aider", name)
114
+ ];
115
+ for (const c of candidates) {
116
+ try {
117
+ if (existsSync2(c)) return c;
118
+ } catch {
119
+ }
120
+ }
121
+ return "";
122
+ }
98
123
  function writeAtomic(target, content) {
99
124
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
100
125
  writeFileSync2(tmp, content);
@@ -160,6 +185,54 @@ function backupFile(p) {
160
185
  }
161
186
  return null;
162
187
  }
188
+ function stripIjfwRegions(src) {
189
+ if (typeof src !== "string") return { text: src, changed: false };
190
+ const before = src;
191
+ let out = src;
192
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
193
+ const regions = [
194
+ ["<!-- IJFW-MEMORY-START", "<!-- IJFW-MEMORY-END -->"],
195
+ ["<!-- IJFW-ROUTING-START", "<!-- IJFW-ROUTING-END -->"],
196
+ ["<!-- IJFW-AGENTS-START", "<!-- IJFW-AGENTS-END -->"],
197
+ ["<!-- IJFW-BLACKBOARD-START", "<!-- IJFW-BLACKBOARD-END -->"],
198
+ ["<!-- IJFW-DISCIPLINE-START", "<!-- IJFW-DISCIPLINE-END -->"]
199
+ ];
200
+ for (const [start, end] of regions) {
201
+ const re = new RegExp("\\n*" + esc(start) + "[\\s\\S]*?" + esc(end) + "[^\\n]*", "g");
202
+ out = out.replace(re, "");
203
+ }
204
+ out = out.replace(/\n*<!-- Auto-generated by IJFW from repo scan\.[^\n]*-->/g, "");
205
+ out = out.replace(/\n*(?:Four|Five) IJFW-managed regions live in this file\.[\s\S]*?IJFW will never touch it\./g, "");
206
+ out = out.replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
207
+ if (out.length) out += "\n";
208
+ return { text: out, changed: out !== before };
209
+ }
210
+ function hasIjfwMarker(text) {
211
+ return /IJFW-MEMORY-START|IJFW-ROUTING-START|IJFW-AGENTS-START|IJFW-BLACKBOARD-START|IJFW-DISCIPLINE-START/.test(text);
212
+ }
213
+ function stripMarkerFile(p, opts = {}) {
214
+ try {
215
+ if (!existsSync2(p)) return null;
216
+ let text;
217
+ try {
218
+ text = readFileSync2(p, "utf8");
219
+ } catch {
220
+ return null;
221
+ }
222
+ if (!hasIjfwMarker(text)) return null;
223
+ const { text: stripped, changed } = stripIjfwRegions(text);
224
+ if (!changed) return null;
225
+ backupFile(p);
226
+ if (opts.deleteIfEmpty && stripped.trim() === "") {
227
+ rmSync(p, { force: true });
228
+ return `${opts.label || p} (removed -- became empty after IJFW region strip)`;
229
+ }
230
+ writeAtomic(p, stripped);
231
+ return `${opts.label || p} (stripped IJFW marker regions, user content preserved)`;
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
163
236
  function removeTomlSection(p) {
164
237
  if (!existsSync2(p)) return false;
165
238
  backupFile(p);
@@ -195,6 +268,82 @@ function removeJsonMcpEntry(p) {
195
268
  }
196
269
  return changed;
197
270
  }
271
+ function removeNestedMcpEntry(p, keyPath) {
272
+ try {
273
+ if (!existsSync2(p)) return false;
274
+ let doc;
275
+ try {
276
+ doc = JSON.parse(readFileSync2(p, "utf8"));
277
+ } catch {
278
+ return false;
279
+ }
280
+ if (!doc || typeof doc !== "object") return false;
281
+ let node = doc;
282
+ for (const k of keyPath) {
283
+ if (!node[k] || typeof node[k] !== "object") return false;
284
+ node = node[k];
285
+ }
286
+ if (!node["ijfw-memory"]) return false;
287
+ backupFile(p);
288
+ delete node["ijfw-memory"];
289
+ writeAtomic(p, JSON.stringify(doc, null, 2) + "\n");
290
+ return true;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ function resolveClineSettingsPath(home) {
296
+ const H = home;
297
+ const APPDATA = process.env.APPDATA || join2(H, "AppData", "Roaming");
298
+ const ext = "saoudrizwan.claude-dev";
299
+ let userDirs;
300
+ if (process.platform === "darwin") {
301
+ 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")
305
+ ];
306
+ } else if (process.platform === "win32") {
307
+ userDirs = [
308
+ join2(APPDATA, "Code", "User"),
309
+ join2(APPDATA, "Code - Insiders", "User"),
310
+ join2(APPDATA, "VSCodium", "User")
311
+ ];
312
+ } else {
313
+ 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")
318
+ ];
319
+ }
320
+ for (const d of userDirs) {
321
+ const settings = join2(d, "globalStorage", ext, "settings", "cline_mcp_settings.json");
322
+ if (existsSync2(settings)) return settings;
323
+ }
324
+ return null;
325
+ }
326
+ function removeAiderFileIfPristine(installedPath, templatePath) {
327
+ try {
328
+ if (!existsSync2(installedPath)) return "absent";
329
+ if (!existsSync2(templatePath)) return "kept-modified";
330
+ let a, b;
331
+ try {
332
+ a = readFileSync2(installedPath);
333
+ b = readFileSync2(templatePath);
334
+ } catch {
335
+ return "kept-modified";
336
+ }
337
+ if (a.equals(b)) {
338
+ backupFile(installedPath);
339
+ rmSync(installedPath, { force: true });
340
+ return "removed";
341
+ }
342
+ return "kept-modified";
343
+ } catch {
344
+ return "kept-modified";
345
+ }
346
+ }
198
347
  function removeCodexHooks(p) {
199
348
  if (!existsSync2(p)) return false;
200
349
  let doc;
@@ -322,60 +471,174 @@ function removeCodexCommands(dir) {
322
471
  }
323
472
  return count;
324
473
  }
325
- function cleanPlatforms() {
474
+ function cleanPlatforms(opts = {}) {
475
+ const home = opts.home || HOME;
476
+ const cwd = opts.cwd || process.cwd();
477
+ const repoRoot = opts.repoRoot || REPO_ROOT;
326
478
  const removed = [];
327
- if (removeTomlSection(join2(HOME, ".codex", "config.toml"))) {
479
+ if (removeTomlSection(join2(home, ".codex", "config.toml"))) {
328
480
  removed.push("~/.codex/config.toml (removed [mcp_servers.ijfw-memory])");
329
481
  }
330
- if (removeCodexHooks(join2(HOME, ".codex", "hooks.json"))) {
482
+ if (removeCodexHooks(join2(home, ".codex", "hooks.json"))) {
331
483
  removed.push("~/.codex/hooks.json (removed IJFW hook entries)");
332
484
  }
333
- const codexSkills = removeIjfwSkills(join2(HOME, ".codex", "skills"));
485
+ const codexSkills = removeIjfwSkills(join2(home, ".codex", "skills"));
334
486
  if (codexSkills > 0) removed.push(`~/.codex/skills/ijfw-* (removed ${codexSkills} skill dirs)`);
335
- const codexCommands = removeCodexCommands(join2(HOME, ".codex", "commands"));
487
+ const codexCommands = removeCodexCommands(join2(home, ".codex", "commands"));
336
488
  if (codexCommands > 0) removed.push(`~/.codex/commands (removed ${codexCommands} IJFW command aliases)`);
337
- const codexMd = join2(HOME, ".codex", "IJFW.md");
489
+ const codexMd = join2(home, ".codex", "IJFW.md");
338
490
  if (existsSync2(codexMd)) {
339
491
  rmSync(codexMd, { force: true });
340
492
  removed.push("~/.codex/IJFW.md");
341
493
  }
342
- if (removeJsonMcpEntry(join2(HOME, ".gemini", "settings.json"))) {
494
+ if (removeJsonMcpEntry(join2(home, ".gemini", "settings.json"))) {
343
495
  removed.push("~/.gemini/settings.json (removed ijfw-memory)");
344
496
  }
345
- const geminiExt = join2(HOME, ".gemini", "extensions", "ijfw");
497
+ const geminiExt = join2(home, ".gemini", "extensions", "ijfw");
346
498
  if (existsSync2(geminiExt)) {
347
499
  rmSync(geminiExt, { recursive: true, force: true });
348
500
  removed.push("~/.gemini/extensions/ijfw/");
349
501
  }
350
- const cursorMcp = join2(".cursor", "mcp.json");
502
+ const cursorMcp = join2(cwd, ".cursor", "mcp.json");
351
503
  if (removeJsonMcpEntry(cursorMcp)) removed.push(".cursor/mcp.json (removed ijfw-memory)");
352
- if (removeJsonMcpEntry(join2(HOME, ".codeium", "windsurf", "mcp_config.json"))) {
504
+ if (removeJsonMcpEntry(join2(home, ".codeium", "windsurf", "mcp_config.json"))) {
353
505
  removed.push("~/.codeium/windsurf/mcp_config.json (removed ijfw-memory)");
354
506
  }
355
- const vscodeMcp = join2(".vscode", "mcp.json");
507
+ const vscodeMcp = join2(cwd, ".vscode", "mcp.json");
356
508
  if (removeJsonMcpEntry(vscodeMcp)) removed.push(".vscode/mcp.json (removed ijfw-memory)");
357
- if (removeYamlMcpEntry(join2(HOME, ".hermes", "config.yaml"))) {
509
+ if (removeYamlMcpEntry(join2(home, ".hermes", "config.yaml"))) {
358
510
  removed.push("~/.hermes/config.yaml (removed ijfw-memory)");
359
511
  }
360
- const hermesSkills = removeIjfwSkills(join2(HOME, ".hermes", "skills"));
512
+ const hermesSkills = removeIjfwSkills(join2(home, ".hermes", "skills"));
361
513
  if (hermesSkills > 0) removed.push(`~/.hermes/skills/ijfw-* (removed ${hermesSkills} skill dirs)`);
362
- const hermesMd = join2(HOME, ".hermes", "HERMES.md");
514
+ const hermesMd = join2(home, ".hermes", "HERMES.md");
363
515
  if (existsSync2(hermesMd)) {
364
516
  rmSync(hermesMd, { force: true });
365
517
  removed.push("~/.hermes/HERMES.md");
366
518
  }
367
- if (removeYamlMcpEntry(join2(HOME, ".wayland", "config.yaml"))) {
368
- removed.push("~/.wayland/config.yaml (removed ijfw-memory)");
519
+ const waylandPluginDir = join2(home, ".wayland", "plugins", "ijfw");
520
+ if (existsSync2(waylandPluginDir)) {
521
+ rmSync(waylandPluginDir, { recursive: true, force: true });
522
+ removed.push("~/.wayland/plugins/ijfw/ (removed plugin.toml + hooks + MCP)");
369
523
  }
370
- const waylandSkills = removeIjfwSkills(join2(HOME, ".wayland", "skills"));
524
+ if (removeYamlMcpEntry(join2(home, ".wayland", "config.yaml"))) {
525
+ removed.push("~/.wayland/config.yaml (removed legacy ijfw-memory)");
526
+ }
527
+ const waylandSkills = removeIjfwSkills(join2(home, ".wayland", "skills"));
371
528
  if (waylandSkills > 0) removed.push(`~/.wayland/skills/ijfw-* (removed ${waylandSkills} skill dirs)`);
372
- const waylandMd = join2(HOME, ".wayland", "WAYLAND.md");
529
+ const waylandMd = join2(home, ".wayland", "WAYLAND.md");
373
530
  if (existsSync2(waylandMd)) {
374
531
  rmSync(waylandMd, { force: true });
375
532
  removed.push("~/.wayland/WAYLAND.md");
376
533
  }
534
+ if (removeJsonMcpEntry(join2(home, ".qwen", "settings.json"))) {
535
+ removed.push("~/.qwen/settings.json (removed ijfw-memory)");
536
+ }
537
+ if (removeJsonMcpEntry(join2(home, ".kimi", "mcp.json"))) {
538
+ removed.push("~/.kimi/mcp.json (removed ijfw-memory)");
539
+ }
540
+ if (removeJsonMcpEntry(join2(home, ".gemini", "antigravity", "mcp_config.json"))) {
541
+ removed.push("~/.gemini/antigravity/mcp_config.json (removed ijfw-memory)");
542
+ }
543
+ if (removeJsonMcpEntry(join2(home, ".gemini", "config", "mcp_config.json"))) {
544
+ removed.push("~/.gemini/config/mcp_config.json (removed ijfw-memory)");
545
+ }
546
+ if (removeNestedMcpEntry(join2(home, ".config", "opencode", "opencode.json"), ["mcp"])) {
547
+ removed.push("~/.config/opencode/opencode.json (removed mcp.ijfw-memory)");
548
+ }
549
+ if (removeNestedMcpEntry(join2(home, ".openclaw", "openclaw.json"), ["mcp", "servers"])) {
550
+ removed.push("~/.openclaw/openclaw.json (removed mcp.servers.ijfw-memory)");
551
+ }
552
+ const piStatus = stripMarkerFile(join2(home, ".pi", "agent", "AGENTS.md"), {
553
+ label: "~/.pi/agent/AGENTS.md"
554
+ });
555
+ if (piStatus) removed.push(piStatus);
556
+ const clineSettings = resolveClineSettingsPath(home);
557
+ if (clineSettings) {
558
+ if (removeJsonMcpEntry(clineSettings)) {
559
+ removed.push(`${clineSettings} (removed ijfw-memory)`);
560
+ }
561
+ } else {
562
+ removed.push("Cline: no globalStorage found -- if you use Cline, remove the ijfw-memory MCP entry manually.");
563
+ }
564
+ const confResult = removeAiderFileIfPristine(
565
+ join2(home, ".aider.conf.yml"),
566
+ resolveAiderTemplate("aider.conf.yml", repoRoot)
567
+ );
568
+ if (confResult === "removed") {
569
+ removed.push("~/.aider.conf.yml (removed -- matched shipped template)");
570
+ } else if (confResult === "kept-modified") {
571
+ removed.push("~/.aider.conf.yml (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
572
+ }
573
+ const convResult = removeAiderFileIfPristine(
574
+ join2(home, "CONVENTIONS.md"),
575
+ resolveAiderTemplate("CONVENTIONS.md", repoRoot)
576
+ );
577
+ if (convResult === "removed") {
578
+ removed.push("~/CONVENTIONS.md (removed -- matched shipped template)");
579
+ } else if (convResult === "kept-modified") {
580
+ removed.push("~/CONVENTIONS.md (KEPT -- differs from shipped template; remove manually if it is IJFW-only)");
581
+ }
377
582
  return removed;
378
583
  }
584
+ function parseRegistryPaths(registryPath) {
585
+ try {
586
+ if (!existsSync2(registryPath)) return [];
587
+ const raw = readFileSync2(registryPath, "utf8");
588
+ const paths = [];
589
+ for (const line of raw.split("\n")) {
590
+ const trimmed = line.trim();
591
+ if (!trimmed) continue;
592
+ const first = trimmed.split("|")[0].trim();
593
+ if (!first) continue;
594
+ if (!first.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(first)) continue;
595
+ paths.push(first);
596
+ }
597
+ return paths;
598
+ } catch {
599
+ return [];
600
+ }
601
+ }
602
+ function stripRegisteredProjectBlocks(opts = {}) {
603
+ const home = opts.home || HOME;
604
+ const cwd = opts.cwd || process.cwd();
605
+ const registryPath = opts.registryPath || join2(home, ".ijfw", "registry.md");
606
+ const results = [];
607
+ for (const projPath of parseRegistryPaths(registryPath)) {
608
+ let dirExists = false;
609
+ try {
610
+ dirExists = existsSync2(projPath);
611
+ } catch {
612
+ dirExists = false;
613
+ }
614
+ if (!dirExists) continue;
615
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
616
+ const filePath = join2(projPath, name);
617
+ const status = stripMarkerFile(filePath, { label: join2(projPath, name) });
618
+ if (status) results.push(status);
619
+ }
620
+ }
621
+ try {
622
+ const cursorRule = join2(cwd, ".cursor", "rules", "ijfw.mdc");
623
+ if (existsSync2(cursorRule)) {
624
+ backupFile(cursorRule);
625
+ rmSync(cursorRule, { force: true });
626
+ results.push(".cursor/rules/ijfw.mdc (removed -- wholly IJFW-authored)");
627
+ }
628
+ } catch {
629
+ }
630
+ const windsurfStatus = stripMarkerFile(join2(cwd, ".windsurfrules"), {
631
+ label: ".windsurfrules",
632
+ deleteIfEmpty: true
633
+ });
634
+ if (windsurfStatus) results.push(windsurfStatus);
635
+ const copilotStatus = stripMarkerFile(join2(cwd, ".github", "copilot-instructions.md"), {
636
+ label: ".github/copilot-instructions.md",
637
+ deleteIfEmpty: true
638
+ });
639
+ if (copilotStatus) results.push(copilotStatus);
640
+ return results;
641
+ }
379
642
  function resolveTarget(opt) {
380
643
  if (opt.dir) return resolve2(opt.dir);
381
644
  if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
@@ -433,13 +696,32 @@ async function main() {
433
696
  console.log(" platform configs cleaned:");
434
697
  for (const line of cleaned) console.log(` ${line}`);
435
698
  }
699
+ const projectCleaned = stripRegisteredProjectBlocks();
700
+ if (projectCleaned.length > 0) {
701
+ console.log(" project blocks cleaned:");
702
+ for (const line of projectCleaned) console.log(` ${line}`);
703
+ }
436
704
  } else {
437
705
  console.log(` custom-dir uninstall (${target}) -- platform configs in your real home left untouched.`);
438
706
  }
439
707
  console.log("\nIJFW uninstalled. Thanks for trying it.");
440
708
  process.exit(0);
441
709
  }
442
- main().catch((e) => {
443
- console.error(e.message || String(e));
444
- process.exit(1);
445
- });
710
+ var isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
711
+ if (isDirectRun) {
712
+ main().catch((e) => {
713
+ console.error(e.message || String(e));
714
+ process.exit(1);
715
+ });
716
+ }
717
+ export {
718
+ cleanPlatforms,
719
+ parseRegistryPaths,
720
+ removeAiderFileIfPristine,
721
+ removeNestedMcpEntry,
722
+ resolveAiderTemplate,
723
+ resolveClineSettingsPath,
724
+ stripIjfwRegions,
725
+ stripMarkerFile,
726
+ stripRegisteredProjectBlocks
727
+ };
package/docs/GUIDE.md CHANGED
@@ -232,11 +232,30 @@ Every command ships in three forms: a shell command, a Claude Code slash command
232
232
  | Command | Purpose |
233
233
  |---------|---------|
234
234
  | `ijfw memory checkpoint "<text>"` | Snapshot a decision, pattern, or note to local memory. |
235
+ | `ijfw checkpoint "<text>"` | Top-level shortcut for `ijfw memory checkpoint`. |
235
236
  | `ijfw memory search "<query>"` | BM25 ranked search over local memory. Add `--scope all` to search every registered IJFW project. |
236
237
  | `ijfw import claude-mem` | Absorb existing claude-mem SQLite memory into IJFW markdown. |
237
238
  | `ijfw import claude-mem --all` | Discover projects automatically, import in bulk. |
238
239
  | `ijfw import claude-mem --dry-run` | Show what would happen first. |
239
240
 
241
+ ### Personalize (profile-bus learning)
242
+
243
+ IJFW can learn your low-sensitivity interaction *style* (verbosity, formality,
244
+ code-vs-prose) locally and — only if you opt in — include a short brief in
245
+ prompts so agents match it. Capture is always local and never includes raw
246
+ text. Injection is **off until you turn it on**.
247
+
248
+ | Command | Purpose |
249
+ |---------|---------|
250
+ | `ijfw personalize status` | Show the current flags plus a summary of what was inferred. |
251
+ | `ijfw personalize on` | Enable injecting the learned low-sensitivity style brief into prompts. |
252
+ | `ijfw personalize off` | Disable injection. Capture continues locally. |
253
+ | `ijfw personalize forget [pattern]` | Delete inferences (no pattern = forget all). |
254
+ | `ijfw personalize share-sensitive on\|off` | Allow/deny medium+high-sensitivity prefs to allowlisted hosts. Default off. |
255
+
256
+ Hard override: set `IJFW_PROFILE_KILL=1` to force-disable all injection
257
+ regardless of these settings.
258
+
240
259
  ### Multi-AI Trident
241
260
 
242
261
  | Command | Purpose |
@@ -521,7 +540,7 @@ Runtime: zero npm dependencies. Tokens: the cross-AI Trident uses your existing
521
540
  Yes, and verifiable in your own metrics. Sources: right-model dispatch (a cheaper tier when adequate, the heavyweight when needed), prompt cache discipline, output rules that cut padding, context discipline that stops re-pasting. Typical observed: 25 percent or more output reduction versus unmanaged baseline. The log is in your project.
522
541
 
523
542
  **Can I turn it off?**
524
- Yes. `ijfw off` disables the core skill. Each command is isolated. The MCP server can be unregistered per platform. Backups are timestamped. Nothing is sticky.
543
+ Yes. `ijfw off` removes what IJFW added: it unregisters the MCP server across all 15 platforms, deletes IJFW skill/command/context files, and strips IJFW's managed marker regions out of every project `CLAUDE.md` / `AGENTS.md` it ever touched -- your own content in those files is preserved. Every file it edits is backed up with a timestamped `.bak` first. Your memory at `~/.ijfw/memory/` is kept (delete by hand, or `ijfw off --purge` to remove it too). Files it can't prove were IJFW-authored (e.g. hand-edited Aider rules) are left in place and called out so you can remove them yourself.
525
544
 
526
545
  **What about my existing memory in claude-mem or other tools?**
527
546
  `ijfw import claude-mem` round-trips the SQLite store into IJFW markdown. Idempotent. Safe to rerun. `--dry-run` shows what would happen first.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.5.5",
3
+ "version": "1.6.0",
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": {
@@ -13,6 +13,7 @@
13
13
  "src/install.ps1",
14
14
  "docs/GUIDE.md",
15
15
  "docs/guide/assets",
16
+ "templates/aider/**",
16
17
  "scripts/pack-hub-extension.js",
17
18
  "scripts/hub-extension/**",
18
19
  "README.md",
@@ -21,8 +22,9 @@
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "node scripts/build.js",
24
- "test": "node --test test.js",
25
+ "test": "node --test test.js test/uninstall-completeness.test.mjs test/project-write-guard.test.mjs",
25
26
  "test:hub-extension": "node --test test/test-pack-hub-extension.js",
27
+ "test:uninstall-completeness": "node --test test/uninstall-completeness.test.mjs",
26
28
  "preflight": "node dist/ijfw.js preflight",
27
29
  "pack:check": "npm pack --dry-run",
28
30
  "pack:hub-extension": "node scripts/pack-hub-extension.js",
package/src/install.ps1 CHANGED
@@ -6,7 +6,7 @@
6
6
  # -> merge marketplace into %USERPROFILE%\.claude\settings.json -> summary.
7
7
  #
8
8
  # Usage:
9
- # Invoke-Expression (iwr https://gitlab.com/therealseandonahoe/ijfw/-/raw/main/installer/src/install.ps1).Content
9
+ # Invoke-Expression (iwr https://raw.githubusercontent.com/FerroxLabs/ijfw/main/installer/src/install.ps1).Content
10
10
  # or:
11
11
  # .\install.ps1 -Dir C:\Users\me\.ijfw -Branch main
12
12
 
@@ -19,7 +19,7 @@ param(
19
19
  )
20
20
 
21
21
  $ErrorActionPreference = "Stop"
22
- $DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git"
22
+ $DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git"
23
23
 
24
24
  function Write-Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green }
25
25
  function Write-Info($msg) { Write-Host " ... $msg" -ForegroundColor Gray }
@@ -101,7 +101,11 @@ function Invoke-CloneOrPull($target, $branch) {
101
101
  $currentOrigin = ($currentOriginRaw | Out-String).Trim()
102
102
  $stalePatterns = @(
103
103
  '^https://github\.com/seandonahoe/ijfw(\.git)?/?$',
104
- '^https://github\.com/therealseandonahoe/ijfw(\.git)?/?$'
104
+ '^https://github\.com/therealseandonahoe/ijfw(\.git)?/?$',
105
+ # V155 rebrand: GitLab was canonical through v1.5.4; Windows users who
106
+ # installed from gitlab.com need their origin migrated forward to
107
+ # FerroxLabs/ijfw on GitHub. Mirrors install.js STALE_PATTERNS.
108
+ '^https://gitlab\.com/therealseandonahoe/ijfw(\.git)?/?$'
105
109
  )
106
110
  $isStale = $false
107
111
  foreach ($pat in $stalePatterns) {
@@ -278,7 +282,7 @@ function Remove-StalePosixLaunchers {
278
282
  $removed++
279
283
  }
280
284
  } catch {
281
- # Best-effort: keep going on locked / inaccessible files.
285
+ $null = $_ # best-effort: keep going on locked / inaccessible files
282
286
  }
283
287
  }
284
288
  if ($removed -gt 0) {
@@ -319,7 +323,7 @@ function Provision-Plugin {
319
323
  }
320
324
  Copy-Item $srcItem.FullName $dstItem -Force
321
325
  # Preserve source mtime so next install doesn't mistake our copy for a user edit.
322
- try { (Get-Item $dstItem -ErrorAction Stop).LastWriteTime = $srcItem.LastWriteTime } catch {}
326
+ try { (Get-Item $dstItem -ErrorAction Stop).LastWriteTime = $srcItem.LastWriteTime } catch { $null = $_ }
323
327
  }
324
328
  }
325
329
 
@@ -0,0 +1,54 @@
1
+ # IJFW Conventions for Aider
2
+
3
+ <!-- Aider MCP support last verified: 2026-05-06 against https://aider.chat/docs/.
4
+ If Aider adds a native MCP client, regenerate this file and the rules-only
5
+ section in scripts/install.sh:aider). -->
6
+
7
+ Aider doesn't have native MCP, so IJFW's memory + cross-audit tools aren't
8
+ available inside Aider sessions. These conventions carry the IJFW spirit
9
+ (disciplined workflow, terse output, no scope creep) into the Aider chat.
10
+
11
+ ## Workflow
12
+
13
+ - One question at a time. Don't dump multi-step plans before user signs off.
14
+ - Lead with the answer. No restating the question.
15
+ - For multi-file changes, propose the plan in chat FIRST. Wait for user "go".
16
+
17
+ ## Code
18
+
19
+ - Match existing style. Don't refactor adjacent code that wasn't asked for.
20
+ - No speculative abstractions. Three similar lines beats a premature helper.
21
+ - No error handling for impossible scenarios. Trust internal code.
22
+ - Default to writing no comments. Only add WHEN the WHY is non-obvious.
23
+
24
+ ## Memory + cross-audit
25
+
26
+ Aider sessions don't see IJFW's persistent memory. After significant work:
27
+
28
+ - Run `ijfw cross audit <file>` in your terminal to get Trident review.
29
+ - Use `ijfw_memory_store` from Claude/Codex/Gemini sessions to persist
30
+ decisions Aider makes -- they won't survive otherwise.
31
+
32
+ ## Scope
33
+
34
+ Stay in the lane the user asked for. If you spot adjacent issues, mention them
35
+ in chat -- don't fix them silently.
36
+
37
+ ## DESIGN picker
38
+
39
+ If the user asks for a design contract and no `DESIGN.md` exists, suggest one
40
+ of the 12 IJFW curated templates (alphabetical):
41
+
42
+ bento-grid, brutalist-luxe, cinematic-dark, data-dense-dashboard,
43
+ editorial-warm, glassmorphic, magazine-editorial, maximalist-vibrant,
44
+ neo-swiss-tech, swiss-minimal, terminal-native, warm-organic.
45
+
46
+ Aider has no MCP client, so it cannot fetch the template body itself. Ask the
47
+ user to run this in any MCP-capable sibling CLI on their machine (Claude Code
48
+ / Codex / Gemini / Cursor / Windsurf / Copilot / Hermes / Wayland / OpenCode /
49
+ Qwen / Kimi / OpenClaw) and paste the output back into the Aider chat:
50
+
51
+ ijfw_memory_recall({ context_hint: "design_template:<name>" })
52
+
53
+ Once the user pastes the body, Aider writes `DESIGN.md` at the project root
54
+ and picks it up automatically on the next turn.
@@ -0,0 +1,23 @@
1
+ # IJFW-shipped Aider config. Aider has no native MCP client (1.1.7 status);
2
+ # this file scaffolds project conventions + auto-loads CONVENTIONS.md so
3
+ # IJFW principles travel into Aider sessions.
4
+ #
5
+ # Edit freely -- IJFW only writes this file if absent.
6
+
7
+ # Auto-load IJFW conventions on every Aider session.
8
+ read:
9
+ - ~/CONVENTIONS.md
10
+
11
+ # Default model -- override per-session with --model or per-project.
12
+ # model: gpt-4o
13
+ # weak-model: gpt-4o-mini
14
+
15
+ # Auto-commits OFF by default. Aider's auto-commit can produce noisy
16
+ # history; IJFW workflow prefers atomic commits at gate boundaries.
17
+ auto-commits: false
18
+
19
+ # Show diffs in the chat for transparency.
20
+ show-diffs: true
21
+
22
+ # Subtree-only -- don't grep the entire monorepo every turn.
23
+ subtree-only: true
@@ -1,49 +0,0 @@
1
- {
2
- "name": "ijfw",
3
- "displayName": "IJFW — AI Efficiency Layer",
4
- "version": "1.5.5",
5
- "description": "One install, every AI coding agent, zero config. Unifies 15 CLIs under a shared MCP memory layer so context follows you across Claude, Codex, Gemini, Cursor, Windsurf, and 10 more.",
6
- "author": "Sean Donahoe",
7
- "icon": "assets/ijfw-logo.svg",
8
- "dist": {
9
- "tarball": "extensions/ijfw-1.5.5.zip",
10
- "integrity": "sha512-vm3ot0+GhXYy0/HcStB/xmFjZhSXWG7jzSLuIPe43/Dla8qhyR9gsCwBI/8+RsUJqMXiPgc0GuxrgwW2f5DDWA==",
11
- "unpackedSize": 3312
12
- },
13
- "engines": {
14
- "wayland": ">=0.6.0"
15
- },
16
- "hubs": [
17
- "acpAdapters",
18
- "mcpServers"
19
- ],
20
- "contributes": {
21
- "acpAdapters": [
22
- "claude",
23
- "codex",
24
- "gemini",
25
- "cursor",
26
- "windsurf",
27
- "copilot",
28
- "hermes",
29
- "wayland",
30
- "aider",
31
- "opencode",
32
- "qwencode",
33
- "cline",
34
- "kimicode",
35
- "openclaw",
36
- "antigravity"
37
- ],
38
- "mcpServers": [
39
- "ijfw-memory"
40
- ]
41
- },
42
- "tags": [
43
- "ai",
44
- "coding",
45
- "mcp",
46
- "memory",
47
- "multi-agent"
48
- ]
49
- }