@naarang/ccc 2.2.0 → 3.0.0-beta.2

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.
@@ -1,533 +1,539 @@
1
- /**
2
- * Build script for creating cross-platform binaries with embedded version
3
- * and native library support (bun-pty)
4
- */
5
-
6
- import { $ } from 'bun';
7
- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
8
- import { join } from 'path';
9
-
10
- // Read version from package.json
11
- const packageJson = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
- const VERSION = packageJson.version;
13
-
14
- console.log(`Building CCC v${VERSION}`);
15
-
16
- // Ensure binaries directory exists
17
- const binariesDir = join(import.meta.dir, '..', 'binaries');
18
- if (!existsSync(binariesDir)) {
19
- mkdirSync(binariesDir, { recursive: true });
20
- }
21
-
22
- // Define targets with their native library paths
23
- const targets = [
24
- {
25
- name: 'linux-x64',
26
- target: 'bun-linux-x64',
27
- outfile: 'ccc-linux-x64',
28
- ptyLib: 'librust_pty.so',
29
- ngrokLib: 'ngrok.linux-x64-gnu.node',
30
- ngrokPkg: '@ngrok/ngrok-linux-x64-gnu',
31
- },
32
- {
33
- name: 'linux-arm64',
34
- target: 'bun-linux-arm64',
35
- outfile: 'ccc-linux-arm64',
36
- ptyLib: 'librust_pty_arm64.so',
37
- ngrokLib: 'ngrok.linux-arm64-gnu.node',
38
- ngrokPkg: '@ngrok/ngrok-linux-arm64-gnu',
39
- },
40
- {
41
- name: 'darwin-x64',
42
- target: 'bun-darwin-x64',
43
- outfile: 'ccc-darwin-x64',
44
- ptyLib: 'librust_pty.dylib',
45
- ngrokLib: 'ngrok.darwin-x64.node',
46
- ngrokPkg: '@ngrok/ngrok-darwin-x64',
47
- },
48
- {
49
- name: 'darwin-arm64',
50
- target: 'bun-darwin-arm64',
51
- outfile: 'ccc-darwin-arm64',
52
- ptyLib: 'librust_pty_arm64.dylib',
53
- ngrokLib: 'ngrok.darwin-arm64.node',
54
- ngrokPkg: '@ngrok/ngrok-darwin-arm64',
55
- },
56
- {
57
- name: 'windows-x64',
58
- target: 'bun-windows-x64',
59
- outfile: 'ccc-windows-x64.exe',
60
- ptyLib: 'rust_pty.dll',
61
- ngrokLib: 'ngrok.win32-x64-msvc.node',
62
- ngrokPkg: '@ngrok/ngrok-win32-x64-msvc',
63
- },
64
- // NOTE: windows-arm64 is not supported by Bun's compile target (no bun-windows-aarch64)
65
- // Re-enable when Bun adds support: https://github.com/oven-sh/bun/issues/9824
66
- ];
67
-
68
- // Parse command line arguments
69
- const args = process.argv.slice(2);
70
- const targetArg = args.find((arg) => !arg.startsWith('-'));
71
- const selectedTargets = targetArg ? targets.filter((t) => t.name === targetArg) : targets;
72
-
73
- if (targetArg && selectedTargets.length === 0) {
74
- console.error(`Unknown target: ${targetArg}`);
75
- console.error(`Available targets: ${targets.map((t) => t.name).join(', ')}`);
76
- process.exit(1);
77
- }
78
-
79
- // Paths to the generated loader files
80
- const loaderDir = join(import.meta.dir, '..', 'src', 'lib');
81
- const ptyLoaderPath = join(loaderDir, 'bun-pty-loader.generated.ts');
82
- const ngrokLoaderPath = join(loaderDir, 'ngrok-loader.generated.ts');
83
- const nodeModulesDir = join(import.meta.dir, '..', 'node_modules');
84
-
85
- // Ensure the lib directory exists
86
- if (!existsSync(loaderDir)) {
87
- mkdirSync(loaderDir, { recursive: true });
88
- }
89
-
90
- /**
91
- * Check if a ngrok platform package is installed
92
- */
93
- function isNgrokPackageInstalled(ngrokPkg: string, ngrokLib: string): boolean {
94
- // For scoped packages like @ngrok/ngrok-darwin-x64, check for the .node file
95
- const nodeFilePath = join(nodeModulesDir, ngrokPkg, ngrokLib);
96
- return existsSync(nodeFilePath);
97
- }
98
-
99
- /**
100
- * Try to install a ngrok platform package
101
- */
102
- async function installNgrokPackage(ngrokPkg: string, ngrokLib: string): Promise<boolean> {
103
- try {
104
- console.log(` Installing ${ngrokPkg}...`);
105
-
106
- // Download the package tarball directly from npm registry and extract it
107
- const pkgDir = join(nodeModulesDir, ngrokPkg);
108
- const scopeDir = join(nodeModulesDir, '@ngrok');
109
-
110
- // Ensure @ngrok scope directory exists
111
- if (!existsSync(scopeDir)) {
112
- mkdirSync(scopeDir, { recursive: true });
113
- }
114
-
115
- // Use npm pack to download and extract the package
116
- await $`npm pack ${ngrokPkg}@1.6.0 --pack-destination ${scopeDir}`;
117
-
118
- // Find the tarball and extract it
119
- const tarball = join(scopeDir, `ngrok-${ngrokPkg.split('/')[1]}-1.6.0.tgz`);
120
- await $`tar -xzf ${tarball} -C ${scopeDir}`;
121
-
122
- // npm pack extracts to 'package' directory, rename it
123
- const extractedDir = join(scopeDir, 'package');
124
- if (existsSync(extractedDir)) {
125
- // Remove existing if any
126
- if (existsSync(pkgDir)) {
127
- await $`rm -rf ${pkgDir}`;
128
- }
129
- await $`mv ${extractedDir} ${pkgDir}`;
130
- }
131
-
132
- // Clean up tarball
133
- await $`rm -f ${tarball}`;
134
-
135
- // Verify the file exists
136
- const nodeFilePath = join(pkgDir, ngrokLib);
137
- if (existsSync(nodeFilePath)) {
138
- console.log(` Successfully installed ${ngrokPkg}`);
139
- return true;
140
- } else {
141
- console.error(` Package installed but ${ngrokLib} not found`);
142
- return false;
143
- }
144
- } catch (error) {
145
- console.error(` Failed to install ${ngrokPkg}:`, error);
146
- return false;
147
- }
148
- }
149
-
150
- /**
151
- * Generate a platform-specific bun-pty loader that embeds the native library
152
- * This inlines the bun-pty implementation to ensure the library is extracted
153
- * before dlopen is called (bun-pty calls dlopen at module load time)
154
- */
155
- function generatePtyLoader(ptyLib: string): void {
156
- // Get the absolute path to the native library for reliable embedding
157
- const nativeLibAbsPath = join(nodeModulesDir, 'bun-pty', 'rust-pty', 'target', 'release', ptyLib).replace(/\\/g, '/');
158
-
159
- const loaderContent = `/**
160
- * Auto-generated bun-pty implementation with embedded native library
161
- * DO NOT EDIT - This file is generated by build-binaries.ts
162
- *
163
- * This is a copy of bun-pty's implementation that uses an embedded native library.
164
- * We can't just set BUN_PTY_LIB and import bun-pty because bun-pty calls dlopen
165
- * at module load time, before our env var setup code runs.
166
- */
167
-
168
- import { dlopen, FFIType, ptr } from 'bun:ffi';
169
- import { Buffer } from 'buffer';
170
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'fs';
171
- import { join } from 'path';
172
- import { homedir } from 'os';
173
-
174
- // Embed the native library file - Bun will include this in the binary
175
- // Using absolute path for reliable resolution during compilation
176
- // @ts-ignore - Bun's file embedding syntax
177
- import embeddedLibPath from '${nativeLibAbsPath}' with { type: 'file' };
178
-
179
- // Extract embedded library to user cache directory (persistent, won't be auto-deleted)
180
- const libName = '${ptyLib}';
181
- const cacheDir = join(homedir(), '.cache', 'ccc', 'lib');
182
- const extractedLibPath = join(cacheDir, libName);
183
-
184
- // Extract library if not present or if embedded version is different size
185
- try {
186
- let needsExtract = true;
187
-
188
- if (existsSync(extractedLibPath)) {
189
- // Check if sizes match (quick check for version changes)
190
- const embeddedSize = statSync(embeddedLibPath).size;
191
- const extractedSize = statSync(extractedLibPath).size;
192
- needsExtract = embeddedSize !== extractedSize;
193
- }
194
-
195
- if (needsExtract) {
196
- if (!existsSync(cacheDir)) {
197
- mkdirSync(cacheDir, { recursive: true });
198
- }
199
-
200
- if (!existsSync(embeddedLibPath)) {
201
- throw new Error(\`Embedded library not found at: \${embeddedLibPath}\`);
202
- }
203
-
204
- const libData = readFileSync(embeddedLibPath);
205
- writeFileSync(extractedLibPath, libData);
206
-
207
- // Make executable on Unix
208
- try {
209
- chmodSync(extractedLibPath, 0o755);
210
- } catch {
211
- // Ignore chmod errors on Windows
212
- }
213
- }
214
- } catch (extractError) {
215
- throw new Error(\`Failed to extract PTY library: \${extractError instanceof Error ? extractError.message : String(extractError)}\`);
216
- }
217
-
218
- // Load the native library
219
- const lib = dlopen(extractedLibPath, {
220
- bun_pty_spawn: {
221
- args: [FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32],
222
- returns: FFIType.i32,
223
- },
224
- bun_pty_write: {
225
- args: [FFIType.i32, FFIType.pointer, FFIType.i32],
226
- returns: FFIType.i32,
227
- },
228
- bun_pty_read: {
229
- args: [FFIType.i32, FFIType.pointer, FFIType.i32],
230
- returns: FFIType.i32,
231
- },
232
- bun_pty_resize: {
233
- args: [FFIType.i32, FFIType.i32, FFIType.i32],
234
- returns: FFIType.i32,
235
- },
236
- bun_pty_kill: { args: [FFIType.i32], returns: FFIType.i32 },
237
- bun_pty_get_pid: { args: [FFIType.i32], returns: FFIType.i32 },
238
- bun_pty_get_exit_code: { args: [FFIType.i32], returns: FFIType.i32 },
239
- bun_pty_close: { args: [FFIType.i32], returns: FFIType.void },
240
- });
241
-
242
- // EventEmitter for PTY events
243
- class EventEmitter<T> {
244
- private listeners: ((data: T) => void)[] = [];
245
-
246
- event = (listener: (data: T) => void) => {
247
- this.listeners.push(listener);
248
- return {
249
- dispose: () => {
250
- const i = this.listeners.indexOf(listener);
251
- if (i !== -1) this.listeners.splice(i, 1);
252
- },
253
- };
254
- };
255
-
256
- fire(data: T) {
257
- for (const listener of this.listeners) {
258
- listener(data);
259
- }
260
- }
261
- }
262
-
263
- // Shell quote helper
264
- function shQuote(s: string): string {
265
- if (s.length === 0) return "''";
266
- return \`'\${s.replace(/'/g, \`'\\\\''\\'\`)}'\`;
267
- }
268
-
269
- const DEFAULT_COLS = 80;
270
- const DEFAULT_ROWS = 24;
271
- const DEFAULT_FILE = process.platform === 'win32' ? 'powershell.exe' : 'sh';
272
-
273
- export interface IPtyOptions {
274
- name?: string;
275
- cols?: number;
276
- rows?: number;
277
- cwd?: string;
278
- env?: Record<string, string>;
279
- }
280
-
281
- export interface IPty {
282
- pid: number;
283
- cols: number;
284
- rows: number;
285
- process: string;
286
- onData: (listener: (data: string) => void) => { dispose: () => void };
287
- onExit: (listener: (data: { exitCode: number; signal?: string }) => void) => { dispose: () => void };
288
- write(data: string): void;
289
- resize(cols: number, rows: number): void;
290
- kill(signal?: string): void;
291
- }
292
-
293
- export class Terminal implements IPty {
294
- private handle = -1;
295
- private _pid = -1;
296
- private _cols = DEFAULT_COLS;
297
- private _rows = DEFAULT_ROWS;
298
- private _readLoop = false;
299
- private _closing = false;
300
- private _onData = new EventEmitter<string>();
301
- private _onExit = new EventEmitter<{ exitCode: number; signal?: string }>();
302
-
303
- constructor(file = DEFAULT_FILE, args: string[] = [], opts: IPtyOptions = {}) {
304
- this._cols = opts.cols ?? DEFAULT_COLS;
305
- this._rows = opts.rows ?? DEFAULT_ROWS;
306
- const cwd = opts.cwd ?? process.cwd();
307
- const cmdline = [file, ...args.map(shQuote)].join(' ');
308
- let envStr = '';
309
- if (opts.env) {
310
- const envPairs = Object.entries(opts.env).map(([k, v]) => \`\${k}=\${v}\`);
311
- envStr = envPairs.join('\\x00') + '\\x00';
312
- }
313
-
314
- this.handle = lib.symbols.bun_pty_spawn(
315
- Buffer.from(\`\${cmdline}\\x00\`, 'utf8'),
316
- Buffer.from(\`\${cwd}\\x00\`, 'utf8'),
317
- Buffer.from(\`\${envStr}\\x00\`, 'utf8'),
318
- this._cols,
319
- this._rows
320
- );
321
-
322
- if (this.handle < 0) throw new Error('PTY spawn failed');
323
- this._pid = lib.symbols.bun_pty_get_pid(this.handle);
324
- this._startReadLoop();
325
- }
326
-
327
- get pid() { return this._pid; }
328
- get cols() { return this._cols; }
329
- get rows() { return this._rows; }
330
- get process() { return 'shell'; }
331
- get onData() { return this._onData.event; }
332
- get onExit() { return this._onExit.event; }
333
-
334
- write(data: string) {
335
- if (this._closing) return;
336
- const buf = Buffer.from(data, 'utf8');
337
- lib.symbols.bun_pty_write(this.handle, ptr(buf), buf.length);
338
- }
339
-
340
- resize(cols: number, rows: number) {
341
- if (this._closing) return;
342
- this._cols = cols;
343
- this._rows = rows;
344
- lib.symbols.bun_pty_resize(this.handle, cols, rows);
345
- }
346
-
347
- kill(signal = 'SIGTERM') {
348
- if (this._closing) return;
349
- this._closing = true;
350
- lib.symbols.bun_pty_kill(this.handle);
351
- lib.symbols.bun_pty_close(this.handle);
352
- this._onExit.fire({ exitCode: 0, signal });
353
- }
354
-
355
- private async _startReadLoop() {
356
- if (this._readLoop) return;
357
- this._readLoop = true;
358
- const buf = Buffer.allocUnsafe(4096);
359
-
360
- while (this._readLoop && !this._closing) {
361
- const n = lib.symbols.bun_pty_read(this.handle, ptr(buf), buf.length);
362
- if (n > 0) {
363
- this._onData.fire(buf.subarray(0, n).toString('utf8'));
364
- } else if (n === -2) {
365
- const exitCode = lib.symbols.bun_pty_get_exit_code(this.handle);
366
- this._onExit.fire({ exitCode });
367
- break;
368
- } else if (n < 0) {
369
- break;
370
- } else {
371
- await new Promise((r) => setTimeout(r, 8));
372
- }
373
- }
374
- }
375
- }
376
-
377
- export function spawn(file: string, args: string[], options: IPtyOptions): IPty {
378
- return new Terminal(file, args, options);
379
- }
380
- `;
381
-
382
- writeFileSync(ptyLoaderPath, loaderContent);
383
- console.log(` Generated bun-pty loader for ${ptyLib}`);
384
- }
385
-
386
- /**
387
- * Generate a platform-specific ngrok loader that embeds the native NAPI module
388
- * This extracts the .node file at runtime and loads it using process.dlopen
389
- */
390
- function generateNgrokLoader(ngrokLib: string, ngrokPkg: string): void {
391
- // Use absolute path to the ngrok native module for reliable resolution
392
- const ngrokNodePath = join(nodeModulesDir, ngrokPkg, ngrokLib).replace(/\\/g, '/');
393
-
394
- const loaderContent = `/**
395
- * Auto-generated ngrok loader with embedded native NAPI module
396
- * DO NOT EDIT - This file is generated by build-binaries.ts
397
- *
398
- * This embeds the platform-specific ngrok .node file and extracts it at runtime.
399
- */
400
-
401
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
402
- import { join } from 'path';
403
- import { tmpdir } from 'os';
404
-
405
- // Embed the native ngrok .node file - Bun will include this in the binary
406
- // @ts-ignore - Bun's file embedding syntax
407
- import embeddedNgrokPath from '${ngrokNodePath}' with { type: 'file' };
408
-
409
- // Extract embedded library to temp directory
410
- const libName = '${ngrokLib}';
411
- const tempDir = join(tmpdir(), 'ccc-ngrok-lib');
412
- const extractedLibPath = join(tempDir, libName);
413
-
414
- if (!existsSync(extractedLibPath)) {
415
- if (!existsSync(tempDir)) {
416
- mkdirSync(tempDir, { recursive: true });
417
- }
418
- const libData = readFileSync(embeddedNgrokPath);
419
- writeFileSync(extractedLibPath, libData);
420
- // Make executable on Unix systems
421
- try {
422
- chmodSync(extractedLibPath, 0o755);
423
- } catch {
424
- // Ignore chmod errors on Windows
425
- }
426
- }
427
-
428
- // Load the native module using process.dlopen (NAPI compatible)
429
- const nativeModule: { exports: Record<string, any> } = { exports: {} };
430
- process.dlopen(nativeModule, extractedLibPath);
431
-
432
- // Re-export all ngrok functions from the native binding
433
- const nativeBinding = nativeModule.exports;
434
-
435
- export const {
436
- connect,
437
- forward,
438
- disconnect,
439
- kill,
440
- Listener,
441
- listeners,
442
- getListener,
443
- getListenerByUrl,
444
- HttpListenerBuilder,
445
- TcpListenerBuilder,
446
- TlsListenerBuilder,
447
- LabeledListenerBuilder,
448
- loggingCallback,
449
- authtoken,
450
- SessionBuilder,
451
- Session,
452
- UpdateRequest,
453
- } = nativeBinding;
454
-
455
- // Export default for compatibility
456
- export default nativeBinding;
457
- `;
458
-
459
- writeFileSync(ngrokLoaderPath, loaderContent);
460
- console.log(` Generated ngrok loader for ${ngrokLib}`);
461
- }
462
-
463
- /**
464
- * Clean up the generated loader files
465
- */
466
- function cleanupLoaders(): void {
467
- if (existsSync(ptyLoaderPath)) {
468
- unlinkSync(ptyLoaderPath);
469
- }
470
- if (existsSync(ngrokLoaderPath)) {
471
- unlinkSync(ngrokLoaderPath);
472
- }
473
- }
474
-
475
- // Build each target
476
- for (const { name, target, outfile, ptyLib, ngrokLib, ngrokPkg } of selectedTargets) {
477
- console.log(`\nBuilding ${name}...`);
478
-
479
- const outfilePath = join(binariesDir, outfile);
480
-
481
- try {
482
- // Ensure ngrok platform package is installed (needed for cross-compilation)
483
- if (!isNgrokPackageInstalled(ngrokPkg, ngrokLib)) {
484
- const installed = await installNgrokPackage(ngrokPkg, ngrokLib);
485
- if (!installed) {
486
- throw new Error(`Failed to install ${ngrokPkg}`);
487
- }
488
- }
489
-
490
- // Verify the .node file exists
491
- const ngrokNodePath = join(nodeModulesDir, ngrokPkg, ngrokLib);
492
- const pkgDir = join(nodeModulesDir, ngrokPkg);
493
-
494
- // Debug: list package contents
495
- console.log(` Checking ${pkgDir}...`);
496
- try {
497
- const { readdirSync } = await import('fs');
498
- const files = readdirSync(pkgDir);
499
- console.log(` Package contents: ${files.join(', ')}`);
500
- } catch (e) {
501
- console.log(` Could not read directory: ${e}`);
502
- }
503
-
504
- if (!existsSync(ngrokNodePath)) {
505
- throw new Error(`ngrok .node file not found at ${ngrokNodePath}`);
506
- }
507
- console.log(` Verified ngrok .node file exists at ${ngrokNodePath}`);
508
-
509
- // Verify bun-pty native library exists
510
- const ptyLibPath = join(nodeModulesDir, 'bun-pty', 'rust-pty', 'target', 'release', ptyLib);
511
- if (!existsSync(ptyLibPath)) {
512
- throw new Error(`bun-pty native library not found at ${ptyLibPath}`);
513
- }
514
- console.log(` Verified bun-pty native library exists at ${ptyLibPath}`);
515
-
516
- // Generate platform-specific loaders
517
- generatePtyLoader(ptyLib);
518
- generateNgrokLoader(ngrokLib, ngrokPkg);
519
-
520
- // Build the binary (bun-pty and ngrok are now bundled via the generated loaders)
521
- await $`bun build --compile --minify --bytecode --target=${target} --define BUILD_VERSION='"${VERSION}"' src/index.ts --outfile ${outfilePath}`;
522
- console.log(` ✓ Built ${outfile}`);
523
- } catch (error) {
524
- console.error(` ✗ Failed to build ${name}:`, error);
525
- cleanupLoaders();
526
- process.exit(1);
527
- }
528
- }
529
-
530
- // Clean up generated files
531
- cleanupLoaders();
532
-
533
- console.log('\nBuild complete!');
1
+ /**
2
+ * Build script for creating cross-platform binaries with embedded version
3
+ * and native library support (bun-pty)
4
+ */
5
+
6
+ import { $ } from 'bun';
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
8
+ import { join } from 'path';
9
+
10
+ // Read version from package.json
11
+ const packageJson = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
+ const VERSION = packageJson.version;
13
+
14
+ console.log(`Building CCC v${VERSION}`);
15
+
16
+ // Ensure binaries directory exists
17
+ const binariesDir = join(import.meta.dir, '..', 'binaries');
18
+ if (!existsSync(binariesDir)) {
19
+ mkdirSync(binariesDir, { recursive: true });
20
+ }
21
+
22
+ // Define targets with their native library paths
23
+ const targets = [
24
+ {
25
+ name: 'linux-x64',
26
+ target: 'bun-linux-x64',
27
+ outfile: 'ccc-linux-x64',
28
+ ptyLib: 'librust_pty.so',
29
+ ngrokLib: 'ngrok.linux-x64-gnu.node',
30
+ ngrokPkg: '@ngrok/ngrok-linux-x64-gnu',
31
+ },
32
+ {
33
+ name: 'linux-arm64',
34
+ target: 'bun-linux-arm64',
35
+ outfile: 'ccc-linux-arm64',
36
+ ptyLib: 'librust_pty_arm64.so',
37
+ ngrokLib: 'ngrok.linux-arm64-gnu.node',
38
+ ngrokPkg: '@ngrok/ngrok-linux-arm64-gnu',
39
+ },
40
+ {
41
+ name: 'darwin-x64',
42
+ target: 'bun-darwin-x64',
43
+ outfile: 'ccc-darwin-x64',
44
+ ptyLib: 'librust_pty.dylib',
45
+ ngrokLib: 'ngrok.darwin-x64.node',
46
+ ngrokPkg: '@ngrok/ngrok-darwin-x64',
47
+ },
48
+ {
49
+ name: 'darwin-arm64',
50
+ target: 'bun-darwin-arm64',
51
+ outfile: 'ccc-darwin-arm64',
52
+ ptyLib: 'librust_pty_arm64.dylib',
53
+ ngrokLib: 'ngrok.darwin-arm64.node',
54
+ ngrokPkg: '@ngrok/ngrok-darwin-arm64',
55
+ },
56
+ {
57
+ name: 'windows-x64',
58
+ target: 'bun-windows-x64',
59
+ outfile: 'ccc-windows-x64.exe',
60
+ ptyLib: 'rust_pty.dll',
61
+ ngrokLib: 'ngrok.win32-x64-msvc.node',
62
+ ngrokPkg: '@ngrok/ngrok-win32-x64-msvc',
63
+ },
64
+ {
65
+ name: 'windows-arm64',
66
+ target: 'bun-windows-arm64',
67
+ outfile: 'ccc-windows-arm64.exe',
68
+ ptyLib: 'rust_pty_arm64.dll',
69
+ ngrokLib: 'ngrok.win32-arm64-msvc.node',
70
+ ngrokPkg: '@ngrok/ngrok-win32-arm64-msvc',
71
+ },
72
+ ];
73
+
74
+ // Parse command line arguments
75
+ const args = process.argv.slice(2);
76
+ const targetArg = args.find((arg) => !arg.startsWith('-'));
77
+ const selectedTargets = targetArg ? targets.filter((t) => t.name === targetArg) : targets;
78
+
79
+ if (targetArg && selectedTargets.length === 0) {
80
+ console.error(`Unknown target: ${targetArg}`);
81
+ console.error(`Available targets: ${targets.map((t) => t.name).join(', ')}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ // Paths to the generated loader files
86
+ const loaderDir = join(import.meta.dir, '..', 'src', 'lib');
87
+ const ptyLoaderPath = join(loaderDir, 'bun-pty-loader.generated.ts');
88
+ const ngrokLoaderPath = join(loaderDir, 'ngrok-loader.generated.ts');
89
+ const nodeModulesDir = join(import.meta.dir, '..', 'node_modules');
90
+
91
+ // Ensure the lib directory exists
92
+ if (!existsSync(loaderDir)) {
93
+ mkdirSync(loaderDir, { recursive: true });
94
+ }
95
+
96
+ /**
97
+ * Check if a ngrok platform package is installed
98
+ */
99
+ function isNgrokPackageInstalled(ngrokPkg: string, ngrokLib: string): boolean {
100
+ // For scoped packages like @ngrok/ngrok-darwin-x64, check for the .node file
101
+ const nodeFilePath = join(nodeModulesDir, ngrokPkg, ngrokLib);
102
+ return existsSync(nodeFilePath);
103
+ }
104
+
105
+ /**
106
+ * Try to install a ngrok platform package
107
+ */
108
+ async function installNgrokPackage(ngrokPkg: string, ngrokLib: string): Promise<boolean> {
109
+ try {
110
+ console.log(` Installing ${ngrokPkg}...`);
111
+
112
+ // Download the package tarball directly from npm registry and extract it
113
+ const pkgDir = join(nodeModulesDir, ngrokPkg);
114
+ const scopeDir = join(nodeModulesDir, '@ngrok');
115
+
116
+ // Ensure @ngrok scope directory exists
117
+ if (!existsSync(scopeDir)) {
118
+ mkdirSync(scopeDir, { recursive: true });
119
+ }
120
+
121
+ // Use npm pack to download and extract the package
122
+ await $`npm pack ${ngrokPkg}@1.6.0 --pack-destination ${scopeDir}`;
123
+
124
+ // Find the tarball and extract it
125
+ const tarball = join(scopeDir, `ngrok-${ngrokPkg.split('/')[1]}-1.6.0.tgz`);
126
+ await $`tar -xzf ${tarball} -C ${scopeDir}`;
127
+
128
+ // npm pack extracts to 'package' directory, rename it
129
+ const extractedDir = join(scopeDir, 'package');
130
+ if (existsSync(extractedDir)) {
131
+ // Remove existing if any
132
+ if (existsSync(pkgDir)) {
133
+ await $`rm -rf ${pkgDir}`;
134
+ }
135
+ await $`mv ${extractedDir} ${pkgDir}`;
136
+ }
137
+
138
+ // Clean up tarball
139
+ await $`rm -f ${tarball}`;
140
+
141
+ // Verify the file exists
142
+ const nodeFilePath = join(pkgDir, ngrokLib);
143
+ if (existsSync(nodeFilePath)) {
144
+ console.log(` Successfully installed ${ngrokPkg}`);
145
+ return true;
146
+ } else {
147
+ console.error(` Package installed but ${ngrokLib} not found`);
148
+ return false;
149
+ }
150
+ } catch (error) {
151
+ console.error(` Failed to install ${ngrokPkg}:`, error);
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Generate a platform-specific bun-pty loader that embeds the native library
158
+ * This inlines the bun-pty implementation to ensure the library is extracted
159
+ * before dlopen is called (bun-pty calls dlopen at module load time)
160
+ */
161
+ function generatePtyLoader(ptyLib: string): void {
162
+ // Get the absolute path to the native library for reliable embedding
163
+ const nativeLibAbsPath = join(nodeModulesDir, 'bun-pty', 'rust-pty', 'target', 'release', ptyLib).replace(/\\/g, '/');
164
+
165
+ const loaderContent = `/**
166
+ * Auto-generated bun-pty implementation with embedded native library
167
+ * DO NOT EDIT - This file is generated by build-binaries.ts
168
+ *
169
+ * This is a copy of bun-pty's implementation that uses an embedded native library.
170
+ * We can't just set BUN_PTY_LIB and import bun-pty because bun-pty calls dlopen
171
+ * at module load time, before our env var setup code runs.
172
+ */
173
+
174
+ import { dlopen, FFIType, ptr } from 'bun:ffi';
175
+ import { Buffer } from 'buffer';
176
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'fs';
177
+ import { join } from 'path';
178
+ import { homedir } from 'os';
179
+
180
+ // Embed the native library file - Bun will include this in the binary
181
+ // Using absolute path for reliable resolution during compilation
182
+ // @ts-ignore - Bun's file embedding syntax
183
+ import embeddedLibPath from '${nativeLibAbsPath}' with { type: 'file' };
184
+
185
+ // Extract embedded library to user cache directory (persistent, won't be auto-deleted)
186
+ const libName = '${ptyLib}';
187
+ const cacheDir = join(homedir(), '.cache', 'ccc', 'lib');
188
+ const extractedLibPath = join(cacheDir, libName);
189
+
190
+ // Extract library if not present or if embedded version is different size
191
+ try {
192
+ let needsExtract = true;
193
+
194
+ if (existsSync(extractedLibPath)) {
195
+ // Check if sizes match (quick check for version changes)
196
+ const embeddedSize = statSync(embeddedLibPath).size;
197
+ const extractedSize = statSync(extractedLibPath).size;
198
+ needsExtract = embeddedSize !== extractedSize;
199
+ }
200
+
201
+ if (needsExtract) {
202
+ if (!existsSync(cacheDir)) {
203
+ mkdirSync(cacheDir, { recursive: true });
204
+ }
205
+
206
+ if (!existsSync(embeddedLibPath)) {
207
+ throw new Error(\`Embedded library not found at: \${embeddedLibPath}\`);
208
+ }
209
+
210
+ const libData = readFileSync(embeddedLibPath);
211
+ writeFileSync(extractedLibPath, libData);
212
+
213
+ // Make executable on Unix
214
+ try {
215
+ chmodSync(extractedLibPath, 0o755);
216
+ } catch {
217
+ // Ignore chmod errors on Windows
218
+ }
219
+ }
220
+ } catch (extractError) {
221
+ throw new Error(\`Failed to extract PTY library: \${extractError instanceof Error ? extractError.message : String(extractError)}\`);
222
+ }
223
+
224
+ // Load the native library
225
+ const lib = dlopen(extractedLibPath, {
226
+ bun_pty_spawn: {
227
+ args: [FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32],
228
+ returns: FFIType.i32,
229
+ },
230
+ bun_pty_write: {
231
+ args: [FFIType.i32, FFIType.pointer, FFIType.i32],
232
+ returns: FFIType.i32,
233
+ },
234
+ bun_pty_read: {
235
+ args: [FFIType.i32, FFIType.pointer, FFIType.i32],
236
+ returns: FFIType.i32,
237
+ },
238
+ bun_pty_resize: {
239
+ args: [FFIType.i32, FFIType.i32, FFIType.i32],
240
+ returns: FFIType.i32,
241
+ },
242
+ bun_pty_kill: { args: [FFIType.i32], returns: FFIType.i32 },
243
+ bun_pty_get_pid: { args: [FFIType.i32], returns: FFIType.i32 },
244
+ bun_pty_get_exit_code: { args: [FFIType.i32], returns: FFIType.i32 },
245
+ bun_pty_close: { args: [FFIType.i32], returns: FFIType.void },
246
+ });
247
+
248
+ // EventEmitter for PTY events
249
+ class EventEmitter<T> {
250
+ private listeners: ((data: T) => void)[] = [];
251
+
252
+ event = (listener: (data: T) => void) => {
253
+ this.listeners.push(listener);
254
+ return {
255
+ dispose: () => {
256
+ const i = this.listeners.indexOf(listener);
257
+ if (i !== -1) this.listeners.splice(i, 1);
258
+ },
259
+ };
260
+ };
261
+
262
+ fire(data: T) {
263
+ for (const listener of this.listeners) {
264
+ listener(data);
265
+ }
266
+ }
267
+ }
268
+
269
+ // Shell quote helper
270
+ function shQuote(s: string): string {
271
+ if (s.length === 0) return "''";
272
+ return \`'\${s.replace(/'/g, \`'\\\\''\\'\`)}'\`;
273
+ }
274
+
275
+ const DEFAULT_COLS = 80;
276
+ const DEFAULT_ROWS = 24;
277
+ const DEFAULT_FILE = process.platform === 'win32' ? 'powershell.exe' : 'sh';
278
+
279
+ export interface IPtyOptions {
280
+ name?: string;
281
+ cols?: number;
282
+ rows?: number;
283
+ cwd?: string;
284
+ env?: Record<string, string>;
285
+ }
286
+
287
+ export interface IPty {
288
+ pid: number;
289
+ cols: number;
290
+ rows: number;
291
+ process: string;
292
+ onData: (listener: (data: string) => void) => { dispose: () => void };
293
+ onExit: (listener: (data: { exitCode: number; signal?: string }) => void) => { dispose: () => void };
294
+ write(data: string): void;
295
+ resize(cols: number, rows: number): void;
296
+ kill(signal?: string): void;
297
+ }
298
+
299
+ export class Terminal implements IPty {
300
+ private handle = -1;
301
+ private _pid = -1;
302
+ private _cols = DEFAULT_COLS;
303
+ private _rows = DEFAULT_ROWS;
304
+ private _readLoop = false;
305
+ private _closing = false;
306
+ private _onData = new EventEmitter<string>();
307
+ private _onExit = new EventEmitter<{ exitCode: number; signal?: string }>();
308
+
309
+ constructor(file = DEFAULT_FILE, args: string[] = [], opts: IPtyOptions = {}) {
310
+ this._cols = opts.cols ?? DEFAULT_COLS;
311
+ this._rows = opts.rows ?? DEFAULT_ROWS;
312
+ const cwd = opts.cwd ?? process.cwd();
313
+ const cmdline = [file, ...args.map(shQuote)].join(' ');
314
+ let envStr = '';
315
+ if (opts.env) {
316
+ const envPairs = Object.entries(opts.env).map(([k, v]) => \`\${k}=\${v}\`);
317
+ envStr = envPairs.join('\\x00') + '\\x00';
318
+ }
319
+
320
+ this.handle = lib.symbols.bun_pty_spawn(
321
+ Buffer.from(\`\${cmdline}\\x00\`, 'utf8'),
322
+ Buffer.from(\`\${cwd}\\x00\`, 'utf8'),
323
+ Buffer.from(\`\${envStr}\\x00\`, 'utf8'),
324
+ this._cols,
325
+ this._rows
326
+ );
327
+
328
+ if (this.handle < 0) throw new Error('PTY spawn failed');
329
+ this._pid = lib.symbols.bun_pty_get_pid(this.handle);
330
+ this._startReadLoop();
331
+ }
332
+
333
+ get pid() { return this._pid; }
334
+ get cols() { return this._cols; }
335
+ get rows() { return this._rows; }
336
+ get process() { return 'shell'; }
337
+ get onData() { return this._onData.event; }
338
+ get onExit() { return this._onExit.event; }
339
+
340
+ write(data: string) {
341
+ if (this._closing) return;
342
+ const buf = Buffer.from(data, 'utf8');
343
+ lib.symbols.bun_pty_write(this.handle, ptr(buf), buf.length);
344
+ }
345
+
346
+ resize(cols: number, rows: number) {
347
+ if (this._closing) return;
348
+ this._cols = cols;
349
+ this._rows = rows;
350
+ lib.symbols.bun_pty_resize(this.handle, cols, rows);
351
+ }
352
+
353
+ kill(signal = 'SIGTERM') {
354
+ if (this._closing) return;
355
+ this._closing = true;
356
+ lib.symbols.bun_pty_kill(this.handle);
357
+ lib.symbols.bun_pty_close(this.handle);
358
+ this._onExit.fire({ exitCode: 0, signal });
359
+ }
360
+
361
+ private async _startReadLoop() {
362
+ if (this._readLoop) return;
363
+ this._readLoop = true;
364
+ const buf = Buffer.allocUnsafe(4096);
365
+
366
+ while (this._readLoop && !this._closing) {
367
+ const n = lib.symbols.bun_pty_read(this.handle, ptr(buf), buf.length);
368
+ if (n > 0) {
369
+ this._onData.fire(buf.subarray(0, n).toString('utf8'));
370
+ } else if (n === -2) {
371
+ const exitCode = lib.symbols.bun_pty_get_exit_code(this.handle);
372
+ this._onExit.fire({ exitCode });
373
+ break;
374
+ } else if (n < 0) {
375
+ break;
376
+ } else {
377
+ await new Promise((r) => setTimeout(r, 8));
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ export function spawn(file: string, args: string[], options: IPtyOptions): IPty {
384
+ return new Terminal(file, args, options);
385
+ }
386
+ `;
387
+
388
+ writeFileSync(ptyLoaderPath, loaderContent);
389
+ console.log(` Generated bun-pty loader for ${ptyLib}`);
390
+ }
391
+
392
+ /**
393
+ * Generate a platform-specific ngrok loader that embeds the native NAPI module
394
+ * This extracts the .node file at runtime and loads it using process.dlopen
395
+ */
396
+ function generateNgrokLoader(ngrokLib: string, ngrokPkg: string): void {
397
+ // Use absolute path to the ngrok native module for reliable resolution
398
+ const ngrokNodePath = join(nodeModulesDir, ngrokPkg, ngrokLib).replace(/\\/g, '/');
399
+
400
+ const loaderContent = `/**
401
+ * Auto-generated ngrok loader with embedded native NAPI module
402
+ * DO NOT EDIT - This file is generated by build-binaries.ts
403
+ *
404
+ * This embeds the platform-specific ngrok .node file and extracts it at runtime.
405
+ */
406
+
407
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
408
+ import { join } from 'path';
409
+ import { tmpdir } from 'os';
410
+
411
+ // Embed the native ngrok .node file - Bun will include this in the binary
412
+ // @ts-ignore - Bun's file embedding syntax
413
+ import embeddedNgrokPath from '${ngrokNodePath}' with { type: 'file' };
414
+
415
+ // Extract embedded library to temp directory
416
+ const libName = '${ngrokLib}';
417
+ const tempDir = join(tmpdir(), 'ccc-ngrok-lib');
418
+ const extractedLibPath = join(tempDir, libName);
419
+
420
+ if (!existsSync(extractedLibPath)) {
421
+ if (!existsSync(tempDir)) {
422
+ mkdirSync(tempDir, { recursive: true });
423
+ }
424
+ const libData = readFileSync(embeddedNgrokPath);
425
+ writeFileSync(extractedLibPath, libData);
426
+ // Make executable on Unix systems
427
+ try {
428
+ chmodSync(extractedLibPath, 0o755);
429
+ } catch {
430
+ // Ignore chmod errors on Windows
431
+ }
432
+ }
433
+
434
+ // Load the native module using process.dlopen (NAPI compatible)
435
+ const nativeModule: { exports: Record<string, any> } = { exports: {} };
436
+ process.dlopen(nativeModule, extractedLibPath);
437
+
438
+ // Re-export all ngrok functions from the native binding
439
+ const nativeBinding = nativeModule.exports;
440
+
441
+ export const {
442
+ connect,
443
+ forward,
444
+ disconnect,
445
+ kill,
446
+ Listener,
447
+ listeners,
448
+ getListener,
449
+ getListenerByUrl,
450
+ HttpListenerBuilder,
451
+ TcpListenerBuilder,
452
+ TlsListenerBuilder,
453
+ LabeledListenerBuilder,
454
+ loggingCallback,
455
+ authtoken,
456
+ SessionBuilder,
457
+ Session,
458
+ UpdateRequest,
459
+ } = nativeBinding;
460
+
461
+ // Export default for compatibility
462
+ export default nativeBinding;
463
+ `;
464
+
465
+ writeFileSync(ngrokLoaderPath, loaderContent);
466
+ console.log(` Generated ngrok loader for ${ngrokLib}`);
467
+ }
468
+
469
+ /**
470
+ * Clean up the generated loader files
471
+ */
472
+ function cleanupLoaders(): void {
473
+ if (existsSync(ptyLoaderPath)) {
474
+ unlinkSync(ptyLoaderPath);
475
+ }
476
+ if (existsSync(ngrokLoaderPath)) {
477
+ unlinkSync(ngrokLoaderPath);
478
+ }
479
+ }
480
+
481
+ // Build each target
482
+ for (const { name, target, outfile, ptyLib, ngrokLib, ngrokPkg } of selectedTargets) {
483
+ console.log(`\nBuilding ${name}...`);
484
+
485
+ const outfilePath = join(binariesDir, outfile);
486
+
487
+ try {
488
+ // Ensure ngrok platform package is installed (needed for cross-compilation)
489
+ if (!isNgrokPackageInstalled(ngrokPkg, ngrokLib)) {
490
+ const installed = await installNgrokPackage(ngrokPkg, ngrokLib);
491
+ if (!installed) {
492
+ throw new Error(`Failed to install ${ngrokPkg}`);
493
+ }
494
+ }
495
+
496
+ // Verify the .node file exists
497
+ const ngrokNodePath = join(nodeModulesDir, ngrokPkg, ngrokLib);
498
+ const pkgDir = join(nodeModulesDir, ngrokPkg);
499
+
500
+ // Debug: list package contents
501
+ console.log(` Checking ${pkgDir}...`);
502
+ try {
503
+ const { readdirSync } = await import('fs');
504
+ const files = readdirSync(pkgDir);
505
+ console.log(` Package contents: ${files.join(', ')}`);
506
+ } catch (e) {
507
+ console.log(` Could not read directory: ${e}`);
508
+ }
509
+
510
+ if (!existsSync(ngrokNodePath)) {
511
+ throw new Error(`ngrok .node file not found at ${ngrokNodePath}`);
512
+ }
513
+ console.log(` Verified ngrok .node file exists at ${ngrokNodePath}`);
514
+
515
+ // Verify bun-pty native library exists
516
+ const ptyLibPath = join(nodeModulesDir, 'bun-pty', 'rust-pty', 'target', 'release', ptyLib);
517
+ if (!existsSync(ptyLibPath)) {
518
+ throw new Error(`bun-pty native library not found at ${ptyLibPath}`);
519
+ }
520
+ console.log(` Verified bun-pty native library exists at ${ptyLibPath}`);
521
+
522
+ // Generate platform-specific loaders
523
+ generatePtyLoader(ptyLib);
524
+ generateNgrokLoader(ngrokLib, ngrokPkg);
525
+
526
+ // Build the binary (bun-pty and ngrok are now bundled via the generated loaders)
527
+ await $`bun build --compile --minify --bytecode --target=${target} --define BUILD_VERSION='"${VERSION}"' src/index.ts --outfile ${outfilePath}`;
528
+ console.log(` ✓ Built ${outfile}`);
529
+ } catch (error) {
530
+ console.error(` ✗ Failed to build ${name}:`, error);
531
+ cleanupLoaders();
532
+ process.exit(1);
533
+ }
534
+ }
535
+
536
+ // Clean up generated files
537
+ cleanupLoaders();
538
+
539
+ console.log('\nBuild complete!');