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