mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
Fix npm package build and installation issues (#360)
Co-authored-by: Alex Mazanov <alexandr.mazanov@gmail.com>
This commit is contained in:
parent
0d3b437887
commit
253d0ae3e7
21 changed files with 1376 additions and 411 deletions
20
.github/workflows/README.md
vendored
20
.github/workflows/README.md
vendored
|
|
@ -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:**
|
||||
|
|
|
|||
21
.github/workflows/mac.yml
vendored
21
.github/workflows/mac.yml
vendored
|
|
@ -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
236
.github/workflows/nightly.yml
vendored
Normal 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}`);
|
||||
10
.github/workflows/node.yml
vendored
10
.github/workflows/node.yml
vendored
|
|
@ -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
130
.github/workflows/npm-test.yml
vendored
Normal 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!"
|
||||
11
.github/workflows/web-ci.yml
vendored
11
.github/workflows/web-ci.yml
vendored
|
|
@ -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
1
web/.gitignore
vendored
|
|
@ -7,6 +7,7 @@ yarn-error.log*
|
|||
# Generated files
|
||||
public/
|
||||
dist/
|
||||
dist-npm/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
78
web/package.npm.json
Normal 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
611
web/scripts/build-npm.js
Executable file → Normal 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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
73
web/scripts/node-pty-plugin.js
Normal file
73
web/scripts/node-pty-plugin.js
Normal 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 };
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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> `);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue