Fix npm package build and installation issues (#360)

Co-authored-by: Alex Mazanov <alexandr.mazanov@gmail.com>
This commit is contained in:
Peter Steinberger 2025-07-16 23:05:26 +02:00 committed by GitHub
parent 0d3b437887
commit 253d0ae3e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1376 additions and 411 deletions

View file

@ -17,7 +17,23 @@ Basic CI workflow that runs on every push and PR affecting the web directory.
- Pull requests to `main`
- Only when files in `web/` directory change
### 2. SEA Build Test (`sea-build-test.yml`)
### 2. NPM Package Test (`npm-test.yml`)
Dedicated workflow for testing the npm package build and installation.
**Jobs:**
- **Test NPM Package**: Builds the npm package and tests installation in a clean environment
**Features:**
- Builds npm package using the clean build approach
- Tests global installation without requiring build tools
- Verifies server startup and API functionality
- Validates package structure and dependencies
**Triggers:**
- Push to `main` or `npm-build` branches when web files change
- Pull requests to `main` when web files change
### 3. SEA Build Test (`sea-build-test.yml`)
Advanced workflow for testing Single Executable Application (SEA) builds with custom Node.js.
**Features:**
@ -46,7 +62,7 @@ Advanced workflow for testing Single Executable Application (SEA) builds with cu
- Helps identify any Blacksmith-specific issues
- Runs only on push events
### 3. Xcode SEA Test (`xcode-sea-test.yml`)
### 4. Xcode SEA Test (`xcode-sea-test.yml`)
Tests the macOS Xcode build with custom Node.js to ensure the VibeTunnel.app works correctly with SEA executables.
**Features:**

View file

@ -154,7 +154,7 @@ jobs:
run: |
echo "Resolving Swift package dependencies..."
# Workspace is at root level
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace -parallel || echo "Dependency resolution completed"
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace || echo "Dependency resolution completed"
# Debug: List available schemes
echo "=== Available schemes ==="
@ -179,25 +179,6 @@ jobs:
DEVELOPMENT_TEAM="" \
COMPILER_INDEX_STORE_ENABLE=NO
- name: Build Release (Native Architecture)
timeout-minutes: 15
run: |
set -o pipefail && \
xcodebuild build \
-workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel-Mac \
-configuration Release \
-destination "platform=macOS" \
-showBuildTimingSummary \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_ENTITLEMENTS="" \
ENABLE_HARDENED_RUNTIME=NO \
PROVISIONING_PROFILE_SPECIFIER="" \
DEVELOPMENT_TEAM="" \
COMPILER_INDEX_STORE_ENABLE=NO
# LINT PHASE
- name: Run SwiftFormat (check mode)
id: swiftformat

236
.github/workflows/nightly.yml vendored Normal file
View file

@ -0,0 +1,236 @@
name: Nightly Release Build
on:
schedule:
# Run at 2 AM UTC every day (10 PM EST / 7 PM PST)
- cron: '0 2 * * *'
workflow_dispatch: # Allow manual triggering
permissions:
contents: read
pull-requests: write
issues: write
jobs:
release-build-test:
name: Build and Test Release Configuration
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 60
steps:
- name: Clean workspace
run: |
# Clean workspace for self-hosted runner
rm -rf * || true
rm -rf .* || true
- name: Checkout code
uses: actions/checkout@v4
- name: Verify Xcode
run: |
xcodebuild -version
swift --version
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
dest: ~/pnpm-${{ github.run_id }}
- name: Cache Homebrew packages
uses: actions/cache@v4
continue-on-error: true
with:
path: |
~/Library/Caches/Homebrew
/opt/homebrew/Cellar/swiftlint
/opt/homebrew/Cellar/swiftformat
/opt/homebrew/Cellar/xcbeautify
key: ${{ runner.os }}-brew-${{ hashFiles('.github/workflows/mac.yml') }}
restore-keys: |
${{ runner.os }}-brew-
- name: Install tools
run: |
# Install or update required tools
MAX_ATTEMPTS=3
WAIT_TIME=5
for attempt in $(seq 1 $MAX_ATTEMPTS); do
echo "Tool installation attempt $attempt of $MAX_ATTEMPTS"
# Check if another brew process is running
if pgrep -x "brew" > /dev/null; then
echo "Another brew process detected, waiting ${WAIT_TIME}s..."
sleep $WAIT_TIME
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
continue
fi
# Update Homebrew and install all tools in one command
if brew update && brew install swiftlint swiftformat xcbeautify; then
echo "Successfully installed/upgraded all tools"
break
else
if [ $attempt -eq $MAX_ATTEMPTS ]; then
echo "Failed to install tools after $MAX_ATTEMPTS attempts"
exit 1
fi
echo "Command failed, waiting ${WAIT_TIME}s before retry..."
sleep $WAIT_TIME
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
fi
done
# Show versions
echo "SwiftLint: $(swiftlint --version || echo 'not found')"
echo "SwiftFormat: $(swiftformat --version || echo 'not found')"
echo "xcbeautify: $(xcbeautify --version || echo 'not found')"
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install web dependencies
run: |
cd web
# Clean any stale lock files
rm -f .pnpm-store.lock .pnpm-debug.log || true
# Set pnpm to use fewer workers to avoid crashes on self-hosted runners
export NODE_OPTIONS="--max-old-space-size=4096"
pnpm config set store-dir ~/.local/share/pnpm/store
pnpm config set package-import-method hardlink
# Install with retries
for i in 1 2 3; do
echo "Install attempt $i"
if pnpm install --frozen-lockfile; then
echo "pnpm install succeeded"
# Force rebuild of native modules
echo "Rebuilding native modules..."
pnpm rebuild || true
break
else
echo "pnpm install failed, cleaning and retrying..."
rm -rf node_modules .pnpm-store.lock || true
sleep 5
fi
done
- name: Build web artifacts
run: |
echo "Building web artifacts..."
cd web
export CI=true
pnpm run build
echo "Web artifacts built successfully"
- name: Resolve Dependencies
run: |
echo "Resolving Swift package dependencies..."
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace || echo "Dependency resolution completed"
# BUILD RELEASE CONFIGURATION
- name: Build Release (Universal Binary)
timeout-minutes: 20
run: |
echo "Building Release configuration with universal binary..."
set -o pipefail && \
xcodebuild build \
-workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel-Mac \
-configuration Release \
-destination "generic/platform=macOS" \
-archivePath build/VibeTunnel.xcarchive \
-showBuildTimingSummary \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_ENTITLEMENTS="" \
ENABLE_HARDENED_RUNTIME=NO \
PROVISIONING_PROFILE_SPECIFIER="" \
DEVELOPMENT_TEAM="" \
ONLY_ACTIVE_ARCH=NO \
archive | xcbeautify
echo "Release build completed successfully"
# TEST RELEASE BUILD
- name: Test Release Configuration
timeout-minutes: 20
run: |
echo "Running tests on Release configuration..."
set -o pipefail && \
xcodebuild test \
-workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel-Mac \
-configuration Release \
-destination "platform=macOS" \
-enableCodeCoverage YES \
-resultBundlePath TestResults-Release.xcresult \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify || {
echo "::error::Release configuration tests failed"
# Try to get more detailed error information
echo "=== Attempting to get test failure details ==="
xcrun xcresulttool get --path TestResults-Release.xcresult --format json 2>/dev/null | jq '.issues._values[]? | select(.severity == "error")' 2>/dev/null || true
exit 1
}
echo "Release tests completed successfully"
# PERFORMANCE VALIDATION
- name: Validate Release Binary
run: |
echo "=== Validating Release Binary ==="
ARCHIVE_PATH="build/VibeTunnel.xcarchive"
APP_PATH="$ARCHIVE_PATH/Products/Applications/VibeTunnel.app"
if [ -d "$APP_PATH" ]; then
echo "Found VibeTunnel.app at: $APP_PATH"
# Check binary architectures
echo -e "\n=== Binary Architecture ==="
lipo -info "$APP_PATH/Contents/MacOS/VibeTunnel" || echo "Binary not found"
# Check binary size
echo -e "\n=== Binary Size ==="
du -sh "$APP_PATH" || echo "Could not determine app size"
# Check if optimizations were applied (Release should be smaller than Debug)
echo -e "\n=== Optimization Check ==="
# Look for debug symbols - Release builds should have minimal symbols
nm "$APP_PATH/Contents/MacOS/VibeTunnel" 2>/dev/null | grep -c "debug" || echo "No debug symbols found (good for Release)"
# Verify entitlements
echo -e "\n=== Entitlements ==="
codesign -d --entitlements - "$APP_PATH" 2>&1 || echo "No code signing (expected in CI)"
else
echo "::warning::Release archive not found at expected location"
fi
# NOTIFY ON FAILURE
- name: Notify on Failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const issue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Nightly Release Build Failed - ${new Date().toISOString().split('T')[0]}`,
body: `The nightly release build failed. Please check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for details.`,
labels: ['ci', 'nightly-build']
});
console.log(`Created issue #${issue.data.number}`);

View file

@ -172,6 +172,11 @@ jobs:
working-directory: web
run: pnpm install --frozen-lockfile
- name: Build node-pty
working-directory: web
run: |
cd node-pty && npm install && npm run build
- name: Build frontend and backend
working-directory: web
run: pnpm run build:ci
@ -327,6 +332,11 @@ jobs:
working-directory: web
run: pnpm install --frozen-lockfile
- name: Build node-pty for TypeScript
working-directory: web
run: |
cd node-pty && npm install && npm run build
- name: Check TypeScript types
working-directory: web
run: pnpm run typecheck

130
.github/workflows/npm-test.yml vendored Normal file
View file

@ -0,0 +1,130 @@
name: NPM Package Test
on:
push:
branches: [ main, npm-build ]
paths:
- 'web/**'
- '.github/workflows/npm-test.yml'
pull_request:
branches: [ main ]
paths:
- 'web/**'
- '.github/workflows/npm-test.yml'
permissions:
contents: read
pull-requests: write
defaults:
run:
working-directory: web
jobs:
test-npm-package:
name: Test NPM Package
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev build-essential python3 make g++
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build node-pty
run: |
cd node-pty && npm install && npm run build
- name: Build npm package
run: pnpm run build:npm -- --current-only
- name: Test npm package installation
run: |
# Create a test directory
mkdir -p /tmp/npm-test
cd /tmp/npm-test
# Copy the built package
cp ${{ github.workspace }}/web/vibetunnel-*.tgz .
# Install the package globally
npm install -g vibetunnel-*.tgz
# Verify installation
echo "=== Verifying installation ==="
which vibetunnel || (echo "vibetunnel not found" && exit 1)
which vt || echo "vt command not installed (expected on Linux)"
# Check if node-pty was extracted correctly
echo "=== Checking node-pty extraction ==="
# With the new build, node-pty is bundled directly in the package
ls -la $(npm root -g)/vibetunnel/node-pty/ || echo "Checking node-pty structure..."
ls -la $(npm root -g)/vibetunnel/node-pty/build/Release/pty.node || echo "node-pty prebuild will be extracted on postinstall"
# Check package structure
echo "=== Checking package structure ==="
ls -la $(npm root -g)/vibetunnel/
ls -la $(npm root -g)/vibetunnel/lib/
# Note: authenticate-pam is installed as a regular dependency now
# It's not bundled in the new clean build approach
# Test server startup
echo "=== Testing server startup ==="
vibetunnel --port 4020 --no-auth &
SERVER_PID=$!
# Wait for server to start
sleep 5
# Check if server is running
if ps -p $SERVER_PID > /dev/null; then
echo "✅ Server process is running"
else
echo "❌ Server process died"
exit 1
fi
# Test HTTP endpoint
if curl -s -f http://localhost:4020 > /dev/null; then
echo "✅ HTTP server is responding"
else
echo "❌ HTTP server not responding"
kill $SERVER_PID 2>/dev/null
exit 1
fi
# Test API endpoint
RESPONSE=$(curl -s http://localhost:4020/api/sessions)
# Check if response is an array (either empty [] or with sessions)
if echo "$RESPONSE" | grep -E '^\[.*\]$' > /dev/null; then
echo "✅ API is responding correctly"
echo "Response: $RESPONSE"
else
echo "❌ API not responding correctly"
echo "Response: $RESPONSE"
kill $SERVER_PID 2>/dev/null
exit 1
fi
# Clean up
kill $SERVER_PID
echo "✅ All tests passed!"

View file

@ -51,6 +51,10 @@ jobs:
- name: Run linting
run: pnpm run lint
- name: Build node-pty for TypeScript
run: |
cd node-pty && npm install && npm run build
- name: Run type checking
run: pnpm run typecheck
@ -84,6 +88,10 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build node-pty
run: |
cd node-pty && npm install && npm run build
- name: Build project
run: pnpm run build
@ -143,4 +151,5 @@ jobs:
with:
name: server-coverage-report
path: web/coverage/server/
retention-days: 7
retention-days: 7

1
web/.gitignore vendored
View file

@ -7,6 +7,7 @@ yarn-error.log*
# Generated files
public/
dist/
dist-npm/
# Logs
*.log

View file

@ -1,4 +1,4 @@
#!/usr/bin/env node
// Start the CLI - it handles all command routing including 'fwd'
require('../dist/cli.js');
require('../dist/vibetunnel-cli');

View file

@ -41,11 +41,12 @@ VibeTunnel requires two native modules:
- **Dependencies**: None (vendored implementation)
### 2. authenticate-pam (Authentication)
- **Purpose**: PAM (Pluggable Authentication Modules) integration
- **Purpose**: PAM (Pluggable Authentication Modules) integration for system authentication
- **Components**:
- `authenticate_pam.node`: Node.js addon for system authentication
- **Platforms**: Linux primarily, macOS for compatibility
- **Platforms**: Both macOS and Linux
- **Dependencies**: System PAM libraries
- **Note**: While macOS uses different authentication mechanisms internally (OpenDirectory), VibeTunnel attempts PAM authentication on both platforms as a fallback after SSH key authentication
## Prebuild System
@ -55,7 +56,9 @@ We use `prebuild` and `prebuild-install` to provide precompiled native modules,
### Coverage
- **Node.js versions**: 20, 22, 23, 24
- **Platforms**: macOS (x64, arm64), Linux (x64, arm64)
- **Total prebuilds**: 32 binaries (16 per native module)
- **Total prebuilds**: 24 binaries
- node-pty: 16 binaries (macOS and Linux, all architectures)
- authenticate-pam: 8 binaries (Linux only - macOS builds may fail due to PAM differences)
### Prebuild Files
```
@ -64,17 +67,23 @@ prebuilds/
├── node-pty-v1.0.0-node-v115-darwin-x64.tar.gz
├── node-pty-v1.0.0-node-v115-linux-arm64.tar.gz
├── node-pty-v1.0.0-node-v115-linux-x64.tar.gz
├── authenticate-pam-v1.0.5-node-v115-darwin-arm64.tar.gz
├── authenticate-pam-v1.0.5-node-v115-darwin-x64.tar.gz
├── authenticate-pam-v1.0.5-node-v115-linux-arm64.tar.gz
├── authenticate-pam-v1.0.5-node-v115-linux-x64.tar.gz
└── ... (similar for node versions 22, 23, 24)
└── ... (similar for node versions 22, 23, 24, Linux only)
```
Note: Node version numbers map to internal versions (v115=Node 20, v127=Node 22, v131=Node 23, v134=Node 24)
## Build Process
### Clean Build Approach
The npm build process uses a clean distribution directory approach that follows npm best practices:
1. **Creates dist-npm/ directory** - Separate from source files
2. **Generates clean package.json** - Only production fields, no dev dependencies
3. **Bundles dependencies** - node-pty is bundled directly, no symlinks needed
4. **Preserves source integrity** - Never modifies source package.json
### Unified Build (Multi-Platform by Default)
```bash
npm run build:npm
@ -83,6 +92,7 @@ npm run build:npm
- Builds native modules for all supported platforms (macOS x64/arm64, Linux x64/arm64)
- Creates comprehensive prebuilds for zero-dependency installation
- Generates npm README optimized for package distribution
- Creates clean dist-npm/ directory for packaging
### Build Options
The unified build script supports flexible targeting:
@ -113,44 +123,45 @@ The build will fail with helpful error messages if Docker is not available.
### For End Users
1. **Install package**: `npm install -g vibetunnel`
2. **Prebuild-install runs**: Attempts to download prebuilt binaries
3. **Fallback compilation**: If prebuilds fail, compiles from source
4. **Result**: Working VibeTunnel installation
2. **Postinstall script runs**: Extracts appropriate prebuilt binaries
3. **No compilation needed**: Prebuilds included for all supported platforms
4. **Result**: Working VibeTunnel installation without build tools
### Key Improvements
- **No symlinks**: node-pty is bundled directly, avoiding postinstall symlink issues
- **Clean package structure**: Only production files in the npm package
- **Reliable installation**: Works in restricted environments (Docker, CI)
### Installation Scripts
The package uses a multi-stage installation approach:
The package uses a simplified postinstall approach:
```json
{
"scripts": {
"install": "prebuild-install || node scripts/postinstall-npm.js"
"postinstall": "node scripts/postinstall.js"
}
}
```
#### Stage 1: prebuild-install
- Downloads appropriate prebuilt binary for current platform/Node version
- Installs to standard locations
- **Success**: Installation complete, no compilation needed
- **Failure**: Proceeds to Stage 2
#### Stage 2: postinstall-npm.js
- **node-pty**: Essential module, installation fails if build fails
- **authenticate-pam**: Optional module, warns if build fails
- Provides helpful error messages about required build tools
#### Postinstall Process
- **Prebuild extraction**: Extracts the appropriate prebuild for the current platform
- **No downloads**: All prebuilds are included in the package
- **No compilation**: Everything is pre-built, no build tools required
- **Platform detection**: Automatically selects correct binary based on OS and architecture
## Platform-Specific Details
### macOS
- **spawn-helper**: Additional C binary needed for proper PTY operations
- **Built during install**: spawn-helper compiles via node-gyp when needed
- **spawn-helper**: Additional C binary needed for proper PTY operations (now prebuilt as universal binary)
- **Authentication**: Attempts PAM authentication but may fall back to environment variables or SSH keys
- **Architecture**: Supports both Intel (x64) and Apple Silicon (arm64)
- **Build tools**: Requires Xcode Command Line Tools for source compilation
- **Build tools**: Not required with prebuilds; Xcode Command Line Tools only needed for source compilation fallback
### Linux
- **PAM libraries**: Requires `libpam0g-dev` for authenticate-pam compilation
- **PAM authentication**: Full support via authenticate-pam module
- **PAM libraries**: Requires `libpam0g-dev` for authenticate-pam compilation from source
- **spawn-helper**: Not used on Linux (macOS-only)
- **Build tools**: Requires `build-essential` package for source compilation
- **Build tools**: Not required with prebuilds; `build-essential` only needed for source compilation fallback
### Docker Build Environment
Linux prebuilds are created using Docker with:

View file

@ -10,8 +10,12 @@
"dist/",
"public/",
"bin/",
"scripts/ensure-native-modules.js",
"scripts/postinstall-npm.js",
"node-pty/lib/",
"node-pty/package.json",
"node-pty/binding.gyp",
"node-pty/src/",
"prebuilds/",
"README.md"
],
@ -40,7 +44,7 @@
"build:ci": "node scripts/build-ci.js",
"build:npm": "node scripts/build-npm.js",
"prepublishOnly": "npm run build:npm",
"postinstall": "node scripts/ensure-native-modules.js",
"postinstall": "node scripts/postinstall-npm.js",
"prebuild": "echo 'Skipping prebuild - handled by build-npm.js'",
"prebuild:upload": "echo 'Skipping prebuild:upload - not used'",
"lint": "concurrently -n biome,tsc-server,tsc-client,tsc-sw \"biome check src\" \"tsc --noEmit --project tsconfig.server.json\" \"tsc --noEmit --project tsconfig.client.json\" \"tsc --noEmit --project tsconfig.sw.json\"",

78
web/package.npm.json Normal file
View file

@ -0,0 +1,78 @@
{
"name": "vibetunnel",
"version": "1.0.0-beta.10",
"description": "Terminal sharing server with web interface - supports macOS, Linux, and headless environments",
"main": "lib/cli.js",
"bin": {
"vibetunnel": "./bin/vibetunnel",
"vt": "./bin/vt"
},
"files": [
"lib/",
"public/",
"bin/",
"scripts/",
"node-pty/",
"node_modules/authenticate-pam/",
"prebuilds/",
"README.md"
],
"os": [
"darwin",
"linux"
],
"engines": {
"node": ">=20.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/amantus-ai/vibetunnel.git",
"directory": "web"
},
"homepage": "https://vibetunnel.sh",
"bugs": {
"url": "https://github.com/amantus-ai/vibetunnel/issues"
},
"scripts": {
"postinstall": "node scripts/postinstall.js"
},
"dependencies": {
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.0",
"@xterm/headless": "^5.5.0",
"authenticate-pam": "^1.0.5",
"bonjour-service": "^1.3.0",
"chalk": "^5.4.1",
"compression": "^1.8.0",
"express": "^5.1.0",
"helmet": "^8.1.0",
"http-proxy-middleware": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"lit": "^3.3.1",
"mime-types": "^3.0.1",
"monaco-editor": "^0.52.2",
"multer": "^2.0.1",
"node-addon-api": "^7.1.0",
"node-pty": "file:node-pty",
"postject": "1.0.0-alpha.6",
"signal-exit": "^4.1.0",
"web-push": "^3.6.7",
"ws": "^8.18.3"
},
"keywords": [
"terminal",
"multiplexer",
"websocket",
"asciinema"
],
"author": "",
"license": "MIT"
}

611
web/scripts/build-npm.js Executable file → Normal file
View file

@ -1,7 +1,8 @@
#!/usr/bin/env node
/**
* Unified npm build script for VibeTunnel
* Clean npm build script for VibeTunnel
* Uses a separate dist-npm directory with its own package.json
* Builds for all platforms by default with complete prebuild support
*
* Options:
@ -11,7 +12,7 @@
* --arch <arch> Build for specific architecture (x64, arm64)
*/
const { execSync, spawn } = require('child_process');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
@ -21,13 +22,18 @@ const ALL_PLATFORMS = {
linux: ['x64', 'arm64']
};
const DIST_DIR = path.join(__dirname, '..', 'dist-npm');
const ROOT_DIR = path.join(__dirname, '..');
// Map Node.js versions to ABI versions
// ABI versions from: https://nodejs.org/api/n-api.html#node-api-version-matrix
// These map to the internal V8 ABI versions used by prebuild
function getNodeAbi(nodeVersion) {
const abiMap = {
'20': '115',
'22': '127',
'23': '131',
'24': '134'
'20': '115', // Node.js 20.x uses ABI 115
'22': '127', // Node.js 22.x uses ABI 127
'23': '131', // Node.js 23.x uses ABI 131
'24': '134' // Node.js 24.x uses ABI 134
};
return abiMap[nodeVersion];
}
@ -41,6 +47,20 @@ const platformFilter = args.find(arg => arg.startsWith('--platform'))?.split('='
const archFilter = args.find(arg => arg.startsWith('--arch'))?.split('=')[1] ||
(args.includes('--arch') ? args[args.indexOf('--arch') + 1] : null);
// Validate platform and architecture arguments
const VALID_PLATFORMS = ['darwin', 'linux'];
const VALID_ARCHS = ['x64', 'arm64'];
if (platformFilter && !VALID_PLATFORMS.includes(platformFilter)) {
console.error(`❌ Invalid platform: ${platformFilter}. Valid options: ${VALID_PLATFORMS.join(', ')}`);
process.exit(1);
}
if (archFilter && !VALID_ARCHS.includes(archFilter)) {
console.error(`❌ Invalid arch: ${archFilter}. Valid options: ${VALID_ARCHS.join(', ')}`);
process.exit(1);
}
let PLATFORMS = ALL_PLATFORMS;
if (currentOnly) {
@ -61,7 +81,7 @@ if (currentOnly) {
}
}
console.log('🚀 Building VibeTunnel for npm distribution...\n');
console.log('🚀 Building VibeTunnel for npm distribution (clean approach)...\n');
if (currentOnly) {
console.log(`📦 Legacy mode: Building for ${process.platform}/${process.arch} only\n`);
@ -202,15 +222,25 @@ function buildMacOS() {
for (const arch of PLATFORMS.darwin || []) {
console.log(` → authenticate-pam for Node.js ${nodeVersion} ${arch}`);
try {
execSync(`npx prebuild --runtime node --target ${nodeVersion}.0.0 --arch ${arch} --tag-prefix authenticate-pam-v`, {
// Use inherit stdio to see any errors during build
const result = execSync(`npx prebuild --runtime node --target ${nodeVersion}.0.0 --arch ${arch} --tag-prefix authenticate-pam-v`, {
cwd: authenticatePamDir,
stdio: 'pipe',
env: { ...process.env, npm_config_target_platform: 'darwin', npm_config_target_arch: arch }
});
// Check if prebuild was actually created
const prebuildFile = path.join(authenticatePamDir, 'prebuilds', `authenticate-pam-v1.0.5-node-v${getNodeAbi(nodeVersion)}-darwin-${arch}.tar.gz`);
if (fs.existsSync(prebuildFile)) {
console.log(` ✅ Created ${path.basename(prebuildFile)}`);
} else {
console.warn(` ⚠️ Prebuild file not created for Node.js ${nodeVersion} ${arch}`);
}
} catch (error) {
console.error(` ❌ Failed to build authenticate-pam for Node.js ${nodeVersion} ${arch}`);
console.error(` Error: ${error.message}`);
process.exit(1);
// Don't exit on macOS authenticate-pam build failures - it might work during npm install
console.warn(` ⚠️ authenticate-pam build failed for macOS (this may be normal)`);
console.warn(` Error: ${error.message}`);
// Continue with other builds instead of exiting
}
}
}
@ -345,38 +375,122 @@ function mergePrebuilds() {
console.log(`✅ Merged prebuilds: ${nodePtyCount} node-pty + ${pamCount} authenticate-pam = ${allPrebuilds.length} total\n`);
}
// Copy authenticate-pam module for Linux support (OUR LINUX FIX)
function copyAuthenticatePam() {
console.log('📦 Copying authenticate-pam module for Linux support...\n');
const srcDir = path.join(ROOT_DIR, 'node_modules', '.pnpm', 'authenticate-pam@1.0.5', 'node_modules', 'authenticate-pam');
const destDir = path.join(DIST_DIR, 'node_modules', 'authenticate-pam');
if (!fs.existsSync(srcDir)) {
console.warn('⚠️ authenticate-pam source not found, Linux PAM auth may not work');
return;
}
// Create destination directory structure
fs.mkdirSync(path.dirname(destDir), { recursive: true });
// Copy entire module
fs.cpSync(srcDir, destDir, { recursive: true });
console.log('✅ authenticate-pam module copied to dist-npm for Linux PAM auth\n');
}
// Enhanced validation (OUR IMPROVEMENT)
function validatePackageHybrid() {
console.log('🔍 Validating hybrid package completeness...\n');
const errors = [];
const warnings = [];
// Check critical files in dist-npm
const criticalFiles = [
'lib/vibetunnel-cli',
'lib/cli.js',
'bin/vibetunnel',
'bin/vt',
'scripts/postinstall.js',
'public/index.html',
'node-pty/package.json',
'node-pty/binding.gyp',
'package.json'
];
for (const file of criticalFiles) {
const fullPath = path.join(DIST_DIR, file);
if (!fs.existsSync(fullPath)) {
errors.push(`Missing critical file: ${file}`);
}
}
// Check prebuilds (only required when not in current-only mode)
const prebuildsDir = path.join(DIST_DIR, 'prebuilds');
if (!currentOnly) {
if (!fs.existsSync(prebuildsDir)) {
errors.push('Missing prebuilds directory in dist-npm');
} else {
const prebuilds = fs.readdirSync(prebuildsDir).filter(f => f.endsWith('.tar.gz'));
if (prebuilds.length === 0) {
warnings.push('No prebuilds found in dist-npm prebuilds directory');
} else {
console.log(` Found ${prebuilds.length} prebuilds in dist-npm`);
}
}
} else {
console.log(' ⚠️ Prebuilds skipped in current-only mode');
}
// Check authenticate-pam (Linux support)
const authPamDir = path.join(DIST_DIR, 'node_modules', 'authenticate-pam');
if (!fs.existsSync(authPamDir)) {
warnings.push('authenticate-pam module not included (Linux PAM auth will not work)');
} else {
console.log(' ✅ authenticate-pam module included for Linux support');
}
// Validate package.json
const packageJsonPath = path.join(DIST_DIR, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Check postinstall script
if (!packageJson.scripts || !packageJson.scripts.postinstall) {
errors.push('Missing postinstall script in package.json');
} else {
console.log(' ✅ Postinstall script configured');
}
}
// Report results
if (errors.length > 0) {
console.error('❌ Package validation failed:');
errors.forEach(err => console.error(` - ${err}`));
process.exit(1);
}
if (warnings.length > 0) {
console.warn('⚠️ Package warnings:');
warnings.forEach(warn => console.warn(` - ${warn}`));
}
console.log('✅ Hybrid package validation passed\n');
}
// Main build process
async function main() {
// Step 0: Temporarily modify package.json for npm packaging
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const originalPackageJson = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(originalPackageJson);
// Step 0: Clean previous build
console.log('0⃣ Cleaning previous build...');
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true });
}
fs.mkdirSync(DIST_DIR, { recursive: true });
// Store original postinstall
const originalPostinstall = packageJson.scripts.postinstall;
// Set install script for npm package
packageJson.scripts.install = 'prebuild-install || node scripts/postinstall-npm.js';
delete packageJson.scripts.postinstall;
// Add prebuild dependencies for npm package only
packageJson.dependencies['prebuild-install'] = '^7.1.3';
// Write modified package.json
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
// Restore original package.json on exit
const restorePackageJson = () => {
fs.writeFileSync(packageJsonPath, originalPackageJson);
};
process.on('exit', restorePackageJson);
process.on('SIGINT', () => { restorePackageJson(); process.exit(1); });
process.on('SIGTERM', () => { restorePackageJson(); process.exit(1); });
// Step 1: Standard build process (includes spawn-helper)
console.log('1⃣ Running standard build process...\n');
// Step 1: Standard build process
console.log('\n1⃣ Running standard build process...\n');
try {
execSync('node scripts/build.js', { stdio: 'inherit' });
execSync('npm run build', {
cwd: ROOT_DIR,
stdio: 'inherit'
});
console.log('✅ Standard build completed\n');
} catch (error) {
console.error('❌ Standard build failed:', error.message);
@ -406,25 +520,199 @@ async function main() {
mergePrebuilds();
}
// Step 3: Ensure node-pty is built for current platform
console.log('3⃣ Ensuring node-pty is built for current platform...\n');
const nodePtyBuild = path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node');
if (!fs.existsSync(nodePtyBuild)) {
console.log(' Building node-pty for current platform...');
const nodePtyDir = path.join(__dirname, '..', 'node-pty');
try {
execSync('npm run install', { cwd: nodePtyDir, stdio: 'inherit' });
console.log('✅ node-pty built successfully');
} catch (error) {
console.error('❌ Failed to build node-pty:', error.message);
process.exit(1);
// Step 3: Copy necessary files to dist-npm
console.log('3⃣ Copying files to dist-npm...\n');
const filesToCopy = [
// Compiled CLI
{ src: 'dist/vibetunnel-cli', dest: 'lib/cli.js' },
{ src: 'dist/tsconfig.server.tsbuildinfo', dest: 'lib/tsconfig.server.tsbuildinfo' },
// Bin scripts
{ src: 'bin', dest: 'bin' },
// Public assets
{ src: 'public', dest: 'public' },
// Node-pty module (bundled)
{ src: 'node-pty/lib', dest: 'node-pty/lib' },
{ src: 'node-pty/src', dest: 'node-pty/src' },
{ src: 'node-pty/binding.gyp', dest: 'node-pty/binding.gyp' },
{ src: 'node-pty/package.json', dest: 'node-pty/package.json' },
{ src: 'node-pty/README.md', dest: 'node-pty/README.md' },
// Prebuilds
{ src: 'prebuilds', dest: 'prebuilds' },
// Scripts
{ src: 'scripts/postinstall-npm.js', dest: 'scripts/postinstall.js' },
{ src: 'scripts/node-pty-plugin.js', dest: 'scripts/node-pty-plugin.js' }
];
function copyRecursive(src, dest) {
const srcPath = path.join(ROOT_DIR, src);
const destPath = path.join(DIST_DIR, dest);
if (!fs.existsSync(srcPath)) {
console.warn(` ⚠️ Source not found: ${src}`);
return;
}
} else {
console.log('✅ node-pty already built');
const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const stats = fs.statSync(srcPath);
if (stats.isDirectory()) {
fs.cpSync(srcPath, destPath, { recursive: true });
} else {
fs.copyFileSync(srcPath, destPath);
}
console.log(`${src}${dest}`);
}
// Step 4: Create package-specific README
console.log('\n4⃣ Creating npm package README...\n');
filesToCopy.forEach(({ src, dest }) => {
copyRecursive(src, dest);
});
// Step 4: Copy authenticate-pam module for Linux support (OUR ENHANCEMENT)
copyAuthenticatePam();
// Step 5: Use package.npm.json if available, otherwise create clean package.json
console.log('\n4⃣ Creating package.json for npm...\n');
const npmPackageJsonPath = path.join(ROOT_DIR, 'package.npm.json');
let npmPackageJson;
if (fs.existsSync(npmPackageJsonPath)) {
// Use our enhanced package.npm.json
console.log('Using package.npm.json configuration...');
npmPackageJson = JSON.parse(fs.readFileSync(npmPackageJsonPath, 'utf8'));
// Remove prebuild-install dependency (our approach is better)
if (npmPackageJson.dependencies && npmPackageJson.dependencies['prebuild-install']) {
delete npmPackageJson.dependencies['prebuild-install'];
console.log('✅ Removed problematic prebuild-install dependency');
}
} else {
// Fallback to creating clean package.json from source
console.log('Creating clean package.json from source...');
const sourcePackageJson = JSON.parse(
fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8')
);
// Extract only necessary fields for npm package
npmPackageJson = {
name: sourcePackageJson.name,
version: sourcePackageJson.version,
description: sourcePackageJson.description,
keywords: sourcePackageJson.keywords,
author: sourcePackageJson.author,
license: sourcePackageJson.license,
homepage: sourcePackageJson.homepage,
repository: sourcePackageJson.repository,
bugs: sourcePackageJson.bugs,
// Main entry point
main: 'lib/cli.js',
// Bin scripts
bin: {
vibetunnel: './bin/vibetunnel',
vt: './bin/vt'
},
// Only runtime dependencies
dependencies: Object.fromEntries(
Object.entries(sourcePackageJson.dependencies)
.filter(([key]) => !key.includes('node-pty')) // Exclude node-pty, it's bundled
),
// Minimal scripts
scripts: {
postinstall: 'node scripts/postinstall.js'
},
// Node.js requirements
engines: sourcePackageJson.engines,
os: sourcePackageJson.os,
// Files to include (everything in dist-npm)
files: [
'lib/',
'bin/',
'public/',
'node-pty/',
'prebuilds/',
'scripts/',
'README.md'
]
};
}
fs.writeFileSync(
path.join(DIST_DIR, 'package.json'),
JSON.stringify(npmPackageJson, null, 2) + '\n'
);
// Step 6: Fix the CLI structure and bin scripts
console.log('\n6⃣ Fixing CLI structure and bin scripts...\n');
// The dist/vibetunnel-cli was copied to lib/cli.js
// We need to rename it and create a wrapper
const cliPath = path.join(DIST_DIR, 'lib', 'cli.js');
const cliBundlePath = path.join(DIST_DIR, 'lib', 'vibetunnel-cli');
// Rename the bundle
fs.renameSync(cliPath, cliBundlePath);
// Create a simple wrapper that requires the bundle
const cliWrapperContent = `#!/usr/bin/env node
require('./vibetunnel-cli');
`;
fs.writeFileSync(cliPath, cliWrapperContent, { mode: 0o755 });
// Fix bin scripts to point to correct path
const binVibetunnelPath = path.join(DIST_DIR, 'bin', 'vibetunnel');
const binVibetunnelContent = `#!/usr/bin/env node
// Start the CLI - it handles all command routing including 'fwd'
const { spawn } = require('child_process');
const path = require('path');
const cliPath = path.join(__dirname, '..', 'lib', 'vibetunnel-cli');
const args = process.argv.slice(2);
const child = spawn('node', [cliPath, ...args], {
stdio: 'inherit',
env: process.env
});
child.on('exit', (code, signal) => {
if (signal) {
// Process was killed by signal, exit with 128 + signal number convention
// Common signals: SIGTERM=15, SIGINT=2, SIGKILL=9
const signalExitCode = signal === 'SIGTERM' ? 143 :
signal === 'SIGINT' ? 130 :
signal === 'SIGKILL' ? 137 : 128;
process.exit(signalExitCode);
} else {
// Normal exit, use the exit code (or 0 if null)
process.exit(code ?? 0);
}
});
`;
fs.writeFileSync(binVibetunnelPath, binVibetunnelContent, { mode: 0o755 });
console.log(' ✓ Fixed bin/vibetunnel path');
// vt script doesn't need fixing - it dynamically finds the binary
// Step 7: Create README
console.log('\n7⃣ Creating npm README...\n');
const readmeContent = `# VibeTunnel CLI
Full-featured terminal sharing server with web interface for macOS and Linux. Windows not yet supported.
@ -435,87 +723,85 @@ Full-featured terminal sharing server with web interface for macOS and Linux. Wi
npm install -g vibetunnel
\`\`\`
## Requirements
- Node.js >= 20.0.0
- macOS or Linux (Windows not yet supported)
- Build tools for native modules (Xcode on macOS, build-essential on Linux)
## Usage
### Start the server
## Quick Start
\`\`\`bash
# Start with default settings (port 4020)
# Start VibeTunnel server
vibetunnel
# Start with custom port
vibetunnel --port 8080
# Or use short alias
vt
# Start without authentication
vibetunnel --no-auth
\`\`\`
# Custom port and settings
vibetunnel --port 4020 --auth
Then open http://localhost:4020 in your browser to access the web interface.
### Use the vt command wrapper
The \`vt\` command allows you to run commands with TTY forwarding:
\`\`\`bash
# Monitor AI agents with automatic activity tracking
vt claude
vt claude --dangerously-skip-permissions
# Run commands with output visible in VibeTunnel
vt npm test
vt python script.py
vt top
# Launch interactive shell
vt --shell
vt -i
# Update session title (inside a session)
vt title "My Project"
\`\`\`
### Forward commands to a session
\`\`\`bash
# Basic usage
vibetunnel fwd <session-id> <command> [args...]
# Examples
vibetunnel fwd --session-id abc123 ls -la
vibetunnel fwd --session-id abc123 npm test
vibetunnel fwd --session-id abc123 python script.py
# Forward mode (connect to remote VibeTunnel)
vibetunnel fwd username@hostname
\`\`\`
## Features
- **Web-based terminal interface** - Access terminals from any browser
- **Multiple concurrent sessions** - Run multiple terminals simultaneously
- **Real-time synchronization** - See output in real-time
- **TTY forwarding** - Full terminal emulation support
- **Session management** - Create, list, and manage sessions
- **Cross-platform** - Works on macOS and Linux
- **No dependencies** - Just Node.js required
- **Terminal Sharing**: Share your terminal through a web browser
- **Web Interface**: Access terminals from any device with a browser
- **Session Management**: Create, manage, and switch between multiple terminal sessions
- **Authentication**: Built-in authentication system
- **Cross-Platform**: Works on macOS and Linux
## Package Contents
## Requirements
This npm package includes:
- Full VibeTunnel server with web UI
- Command-line tools (vibetunnel, vt)
- Native PTY support for terminal emulation
- Web interface with xterm.js
- Session management and forwarding
- **Node.js**: Version 20 or higher
- **Operating System**: macOS or Linux (Windows not yet supported)
- **Build Tools**: For source compilation fallback (make, gcc, python3)
## Platform Support
- macOS (Intel and Apple Silicon)
- Linux (x64 and ARM64)
- Windows: Not yet supported ([#252](https://github.com/amantus-ai/vibetunnel/issues/252))
### macOS
- Intel (x64) and Apple Silicon (arm64)
- Requires Xcode Command Line Tools for source compilation
### Linux
- x64 and ARM64 architectures
- PAM authentication support
- Automatic fallback to source compilation when prebuilds unavailable
## Configuration
VibeTunnel can be configured via command line arguments:
\`\`\`bash
vibetunnel --help
\`\`\`
## Troubleshooting
### Installation Issues
If you encounter issues during installation:
1. **Missing Build Tools**: Install build essentials
\`\`\`bash
# Ubuntu/Debian
sudo apt-get install build-essential python3-dev
# macOS
xcode-select --install
\`\`\`
2. **Permission Issues**: Use sudo for global installation
\`\`\`bash
sudo npm install -g vibetunnel
\`\`\`
3. **Node Version**: Ensure Node.js 20+ is installed
\`\`\`bash
node --version
\`\`\`
### Runtime Issues
- **Server Won't Start**: Check if port is already in use
- **Authentication Failed**: Verify system authentication setup
- **Terminal Not Responsive**: Check browser console for WebSocket errors
## Documentation
@ -525,20 +811,21 @@ See the main repository for complete documentation: https://github.com/amantus-a
MIT
`;
const readmePath = path.join(__dirname, '..', 'README.md');
fs.writeFileSync(readmePath, readmeContent);
console.log('✅ npm README created');
// Step 5: Clean up test files (keep screencap.js - it's needed)
console.log('\n5⃣ Cleaning up test files...\n');
fs.writeFileSync(
path.join(DIST_DIR, 'README.md'),
readmeContent
);
// Step 8: Clean up test files in dist-npm
console.log('\n8⃣ Cleaning up test files...\n');
const testFiles = [
'public/bundle/test.js',
'public/test' // Remove entire test directory
];
for (const file of testFiles) {
const filePath = path.join(__dirname, '..', file);
const filePath = path.join(DIST_DIR, file);
if (fs.existsSync(filePath)) {
if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
@ -549,50 +836,40 @@ MIT
}
}
}
// Step 9: Validate package with our comprehensive checks
validatePackageHybrid();
// Step 6: Show final package info
console.log('\n6⃣ Package summary...\n');
// Calculate total size
function getDirectorySize(dirPath) {
let totalSize = 0;
const items = fs.readdirSync(dirPath);
// Step 10: Create npm package
console.log('\n9⃣ Creating npm package...\n');
try {
execSync('npm pack', {
cwd: DIST_DIR,
stdio: 'inherit'
});
for (const item of items) {
const itemPath = path.join(dirPath, item);
const stats = fs.statSync(itemPath);
if (stats.isFile()) {
totalSize += stats.size;
} else if (stats.isDirectory()) {
totalSize += getDirectorySize(itemPath);
}
// Move the package to root directory
const packageFiles = fs.readdirSync(DIST_DIR)
.filter(f => f.endsWith('.tgz'));
if (packageFiles.length > 0) {
const packageFile = packageFiles[0];
fs.renameSync(
path.join(DIST_DIR, packageFile),
path.join(ROOT_DIR, packageFile)
);
console.log(`\n✅ Package created: ${packageFile}`);
}
return totalSize;
} catch (error) {
console.error('❌ npm pack failed:', error.message);
process.exit(1);
}
const packageRoot = path.join(__dirname, '..');
const totalSize = getDirectorySize(packageRoot);
const sizeMB = (totalSize / 1024 / 1024).toFixed(1);
console.log(`📦 Package size: ${sizeMB} MB`);
if (!currentOnly) {
const prebuildsDir = path.join(__dirname, '..', 'prebuilds');
if (fs.existsSync(prebuildsDir)) {
const prebuildFiles = fs.readdirSync(prebuildsDir).filter(f => f.endsWith('.tar.gz'));
console.log(`🔧 Prebuilds: ${prebuildFiles.length} binaries included`);
}
}
console.log('\n🎉 npm package build completed successfully!');
console.log('\n🎉 Hybrid npm build completed successfully!');
console.log('\nNext steps:');
console.log(' - Test locally: npm pack');
console.log(' - Test locally: npm pack && npm install -g vibetunnel-*.tgz');
console.log(' - Test Linux compatibility: Check authenticate-pam and fallback compilation');
console.log(' - Publish: npm publish');
// Restore original package.json
restorePackageJson();
}
main().catch(error => {

View file

@ -3,6 +3,7 @@ const fs = require('fs');
const path = require('path');
const esbuild = require('esbuild');
const { prodOptions } = require('./esbuild-config.js');
const { nodePtyPlugin } = require('./node-pty-plugin.js');
async function build() {
console.log('Starting build process...');
@ -76,9 +77,22 @@ async function build() {
target: 'node18',
format: 'cjs',
outfile: 'dist/vibetunnel-cli',
plugins: [nodePtyPlugin],
external: [
'node-pty',
// 'node-pty', // Removed - handled by plugin
'authenticate-pam',
'compression',
'helmet',
'express',
'ws',
'jsonwebtoken',
'web-push',
'bonjour-service',
'signal-exit',
'http-proxy-middleware',
'multer',
'mime-types',
'@xterm/headless',
],
minify: true,
sourcemap: false,

View file

@ -0,0 +1,73 @@
/**
* ESBuild plugin to handle node-pty resolution for npm packages
*/
const path = require('path');
const fs = require('fs');
const nodePtyPlugin = {
name: 'node-pty-resolver',
setup(build) {
// Resolve node-pty imports to our bundled version
build.onResolve({ filter: /^node-pty$/ }, args => {
// In development, use the normal node_modules resolution
if (process.env.NODE_ENV === 'development') {
return null;
}
// For npm builds, resolve to our bundled node-pty
return {
path: 'node-pty',
namespace: 'node-pty-stub'
};
});
// Provide stub that dynamically loads the bundled node-pty
build.onLoad({ filter: /^node-pty$/, namespace: 'node-pty-stub' }, () => {
return {
contents: `
const path = require('path');
const fs = require('fs');
// Try multiple possible locations for node-pty
const possiblePaths = [
// When installed via npm
path.join(__dirname, '../node-pty'),
path.join(__dirname, '../../node-pty'),
// During development
path.join(__dirname, '../node_modules/node-pty'),
// Fallback to regular require
'node-pty'
];
let nodePty;
let loadError;
for (const tryPath of possiblePaths) {
try {
if (tryPath === 'node-pty') {
// Try regular require as last resort
nodePty = require(tryPath);
} else if (fs.existsSync(tryPath)) {
// Check if the path exists before trying to load
nodePty = require(tryPath);
}
if (nodePty) break;
} catch (err) {
loadError = err;
}
}
if (!nodePty) {
throw new Error(\`Failed to load node-pty from any location. Last error: \${loadError?.message}\`);
}
module.exports = nodePty;
`,
loader: 'js'
};
});
}
};
module.exports = { nodePtyPlugin };

View file

@ -2,12 +2,13 @@
/**
* Postinstall script for npm package
* Fallback build script when prebuild-install fails
* Handles prebuild extraction and fallback compilation
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');
console.log('Setting up native modules for VibeTunnel...');
@ -20,125 +21,264 @@ if (isDevelopment) {
return;
}
// Try prebuild-install first for each module
const tryPrebuildInstall = (name, dir) => {
console.log(`Trying prebuild-install for ${name}...`);
// Create node_modules directory if it doesn't exist
const nodeModulesDir = path.join(__dirname, '..', 'node_modules');
if (!fs.existsSync(nodeModulesDir)) {
fs.mkdirSync(nodeModulesDir, { recursive: true });
}
// Create symlink for node-pty so it can be required normally
const nodePtySource = path.join(__dirname, '..', 'node-pty');
const nodePtyTarget = path.join(nodeModulesDir, 'node-pty');
if (!fs.existsSync(nodePtyTarget) && fs.existsSync(nodePtySource)) {
try {
execSync('prebuild-install', {
cwd: dir,
fs.symlinkSync(nodePtySource, nodePtyTarget, 'dir');
console.log('✓ Created node-pty symlink in node_modules');
} catch (error) {
console.warn('Warning: Could not create node-pty symlink:', error.message);
}
}
// Get Node ABI version
const nodeABI = process.versions.modules;
// Get platform and architecture
const platform = process.platform;
const arch = os.arch();
// Convert architecture names
const archMap = {
'arm64': 'arm64',
'aarch64': 'arm64',
'x64': 'x64',
'x86_64': 'x64'
};
const normalizedArch = archMap[arch] || arch;
console.log(`Platform: ${platform}-${normalizedArch}, Node ABI: ${nodeABI}`);
// Function to try prebuild-install first
const tryPrebuildInstall = (moduleName, moduleDir) => {
try {
// Check if prebuild-install is available
const prebuildInstallPath = require.resolve('prebuild-install/bin.js');
console.log(` Attempting to use prebuild-install for ${moduleName}...`);
execSync(`node "${prebuildInstallPath}"`, {
cwd: moduleDir,
stdio: 'inherit',
env: { ...process.env, npm_config_cache: path.join(require('os').homedir(), '.npm') }
env: { ...process.env, npm_config_build_from_source: 'false' }
});
console.log(`${name} prebuilt binary installed`);
return true;
} catch (error) {
console.log(` No prebuilt binary available for ${name}, will compile from source`);
console.log(` prebuild-install failed for ${moduleName}, will try manual extraction`);
return false;
}
};
// Handle both native modules with prebuild-install fallback
// Function to manually extract prebuild
const extractPrebuild = (name, version, targetDir) => {
const prebuildFile = path.join(__dirname, '..', 'prebuilds',
`${name}-v${version}-node-v${nodeABI}-${platform}-${normalizedArch}.tar.gz`);
if (!fs.existsSync(prebuildFile)) {
console.log(` No prebuild found for ${name} on this platform`);
return false;
}
// Create the parent directory
const buildParentDir = path.join(targetDir);
fs.mkdirSync(buildParentDir, { recursive: true });
try {
// Extract directly into the module directory - the tar already contains build/Release structure
execSync(`tar -xzf "${prebuildFile}" -C "${buildParentDir}"`, { stdio: 'inherit' });
console.log(`${name} prebuilt binary extracted`);
return true;
} catch (error) {
console.error(` Failed to extract ${name} prebuild:`, error.message);
return false;
}
};
// Function to compile from source
const compileFromSource = (moduleName, moduleDir) => {
console.log(` Building ${moduleName} from source...`);
try {
// First check if node-gyp is available
try {
execSync('node-gyp --version', { stdio: 'pipe' });
} catch (e) {
console.log(' Installing node-gyp...');
execSync('npm install -g node-gyp', { stdio: 'inherit' });
}
// For node-pty, ensure node-addon-api is available
if (moduleName === 'node-pty') {
const nodeAddonApiPath = path.join(moduleDir, 'node_modules', 'node-addon-api');
if (!fs.existsSync(nodeAddonApiPath)) {
console.log(` Setting up node-addon-api for ${moduleName}...`);
// Create node_modules directory
const nodeModulesDir = path.join(moduleDir, 'node_modules');
fs.mkdirSync(nodeModulesDir, { recursive: true });
// Try multiple locations for node-addon-api
const possiblePaths = [
path.join(__dirname, '..', 'node_modules', 'node-addon-api'),
path.join(__dirname, '..', '..', 'node_modules', 'node-addon-api'),
path.join(__dirname, '..', '..', '..', 'node_modules', 'node-addon-api'),
'/usr/local/lib/node_modules/vibetunnel/node_modules/node-addon-api',
'/usr/lib/node_modules/vibetunnel/node_modules/node-addon-api'
];
let found = false;
for (const sourcePath of possiblePaths) {
if (fs.existsSync(sourcePath)) {
console.log(` Found node-addon-api at: ${sourcePath}`);
console.log(` Copying to ${nodeAddonApiPath}...`);
fs.cpSync(sourcePath, nodeAddonApiPath, { recursive: true });
found = true;
break;
}
}
if (!found) {
// As a fallback, install it
console.log(` Installing node-addon-api package...`);
try {
execSync('npm install node-addon-api@^7.1.0 --no-save --no-package-lock', {
cwd: moduleDir,
stdio: 'inherit'
});
} catch (e) {
console.error(' Failed to install node-addon-api:', e.message);
console.error(' Trying to continue anyway...');
}
}
}
}
execSync('node-gyp rebuild', {
cwd: moduleDir,
stdio: 'inherit'
});
console.log(`${moduleName} built successfully`);
return true;
} catch (error) {
console.error(` Failed to build ${moduleName}:`, error.message);
return false;
}
};
// Handle both native modules
const modules = [
{
name: 'node-pty',
version: '1.0.0',
dir: path.join(__dirname, '..', 'node-pty'),
build: path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node'),
essential: true
},
{
name: 'authenticate-pam',
version: '1.0.5',
dir: path.join(__dirname, '..', 'node_modules', 'authenticate-pam'),
build: path.join(__dirname, '..', 'node_modules', 'authenticate-pam', 'build', 'Release', 'authenticate_pam.node'),
essential: false
essential: true, // PAM is essential for server environments
platforms: ['linux', 'darwin'] // Needed on Linux and macOS
}
];
let hasErrors = false;
for (const module of modules) {
if (!fs.existsSync(module.build)) {
// First try prebuild-install
const prebuildSuccess = tryPrebuildInstall(module.name, module.dir);
if (!prebuildSuccess) {
// Fall back to compilation
console.log(`Building ${module.name} from source...`);
try {
execSync('node-gyp rebuild', {
cwd: module.dir,
stdio: 'inherit'
});
console.log(`${module.name} built successfully`);
} catch (error) {
console.error(`Failed to build ${module.name}:`, error.message);
if (module.essential) {
console.error(`${module.name} is required for VibeTunnel to function.`);
console.error('You may need to install build tools for your platform:');
console.error('- macOS: Install Xcode Command Line Tools');
console.error('- Linux: Install build-essential package');
hasErrors = true;
} else {
console.warn(`Warning: ${module.name} build failed. Some features may be limited.`);
}
}
console.log(`\nProcessing ${module.name}...`);
// Skip platform-specific modules if not on that platform
if (module.platforms && !module.platforms.includes(platform)) {
console.log(` Skipping ${module.name} (not needed on ${platform})`);
continue;
}
// Check if module directory exists
if (!fs.existsSync(module.dir)) {
console.warn(` Warning: ${module.name} directory not found at ${module.dir}`);
if (module.essential) {
hasErrors = true;
}
} else {
continue;
}
// Check if already built
if (fs.existsSync(module.build)) {
console.log(`${module.name} already available`);
continue;
}
// Try installation methods in order
let success = false;
// Method 1: Try prebuild-install (preferred)
success = tryPrebuildInstall(module.name, module.dir);
// Method 2: Manual prebuild extraction
if (!success) {
success = extractPrebuild(module.name, module.version, module.dir);
}
// Method 3: Compile from source
if (!success && fs.existsSync(path.join(module.dir, 'binding.gyp'))) {
success = compileFromSource(module.name, module.dir);
}
// Check final result
if (!success) {
// Special handling for authenticate-pam on macOS
if (module.name === 'authenticate-pam' && process.platform === 'darwin') {
console.warn(`⚠️ Warning: ${module.name} installation failed on macOS.`);
console.warn(' This is expected - macOS will fall back to environment variable or SSH key authentication.');
console.warn(' To enable PAM authentication, install Xcode Command Line Tools and rebuild.');
} else if (module.essential) {
console.error(`\n${module.name} is required for VibeTunnel to function.`);
console.error('You may need to install build tools for your platform:');
console.error('- macOS: Install Xcode Command Line Tools');
console.error('- Linux: Install build-essential and libpam0g-dev packages');
hasErrors = true;
} else {
console.warn(`⚠️ Warning: ${module.name} installation failed. Some features may be limited.`);
}
}
}
// Install vt symlink/wrapper
if (!hasErrors && !isDevelopment) {
console.log('\nSetting up vt command...');
const vtSource = path.join(__dirname, '..', 'bin', 'vt');
// Check if vt script exists
if (!fs.existsSync(vtSource)) {
console.warn('⚠️ vt command script not found in package');
console.log(' Use "vibetunnel" command instead');
} else {
try {
// Make vt script executable
fs.chmodSync(vtSource, '755');
console.log('✓ vt command configured');
console.log(' Note: The vt command is available through npm/npx');
} catch (error) {
console.warn('⚠️ Could not configure vt command:', error.message);
console.log(' Use "vibetunnel" command instead');
}
}
}
if (hasErrors) {
console.error('\n❌ Setup failed with errors');
process.exit(1);
}
// Conditionally install vt symlink
if (!isDevelopment) {
try {
// Find npm's global bin directory
const npmBinDir = execSync('npm bin -g', { encoding: 'utf8' }).trim();
const vtTarget = path.join(npmBinDir, 'vt');
const vtSource = path.join(__dirname, '..', 'bin', 'vt');
// Check if vt already exists
if (fs.existsSync(vtTarget)) {
// Check if it's already our symlink
try {
const stats = fs.lstatSync(vtTarget);
if (stats.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(vtTarget);
if (linkTarget.includes('vibetunnel')) {
console.log('✓ vt command already installed (VibeTunnel)');
} else {
console.log('⚠️ vt command already exists (different tool)');
console.log(' Use "vibetunnel" command or "npx vt" instead');
}
} else {
console.log('⚠️ vt command already exists (not a symlink)');
console.log(' Use "vibetunnel" command instead');
}
} catch (e) {
// Ignore errors checking the existing file
console.log('⚠️ vt command already exists');
console.log(' Use "vibetunnel" command instead');
}
} else {
// Create the symlink
try {
fs.symlinkSync(vtSource, vtTarget);
// Make it executable
fs.chmodSync(vtTarget, '755');
console.log('✓ vt command installed successfully');
} catch (error) {
console.warn('⚠️ Could not install vt command:', error.message);
console.log(' Use "vibetunnel" command instead');
}
}
} catch (error) {
// If we can't determine npm bin dir or create symlink, just warn
console.warn('⚠️ Could not install vt command:', error.message);
console.log(' Use "vibetunnel" command instead');
}
}
console.log('✓ VibeTunnel is ready to use');
console.log('Run "vibetunnel --help" for usage information');
} else {
console.log('\n✅ VibeTunnel is ready to use');
console.log('Run "vibetunnel --help" for usage information');
}

View file

@ -1,55 +0,0 @@
#!/bin/bash
set -e
echo "Testing Docker build for Linux x64..."
# Create the build script
cat > docker-build-test.sh << 'EOF'
#!/bin/bash
set -e
echo "Installing build dependencies..."
apt-get update && apt-get install -y python3 make g++ git
echo "Setting up project..."
cd /workspace
# Fix npm permissions issue in Docker
mkdir -p ~/.npm
chown -R $(id -u):$(id -g) ~/.npm
# Install pnpm using corepack (more reliable)
corepack enable
corepack prepare pnpm@latest --activate
# Install dependencies
cd /workspace
pnpm install --ignore-scripts --no-frozen-lockfile
# Go to node-pty directory
cd node-pty
# Install prebuild locally in node-pty
pnpm add -D prebuild
# Build for Node.js 20
echo "Building for Node.js 20..."
./node_modules/.bin/prebuild --runtime node --target 20.0.0
# List results
echo "Build complete. Prebuilds:"
ls -la prebuilds/
EOF
chmod +x docker-build-test.sh
# Run the test
docker run --rm \
-v "$(pwd)":/workspace \
-w /workspace \
--platform linux/amd64 \
node:22-bookworm \
/workspace/docker-build-test.sh
# Clean up
rm docker-build-test.sh

View file

@ -1,35 +0,0 @@
#!/bin/bash
# Script to run Playwright tests with parallel configuration
echo "Running Playwright tests with parallel configuration..."
echo ""
# Run all tests (parallel and serial)
if [ "$1" == "all" ]; then
echo "Running all tests (parallel + serial)..."
pnpm exec playwright test
elif [ "$1" == "parallel" ]; then
echo "Running only parallel tests..."
pnpm exec playwright test --project=chromium-parallel
elif [ "$1" == "serial" ]; then
echo "Running only serial tests..."
pnpm exec playwright test --project=chromium-serial
elif [ "$1" == "debug" ]; then
echo "Running tests in debug mode..."
pnpm exec playwright test --debug
elif [ "$1" == "ui" ]; then
echo "Running tests with UI mode..."
pnpm exec playwright test --ui
else
echo "Usage: ./scripts/test-parallel.sh [all|parallel|serial|debug|ui]"
echo ""
echo "Options:"
echo " all - Run all tests (parallel and serial)"
echo " parallel - Run only parallel tests"
echo " serial - Run only serial tests"
echo " debug - Run tests in debug mode"
echo " ui - Run tests with Playwright UI"
echo ""
echo "If no option is provided, this help message is shown."
fi

View file

@ -1,9 +0,0 @@
#!/bin/bash
# Clean up existing sessions
echo "Cleaning up existing sessions..."
rm -rf ~/.vibetunnel/control/*
# Run Playwright tests
echo "Running Playwright tests..."
pnpm playwright test "$@"

View file

@ -54,8 +54,16 @@ process.on('unhandledRejection', (reason, promise) => {
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)) {
// Only execute if this is the main module (or in SEA/bundled context where require.main is undefined)
// In bundled builds, both module.parent and require.main are undefined
// In npm package context, check if we're the actual CLI entry point
const isMainModule =
!module.parent &&
(require.main === module ||
require.main === undefined ||
(require.main?.filename?.endsWith('/vibetunnel-cli') ?? false));
if (isMainModule) {
if (process.argv[2] === 'version') {
console.log(`VibeTunnel Server v${VERSION}`);
process.exit(0);

View file

@ -65,6 +65,13 @@ describe('SessionView', () => {
// Setup fetch mock
fetchMock = setupFetchMock();
// Mock the server status endpoint that's called on component connect
fetchMock.mockResponse('/api/server/status', {
macAppConnected: false,
cloudflareEnabled: false,
isDevelopmentServer: false,
});
// Create component
element = await fixture<SessionView>(html` <session-view></session-view> `);

View file

@ -559,7 +559,76 @@ export async function createApp(): Promise<AppInstance> {
});
// Serve static files with .html extension handling and caching headers
const publicPath = path.join(process.cwd(), 'public');
// In production/bundled mode, use the package directory; in development, use cwd
const getPublicPath = () => {
// More precise npm package detection:
// 1. Check if we're explicitly in an npm package structure
// 2. The file should be in node_modules/vibetunnel/lib/
// 3. Or check for our specific package markers
const isNpmPackage = (() => {
// Most reliable: check if we're in node_modules/vibetunnel structure
if (__filename.includes(path.join('node_modules', 'vibetunnel', 'lib'))) {
return true;
}
// Check for Windows path variant
if (__filename.includes('node_modules\\vibetunnel\\lib')) {
return true;
}
// Secondary check: if we're in a lib directory, verify it's actually an npm package
// by checking for the existence of package.json in the parent directory
if (path.basename(__dirname) === 'lib') {
const parentDir = path.dirname(__dirname);
const packageJsonPath = path.join(parentDir, 'package.json');
try {
const packageJson = require(packageJsonPath);
// Verify this is actually our package
return packageJson.name === 'vibetunnel';
} catch {
// Not a valid npm package structure
return false;
}
}
return false;
})();
if (process.env.VIBETUNNEL_BUNDLED === 'true' || process.env.BUILD_DATE || isNpmPackage) {
// In bundled/production/npm mode, find package root
// When bundled, __dirname is /path/to/package/dist, so go up one level
// When globally installed, we need to find the package root
let packageRoot = __dirname;
// If we're in the dist directory, go up one level
if (path.basename(packageRoot) === 'dist') {
packageRoot = path.dirname(packageRoot);
}
// For npm package context, if we're in lib directory, go up one level
if (path.basename(packageRoot) === 'lib') {
packageRoot = path.dirname(packageRoot);
}
// Look for package.json to confirm we're in the right place
const publicPath = path.join(packageRoot, 'public');
const indexPath = path.join(publicPath, 'index.html');
// If index.html exists, we found the right path
if (require('fs').existsSync(indexPath)) {
return publicPath;
}
// Fallback: try going up from the bundled CLI location
// The bundled CLI might be in node_modules/vibetunnel/dist/
return path.join(__dirname, '..', 'public');
} else {
// In development mode, use current working directory
return path.join(process.cwd(), 'public');
}
};
const publicPath = getPublicPath();
const isDevelopment = !process.env.BUILD_DATE || process.env.NODE_ENV === 'development';
app.use(