@ijfw/install 1.5.4 → 1.5.5
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/README.md +33 -65
- package/dist/hub-index-snippet.json +4 -4
- package/dist/ijfw.js +87 -23
- package/dist/install.js +201 -33
- package/docs/GUIDE.md +2 -2
- package/docs/guide/assets/ferrox-hero.png +0 -0
- package/package.json +10 -4
- package/scripts/hub-extension/install.js.tmpl +6 -1
- package/scripts/pack-hub-extension.js +61 -14
- package/src/install.ps1 +30 -3
package/README.md
CHANGED
|
@@ -1,92 +1,60 @@
|
|
|
1
|
-
# @ijfw/install
|
|
1
|
+
# @ijfw/install — One-command installer for IJFW
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
> Ferrox Labs · Local-first infrastructure for AI coding agents
|
|
4
|
+
|
|
5
|
+
IJFW is Ferrox Labs' shared development infrastructure for AI-driven teams — shared memory across projects, smart model routing, cross-AI adversarial audits (Trident: Claude + Codex + Gemini in parallel), and a disciplined think-build-ship workflow. v1.5.5 closed 60 audit findings in a single milestone. This package wires it onto every AI coding agent on your machine.
|
|
6
|
+
|
|
7
|
+
Full docs: [github.com/FerroxLabs/ijfw](https://github.com/FerroxLabs/ijfw)
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Install](#install)
|
|
12
|
+
- [What it does](#what-it-does)
|
|
13
|
+
- [Options](#options)
|
|
14
|
+
- [Uninstall](#uninstall)
|
|
15
|
+
- [Links](#links)
|
|
5
16
|
|
|
6
17
|
## Install
|
|
7
18
|
|
|
8
19
|
```bash
|
|
9
20
|
npm install -g @ijfw/install
|
|
10
|
-
ijfw
|
|
21
|
+
ijfw install
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
If no AI agents are detected, install Claude Code or Codex first, then re-run.
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
- Installs the IJFW source tree at `~/.ijfw/`
|
|
29
|
+
- Wires the MCP server into 14 platforms via their config files
|
|
30
|
+
- Adds Aider as a rules-only tier — 15 agents supported total
|
|
31
|
+
- Sets up shared memory at `~/.ijfw/memory/` (plain markdown hot, SQLite FTS5 warm, optional vectors cold)
|
|
32
|
+
- Runs an 8-gate preflight before declaring done
|
|
33
|
+
|
|
34
|
+
## Options
|
|
16
35
|
|
|
17
36
|
| Flag | Default | Notes |
|
|
18
37
|
|------|---------|-------|
|
|
19
38
|
| `--dir <path>` | `$IJFW_HOME` or `~/.ijfw` | Install location |
|
|
20
39
|
| `--branch <name>` | latest released tag | Git branch or tag |
|
|
21
|
-
| `--no-marketplace` |
|
|
22
|
-
| `--yes` |
|
|
40
|
+
| `--no-marketplace` | enabled | Skip settings.json edits |
|
|
41
|
+
| `--yes` | interactive | Non-interactive run |
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
## Uninstall
|
|
25
44
|
|
|
26
45
|
```bash
|
|
27
46
|
ijfw uninstall # preserves ~/.ijfw/memory/
|
|
28
47
|
ijfw uninstall --purge # removes memory too
|
|
29
48
|
```
|
|
30
49
|
|
|
31
|
-
If `ijfw`
|
|
32
|
-
package already), invoke the bin directly:
|
|
50
|
+
If `ijfw` is no longer on your PATH, invoke the bin directly:
|
|
33
51
|
|
|
34
52
|
```bash
|
|
35
53
|
npx -p @ijfw/install ijfw-uninstall
|
|
36
54
|
```
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Preflight
|
|
41
|
-
|
|
42
|
-
Requires `node >=18` and `git` (used for the initial repo clone). The
|
|
43
|
-
installer is Node-native end to end -- no bash, no WSL, no Git for Windows
|
|
44
|
-
shell. On native Windows use the PowerShell installer (PS 5.1+), which
|
|
45
|
-
delegates to Node directly:
|
|
46
|
-
|
|
47
|
-
```powershell
|
|
48
|
-
iwr https://gitlab.com/therealseandonahoe/ijfw/-/raw/main/installer/src/install.ps1 -OutFile install.ps1
|
|
49
|
-
.\install.ps1 -Dir $env:USERPROFILE\.ijfw
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Extension CLI
|
|
53
|
-
|
|
54
|
-
IJFW ships a full extension system for installing and sandboxing third-party skills.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# Publisher key management
|
|
58
|
-
ijfw extension keygen <author> # Generate an Ed25519 publisher keypair
|
|
59
|
-
ijfw extension trust <keyId> <publicKey> # Add a publisher to your trusted store
|
|
60
|
-
ijfw extension trust-registry [<url>] # Pull + apply the hosted publisher registry
|
|
61
|
-
ijfw extension untrust <keyId> # Remove a publisher from your trusted store
|
|
62
|
-
ijfw extension trusted # List all trusted publishers
|
|
63
|
-
|
|
64
|
-
# Extension lifecycle
|
|
65
|
-
ijfw extension add <source> [flags] # Install an extension (npm name, local path, or https git URL)
|
|
66
|
-
--allow-unsigned # Accept extensions with no signature
|
|
67
|
-
--accept-untrusted # Accept extensions signed by an untrusted publisher (prompts on TTY)
|
|
68
|
-
--activate # Auto-activate after install
|
|
69
|
-
ijfw extension activate <name> # Activate an installed extension (enforces declared permissions)
|
|
70
|
-
ijfw extension deactivate # Deactivate the current extension
|
|
71
|
-
|
|
72
|
-
# Admin / registry maintainer (rare)
|
|
73
|
-
ijfw extension rotate-keys <oldKeyId> <newKeyId> # Produce a signed rotation token
|
|
74
|
-
ijfw extension keygen-meta <author> # Generate the registry meta-keypair
|
|
75
|
-
ijfw extension sign-registry <path> # Sign a registry JSON file in place
|
|
76
|
-
ijfw extension verify-registry <path> # Verify a registry JSON signature
|
|
77
|
-
ijfw extension registry-status # Show registry cache age + signature status
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
The rotation flow and registry maintainer docs live in `docs/REGISTRY-MAINTAINER.md`.
|
|
81
|
-
|
|
82
|
-
## Build (contributors)
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
cd installer
|
|
86
|
-
npm install
|
|
87
|
-
npm run build # outputs dist/install.js + dist/uninstall.js
|
|
88
|
-
npm test
|
|
89
|
-
npm run pack:check
|
|
90
|
-
```
|
|
56
|
+
## Links
|
|
91
57
|
|
|
92
|
-
|
|
58
|
+
- Source, issues, and docs: [github.com/FerroxLabs/ijfw](https://github.com/FerroxLabs/ijfw)
|
|
59
|
+
- npm package: [@ijfw/install](https://www.npmjs.com/package/@ijfw/install)
|
|
60
|
+
- License: MIT
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ijfw",
|
|
3
3
|
"displayName": "IJFW — AI Efficiency Layer",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.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
6
|
"author": "Sean Donahoe",
|
|
7
7
|
"icon": "assets/ijfw-logo.svg",
|
|
8
8
|
"dist": {
|
|
9
|
-
"tarball": "extensions/ijfw-1.5.
|
|
10
|
-
"integrity": "sha512-
|
|
11
|
-
"unpackedSize":
|
|
9
|
+
"tarball": "extensions/ijfw-1.5.5.zip",
|
|
10
|
+
"integrity": "sha512-vm3ot0+GhXYy0/HcStB/xmFjZhSXWG7jzSLuIPe43/Dla8qhyR9gsCwBI/8+RsUJqMXiPgc0GuxrgwW2f5DDWA==",
|
|
11
|
+
"unpackedSize": 3312
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"wayland": ">=0.6.0"
|
package/dist/ijfw.js
CHANGED
|
@@ -2110,7 +2110,7 @@ __export(upgrade_smoke_exports, {
|
|
|
2110
2110
|
severity: () => severity11
|
|
2111
2111
|
});
|
|
2112
2112
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
2113
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
2113
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync4, cpSync } from "node:fs";
|
|
2114
2114
|
import { join as join10, resolve as resolve2 } from "node:path";
|
|
2115
2115
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
2116
2116
|
async function run11(ctx) {
|
|
@@ -2199,30 +2199,94 @@ async function run11(ctx) {
|
|
|
2199
2199
|
durationMs: Date.now() - t0
|
|
2200
2200
|
};
|
|
2201
2201
|
}
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
return {
|
|
2209
|
-
name: "upgrade-smoke",
|
|
2210
|
-
status: "FAIL",
|
|
2211
|
-
message: "upgrade-smoke: settings.json is not valid JSON",
|
|
2212
|
-
details: [e.message],
|
|
2213
|
-
durationMs: Date.now() - t0
|
|
2214
|
-
};
|
|
2202
|
+
const targetIjfwHome = join10(fakeHome, ".ijfw");
|
|
2203
|
+
mkdirSync4(targetIjfwHome, { recursive: true });
|
|
2204
|
+
for (const sub of ["claude", "mcp-server"]) {
|
|
2205
|
+
const src = join10(ctx.repoRoot, sub);
|
|
2206
|
+
if (existsSync4(src)) {
|
|
2207
|
+
cpSync(src, join10(targetIjfwHome, sub), { recursive: true });
|
|
2215
2208
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2209
|
+
}
|
|
2210
|
+
const installerPkgSrc = join10(ctx.repoRoot, "installer", "package.json");
|
|
2211
|
+
if (existsSync4(installerPkgSrc)) {
|
|
2212
|
+
mkdirSync4(join10(targetIjfwHome, "installer"), { recursive: true });
|
|
2213
|
+
cpSync(installerPkgSrc, join10(targetIjfwHome, "installer", "package.json"));
|
|
2214
|
+
}
|
|
2215
|
+
const runInstaller = spawnSync11(installerBin, ["--yes"], {
|
|
2216
|
+
encoding: "utf8",
|
|
2217
|
+
cwd: installDir,
|
|
2218
|
+
timeout: 12e4,
|
|
2219
|
+
env: {
|
|
2220
|
+
...cleanEnv,
|
|
2221
|
+
HOME: fakeHome,
|
|
2222
|
+
USERPROFILE: fakeHome,
|
|
2223
|
+
IJFW_HOME: targetIjfwHome,
|
|
2224
|
+
// Hermetic: install.js refuses any network attempt under this flag
|
|
2225
|
+
// (TR-001). The gate's contract is "the installer either completes
|
|
2226
|
+
// without network or fails clearly". The marketplace merge step
|
|
2227
|
+
// (which is what we actually want to verify) does NOT need network.
|
|
2228
|
+
CI: "1",
|
|
2229
|
+
IJFW_SKIP_NETWORK: "1"
|
|
2230
|
+
},
|
|
2231
|
+
shell: process.platform === "win32"
|
|
2232
|
+
});
|
|
2233
|
+
if (runInstaller.status !== 0 || runInstaller.signal) {
|
|
2234
|
+
const stderrLines = (runInstaller.stderr || "").split("\n").filter(Boolean);
|
|
2235
|
+
const lastStderrLine = stderrLines.length > 0 ? stderrLines[stderrLines.length - 1].slice(0, 200) : "(no stderr)";
|
|
2236
|
+
let cat;
|
|
2237
|
+
if (runInstaller.signal) {
|
|
2238
|
+
cat = `killed by ${runInstaller.signal}`;
|
|
2239
|
+
} else if (runInstaller.status === 137 || runInstaller.status === 124) {
|
|
2240
|
+
cat = `timed out (exit ${runInstaller.status})`;
|
|
2241
|
+
} else if (runInstaller.status === null) {
|
|
2242
|
+
cat = "timed out (status null)";
|
|
2243
|
+
} else {
|
|
2244
|
+
cat = `exited ${runInstaller.status}`;
|
|
2225
2245
|
}
|
|
2246
|
+
return {
|
|
2247
|
+
name: "upgrade-smoke",
|
|
2248
|
+
status: "FAIL",
|
|
2249
|
+
message: `upgrade-smoke: installer ${cat}. Last stderr line: ${lastStderrLine}`,
|
|
2250
|
+
details: ((runInstaller.stdout || "") + (runInstaller.stderr || "")).split("\n").filter(Boolean).slice(0, 15),
|
|
2251
|
+
durationMs: Date.now() - t0
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
const settingsPath = join10(claudeDir, "settings.json");
|
|
2255
|
+
if (!existsSync4(settingsPath)) {
|
|
2256
|
+
return {
|
|
2257
|
+
name: "upgrade-smoke",
|
|
2258
|
+
status: "FAIL",
|
|
2259
|
+
message: "upgrade-smoke: installer did not write ~/.claude/settings.json",
|
|
2260
|
+
details: [
|
|
2261
|
+
`expected: ${settingsPath}`,
|
|
2262
|
+
"The installer ran (exit 0) but the marketplace merge never produced settings.json.",
|
|
2263
|
+
"This is the false-pass shape TR-001 retired \u2014 the gate must observe the write.",
|
|
2264
|
+
...((runInstaller.stdout || "") + (runInstaller.stderr || "")).split("\n").filter(Boolean).slice(0, 8)
|
|
2265
|
+
],
|
|
2266
|
+
durationMs: Date.now() - t0
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
let settings;
|
|
2270
|
+
try {
|
|
2271
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
2272
|
+
} catch (e) {
|
|
2273
|
+
return {
|
|
2274
|
+
name: "upgrade-smoke",
|
|
2275
|
+
status: "FAIL",
|
|
2276
|
+
message: "upgrade-smoke: settings.json is not valid JSON",
|
|
2277
|
+
details: [e.message],
|
|
2278
|
+
durationMs: Date.now() - t0
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
const hasWrongKey = JSON.stringify(settings).includes("ijfw-core");
|
|
2282
|
+
if (hasWrongKey) {
|
|
2283
|
+
return {
|
|
2284
|
+
name: "upgrade-smoke",
|
|
2285
|
+
status: "FAIL",
|
|
2286
|
+
message: 'upgrade-smoke: settings.json still uses deprecated "ijfw-core" key',
|
|
2287
|
+
details: [`Found "ijfw-core" in: ${settingsPath}`],
|
|
2288
|
+
durationMs: Date.now() - t0
|
|
2289
|
+
};
|
|
2226
2290
|
}
|
|
2227
2291
|
const marketplaceSrc = join10(installerDir, "src", "marketplace.js");
|
|
2228
2292
|
if (existsSync4(marketplaceSrc)) {
|
package/dist/install.js
CHANGED
|
@@ -73,20 +73,44 @@ function writeAtomic(path3, contents, opts = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
function backup(path3, ts) {
|
|
76
|
+
const res = backupDetailed(path3, ts);
|
|
77
|
+
return res.ok && res.path ? res.path : null;
|
|
78
|
+
}
|
|
79
|
+
function backupDetailed(path3, ts) {
|
|
80
|
+
let st;
|
|
76
81
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
82
|
+
st = statSync(path3);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err && err.code === "ENOENT") return { ok: true, path: null, reason: "absent" };
|
|
85
|
+
return { ok: false, reason: "stat-failed", error: err };
|
|
86
|
+
}
|
|
87
|
+
if (!st.isFile()) {
|
|
88
|
+
return { ok: true, path: null, reason: "not-a-file" };
|
|
81
89
|
}
|
|
82
90
|
const dst = `${path3}.bak.${ts}`;
|
|
83
91
|
try {
|
|
84
92
|
copyFileSync(path3, dst);
|
|
85
93
|
printInfo(`backup: ${basename(path3)}.bak.${ts}`);
|
|
86
|
-
return dst;
|
|
87
|
-
} catch {
|
|
94
|
+
return { ok: true, path: dst };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { ok: false, reason: "copy-failed", error: err };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function requireBackup(path3, ts) {
|
|
100
|
+
if (!ts) return null;
|
|
101
|
+
const res = backupDetailed(path3, ts);
|
|
102
|
+
if (res.ok) return res.path;
|
|
103
|
+
if (process.env.IJFW_FORCE_NO_BACKUP === "1") {
|
|
104
|
+
const why2 = res.error && res.error.message ? res.error.message : res.reason;
|
|
105
|
+
printInfo(`backup: forced past failure for ${basename(path3)} (${why2})`);
|
|
88
106
|
return null;
|
|
89
107
|
}
|
|
108
|
+
const why = res.error && res.error.message ? res.error.message : res.reason;
|
|
109
|
+
const err = new Error(
|
|
110
|
+
`Refusing to merge into existing config without backup: ${path3} (${why}). Re-run with IJFW_FORCE_NO_BACKUP=1 to proceed.`
|
|
111
|
+
);
|
|
112
|
+
err.code = "BACKUP_REQUIRED";
|
|
113
|
+
throw err;
|
|
90
114
|
}
|
|
91
115
|
function safeChecksum(path3) {
|
|
92
116
|
try {
|
|
@@ -244,7 +268,7 @@ function readJsonOrEmpty(path3) {
|
|
|
244
268
|
}
|
|
245
269
|
function mergeJson(dst, serverJs, ts) {
|
|
246
270
|
mkdirSync2(dirname3(dst), { recursive: true });
|
|
247
|
-
|
|
271
|
+
requireBackup(dst, ts);
|
|
248
272
|
const doc = readJsonOrEmpty(dst);
|
|
249
273
|
if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
|
|
250
274
|
const isWin = IS_WIN;
|
|
@@ -272,7 +296,7 @@ function mergeJson(dst, serverJs, ts) {
|
|
|
272
296
|
}
|
|
273
297
|
function mergeToml(dst, serverJs, ts) {
|
|
274
298
|
mkdirSync2(dirname3(dst), { recursive: true });
|
|
275
|
-
|
|
299
|
+
requireBackup(dst, ts);
|
|
276
300
|
let text = "";
|
|
277
301
|
try {
|
|
278
302
|
text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
|
|
@@ -560,7 +584,8 @@ import {
|
|
|
560
584
|
statSync as statSync2,
|
|
561
585
|
copyFileSync as copyFileSync2,
|
|
562
586
|
cpSync,
|
|
563
|
-
chmodSync as chmodSync2
|
|
587
|
+
chmodSync as chmodSync2,
|
|
588
|
+
rmSync
|
|
564
589
|
} from "node:fs";
|
|
565
590
|
import { join as join4, dirname as dirname4, isAbsolute } from "node:path";
|
|
566
591
|
import { platform } from "node:os";
|
|
@@ -935,10 +960,31 @@ async function installWayland(ctx) {
|
|
|
935
960
|
const pluginDst = join4(ctx.home, ".wayland", "plugins", "ijfw");
|
|
936
961
|
ensureDir(pluginDst);
|
|
937
962
|
let entries;
|
|
963
|
+
let readdirErr = null;
|
|
938
964
|
try {
|
|
939
965
|
entries = readdirSync(pluginSrc);
|
|
940
|
-
} catch {
|
|
966
|
+
} catch (err) {
|
|
941
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
|
+
}
|
|
942
988
|
}
|
|
943
989
|
for (const name of entries) {
|
|
944
990
|
if (name === "__pycache__") continue;
|
|
@@ -986,10 +1032,31 @@ async function installHermes(ctx) {
|
|
|
986
1032
|
const pluginDst = join4(ctx.home, ".hermes", "plugins", "ijfw");
|
|
987
1033
|
ensureDir(pluginDst);
|
|
988
1034
|
let entries;
|
|
1035
|
+
let readdirErr = null;
|
|
989
1036
|
try {
|
|
990
1037
|
entries = readdirSync(pluginSrc);
|
|
991
|
-
} catch {
|
|
1038
|
+
} catch (err) {
|
|
992
1039
|
entries = [];
|
|
1040
|
+
readdirErr = err;
|
|
1041
|
+
}
|
|
1042
|
+
if (readdirErr) {
|
|
1043
|
+
ctx.log.warn(`Hermes plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
|
|
1044
|
+
}
|
|
1045
|
+
const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
|
|
1046
|
+
let dstEntries = [];
|
|
1047
|
+
try {
|
|
1048
|
+
dstEntries = readdirSync(pluginDst);
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
for (const name of dstEntries) {
|
|
1052
|
+
if (name === "__pycache__") continue;
|
|
1053
|
+
if (!srcNames.has(name)) {
|
|
1054
|
+
try {
|
|
1055
|
+
rmSync(join4(pluginDst, name), { recursive: true, force: true });
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
ctx.log.warn(`Hermes plugin: could not remove stale ${name}: ${err.message || err}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
993
1060
|
}
|
|
994
1061
|
for (const name of entries) {
|
|
995
1062
|
if (name === "__pycache__") continue;
|
|
@@ -1181,18 +1248,27 @@ function installOpenclaw(ctx) {
|
|
|
1181
1248
|
}
|
|
1182
1249
|
const dst = path.join(ctx.home, ".openclaw", "openclaw.json");
|
|
1183
1250
|
const serverJs = ctx.serverJsNative || ctx.serverJs;
|
|
1251
|
+
let cliRegistered = false;
|
|
1184
1252
|
if (commandExists("openclaw")) {
|
|
1185
1253
|
try {
|
|
1186
1254
|
const payload = JSON.stringify({ command: "node", args: [serverJs] });
|
|
1187
|
-
execFileSync("openclaw", ["mcp", "set", "ijfw-memory", payload], {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1255
|
+
execFileSync("openclaw", ["mcp", "set", "ijfw-memory", payload], {
|
|
1256
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1257
|
+
encoding: "utf8"
|
|
1258
|
+
});
|
|
1259
|
+
cliRegistered = true;
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
const msg = err && err.stderr ? String(err.stderr).trim() : err && err.message || String(err);
|
|
1262
|
+
printInfo(`openclaw CLI registration failed (${msg}); falling back to file-write merge.`);
|
|
1191
1263
|
}
|
|
1192
1264
|
}
|
|
1193
1265
|
ensureDir2(path.dirname(dst));
|
|
1194
1266
|
openclawMerge(dst, serverJs);
|
|
1195
|
-
|
|
1267
|
+
if (cliRegistered) {
|
|
1268
|
+
printOk(`Registered ijfw-memory via 'openclaw mcp set' AND file-write merge (${dst})`);
|
|
1269
|
+
} else {
|
|
1270
|
+
printOk(`Merged MCP into ${dst} (openclaw mcp.servers schema)`);
|
|
1271
|
+
}
|
|
1196
1272
|
return { status: "ok" };
|
|
1197
1273
|
}
|
|
1198
1274
|
function installAider(ctx) {
|
|
@@ -1294,6 +1370,14 @@ function linkPlugin({ repoRoot, ijfwHome, ts }) {
|
|
|
1294
1370
|
} catch {
|
|
1295
1371
|
}
|
|
1296
1372
|
} else if (st.isDirectory()) {
|
|
1373
|
+
try {
|
|
1374
|
+
fs2.rmSync(pluginDst, { recursive: true, force: true });
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
process.stderr.write(
|
|
1377
|
+
`[ijfw] linkPlugin: could not clear ${pluginDst} for mirror (${err && err.message ? err.message : err}); falling back to merge.
|
|
1378
|
+
`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1297
1381
|
fs2.cpSync(pluginSrc, pluginDst, { recursive: true });
|
|
1298
1382
|
printOk(`Plugin tree mirrored to ${pluginDst}`);
|
|
1299
1383
|
return;
|
|
@@ -1369,12 +1453,20 @@ function seedState({ ijfwHome, repoRoot, nodeBin: _nodeBin }) {
|
|
|
1369
1453
|
}
|
|
1370
1454
|
} catch {
|
|
1371
1455
|
}
|
|
1372
|
-
|
|
1456
|
+
const pkgPath = path2.join(repoRoot, "installer", "package.json");
|
|
1457
|
+
let installedVer;
|
|
1373
1458
|
try {
|
|
1374
|
-
const pkgPath = path2.join(repoRoot, "installer", "package.json");
|
|
1375
1459
|
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
1376
|
-
installedVer = pkg.version
|
|
1377
|
-
} catch {
|
|
1460
|
+
installedVer = pkg.version;
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
throw new Error(
|
|
1463
|
+
`Preflight: installer/package.json missing or unreadable at ${pkgPath} (${err && err.message ? err.message : err}). Repo tree incomplete; rerun bootstrap from a clean clone.`
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
if (!installedVer || typeof installedVer !== "string") {
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
`Preflight: installer/package.json at ${pkgPath} has no usable "version" field. Rerun bootstrap from a clean clone.`
|
|
1469
|
+
);
|
|
1378
1470
|
}
|
|
1379
1471
|
const nowTs = Math.floor(Date.now() / 1e3);
|
|
1380
1472
|
const state = {
|
|
@@ -1484,7 +1576,14 @@ function patchPluginMcpJson({ ijfwHome, repoRoot, nodeBin, serverJs }) {
|
|
|
1484
1576
|
d.mcpServers["ijfw-memory"].command = nodeBin;
|
|
1485
1577
|
d.mcpServers["ijfw-memory"].args = [serverJs];
|
|
1486
1578
|
const envSep = process.platform === "win32" ? ";" : ":";
|
|
1487
|
-
|
|
1579
|
+
let commonPaths;
|
|
1580
|
+
if (process.platform === "win32") {
|
|
1581
|
+
commonPaths = [nodeDir, "C:\\Windows\\System32"];
|
|
1582
|
+
} else if (process.platform === "darwin") {
|
|
1583
|
+
commonPaths = [nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
1584
|
+
} else {
|
|
1585
|
+
commonPaths = [nodeDir, "/usr/local/bin", "/usr/bin", "/bin"];
|
|
1586
|
+
}
|
|
1488
1587
|
const dedup = [...new Set(commonPaths.filter((x) => x && fs2.existsSync(x)))];
|
|
1489
1588
|
d.mcpServers["ijfw-memory"].env = { PATH: dedup.join(envSep) };
|
|
1490
1589
|
try {
|
|
@@ -1857,7 +1956,7 @@ var init_install_flow = __esm({
|
|
|
1857
1956
|
|
|
1858
1957
|
// src/install.js
|
|
1859
1958
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1860
|
-
import { existsSync as existsSync5, rmSync, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3 } from "node:fs";
|
|
1959
|
+
import { existsSync as existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3, readdirSync as readdirSync2, cpSync as cpSync2 } from "node:fs";
|
|
1861
1960
|
import { resolve as resolve4, join as join5, dirname as dirname5 } from "node:path";
|
|
1862
1961
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
1863
1962
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -2017,7 +2116,7 @@ function triggerColdScan(projectRoot, options = {}) {
|
|
|
2017
2116
|
}
|
|
2018
2117
|
|
|
2019
2118
|
// src/install.js
|
|
2020
|
-
var DEFAULT_REPO = "https://
|
|
2119
|
+
var DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git";
|
|
2021
2120
|
var DEFAULT_BRANCH = "main";
|
|
2022
2121
|
function parseArgs(argv) {
|
|
2023
2122
|
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false };
|
|
@@ -2037,7 +2136,11 @@ function parseArgs(argv) {
|
|
|
2037
2136
|
}
|
|
2038
2137
|
return out;
|
|
2039
2138
|
}
|
|
2139
|
+
function skipNetwork() {
|
|
2140
|
+
return process.env.IJFW_SKIP_NETWORK === "1";
|
|
2141
|
+
}
|
|
2040
2142
|
function latestTagFromGithub() {
|
|
2143
|
+
if (skipNetwork()) return null;
|
|
2041
2144
|
try {
|
|
2042
2145
|
const res = spawnSync2("git", ["ls-remote", "--tags", "--refs", "--sort=-v:refname", DEFAULT_REPO], {
|
|
2043
2146
|
encoding: "utf8",
|
|
@@ -2051,7 +2154,7 @@ function latestTagFromGithub() {
|
|
|
2051
2154
|
return null;
|
|
2052
2155
|
}
|
|
2053
2156
|
}
|
|
2054
|
-
function resolveBranchOrTag({ branch, branchExplicit, _tagLookup } = {}) {
|
|
2157
|
+
function resolveBranchOrTag({ branch, branchExplicit, _tagLookup, _logger } = {}) {
|
|
2055
2158
|
if (branchExplicit) return branch;
|
|
2056
2159
|
const lookup = _tagLookup || latestTagFromGithub;
|
|
2057
2160
|
let tag = null;
|
|
@@ -2060,7 +2163,15 @@ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup } = {}) {
|
|
|
2060
2163
|
} catch {
|
|
2061
2164
|
tag = null;
|
|
2062
2165
|
}
|
|
2063
|
-
|
|
2166
|
+
if (!tag) {
|
|
2167
|
+
const log = _logger || console.warn;
|
|
2168
|
+
const eff = branch || DEFAULT_BRANCH;
|
|
2169
|
+
log(
|
|
2170
|
+
` note: could not resolve latest tag from upstream (network or rate-limit?). Using branch "${eff}" instead. Pin a specific version with --branch vX.Y.Z if needed.`
|
|
2171
|
+
);
|
|
2172
|
+
return eff;
|
|
2173
|
+
}
|
|
2174
|
+
return tag;
|
|
2064
2175
|
}
|
|
2065
2176
|
function printHelp() {
|
|
2066
2177
|
console.log(`ijfw-install -- IJFW installer
|
|
@@ -2128,6 +2239,14 @@ function runCheck(cmd, args, opts) {
|
|
|
2128
2239
|
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
|
|
2129
2240
|
}
|
|
2130
2241
|
function cloneOrPull(dir, branch) {
|
|
2242
|
+
if (skipNetwork()) {
|
|
2243
|
+
if (existsSync5(dir)) {
|
|
2244
|
+
return "skipped-network";
|
|
2245
|
+
}
|
|
2246
|
+
throw new Error(
|
|
2247
|
+
`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.`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2131
2250
|
if (!existsSync5(dir)) {
|
|
2132
2251
|
mkdirSync4(dir, { recursive: true });
|
|
2133
2252
|
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
@@ -2143,7 +2262,11 @@ function cloneOrPull(dir, branch) {
|
|
|
2143
2262
|
if (remoteStatus === 0) {
|
|
2144
2263
|
const STALE_PATTERNS = [
|
|
2145
2264
|
/^https:\/\/github\.com\/seandonahoe\/ijfw(\.git)?\/?$/i,
|
|
2146
|
-
/^https:\/\/github\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i
|
|
2265
|
+
/^https:\/\/github\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i,
|
|
2266
|
+
// V155 rebrand: GitLab was the canonical source through v1.5.4;
|
|
2267
|
+
// users who installed from gitlab.com need their origin migrated
|
|
2268
|
+
// forward to FerroxLabs/ijfw on GitHub on next `ijfw-install`.
|
|
2269
|
+
/^https:\/\/gitlab\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i
|
|
2147
2270
|
];
|
|
2148
2271
|
const currentOrigin = (stdout || "").trim();
|
|
2149
2272
|
if (STALE_PATTERNS.some((re) => re.test(currentOrigin))) {
|
|
@@ -2161,23 +2284,63 @@ function cloneOrPull(dir, branch) {
|
|
|
2161
2284
|
return "updated";
|
|
2162
2285
|
}
|
|
2163
2286
|
}
|
|
2287
|
+
const RESTORE_ALLOWLIST = [
|
|
2288
|
+
"memory",
|
|
2289
|
+
"sessions",
|
|
2290
|
+
"install.log",
|
|
2291
|
+
".session-counter",
|
|
2292
|
+
// v1.5.x additions:
|
|
2293
|
+
"ijfw",
|
|
2294
|
+
// visible brain layer (wiki + facts)
|
|
2295
|
+
"state",
|
|
2296
|
+
// state.json, deploy-failures.jsonl, .dream-state-v2.json
|
|
2297
|
+
"cache",
|
|
2298
|
+
// npm-view-cache and friends
|
|
2299
|
+
"logs",
|
|
2300
|
+
// post-tool-use logs, jsonl observations
|
|
2301
|
+
"run",
|
|
2302
|
+
// runtime lock files / pid markers
|
|
2303
|
+
".ijfw"
|
|
2304
|
+
// internal — recall counter, indexes, layout version
|
|
2305
|
+
];
|
|
2164
2306
|
const backupDir = dir + ".bak." + Date.now();
|
|
2165
2307
|
renameSync3(dir, backupDir);
|
|
2166
2308
|
try {
|
|
2167
2309
|
const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
|
|
2168
2310
|
if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
|
|
2169
|
-
|
|
2311
|
+
let restoredCount = 0;
|
|
2312
|
+
for (const item of RESTORE_ALLOWLIST) {
|
|
2170
2313
|
const src = join5(backupDir, item);
|
|
2171
2314
|
if (existsSync5(src)) {
|
|
2172
2315
|
const dst = join5(dir, item);
|
|
2173
|
-
if (existsSync5(dst))
|
|
2174
|
-
|
|
2316
|
+
if (existsSync5(dst)) rmSync2(dst, { recursive: true, force: true });
|
|
2317
|
+
try {
|
|
2318
|
+
cpSync2(src, dst, { recursive: true, dereference: false });
|
|
2319
|
+
rmSync2(src, { recursive: true, force: true });
|
|
2320
|
+
restoredCount++;
|
|
2321
|
+
} catch (cpErr) {
|
|
2322
|
+
const msg = cpErr && cpErr.message ? cpErr.message : String(cpErr);
|
|
2323
|
+
throw new Error(
|
|
2324
|
+
`IJFW restore: cpSync failed for "${item}" (${msg}). Your data is still intact under: ${backupDir}. Move it back into ${dir} manually after diagnosing the copy failure.`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2175
2327
|
}
|
|
2176
2328
|
}
|
|
2177
|
-
|
|
2329
|
+
let backupResidual = [];
|
|
2330
|
+
try {
|
|
2331
|
+
backupResidual = readdirSync2(backupDir);
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
if (backupResidual.length === 0) {
|
|
2335
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
2336
|
+
} else {
|
|
2337
|
+
console.warn(
|
|
2338
|
+
` [!] restored ${restoredCount} known dirs; backup retained at ${backupDir} (contains: ${backupResidual.slice(0, 8).join(", ")}${backupResidual.length > 8 ? ", ..." : ""}). Remove manually after verifying.`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2178
2341
|
return "updated";
|
|
2179
2342
|
} catch (err) {
|
|
2180
|
-
if (existsSync5(dir))
|
|
2343
|
+
if (existsSync5(dir)) rmSync2(dir, { recursive: true, force: true });
|
|
2181
2344
|
renameSync3(backupDir, dir);
|
|
2182
2345
|
throw err;
|
|
2183
2346
|
}
|
|
@@ -2208,8 +2371,13 @@ async function main() {
|
|
|
2208
2371
|
const sigint = () => {
|
|
2209
2372
|
if (createdThisRun && existsSync5(target)) {
|
|
2210
2373
|
try {
|
|
2211
|
-
|
|
2212
|
-
} catch {
|
|
2374
|
+
rmSync2(target, { recursive: true, force: true });
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
const msg = err && err.message ? err.message : String(err);
|
|
2377
|
+
console.warn(
|
|
2378
|
+
`
|
|
2379
|
+
[!] partial install at ${target} could not be cleaned (${msg}) \u2014 run \`rm -rf "${target}"\` (or Remove-Item -Recurse -Force on Windows) before retrying.`
|
|
2380
|
+
);
|
|
2213
2381
|
}
|
|
2214
2382
|
}
|
|
2215
2383
|
process.exit(130);
|
package/docs/GUIDE.md
CHANGED
|
@@ -502,7 +502,7 @@ cat ~/.ijfw/dashboard.port
|
|
|
502
502
|
|
|
503
503
|
### Still stuck
|
|
504
504
|
|
|
505
|
-
Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [
|
|
505
|
+
Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [github.com/FerroxLabs/ijfw/issues](https://github.com/FerroxLabs/ijfw/issues) with both files redacted and attached.
|
|
506
506
|
|
|
507
507
|
---
|
|
508
508
|
|
|
@@ -545,7 +545,7 @@ Yes. `.ijfw/team/` is git-committed by default. Decisions, patterns, and stack c
|
|
|
545
545
|
</p>
|
|
546
546
|
|
|
547
547
|
<p align="center">
|
|
548
|
-
<a href="https://
|
|
548
|
+
<a href="https://github.com/FerroxLabs/ijfw">github.com/FerroxLabs/ijfw</a>
|
|
549
549
|
|
|
|
550
550
|
<a href="https://www.npmjs.com/package/@ijfw/install">npm</a>
|
|
551
551
|
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/install",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
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": {
|
|
@@ -51,13 +51,19 @@
|
|
|
51
51
|
],
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"author": "Sean Donahoe",
|
|
54
|
-
"
|
|
54
|
+
"contributors": [
|
|
55
|
+
{
|
|
56
|
+
"name": "Ferrox Labs",
|
|
57
|
+
"url": "https://ferroxlabs.com"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"homepage": "https://github.com/FerroxLabs/ijfw",
|
|
55
61
|
"bugs": {
|
|
56
|
-
"url": "https://
|
|
62
|
+
"url": "https://github.com/FerroxLabs/ijfw/issues"
|
|
57
63
|
},
|
|
58
64
|
"repository": {
|
|
59
65
|
"type": "git",
|
|
60
|
-
"url": "git+https://
|
|
66
|
+
"url": "git+https://github.com/FerroxLabs/ijfw.git"
|
|
61
67
|
},
|
|
62
68
|
"publishConfig": {
|
|
63
69
|
"access": "public"
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
const { spawnSync } = require('child_process');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// V155-066: use os.tmpdir() instead of hardcoded /tmp so the hook works on
|
|
16
|
+
// Windows (where /tmp does not exist) and on hosts whose /tmp is read-only.
|
|
17
|
+
const packDest = os.tmpdir();
|
|
13
18
|
|
|
14
19
|
// Phase 1: pre-fetch (network) — pull package into npm cache.
|
|
15
20
|
const fetchResult = spawnSync(
|
|
16
21
|
'npm',
|
|
17
|
-
['pack', '@ijfw/install@{{VERSION}}', '--silent', '--prefer-offline', '--pack-destination',
|
|
22
|
+
['pack', '@ijfw/install@{{VERSION}}', '--silent', '--prefer-offline', '--pack-destination', packDest],
|
|
18
23
|
{ stdio: 'pipe', timeout: 60_000 },
|
|
19
24
|
);
|
|
20
25
|
if (fetchResult.status !== 0) {
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
writeFileSync,
|
|
30
30
|
} from 'node:fs';
|
|
31
31
|
import { tmpdir as osTmpdir } from 'node:os';
|
|
32
|
-
import { dirname, join,
|
|
32
|
+
import { dirname, join, resolve } from 'node:path';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
34
34
|
|
|
35
35
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -103,25 +103,72 @@ const WINDOWS_SYSTEM_DIRS = [
|
|
|
103
103
|
* @param {string} absPath Already-resolved absolute path.
|
|
104
104
|
* @returns {boolean}
|
|
105
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* canonicalizeAtomic — resolve a path through realpath even when the leaf
|
|
108
|
+
* does not yet exist. Walks up to the deepest existing ancestor, realpaths
|
|
109
|
+
* that, then reattaches the remaining suffix. Handles the macOS
|
|
110
|
+
* /var ↔ /private/var symlink AND the "not-yet-created subdir" case in one
|
|
111
|
+
* pass. Mirrors the same helper in path-guard.js. (V155-036 / L1-02 recur)
|
|
112
|
+
*/
|
|
113
|
+
function canonicalizeAtomic(p) {
|
|
114
|
+
try { return realpathSync(p); } catch { /* fall through */ }
|
|
115
|
+
const parts = p.split(/[/\\]/);
|
|
116
|
+
const sep = p.includes('\\') && !p.includes('/') ? '\\' : '/';
|
|
117
|
+
let suffix = [];
|
|
118
|
+
while (parts.length > 0) {
|
|
119
|
+
suffix.unshift(parts.pop());
|
|
120
|
+
const head = parts.join(sep) || sep;
|
|
121
|
+
try {
|
|
122
|
+
const real = realpathSync(head);
|
|
123
|
+
return suffix.length > 0 ? `${real}${sep}${suffix.join(sep)}` : real;
|
|
124
|
+
} catch { /* keep walking up */ }
|
|
125
|
+
}
|
|
126
|
+
return p; // give up; return the input
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
function isSystemPath(absPath) {
|
|
107
130
|
// Reject bare filesystem roots: '/', 'C:\', 'D:\', etc.
|
|
108
131
|
if (/^[A-Za-z]:\\?$/.test(absPath) || absPath === '/') return true;
|
|
109
132
|
|
|
133
|
+
// V155-036: pre-canonicalize BOTH sides via the deepest-existing-ancestor
|
|
134
|
+
// walk so the macOS /var → /private/var symlink doesn't slip through when
|
|
135
|
+
// the requested output dir doesn't exist yet (the prior realpathSync on
|
|
136
|
+
// absPath silently kept the unresolved form, which then matched the
|
|
137
|
+
// /private prefix in the blocklist).
|
|
138
|
+
const tmp = osTmpdir();
|
|
139
|
+
const tmpReal = canonicalizeAtomic(tmp);
|
|
140
|
+
const absReal = canonicalizeAtomic(absPath);
|
|
141
|
+
|
|
110
142
|
// OS temp-dir whitelist — overrides the system-prefix blocklist.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
143
|
+
if (
|
|
144
|
+
absReal === tmpReal ||
|
|
145
|
+
absReal.startsWith(tmpReal + '/') ||
|
|
146
|
+
absReal.startsWith(tmpReal + '\\') ||
|
|
147
|
+
absPath === tmp ||
|
|
148
|
+
absPath.startsWith(tmp + '/') ||
|
|
149
|
+
absPath.startsWith(tmp + '\\')
|
|
150
|
+
) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
121
153
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
// V155-059: gate the system-prefix lists by platform so cross-platform
|
|
155
|
+
// false positives go away. macOS users packing under /Library/MyProjects
|
|
156
|
+
// shouldn't trip the Windows blocklist, and Windows users shouldn't trip
|
|
157
|
+
// the POSIX blocklist. Prefer runtime-derived Windows prefixes when set.
|
|
158
|
+
const isWin = process.platform === 'win32';
|
|
159
|
+
const dynamicWinDirs = isWin
|
|
160
|
+
? [
|
|
161
|
+
process.env.windir,
|
|
162
|
+
process.env.ProgramFiles,
|
|
163
|
+
process.env['ProgramFiles(x86)'],
|
|
164
|
+
].filter((s) => typeof s === 'string' && s.length > 0)
|
|
165
|
+
: [];
|
|
166
|
+
const blockedForPlatform = isWin
|
|
167
|
+
? [...WINDOWS_SYSTEM_DIRS, ...dynamicWinDirs]
|
|
168
|
+
: [...POSIX_SYSTEM_DIRS];
|
|
169
|
+
|
|
170
|
+
const lc = absReal.toLowerCase();
|
|
171
|
+
for (const blocked of blockedForPlatform) {
|
|
125
172
|
const bl = blocked.toLowerCase();
|
|
126
173
|
if (lc === bl || lc.startsWith(bl + '/') || lc.startsWith(bl + '\\')) {
|
|
127
174
|
return true;
|
package/src/install.ps1
CHANGED
|
@@ -94,10 +94,25 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
94
94
|
if ($LASTEXITCODE -eq 0) {
|
|
95
95
|
# Self-heal stale origin URLs across host migrations (1.2.9 parity with install.js).
|
|
96
96
|
# Without this, Windows users on the pre-GitLab origin still 404 on every upgrade.
|
|
97
|
+
# V155-012: only rewrite ORIGINS THAT MATCH KNOWN STALE PATTERNS. Previously
|
|
98
|
+
# this clobbered SSH remotes, forks, and any user-customized origin — anyone
|
|
99
|
+
# working on the IJFW source itself ended up silently retargeted to upstream.
|
|
100
|
+
# Port the install.js STALE_PATTERNS allowlist verbatim (case-insensitive).
|
|
97
101
|
$currentOrigin = ($currentOriginRaw | Out-String).Trim()
|
|
98
|
-
|
|
102
|
+
$stalePatterns = @(
|
|
103
|
+
'^https://github\.com/seandonahoe/ijfw(\.git)?/?$',
|
|
104
|
+
'^https://github\.com/therealseandonahoe/ijfw(\.git)?/?$'
|
|
105
|
+
)
|
|
106
|
+
$isStale = $false
|
|
107
|
+
foreach ($pat in $stalePatterns) {
|
|
108
|
+
if ($currentOrigin -imatch $pat) { $isStale = $true; break }
|
|
109
|
+
}
|
|
110
|
+
if ($currentOrigin -and $isStale) {
|
|
99
111
|
Write-Host " origin migration: $currentOrigin -> $DEFAULT_REPO"
|
|
100
112
|
& git -C $target remote set-url origin $DEFAULT_REPO
|
|
113
|
+
if ($LASTEXITCODE -ne 0) {
|
|
114
|
+
Write-Warning " origin migration failed -- could not repoint $currentOrigin to $DEFAULT_REPO"
|
|
115
|
+
}
|
|
101
116
|
}
|
|
102
117
|
# fetch + hard checkout avoids ff-only failures from local divergence.
|
|
103
118
|
& git -C $target fetch --depth 1 origin $branch
|
|
@@ -118,7 +133,13 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
118
133
|
try {
|
|
119
134
|
& git clone --depth 1 --branch $branch $DEFAULT_REPO $target
|
|
120
135
|
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
121
|
-
|
|
136
|
+
# V155-013: expanded restore allowlist + retain backup if residual content remains
|
|
137
|
+
# so the operator can recover anything the allowlist doesn't know about.
|
|
138
|
+
$restoreAllowlist = @(
|
|
139
|
+
'memory', 'sessions', 'install.log', '.session-counter',
|
|
140
|
+
'ijfw', 'state', 'cache', 'logs', 'run', '.ijfw'
|
|
141
|
+
)
|
|
142
|
+
foreach ($item in $restoreAllowlist) {
|
|
122
143
|
$src = Join-Path $backupDir $item
|
|
123
144
|
if (Test-Path $src) {
|
|
124
145
|
$dst = Join-Path $target $item
|
|
@@ -126,7 +147,13 @@ function Invoke-CloneOrPull($target, $branch) {
|
|
|
126
147
|
Move-Item -LiteralPath $src -Destination $dst -Force
|
|
127
148
|
}
|
|
128
149
|
}
|
|
129
|
-
|
|
150
|
+
$residual = Get-ChildItem -LiteralPath $backupDir -Force -ErrorAction SilentlyContinue
|
|
151
|
+
if (-not $residual -or $residual.Count -eq 0) {
|
|
152
|
+
Remove-Item -Recurse -Force -LiteralPath $backupDir -ErrorAction SilentlyContinue
|
|
153
|
+
} else {
|
|
154
|
+
$sample = ($residual | Select-Object -First 8 | ForEach-Object { $_.Name }) -join ', '
|
|
155
|
+
Write-Warning " restored known dirs; backup retained at $backupDir (contains: $sample). Remove manually after verifying."
|
|
156
|
+
}
|
|
130
157
|
return "updated"
|
|
131
158
|
} catch {
|
|
132
159
|
if (Test-Path $target) { Remove-Item -Recurse -Force -LiteralPath $target }
|