@ijfw/install 1.2.6 → 1.2.8
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/CHANGELOG.md +10 -10
- package/README.md +2 -2
- package/dist/ijfw.js +13 -7
- package/dist/install.js +21 -10
- package/dist/uninstall.js +4 -4
- package/docs/GUIDE.md +3 -3
- package/docs/guide/assets/hero.png +0 -0
- package/package.json +4 -4
- package/src/install.ps1 +204 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [1.2.6] -- 2026-05-01
|
|
4
4
|
|
|
5
|
-
**Token sandbox + parallel workflow dispatch + DeepSeek frontier upgrade.** A new `ijfw_run` MCP tool keeps large command output out of your context window entirely
|
|
5
|
+
**Token sandbox + parallel workflow dispatch + DeepSeek frontier upgrade.** A new `ijfw_run` MCP tool keeps large command output out of your context window entirely -- builds, test suites, grep runs, and log tails are sandboxed to disk and summarized in a few lines instead of flooding thousands of tokens. The `ijfw-workflow` execution engine gains a formal Wave Table that makes parallel agent dispatch deterministic rather than inferred. DeepSeek moves to `deepseek-v4-pro` -- the actual frontier model -- so the Trident gets Frontier AI checking Frontier AI.
|
|
6
6
|
|
|
7
|
-
### `ijfw_run`
|
|
7
|
+
### `ijfw_run` -- command output sandbox
|
|
8
8
|
|
|
9
|
-
Large shell commands (builds, test suites, `grep -r`, log tails) routinely produce hundreds or thousands of lines that consume a disproportionate share of the context window. `ijfw_run` solves this at the tool level: run the command via `child_process.spawn` (never `exec`
|
|
9
|
+
Large shell commands (builds, test suites, `grep -r`, log tails) routinely produce hundreds or thousands of lines that consume a disproportionate share of the context window. `ijfw_run` solves this at the tool level: run the command via `child_process.spawn` (never `exec` -- no RAM buffer ceiling), stream output to `~/.ijfw/session-sandbox/`, and return a domain-aware summary to context instead of the raw flood.
|
|
10
10
|
|
|
11
11
|
**Domain-aware summarizers** detect output type by pattern and extract only what matters:
|
|
12
12
|
- **Test runner** (Jest/Vitest/pytest/go test/cargo test): pass/fail counts + failing test names only
|
|
@@ -15,23 +15,23 @@ Large shell commands (builds, test suites, `grep -r`, log tails) routinely produ
|
|
|
15
15
|
- **Log**: ERROR/WARN lines + counts
|
|
16
16
|
- **Raw fallback**: first 15 + last 10 lines + "N lines omitted"
|
|
17
17
|
|
|
18
|
-
Every summary appends the last 10 raw lines as a reliability backstop (catches segfaults, OOM kills, and non-standard failures that heuristics miss), and includes the command, exit code, duration, and a retrieval label. Commands at or under 40 lines / 50 KB return inline with zero overhead
|
|
18
|
+
Every summary appends the last 10 raw lines as a reliability backstop (catches segfaults, OOM kills, and non-standard failures that heuristics miss), and includes the command, exit code, duration, and a retrieval label. Commands at or under 40 lines / 50 KB return inline with zero overhead -- `ijfw_run` only sandboxes when it pays off.
|
|
19
19
|
|
|
20
20
|
**Retrieval**: full output is indexed to `~/.ijfw/session-sandbox/{label}.txt` with a `.json` metadata sidecar. Retrieve with `ijfw_memory_search({ scope: "sandbox", label: "..." })` or list all current sandbox entries with `ijfw_memory_search({ scope: "sandbox" })`. Sandbox files auto-purge after 24 hours (TTL sweep runs on every `ijfw_run` call).
|
|
21
21
|
|
|
22
22
|
**Security**: all labels are sanitized before becoming filenames; sandbox files are written at mode `0o600` (user-read-only); all SQLite interactions use parameterized queries; ANSI escape codes are stripped before heuristic detection and before content is returned to the LLM context.
|
|
23
23
|
|
|
24
|
-
**Routing rule**: `ijfw-core` SKILL.md now carries the one-line routing rule
|
|
24
|
+
**Routing rule**: `ijfw-core` SKILL.md now carries the one-line routing rule -- large-output commands → `ijfw_run`; git, navigation, and quick ops → Bash directly.
|
|
25
25
|
|
|
26
26
|
**`ijfw_memory_status` retired** to free the MCP tool slot. The case handler is preserved for backward compatibility; the tool no longer appears in `tools/list`. Status information remains available via `ijfw_memory_prelude`.
|
|
27
27
|
|
|
28
28
|
**New `sanitizeForSandbox()`** in `sanitizer.js`: a sandbox-specific sanitizer that preserves newlines (unlike `sanitizeContent` which collapses to `" | "`), strips ANSI codes, defangs structural markdown elements (`#` headings, fenced code delimiters, `<system>/<prompt>/<assistant>` tags), and truncates lines over 2000 characters. Used for all LLM-facing sandbox output.
|
|
29
29
|
|
|
30
|
-
**`sandbox-nudge.sh` PreToolUse hook**: registered alongside the existing `pre-tool-use.sh`, this advisory hook pattern-matches known large-output command prefixes (`npm test`, `jest`, `vitest`, `pytest`, `cargo build`, `cargo test`, `make`, `gradle`, `mvn`, `go test`, `node --test`, `tsc --`, `webpack`, `vite build`, `rollup`, `grep -r`, `find /`) and emits a one-line nudge. Advisory only
|
|
30
|
+
**`sandbox-nudge.sh` PreToolUse hook**: registered alongside the existing `pre-tool-use.sh`, this advisory hook pattern-matches known large-output command prefixes (`npm test`, `jest`, `vitest`, `pytest`, `cargo build`, `cargo test`, `make`, `gradle`, `mvn`, `go test`, `node --test`, `tsc --`, `webpack`, `vite build`, `rollup`, `grep -r`, `find /`) and emits a one-line nudge. Advisory only -- never blocks.
|
|
31
31
|
|
|
32
32
|
Files: `mcp-server/src/sandbox.js` (new), `mcp-server/src/server.js`, `mcp-server/src/sanitizer.js`, `mcp-server/test-sandbox.js` (new, 32 tests), `mcp-server/test.js` (slot-swap update), `claude/skills/ijfw-core/SKILL.md`, `claude/hooks/hooks.json`, `claude/hooks/scripts/sandbox-nudge.sh` (new).
|
|
33
33
|
|
|
34
|
-
### Parallel workflow dispatch
|
|
34
|
+
### Parallel workflow dispatch -- Wave Table
|
|
35
35
|
|
|
36
36
|
The `ijfw-workflow` execution engine had a structural gap: Step 5 (Plan) described dependency relationships in prose, which meant Step 6 (Execute) had to re-infer parallelism at dispatch time. Re-inference defaults to sequential to avoid mistakes. The result: agents that could run concurrently ran one-by-one.
|
|
37
37
|
|
|
@@ -40,12 +40,12 @@ The `ijfw-workflow` execution engine had a structural gap: Step 5 (Plan) describ
|
|
|
40
40
|
```
|
|
41
41
|
| Wave | Tasks | Mode | Depends on | Reason |
|
|
42
42
|
|------|-----------|------------|------------|---------------------|
|
|
43
|
-
| W1 | t1, t2, t3 | PARALLEL |
|
|
43
|
+
| W1 | t1, t2, t3 | PARALLEL | -- | independent files |
|
|
44
44
|
| W2 | t4 | SEQUENTIAL | W1 | needs t2 output |
|
|
45
45
|
| W3 | t5, t6 | PARALLEL | W2 | independent of each |
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Wave mode is determined by a four-question dependency test before the table is written: (a) shared file writes? (b) one reads what the other writes? (c) output dependency? (d) otherwise → PARALLEL. The Wave Table is the execution contract
|
|
48
|
+
Wave mode is determined by a four-question dependency test before the table is written: (a) shared file writes? (b) one reads what the other writes? (c) output dependency? (d) otherwise → PARALLEL. The Wave Table is the execution contract -- decided once at plan time.
|
|
49
49
|
|
|
50
50
|
**Step 6 reads the Wave Table directly**: PARALLEL waves → all tasks dispatched as Agent tool calls in a single response (they run concurrently); SEQUENTIAL waves → one Agent call, wait for result, advance. If `plan.md` has no Wave Table (legacy plans, quick-mode tasks), Step 6 builds one on the spot using the same four-question test before dispatching anything. The instruction is now unambiguous: parallel waves produce multiple tool calls in one response block, not one-by-one messages.
|
|
51
51
|
|
|
@@ -53,7 +53,7 @@ Files: `claude/skills/ijfw-workflow/SKILL.md`.
|
|
|
53
53
|
|
|
54
54
|
### DeepSeek Trident auditor upgraded to `deepseek-v4-pro`
|
|
55
55
|
|
|
56
|
-
The 1.2.5 DeepSeek roster entry used `deepseek-v4-flash` as the API model ID
|
|
56
|
+
The 1.2.5 DeepSeek roster entry used `deepseek-v4-flash` as the API model ID -- a model that does not exist on DeepSeek Platform. Calls returned 4xx errors that surfaced as apparent timeouts. The entry is corrected to `deepseek-v4-pro`: DeepSeek's 1.6T-parameter frontier model (49B activated), supporting 1M context and dual thinking/non-thinking modes. `deepseek-chat` and `deepseek-reasoner` -- the previous canonical aliases -- are deprecated aliases for V4-Flash non-thinking and thinking modes respectively, scheduled for removal 2026-07-24. `deepseek-v4-pro` is the correct Trident-grade choice: Frontier AI checking Frontier AI.
|
|
57
57
|
|
|
58
58
|
Files: `mcp-server/src/audit-roster.js`.
|
|
59
59
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ijfw/install
|
|
2
2
|
|
|
3
|
-
One-command installer for [IJFW](https://
|
|
3
|
+
One-command installer for [IJFW](https://gitlab.com/therealseandonahoe/ijfw) -- the AI
|
|
4
4
|
efficiency layer for Claude Code, Codex, Gemini, Cursor, Windsurf, Copilot.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -43,7 +43,7 @@ Requires `node >=18`, `git`, `bash`. On native Windows use the PowerShell
|
|
|
43
43
|
installer (PS 5.1+), which shells Git Bash under the hood -- no WSL required:
|
|
44
44
|
|
|
45
45
|
```powershell
|
|
46
|
-
iwr https://
|
|
46
|
+
iwr https://gitlab.com/therealseandonahoe/ijfw/-/raw/main/installer/src/install.ps1 -OutFile install.ps1
|
|
47
47
|
.\install.ps1 -Dir $env:USERPROFILE\.ijfw
|
|
48
48
|
```
|
|
49
49
|
|
package/dist/ijfw.js
CHANGED
|
@@ -736,7 +736,8 @@ async function run10(ctx) {
|
|
|
736
736
|
const build = spawnSync10("npm", ["run", "build"], {
|
|
737
737
|
encoding: "utf8",
|
|
738
738
|
cwd: installerDir,
|
|
739
|
-
timeout: 6e4
|
|
739
|
+
timeout: 6e4,
|
|
740
|
+
shell: process.platform === "win32"
|
|
740
741
|
});
|
|
741
742
|
if (build.status !== 0) {
|
|
742
743
|
return {
|
|
@@ -750,7 +751,8 @@ async function run10(ctx) {
|
|
|
750
751
|
const pack = spawnSync10("npm", ["pack", "--silent"], {
|
|
751
752
|
encoding: "utf8",
|
|
752
753
|
cwd: installerDir,
|
|
753
|
-
timeout: 3e4
|
|
754
|
+
timeout: 3e4,
|
|
755
|
+
shell: process.platform === "win32"
|
|
754
756
|
});
|
|
755
757
|
if (pack.status !== 0) {
|
|
756
758
|
return {
|
|
@@ -783,7 +785,8 @@ async function run10(ctx) {
|
|
|
783
785
|
encoding: "utf8",
|
|
784
786
|
cwd: installDir,
|
|
785
787
|
timeout: 6e4,
|
|
786
|
-
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome }
|
|
788
|
+
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome },
|
|
789
|
+
shell: process.platform === "win32"
|
|
787
790
|
});
|
|
788
791
|
if (install.status !== 0) {
|
|
789
792
|
return {
|
|
@@ -889,7 +892,8 @@ async function run11(ctx) {
|
|
|
889
892
|
const build = spawnSync11("npm", ["run", "build"], {
|
|
890
893
|
encoding: "utf8",
|
|
891
894
|
cwd: installerDir,
|
|
892
|
-
timeout: 6e4
|
|
895
|
+
timeout: 6e4,
|
|
896
|
+
shell: process.platform === "win32"
|
|
893
897
|
});
|
|
894
898
|
if (build.status !== 0) {
|
|
895
899
|
return {
|
|
@@ -903,7 +907,8 @@ async function run11(ctx) {
|
|
|
903
907
|
const pack = spawnSync11("npm", ["pack", "--silent"], {
|
|
904
908
|
encoding: "utf8",
|
|
905
909
|
cwd: installerDir,
|
|
906
|
-
timeout: 3e4
|
|
910
|
+
timeout: 3e4,
|
|
911
|
+
shell: process.platform === "win32"
|
|
907
912
|
});
|
|
908
913
|
if (pack.status !== 0) {
|
|
909
914
|
return {
|
|
@@ -929,7 +934,8 @@ async function run11(ctx) {
|
|
|
929
934
|
encoding: "utf8",
|
|
930
935
|
cwd: installDir,
|
|
931
936
|
timeout: 6e4,
|
|
932
|
-
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome }
|
|
937
|
+
env: { ...process.env, HOME: fakeHome, npm_config_prefix: fakeHome },
|
|
938
|
+
shell: process.platform === "win32"
|
|
933
939
|
});
|
|
934
940
|
if (install.status !== 0) {
|
|
935
941
|
return {
|
|
@@ -2584,7 +2590,7 @@ async function main() {
|
|
|
2584
2590
|
];
|
|
2585
2591
|
const guidePath = candidates.find((p) => existsSync3(p));
|
|
2586
2592
|
if (!guidePath) {
|
|
2587
|
-
console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://
|
|
2593
|
+
console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://gitlab.com/therealseandonahoe/ijfw/-/blob/main/docs/GUIDE.md");
|
|
2588
2594
|
process.exit(1);
|
|
2589
2595
|
}
|
|
2590
2596
|
if (wantsBrowser) {
|
package/dist/install.js
CHANGED
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
// src/install.js
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync as existsSync2, rmSync, mkdirSync as mkdirSync2, realpathSync, renameSync as renameSync2 } from "node:fs";
|
|
6
|
-
import { resolve, join as join2, dirname as dirname2 } from "node:path";
|
|
6
|
+
import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
|
|
7
7
|
import { homedir as homedir2, platform } from "node:os";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/marketplace.js
|
|
11
11
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
function claudeSettingsPath() {
|
|
15
15
|
return join(homedir(), ".claude", "settings.json");
|
|
16
16
|
}
|
|
17
|
+
function pluginInstallPath(rootDir) {
|
|
18
|
+
if (rootDir) return resolve(rootDir, "claude");
|
|
19
|
+
if (process.env.IJFW_HOME) return resolve(process.env.IJFW_HOME, "claude");
|
|
20
|
+
return join(homedir(), ".ijfw", "claude");
|
|
21
|
+
}
|
|
17
22
|
function stripJsoncComments(raw) {
|
|
18
23
|
if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
|
|
19
24
|
let out = "";
|
|
@@ -69,7 +74,7 @@ function tolerantJsonParse(raw, filepath) {
|
|
|
69
74
|
throw err;
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
|
-
function mergeMarketplace(settingsPath = claudeSettingsPath()) {
|
|
77
|
+
function mergeMarketplace(settingsPath = claudeSettingsPath(), options = {}) {
|
|
73
78
|
let settings = {};
|
|
74
79
|
if (existsSync(settingsPath)) {
|
|
75
80
|
const raw = readFileSync(settingsPath, "utf8");
|
|
@@ -77,9 +82,10 @@ function mergeMarketplace(settingsPath = claudeSettingsPath()) {
|
|
|
77
82
|
} else {
|
|
78
83
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
79
84
|
}
|
|
85
|
+
const pluginPath = options.pluginPath || pluginInstallPath(options.rootDir);
|
|
80
86
|
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
81
87
|
settings.extraKnownMarketplaces.ijfw = {
|
|
82
|
-
source: { source: "
|
|
88
|
+
source: { source: "directory", path: pluginPath }
|
|
83
89
|
};
|
|
84
90
|
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
85
91
|
if ("ijfw-core@ijfw" in settings.enabledPlugins) {
|
|
@@ -93,7 +99,7 @@ function mergeMarketplace(settingsPath = claudeSettingsPath()) {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
// src/install.js
|
|
96
|
-
var DEFAULT_REPO = "https://
|
|
102
|
+
var DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git";
|
|
97
103
|
var DEFAULT_BRANCH = "main";
|
|
98
104
|
function parseArgs(argv) {
|
|
99
105
|
const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false };
|
|
@@ -202,8 +208,8 @@ function findBash() {
|
|
|
202
208
|
return null;
|
|
203
209
|
}
|
|
204
210
|
function resolveTarget(opt) {
|
|
205
|
-
if (opt.dir) return
|
|
206
|
-
if (process.env.IJFW_HOME) return
|
|
211
|
+
if (opt.dir) return resolve2(opt.dir);
|
|
212
|
+
if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
|
|
207
213
|
return join2(homedir2(), ".ijfw");
|
|
208
214
|
}
|
|
209
215
|
function runCheck(cmd, args, opts) {
|
|
@@ -219,8 +225,13 @@ function cloneOrPull(dir, branch) {
|
|
|
219
225
|
}
|
|
220
226
|
const hasGit = existsSync2(join2(dir, ".git"));
|
|
221
227
|
if (hasGit) {
|
|
222
|
-
const { status: remoteStatus } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
228
|
+
const { status: remoteStatus, stdout } = runCheck("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
223
229
|
if (remoteStatus === 0) {
|
|
230
|
+
const currentOrigin = (stdout || "").trim();
|
|
231
|
+
if (currentOrigin && currentOrigin !== DEFAULT_REPO) {
|
|
232
|
+
console.log(` origin migration: ${currentOrigin} -> ${DEFAULT_REPO}`);
|
|
233
|
+
spawnSync("git", ["-C", dir, "remote", "set-url", "origin", DEFAULT_REPO], { stdio: "inherit" });
|
|
234
|
+
}
|
|
224
235
|
const fetch = spawnSync("git", ["-C", dir, "fetch", "--depth", "1", "origin", branch], { stdio: "inherit" });
|
|
225
236
|
if (fetch.status !== 0) throw new Error(`IJFW fetch did not complete (exit ${fetch.status}) -- check network access and retry.`);
|
|
226
237
|
const co = spawnSync("git", ["-C", dir, "checkout", "-f", "FETCH_HEAD"], { stdio: "inherit" });
|
|
@@ -253,7 +264,7 @@ function runInstallScript(dir) {
|
|
|
253
264
|
const script = join2(dir, "scripts", "install.sh");
|
|
254
265
|
if (!existsSync2(script)) throw new Error(`IJFW install script not found at ${script} -- re-run the installer to restore it.`);
|
|
255
266
|
const canonicalDir = join2(homedir2(), ".ijfw");
|
|
256
|
-
const isCustomDir =
|
|
267
|
+
const isCustomDir = resolve2(dir) !== canonicalDir ? "1" : "0";
|
|
257
268
|
const env = {
|
|
258
269
|
...process.env,
|
|
259
270
|
IJFW_NONINTERACTIVE: process.env.CI ? "1" : process.env.IJFW_NONINTERACTIVE ?? "",
|
|
@@ -296,7 +307,7 @@ async function main() {
|
|
|
296
307
|
console.log(" platform configs applied");
|
|
297
308
|
if (!opts.noMarketplace) {
|
|
298
309
|
const settingsPath = claudeSettingsPath();
|
|
299
|
-
mergeMarketplace(settingsPath);
|
|
310
|
+
mergeMarketplace(settingsPath, { rootDir: target });
|
|
300
311
|
console.log(` marketplace registered in ${settingsPath}`);
|
|
301
312
|
}
|
|
302
313
|
console.log("");
|
package/dist/uninstall.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/uninstall.js
|
|
4
4
|
import { existsSync as existsSync2, rmSync, cpSync, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
|
|
5
|
-
import { resolve, join as join2 } from "node:path";
|
|
5
|
+
import { resolve as resolve2, join as join2 } from "node:path";
|
|
6
6
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
// src/marketplace.js
|
|
10
10
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
function claudeSettingsPath() {
|
|
14
14
|
return join(homedir(), ".claude", "settings.json");
|
|
@@ -291,8 +291,8 @@ function cleanPlatforms() {
|
|
|
291
291
|
return removed;
|
|
292
292
|
}
|
|
293
293
|
function resolveTarget(opt) {
|
|
294
|
-
if (opt.dir) return
|
|
295
|
-
if (process.env.IJFW_HOME) return
|
|
294
|
+
if (opt.dir) return resolve2(opt.dir);
|
|
295
|
+
if (process.env.IJFW_HOME) return resolve2(process.env.IJFW_HOME);
|
|
296
296
|
return join2(homedir2(), ".ijfw");
|
|
297
297
|
}
|
|
298
298
|
async function main() {
|
package/docs/GUIDE.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://
|
|
2
|
+
<img src="https://gitlab.com/therealseandonahoe/ijfw/releases/download/v1.1.1/ijfw-hero.png" alt="IJFW" width="100%"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# The IJFW Guide
|
|
@@ -503,7 +503,7 @@ cat ~/.ijfw/dashboard.port
|
|
|
503
503
|
|
|
504
504
|
### Still stuck
|
|
505
505
|
|
|
506
|
-
Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [
|
|
506
|
+
Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [gitlab.com/therealseandonahoe/ijfw/-/issues](https://gitlab.com/therealseandonahoe/ijfw/-/issues) with both files redacted and attached.
|
|
507
507
|
|
|
508
508
|
---
|
|
509
509
|
|
|
@@ -546,7 +546,7 @@ Yes. `.ijfw/team/` is git-committed by default. Decisions, patterns, and stack c
|
|
|
546
546
|
</p>
|
|
547
547
|
|
|
548
548
|
<p align="center">
|
|
549
|
-
<a href="https://
|
|
549
|
+
<a href="https://gitlab.com/therealseandonahoe/ijfw">gitlab.com/therealseandonahoe/ijfw</a>
|
|
550
550
|
|
|
|
551
551
|
<a href="https://www.npmjs.com/package/@ijfw/install">npm</a>
|
|
552
552
|
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/install",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
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": {
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
],
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"author": "Sean Donahoe",
|
|
49
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://gitlab.com/therealseandonahoe/ijfw",
|
|
50
50
|
"bugs": {
|
|
51
|
-
"url": "https://
|
|
51
|
+
"url": "https://gitlab.com/therealseandonahoe/ijfw/-/issues"
|
|
52
52
|
},
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|
|
55
|
-
"url": "git+https://
|
|
55
|
+
"url": "git+https://gitlab.com/therealseandonahoe/ijfw.git"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public",
|
package/src/install.ps1
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# -> merge marketplace into %USERPROFILE%\.claude\settings.json -> summary.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# Invoke-Expression (iwr https://
|
|
9
|
+
# Invoke-Expression (iwr https://gitlab.com/therealseandonahoe/ijfw/-/raw/main/installer/src/install.ps1).Content
|
|
10
10
|
# or:
|
|
11
11
|
# .\install.ps1 -Dir C:\Users\me\.ijfw -Branch main
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ param(
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
$ErrorActionPreference = "Stop"
|
|
22
|
-
$DEFAULT_REPO = "https://
|
|
22
|
+
$DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git"
|
|
23
23
|
|
|
24
24
|
function Write-Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green }
|
|
25
25
|
function Write-Info($msg) { Write-Host " ... $msg" -ForegroundColor Gray }
|
|
@@ -208,10 +208,172 @@ function ConvertFrom-Jsonc($raw) {
|
|
|
208
208
|
return ($intermediate -replace ',(\s*[}\]])','$1')
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function Remove-StalePosixLaunchers {
|
|
212
|
+
# Pre-1.2.7 installs (and any install run from WSL) wrote POSIX bash
|
|
213
|
+
# launchers into ~/.local/bin/ even on Windows. Windows can't execute
|
|
214
|
+
# files without an extension, so PATH lookup finds them ahead of npm's
|
|
215
|
+
# ijfw.cmd shim and Windows hands them to Notepad as plain text.
|
|
216
|
+
#
|
|
217
|
+
# On every install, sweep ~/.local/bin/ for the five known POSIX
|
|
218
|
+
# launchers. Defensive: only remove files whose first line is a shebang
|
|
219
|
+
# ("#!") so we never delete user content or Windows binaries that
|
|
220
|
+
# happened to share a name.
|
|
221
|
+
$localBin = Join-Path $env:USERPROFILE ".local\bin"
|
|
222
|
+
if (-not (Test-Path $localBin)) { return }
|
|
223
|
+
|
|
224
|
+
$launchers = @("ijfw", "ijfw-dashboard", "ijfw-dispatch-plan", "ijfw-memorize", "ijfw-memory")
|
|
225
|
+
$removed = 0
|
|
226
|
+
foreach ($name in $launchers) {
|
|
227
|
+
$path = Join-Path $localBin $name
|
|
228
|
+
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
|
|
229
|
+
try {
|
|
230
|
+
$first = (Get-Content -LiteralPath $path -TotalCount 1 -ErrorAction Stop)
|
|
231
|
+
if ($first -and $first.StartsWith("#!")) {
|
|
232
|
+
Remove-Item -LiteralPath $path -Force -ErrorAction Stop
|
|
233
|
+
$removed++
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
# Best-effort: keep going on locked / inaccessible files.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if ($removed -gt 0) {
|
|
240
|
+
Write-Ok "Cleared $removed stale POSIX launcher$(if ($removed -ne 1) { 's' }) from $localBin (npm shim now wins PATH)"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function Provision-Plugin {
|
|
245
|
+
param(
|
|
246
|
+
[string]$Src, # Source dir inside repo (relative to $IjfwHome), e.g. "wayland\plugins\ijfw"
|
|
247
|
+
[string]$Dst, # Absolute destination path, e.g. "$env:USERPROFILE\.wayland\plugins\ijfw"
|
|
248
|
+
[string]$IjfwHome
|
|
249
|
+
)
|
|
250
|
+
$srcPath = Join-Path $IjfwHome $Src
|
|
251
|
+
if (-not (Test-Path $srcPath)) {
|
|
252
|
+
Write-Info "Plugin source not found at $srcPath -- skipping."
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
New-Item -ItemType Directory -Force -Path $Dst | Out-Null
|
|
256
|
+
Copy-Item -Recurse -Force -Path (Join-Path $srcPath "*") -Destination $Dst
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function Merge-PluginsEnabled {
|
|
260
|
+
param(
|
|
261
|
+
[string]$ConfigPath # Full path to ~/.hermes/config.yaml
|
|
262
|
+
)
|
|
263
|
+
$pluginName = "ijfw"
|
|
264
|
+
$configDir = Split-Path -Parent $ConfigPath
|
|
265
|
+
|
|
266
|
+
# Ensure the directory exists.
|
|
267
|
+
if (-not (Test-Path $configDir)) { New-Item -ItemType Directory -Force -Path $configDir | Out-Null }
|
|
268
|
+
|
|
269
|
+
# Backup before modifying (mirrors Merge-Marketplace pattern).
|
|
270
|
+
if (Test-Path $ConfigPath) {
|
|
271
|
+
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
272
|
+
$backup = "$ConfigPath.bak.plugins.$ts"
|
|
273
|
+
Copy-Item -LiteralPath $ConfigPath -Destination $backup -Force
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Read existing YAML or start from empty string.
|
|
277
|
+
$content = if (Test-Path $ConfigPath) { Get-Content -Raw -LiteralPath $ConfigPath } else { "" }
|
|
278
|
+
if ($null -eq $content) { $content = "" }
|
|
279
|
+
|
|
280
|
+
# Strategy: try python3 first (has real YAML support); fall back to sentinel-anchored
|
|
281
|
+
# regex that appends to a plugins.enabled: block or creates one.
|
|
282
|
+
|
|
283
|
+
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
284
|
+
if ($python) {
|
|
285
|
+
$pyScript = @"
|
|
286
|
+
import sys, re
|
|
287
|
+
path = sys.argv[1]
|
|
288
|
+
name = sys.argv[2]
|
|
289
|
+
try:
|
|
290
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
291
|
+
text = f.read()
|
|
292
|
+
except FileNotFoundError:
|
|
293
|
+
text = ''
|
|
294
|
+
|
|
295
|
+
# Find or create plugins.enabled block.
|
|
296
|
+
block_re = re.compile(r'(?m)^(plugins\s*:\s*\n(?:[ \t]+\S[^\n]*\n)*[ \t]+enabled\s*:\s*\[)([^\]]*)\]')
|
|
297
|
+
inline_re = re.compile(r'(?m)^([ \t]+enabled\s*:\s*\[)([^\]]*)\]')
|
|
298
|
+
plugins_section_re = re.compile(r'(?m)^plugins\s*:', re.MULTILINE)
|
|
299
|
+
|
|
300
|
+
def ensure_name(lst_str, name):
|
|
301
|
+
items = [x.strip().strip('"').strip("'") for x in lst_str.split(',') if x.strip()]
|
|
302
|
+
if name in items:
|
|
303
|
+
return lst_str
|
|
304
|
+
items.append(name)
|
|
305
|
+
return ', '.join(repr(i) for i in items)
|
|
306
|
+
|
|
307
|
+
m = block_re.search(text)
|
|
308
|
+
if m:
|
|
309
|
+
new_list = ensure_name(m.group(2), name)
|
|
310
|
+
text = text[:m.start(2)] + new_list + text[m.end(2):]
|
|
311
|
+
else:
|
|
312
|
+
m2 = inline_re.search(text)
|
|
313
|
+
if m2:
|
|
314
|
+
new_list = ensure_name(m2.group(2), name)
|
|
315
|
+
text = text[:m2.start(2)] + new_list + text[m2.end(2):]
|
|
316
|
+
elif plugins_section_re.search(text):
|
|
317
|
+
# plugins: exists but no enabled key -- append it.
|
|
318
|
+
text = plugins_section_re.sub(lambda mo: mo.group(0) + '\n enabled: [' + repr(name) + ']', text, count=1)
|
|
319
|
+
else:
|
|
320
|
+
# No plugins section at all.
|
|
321
|
+
if text and not text.endswith('\n'):
|
|
322
|
+
text += '\n'
|
|
323
|
+
text += 'plugins:\n enabled: [' + repr(name) + ']\n'
|
|
324
|
+
|
|
325
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
326
|
+
f.write(text)
|
|
327
|
+
sys.exit(0)
|
|
328
|
+
"@
|
|
329
|
+
$tmp = [System.IO.Path]::GetTempFileName() + ".py"
|
|
330
|
+
[System.IO.File]::WriteAllText($tmp, $pyScript)
|
|
331
|
+
try {
|
|
332
|
+
& python3 $tmp $ConfigPath $pluginName
|
|
333
|
+
$ok = ($LASTEXITCODE -eq 0)
|
|
334
|
+
} finally {
|
|
335
|
+
Remove-Item -LiteralPath $tmp -ErrorAction SilentlyContinue
|
|
336
|
+
}
|
|
337
|
+
if ($ok) { return $true }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Fallback: pure PowerShell sentinel-anchored regex approach.
|
|
341
|
+
# If the file has a plugins.enabled: [...] line, splice in the name.
|
|
342
|
+
# Otherwise append a plugins.enabled block.
|
|
343
|
+
$enabledRe = [regex]'(?m)^([ \t]+enabled\s*:\s*\[)([^\]]*)\]'
|
|
344
|
+
$pluginsSectionRe = [regex]'(?m)^plugins\s*:'
|
|
345
|
+
|
|
346
|
+
$m = $enabledRe.Match($content)
|
|
347
|
+
if ($m.Success) {
|
|
348
|
+
$lst = $m.Groups[2].Value
|
|
349
|
+
$items = $lst -split ',' | ForEach-Object { $_.Trim().Trim('"').Trim("'") } | Where-Object { $_ -ne '' }
|
|
350
|
+
if ($pluginName -notin $items) {
|
|
351
|
+
$items += $pluginName
|
|
352
|
+
$newLst = ($items | ForEach-Object { '"' + $_ + '"' }) -join ', '
|
|
353
|
+
$content = $content.Substring(0, $m.Groups[2].Index) + $newLst + $content.Substring($m.Groups[2].Index + $m.Groups[2].Length)
|
|
354
|
+
}
|
|
355
|
+
} elseif ($pluginsSectionRe.IsMatch($content)) {
|
|
356
|
+
$content = $pluginsSectionRe.Replace($content, "plugins:`n enabled: [""$pluginName""]", 1)
|
|
357
|
+
} else {
|
|
358
|
+
if ($content -and -not $content.EndsWith("`n")) { $content += "`n" }
|
|
359
|
+
$content += "plugins:`n enabled: [""$pluginName""]`n"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
$tmp2 = "$ConfigPath.tmp"
|
|
363
|
+
[System.IO.File]::WriteAllText($tmp2, $content, [System.Text.Encoding]::UTF8)
|
|
364
|
+
Move-Item -Force -LiteralPath $tmp2 -Destination $ConfigPath
|
|
365
|
+
return $true
|
|
366
|
+
}
|
|
367
|
+
|
|
211
368
|
function Merge-Marketplace {
|
|
369
|
+
param(
|
|
370
|
+
[string]$IjfwHome # Install root; the plugin lives at "$IjfwHome\claude".
|
|
371
|
+
)
|
|
212
372
|
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
|
|
213
373
|
$settingsDir = Split-Path -Parent $settingsPath
|
|
214
374
|
if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null }
|
|
375
|
+
if (-not $IjfwHome) { $IjfwHome = Join-Path $env:USERPROFILE ".ijfw" }
|
|
376
|
+
$pluginPath = Join-Path $IjfwHome "claude"
|
|
215
377
|
|
|
216
378
|
$settings = @{}
|
|
217
379
|
if (Test-Path $settingsPath) {
|
|
@@ -236,7 +398,11 @@ function Merge-Marketplace {
|
|
|
236
398
|
}
|
|
237
399
|
}
|
|
238
400
|
if (-not $settings.ContainsKey('extraKnownMarketplaces')) { $settings['extraKnownMarketplaces'] = @{} }
|
|
239
|
-
|
|
401
|
+
# Self-heal: write the directory source matching the actual install. Prior
|
|
402
|
+
# installs (<= 1.2.6) wrote a github source pointing at TheRealSeanDonahoe/
|
|
403
|
+
# ijfw, but Claude Code never clones that repo into its marketplaces cache,
|
|
404
|
+
# so the entry resolved to "Marketplace file not found". Idempotent.
|
|
405
|
+
$settings.extraKnownMarketplaces['ijfw'] = @{ source = @{ source = 'directory'; path = $pluginPath } }
|
|
240
406
|
if (-not $settings.ContainsKey('enabledPlugins')) { $settings['enabledPlugins'] = @{} }
|
|
241
407
|
# Opportunistically clean up the legacy key written by v1.0.0-1.0.2.
|
|
242
408
|
if ($settings.enabledPlugins.ContainsKey('ijfw-core@ijfw')) {
|
|
@@ -260,15 +426,49 @@ if ($issues.Count -gt 0) {
|
|
|
260
426
|
|
|
261
427
|
$target = Get-Target
|
|
262
428
|
|
|
429
|
+
# Sweep stale POSIX bash launchers from ~/.local/bin/ before any other
|
|
430
|
+
# install logic. Without this, leftover #!/usr/bin/env bash files from a
|
|
431
|
+
# pre-1.2.7 install (or one run from WSL) shadow npm's ijfw.cmd shim and
|
|
432
|
+
# Windows opens them in Notepad. (Bug report: John H.)
|
|
433
|
+
Remove-StalePosixLaunchers
|
|
434
|
+
|
|
263
435
|
# scripts/install.sh owns the summary (Live now / Standing by / next step).
|
|
264
436
|
# Keep clone/pull output suppressed so the final banner reads clean.
|
|
265
437
|
Invoke-CloneOrPull $target $Branch | Out-Null
|
|
266
438
|
|
|
267
439
|
Invoke-InstallScript $target
|
|
268
440
|
|
|
441
|
+
# Provision Wayland plugin (~/.wayland/plugins/ijfw/).
|
|
442
|
+
$waylandPluginDst = Join-Path $env:USERPROFILE ".wayland\plugins\ijfw"
|
|
443
|
+
Provision-Plugin -Src "wayland\plugins\ijfw" -Dst $waylandPluginDst -IjfwHome $target
|
|
444
|
+
|
|
445
|
+
# Provision Hermes plugin (~/.hermes/plugins/ijfw/).
|
|
446
|
+
$hermesPluginDst = Join-Path $env:USERPROFILE ".hermes\plugins\ijfw"
|
|
447
|
+
Provision-Plugin -Src "hermes\plugins\ijfw" -Dst $hermesPluginDst -IjfwHome $target
|
|
448
|
+
|
|
449
|
+
# Deploy shared patterns.json (~/.ijfw/shared/lib/patterns.json).
|
|
450
|
+
$patternsDir = Join-Path $env:USERPROFILE ".ijfw\shared\lib"
|
|
451
|
+
$patternsSrc = Join-Path $target "shared\lib\patterns.json"
|
|
452
|
+
if (Test-Path $patternsSrc) {
|
|
453
|
+
New-Item -ItemType Directory -Force -Path $patternsDir | Out-Null
|
|
454
|
+
Copy-Item -Force -LiteralPath $patternsSrc -Destination (Join-Path $patternsDir "patterns.json")
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# Add "ijfw" to plugins.enabled[] in ~/.hermes/config.yaml (Hermes opt-in allow-list).
|
|
458
|
+
$hermesConfig = Join-Path $env:USERPROFILE ".hermes\config.yaml"
|
|
459
|
+
[void](Merge-PluginsEnabled -ConfigPath $hermesConfig)
|
|
460
|
+
|
|
461
|
+
# Regenerate per-platform rules from shared source.
|
|
462
|
+
if (Get-Command node -ErrorAction SilentlyContinue) {
|
|
463
|
+
$rulesGen = Join-Path $target "scripts\generate-platform-rules.js"
|
|
464
|
+
if (Test-Path $rulesGen) { & node $rulesGen }
|
|
465
|
+
}
|
|
466
|
+
|
|
269
467
|
if (-not $NoMarketplace) {
|
|
270
468
|
# Best-effort: returns $true on success, prints its own message on fallback.
|
|
271
|
-
|
|
469
|
+
# Pass the resolved install root so the marketplace path matches reality
|
|
470
|
+
# for both default and --dir installs.
|
|
471
|
+
[void](Merge-Marketplace -IjfwHome $target)
|
|
272
472
|
}
|
|
273
473
|
|
|
274
474
|
$log = Join-Path $env:USERPROFILE ".ijfw\install.log"
|