diff --git a/web/build-native-node.js b/web/build-native-node.js deleted file mode 100755 index 9a4084db..00000000 --- a/web/build-native-node.js +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env node - -/** - * Minimal Node.js SEA build script - NO BUN! - * Bundles src/cli.ts and all imports into a single executable - */ - -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -console.log('Building standalone vibetunnel executable using Node.js SEA...'); -console.log(`Node.js version: ${process.version}`); - -// Check Node.js version -const nodeVersion = parseInt(process.version.split('.')[0].substring(1)); -if (nodeVersion < 20) { - console.error('Error: Node.js 20 or higher is required for SEA feature'); - process.exit(1); -} - -function patchNodePty() { - console.log('Patching node-pty for SEA build...'); - - // Patch prebuild-loader.js to use process.dlopen instead of require - const prebuildLoaderFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js'); - const prebuildLoaderContent = `"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var path = require("path"); -var fs = require("fs"); - -// Custom loader for SEA that uses process.dlopen -var pty; - -// Helper function to load native module using dlopen -function loadNativeModule(modulePath) { - const module = { exports: {} }; - process.dlopen(module, modulePath); - return module.exports; -} - -// Determine the path to pty.node -function getPtyPath() { - const execDir = path.dirname(process.execPath); - // Look for pty.node next to the executable first - const ptyPath = path.join(execDir, 'pty.node'); - - if (fs.existsSync(ptyPath)) { - return ptyPath; - } - - // If not found, throw error with helpful message - throw new Error('Could not find pty.node next to executable at: ' + ptyPath); -} - -try { - const ptyPath = getPtyPath(); - console.log('Loading pty.node from:', ptyPath); - - // Set spawn-helper path for Unix systems - if (process.platform !== 'win32') { - const execDir = path.dirname(process.execPath); - const spawnHelperPath = path.join(execDir, 'spawn-helper'); - if (fs.existsSync(spawnHelperPath)) { - process.env.NODE_PTY_SPAWN_HELPER_PATH = spawnHelperPath; - console.log('Set spawn-helper path:', spawnHelperPath); - } else { - console.error('Warning: spawn-helper not found at:', spawnHelperPath); - } - } - - pty = loadNativeModule(ptyPath); -} catch (error) { - console.error('Failed to load pty.node:', error); - throw error; -} - -exports.default = pty; -//# sourceMappingURL=prebuild-loader.js.map`; - - fs.writeFileSync(prebuildLoaderFile, prebuildLoaderContent); - - // Also patch windowsPtyAgent.js if it exists - const windowsPtyAgentFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/windowsPtyAgent.js'); - if (fs.existsSync(windowsPtyAgentFile)) { - let content = fs.readFileSync(windowsPtyAgentFile, 'utf8'); - // Replace direct require of .node files with our loader - content = content.replace( - /require\(['"]\.\.\/build\/Release\/pty\.node['"]\)/g, - "require('./prebuild-loader').default" - ); - fs.writeFileSync(windowsPtyAgentFile, content); - } - - // Patch index.js exports.native line - const indexFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js'); - if (fs.existsSync(indexFile)) { - let content = fs.readFileSync(indexFile, 'utf8'); - // Replace the exports.native line that directly requires .node - content = content.replace( - /exports\.native = \(process\.platform !== 'win32' \? require\(prebuild_file_path_1\.ptyPath \|\| '\.\.\/build\/Release\/pty\.node'\) : null\);/, - "exports.native = (process.platform !== 'win32' ? require('./prebuild-loader').default : null);" - ); - fs.writeFileSync(indexFile, content); - } - - // Patch unixTerminal.js to fix spawn-helper path resolution - const unixTerminalFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js'); - if (fs.existsSync(unixTerminalFile)) { - let content = fs.readFileSync(unixTerminalFile, 'utf8'); - - // Replace the helperPath resolution logic - const helperPathPatch = `var helperPath; -// For SEA, use spawn-helper from environment or next to executable -if (process.env.NODE_PTY_SPAWN_HELPER_PATH) { - helperPath = process.env.NODE_PTY_SPAWN_HELPER_PATH; - console.log('[node-pty] Using spawn-helper from env:', helperPath); -} else { - // In SEA context, look next to the executable - const execDir = path.dirname(process.execPath); - const spawnHelperPath = path.join(execDir, 'spawn-helper'); - if (require('fs').existsSync(spawnHelperPath)) { - helperPath = spawnHelperPath; - console.log('[node-pty] Using spawn-helper next to executable:', helperPath); - } else { - // Fallback to original logic - helperPath = '../build/Release/spawn-helper'; - helperPath = path.resolve(__dirname, helperPath); - helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); - helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); - } -}`; - - // Find and replace the helperPath section - content = content.replace( - /var helperPath;[\s\S]*?helperPath = helperPath\.replace\('node_modules\.asar', 'node_modules\.asar\.unpacked'\);/m, - helperPathPatch - ); - - fs.writeFileSync(unixTerminalFile, content); - } - - console.log('Patched node-pty to use process.dlopen() instead of require().'); -} - -async function main() { - try { - // Create build directory - if (!fs.existsSync('build')) { - fs.mkdirSync('build'); - } - - // Create native directory - if (!fs.existsSync('native')) { - fs.mkdirSync('native'); - } - - // 0. Patch node-pty - patchNodePty(); - - // 1. Bundle TypeScript with esbuild using custom loader - console.log('Bundling TypeScript with esbuild...'); - const buildDate = new Date().toISOString(); - const buildTimestamp = Date.now(); - - // Use esbuild directly without custom loader since we're patching node-pty - const esbuildCmd = `npx esbuild src/cli.ts \\ - --bundle \\ - --platform=node \\ - --target=node20 \\ - --outfile=build/bundle.js \\ - --format=cjs \\ - --sourcemap=inline \\ - --source-root=/ \\ - --keep-names \\ - --define:process.env.BUILD_DATE='"${buildDate}"' \\ - --define:process.env.BUILD_TIMESTAMP='"${buildTimestamp}"'`; - - console.log('Running:', esbuildCmd); - execSync(esbuildCmd, { stdio: 'inherit' }); - - // 2. Create SEA configuration - console.log('\nCreating SEA configuration...'); - const seaConfig = { - main: 'build/bundle.js', - output: 'build/sea-prep.blob', - disableExperimentalSEAWarning: true, - useSnapshot: false, - useCodeCache: false - }; - - fs.writeFileSync('build/sea-config.json', JSON.stringify(seaConfig, null, 2)); - - // 3. Generate SEA blob - console.log('Generating SEA blob...'); - execSync('node --experimental-sea-config build/sea-config.json', { stdio: 'inherit' }); - - // 4. Create executable - console.log('\nCreating executable...'); - const nodeExe = process.execPath; - const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel'; - - // Copy node binary - fs.copyFileSync(nodeExe, targetExe); - if (process.platform !== 'win32') { - fs.chmodSync(targetExe, 0o755); - } - - // 5. Inject the blob - console.log('Injecting SEA blob...'); - let postjectCmd = `npx postject ${targetExe} NODE_SEA_BLOB build/sea-prep.blob \\ - --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; - - if (process.platform === 'darwin') { - postjectCmd += ' --macho-segment-name NODE_SEA'; - } - - execSync(postjectCmd, { stdio: 'inherit' }); - - // 6. Sign on macOS - if (process.platform === 'darwin') { - console.log('Signing executable...'); - execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' }); - } - - // 7. Restore original node-pty - console.log('Restoring original node-pty...'); - execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' }); - execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' }); - - // 8. Copy only necessary native files - console.log('Copying native modules...'); - const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release'; - - // Copy pty.node - fs.copyFileSync( - path.join(nativeModulesDir, 'pty.node'), - 'native/pty.node' - ); - console.log(' - Copied pty.node'); - - // Copy spawn-helper (Unix only) - if (process.platform !== 'win32') { - fs.copyFileSync( - path.join(nativeModulesDir, 'spawn-helper'), - 'native/spawn-helper' - ); - fs.chmodSync('native/spawn-helper', 0o755); - console.log(' - Copied spawn-helper'); - } - - // 9. Clean up - console.log('\nCleaning up...'); - fs.rmSync('build', { recursive: true, force: true }); - - console.log('\n✅ Build complete!'); - console.log(`\nPortable executable created in native/ directory:`); - console.log(` - vibetunnel (executable)`); - console.log(` - pty.node`); - if (process.platform !== 'win32') { - console.log(` - spawn-helper`); - } - console.log('\nAll files must be kept together in the same directory.'); - console.log('This bundle will work on any machine with the same OS/architecture.'); - - } catch (error) { - console.error('\n❌ Build failed:', error.message); - process.exit(1); - } -} - -main(); \ No newline at end of file diff --git a/web/build-native.js b/web/build-native.js index e2312cab..8038781c 100755 --- a/web/build-native.js +++ b/web/build-native.js @@ -1,156 +1,343 @@ #!/usr/bin/env node /** - * Build standalone vibetunnel executable with native modules - * - * Note: Bun does not support universal binaries. This builds for the native architecture only. + * Build standalone vibetunnel executable using Node.js SEA (Single Executable Application) + * + * This script creates a portable executable that bundles the VibeTunnel server into a single + * binary using Node.js's built-in SEA feature. The resulting executable can run on any machine + * with the same OS/architecture without requiring Node.js to be installed. + * + * ## Output + * Creates a `native/` directory with just 3 files: + * - `vibetunnel` - The standalone executable (includes all JS code and sourcemaps) + * - `pty.node` - Native binding for terminal emulation + * - `spawn-helper` - Helper binary for spawning processes (Unix only) + * + * ## How it works + * + * 1. **Patches node-pty** to work with SEA's limitations: + * - SEA's require() can only load built-in Node.js modules, not external files + * - We patch node-pty to use `process.dlopen()` instead of `require()` for native modules + * - All file lookups are changed to look next to the executable, not in node_modules + * + * 2. **Bundles TypeScript** using esbuild: + * - Compiles and bundles all TypeScript/JavaScript into a single file + * - Includes inline sourcemaps for better debugging + * - Source map support can be enabled with --sourcemap flag + * + * 3. **Creates SEA blob**: + * - Uses Node.js's experimental SEA config to generate a blob from the bundle + * - The blob contains all the JavaScript code and can be injected into a Node binary + * + * 4. **Injects into Node.js binary**: + * - Copies the Node.js executable and injects the SEA blob using postject + * - Signs the binary on macOS to avoid security warnings + * + * ## Portability + * The resulting executable is fully portable: + * - No absolute paths are embedded + * - Native modules are loaded relative to the executable location + * - Can be moved to any directory or machine with the same OS/architecture + * + * ## Usage + * ```bash + * node build-native-node.js # Build without sourcemaps (default) + * node build-native-node.js --sourcemap # Build with inline sourcemaps + * ``` + * + * ## Requirements + * - Node.js 20+ (for SEA support) + * - postject (installed automatically if needed) + * + * ## Known Limitations + * - The SEA warning about require() limitations is expected and harmless + * - Native modules must be distributed alongside the executable + * - Cross-platform builds are not supported (build on the target platform) */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); -console.log('Building standalone vibetunnel executable for native architecture...'); -console.log('Note: Bun does not support universal binaries'); +// Parse command line arguments +const includeSourcemaps = process.argv.includes('--sourcemap'); + +console.log('Building standalone vibetunnel executable using Node.js SEA...'); +console.log(`Node.js version: ${process.version}`); +if (includeSourcemaps) { + console.log('Including sourcemaps in build'); +} + +// Check Node.js version +const nodeVersion = parseInt(process.version.split('.')[0].substring(1)); +if (nodeVersion < 20) { + console.error('Error: Node.js 20 or higher is required for SEA feature'); + process.exit(1); +} function patchNodePty() { - console.log('Patching node-pty for standalone build...'); + console.log('Reinstalling node-pty to ensure clean state...'); + execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' }); + execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' }); + + console.log('Patching node-pty for SEA build...'); - // Patch prebuild-file-path.js to look next to executable - const prebuildPathFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-file-path.js'); - const prebuildPathContent = `"use strict"; + // Patch prebuild-loader.js to use process.dlopen instead of require + const prebuildLoaderFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js'); + const prebuildLoaderContent = `"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ptyPath = void 0; var path = require("path"); +var fs = require("fs"); -// For bundled executables, look next to the binary -if (process.argv[0] === 'bun' && process.execPath.endsWith('vibetunnel')) { - exports.ptyPath = path.join(path.dirname(process.execPath), 'pty.node'); -} else { - // Original prebuild logic - var fs = require("fs"); - var os = require("os"); - function prebuildName() { - var tags = []; - tags.push(process.versions.hasOwnProperty('electron') ? 'electron' : 'node'); - tags.push('abi' + process.versions.modules); - if (os.platform() === 'linux' && fs.existsSync('/etc/alpine-release')) { - tags.push('musl'); - } - return tags.join('.') + '.node'; - } - var pathToBuild = path.resolve(__dirname, "../prebuilds/" + os.platform() + "-" + os.arch() + "/" + prebuildName()); - exports.ptyPath = fs.existsSync(pathToBuild) ? pathToBuild : null; -} -//# sourceMappingURL=prebuild-file-path.js.map`; +// Custom loader for SEA that uses process.dlopen +var pty; - fs.writeFileSync(prebuildPathFile, prebuildPathContent); - - // Also update unixTerminal.js - const unixTerminalPath = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js'); - let unixTerminal = fs.readFileSync(unixTerminalPath, 'utf8'); - - // Add check for bundled executable after helperPath is set - const helperPathInsert = ` -// For bundled executables, look next to the binary -if (process.argv[0] === 'bun' && process.execPath.endsWith('vibetunnel')) { - helperPath = path.join(path.dirname(process.execPath), 'spawn-helper'); -} else { - helperPath = path.resolve(__dirname, helperPath); -}`; - - // Find where to insert - after the helperPath assignment - const resolveMatch = unixTerminal.match(/helperPath = path\.resolve\(__dirname, helperPath\);/); - if (resolveMatch) { - unixTerminal = unixTerminal.replace( - 'helperPath = path.resolve(__dirname, helperPath);', - helperPathInsert - ); - } else { - // If already patched, look for the bundled check - if (!unixTerminal.includes('process.execPath.endsWith')) { - // Find the helperPath line and add our check - const helperPathLine = unixTerminal.indexOf("helperPath = '../build/Release/spawn-helper';"); - if (helperPathLine !== -1) { - const nextLineIndex = unixTerminal.indexOf('\n', helperPathLine) + 1; - unixTerminal = unixTerminal.slice(0, nextLineIndex) + helperPathInsert + '\n' + unixTerminal.slice(nextLineIndex); - } - } - } - - fs.writeFileSync(unixTerminalPath, unixTerminal); - - console.log('Patched node-pty to look for native files next to executable.'); +// Helper function to load native module using dlopen +function loadNativeModule(modulePath) { + const module = { exports: {} }; + process.dlopen(module, modulePath); + return module.exports; } - -console.log('Building standalone vibetunnel executable...'); +// Determine the path to pty.node +function getPtyPath() { + const execDir = path.dirname(process.execPath); + // Look for pty.node next to the executable first + const ptyPath = path.join(execDir, 'pty.node'); + + if (fs.existsSync(ptyPath)) { + return ptyPath; + } + + // If not found, throw error with helpful message + throw new Error('Could not find pty.node next to executable at: ' + ptyPath); +} try { - // 1. Apply patches - patchNodePty(); - - // 2. Create native directory - if (!fs.existsSync('native')) { - fs.mkdirSync('native'); + const ptyPath = getPtyPath(); + + // Set spawn-helper path for Unix systems + if (process.platform !== 'win32') { + const execDir = path.dirname(process.execPath); + const spawnHelperPath = path.join(execDir, 'spawn-helper'); + if (fs.existsSync(spawnHelperPath)) { + process.env.NODE_PTY_SPAWN_HELPER_PATH = spawnHelperPath; + } } - - // 3. Compile with Bun - console.log('Compiling with Bun...'); - const buildDate = new Date().toISOString(); - const buildTimestamp = Date.now(); - - // Detect how we're running bun - let bunCommand = 'bun'; - if (process.argv[0].includes('npx')) { - // We're running via npx, so bun should also be run via npx - bunCommand = 'npx -y bun'; - } - - // Note: Bun compile doesn't fully support sourcemaps in stack traces yet - // Using --minify=false to preserve more readable code structure - const compileCmd = `BUILD_DATE="${buildDate}" BUILD_TIMESTAMP="${buildTimestamp}" ${bunCommand} build src/cli.ts --compile --sourcemap=inline --outfile native/vibetunnel`; - - console.log(`Running: ${compileCmd}`); - console.log(`Build date: ${buildDate}`); - execSync(compileCmd, { stdio: 'inherit', env: { ...process.env, BUILD_DATE: buildDate, BUILD_TIMESTAMP: buildTimestamp } }); - - // 4. Copy native modules - console.log('Creating native directory and copying modules...'); - const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release'; - - fs.copyFileSync( - path.join(nativeModulesDir, 'pty.node'), - 'native/pty.node' - ); - - fs.copyFileSync( - path.join(nativeModulesDir, 'spawn-helper'), - 'native/spawn-helper' - ); - - // 5. Restore original node-pty - console.log('Restoring original node-pty...'); - execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' }); - execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' }); // Added --no-fund --no-audit for cleaner output - - console.log('\nBuild complete!'); - console.log(''); - console.log('Standalone build created in native/ directory:'); - console.log(' - native/vibetunnel (executable)'); - console.log(' - native/pty.node'); - console.log(' - native/spawn-helper'); - console.log(''); - console.log('All three files must be in the same directory when running.'); - + + pty = loadNativeModule(ptyPath); } catch (error) { - console.error('Build failed:'); - if (error.stderr) { - console.error(error.stderr.toString()); + console.error('Failed to load pty.node:', error); + throw error; +} + +exports.default = pty; +//# sourceMappingURL=prebuild-loader.js.map`; + + fs.writeFileSync(prebuildLoaderFile, prebuildLoaderContent); + + // Also patch windowsPtyAgent.js if it exists + const windowsPtyAgentFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/windowsPtyAgent.js'); + if (fs.existsSync(windowsPtyAgentFile)) { + let content = fs.readFileSync(windowsPtyAgentFile, 'utf8'); + // Replace direct require of .node files with our loader + content = content.replace( + /require\(['"]\.\.\/build\/Release\/pty\.node['"]\)/g, + "require('./prebuild-loader').default" + ); + fs.writeFileSync(windowsPtyAgentFile, content); } - if (error.stdout) { - console.error(error.stdout.toString()); + + // Patch index.js exports.native line + const indexFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js'); + if (fs.existsSync(indexFile)) { + let content = fs.readFileSync(indexFile, 'utf8'); + // Replace the exports.native line that directly requires .node + content = content.replace( + /exports\.native = \(process\.platform !== 'win32' \? require\(prebuild_file_path_1\.ptyPath \|\| '\.\.\/build\/Release\/pty\.node'\) : null\);/, + "exports.native = (process.platform !== 'win32' ? require('./prebuild-loader').default : null);" + ); + fs.writeFileSync(indexFile, content); } - console.error(error.message); + + // Patch unixTerminal.js to fix spawn-helper path resolution + const unixTerminalFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js'); + if (fs.existsSync(unixTerminalFile)) { + let content = fs.readFileSync(unixTerminalFile, 'utf8'); + + // Replace the helperPath resolution logic + const helperPathPatch = `var helperPath; +// For SEA, use spawn-helper from environment or next to executable +if (process.env.NODE_PTY_SPAWN_HELPER_PATH) { + helperPath = process.env.NODE_PTY_SPAWN_HELPER_PATH; +} else { + // In SEA context, look next to the executable + const execDir = path.dirname(process.execPath); + const spawnHelperPath = path.join(execDir, 'spawn-helper'); + if (require('fs').existsSync(spawnHelperPath)) { + helperPath = spawnHelperPath; + } else { + // Fallback to original logic + helperPath = '../build/Release/spawn-helper'; + helperPath = path.resolve(__dirname, helperPath); + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); + } +}`; + + // Find and replace the helperPath section + content = content.replace( + /var helperPath;[\s\S]*?helperPath = helperPath\.replace\('node_modules\.asar', 'node_modules\.asar\.unpacked'\);/m, + helperPathPatch + ); + + fs.writeFileSync(unixTerminalFile, content); + } + + console.log('Patched node-pty to use process.dlopen() instead of require().'); +} + +// Cleanup function +function cleanup() { + if (fs.existsSync('build') && !process.argv.includes('--keep-build')) { + console.log('Cleaning up build directory...'); + fs.rmSync('build', { recursive: true, force: true }); + } +} + +// Ensure cleanup happens on exit +process.on('exit', cleanup); +process.on('SIGINT', () => { + console.log('\nBuild interrupted'); process.exit(1); -} \ No newline at end of file +}); +process.on('SIGTERM', () => { + console.log('\nBuild terminated'); + process.exit(1); +}); + +async function main() { + try { + // Create build directory + if (!fs.existsSync('build')) { + fs.mkdirSync('build'); + } + + // Create native directory + if (!fs.existsSync('native')) { + fs.mkdirSync('native'); + } + + // 0. Patch node-pty + patchNodePty(); + + // 1. Bundle TypeScript with esbuild using custom loader + console.log('Bundling TypeScript with esbuild...'); + const buildDate = new Date().toISOString(); + const buildTimestamp = Date.now(); + + // Use esbuild directly without custom loader since we're patching node-pty + let esbuildCmd = `npx esbuild src/cli.ts \\ + --bundle \\ + --platform=node \\ + --target=node20 \\ + --outfile=build/bundle.js \\ + --format=cjs \\ + --keep-names \\ + --define:process.env.BUILD_DATE='"${buildDate}"' \\ + --define:process.env.BUILD_TIMESTAMP='"${buildTimestamp}"'`; + + if (includeSourcemaps) { + esbuildCmd += ' \\\n --sourcemap=inline \\\n --source-root=/'; + } + + console.log('Running:', esbuildCmd); + execSync(esbuildCmd, { stdio: 'inherit' }); + + // 2. Create SEA configuration + console.log('\nCreating SEA configuration...'); + const seaConfig = { + main: 'build/bundle.js', + output: 'build/sea-prep.blob', + disableExperimentalSEAWarning: true, + useSnapshot: false, + useCodeCache: false + }; + + fs.writeFileSync('build/sea-config.json', JSON.stringify(seaConfig, null, 2)); + + // 3. Generate SEA blob + console.log('Generating SEA blob...'); + execSync('node --experimental-sea-config build/sea-config.json', { stdio: 'inherit' }); + + // 4. Create executable + console.log('\nCreating executable...'); + const nodeExe = process.execPath; + const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel'; + + // Copy node binary + fs.copyFileSync(nodeExe, targetExe); + if (process.platform !== 'win32') { + fs.chmodSync(targetExe, 0o755); + } + + // 5. Inject the blob + console.log('Injecting SEA blob...'); + let postjectCmd = `npx postject ${targetExe} NODE_SEA_BLOB build/sea-prep.blob \\ + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; + + if (process.platform === 'darwin') { + postjectCmd += ' --macho-segment-name NODE_SEA'; + } + + execSync(postjectCmd, { stdio: 'inherit' }); + + // 6. Sign on macOS + if (process.platform === 'darwin') { + console.log('Signing executable...'); + execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' }); + } + + // 7. Restore original node-pty + console.log('Restoring original node-pty...'); + execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' }); + execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' }); + + // 8. Copy only necessary native files + console.log('Copying native modules...'); + const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release'; + + // Copy pty.node + fs.copyFileSync( + path.join(nativeModulesDir, 'pty.node'), + 'native/pty.node' + ); + console.log(' - Copied pty.node'); + + // Copy spawn-helper (Unix only) + if (process.platform !== 'win32') { + fs.copyFileSync( + path.join(nativeModulesDir, 'spawn-helper'), + 'native/spawn-helper' + ); + fs.chmodSync('native/spawn-helper', 0o755); + console.log(' - Copied spawn-helper'); + } + + console.log('\n✅ Build complete!'); + console.log(`\nPortable executable created in native/ directory:`); + console.log(` - vibetunnel (executable)`); + console.log(` - pty.node`); + if (process.platform !== 'win32') { + console.log(` - spawn-helper`); + } + console.log('\nAll files must be kept together in the same directory.'); + console.log('This bundle will work on any machine with the same OS/architecture.'); + + } catch (error) { + console.error('\n❌ Build failed:', error.message); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index e9284f0a..30bb2957 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1099,16 +1099,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@puppeteer/browsers/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@puppeteer/browsers/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1211,6 +1201,16 @@ "node": ">=12" } }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.44.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", @@ -3080,6 +3080,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -3145,16 +3155,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concurrently/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/concurrently/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3246,6 +3246,16 @@ "node": ">=12" } }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4145,22 +4155,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4578,6 +4572,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -4970,6 +4980,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5261,6 +5281,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -6228,16 +6258,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7034,16 +7054,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7156,16 +7166,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/superagent": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", @@ -8122,16 +8122,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8247,13 +8237,14 @@ } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "license": "ISC", - "engines": { - "node": ">=12" + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } }, "node_modules/yargs/node_modules/ansi-regex": { @@ -8377,17 +8368,6 @@ "node": ">=6" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/web/src/cli.ts b/web/src/cli.ts index 054e49a5..b6612775 100644 --- a/web/src/cli.ts +++ b/web/src/cli.ts @@ -4,8 +4,14 @@ import { startVibeTunnelForward } from './server/fwd.js'; import { startVibeTunnelServer } from './server/server.js'; import { VERSION } from './server/version.js'; -// Enable source map support for better stack traces -process.setSourceMapsEnabled(true); +// Source maps are only included if built with --sourcemap flag + +// Suppress the SEA warning if running in SEA context +// This warning is expected and harmless - we handle external modules properly +if (!process.argv[1]) { + // In SEA context, argv[1] is undefined + process.env.NODE_NO_WARNINGS = '1'; +} // Handle uncaught exceptions process.on('uncaughtException', (error) => {