@ijfw/install 1.5.3 → 1.5.4

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.4] -- 2026-05-26
4
+
5
+ **Wayland Hub Extension pack pipeline.** Adds `pack-hub-extension` -- a new CLI subcommand that produces the three distributable artifacts Wayland's prebuild sync script needs to bundle IJFW as a native Hub Extension without committing binaries to the Wayland repo.
6
+
7
+ ### Added
8
+
9
+ - `ijfw pack-hub-extension [--output <dir>]` produces three artifacts in one shot:
10
+ - `ijfw-<version>.zip` -- the installable Hub Extension bundle (manifest + lifecycle hooks + assets)
11
+ - `ijfw-<version>.sha512` -- SHA-512 SRI checksum for Wayland's integrity verifier
12
+ - `hub-index-snippet.json` -- drop-in entry for Wayland's Hub Index, ready to merge at build time
13
+ - `--output <dir>` flag lets callers stage artifacts anywhere (defaults to `dist/`); the flag is forwarded verbatim to `scripts/pack-hub-extension.js`.
14
+ - `scripts/pack-hub-extension.js` + `scripts/hub-extension/**` now ship inside the npm tarball, so Wayland's prebuild script can invoke the packer directly via `npx -y @ijfw/install@<version> pack-hub-extension --output <stage-dir>` with no local clone required.
15
+ - Hub Extension bundle includes `scripts/install.js` (runs `npx -y @ijfw/install@latest` non-interactively) and `scripts/uninstall.js` with split timeouts -- 100s for install, 30s for uninstall -- so Wayland's sandboxed fork never hangs the boot sequence.
16
+ - Manifest template (`aion-extension.json.tmpl`) declares all 15 `acpAdapters` so Wayland's existing verifier checks every supported CLI on PATH post-install. If any are missing, install fails with a clear error rather than silently succeeding.
17
+ - Wayland integration: their `scripts/sync-hub-extensions.ts` prebuild step invokes `pack-hub-extension` at every build so each Wayland release snapshots the latest published IJFW without storing binaries in the repo.
18
+
19
+ ### Internal
20
+
21
+ - `@ijfw/memory-server@1.5.4` republishes alongside with no functional changes vs 1.5.3 -- preserves the version-lockstep invariant between the two packages.
22
+ - Test coverage: `installer/test/test-pack-hub-extension.js` verifies artifact production, SHA-512 stability across runs on identical content, and manifest shape conformance.
23
+
3
24
  ## [1.3.2] -- 2026-05-15
4
25
 
5
26
  **Project-agnostic swarm orchestration + live visual workflow + richer Codex native surface.** Adds first-class Team Assembly, blackboard coordination, swarm task lifecycle, conservative git worktrees, recovery checkpoints, Superpowers-style live design previews, and Claude-parity Codex command aliases.
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ijfw",
3
+ "displayName": "IJFW — AI Efficiency Layer",
4
+ "version": "1.5.4",
5
+ "description": "One install, every AI coding agent, zero config. Unifies 15 CLIs under a shared MCP memory layer so context follows you across Claude, Codex, Gemini, Cursor, Windsurf, and 10 more.",
6
+ "author": "Sean Donahoe",
7
+ "icon": "assets/ijfw-logo.svg",
8
+ "dist": {
9
+ "tarball": "extensions/ijfw-1.5.4.zip",
10
+ "integrity": "sha512-VzLLJg/kN1vP4kp6uSe26PJi92JoxT0rgLyulG7JLFuqr9AetZKIQBSBD/d4qWMP5q0k7Mn0J4eI7uZl27pfFw==",
11
+ "unpackedSize": 3205
12
+ },
13
+ "engines": {
14
+ "wayland": ">=0.6.0"
15
+ },
16
+ "hubs": [
17
+ "acpAdapters",
18
+ "mcpServers"
19
+ ],
20
+ "contributes": {
21
+ "acpAdapters": [
22
+ "claude",
23
+ "codex",
24
+ "gemini",
25
+ "cursor",
26
+ "windsurf",
27
+ "copilot",
28
+ "hermes",
29
+ "wayland",
30
+ "aider",
31
+ "opencode",
32
+ "qwencode",
33
+ "cline",
34
+ "kimicode",
35
+ "openclaw",
36
+ "antigravity"
37
+ ],
38
+ "mcpServers": [
39
+ "ijfw-memory"
40
+ ]
41
+ },
42
+ "tags": [
43
+ "ai",
44
+ "coding",
45
+ "mcp",
46
+ "memory",
47
+ "multi-agent"
48
+ ]
49
+ }
package/dist/ijfw.js CHANGED
@@ -3782,6 +3782,16 @@ var COMMAND_REGISTRY = Object.freeze([
3782
3782
  helpGroup: "EXPLORE"
3783
3783
  },
3784
3784
  // ---------- TIER 2: COORDINATION (shown in `ijfw commands`) ----------
3785
+ {
3786
+ name: "pack-hub-extension",
3787
+ tier: "coordination",
3788
+ owner: "installer-direct",
3789
+ description: "Pack the Wayland Hub Extension (zip + SHA-512 + manifest snippet). Used by Wayland's prebuild sync.",
3790
+ aliases: [],
3791
+ since: "1.5.4",
3792
+ status: "active",
3793
+ helpGroup: "BUILD"
3794
+ },
3785
3795
  {
3786
3796
  name: "status",
3787
3797
  tier: "coordination",
@@ -4246,6 +4256,12 @@ async function main() {
4246
4256
  process.exit(r.status ?? 1);
4247
4257
  break;
4248
4258
  }
4259
+ case "pack-hub-extension": {
4260
+ const packScript = resolve4(__dirname2, "..", "scripts", "pack-hub-extension.js");
4261
+ const r = spawnSync12("node", [packScript, ...argv.slice(3)], { stdio: "inherit" });
4262
+ process.exit(r.status ?? 1);
4263
+ break;
4264
+ }
4249
4265
  case "uninstall": {
4250
4266
  const uninstallBin = resolve4(__dirname2, "..", "dist", "uninstall.js");
4251
4267
  const r = spawnSync12("node", [uninstallBin, ...argv.slice(3)], { stdio: "inherit" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "One-command installer for IJFW -- the AI efficiency layer. One install, every AI coding agent, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,8 @@
13
13
  "src/install.ps1",
14
14
  "docs/GUIDE.md",
15
15
  "docs/guide/assets",
16
+ "scripts/pack-hub-extension.js",
17
+ "scripts/hub-extension/**",
16
18
  "README.md",
17
19
  "CHANGELOG.md",
18
20
  "LICENSE"
@@ -20,9 +22,12 @@
20
22
  "scripts": {
21
23
  "build": "node scripts/build.js",
22
24
  "test": "node --test test.js",
25
+ "test:hub-extension": "node --test test/test-pack-hub-extension.js",
23
26
  "preflight": "node dist/ijfw.js preflight",
24
27
  "pack:check": "npm pack --dry-run",
25
- "prepublishOnly": "npm run build && npm run preflight"
28
+ "pack:hub-extension": "node scripts/pack-hub-extension.js",
29
+ "prepack": "node -e \"const fs=require('fs'),path=require('path');const d='dist';if(fs.existsSync(d)){for(const f of fs.readdirSync(d)){if(/^ijfw-\\d+\\.\\d+\\.\\d+\\.(zip|sha512)$/.test(f)){fs.rmSync(path.join(d,f))}}}\"",
30
+ "prepublishOnly": "npm run build && npm run preflight && npm run pack:check"
26
31
  },
27
32
  "devDependencies": {
28
33
  "esbuild": "^0.28.0",
@@ -0,0 +1,156 @@
1
+ {
2
+ "name": "ijfw",
3
+ "displayName": "IJFW — AI Efficiency Layer",
4
+ "version": "{{VERSION}}",
5
+ "description": "One install, every AI coding agent, zero config. Unifies 15 CLIs under a shared MCP memory layer so context follows you across Claude, Codex, Gemini, Cursor, Windsurf, and 10 more.",
6
+ "author": "Sean Donahoe",
7
+ "icon": "assets/ijfw-logo.svg",
8
+ "engines": {
9
+ "wayland": ">=0.6.0"
10
+ },
11
+ "hubs": ["acpAdapters", "mcpServers"],
12
+ "lifecycle": {
13
+ "onInstall": "scripts/install.js",
14
+ "onUninstall": "scripts/uninstall.js"
15
+ },
16
+ "contributes": {
17
+ "acpAdapters": [
18
+ {
19
+ "id": "claude",
20
+ "name": "Claude Code",
21
+ "description": "Anthropic's Claude Code agentic coding CLI with IJFW memory + plugin layer",
22
+ "connectionType": "cli",
23
+ "cliCommand": "claude",
24
+ "defaultCliPath": "npx @anthropic-ai/claude-code",
25
+ "supportsStreaming": true
26
+ },
27
+ {
28
+ "id": "codex",
29
+ "name": "OpenAI Codex CLI",
30
+ "description": "OpenAI Codex CLI with IJFW MCP memory, hooks, skills, and command aliases",
31
+ "connectionType": "cli",
32
+ "cliCommand": "codex",
33
+ "defaultCliPath": "npx @openai/codex",
34
+ "supportsStreaming": true
35
+ },
36
+ {
37
+ "id": "gemini",
38
+ "name": "Gemini CLI",
39
+ "description": "Google Gemini CLI with IJFW MCP memory, extension bundle, and hooks",
40
+ "connectionType": "cli",
41
+ "cliCommand": "gemini",
42
+ "defaultCliPath": "npx @google/gemini-cli",
43
+ "supportsStreaming": true
44
+ },
45
+ {
46
+ "id": "cursor",
47
+ "name": "Cursor",
48
+ "description": "Cursor AI editor with IJFW MCP memory and project rules",
49
+ "connectionType": "cli",
50
+ "cliCommand": "cursor",
51
+ "defaultCliPath": "cursor",
52
+ "supportsStreaming": false
53
+ },
54
+ {
55
+ "id": "windsurf",
56
+ "name": "Windsurf",
57
+ "description": "Windsurf AI editor with IJFW MCP memory and project rules",
58
+ "connectionType": "cli",
59
+ "cliCommand": "windsurf",
60
+ "defaultCliPath": "windsurf",
61
+ "supportsStreaming": false
62
+ },
63
+ {
64
+ "id": "copilot",
65
+ "name": "GitHub Copilot",
66
+ "description": "GitHub Copilot (VS Code) with IJFW MCP memory and copilot-instructions",
67
+ "connectionType": "cli",
68
+ "cliCommand": "code",
69
+ "defaultCliPath": "code",
70
+ "supportsStreaming": false
71
+ },
72
+ {
73
+ "id": "hermes",
74
+ "name": "Hermes CLI",
75
+ "description": "Hermes CLI with IJFW MCP memory, skills, plugin, and tier-2 hook",
76
+ "connectionType": "cli",
77
+ "cliCommand": "hermes",
78
+ "defaultCliPath": "npx @hermes-ai/cli",
79
+ "supportsStreaming": true
80
+ },
81
+ {
82
+ "id": "wayland",
83
+ "name": "Wayland CLI",
84
+ "description": "Wayland CLI with IJFW MCP memory, skills, plugin, and tier-2 hook",
85
+ "connectionType": "cli",
86
+ "cliCommand": "wayland",
87
+ "defaultCliPath": "npx @wayland-ai/cli",
88
+ "supportsStreaming": true
89
+ },
90
+ {
91
+ "id": "aider",
92
+ "name": "Aider",
93
+ "description": "Aider AI pair programmer with IJFW rules (aider.conf.yml + CONVENTIONS.md)",
94
+ "connectionType": "cli",
95
+ "cliCommand": "aider",
96
+ "defaultCliPath": "npx aider-chat",
97
+ "supportsStreaming": false
98
+ },
99
+ {
100
+ "id": "opencode",
101
+ "name": "OpenCode",
102
+ "description": "OpenCode CLI with IJFW MCP memory (opencode mcp.local schema)",
103
+ "connectionType": "cli",
104
+ "cliCommand": "opencode",
105
+ "defaultCliPath": "npx opencode-ai",
106
+ "supportsStreaming": true
107
+ },
108
+ {
109
+ "id": "qwencode",
110
+ "name": "Qwen Code",
111
+ "description": "Alibaba Qwen Code CLI with IJFW MCP memory",
112
+ "connectionType": "cli",
113
+ "cliCommand": "qwen",
114
+ "defaultCliPath": "npx @qwen-ai/qwen-code",
115
+ "supportsStreaming": true
116
+ },
117
+ {
118
+ "id": "cline",
119
+ "name": "Cline",
120
+ "description": "Cline VS Code extension with IJFW MCP memory (globalStorage schema)",
121
+ "connectionType": "cli",
122
+ "cliCommand": "cline",
123
+ "defaultCliPath": "cline",
124
+ "supportsStreaming": false
125
+ },
126
+ {
127
+ "id": "kimicode",
128
+ "name": "Kimi Code",
129
+ "description": "Moonshot Kimi Code CLI with IJFW MCP memory",
130
+ "connectionType": "cli",
131
+ "cliCommand": "kimi",
132
+ "defaultCliPath": "npx @moonshot-ai/kimi-code",
133
+ "supportsStreaming": true
134
+ },
135
+ {
136
+ "id": "openclaw",
137
+ "name": "OpenClaw",
138
+ "description": "OpenClaw CLI with IJFW MCP memory (mcp.servers schema)",
139
+ "connectionType": "cli",
140
+ "cliCommand": "openclaw",
141
+ "defaultCliPath": "npx @openclaw-ai/cli",
142
+ "supportsStreaming": true
143
+ },
144
+ {
145
+ "id": "antigravity",
146
+ "name": "Antigravity",
147
+ "description": "Antigravity AI IDE (ex-Windsurf team) with IJFW MCP memory (IDE + CLI paths)",
148
+ "connectionType": "cli",
149
+ "cliCommand": "agy",
150
+ "defaultCliPath": "npx @antigravity-ai/cli",
151
+ "supportsStreaming": true
152
+ }
153
+ ],
154
+ "mcpServers": ["ijfw-memory"]
155
+ }
156
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <circle cx="16" cy="16" r="16" fill="#ff6b35"/>
3
+ <text x="16" y="21" font-family="sans-serif" font-size="10" font-weight="bold" fill="white" text-anchor="middle">IJFW</text>
4
+ </svg>
@@ -0,0 +1,41 @@
1
+ // IJFW Hub Extension — onInstall lifecycle hook.
2
+ // Runs in a sandboxed forked process via require() (NOT ESM).
3
+ // Wayland forks this script with a 120s timeout; we set our own inner
4
+ // timeouts split across two phases so we surface a clean error before
5
+ // Wayland's hard kill.
6
+ //
7
+ // Phase 1 (pre-fetch): 60s — network round-trip to npm registry.
8
+ // Phase 2 (execute): 40s — local install from cached tarball.
9
+
10
+ 'use strict';
11
+
12
+ const { spawnSync } = require('child_process');
13
+
14
+ // Phase 1: pre-fetch (network) — pull package into npm cache.
15
+ const fetchResult = spawnSync(
16
+ 'npm',
17
+ ['pack', '@ijfw/install@{{VERSION}}', '--silent', '--prefer-offline', '--pack-destination', '/tmp'],
18
+ { stdio: 'pipe', timeout: 60_000 },
19
+ );
20
+ if (fetchResult.status !== 0) {
21
+ console.error('[ijfw] pre-fetch failed');
22
+ process.exit(1);
23
+ }
24
+
25
+ // Phase 2: execute install (local) — npx honours cached tarball.
26
+ const result = spawnSync(
27
+ 'npx',
28
+ ['-y', '--prefer-offline', '@ijfw/install@{{VERSION}}'],
29
+ {
30
+ stdio: 'inherit',
31
+ timeout: 40_000,
32
+ env: { ...process.env, IJFW_NONINTERACTIVE: '1' },
33
+ },
34
+ );
35
+
36
+ if (result.status !== 0) {
37
+ console.error(`[ijfw] install failed (exit ${result.status})`);
38
+ process.exit(result.status || 1);
39
+ }
40
+
41
+ console.log('[ijfw] 15 AI coding CLIs unified');
@@ -0,0 +1,42 @@
1
+ // IJFW Hub Extension — onUninstall lifecycle hook.
2
+ // Runs in a sandboxed forked process via require() (NOT ESM).
3
+ // Wayland forks this script with a 120s timeout; we set our own inner
4
+ // timeouts split across two phases so we surface a clean error before
5
+ // Wayland's hard kill.
6
+ //
7
+ // Phase 1 (pre-fetch): 60s — network round-trip to npm registry.
8
+ // Phase 2 (execute): 40s — local uninstall from cached tarball.
9
+
10
+ 'use strict';
11
+
12
+ const { spawnSync } = require('child_process');
13
+
14
+ // Phase 1: pre-fetch (network) — pull package into npm cache.
15
+ const fetchResult = spawnSync(
16
+ 'npm',
17
+ ['pack', '@ijfw/install@{{VERSION}}', '--silent', '--prefer-offline', '--pack-destination', '/tmp'],
18
+ { stdio: 'pipe', timeout: 60_000 },
19
+ );
20
+ if (fetchResult.status !== 0) {
21
+ console.error('[ijfw] pre-fetch failed');
22
+ process.exit(1);
23
+ }
24
+
25
+ // Phase 2: execute uninstall (local) — positional subcommand, no '--' prefix.
26
+ // '--uninstall' routes to "Unknown subcommand" (exit 1); 'uninstall' is correct.
27
+ const result = spawnSync(
28
+ 'npx',
29
+ ['-y', '--prefer-offline', '@ijfw/install@{{VERSION}}', 'uninstall'],
30
+ {
31
+ stdio: 'inherit',
32
+ timeout: 40_000,
33
+ env: { ...process.env, IJFW_NONINTERACTIVE: '1' },
34
+ },
35
+ );
36
+
37
+ if (result.status !== 0) {
38
+ console.error(`[ijfw] uninstall failed (exit ${result.status})`);
39
+ process.exit(result.status || 1);
40
+ }
41
+
42
+ console.log('[ijfw] IJFW uninstalled');
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+ // installer/scripts/pack-hub-extension.js
3
+ //
4
+ // Packs the IJFW Wayland Hub Extension into a distributable zip artifact.
5
+ //
6
+ // Outputs (all under installer/dist/ by default, or --output <dir>):
7
+ // ijfw-<version>.zip — archive containing manifest + scripts dir + assets
8
+ // ijfw-<version>.sha512 — SRI integrity string: "sha512-<base64>"
9
+ // hub-index-snippet.json — IHubExtension-shaped JSON for Wayland's Hub Index
10
+ //
11
+ // Usage:
12
+ // node scripts/pack-hub-extension.js [--output <dir>]
13
+ // npm run pack:hub-extension
14
+ // node scripts/pack-hub-extension.js --help
15
+ //
16
+ // Requirements: Node.js >=18. Uses only Node builtins — no external deps.
17
+ // Deterministic: all zip entries use fixed epoch timestamp (1980-01-01 00:00:00)
18
+ // so SHA-512 is stable across runs on identical content.
19
+
20
+ import { createHash } from 'node:crypto';
21
+ import { deflateRawSync, crc32 } from 'node:zlib';
22
+ import {
23
+ existsSync,
24
+ mkdirSync,
25
+ readdirSync,
26
+ readFileSync,
27
+ realpathSync,
28
+ statSync,
29
+ writeFileSync,
30
+ } from 'node:fs';
31
+ import { tmpdir as osTmpdir } from 'node:os';
32
+ import { dirname, join, relative, resolve } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // --help / -h short-circuit (L3-06)
39
+ // Must happen before any side-effectful code runs.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
43
+ console.log(`
44
+ Usage: node scripts/pack-hub-extension.js [--output <dir>]
45
+
46
+ Options:
47
+ --output <dir> Write zip, sha512, and snippet into <dir> instead of the
48
+ default installer/dist/ (or process.cwd() when invoked from
49
+ inside node_modules).
50
+ --help, -h Print this help and exit.
51
+
52
+ Outputs written:
53
+ ijfw-<version>.zip Distributable extension archive
54
+ ijfw-<version>.sha512 SRI integrity string (sha512-<base64>)
55
+ hub-index-snippet.json IHubExtension entry for Wayland's Hub Index
56
+ `.trim());
57
+ process.exit(0);
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Paths
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const INSTALLER_DIR = resolve(__dirname, '..');
65
+ const PKG_PATH = join(INSTALLER_DIR, 'package.json');
66
+ const TMPL_PATH = join(__dirname, 'hub-extension', 'aion-extension.json.tmpl');
67
+ const HUB_EXT_DIR = join(__dirname, 'hub-extension');
68
+
69
+ // Output directory: defaults to installer/dist/. Override via `--output <dir>`
70
+ // so consumers like Wayland's prebuild sync script can stage artifacts directly
71
+ // into their own resources/hub/ dir without an intermediate copy step.
72
+ //
73
+ // The CLI flag is parsed at the script-arg level so all three invocations work:
74
+ // node scripts/pack-hub-extension.js (default dist/)
75
+ // node scripts/pack-hub-extension.js --output /tmp/stage (custom dir)
76
+ // npx -y @ijfw/install --pack-hub-extension --output /tmp (via ijfw CLI)
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // System-path blocklist for --output validation (L1-02, L1-06)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ const POSIX_SYSTEM_DIRS = [
83
+ '/etc', '/usr', '/bin', '/sbin', '/var',
84
+ '/System', '/Library', '/private',
85
+ ];
86
+
87
+ const WINDOWS_SYSTEM_DIRS = [
88
+ 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
89
+ 'C:\\System Volume Information',
90
+ ];
91
+
92
+ /**
93
+ * Returns true if the resolved path is the filesystem root or a known system
94
+ * directory (or a child of one).
95
+ *
96
+ * Whitelist exception: paths inside the OS temp dir (os.tmpdir()) are always
97
+ * allowed even when they fall under a blocked system prefix. This matters on
98
+ * macOS where os.tmpdir() returns `/var/folders/.../T/` — a legitimate
99
+ * user-writable temp space whose parent `/var` is in the blocklist. Without
100
+ * this exception, every Wayland prebuild that stages into a temp dir on
101
+ * macOS would be rejected, breaking the documented sync pipeline.
102
+ *
103
+ * @param {string} absPath Already-resolved absolute path.
104
+ * @returns {boolean}
105
+ */
106
+ function isSystemPath(absPath) {
107
+ // Reject bare filesystem roots: '/', 'C:\', 'D:\', etc.
108
+ if (/^[A-Za-z]:\\?$/.test(absPath) || absPath === '/') return true;
109
+
110
+ // OS temp-dir whitelist — overrides the system-prefix blocklist.
111
+ // Resolve both sides so macOS /var → /private/var symlinks compare cleanly.
112
+ try {
113
+ const tmp = osTmpdir();
114
+ const tmpReal = realpathSync(tmp);
115
+ const absReal = (() => { try { return realpathSync(absPath); } catch { return absPath; } })();
116
+ if (absReal === tmpReal || absReal.startsWith(tmpReal + '/') || absReal.startsWith(tmpReal + '\\')
117
+ || absPath === tmp || absPath.startsWith(tmp + '/') || absPath.startsWith(tmp + '\\')) {
118
+ return false;
119
+ }
120
+ } catch { /* fall through to blocklist */ }
121
+
122
+ const lc = absPath.toLowerCase();
123
+ const allBlocked = [...POSIX_SYSTEM_DIRS, ...WINDOWS_SYSTEM_DIRS];
124
+ for (const blocked of allBlocked) {
125
+ const bl = blocked.toLowerCase();
126
+ if (lc === bl || lc.startsWith(bl + '/') || lc.startsWith(bl + '\\')) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+
133
+ /**
134
+ * Parse --output from argv. Last occurrence wins (L2-02 last-wins convention).
135
+ * Validates non-empty, non-flag-shaped, non-system-path (L2-01, L1-02, L1-06).
136
+ * Returns the resolved absolute path, or null if --output was not present.
137
+ * @param {string[]} argv process.argv.slice(2)
138
+ * @returns {string|null}
139
+ */
140
+ function parseOutputArg(argv) {
141
+ let dir = null;
142
+
143
+ // Iterate all positions so last --output wins (L2-02).
144
+ for (let i = 0; i < argv.length; i++) {
145
+ if (argv[i] === '--output') {
146
+ if (i + 1 >= argv.length) {
147
+ console.error('[pack-hub-extension] ERROR: --output requires a directory argument');
148
+ process.exit(2);
149
+ }
150
+ dir = argv[i + 1];
151
+ i++; // skip value on next iteration
152
+ }
153
+ }
154
+
155
+ if (dir === null) return null; // flag not present — caller uses default
156
+
157
+ // L2-01: reject whitespace-only or flag-shaped values.
158
+ if (!dir || dir.trim() === '' || dir.startsWith('-')) {
159
+ console.error('[pack-hub-extension] ERROR: --output requires a non-empty directory argument');
160
+ process.exit(2);
161
+ }
162
+
163
+ const abs = resolve(process.cwd(), dir.trim());
164
+
165
+ // L1-02, L1-06: reject filesystem root and system directories.
166
+ if (isSystemPath(abs)) {
167
+ console.error(
168
+ `[pack-hub-extension] ERROR: --output "${abs}" is a system path. ` +
169
+ 'Choose a user-writable directory.'
170
+ );
171
+ process.exit(2);
172
+ }
173
+
174
+ return abs;
175
+ }
176
+
177
+ /**
178
+ * Return the default output directory.
179
+ * When invoked from inside node_modules (npm-global install), default to
180
+ * process.cwd() so we don't pollute the package installation directory (L3-07).
181
+ * @returns {string}
182
+ */
183
+ function defaultOutputDir() {
184
+ return __dirname.includes('node_modules')
185
+ ? process.cwd()
186
+ : join(INSTALLER_DIR, 'dist');
187
+ }
188
+
189
+ const _explicitOutput = parseOutputArg(process.argv.slice(2));
190
+ const DIST_DIR = _explicitOutput !== null ? _explicitOutput : defaultOutputDir();
191
+
192
+ // L3-07: always announce where output will land so the caller knows.
193
+ console.log(
194
+ `[pack-hub-extension] NOTE: writing artifacts to ${resolve(DIST_DIR)}` +
195
+ (_explicitOutput === null ? '. Use --output <dir> to redirect.' : '.')
196
+ );
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Safe write helper — wraps writeFileSync with a clean error UX (L2-09)
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Write `content` to `filePath`, exiting with a clean error message on failure.
204
+ * @param {string} filePath
205
+ * @param {Buffer|string} content
206
+ * @param {string} [encoding]
207
+ */
208
+ function safeWrite(filePath, content, encoding) {
209
+ try {
210
+ if (encoding !== undefined) {
211
+ writeFileSync(filePath, content, encoding);
212
+ } else {
213
+ writeFileSync(filePath, content);
214
+ }
215
+ } catch (err) {
216
+ console.error(`[pack-hub-extension] ERROR: cannot write to ${filePath}: ${err.message}`);
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // L2-06: verify hub-extension/ source tree exists before any read/zip work.
223
+ // Without this guard, a malformed install (e.g., scripts/hub-extension/ pruned
224
+ // from a custom build) produces a raw Node ENOENT stack trace mid-banner with
225
+ // no [pack-hub-extension] prefix — confusing for Wayland's sync script
226
+ // consumers. Fail-fast with a clean, actionable error instead.
227
+ // ---------------------------------------------------------------------------
228
+
229
+ if (!existsSync(HUB_EXT_DIR)) {
230
+ console.error(
231
+ `[pack-hub-extension] ERROR: hub-extension source dir missing: ${HUB_EXT_DIR}\n` +
232
+ '[pack-hub-extension] Ensure scripts/hub-extension/ ships alongside this script.\n' +
233
+ '[pack-hub-extension] If invoked via npx, your @ijfw/install tarball may be incomplete.'
234
+ );
235
+ process.exit(1);
236
+ }
237
+ if (!existsSync(TMPL_PATH)) {
238
+ console.error(
239
+ `[pack-hub-extension] ERROR: hub-extension manifest template missing: ${TMPL_PATH}`
240
+ );
241
+ process.exit(1);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Read version from package.json
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'));
249
+ const VERSION = pkg.version;
250
+ if (!VERSION) {
251
+ console.error('[pack-hub-extension] ERROR: could not read version from package.json');
252
+ process.exit(1);
253
+ }
254
+
255
+ console.log(`[pack-hub-extension] packaging IJFW Hub Extension v${VERSION}`);
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Render manifest template
259
+ // ---------------------------------------------------------------------------
260
+
261
+ const tmpl = readFileSync(TMPL_PATH, 'utf8');
262
+ const manifest = tmpl.replaceAll('{{VERSION}}', VERSION);
263
+
264
+ let manifestObj;
265
+ try {
266
+ manifestObj = JSON.parse(manifest);
267
+ } catch (err) {
268
+ console.error('[pack-hub-extension] ERROR: rendered manifest is not valid JSON:', err.message);
269
+ process.exit(1);
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Collect files to pack (deterministic: sorted by zip entry path)
274
+ // ---------------------------------------------------------------------------
275
+ // Layout inside zip:
276
+ // aion-extension.json
277
+ // scripts/install.js
278
+ // scripts/uninstall.js
279
+ // assets/ijfw-logo.svg (if present)
280
+
281
+ /** @type {Array<{zipPath: string, content: Buffer}>} */
282
+ const entries = [];
283
+
284
+ // Manifest (rendered in memory — no temp file needed)
285
+ entries.push({
286
+ zipPath: 'aion-extension.json',
287
+ content: Buffer.from(manifest + '\n', 'utf8'),
288
+ });
289
+
290
+ // scripts/ — render .tmpl files with {{VERSION}} substitution at pack time so
291
+ // the bundled hooks reference the exact pinned version rather than @latest.
292
+ // Backward-compat: if no .tmpl exists but the plain .js does, zip it as-is.
293
+ for (const name of ['install.js', 'uninstall.js']) {
294
+ const tmplSrc = join(HUB_EXT_DIR, `${name}.tmpl`);
295
+ const jsSrc = join(HUB_EXT_DIR, name);
296
+ let content;
297
+ if (existsSync(tmplSrc)) {
298
+ const raw = readFileSync(tmplSrc, 'utf8');
299
+ content = Buffer.from(raw.replaceAll('{{VERSION}}', VERSION), 'utf8');
300
+ } else if (existsSync(jsSrc)) {
301
+ content = readFileSync(jsSrc);
302
+ } else {
303
+ console.error(`[pack-hub-extension] ERROR: neither ${name}.tmpl nor ${name} found in hub-extension/scripts`);
304
+ process.exit(1);
305
+ }
306
+ entries.push({ zipPath: `scripts/${name}`, content });
307
+ }
308
+
309
+ // assets/ (walk directory, sorted)
310
+ const assetsDir = join(HUB_EXT_DIR, 'assets');
311
+ if (existsSync(assetsDir)) {
312
+ const assetFiles = readdirSync(assetsDir).sort();
313
+ for (const name of assetFiles) {
314
+ const full = join(assetsDir, name);
315
+ if (statSync(full).isFile()) {
316
+ entries.push({
317
+ zipPath: `assets/${name}`,
318
+ content: readFileSync(full),
319
+ });
320
+ } else if (statSync(full).isDirectory()) {
321
+ // L2-08: future-proof warning; current placeholder SVG is single-file.
322
+ console.warn(`[pack-hub-extension] WARNING: assets subdir ignored: ${name}/`);
323
+ }
324
+ }
325
+ }
326
+
327
+ // Sort entries for fully deterministic output regardless of fs ordering.
328
+ entries.sort((a, b) => a.zipPath < b.zipPath ? -1 : a.zipPath > b.zipPath ? 1 : 0);
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Build deterministic ZIP (PKZIP format, all entries stored with deflate)
332
+ //
333
+ // Fixed MS-DOS timestamp: 1980-01-01 00:00:00
334
+ // date word: year-1980=0, month=1, day=1 → 0x0021
335
+ // time word: hour=0, min=0, sec/2=0 → 0x0000
336
+ // ---------------------------------------------------------------------------
337
+
338
+ const DOS_DATE = 0x0021; // 1980-01-01
339
+ const DOS_TIME = 0x0000; // 00:00:00
340
+
341
+ /**
342
+ * Write a little-endian uint16 into buf at offset.
343
+ * @param {Buffer} buf
344
+ * @param {number} offset
345
+ * @param {number} value
346
+ */
347
+ function writeU16LE(buf, offset, value) {
348
+ buf[offset] = value & 0xff;
349
+ buf[offset + 1] = (value >> 8) & 0xff;
350
+ }
351
+
352
+ /**
353
+ * Write a little-endian uint32 into buf at offset.
354
+ * @param {Buffer} buf
355
+ * @param {number} offset
356
+ * @param {number} value
357
+ */
358
+ function writeU32LE(buf, offset, value) {
359
+ buf.writeUInt32LE(value >>> 0, offset);
360
+ }
361
+
362
+ const ZIP_PARTS = []; // array of Buffers forming the zip stream
363
+
364
+ /** @type {Array<{zipPath: string, crc: number, compSize: number, uncompSize: number, offset: number}>} */
365
+ const centralDirEntries = [];
366
+
367
+ let offset = 0;
368
+
369
+ for (const { zipPath, content } of entries) {
370
+ const nameBytes = Buffer.from(zipPath, 'utf8');
371
+ const uncompSize = content.length;
372
+ const crc = crc32(content) >>> 0;
373
+
374
+ // Deflate the content.
375
+ const compressed = deflateRawSync(content, { level: 9 });
376
+ // Use stored (method=0) if deflate is bigger or equal — keeps zip valid.
377
+ const useDeflate = compressed.length < uncompSize;
378
+ const compMethod = useDeflate ? 8 : 0;
379
+ const compData = useDeflate ? compressed : content;
380
+ const compSize = compData.length;
381
+
382
+ // Local file header: signature + 26 bytes fixed + filename.
383
+ const localHeader = Buffer.alloc(30 + nameBytes.length);
384
+ writeU32LE(localHeader, 0, 0x04034b50); // local file header sig
385
+ writeU16LE(localHeader, 4, 20); // version needed: 2.0
386
+ writeU16LE(localHeader, 6, 0); // general purpose bit flag
387
+ writeU16LE(localHeader, 8, compMethod); // compression method
388
+ writeU16LE(localHeader, 10, DOS_TIME); // last mod time
389
+ writeU16LE(localHeader, 12, DOS_DATE); // last mod date
390
+ writeU32LE(localHeader, 14, crc); // crc-32
391
+ writeU32LE(localHeader, 18, compSize); // compressed size
392
+ writeU32LE(localHeader, 22, uncompSize); // uncompressed size
393
+ writeU16LE(localHeader, 26, nameBytes.length); // file name length
394
+ writeU16LE(localHeader, 28, 0); // extra field length
395
+ nameBytes.copy(localHeader, 30);
396
+
397
+ centralDirEntries.push({
398
+ zipPath,
399
+ nameBytes,
400
+ crc,
401
+ compSize,
402
+ uncompSize,
403
+ compMethod,
404
+ offset,
405
+ });
406
+
407
+ ZIP_PARTS.push(localHeader, compData);
408
+ offset += localHeader.length + compData.length;
409
+ }
410
+
411
+ // Central directory.
412
+ const centralDirStart = offset;
413
+ for (const e of centralDirEntries) {
414
+ const cdEntry = Buffer.alloc(46 + e.nameBytes.length);
415
+ writeU32LE(cdEntry, 0, 0x02014b50); // central dir sig
416
+ writeU16LE(cdEntry, 4, 20); // version made by: 2.0
417
+ writeU16LE(cdEntry, 6, 20); // version needed: 2.0
418
+ writeU16LE(cdEntry, 8, 0); // general purpose bit flag
419
+ writeU16LE(cdEntry, 10, e.compMethod); // compression method
420
+ writeU16LE(cdEntry, 12, DOS_TIME); // last mod time
421
+ writeU16LE(cdEntry, 14, DOS_DATE); // last mod date
422
+ writeU32LE(cdEntry, 16, e.crc); // crc-32
423
+ writeU32LE(cdEntry, 20, e.compSize); // compressed size
424
+ writeU32LE(cdEntry, 24, e.uncompSize); // uncompressed size
425
+ writeU16LE(cdEntry, 28, e.nameBytes.length); // file name length
426
+ writeU16LE(cdEntry, 30, 0); // extra field length
427
+ writeU16LE(cdEntry, 32, 0); // file comment length
428
+ writeU16LE(cdEntry, 34, 0); // disk number start
429
+ writeU16LE(cdEntry, 36, 0); // internal file attributes
430
+ writeU32LE(cdEntry, 38, 0); // external file attributes
431
+ writeU32LE(cdEntry, 42, e.offset); // relative offset of local header
432
+ e.nameBytes.copy(cdEntry, 46);
433
+ ZIP_PARTS.push(cdEntry);
434
+ offset += cdEntry.length;
435
+ }
436
+
437
+ const centralDirSize = offset - centralDirStart;
438
+
439
+ // End of central directory record.
440
+ const eocd = Buffer.alloc(22);
441
+ writeU32LE(eocd, 0, 0x06054b50); // EOCD signature
442
+ writeU16LE(eocd, 4, 0); // disk number
443
+ writeU16LE(eocd, 6, 0); // disk with central dir
444
+ writeU16LE(eocd, 8, centralDirEntries.length); // entries on this disk
445
+ writeU16LE(eocd, 10, centralDirEntries.length); // total entries
446
+ writeU32LE(eocd, 12, centralDirSize); // size of central directory
447
+ writeU32LE(eocd, 16, centralDirStart); // offset of central directory
448
+ writeU16LE(eocd, 20, 0); // comment length
449
+ ZIP_PARTS.push(eocd);
450
+
451
+ const zipBuffer = Buffer.concat(ZIP_PARTS);
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // Write zip
455
+ // ---------------------------------------------------------------------------
456
+
457
+ mkdirSync(DIST_DIR, { recursive: true });
458
+
459
+ const ZIP_NAME = `ijfw-${VERSION}.zip`;
460
+ const ZIP_PATH = join(DIST_DIR, ZIP_NAME);
461
+ safeWrite(ZIP_PATH, zipBuffer);
462
+
463
+ const zipSize = zipBuffer.length;
464
+ console.log(`[pack-hub-extension] zip: ${ZIP_PATH} (${zipSize} bytes)`);
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // Compute SHA-512 SRI (sync — buffer already in memory)
468
+ // ---------------------------------------------------------------------------
469
+
470
+ const hashHex = createHash('sha512').update(zipBuffer).digest('hex');
471
+ const sri = `sha512-${Buffer.from(hashHex, 'hex').toString('base64')}`;
472
+
473
+ const SHA_PATH = join(DIST_DIR, `ijfw-${VERSION}.sha512`);
474
+ safeWrite(SHA_PATH, sri, 'utf8');
475
+ console.log(`[pack-hub-extension] sha512: ${SHA_PATH}`);
476
+ console.log(`[pack-hub-extension] SRI: ${sri}`);
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Write hub-index-snippet.json (IHubExtension shape for Wayland's Hub Index)
480
+ // ---------------------------------------------------------------------------
481
+
482
+ // HubContributes in the index uses string ID arrays (not full adapter objects).
483
+ const adapterIds = manifestObj.contributes.acpAdapters.map((a) => a.id);
484
+
485
+ const hubIndexSnippet = {
486
+ name: manifestObj.name,
487
+ displayName: manifestObj.displayName,
488
+ version: VERSION,
489
+ description: manifestObj.description,
490
+ author: manifestObj.author,
491
+ icon: manifestObj.icon,
492
+ dist: {
493
+ tarball: `extensions/${ZIP_NAME}`,
494
+ integrity: sri,
495
+ unpackedSize: zipSize,
496
+ },
497
+ engines: manifestObj.engines,
498
+ hubs: manifestObj.hubs,
499
+ contributes: {
500
+ acpAdapters: adapterIds,
501
+ mcpServers: manifestObj.contributes.mcpServers,
502
+ },
503
+ tags: ['ai', 'coding', 'mcp', 'memory', 'multi-agent'],
504
+ };
505
+
506
+ const SNIPPET_PATH = join(DIST_DIR, 'hub-index-snippet.json');
507
+ safeWrite(SNIPPET_PATH, JSON.stringify(hubIndexSnippet, null, 2) + '\n', 'utf8');
508
+ console.log(`[pack-hub-extension] hub index snippet: ${SNIPPET_PATH}`);
509
+
510
+ // ---------------------------------------------------------------------------
511
+ // Done
512
+ // ---------------------------------------------------------------------------
513
+
514
+ console.log(`[pack-hub-extension] DONE — ${ZIP_NAME} ready for Wayland bundling`);