@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 +125 -47
- package/dist/install.js +138 -56
- package/dist/uninstall.js +305 -23
- package/docs/GUIDE.md +20 -1
- package/docs/guide/assets/ferrox-hero.png +0 -0
- package/package.json +4 -2
- package/src/install.ps1 +9 -5
- package/templates/aider/CONVENTIONS.md +54 -0
- package/templates/aider/aider.conf.yml +23 -0
- package/dist/hub-index-snippet.json +0 -49
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 =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 || !
|
|
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 =
|
|
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
|
|
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
|
|
1785
|
-
const
|
|
1786
|
-
const
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
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 (!
|
|
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) =>
|
|
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://
|
|
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 (
|
|
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
|
|
4520
|
-
|
|
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 (
|
|
888
|
+
if (guardProjectWrite(cwd, ctx.home, {
|
|
889
|
+
platformLabel: "Codex project skills/commands",
|
|
890
|
+
log: ctx.log
|
|
891
|
+
}) && (existsSync4(join4(cwd, ".codex", "config.toml")) || existsSync4(join4(cwd, ".ijfw")))) {
|
|
851
892
|
const projSkills = join4(cwd, ".codex", "skills");
|
|
852
893
|
ensureDir(projSkills);
|
|
853
894
|
for (const sd of listSubdirs(repoSkills)) {
|
|
@@ -942,9 +983,9 @@ async function installWayland(ctx) {
|
|
|
942
983
|
"Custom-dir install -- skipping ~/.wayland/ merges."
|
|
943
984
|
);
|
|
944
985
|
}
|
|
945
|
-
const
|
|
946
|
-
ensureDir(dirname4(
|
|
947
|
-
|
|
986
|
+
const pluginToml = join4(ctx.home, ".wayland", "plugins", "ijfw", "plugin.toml");
|
|
987
|
+
ensureDir(dirname4(pluginToml));
|
|
988
|
+
writeFileSync3(pluginToml, renderWaylandPluginToml(ctx), { encoding: "utf8" });
|
|
948
989
|
ensureDir(join4(ctx.home, ".wayland"));
|
|
949
990
|
copyIfAbsent(
|
|
950
991
|
join4(ctx.repoRoot, "wayland", "WAYLAND.md"),
|
|
@@ -955,56 +996,61 @@ async function installWayland(ctx) {
|
|
|
955
996
|
for (const sd of listSubdirs(sharedSkills)) {
|
|
956
997
|
copyDirIfAbsent(sd.path, join4(ctx.home, ".wayland", "skills", sd.name));
|
|
957
998
|
}
|
|
958
|
-
|
|
959
|
-
if (existsSync4(pluginSrc)) {
|
|
960
|
-
const pluginDst = join4(ctx.home, ".wayland", "plugins", "ijfw");
|
|
961
|
-
ensureDir(pluginDst);
|
|
962
|
-
let entries;
|
|
963
|
-
let readdirErr = null;
|
|
964
|
-
try {
|
|
965
|
-
entries = readdirSync(pluginSrc);
|
|
966
|
-
} catch (err) {
|
|
967
|
-
entries = [];
|
|
968
|
-
readdirErr = err;
|
|
969
|
-
}
|
|
970
|
-
if (readdirErr) {
|
|
971
|
-
ctx.log.warn(`Wayland plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
|
|
972
|
-
}
|
|
973
|
-
const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
|
|
974
|
-
let dstEntries = [];
|
|
975
|
-
try {
|
|
976
|
-
dstEntries = readdirSync(pluginDst);
|
|
977
|
-
} catch {
|
|
978
|
-
}
|
|
979
|
-
for (const name of dstEntries) {
|
|
980
|
-
if (name === "__pycache__") continue;
|
|
981
|
-
if (!srcNames.has(name)) {
|
|
982
|
-
try {
|
|
983
|
-
rmSync(join4(pluginDst, name), { recursive: true, force: true });
|
|
984
|
-
} catch (err) {
|
|
985
|
-
ctx.log.warn(`Wayland plugin: could not remove stale ${name}: ${err.message || err}`);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
for (const name of entries) {
|
|
990
|
-
if (name === "__pycache__") continue;
|
|
991
|
-
const src = join4(pluginSrc, name);
|
|
992
|
-
const dstEntry = join4(pluginDst, name);
|
|
993
|
-
try {
|
|
994
|
-
const st = statSync2(src);
|
|
995
|
-
if (st.isDirectory()) {
|
|
996
|
-
cpSync(src, dstEntry, { recursive: true, force: true });
|
|
997
|
-
} else if (st.isFile()) {
|
|
998
|
-
copyFileSync2(src, dstEntry);
|
|
999
|
-
}
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
mergeYamlHook(dst, "plugins/ijfw/hooks/pre_tool_use_extension_check.py", ctx.ts);
|
|
1005
|
-
ctx.log.ok("Installed Wayland bundle: MCP + WAYLAND.md + skills + plugin + tier-2 hook");
|
|
999
|
+
ctx.log.ok("Installed Wayland bundle: declarative plugin.toml + WAYLAND.md + skills");
|
|
1006
1000
|
return { status: "ok" };
|
|
1007
1001
|
}
|
|
1002
|
+
function ijfwVersion(ctx) {
|
|
1003
|
+
try {
|
|
1004
|
+
const pkg = JSON.parse(
|
|
1005
|
+
readFileSync3(join4(ctx.repoRoot, "installer", "package.json"), "utf8")
|
|
1006
|
+
);
|
|
1007
|
+
if (pkg && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
return "0.0.0";
|
|
1011
|
+
}
|
|
1012
|
+
function tomlBasicString(value) {
|
|
1013
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1014
|
+
}
|
|
1015
|
+
function renderWaylandPluginToml(ctx) {
|
|
1016
|
+
const serverJs = tomlBasicString(ctx.serverJsNative);
|
|
1017
|
+
const version = tomlBasicString(ijfwVersion(ctx));
|
|
1018
|
+
return [
|
|
1019
|
+
"[plugin]",
|
|
1020
|
+
'name = "wayland-ijfw"',
|
|
1021
|
+
`version = "${version}"`,
|
|
1022
|
+
'description = "IJFW memory + lifecycle hooks for Wayland Core"',
|
|
1023
|
+
'license = "MIT"',
|
|
1024
|
+
"",
|
|
1025
|
+
"[permissions]",
|
|
1026
|
+
"register_hooks = true",
|
|
1027
|
+
"register_mcp_server = true",
|
|
1028
|
+
"",
|
|
1029
|
+
"[runtime]",
|
|
1030
|
+
'kind = "declarative"',
|
|
1031
|
+
"",
|
|
1032
|
+
"[mcp_server]",
|
|
1033
|
+
'name = "ijfw-memory"',
|
|
1034
|
+
"",
|
|
1035
|
+
"[mcp_server.transport]",
|
|
1036
|
+
'kind = "stdio"',
|
|
1037
|
+
'command = "node"',
|
|
1038
|
+
`args = ["${serverJs}"]`,
|
|
1039
|
+
"",
|
|
1040
|
+
// Only session_start and pre_prompt dispatch in Wayland today; the
|
|
1041
|
+
// post_tool_use / session_end / pre_compact phases are registered-but-
|
|
1042
|
+
// log-only on the Wayland side, so they are intentionally omitted. Add
|
|
1043
|
+
// them here once Wayland wires those phases.
|
|
1044
|
+
"[[hooks]]",
|
|
1045
|
+
'phase = "session_start"',
|
|
1046
|
+
'tool = "ijfw_memory_prelude"',
|
|
1047
|
+
"",
|
|
1048
|
+
"[[hooks]]",
|
|
1049
|
+
'phase = "pre_prompt"',
|
|
1050
|
+
'tool = "ijfw_memory_recall"',
|
|
1051
|
+
""
|
|
1052
|
+
].join("\n");
|
|
1053
|
+
}
|
|
1008
1054
|
async function installHermes(ctx) {
|
|
1009
1055
|
if (ctx.ijfwCustomDir) {
|
|
1010
1056
|
return customDirNoop(
|
|
@@ -1093,6 +1139,13 @@ async function installCursor(ctx) {
|
|
|
1093
1139
|
);
|
|
1094
1140
|
}
|
|
1095
1141
|
const cwd = ctx.cwd || process.cwd();
|
|
1142
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1143
|
+
platformLabel: "Cursor project rules",
|
|
1144
|
+
log: ctx.log
|
|
1145
|
+
})) {
|
|
1146
|
+
ctx.log.ok("Cursor: real platform config left untouched.");
|
|
1147
|
+
return { status: "noop" };
|
|
1148
|
+
}
|
|
1096
1149
|
const dst = join4(cwd, ".cursor", "mcp.json");
|
|
1097
1150
|
ensureDir(dirname4(dst));
|
|
1098
1151
|
mergeJson(dst, ctx.serverJsNative);
|
|
@@ -1121,6 +1174,13 @@ async function installWindsurf(ctx) {
|
|
|
1121
1174
|
ensureDir(dirname4(dst));
|
|
1122
1175
|
mergeJson(dst, ctx.serverJsNative);
|
|
1123
1176
|
const cwd = ctx.cwd || process.cwd();
|
|
1177
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1178
|
+
platformLabel: "Windsurf project rules (.windsurfrules)",
|
|
1179
|
+
log: ctx.log
|
|
1180
|
+
})) {
|
|
1181
|
+
ctx.log.ok(`Merged MCP into ${dst}`);
|
|
1182
|
+
return { status: "ok" };
|
|
1183
|
+
}
|
|
1124
1184
|
const projectRules = join4(cwd, ".windsurfrules");
|
|
1125
1185
|
const repoRules = join4(ctx.repoRoot, "windsurf", ".windsurfrules");
|
|
1126
1186
|
let installedRules = false;
|
|
@@ -1181,10 +1241,18 @@ function installCopilot(ctx) {
|
|
|
1181
1241
|
printOk("Copilot: source tree left untouched.");
|
|
1182
1242
|
return { status: "noop" };
|
|
1183
1243
|
}
|
|
1184
|
-
const
|
|
1244
|
+
const cwd = ctx.cwd || process.cwd();
|
|
1245
|
+
if (!guardProjectWrite(cwd, ctx.home, {
|
|
1246
|
+
platformLabel: "Copilot project rules",
|
|
1247
|
+
log: ctx.log
|
|
1248
|
+
})) {
|
|
1249
|
+
printOk("Copilot: real platform config left untouched.");
|
|
1250
|
+
return { status: "noop" };
|
|
1251
|
+
}
|
|
1252
|
+
const dst = path.join(cwd, ".vscode", "mcp.json");
|
|
1185
1253
|
ensureDir2(path.dirname(dst));
|
|
1186
1254
|
mergeJson(dst, ctx.serverJsNative || ctx.serverJs);
|
|
1187
|
-
const rulesDst = path.join(
|
|
1255
|
+
const rulesDst = path.join(cwd, ".github", "copilot-instructions.md");
|
|
1188
1256
|
const rulesSrc = path.join(ctx.repoRoot, "copilot", "copilot-instructions.md");
|
|
1189
1257
|
const wroteRules = copyIfMissing(rulesSrc, rulesDst);
|
|
1190
1258
|
if (wroteRules) {
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
494
|
+
if (removeJsonMcpEntry(join2(home, ".gemini", "settings.json"))) {
|
|
343
495
|
removed.push("~/.gemini/settings.json (removed ijfw-memory)");
|
|
344
496
|
}
|
|
345
|
-
const geminiExt = join2(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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`
|
|
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.
|
|
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://
|
|
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://
|
|
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
|
-
#
|
|
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
|
-
}
|