@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.
@@ -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`);