diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b2f1ec55..6b635599 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -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:** diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8e7c53ec..ca4bc6b0 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -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 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..9cedbe07 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -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}`); \ No newline at end of file diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 9e811c7c..0388ed36 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -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 diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml new file mode 100644 index 00000000..96d8c5d0 --- /dev/null +++ b/.github/workflows/npm-test.yml @@ -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!" \ No newline at end of file diff --git a/.github/workflows/web-ci.yml b/.github/workflows/web-ci.yml index 255c2279..6a821d71 100644 --- a/.github/workflows/web-ci.yml +++ b/.github/workflows/web-ci.yml @@ -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 \ No newline at end of file + retention-days: 7 + diff --git a/web/.gitignore b/web/.gitignore index 0f7b0512..070ab7e1 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -7,6 +7,7 @@ yarn-error.log* # Generated files public/ dist/ +dist-npm/ # Logs *.log diff --git a/web/bin/vibetunnel b/web/bin/vibetunnel index e6c099f6..c8dcdc92 100755 --- a/web/bin/vibetunnel +++ b/web/bin/vibetunnel @@ -1,4 +1,4 @@ #!/usr/bin/env node // Start the CLI - it handles all command routing including 'fwd' -require('../dist/cli.js'); \ No newline at end of file +require('../dist/vibetunnel-cli'); \ No newline at end of file diff --git a/web/docs/npm.md b/web/docs/npm.md index dd48cf7b..282520dc 100644 --- a/web/docs/npm.md +++ b/web/docs/npm.md @@ -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: diff --git a/web/package.json b/web/package.json index 3d5f1b8f..5a1fb022 100644 --- a/web/package.json +++ b/web/package.json @@ -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\"", diff --git a/web/package.npm.json b/web/package.npm.json new file mode 100644 index 00000000..c92543fa --- /dev/null +++ b/web/package.npm.json @@ -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" +} \ No newline at end of file diff --git a/web/scripts/build-npm.js b/web/scripts/build-npm.js old mode 100755 new mode 100644 index 34c23663..bc733fe6 --- a/web/scripts/build-npm.js +++ b/web/scripts/build-npm.js @@ -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 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 [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 => { diff --git a/web/scripts/build.js b/web/scripts/build.js index 0b9a0967..98364e19 100644 --- a/web/scripts/build.js +++ b/web/scripts/build.js @@ -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, diff --git a/web/scripts/node-pty-plugin.js b/web/scripts/node-pty-plugin.js new file mode 100644 index 00000000..2284a43b --- /dev/null +++ b/web/scripts/node-pty-plugin.js @@ -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 }; \ No newline at end of file diff --git a/web/scripts/postinstall-npm.js b/web/scripts/postinstall-npm.js index cdd73fd7..9da8b0e7 100755 --- a/web/scripts/postinstall-npm.js +++ b/web/scripts/postinstall-npm.js @@ -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'); \ No newline at end of file +} else { + console.log('\n✅ VibeTunnel is ready to use'); + console.log('Run "vibetunnel --help" for usage information'); +} \ No newline at end of file diff --git a/web/scripts/test-docker-build.sh b/web/scripts/test-docker-build.sh deleted file mode 100755 index fc18c488..00000000 --- a/web/scripts/test-docker-build.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/web/scripts/test-parallel.sh b/web/scripts/test-parallel.sh deleted file mode 100755 index cffa045a..00000000 --- a/web/scripts/test-parallel.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/web/scripts/test-playwright-clean.sh b/web/scripts/test-playwright-clean.sh deleted file mode 100755 index 7d85b6f0..00000000 --- a/web/scripts/test-playwright-clean.sh +++ /dev/null @@ -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 "$@" \ No newline at end of file diff --git a/web/src/cli.ts b/web/src/cli.ts index 8a26d85a..dc96f513 100644 --- a/web/src/cli.ts +++ b/web/src/cli.ts @@ -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); diff --git a/web/src/client/components/session-view.test.ts b/web/src/client/components/session-view.test.ts index a9a2fdfb..1ad91c10 100644 --- a/web/src/client/components/session-view.test.ts +++ b/web/src/client/components/session-view.test.ts @@ -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(html` `); diff --git a/web/src/server/server.ts b/web/src/server/server.ts index edefb48f..66aa4fed 100644 --- a/web/src/server/server.ts +++ b/web/src/server/server.ts @@ -559,7 +559,76 @@ export async function createApp(): Promise { }); // 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(