@ijfw/install 1.5.1 → 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 +21 -0
- package/dist/hub-index-snippet.json +49 -0
- package/dist/ijfw.js +19 -3
- package/dist/install.js +5 -1
- package/package.json +7 -2
- package/scripts/hub-extension/aion-extension.json.tmpl +156 -0
- package/scripts/hub-extension/assets/ijfw-logo.svg +4 -0
- package/scripts/hub-extension/install.js.tmpl +41 -0
- package/scripts/hub-extension/uninstall.js.tmpl +42 -0
- package/scripts/pack-hub-extension.js +514 -0
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",
|
|
@@ -4174,11 +4184,11 @@ function printCommands() {
|
|
|
4174
4184
|
const t = commandsByTier();
|
|
4175
4185
|
let out = "\nijfw -- full command surface\n";
|
|
4176
4186
|
out += "\nPRIMARY (shown in --help)\n";
|
|
4177
|
-
out += " " + t.primary.map((e) => e.name).join("
|
|
4187
|
+
out += " " + t.primary.map((e) => e.name).join(" | ") + "\n";
|
|
4178
4188
|
out += "\nCOORDINATION (agents drive these; humans inspect)\n";
|
|
4179
|
-
out += " " + t.coordination.map((e) => e.name).join("
|
|
4189
|
+
out += " " + t.coordination.map((e) => e.name).join(" | ") + "\n";
|
|
4180
4190
|
out += "\nPLUMBING (most users never need these)\n";
|
|
4181
|
-
out += " " + t.plumbing.map((e) => e.name).join("
|
|
4191
|
+
out += " " + t.plumbing.map((e) => e.name).join(" | ") + "\n";
|
|
4182
4192
|
console.log(out);
|
|
4183
4193
|
}
|
|
4184
4194
|
function doctorCheck(cmd, args) {
|
|
@@ -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/dist/install.js
CHANGED
|
@@ -2222,10 +2222,14 @@ async function main() {
|
|
|
2222
2222
|
console.log(` repo ${action}`);
|
|
2223
2223
|
await runInstallScript(target);
|
|
2224
2224
|
console.log(" platform configs applied");
|
|
2225
|
-
|
|
2225
|
+
const canonicalDir = join5(homedir3(), ".ijfw");
|
|
2226
|
+
const isCustomDir = process.env.IJFW_CUSTOM_DIR === "1" || resolve4(target) !== canonicalDir;
|
|
2227
|
+
if (!opts.noMarketplace && !isCustomDir) {
|
|
2226
2228
|
const settingsPath = claudeSettingsPath();
|
|
2227
2229
|
mergeMarketplace(settingsPath, { rootDir: target });
|
|
2228
2230
|
console.log(` marketplace registered in ${settingsPath}`);
|
|
2231
|
+
} else if (isCustomDir) {
|
|
2232
|
+
console.log(" marketplace merge skipped (custom-dir install)");
|
|
2229
2233
|
}
|
|
2230
2234
|
try {
|
|
2231
2235
|
const coldScanRoot = process.env.IJFW_PROJECT_DIR || process.cwd();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/install",
|
|
3
|
-
"version": "1.5.
|
|
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
|
-
"
|
|
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,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`);
|