Add logic to use custom node compiler

This commit is contained in:
Peter Steinberger 2025-06-22 11:50:15 +02:00
parent dbe280a68e
commit 180caf7e81
14 changed files with 812 additions and 335 deletions

115
docs/custom-node.md Normal file
View file

@ -0,0 +1,115 @@
# Custom Node.js Build
## Motivation
VibeTunnel uses Node.js Single Executable Applications (SEA) to create a standalone terminal server. However, the standard Node.js binary is quite large:
- **Standard Node.js binary**: ~110MB
- **Custom minimal Node.js**: ~43MB (61% reduction)
- **Final executable size**: ~45MB (down from ~105MB)
- **Final app size impact**: Reduces app from ~130MB to ~88MB
We don't need many Node.js features for VibeTunnel:
- No internationalization (ICU) support needed
- No npm package manager in the binary
- No inspector/debugging protocol
- No V8 snapshots or code cache
By building a custom Node.js without these features, we achieve a significantly smaller app bundle while maintaining full functionality.
## Build Behavior
### Debug Mode (Xcode)
- Uses system Node.js for faster iteration
- No custom Node.js compilation required
- Build output shows: `"Debug build - using system Node.js for faster builds"`
- If a custom Node.js was previously built, it will be reused for consistency
### Release Mode (Xcode)
- Automatically builds custom minimal Node.js on first run
- Compilation takes 10-20 minutes but is cached for future builds
- Uses the custom Node.js to create a smaller executable
- Build output shows version and size comparison
## Build Automation
### Release Builds
The release script (`mac/scripts/release.sh`) automatically checks for and builds custom Node.js if needed. You don't need to manually build it before releases.
### Manual Custom Node.js Build
To build the custom Node.js manually (outside of Xcode):
```bash
cd web
node build-custom-node.js --latest
```
This will:
1. Download the latest Node.js source
2. Configure it without unnecessary features
3. Build with optimizations (`-Os`, `-flto`, etc.)
4. Cache the result in `web/.node-builds/`
To use the custom Node.js for building the executable:
```bash
cd web
npm run build -- --custom-node
```
Or directly:
```bash
node build-native.js --custom-node
```
## Build Process Details
### Automatic Detection
The build system automatically searches for custom Node.js builds in `.node-builds/` when `--custom-node` is passed without a path. It finds the most recent build by checking directory modification times.
### Code Signing on macOS
When building the executable:
1. The Node.js binary is injected with our JavaScript code (SEA process)
2. The binary is stripped to remove debug symbols
3. The executable is re-signed with an ad-hoc signature
Note: You may see a warning about "invalidating the code signature" during the strip process - this is expected and harmless since we re-sign immediately after.
## Technical Details
### Features Disabled
- `--without-intl` - Removes internationalization support
- `--without-npm` - Excludes npm from the binary
- `--without-corepack` - Removes package manager wrapper
- `--without-inspector` - Disables debugging protocol
- `--without-node-snapshot` - Skips V8 snapshot (~2-3MB)
- `--without-node-code-cache` - Skips code cache (~1-2MB)
### Optimization Flags
- `-Os` - Optimize for size
- `-flto` - Link-time optimization
- `-ffunction-sections` / `-fdata-sections` - Enable dead code elimination
- `-Wl,-dead_strip` - Remove unused code at link time
### Build Cache
Custom Node.js builds are stored in `web/.node-builds/` and are excluded from git via `.gitignore`. The build system automatically detects and reuses existing builds.
## File Locations
- Build script: `web/build-custom-node.js`
- Native executable builder: `web/build-native.js`
- Xcode integration: `mac/scripts/build-web-frontend.sh`
- Build output: `web/.node-builds/node-v*-minimal/`
- Final executable: `web/native/vibetunnel`
## Troubleshooting
### Custom Node.js not detected
- Ensure the build completed successfully: check for `.node-builds/node-v*-minimal/out/Release/node`
- In Debug mode, the system will use custom Node.js if already built
- In Release mode, it will build custom Node.js automatically if not present
### Code signature warnings
The warning "changes being made to the file will invalidate the code signature" is expected and handled automatically. The build process re-signs the executable after all modifications.

View file

@ -251,7 +251,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "# Build web frontend conditionally based on hash\necho \"Checking if web frontend needs rebuild...\"\n\n# Run the conditional build script\n\"${SRCROOT}/scripts/build-web-frontend-conditional.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Web frontend build failed\"\n exit 1\nfi\n";
shellScript = "# Build web frontend conditionally based on hash\necho \"Checking if web frontend needs rebuild...\"\n\n# Run the conditional build script\n\"${SRCROOT}/scripts/build-web-frontend.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Web frontend build failed\"\n exit 1\nfi\n";
};
C3D4E5F6A7B8C9D0E1F23456 /* Install Build Dependencies */ = {
isa = PBXShellScriptBuildPhase;
@ -270,7 +270,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "# Install build dependencies\necho \"Checking build dependencies...\"\n\n# Run the install script\n\"${SRCROOT}/scripts/install-bun.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Failed to install build dependencies\"\n exit 1\nfi\n";
shellScript = "# Check for Node.js availability\necho \"Checking build dependencies...\"\n\n# Run the install script\n\"${SRCROOT}/scripts/install-node.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Node.js is required to build VibeTunnel\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */

View file

@ -1,152 +0,0 @@
#!/bin/bash
#
# Build and copy Node.js SEA executable and native modules to the app bundle
# ARM64 only - VibeTunnel requires Apple Silicon
#
set -euo pipefail
# Add common Node.js installation paths to PATH
# Homebrew on Apple Silicon
if [ -d "/opt/homebrew/bin" ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
# Homebrew on Intel Macs
if [ -d "/usr/local/bin" ]; then
export PATH="/usr/local/bin:$PATH"
fi
# NVM default location
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
fi
# Node Version Manager (n)
if [ -d "/usr/local/n/versions" ]; then
export PATH="/usr/local/bin:$PATH"
fi
# MacPorts
if [ -d "/opt/local/bin" ]; then
export PATH="/opt/local/bin:$PATH"
fi
# Export CI environment variable to prevent interactive prompts
export CI=true
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$SCRIPT_DIR/../.."
WEB_DIR="$PROJECT_ROOT/web"
NATIVE_DIR="$WEB_DIR/native"
# Destination from Xcode (passed as argument or use BUILT_PRODUCTS_DIR)
if [ $# -eq 0 ]; then
if [ -z "${BUILT_PRODUCTS_DIR:-}" ]; then
echo -e "${RED}Error: No destination path provided and BUILT_PRODUCTS_DIR not set${NC}"
exit 1
fi
DEST_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
else
DEST_RESOURCES="$1"
fi
echo -e "${GREEN}Building and copying Node.js SEA executable (ARM64 only)...${NC}"
# Change to web directory
cd "$WEB_DIR"
# Check if native directory exists, if not use prebuilts
if [ ! -d "$NATIVE_DIR" ] || [ ! -f "$NATIVE_DIR/vibetunnel" ]; then
echo -e "${YELLOW}Native directory not found. Checking for prebuilt binaries...${NC}"
# Use prebuilt binaries if available
PREBUILTS_DIR="$PROJECT_ROOT/mac/Resources/BunPrebuilts"
ARCH=$(uname -m)
if [ "$ARCH" != "arm64" ]; then
echo -e "${RED}Error: VibeTunnel requires Apple Silicon (ARM64)${NC}"
exit 1
fi
if [ -d "$PREBUILTS_DIR/arm64" ] && [ -f "$PREBUILTS_DIR/arm64/vibetunnel" ]; then
echo -e "${GREEN}Using prebuilt binaries for ARM64${NC}"
mkdir -p "$NATIVE_DIR"
cp "$PREBUILTS_DIR/arm64/vibetunnel" "$NATIVE_DIR/"
cp "$PREBUILTS_DIR/arm64/pty.node" "$NATIVE_DIR/"
cp "$PREBUILTS_DIR/arm64/spawn-helper" "$NATIVE_DIR/"
chmod +x "$NATIVE_DIR/vibetunnel"
chmod +x "$NATIVE_DIR/spawn-helper"
else
# Try to build with Node.js
echo -e "${YELLOW}Prebuilt binaries not found. Attempting to build...${NC}"
# Check if build-native.js exists
if [ -f "build-native.js" ]; then
# Use Node.js to build
if command -v node &> /dev/null; then
echo "Using Node.js to build SEA executable..."
echo "Using Node.js version: $(node --version)"
echo "PATH: $PATH"
node build-native.js
else
echo -e "${RED}Error: Node.js not found in PATH${NC}"
echo -e "${RED}PATH is: $PATH${NC}"
echo -e "${RED}Please install Node.js 20+ or ensure prebuilt binaries are available in:${NC}"
echo -e "${RED} $PREBUILTS_DIR/arm64/${NC}"
exit 1
fi
else
echo -e "${RED}Error: build-native.js not found and no prebuilt binaries available${NC}"
exit 1
fi
fi
fi
# Verify native files exist
if [ ! -f "$NATIVE_DIR/vibetunnel" ]; then
echo -e "${RED}Error: Executable not found at $NATIVE_DIR/vibetunnel${NC}"
exit 1
fi
# Copy executable
echo "Copying executable to app bundle..."
cp "$NATIVE_DIR/vibetunnel" "$DEST_RESOURCES/"
chmod +x "$DEST_RESOURCES/vibetunnel"
# Copy native modules
if [ -f "$NATIVE_DIR/pty.node" ]; then
echo "Copying pty.node..."
cp "$NATIVE_DIR/pty.node" "$DEST_RESOURCES/"
else
echo -e "${RED}Error: pty.node not found${NC}"
exit 1
fi
if [ -f "$NATIVE_DIR/spawn-helper" ]; then
echo "Copying spawn-helper..."
cp "$NATIVE_DIR/spawn-helper" "$DEST_RESOURCES/"
chmod +x "$DEST_RESOURCES/spawn-helper"
else
echo -e "${RED}Error: spawn-helper not found${NC}"
exit 1
fi
echo -e "${GREEN}✓ Executable and native modules copied successfully${NC}"
# Verify the files
echo "Verifying copied files:"
ls -la "$DEST_RESOURCES/vibetunnel" || echo "vibetunnel not found!"
ls -la "$DEST_RESOURCES/pty.node" || echo "pty.node not found!"
ls -la "$DEST_RESOURCES/spawn-helper" || echo "spawn-helper not found!"
echo ""
echo -e "${GREEN}Note: VibeTunnel requires Apple Silicon (M1/M2/M3) Macs.${NC}"

View file

@ -1,151 +0,0 @@
#!/bin/zsh
set -e # Exit on any error
# Get the project directory
if [ -z "${SRCROOT}" ]; then
# If SRCROOT is not set (running outside Xcode), determine it from script location
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
else
PROJECT_DIR="${SRCROOT}"
fi
WEB_DIR="${PROJECT_DIR}/../web"
HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash"
PREVIOUS_HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash.previous"
PUBLIC_DIR="${WEB_DIR}/public"
# Set destination directory
if [ -z "${BUILT_PRODUCTS_DIR}" ]; then
# Default for testing outside Xcode
DEST_DIR="/tmp/vibetunnel-web-build"
else
DEST_DIR="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public"
fi
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
# Read the current hash
if [ -f "${HASH_FILE}" ]; then
CURRENT_HASH=$(cat "${HASH_FILE}")
else
echo "error: Hash file not found. Run 'Calculate Web Hash' build phase first."
exit 1
fi
# Check if we need to rebuild
NEED_REBUILD=1
# Check if previous hash exists and matches current
if [ -f "${PREVIOUS_HASH_FILE}" ]; then
PREVIOUS_HASH=$(cat "${PREVIOUS_HASH_FILE}")
if [ "${CURRENT_HASH}" = "${PREVIOUS_HASH}" ]; then
# Also check if the built files actually exist
if [ -d "${DEST_DIR}" ] && [ -f "${APP_RESOURCES}/vibetunnel" ] && [ -f "${APP_RESOURCES}/pty.node" ] && [ -f "${APP_RESOURCES}/spawn-helper" ]; then
echo "Web content unchanged and build outputs exist. Skipping rebuild."
NEED_REBUILD=0
else
echo "Web content unchanged but build outputs missing. Rebuilding..."
fi
else
echo "Web content changed. Hash: ${PREVIOUS_HASH} -> ${CURRENT_HASH}"
fi
else
echo "No previous build hash found. Building web frontend..."
fi
if [ ${NEED_REBUILD} -eq 0 ]; then
echo "Skipping web frontend build (no changes detected)"
exit 0
fi
echo "Building web frontend..."
# Setup PATH for Node.js
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
# Load NVM if available
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
fi
# Put volta on the path if it exists
export PATH="$HOME/.volta/bin:$PATH"
# Trigger rehash
rehash
# Export CI to prevent interactive prompts
export CI=true
# Check if npm is available
if ! command -v npm &> /dev/null; then
echo "error: npm not found. Please install Node.js"
exit 1
fi
echo "Using npm version: $(npm --version)"
echo "Using Node.js version: $(node --version)"
# Check if web directory exists
if [ ! -d "${WEB_DIR}" ]; then
echo "error: Web directory not found at ${WEB_DIR}"
exit 1
fi
# Change to web directory
cd "${WEB_DIR}"
# Clean build artifacts
echo "Cleaning build artifacts..."
rm -rf dist public/bundle public/output.css
# Install dependencies
echo "Installing dependencies..."
npm install
# Build the web frontend
echo "Building web frontend..."
npm run build
# Clean and create destination directory
echo "Cleaning destination directory..."
rm -rf "${DEST_DIR}"
mkdir -p "${DEST_DIR}"
# Copy built files to Resources
echo "Copying web files to app bundle..."
cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/"
# Copy native executable and modules to app bundle root
echo "Copying native executable and modules..."
NATIVE_DIR="${WEB_DIR}/native"
if [ -f "${NATIVE_DIR}/vibetunnel" ]; then
cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/"
chmod +x "${APP_RESOURCES}/vibetunnel"
else
echo "error: vibetunnel executable not found"
exit 1
fi
if [ -f "${NATIVE_DIR}/pty.node" ]; then
cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/"
else
echo "error: pty.node not found"
exit 1
fi
if [ -f "${NATIVE_DIR}/spawn-helper" ]; then
cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/"
chmod +x "${APP_RESOURCES}/spawn-helper"
else
echo "error: spawn-helper not found"
exit 1
fi
# Save the current hash as the previous hash for next build
cp "${HASH_FILE}" "${PREVIOUS_HASH_FILE}"
echo "Web frontend build completed successfully"

View file

@ -1,8 +1,6 @@
#!/bin/zsh
set -e # Exit on any error
echo "Building web frontend..."
# Get the project directory
if [ -z "${SRCROOT}" ]; then
# If SRCROOT is not set (running outside Xcode), determine it from script location
@ -13,6 +11,8 @@ else
fi
WEB_DIR="${PROJECT_DIR}/../web"
HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash"
PREVIOUS_HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash.previous"
PUBLIC_DIR="${WEB_DIR}/public"
# Set destination directory
@ -23,6 +23,44 @@ else
DEST_DIR="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public"
fi
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
# Read the current hash
if [ -f "${HASH_FILE}" ]; then
CURRENT_HASH=$(cat "${HASH_FILE}")
else
echo "error: Hash file not found. Run 'Calculate Web Hash' build phase first."
exit 1
fi
# Check if we need to rebuild
NEED_REBUILD=1
# Check if previous hash exists and matches current
if [ -f "${PREVIOUS_HASH_FILE}" ]; then
PREVIOUS_HASH=$(cat "${PREVIOUS_HASH_FILE}")
if [ "${CURRENT_HASH}" = "${PREVIOUS_HASH}" ]; then
# Also check if the built files actually exist
if [ -d "${DEST_DIR}" ] && [ -f "${APP_RESOURCES}/vibetunnel" ] && [ -f "${APP_RESOURCES}/pty.node" ] && [ -f "${APP_RESOURCES}/spawn-helper" ]; then
echo "Web content unchanged and build outputs exist. Skipping rebuild."
NEED_REBUILD=0
else
echo "Web content unchanged but build outputs missing. Rebuilding..."
fi
else
echo "Web content changed. Hash: ${PREVIOUS_HASH} -> ${CURRENT_HASH}"
fi
else
echo "No previous build hash found. Building web frontend..."
fi
if [ ${NEED_REBUILD} -eq 0 ]; then
echo "Skipping web frontend build (no changes detected)"
exit 0
fi
echo "Building web frontend..."
# Setup PATH for Node.js
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
@ -55,15 +93,56 @@ cd "${WEB_DIR}"
# Clean build artifacts
echo "Cleaning build artifacts..."
rm -rf dist public/bundle public/output.css
rm -rf dist public/bundle public/output.css native
# Install dependencies
echo "Installing dependencies..."
npm install
# Determine build configuration
BUILD_CONFIG="${CONFIGURATION:-Debug}"
echo "Build configuration: $BUILD_CONFIG"
# Check for custom Node.js build
CUSTOM_NODE_PATH=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
# Build the web frontend
echo "Building web frontend..."
npm run build
if [ "$BUILD_CONFIG" = "Release" ]; then
echo "Release build - checking for custom Node.js..."
if [ ! -f "$CUSTOM_NODE_PATH" ]; then
echo "Custom Node.js not found, building it for optimal size..."
echo "This will take 10-20 minutes on first run but will be cached."
node build-custom-node.js --latest
CUSTOM_NODE_PATH=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
fi
if [ -f "$CUSTOM_NODE_PATH" ]; then
CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown")
CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" 2>/dev/null | awk '{print $5}' || echo "unknown")
echo "Using custom Node.js for release build:"
echo " Version: $CUSTOM_NODE_VERSION"
echo " Size: $CUSTOM_NODE_SIZE (vs ~110MB for standard Node.js)"
echo " Path: $CUSTOM_NODE_PATH"
npm run build -- --custom-node
else
echo "WARNING: Custom Node.js build failed, using system Node.js"
echo "The app will be larger than optimal."
npm run build
fi
else
# Debug build
if [ -f "$CUSTOM_NODE_PATH" ]; then
CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown")
echo "Debug build - found existing custom Node.js $CUSTOM_NODE_VERSION, using it for consistency"
npm run build -- --custom-node
else
echo "Debug build - using system Node.js for faster builds"
echo "System Node.js: $(node --version)"
echo "To use custom Node.js in debug builds, run: cd web && node build-custom-node.js --latest"
npm run build
fi
fi
# Clean and create destination directory
echo "Cleaning destination directory..."
@ -74,20 +153,22 @@ mkdir -p "${DEST_DIR}"
echo "Copying web files to app bundle..."
cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/"
# Copy native executable and modules to app bundle root
echo "Copying native executable and modules..."
# Copy native executable and modules to app bundle
NATIVE_DIR="${WEB_DIR}/native"
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
if [ -f "${NATIVE_DIR}/vibetunnel" ]; then
echo "Copying native executable to app bundle..."
EXEC_SIZE=$(ls -lh "${NATIVE_DIR}/vibetunnel" | awk '{print $5}')
echo " Executable size: $EXEC_SIZE"
cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/"
chmod +x "${APP_RESOURCES}/vibetunnel"
else
echo "error: vibetunnel executable not found"
echo "error: Native executable not found at ${NATIVE_DIR}/vibetunnel"
exit 1
fi
if [ -f "${NATIVE_DIR}/pty.node" ]; then
echo "Copying pty.node..."
cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/"
else
echo "error: pty.node not found"
@ -95,6 +176,7 @@ else
fi
if [ -f "${NATIVE_DIR}/spawn-helper" ]; then
echo "Copying spawn-helper..."
cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/"
chmod +x "${APP_RESOURCES}/spawn-helper"
else
@ -102,4 +184,65 @@ else
exit 1
fi
echo "✓ Native executable and modules copied successfully"
# Sanity check: Verify all required binaries are present in the app bundle
echo "Performing final sanity check..."
MISSING_FILES=()
# Check for vibetunnel executable
if [ ! -f "${APP_RESOURCES}/vibetunnel" ]; then
MISSING_FILES+=("vibetunnel executable")
fi
# Check for pty.node
if [ ! -f "${APP_RESOURCES}/pty.node" ]; then
MISSING_FILES+=("pty.node native module")
fi
# Check for spawn-helper (Unix only)
if [ ! -f "${APP_RESOURCES}/spawn-helper" ]; then
MISSING_FILES+=("spawn-helper")
fi
# Check if vibetunnel is executable
if [ -f "${APP_RESOURCES}/vibetunnel" ] && [ ! -x "${APP_RESOURCES}/vibetunnel" ]; then
MISSING_FILES+=("vibetunnel is not executable")
fi
# Check if spawn-helper is executable
if [ -f "${APP_RESOURCES}/spawn-helper" ] && [ ! -x "${APP_RESOURCES}/spawn-helper" ]; then
MISSING_FILES+=("spawn-helper is not executable")
fi
# If any files are missing, fail the build
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
echo "error: Build sanity check failed! Missing required files:"
for file in "${MISSING_FILES[@]}"; do
echo " - $file"
done
echo "Build artifacts in ${NATIVE_DIR}:"
ls -la "${NATIVE_DIR}" || echo " Directory does not exist"
echo "App resources in ${APP_RESOURCES}:"
ls -la "${APP_RESOURCES}/vibetunnel" "${APP_RESOURCES}/pty.node" "${APP_RESOURCES}/spawn-helper" 2>/dev/null || true
exit 1
fi
# Optional: Verify the executable works
echo "Verifying vibetunnel executable..."
if "${APP_RESOURCES}/vibetunnel" --version &>/dev/null; then
VERSION_OUTPUT=$("${APP_RESOURCES}/vibetunnel" --version 2>&1 | head -1)
echo "✓ VibeTunnel executable verified: $VERSION_OUTPUT"
else
echo "error: VibeTunnel executable failed verification (--version test failed)"
echo "This might indicate a corrupted or incompatible binary"
exit 1
fi
echo "✓ All sanity checks passed"
# Save the current hash as the previous hash for next build
cp "${HASH_FILE}" "${PREVIOUS_HASH_FILE}"
echo "Web frontend build completed successfully"

View file

@ -27,7 +27,7 @@ cd "${WEB_DIR}"
# Find all relevant files and calculate their size, modification time, and content hash
# This approach is more reliable than just content hash as it catches permission changes
# Exclude: node_modules, dist, public (all build outputs), package-lock.json
# Exclude: node_modules, dist, public (all build outputs), package-lock.json, and build directories
CONTENT_HASH=$(find . \
-type f \
\( -name "*.ts" -o -name "*.js" -o -name "*.json" -o -name "*.css" -o -name "*.html" \
@ -39,6 +39,9 @@ CONTENT_HASH=$(find . \
-not -path "./.next/*" \
-not -path "./coverage/*" \
-not -path "./.cache/*" \
-not -path "./.node-builds/*" \
-not -path "./build/*" \
-not -path "./native/*" \
-not -name "package-lock.json" \
-exec stat -f "%m %z %p" {} \; \
-exec shasum -a 256 {} \; | \

View file

@ -150,6 +150,16 @@ if [[ "$CLEAN_ALL" == "true" ]]; then
remove_item "web/.next" "Next.js build cache"
fi
# Clean web build artifacts (always clean these)
if [[ -d "$PROJECT_ROOT/../web" ]]; then
remove_item "$PROJECT_ROOT/../web/native" "Web native executables"
remove_item "$PROJECT_ROOT/../web/dist" "Web dist directory"
remove_item "$PROJECT_ROOT/../web/public/bundle" "Web bundle directory"
remove_item "$PROJECT_ROOT/../web/public/output.css" "Web CSS output"
remove_item "$PROJECT_ROOT/../web/build" "Web build directory"
remove_item "$PROJECT_ROOT/../web/.node-builds" "Custom Node.js builds"
fi
# Clean Python caches
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true

69
mac/scripts/install-node.sh Executable file
View file

@ -0,0 +1,69 @@
#!/bin/bash
#
# Check for Node.js availability for the build process
#
# This script ensures Node.js is available for building VibeTunnel
#
set -uo pipefail
# Script directory and paths
if [ -n "${BASH_SOURCE[0]:-}" ]; then
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
else
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
fi
echo "Checking for Node.js..."
# Add common Node.js installation paths to PATH
# Homebrew on Apple Silicon
if [ -d "/opt/homebrew/bin" ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
# Homebrew on Intel Macs
if [ -d "/usr/local/bin" ]; then
export PATH="/usr/local/bin:$PATH"
fi
# NVM default location
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
fi
# Check if Node.js is available
if command -v node &> /dev/null; then
echo "✓ Node.js found: $(which node)"
echo " Version: $(node --version)"
# Check Node.js version (need v20+)
NODE_VERSION=$(node --version | cut -d'v' -f2)
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'.' -f1)
if [ "$NODE_MAJOR" -lt 20 ]; then
echo "Warning: Node.js v20+ is recommended (found v$NODE_VERSION)"
fi
# Check if npm is available
if command -v npm &> /dev/null; then
echo "✓ npm found: $(which npm)"
echo " Version: $(npm --version)"
else
echo "Error: npm not found. Please ensure Node.js is properly installed."
exit 1
fi
exit 0
else
echo "Error: Node.js not found in PATH"
echo ""
echo "Please install Node.js 20+ using one of these methods:"
echo " - Homebrew: brew install node"
echo " - Download from: https://nodejs.org/"
echo " - Using nvm: nvm install 20"
echo ""
echo "PATH checked: $PATH"
exit 1
fi

View file

@ -319,6 +319,38 @@ fi
echo ""
echo -e "${BLUE}📋 Step 4/8: Building universal application...${NC}"
# Check for custom Node.js build
echo ""
echo "🔍 Checking for custom Node.js build..."
WEB_DIR="$PROJECT_ROOT/../web"
CUSTOM_NODE_PATH=$(find "$WEB_DIR/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
if [[ ! -f "$CUSTOM_NODE_PATH" ]]; then
echo -e "${YELLOW}⚠️ Custom Node.js not found. Building for optimal app size...${NC}"
echo " This will take 10-20 minutes on first run."
# Build custom Node.js
pushd "$WEB_DIR" > /dev/null
if node build-custom-node.js --latest; then
echo -e "${GREEN}✅ Custom Node.js built successfully${NC}"
CUSTOM_NODE_PATH=$(find "$WEB_DIR/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
if [[ -f "$CUSTOM_NODE_PATH" ]]; then
CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" | awk '{print $5}')
echo " Size: $CUSTOM_NODE_SIZE (vs ~110MB for standard Node.js)"
fi
else
echo -e "${RED}❌ Failed to build custom Node.js${NC}"
echo " Continuing with standard Node.js (larger app size)"
fi
popd > /dev/null
else
CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" | awk '{print $5}')
CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown")
echo -e "${GREEN}✅ Found custom Node.js${NC}"
echo " Version: $CUSTOM_NODE_VERSION"
echo " Size: $CUSTOM_NODE_SIZE"
fi
# For pre-release builds, set the environment variable
if [[ "$RELEASE_TYPE" != "stable" ]]; then
echo "📝 Marking build as pre-release..."

250
web/build-custom-node.js Executable file
View file

@ -0,0 +1,250 @@
#!/usr/bin/env node
/**
* Build a custom Node.js binary with reduced size by excluding features we don't need.
*
* This creates a Node.js build without:
* - International support (ICU) - saves ~28MB
* - npm/npx - saves ~5MB
* - corepack
* - dtrace/etw instrumentation
*
* The resulting binary is typically 50-60MB instead of 110+MB.
*
* Usage:
* ```bash
* node build-custom-node.js # Build for current Node.js version
* node build-custom-node.js --latest # Build latest Node.js version
* node build-custom-node.js --version=24.2.0 # Build specific version
* ```
*
* The custom Node.js will be built in:
* .node-builds/node-vXX-minimal/out/Release/node
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const https = require('https');
// Parse command line arguments
const args = process.argv.slice(2);
let targetVersion = null;
let useLatest = false;
for (const arg of args) {
if (arg.startsWith('--version=')) {
targetVersion = arg.split('=')[1];
} else if (arg === '--latest') {
useLatest = true;
}
}
// Helper to download files
function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destPath);
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
return;
}
response.pipe(file);
file.on('finish', () => {
file.close(resolve);
});
}).on('error', (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
});
}
// Helper to get latest Node.js version
async function getLatestNodeVersion() {
return new Promise((resolve, reject) => {
https.get('https://nodejs.org/dist/latest/SHASUMS256.txt', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
// Extract version from first line like: "1234567890abcdef node-v24.2.0-darwin-arm64.tar.gz"
const match = data.match(/node-v(\d+\.\d+\.\d+)/);
if (match) {
resolve(match[1]);
} else {
reject(new Error('Could not determine latest Node.js version'));
}
});
}).on('error', reject);
});
}
async function buildCustomNode() {
// Determine version to build
let nodeSourceVersion;
if (useLatest) {
console.log('Fetching latest Node.js version...');
nodeSourceVersion = await getLatestNodeVersion();
console.log(`Latest Node.js version: ${nodeSourceVersion}`);
} else if (targetVersion) {
nodeSourceVersion = targetVersion;
} else {
// Use current Node.js version
const nodeVersionMatch = process.version.match(/^v(\d+)\.(\d+)\.(\d+)/);
nodeSourceVersion = `${nodeVersionMatch[1]}.${nodeVersionMatch[2]}.${nodeVersionMatch[3]}`;
}
console.log(`Building custom Node.js ${nodeSourceVersion} without intl and npm...`);
console.log('This will take 10-20 minutes on first run, but will be cached for future builds.');
const nodeSourceUrl = `https://nodejs.org/dist/v${nodeSourceVersion}/node-v${nodeSourceVersion}.tar.gz`;
const majorVersion = nodeSourceVersion.split('.')[0];
const buildDir = path.join(__dirname, '.node-builds');
const versionDir = path.join(buildDir, `node-v${nodeSourceVersion}-minimal`);
const markerFile = path.join(versionDir, '.build-complete');
const customNodePath = path.join(versionDir, 'out', 'Release', 'node');
// Check if already built
if (fs.existsSync(markerFile) && fs.existsSync(customNodePath)) {
console.log(`Using cached custom Node.js build from ${customNodePath}`);
const stats = fs.statSync(customNodePath);
console.log(`Cached custom Node.js size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
return customNodePath;
}
// Create build directory
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Clean up old version directory if exists
if (fs.existsSync(versionDir)) {
console.log('Cleaning up incomplete build...');
fs.rmSync(versionDir, { recursive: true, force: true });
}
const tarPath = path.join(buildDir, `node-v${nodeSourceVersion}.tar.gz`);
const originalCwd = process.cwd();
try {
// Download Node.js source if not cached
if (!fs.existsSync(tarPath)) {
console.log(`Downloading Node.js source from ${nodeSourceUrl}...`);
await downloadFile(nodeSourceUrl, tarPath);
}
// Extract source
console.log('Extracting Node.js source...');
execSync(`tar -xzf "${tarPath}" -C "${buildDir}"`, { stdio: 'inherit' });
// Rename to version-specific directory
const extractedDir = path.join(buildDir, `node-v${nodeSourceVersion}`);
fs.renameSync(extractedDir, versionDir);
// Configure and build
process.chdir(versionDir);
console.log('Configuring Node.js build (without intl and npm)...');
const configureArgs = [
'--without-intl',
'--without-npm',
'--without-corepack',
'--without-inspector',
'--without-node-snapshot', // Disable V8 snapshot (saves ~2-3MB)
'--without-node-code-cache', // Disable code cache (saves ~1-2MB)
'--ninja', // Use ninja if available for faster builds
];
// Set optimization flags for smaller binary
// -Os: Optimize for size
// -flto: Link Time Optimization (can save 10-20%)
// -ffunction-sections -fdata-sections: Allow linker to remove unused code
process.env.CFLAGS = '-Os -flto -ffunction-sections -fdata-sections';
process.env.CXXFLAGS = '-Os -flto -ffunction-sections -fdata-sections';
process.env.LDFLAGS = '-Wl,-dead_strip'; // macOS equivalent of --gc-sections
// Check if ninja is available, install if not
try {
execSync('which ninja', { stdio: 'ignore' });
console.log('Using Ninja for faster builds...');
} catch {
console.log('Ninja not found, installing via Homebrew...');
try {
execSync('brew install ninja', { stdio: 'inherit' });
console.log('Ninja installed successfully');
} catch (brewError) {
console.log('Failed to install ninja, falling back to Make...');
// Remove --ninja if not available
configureArgs.pop();
}
}
execSync(`./configure ${configureArgs.join(' ')}`, { stdio: 'inherit' });
console.log('Building Node.js (this will take a while)...');
const cores = require('os').cpus().length;
// Check if we're using ninja or make
const buildSystem = configureArgs.includes('--ninja') ? 'ninja' : 'make';
if (buildSystem === 'ninja') {
execSync(`ninja -C out/Release -j ${cores}`, { stdio: 'inherit' });
} else {
execSync(`make -j${cores}`, { stdio: 'inherit' });
}
// Verify the build
if (!fs.existsSync(customNodePath)) {
throw new Error('Node.js build failed - binary not found');
}
// Strip the binary
console.log('Stripping Node.js binary...');
execSync(`strip -S "${customNodePath}"`, { stdio: 'inherit' });
// Check final size
const stats = fs.statSync(customNodePath);
console.log(`\n✅ Custom Node.js built successfully!`);
console.log(`Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
// Compare with system Node.js
try {
const systemNodeStats = fs.statSync(process.execPath);
const reduction = ((systemNodeStats.size - stats.size) / systemNodeStats.size * 100).toFixed(1);
console.log(`Size reduction: ${reduction}% compared to system Node.js`);
} catch (e) {
// Ignore if we can't stat system node
}
// Mark build as complete
fs.writeFileSync(markerFile, JSON.stringify({
version: nodeSourceVersion,
buildDate: new Date().toISOString(),
size: stats.size,
configureArgs: configureArgs
}, null, 2));
// Change back to original directory
process.chdir(originalCwd);
console.log(`\nCustom Node.js location: ${customNodePath}`);
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
return customNodePath;
} catch (error) {
process.chdir(originalCwd);
console.error('Failed to build custom Node.js:', error.message);
process.exit(1);
}
}
// Run the build
buildCustomNode().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});

View file

@ -41,8 +41,13 @@
*
* ## Usage
* ```bash
* node build-native-node.js # Build without sourcemaps (default)
* node build-native-node.js --sourcemap # Build with inline sourcemaps
* node build-native.js # Build with system Node.js
* node build-native.js --sourcemap # Build with inline sourcemaps
* node build-native.js --custom-node=/path/to/node # Use custom Node.js binary
*
* # Build custom Node.js first:
* node build-custom-node.js # Build minimal Node.js for current version
* node build-custom-node.js --version=24.2.0 # Build specific version
* ```
*
* ## Requirements
@ -61,9 +66,45 @@ const path = require('path');
// Parse command line arguments
const includeSourcemaps = process.argv.includes('--sourcemap');
let customNodePath = null;
// Parse --custom-node argument
for (let i = 0; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg.startsWith('--custom-node=')) {
customNodePath = arg.split('=')[1];
} else if (arg === '--custom-node') {
// Check if next argument is a path
if (i + 1 < process.argv.length && !process.argv[i + 1].startsWith('--')) {
customNodePath = process.argv[i + 1];
} else {
// No path provided, search for custom Node.js build
console.log('Searching for custom Node.js build...');
const customBuildsDir = path.join(__dirname, '.node-builds');
if (fs.existsSync(customBuildsDir)) {
const dirs = fs.readdirSync(customBuildsDir)
.filter(dir => dir.startsWith('node-v') && dir.endsWith('-minimal'))
.map(dir => ({
name: dir,
path: path.join(customBuildsDir, dir, 'out/Release/node'),
mtime: fs.statSync(path.join(customBuildsDir, dir)).mtime
}))
.filter(item => fs.existsSync(item.path))
.sort((a, b) => b.mtime - a.mtime); // Sort by modification time, newest first
if (dirs.length > 0) {
customNodePath = dirs[0].path;
console.log(`Found custom Node.js at: ${customNodePath}`);
} else {
console.log('No custom Node.js builds found in .node-builds/');
}
}
}
}
}
console.log('Building standalone vibetunnel executable using Node.js SEA...');
console.log(`Node.js version: ${process.version}`);
console.log(`System Node.js version: ${process.version}`);
if (includeSourcemaps) {
console.log('Including sourcemaps in build');
}
@ -228,11 +269,38 @@ async function main() {
fs.mkdirSync('native');
}
// 0. Patch node-pty
// 0. Determine which Node.js to use
let nodeExe = process.execPath;
if (customNodePath) {
// Validate custom node exists
if (!fs.existsSync(customNodePath)) {
console.error(`Error: Custom Node.js not found at ${customNodePath}`);
console.error('Build one using: node build-custom-node.js');
process.exit(1);
}
nodeExe = customNodePath;
}
console.log(`Using Node.js binary: ${nodeExe}`);
const nodeStats = fs.statSync(nodeExe);
console.log(`Node.js binary size: ${(nodeStats.size / 1024 / 1024).toFixed(2)} MB`);
// Get version of the Node.js we're using
if (customNodePath) {
try {
const customVersion = execSync(`"${nodeExe}" --version`, { encoding: 'utf8' }).trim();
console.log(`Custom Node.js version: ${customVersion}`);
console.log('This minimal build excludes intl, npm, inspector, and other unused features.');
} catch (e) {
console.log('Could not determine custom Node.js version');
}
}
// 1. Patch node-pty
patchNodePty();
// 1. Bundle TypeScript with esbuild using custom loader
console.log('Bundling TypeScript with esbuild...');
// 2. Bundle TypeScript with esbuild using custom loader
console.log('\nBundling TypeScript with esbuild...');
const buildDate = new Date().toISOString();
const buildTimestamp = Date.now();
@ -272,7 +340,6 @@ async function main() {
// 4. Create executable
console.log('\nCreating executable...');
const nodeExe = process.execPath;
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
// Copy node binary
@ -292,18 +359,32 @@ async function main() {
execSync(postjectCmd, { stdio: 'inherit' });
// 6. Sign on macOS
// 6. Strip the executable first (before signing)
console.log('Stripping final executable...');
// Note: This will show a warning about invalidating code signature, which is expected
// since we're modifying a signed Node.js binary. We'll re-sign it in the next step.
execSync(`strip -S ${targetExe} 2>&1 | grep -v "warning: changes being made" || true`, {
stdio: 'inherit',
shell: true
});
// 7. Sign on macOS (after stripping)
if (process.platform === 'darwin') {
console.log('Signing executable...');
execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' });
}
// Check final size
const finalStats = fs.statSync(targetExe);
console.log(`Final executable size: ${(finalStats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Size reduction: ${((nodeStats.size - finalStats.size) / 1024 / 1024).toFixed(2)} MB`);
// 7. Restore original node-pty
// 8. 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
// 9. Copy only necessary native files
console.log('Copying native modules...');
const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release';

View file

@ -27,6 +27,16 @@ execSync('tsc', { stdio: 'inherit' });
// Build native executable
console.log('Building native executable...');
execSync('node build-native.js', { stdio: 'inherit' });
// Check for --custom-node flag
const useCustomNode = process.argv.includes('--custom-node');
if (useCustomNode) {
console.log('Using custom Node.js for smaller binary size...');
execSync('node build-native.js --custom-node', { stdio: 'inherit' });
} else {
console.log('Using system Node.js...');
execSync('node build-native.js', { stdio: 'inherit' });
}
console.log('Build completed successfully!');

View file

@ -6,6 +6,16 @@ import { VERSION } from './server/version.js';
// Source maps are only included if built with --sourcemap flag
// Prevent double execution in SEA context where require.main might be undefined
// Use a global flag to ensure we only run once
if ((global as any).__vibetunnelStarted) {
console.log('VibeTunnel already started, skipping duplicate execution');
console.log('Global flag was already set, exiting to prevent duplicate server');
process.exit(0);
}
(global as any).__vibetunnelStarted = true;
console.log('Setting global flag to prevent duplicate execution');
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
@ -21,14 +31,25 @@ process.on('unhandledRejection', (reason, promise) => {
process.exit(1);
});
if (process.argv[2] === 'version') {
console.log(`VibeTunnel Linux v${VERSION}`);
process.exit(0);
} else if (process.argv[2] === 'fwd') {
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
// Only execute if this is the main module (or in SEA where require.main is undefined)
if (!module.parent && (require.main === module || require.main === undefined)) {
console.log('Main module check passed, proceeding with server startup');
console.log('module.parent:', module.parent);
console.log('require.main === module:', require.main === module);
console.log('require.main:', require.main);
if (process.argv[2] === 'version') {
console.log(`VibeTunnel Server v${VERSION}`);
process.exit(0);
} else if (process.argv[2] === 'fwd') {
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
} else {
console.log('Starting VibeTunnel server...');
startVibeTunnelServer();
}
} else {
startVibeTunnelServer();
console.log('Not main module, skipping server startup');
}

View file

@ -247,7 +247,17 @@ interface AppInstance {
activityMonitor: ActivityMonitor;
}
// Track if app has been created
let appCreated = false;
export function createApp(): AppInstance {
// Prevent multiple app instances
if (appCreated) {
console.error(chalk.red('ERROR: App already created, preventing duplicate instance'));
throw new Error('Duplicate app creation detected');
}
appCreated = true;
const config = parseArgs();
// Check if help was requested
@ -271,6 +281,7 @@ export function createApp(): AppInstance {
validateConfig(config);
console.log('Creating Express app and HTTP server...');
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
@ -406,6 +417,28 @@ export function createApp(): AppInstance {
// Start server function
const startServer = () => {
const requestedPort = config.port !== null ? config.port : Number(process.env.PORT) || 4020;
console.log(`Attempting to start server on port ${requestedPort}`);
// Remove all existing error listeners first to prevent duplicates
server.removeAllListeners('error');
// Add error handler for port already in use
server.on('error', (error: any) => {
if (error.code === 'EADDRINUSE') {
console.error(chalk.red(`Error: Port ${requestedPort} is already in use`));
console.error(
chalk.yellow(
'Please use a different port with --port <number> or stop the existing server'
)
);
process.exit(9); // Exit with code 9 to indicate port conflict
} else {
console.error(chalk.red('Server error:'), error);
process.exit(1);
}
});
server.listen(requestedPort, () => {
const address = server.address();
const actualPort =
@ -485,8 +518,20 @@ export function createApp(): AppInstance {
};
}
// Track if server has been started
let serverStarted = false;
// Export a function to start the server
export function startVibeTunnelServer() {
// Prevent multiple server instances
if (serverStarted) {
console.error(chalk.red('ERROR: Server already started, preventing duplicate instance'));
console.error('This should not happen - duplicate server startup detected');
process.exit(1);
}
serverStarted = true;
console.log('Creating app instance...');
// Create and configure the app
const appInstance = createApp();
const {
@ -499,6 +544,7 @@ export function startVibeTunnelServer() {
activityMonitor,
} = appInstance;
console.log('Starting server...');
startServer();
// Cleanup old terminals every 5 minutes