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` - Pull requests to `main`
- Only when files in `web/` directory change - 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. Advanced workflow for testing Single Executable Application (SEA) builds with custom Node.js.
**Features:** **Features:**
@ -46,7 +62,7 @@ Advanced workflow for testing Single Executable Application (SEA) builds with cu
- Helps identify any Blacksmith-specific issues - Helps identify any Blacksmith-specific issues
- Runs only on push events - 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. Tests the macOS Xcode build with custom Node.js to ensure the VibeTunnel.app works correctly with SEA executables.
**Features:** **Features:**

View file

@ -154,7 +154,7 @@ jobs:
run: | run: |
echo "Resolving Swift package dependencies..." echo "Resolving Swift package dependencies..."
# Workspace is at root level # 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 # Debug: List available schemes
echo "=== Available schemes ===" echo "=== Available schemes ==="
@ -179,25 +179,6 @@ jobs:
DEVELOPMENT_TEAM="" \ DEVELOPMENT_TEAM="" \
COMPILER_INDEX_STORE_ENABLE=NO 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 # LINT PHASE
- name: Run SwiftFormat (check mode) - name: Run SwiftFormat (check mode)
id: swiftformat 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 working-directory: web
run: pnpm install --frozen-lockfile 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 - name: Build frontend and backend
working-directory: web working-directory: web
run: pnpm run build:ci run: pnpm run build:ci
@ -327,6 +332,11 @@ jobs:
working-directory: web working-directory: web
run: pnpm install --frozen-lockfile 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 - name: Check TypeScript types
working-directory: web working-directory: web
run: pnpm run typecheck 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 - name: Run linting
run: pnpm run lint run: pnpm run lint
- name: Build node-pty for TypeScript
run: |
cd node-pty && npm install && npm run build
- name: Run type checking - name: Run type checking
run: pnpm run typecheck run: pnpm run typecheck
@ -84,6 +88,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build node-pty
run: |
cd node-pty && npm install && npm run build
- name: Build project - name: Build project
run: pnpm run build run: pnpm run build
@ -143,4 +151,5 @@ jobs:
with: with:
name: server-coverage-report name: server-coverage-report
path: web/coverage/server/ 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 # Generated files
public/ public/
dist/ dist/
dist-npm/
# Logs # Logs
*.log *.log

View file

@ -1,4 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
// Start the CLI - it handles all command routing including 'fwd' // 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) - **Dependencies**: None (vendored implementation)
### 2. authenticate-pam (Authentication) ### 2. authenticate-pam (Authentication)
- **Purpose**: PAM (Pluggable Authentication Modules) integration - **Purpose**: PAM (Pluggable Authentication Modules) integration for system authentication
- **Components**: - **Components**:
- `authenticate_pam.node`: Node.js addon for system authentication - `authenticate_pam.node`: Node.js addon for system authentication
- **Platforms**: Linux primarily, macOS for compatibility - **Platforms**: Both macOS and Linux
- **Dependencies**: System PAM libraries - **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 ## Prebuild System
@ -55,7 +56,9 @@ We use `prebuild` and `prebuild-install` to provide precompiled native modules,
### Coverage ### Coverage
- **Node.js versions**: 20, 22, 23, 24 - **Node.js versions**: 20, 22, 23, 24
- **Platforms**: macOS (x64, arm64), Linux (x64, arm64) - **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 ### 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-darwin-x64.tar.gz
├── node-pty-v1.0.0-node-v115-linux-arm64.tar.gz ├── node-pty-v1.0.0-node-v115-linux-arm64.tar.gz
├── node-pty-v1.0.0-node-v115-linux-x64.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-arm64.tar.gz
├── authenticate-pam-v1.0.5-node-v115-linux-x64.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) Note: Node version numbers map to internal versions (v115=Node 20, v127=Node 22, v131=Node 23, v134=Node 24)
## Build Process ## 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) ### Unified Build (Multi-Platform by Default)
```bash ```bash
npm run build:npm 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) - Builds native modules for all supported platforms (macOS x64/arm64, Linux x64/arm64)
- Creates comprehensive prebuilds for zero-dependency installation - Creates comprehensive prebuilds for zero-dependency installation
- Generates npm README optimized for package distribution - Generates npm README optimized for package distribution
- Creates clean dist-npm/ directory for packaging
### Build Options ### Build Options
The unified build script supports flexible targeting: 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 ### For End Users
1. **Install package**: `npm install -g vibetunnel` 1. **Install package**: `npm install -g vibetunnel`
2. **Prebuild-install runs**: Attempts to download prebuilt binaries 2. **Postinstall script runs**: Extracts appropriate prebuilt binaries
3. **Fallback compilation**: If prebuilds fail, compiles from source 3. **No compilation needed**: Prebuilds included for all supported platforms
4. **Result**: Working VibeTunnel installation 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 ### Installation Scripts
The package uses a multi-stage installation approach: The package uses a simplified postinstall approach:
```json ```json
{ {
"scripts": { "scripts": {
"install": "prebuild-install || node scripts/postinstall-npm.js" "postinstall": "node scripts/postinstall.js"
} }
} }
``` ```
#### Stage 1: prebuild-install #### Postinstall Process
- Downloads appropriate prebuilt binary for current platform/Node version - **Prebuild extraction**: Extracts the appropriate prebuild for the current platform
- Installs to standard locations - **No downloads**: All prebuilds are included in the package
- **Success**: Installation complete, no compilation needed - **No compilation**: Everything is pre-built, no build tools required
- **Failure**: Proceeds to Stage 2 - **Platform detection**: Automatically selects correct binary based on OS and architecture
#### 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
## Platform-Specific Details ## Platform-Specific Details
### macOS ### macOS
- **spawn-helper**: Additional C binary needed for proper PTY operations - **spawn-helper**: Additional C binary needed for proper PTY operations (now prebuilt as universal binary)
- **Built during install**: spawn-helper compiles via node-gyp when needed - **Authentication**: Attempts PAM authentication but may fall back to environment variables or SSH keys
- **Architecture**: Supports both Intel (x64) and Apple Silicon (arm64) - **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 ### 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) - **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 ### Docker Build Environment
Linux prebuilds are created using Docker with: Linux prebuilds are created using Docker with:

View file

@ -10,8 +10,12 @@
"dist/", "dist/",
"public/", "public/",
"bin/", "bin/",
"scripts/ensure-native-modules.js",
"scripts/postinstall-npm.js",
"node-pty/lib/", "node-pty/lib/",
"node-pty/package.json", "node-pty/package.json",
"node-pty/binding.gyp",
"node-pty/src/",
"prebuilds/", "prebuilds/",
"README.md" "README.md"
], ],
@ -40,7 +44,7 @@
"build:ci": "node scripts/build-ci.js", "build:ci": "node scripts/build-ci.js",
"build:npm": "node scripts/build-npm.js", "build:npm": "node scripts/build-npm.js",
"prepublishOnly": "npm run build:npm", "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": "echo 'Skipping prebuild - handled by build-npm.js'",
"prebuild:upload": "echo 'Skipping prebuild:upload - not used'", "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\"", "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 #!/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 * Builds for all platforms by default with complete prebuild support
* *
* Options: * Options:
@ -11,7 +12,7 @@
* --arch <arch> Build for specific architecture (x64, arm64) * --arch <arch> Build for specific architecture (x64, arm64)
*/ */
const { execSync, spawn } = require('child_process'); const { execSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -21,13 +22,18 @@ const ALL_PLATFORMS = {
linux: ['x64', 'arm64'] linux: ['x64', 'arm64']
}; };
const DIST_DIR = path.join(__dirname, '..', 'dist-npm');
const ROOT_DIR = path.join(__dirname, '..');
// Map Node.js versions to ABI versions // 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) { function getNodeAbi(nodeVersion) {
const abiMap = { const abiMap = {
'20': '115', '20': '115', // Node.js 20.x uses ABI 115
'22': '127', '22': '127', // Node.js 22.x uses ABI 127
'23': '131', '23': '131', // Node.js 23.x uses ABI 131
'24': '134' '24': '134' // Node.js 24.x uses ABI 134
}; };
return abiMap[nodeVersion]; 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] || const archFilter = args.find(arg => arg.startsWith('--arch'))?.split('=')[1] ||
(args.includes('--arch') ? args[args.indexOf('--arch') + 1] : null); (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; let PLATFORMS = ALL_PLATFORMS;
if (currentOnly) { 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) { if (currentOnly) {
console.log(`📦 Legacy mode: Building for ${process.platform}/${process.arch} only\n`); console.log(`📦 Legacy mode: Building for ${process.platform}/${process.arch} only\n`);
@ -202,15 +222,25 @@ function buildMacOS() {
for (const arch of PLATFORMS.darwin || []) { for (const arch of PLATFORMS.darwin || []) {
console.log(` → authenticate-pam for Node.js ${nodeVersion} ${arch}`); console.log(` → authenticate-pam for Node.js ${nodeVersion} ${arch}`);
try { 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, cwd: authenticatePamDir,
stdio: 'pipe', stdio: 'pipe',
env: { ...process.env, npm_config_target_platform: 'darwin', npm_config_target_arch: arch } 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) { } catch (error) {
console.error(` ❌ Failed to build authenticate-pam for Node.js ${nodeVersion} ${arch}`); // Don't exit on macOS authenticate-pam build failures - it might work during npm install
console.error(` Error: ${error.message}`); console.warn(` ⚠️ authenticate-pam build failed for macOS (this may be normal)`);
process.exit(1); 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`); 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 // Main build process
async function main() { async function main() {
// Step 0: Temporarily modify package.json for npm packaging // Step 0: Clean previous build
const packageJsonPath = path.join(__dirname, '..', 'package.json'); console.log('0⃣ Cleaning previous build...');
const originalPackageJson = fs.readFileSync(packageJsonPath, 'utf8'); if (fs.existsSync(DIST_DIR)) {
const packageJson = JSON.parse(originalPackageJson); fs.rmSync(DIST_DIR, { recursive: true });
}
fs.mkdirSync(DIST_DIR, { recursive: true });
// Store original postinstall // Step 1: Standard build process
const originalPostinstall = packageJson.scripts.postinstall; console.log('\n1⃣ Running standard build process...\n');
// 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');
try { try {
execSync('node scripts/build.js', { stdio: 'inherit' }); execSync('npm run build', {
cwd: ROOT_DIR,
stdio: 'inherit'
});
console.log('✅ Standard build completed\n'); console.log('✅ Standard build completed\n');
} catch (error) { } catch (error) {
console.error('❌ Standard build failed:', error.message); console.error('❌ Standard build failed:', error.message);
@ -406,25 +520,199 @@ async function main() {
mergePrebuilds(); mergePrebuilds();
} }
// Step 3: Ensure node-pty is built for current platform // Step 3: Copy necessary files to dist-npm
console.log('3⃣ Ensuring node-pty is built for current platform...\n'); console.log('3⃣ Copying files to dist-npm...\n');
const nodePtyBuild = path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node');
if (!fs.existsSync(nodePtyBuild)) { const filesToCopy = [
console.log(' Building node-pty for current platform...'); // Compiled CLI
const nodePtyDir = path.join(__dirname, '..', 'node-pty'); { src: 'dist/vibetunnel-cli', dest: 'lib/cli.js' },
try { { src: 'dist/tsconfig.server.tsbuildinfo', dest: 'lib/tsconfig.server.tsbuildinfo' },
execSync('npm run install', { cwd: nodePtyDir, stdio: 'inherit' });
console.log('✅ node-pty built successfully'); // Bin scripts
} catch (error) { { src: 'bin', dest: 'bin' },
console.error('❌ Failed to build node-pty:', error.message);
process.exit(1); // 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 filesToCopy.forEach(({ src, dest }) => {
console.log('\n4⃣ Creating npm package README...\n'); 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 const readmeContent = `# VibeTunnel CLI
Full-featured terminal sharing server with web interface for macOS and Linux. Windows not yet supported. 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 npm install -g vibetunnel
\`\`\` \`\`\`
## Requirements ## Quick Start
- 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
\`\`\`bash \`\`\`bash
# Start with default settings (port 4020) # Start VibeTunnel server
vibetunnel vibetunnel
# Start with custom port # Or use short alias
vibetunnel --port 8080 vt
# Start without authentication # Custom port and settings
vibetunnel --no-auth vibetunnel --port 4020 --auth
\`\`\`
Then open http://localhost:4020 in your browser to access the web interface. # Forward mode (connect to remote VibeTunnel)
vibetunnel fwd username@hostname
### 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
\`\`\` \`\`\`
## Features ## Features
- **Web-based terminal interface** - Access terminals from any browser - **Terminal Sharing**: Share your terminal through a web browser
- **Multiple concurrent sessions** - Run multiple terminals simultaneously - **Web Interface**: Access terminals from any device with a browser
- **Real-time synchronization** - See output in real-time - **Session Management**: Create, manage, and switch between multiple terminal sessions
- **TTY forwarding** - Full terminal emulation support - **Authentication**: Built-in authentication system
- **Session management** - Create, list, and manage sessions - **Cross-Platform**: Works on macOS and Linux
- **Cross-platform** - Works on macOS and Linux
- **No dependencies** - Just Node.js required
## Package Contents ## Requirements
This npm package includes: - **Node.js**: Version 20 or higher
- Full VibeTunnel server with web UI - **Operating System**: macOS or Linux (Windows not yet supported)
- Command-line tools (vibetunnel, vt) - **Build Tools**: For source compilation fallback (make, gcc, python3)
- Native PTY support for terminal emulation
- Web interface with xterm.js
- Session management and forwarding
## Platform Support ## Platform Support
- macOS (Intel and Apple Silicon) ### macOS
- Linux (x64 and ARM64) - Intel (x64) and Apple Silicon (arm64)
- Windows: Not yet supported ([#252](https://github.com/amantus-ai/vibetunnel/issues/252)) - 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 ## Documentation
@ -525,20 +811,21 @@ See the main repository for complete documentation: https://github.com/amantus-a
MIT MIT
`; `;
const readmePath = path.join(__dirname, '..', 'README.md'); fs.writeFileSync(
fs.writeFileSync(readmePath, readmeContent); path.join(DIST_DIR, 'README.md'),
console.log('✅ npm README created'); readmeContent
);
// Step 5: Clean up test files (keep screencap.js - it's needed)
console.log('\n5⃣ Cleaning up test files...\n'); // Step 8: Clean up test files in dist-npm
console.log('\n8⃣ Cleaning up test files...\n');
const testFiles = [ const testFiles = [
'public/bundle/test.js', 'public/bundle/test.js',
'public/test' // Remove entire test directory 'public/test' // Remove entire test directory
]; ];
for (const file of testFiles) { for (const file of testFiles) {
const filePath = path.join(__dirname, '..', file); const filePath = path.join(DIST_DIR, file);
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
if (fs.statSync(filePath).isDirectory()) { if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true }); 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 // Step 10: Create npm package
console.log('\n6⃣ Package summary...\n'); console.log('\n9⃣ Creating npm package...\n');
try {
// Calculate total size execSync('npm pack', {
function getDirectorySize(dirPath) { cwd: DIST_DIR,
let totalSize = 0; stdio: 'inherit'
const items = fs.readdirSync(dirPath); });
for (const item of items) { // Move the package to root directory
const itemPath = path.join(dirPath, item); const packageFiles = fs.readdirSync(DIST_DIR)
const stats = fs.statSync(itemPath); .filter(f => f.endsWith('.tgz'));
if (stats.isFile()) { if (packageFiles.length > 0) {
totalSize += stats.size; const packageFile = packageFiles[0];
} else if (stats.isDirectory()) { fs.renameSync(
totalSize += getDirectorySize(itemPath); path.join(DIST_DIR, packageFile),
} path.join(ROOT_DIR, packageFile)
);
console.log(`\n✅ Package created: ${packageFile}`);
} }
} catch (error) {
return totalSize; console.error('❌ npm pack failed:', error.message);
process.exit(1);
} }
const packageRoot = path.join(__dirname, '..'); console.log('\n🎉 Hybrid npm build completed successfully!');
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('\nNext steps:'); 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'); console.log(' - Publish: npm publish');
// Restore original package.json
restorePackageJson();
} }
main().catch(error => { main().catch(error => {

View file

@ -3,6 +3,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const esbuild = require('esbuild'); const esbuild = require('esbuild');
const { prodOptions } = require('./esbuild-config.js'); const { prodOptions } = require('./esbuild-config.js');
const { nodePtyPlugin } = require('./node-pty-plugin.js');
async function build() { async function build() {
console.log('Starting build process...'); console.log('Starting build process...');
@ -76,9 +77,22 @@ async function build() {
target: 'node18', target: 'node18',
format: 'cjs', format: 'cjs',
outfile: 'dist/vibetunnel-cli', outfile: 'dist/vibetunnel-cli',
plugins: [nodePtyPlugin],
external: [ external: [
'node-pty', // 'node-pty', // Removed - handled by plugin
'authenticate-pam', 'authenticate-pam',
'compression',
'helmet',
'express',
'ws',
'jsonwebtoken',
'web-push',
'bonjour-service',
'signal-exit',
'http-proxy-middleware',
'multer',
'mime-types',
'@xterm/headless',
], ],
minify: true, minify: true,
sourcemap: false, 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 * Postinstall script for npm package
* Fallback build script when prebuild-install fails * Handles prebuild extraction and fallback compilation
*/ */
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const os = require('os');
console.log('Setting up native modules for VibeTunnel...'); console.log('Setting up native modules for VibeTunnel...');
@ -20,125 +21,264 @@ if (isDevelopment) {
return; return;
} }
// Try prebuild-install first for each module // Create node_modules directory if it doesn't exist
const tryPrebuildInstall = (name, dir) => { const nodeModulesDir = path.join(__dirname, '..', 'node_modules');
console.log(`Trying prebuild-install for ${name}...`); 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 { try {
execSync('prebuild-install', { fs.symlinkSync(nodePtySource, nodePtyTarget, 'dir');
cwd: 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', 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; return true;
} catch (error) { } 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; 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 = [ const modules = [
{ {
name: 'node-pty', name: 'node-pty',
version: '1.0.0',
dir: path.join(__dirname, '..', 'node-pty'), dir: path.join(__dirname, '..', 'node-pty'),
build: path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node'), build: path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node'),
essential: true essential: true
}, },
{ {
name: 'authenticate-pam', name: 'authenticate-pam',
version: '1.0.5',
dir: path.join(__dirname, '..', 'node_modules', 'authenticate-pam'), dir: path.join(__dirname, '..', 'node_modules', 'authenticate-pam'),
build: path.join(__dirname, '..', 'node_modules', 'authenticate-pam', 'build', 'Release', 'authenticate_pam.node'), 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; let hasErrors = false;
for (const module of modules) { for (const module of modules) {
if (!fs.existsSync(module.build)) { console.log(`\nProcessing ${module.name}...`);
// First try prebuild-install
const prebuildSuccess = tryPrebuildInstall(module.name, module.dir); // Skip platform-specific modules if not on that platform
if (module.platforms && !module.platforms.includes(platform)) {
if (!prebuildSuccess) { console.log(` Skipping ${module.name} (not needed on ${platform})`);
// Fall back to compilation continue;
console.log(`Building ${module.name} from source...`); }
try {
execSync('node-gyp rebuild', { // Check if module directory exists
cwd: module.dir, if (!fs.existsSync(module.dir)) {
stdio: 'inherit' console.warn(` Warning: ${module.name} directory not found at ${module.dir}`);
}); if (module.essential) {
console.log(`${module.name} built successfully`); hasErrors = true;
} 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.`);
}
}
} }
} else { continue;
}
// Check if already built
if (fs.existsSync(module.build)) {
console.log(`${module.name} already available`); 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) { if (hasErrors) {
console.error('\n❌ Setup failed with errors');
process.exit(1); process.exit(1);
} } else {
console.log('\n✅ VibeTunnel is ready to use');
// Conditionally install vt symlink console.log('Run "vibetunnel --help" for usage information');
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');

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); process.exit(1);
}); });
// Only execute if this is the main module (or in SEA where require.main is undefined) // Only execute if this is the main module (or in SEA/bundled context where require.main is undefined)
if (!module.parent && (require.main === module || require.main === 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') { if (process.argv[2] === 'version') {
console.log(`VibeTunnel Server v${VERSION}`); console.log(`VibeTunnel Server v${VERSION}`);
process.exit(0); process.exit(0);

View file

@ -65,6 +65,13 @@ describe('SessionView', () => {
// Setup fetch mock // Setup fetch mock
fetchMock = setupFetchMock(); 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 // Create component
element = await fixture<SessionView>(html` <session-view></session-view> `); 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 // 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'; const isDevelopment = !process.env.BUILD_DATE || process.env.NODE_ENV === 'development';
app.use( app.use(