@ijfw/install 1.5.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +33 -65
- package/dist/hub-index-snippet.json +49 -0
- package/dist/ijfw.js +103 -23
- package/dist/install.js +201 -33
- package/docs/GUIDE.md +2 -2
- package/docs/guide/assets/ferrox-hero.png +0 -0
- package/package.json +16 -5
- 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 +46 -0
- package/scripts/hub-extension/uninstall.js.tmpl +42 -0
- package/scripts/pack-hub-extension.js +561 -0
- package/src/install.ps1 +30 -3
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// V155-066: use os.tmpdir() instead of hardcoded /tmp so the hook works on
|
|
16
|
+
// Windows (where /tmp does not exist) and on hosts whose /tmp is read-only.
|
|
17
|
+
const packDest = os.tmpdir();
|
|
18
|
+
|
|
19
|
+
// Phase 1: pre-fetch (network) — pull package into npm cache.
|
|
20
|
+
const fetchResult = spawnSync(
|
|
21
|
+
'npm',
|
|
22
|
+
['pack', '@ijfw/install@{{VERSION}}', '--silent', '--prefer-offline', '--pack-destination', packDest],
|
|
23
|
+
{ stdio: 'pipe', timeout: 60_000 },
|
|
24
|
+
);
|
|
25
|
+
if (fetchResult.status !== 0) {
|
|
26
|
+
console.error('[ijfw] pre-fetch failed');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Phase 2: execute install (local) — npx honours cached tarball.
|
|
31
|
+
const result = spawnSync(
|
|
32
|
+
'npx',
|
|
33
|
+
['-y', '--prefer-offline', '@ijfw/install@{{VERSION}}'],
|
|
34
|
+
{
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
timeout: 40_000,
|
|
37
|
+
env: { ...process.env, IJFW_NONINTERACTIVE: '1' },
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
console.error(`[ijfw] install failed (exit ${result.status})`);
|
|
43
|
+
process.exit(result.status || 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
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,561 @@
|
|
|
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, 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
|
+
/**
|
|
107
|
+
* canonicalizeAtomic — resolve a path through realpath even when the leaf
|
|
108
|
+
* does not yet exist. Walks up to the deepest existing ancestor, realpaths
|
|
109
|
+
* that, then reattaches the remaining suffix. Handles the macOS
|
|
110
|
+
* /var ↔ /private/var symlink AND the "not-yet-created subdir" case in one
|
|
111
|
+
* pass. Mirrors the same helper in path-guard.js. (V155-036 / L1-02 recur)
|
|
112
|
+
*/
|
|
113
|
+
function canonicalizeAtomic(p) {
|
|
114
|
+
try { return realpathSync(p); } catch { /* fall through */ }
|
|
115
|
+
const parts = p.split(/[/\\]/);
|
|
116
|
+
const sep = p.includes('\\') && !p.includes('/') ? '\\' : '/';
|
|
117
|
+
let suffix = [];
|
|
118
|
+
while (parts.length > 0) {
|
|
119
|
+
suffix.unshift(parts.pop());
|
|
120
|
+
const head = parts.join(sep) || sep;
|
|
121
|
+
try {
|
|
122
|
+
const real = realpathSync(head);
|
|
123
|
+
return suffix.length > 0 ? `${real}${sep}${suffix.join(sep)}` : real;
|
|
124
|
+
} catch { /* keep walking up */ }
|
|
125
|
+
}
|
|
126
|
+
return p; // give up; return the input
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isSystemPath(absPath) {
|
|
130
|
+
// Reject bare filesystem roots: '/', 'C:\', 'D:\', etc.
|
|
131
|
+
if (/^[A-Za-z]:\\?$/.test(absPath) || absPath === '/') return true;
|
|
132
|
+
|
|
133
|
+
// V155-036: pre-canonicalize BOTH sides via the deepest-existing-ancestor
|
|
134
|
+
// walk so the macOS /var → /private/var symlink doesn't slip through when
|
|
135
|
+
// the requested output dir doesn't exist yet (the prior realpathSync on
|
|
136
|
+
// absPath silently kept the unresolved form, which then matched the
|
|
137
|
+
// /private prefix in the blocklist).
|
|
138
|
+
const tmp = osTmpdir();
|
|
139
|
+
const tmpReal = canonicalizeAtomic(tmp);
|
|
140
|
+
const absReal = canonicalizeAtomic(absPath);
|
|
141
|
+
|
|
142
|
+
// OS temp-dir whitelist — overrides the system-prefix blocklist.
|
|
143
|
+
if (
|
|
144
|
+
absReal === tmpReal ||
|
|
145
|
+
absReal.startsWith(tmpReal + '/') ||
|
|
146
|
+
absReal.startsWith(tmpReal + '\\') ||
|
|
147
|
+
absPath === tmp ||
|
|
148
|
+
absPath.startsWith(tmp + '/') ||
|
|
149
|
+
absPath.startsWith(tmp + '\\')
|
|
150
|
+
) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// V155-059: gate the system-prefix lists by platform so cross-platform
|
|
155
|
+
// false positives go away. macOS users packing under /Library/MyProjects
|
|
156
|
+
// shouldn't trip the Windows blocklist, and Windows users shouldn't trip
|
|
157
|
+
// the POSIX blocklist. Prefer runtime-derived Windows prefixes when set.
|
|
158
|
+
const isWin = process.platform === 'win32';
|
|
159
|
+
const dynamicWinDirs = isWin
|
|
160
|
+
? [
|
|
161
|
+
process.env.windir,
|
|
162
|
+
process.env.ProgramFiles,
|
|
163
|
+
process.env['ProgramFiles(x86)'],
|
|
164
|
+
].filter((s) => typeof s === 'string' && s.length > 0)
|
|
165
|
+
: [];
|
|
166
|
+
const blockedForPlatform = isWin
|
|
167
|
+
? [...WINDOWS_SYSTEM_DIRS, ...dynamicWinDirs]
|
|
168
|
+
: [...POSIX_SYSTEM_DIRS];
|
|
169
|
+
|
|
170
|
+
const lc = absReal.toLowerCase();
|
|
171
|
+
for (const blocked of blockedForPlatform) {
|
|
172
|
+
const bl = blocked.toLowerCase();
|
|
173
|
+
if (lc === bl || lc.startsWith(bl + '/') || lc.startsWith(bl + '\\')) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse --output from argv. Last occurrence wins (L2-02 last-wins convention).
|
|
182
|
+
* Validates non-empty, non-flag-shaped, non-system-path (L2-01, L1-02, L1-06).
|
|
183
|
+
* Returns the resolved absolute path, or null if --output was not present.
|
|
184
|
+
* @param {string[]} argv process.argv.slice(2)
|
|
185
|
+
* @returns {string|null}
|
|
186
|
+
*/
|
|
187
|
+
function parseOutputArg(argv) {
|
|
188
|
+
let dir = null;
|
|
189
|
+
|
|
190
|
+
// Iterate all positions so last --output wins (L2-02).
|
|
191
|
+
for (let i = 0; i < argv.length; i++) {
|
|
192
|
+
if (argv[i] === '--output') {
|
|
193
|
+
if (i + 1 >= argv.length) {
|
|
194
|
+
console.error('[pack-hub-extension] ERROR: --output requires a directory argument');
|
|
195
|
+
process.exit(2);
|
|
196
|
+
}
|
|
197
|
+
dir = argv[i + 1];
|
|
198
|
+
i++; // skip value on next iteration
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (dir === null) return null; // flag not present — caller uses default
|
|
203
|
+
|
|
204
|
+
// L2-01: reject whitespace-only or flag-shaped values.
|
|
205
|
+
if (!dir || dir.trim() === '' || dir.startsWith('-')) {
|
|
206
|
+
console.error('[pack-hub-extension] ERROR: --output requires a non-empty directory argument');
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const abs = resolve(process.cwd(), dir.trim());
|
|
211
|
+
|
|
212
|
+
// L1-02, L1-06: reject filesystem root and system directories.
|
|
213
|
+
if (isSystemPath(abs)) {
|
|
214
|
+
console.error(
|
|
215
|
+
`[pack-hub-extension] ERROR: --output "${abs}" is a system path. ` +
|
|
216
|
+
'Choose a user-writable directory.'
|
|
217
|
+
);
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return abs;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Return the default output directory.
|
|
226
|
+
* When invoked from inside node_modules (npm-global install), default to
|
|
227
|
+
* process.cwd() so we don't pollute the package installation directory (L3-07).
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
function defaultOutputDir() {
|
|
231
|
+
return __dirname.includes('node_modules')
|
|
232
|
+
? process.cwd()
|
|
233
|
+
: join(INSTALLER_DIR, 'dist');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const _explicitOutput = parseOutputArg(process.argv.slice(2));
|
|
237
|
+
const DIST_DIR = _explicitOutput !== null ? _explicitOutput : defaultOutputDir();
|
|
238
|
+
|
|
239
|
+
// L3-07: always announce where output will land so the caller knows.
|
|
240
|
+
console.log(
|
|
241
|
+
`[pack-hub-extension] NOTE: writing artifacts to ${resolve(DIST_DIR)}` +
|
|
242
|
+
(_explicitOutput === null ? '. Use --output <dir> to redirect.' : '.')
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Safe write helper — wraps writeFileSync with a clean error UX (L2-09)
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Write `content` to `filePath`, exiting with a clean error message on failure.
|
|
251
|
+
* @param {string} filePath
|
|
252
|
+
* @param {Buffer|string} content
|
|
253
|
+
* @param {string} [encoding]
|
|
254
|
+
*/
|
|
255
|
+
function safeWrite(filePath, content, encoding) {
|
|
256
|
+
try {
|
|
257
|
+
if (encoding !== undefined) {
|
|
258
|
+
writeFileSync(filePath, content, encoding);
|
|
259
|
+
} else {
|
|
260
|
+
writeFileSync(filePath, content);
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(`[pack-hub-extension] ERROR: cannot write to ${filePath}: ${err.message}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// L2-06: verify hub-extension/ source tree exists before any read/zip work.
|
|
270
|
+
// Without this guard, a malformed install (e.g., scripts/hub-extension/ pruned
|
|
271
|
+
// from a custom build) produces a raw Node ENOENT stack trace mid-banner with
|
|
272
|
+
// no [pack-hub-extension] prefix — confusing for Wayland's sync script
|
|
273
|
+
// consumers. Fail-fast with a clean, actionable error instead.
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
if (!existsSync(HUB_EXT_DIR)) {
|
|
277
|
+
console.error(
|
|
278
|
+
`[pack-hub-extension] ERROR: hub-extension source dir missing: ${HUB_EXT_DIR}\n` +
|
|
279
|
+
'[pack-hub-extension] Ensure scripts/hub-extension/ ships alongside this script.\n' +
|
|
280
|
+
'[pack-hub-extension] If invoked via npx, your @ijfw/install tarball may be incomplete.'
|
|
281
|
+
);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
if (!existsSync(TMPL_PATH)) {
|
|
285
|
+
console.error(
|
|
286
|
+
`[pack-hub-extension] ERROR: hub-extension manifest template missing: ${TMPL_PATH}`
|
|
287
|
+
);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Read version from package.json
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'));
|
|
296
|
+
const VERSION = pkg.version;
|
|
297
|
+
if (!VERSION) {
|
|
298
|
+
console.error('[pack-hub-extension] ERROR: could not read version from package.json');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(`[pack-hub-extension] packaging IJFW Hub Extension v${VERSION}`);
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Render manifest template
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
const tmpl = readFileSync(TMPL_PATH, 'utf8');
|
|
309
|
+
const manifest = tmpl.replaceAll('{{VERSION}}', VERSION);
|
|
310
|
+
|
|
311
|
+
let manifestObj;
|
|
312
|
+
try {
|
|
313
|
+
manifestObj = JSON.parse(manifest);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error('[pack-hub-extension] ERROR: rendered manifest is not valid JSON:', err.message);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Collect files to pack (deterministic: sorted by zip entry path)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Layout inside zip:
|
|
323
|
+
// aion-extension.json
|
|
324
|
+
// scripts/install.js
|
|
325
|
+
// scripts/uninstall.js
|
|
326
|
+
// assets/ijfw-logo.svg (if present)
|
|
327
|
+
|
|
328
|
+
/** @type {Array<{zipPath: string, content: Buffer}>} */
|
|
329
|
+
const entries = [];
|
|
330
|
+
|
|
331
|
+
// Manifest (rendered in memory — no temp file needed)
|
|
332
|
+
entries.push({
|
|
333
|
+
zipPath: 'aion-extension.json',
|
|
334
|
+
content: Buffer.from(manifest + '\n', 'utf8'),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// scripts/ — render .tmpl files with {{VERSION}} substitution at pack time so
|
|
338
|
+
// the bundled hooks reference the exact pinned version rather than @latest.
|
|
339
|
+
// Backward-compat: if no .tmpl exists but the plain .js does, zip it as-is.
|
|
340
|
+
for (const name of ['install.js', 'uninstall.js']) {
|
|
341
|
+
const tmplSrc = join(HUB_EXT_DIR, `${name}.tmpl`);
|
|
342
|
+
const jsSrc = join(HUB_EXT_DIR, name);
|
|
343
|
+
let content;
|
|
344
|
+
if (existsSync(tmplSrc)) {
|
|
345
|
+
const raw = readFileSync(tmplSrc, 'utf8');
|
|
346
|
+
content = Buffer.from(raw.replaceAll('{{VERSION}}', VERSION), 'utf8');
|
|
347
|
+
} else if (existsSync(jsSrc)) {
|
|
348
|
+
content = readFileSync(jsSrc);
|
|
349
|
+
} else {
|
|
350
|
+
console.error(`[pack-hub-extension] ERROR: neither ${name}.tmpl nor ${name} found in hub-extension/scripts`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
entries.push({ zipPath: `scripts/${name}`, content });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// assets/ (walk directory, sorted)
|
|
357
|
+
const assetsDir = join(HUB_EXT_DIR, 'assets');
|
|
358
|
+
if (existsSync(assetsDir)) {
|
|
359
|
+
const assetFiles = readdirSync(assetsDir).sort();
|
|
360
|
+
for (const name of assetFiles) {
|
|
361
|
+
const full = join(assetsDir, name);
|
|
362
|
+
if (statSync(full).isFile()) {
|
|
363
|
+
entries.push({
|
|
364
|
+
zipPath: `assets/${name}`,
|
|
365
|
+
content: readFileSync(full),
|
|
366
|
+
});
|
|
367
|
+
} else if (statSync(full).isDirectory()) {
|
|
368
|
+
// L2-08: future-proof warning; current placeholder SVG is single-file.
|
|
369
|
+
console.warn(`[pack-hub-extension] WARNING: assets subdir ignored: ${name}/`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Sort entries for fully deterministic output regardless of fs ordering.
|
|
375
|
+
entries.sort((a, b) => a.zipPath < b.zipPath ? -1 : a.zipPath > b.zipPath ? 1 : 0);
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Build deterministic ZIP (PKZIP format, all entries stored with deflate)
|
|
379
|
+
//
|
|
380
|
+
// Fixed MS-DOS timestamp: 1980-01-01 00:00:00
|
|
381
|
+
// date word: year-1980=0, month=1, day=1 → 0x0021
|
|
382
|
+
// time word: hour=0, min=0, sec/2=0 → 0x0000
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
const DOS_DATE = 0x0021; // 1980-01-01
|
|
386
|
+
const DOS_TIME = 0x0000; // 00:00:00
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Write a little-endian uint16 into buf at offset.
|
|
390
|
+
* @param {Buffer} buf
|
|
391
|
+
* @param {number} offset
|
|
392
|
+
* @param {number} value
|
|
393
|
+
*/
|
|
394
|
+
function writeU16LE(buf, offset, value) {
|
|
395
|
+
buf[offset] = value & 0xff;
|
|
396
|
+
buf[offset + 1] = (value >> 8) & 0xff;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Write a little-endian uint32 into buf at offset.
|
|
401
|
+
* @param {Buffer} buf
|
|
402
|
+
* @param {number} offset
|
|
403
|
+
* @param {number} value
|
|
404
|
+
*/
|
|
405
|
+
function writeU32LE(buf, offset, value) {
|
|
406
|
+
buf.writeUInt32LE(value >>> 0, offset);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const ZIP_PARTS = []; // array of Buffers forming the zip stream
|
|
410
|
+
|
|
411
|
+
/** @type {Array<{zipPath: string, crc: number, compSize: number, uncompSize: number, offset: number}>} */
|
|
412
|
+
const centralDirEntries = [];
|
|
413
|
+
|
|
414
|
+
let offset = 0;
|
|
415
|
+
|
|
416
|
+
for (const { zipPath, content } of entries) {
|
|
417
|
+
const nameBytes = Buffer.from(zipPath, 'utf8');
|
|
418
|
+
const uncompSize = content.length;
|
|
419
|
+
const crc = crc32(content) >>> 0;
|
|
420
|
+
|
|
421
|
+
// Deflate the content.
|
|
422
|
+
const compressed = deflateRawSync(content, { level: 9 });
|
|
423
|
+
// Use stored (method=0) if deflate is bigger or equal — keeps zip valid.
|
|
424
|
+
const useDeflate = compressed.length < uncompSize;
|
|
425
|
+
const compMethod = useDeflate ? 8 : 0;
|
|
426
|
+
const compData = useDeflate ? compressed : content;
|
|
427
|
+
const compSize = compData.length;
|
|
428
|
+
|
|
429
|
+
// Local file header: signature + 26 bytes fixed + filename.
|
|
430
|
+
const localHeader = Buffer.alloc(30 + nameBytes.length);
|
|
431
|
+
writeU32LE(localHeader, 0, 0x04034b50); // local file header sig
|
|
432
|
+
writeU16LE(localHeader, 4, 20); // version needed: 2.0
|
|
433
|
+
writeU16LE(localHeader, 6, 0); // general purpose bit flag
|
|
434
|
+
writeU16LE(localHeader, 8, compMethod); // compression method
|
|
435
|
+
writeU16LE(localHeader, 10, DOS_TIME); // last mod time
|
|
436
|
+
writeU16LE(localHeader, 12, DOS_DATE); // last mod date
|
|
437
|
+
writeU32LE(localHeader, 14, crc); // crc-32
|
|
438
|
+
writeU32LE(localHeader, 18, compSize); // compressed size
|
|
439
|
+
writeU32LE(localHeader, 22, uncompSize); // uncompressed size
|
|
440
|
+
writeU16LE(localHeader, 26, nameBytes.length); // file name length
|
|
441
|
+
writeU16LE(localHeader, 28, 0); // extra field length
|
|
442
|
+
nameBytes.copy(localHeader, 30);
|
|
443
|
+
|
|
444
|
+
centralDirEntries.push({
|
|
445
|
+
zipPath,
|
|
446
|
+
nameBytes,
|
|
447
|
+
crc,
|
|
448
|
+
compSize,
|
|
449
|
+
uncompSize,
|
|
450
|
+
compMethod,
|
|
451
|
+
offset,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
ZIP_PARTS.push(localHeader, compData);
|
|
455
|
+
offset += localHeader.length + compData.length;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Central directory.
|
|
459
|
+
const centralDirStart = offset;
|
|
460
|
+
for (const e of centralDirEntries) {
|
|
461
|
+
const cdEntry = Buffer.alloc(46 + e.nameBytes.length);
|
|
462
|
+
writeU32LE(cdEntry, 0, 0x02014b50); // central dir sig
|
|
463
|
+
writeU16LE(cdEntry, 4, 20); // version made by: 2.0
|
|
464
|
+
writeU16LE(cdEntry, 6, 20); // version needed: 2.0
|
|
465
|
+
writeU16LE(cdEntry, 8, 0); // general purpose bit flag
|
|
466
|
+
writeU16LE(cdEntry, 10, e.compMethod); // compression method
|
|
467
|
+
writeU16LE(cdEntry, 12, DOS_TIME); // last mod time
|
|
468
|
+
writeU16LE(cdEntry, 14, DOS_DATE); // last mod date
|
|
469
|
+
writeU32LE(cdEntry, 16, e.crc); // crc-32
|
|
470
|
+
writeU32LE(cdEntry, 20, e.compSize); // compressed size
|
|
471
|
+
writeU32LE(cdEntry, 24, e.uncompSize); // uncompressed size
|
|
472
|
+
writeU16LE(cdEntry, 28, e.nameBytes.length); // file name length
|
|
473
|
+
writeU16LE(cdEntry, 30, 0); // extra field length
|
|
474
|
+
writeU16LE(cdEntry, 32, 0); // file comment length
|
|
475
|
+
writeU16LE(cdEntry, 34, 0); // disk number start
|
|
476
|
+
writeU16LE(cdEntry, 36, 0); // internal file attributes
|
|
477
|
+
writeU32LE(cdEntry, 38, 0); // external file attributes
|
|
478
|
+
writeU32LE(cdEntry, 42, e.offset); // relative offset of local header
|
|
479
|
+
e.nameBytes.copy(cdEntry, 46);
|
|
480
|
+
ZIP_PARTS.push(cdEntry);
|
|
481
|
+
offset += cdEntry.length;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const centralDirSize = offset - centralDirStart;
|
|
485
|
+
|
|
486
|
+
// End of central directory record.
|
|
487
|
+
const eocd = Buffer.alloc(22);
|
|
488
|
+
writeU32LE(eocd, 0, 0x06054b50); // EOCD signature
|
|
489
|
+
writeU16LE(eocd, 4, 0); // disk number
|
|
490
|
+
writeU16LE(eocd, 6, 0); // disk with central dir
|
|
491
|
+
writeU16LE(eocd, 8, centralDirEntries.length); // entries on this disk
|
|
492
|
+
writeU16LE(eocd, 10, centralDirEntries.length); // total entries
|
|
493
|
+
writeU32LE(eocd, 12, centralDirSize); // size of central directory
|
|
494
|
+
writeU32LE(eocd, 16, centralDirStart); // offset of central directory
|
|
495
|
+
writeU16LE(eocd, 20, 0); // comment length
|
|
496
|
+
ZIP_PARTS.push(eocd);
|
|
497
|
+
|
|
498
|
+
const zipBuffer = Buffer.concat(ZIP_PARTS);
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Write zip
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
mkdirSync(DIST_DIR, { recursive: true });
|
|
505
|
+
|
|
506
|
+
const ZIP_NAME = `ijfw-${VERSION}.zip`;
|
|
507
|
+
const ZIP_PATH = join(DIST_DIR, ZIP_NAME);
|
|
508
|
+
safeWrite(ZIP_PATH, zipBuffer);
|
|
509
|
+
|
|
510
|
+
const zipSize = zipBuffer.length;
|
|
511
|
+
console.log(`[pack-hub-extension] zip: ${ZIP_PATH} (${zipSize} bytes)`);
|
|
512
|
+
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
// Compute SHA-512 SRI (sync — buffer already in memory)
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
const hashHex = createHash('sha512').update(zipBuffer).digest('hex');
|
|
518
|
+
const sri = `sha512-${Buffer.from(hashHex, 'hex').toString('base64')}`;
|
|
519
|
+
|
|
520
|
+
const SHA_PATH = join(DIST_DIR, `ijfw-${VERSION}.sha512`);
|
|
521
|
+
safeWrite(SHA_PATH, sri, 'utf8');
|
|
522
|
+
console.log(`[pack-hub-extension] sha512: ${SHA_PATH}`);
|
|
523
|
+
console.log(`[pack-hub-extension] SRI: ${sri}`);
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// Write hub-index-snippet.json (IHubExtension shape for Wayland's Hub Index)
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
// HubContributes in the index uses string ID arrays (not full adapter objects).
|
|
530
|
+
const adapterIds = manifestObj.contributes.acpAdapters.map((a) => a.id);
|
|
531
|
+
|
|
532
|
+
const hubIndexSnippet = {
|
|
533
|
+
name: manifestObj.name,
|
|
534
|
+
displayName: manifestObj.displayName,
|
|
535
|
+
version: VERSION,
|
|
536
|
+
description: manifestObj.description,
|
|
537
|
+
author: manifestObj.author,
|
|
538
|
+
icon: manifestObj.icon,
|
|
539
|
+
dist: {
|
|
540
|
+
tarball: `extensions/${ZIP_NAME}`,
|
|
541
|
+
integrity: sri,
|
|
542
|
+
unpackedSize: zipSize,
|
|
543
|
+
},
|
|
544
|
+
engines: manifestObj.engines,
|
|
545
|
+
hubs: manifestObj.hubs,
|
|
546
|
+
contributes: {
|
|
547
|
+
acpAdapters: adapterIds,
|
|
548
|
+
mcpServers: manifestObj.contributes.mcpServers,
|
|
549
|
+
},
|
|
550
|
+
tags: ['ai', 'coding', 'mcp', 'memory', 'multi-agent'],
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const SNIPPET_PATH = join(DIST_DIR, 'hub-index-snippet.json');
|
|
554
|
+
safeWrite(SNIPPET_PATH, JSON.stringify(hubIndexSnippet, null, 2) + '\n', 'utf8');
|
|
555
|
+
console.log(`[pack-hub-extension] hub index snippet: ${SNIPPET_PATH}`);
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Done
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
console.log(`[pack-hub-extension] DONE — ${ZIP_NAME} ready for Wayland bundling`);
|