From 84b7467e83c49d29c20ae6d3634e9838ecf483c8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 17 Jul 2025 08:57:08 +0200 Subject: [PATCH] fix: handle authenticate-pam as optional dependency in npm package - Modified postinstall script to extract authenticate-pam prebuilds when npm skips optional deps - Updated authenticate-pam-loader to load from optional-modules directory - Prepared beta.12 release with all fixes consolidated - Moved Docker test script to scripts folder - Updated documentation in npm.md --- web/package/README.md | 293 +++++++ web/package/bin/vibetunnel | 27 + web/package/bin/vt | 343 ++++++++ web/package/lib/cli.js | 2 + web/package/lib/vibetunnel-cli | 125 +++ web/package/node-pty/README.md | 47 + web/package/node-pty/binding.gyp | 63 ++ web/package/node-pty/lib/eventEmitter2.d.ts | 13 + web/package/node-pty/lib/eventEmitter2.js | 40 + web/package/node-pty/lib/index.d.ts | 15 + web/package/node-pty/lib/index.js | 41 + web/package/node-pty/lib/interfaces.d.ts | 120 +++ web/package/node-pty/lib/interfaces.js | 6 + web/package/node-pty/lib/terminal.d.ts | 66 ++ web/package/node-pty/lib/terminal.js | 162 ++++ web/package/node-pty/lib/types.d.ts | 12 + web/package/node-pty/lib/types.js | 6 + web/package/node-pty/lib/unixTerminal.d.ts | 43 + web/package/node-pty/lib/unixTerminal.js | 300 +++++++ web/package/node-pty/lib/utils.d.ts | 5 + web/package/node-pty/lib/utils.js | 11 + web/package/node-pty/lib/windowsTerminal.d.ts | 34 + web/package/node-pty/lib/windowsTerminal.js | 200 +++++ web/package/node-pty/package.json | 24 + web/package/node-pty/src/eventEmitter2.ts | 48 ++ web/package/node-pty/src/index.ts | 32 + web/package/node-pty/src/interfaces.ts | 143 +++ web/package/node-pty/src/native.d.ts | 54 ++ web/package/node-pty/src/terminal.ts | 211 +++++ web/package/node-pty/src/types.ts | 15 + web/package/node-pty/src/unix/pty.cc | 816 ++++++++++++++++++ web/package/node-pty/src/unixTerminal.ts | 332 +++++++ web/package/node-pty/src/utils.ts | 9 + web/package/node-pty/src/win/conpty.cc | 583 +++++++++++++ web/package/node-pty/src/win/conpty.h | 41 + .../node-pty/src/win/conpty_console_list.cc | 44 + web/package/node-pty/src/win/path_util.cc | 95 ++ web/package/node-pty/src/win/path_util.h | 26 + web/package/node-pty/src/win/winpty.cc | 333 +++++++ web/package/node-pty/src/windowsTerminal.ts | 214 +++++ web/package/package.json | 78 ++ web/package/scripts/node-pty-plugin.js | 73 ++ web/package/scripts/postinstall.js | 257 ++++++ web/scripts/test-npm-docker-verbose.sh | 45 + web/scripts/test-npm-docker.sh | 60 ++ .../services/authenticate-pam-loader.ts | 18 +- web/test-vibetunnel-simple.dockerfile | 29 + ...est-vibetunnel-with-postinstall.dockerfile | 26 + web/test-vibetunnel.dockerfile | 43 + 49 files changed, 5609 insertions(+), 14 deletions(-) create mode 100644 web/package/README.md create mode 100755 web/package/bin/vibetunnel create mode 100755 web/package/bin/vt create mode 100755 web/package/lib/cli.js create mode 100755 web/package/lib/vibetunnel-cli create mode 100644 web/package/node-pty/README.md create mode 100644 web/package/node-pty/binding.gyp create mode 100644 web/package/node-pty/lib/eventEmitter2.d.ts create mode 100644 web/package/node-pty/lib/eventEmitter2.js create mode 100644 web/package/node-pty/lib/index.d.ts create mode 100644 web/package/node-pty/lib/index.js create mode 100644 web/package/node-pty/lib/interfaces.d.ts create mode 100644 web/package/node-pty/lib/interfaces.js create mode 100644 web/package/node-pty/lib/terminal.d.ts create mode 100644 web/package/node-pty/lib/terminal.js create mode 100644 web/package/node-pty/lib/types.d.ts create mode 100644 web/package/node-pty/lib/types.js create mode 100644 web/package/node-pty/lib/unixTerminal.d.ts create mode 100644 web/package/node-pty/lib/unixTerminal.js create mode 100644 web/package/node-pty/lib/utils.d.ts create mode 100644 web/package/node-pty/lib/utils.js create mode 100644 web/package/node-pty/lib/windowsTerminal.d.ts create mode 100644 web/package/node-pty/lib/windowsTerminal.js create mode 100644 web/package/node-pty/package.json create mode 100644 web/package/node-pty/src/eventEmitter2.ts create mode 100644 web/package/node-pty/src/index.ts create mode 100644 web/package/node-pty/src/interfaces.ts create mode 100644 web/package/node-pty/src/native.d.ts create mode 100644 web/package/node-pty/src/terminal.ts create mode 100644 web/package/node-pty/src/types.ts create mode 100644 web/package/node-pty/src/unix/pty.cc create mode 100644 web/package/node-pty/src/unixTerminal.ts create mode 100644 web/package/node-pty/src/utils.ts create mode 100644 web/package/node-pty/src/win/conpty.cc create mode 100644 web/package/node-pty/src/win/conpty.h create mode 100644 web/package/node-pty/src/win/conpty_console_list.cc create mode 100644 web/package/node-pty/src/win/path_util.cc create mode 100644 web/package/node-pty/src/win/path_util.h create mode 100644 web/package/node-pty/src/win/winpty.cc create mode 100644 web/package/node-pty/src/windowsTerminal.ts create mode 100644 web/package/package.json create mode 100644 web/package/scripts/node-pty-plugin.js create mode 100755 web/package/scripts/postinstall.js create mode 100755 web/scripts/test-npm-docker-verbose.sh create mode 100755 web/scripts/test-npm-docker.sh create mode 100644 web/test-vibetunnel-simple.dockerfile create mode 100644 web/test-vibetunnel-with-postinstall.dockerfile create mode 100644 web/test-vibetunnel.dockerfile diff --git a/web/package/README.md b/web/package/README.md new file mode 100644 index 00000000..2dc0d469 --- /dev/null +++ b/web/package/README.md @@ -0,0 +1,293 @@ +# VibeTunnel CLI + +**Turn any browser into your terminal.** VibeTunnel proxies your terminals right into the browser, so you can vibe-code anywhere. + +Full-featured terminal sharing server with web interface for macOS and Linux. Windows not yet supported. + +## Why VibeTunnel? + +Ever wanted to check on your AI agents while you're away? Need to monitor that long-running build from your phone? Want to share a terminal session with a colleague without complex SSH setups? VibeTunnel makes it happen with zero friction. + +## Installation + +### From npm (Recommended) +```bash +npm install -g vibetunnel +``` + +### From Source +```bash +git clone https://github.com/amantus-ai/vibetunnel.git +cd vibetunnel/web +pnpm install +pnpm run build +``` + +## Installation Differences + +**npm package**: +- Pre-built binaries for common platforms (macOS x64/arm64, Linux x64/arm64) +- Automatic fallback to source compilation if pre-built binaries unavailable +- Global installation makes `vibetunnel` and `vt` commands available system-wide +- Includes production dependencies only + +**Source installation**: +- Full development environment with hot reload (`pnpm run dev`) +- Access to all development scripts and tools +- Ability to modify and rebuild the application +- Includes test suites and development dependencies + +## 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 + +```bash +# Start with default settings (port 4020) +vibetunnel + +# Start with custom port +vibetunnel --port 8080 + +# Start without authentication +vibetunnel --no-auth + +# Bind to specific interface +vibetunnel --bind 127.0.0.1 --port 4020 + +# Enable SSH key authentication +vibetunnel --enable-ssh-keys + +# SSH keys only (no password auth) +vibetunnel --disallow-user-password +``` + +Then open http://localhost:4020 in your browser to access the web interface. + +### Command-line Options + +``` +vibetunnel [options] + +Basic Options: + --help, -h Show help message + --version, -v Show version information + --port Server port (default: 4020 or PORT env var) + --bind
Bind address (default: 0.0.0.0, all interfaces) + +Authentication Options: + --no-auth Disable authentication (auto-login as current user) + --enable-ssh-keys Enable SSH key authentication UI and functionality + --disallow-user-password Disable password auth, SSH keys only (auto-enables --enable-ssh-keys) + --allow-local-bypass Allow localhost connections to bypass authentication + --local-auth-token Token for localhost authentication bypass + +Push Notification Options: + --push-enabled Enable push notifications (default: enabled) + --push-disabled Disable push notifications + --vapid-email Contact email for VAPID configuration + --generate-vapid-keys Generate new VAPID keys if none exist + +Network Discovery Options: + --no-mdns Disable mDNS/Bonjour advertisement (enabled by default) + +HQ Mode Options: + --hq Run as HQ (headquarters) server + --no-hq-auth Disable HQ authentication + +Remote Server Options: + --hq-url HQ server URL to register with + --hq-username Username for HQ authentication + --hq-password Password for HQ authentication + --name Unique name for remote server + --allow-insecure-hq Allow HTTP URLs for HQ (not recommended) + +Repository Options: + --repository-base-path Base path for repository discovery + +Debugging: + --debug Enable debug logging +``` + +### 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" + +# Execute command directly without shell wrapper +vt --no-shell-wrap ls -la +vt -S ls -la + +# Control terminal title behavior +vt --title-mode none # No title management +vt --title-mode filter # Block all title changes +vt --title-mode static # Show directory and command +vt --title-mode dynamic # Show directory, command, and activity + +# Verbosity control +vt -q npm test # Quiet mode (errors only) +vt -v npm run dev # Verbose mode +vt -vv npm test # Extra verbose +vt -vvv npm build # Debug mode +``` + +### 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 +``` + +### Environment Variables + +VibeTunnel respects the following environment variables: + +```bash +PORT=8080 # Default port if --port not specified +VIBETUNNEL_USERNAME=myuser # Username (for env-based auth, not CLI) +VIBETUNNEL_PASSWORD=mypass # Password (for env-based auth, not CLI) +VIBETUNNEL_CONTROL_DIR=/path # Control directory for session data +VIBETUNNEL_SESSION_ID=abc123 # Current session ID (set automatically inside sessions) +VIBETUNNEL_LOG_LEVEL=debug # Log level: error, warn, info, verbose, debug +PUSH_CONTACT_EMAIL=admin@example.com # Contact email for VAPID configuration +``` + +## 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 + +## Package Contents + +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 + +## Platform Support + +- macOS (Intel and Apple Silicon) +- Linux (x64 and ARM64) +- Windows: Not yet supported ([#252](https://github.com/amantus-ai/vibetunnel/issues/252)) + +## 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 + +### SSH Key Authentication Issues + +If you encounter errors when generating or importing SSH keys (e.g., "Cannot read properties of undefined"), this is due to browser security restrictions on the Web Crypto API. + +#### The Issue +Modern browsers (Chrome 60+, Firefox 75+) block the Web Crypto API when accessing web applications over HTTP from non-localhost addresses. This affects: +- Local network IPs (192.168.x.x, 10.x.x.x, 172.16-31.x.x) +- Any non-localhost hostname over HTTP + +#### Solutions + +1. **Use localhost (Recommended)** + ```bash + # Access VibeTunnel via localhost + http://localhost:4020 + + # If running on a remote server, use SSH tunneling: + ssh -L 4020:localhost:4020 user@your-server + # Then access http://localhost:4020 in your browser + ``` + +2. **Enable HTTPS** + Set up a reverse proxy with HTTPS using nginx or Caddy (recommended for production). + +3. **Chrome Flag Workaround** (Development only) + - Navigate to `chrome://flags/#unsafely-treat-insecure-origin-as-secure` + - Add your server URL (e.g., `http://192.168.1.100:4020`) + - Enable the flag and restart Chrome + - ⚠️ This reduces security - use only for development + +#### Why This Happens +The Web Crypto API is restricted to secure contexts (HTTPS or localhost) to prevent man-in-the-middle attacks on cryptographic operations. This is a browser security feature, not a VibeTunnel limitation. + +### Development Setup + +For source installations: +```bash +# Install dependencies +pnpm install + +# Run development server with hot reload +pnpm run dev + +# Run code quality checks +pnpm run check + +# Build for production +pnpm run build +``` + +## Documentation + +See the main repository for complete documentation: https://github.com/amantus-ai/vibetunnel + +## License + +MIT diff --git a/web/package/bin/vibetunnel b/web/package/bin/vibetunnel new file mode 100755 index 00000000..91595ddf --- /dev/null +++ b/web/package/bin/vibetunnel @@ -0,0 +1,27 @@ +#!/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); + } +}); diff --git a/web/package/bin/vt b/web/package/bin/vt new file mode 100755 index 00000000..223e517f --- /dev/null +++ b/web/package/bin/vt @@ -0,0 +1,343 @@ +#!/bin/bash +# Unified VibeTunnel CLI wrapper - compatible with both Mac app and npm installations + +# Only check for Mac app on macOS +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS symlink resolution function using BSD readlink + resolve_symlink_macos() { + local target="$1" + local current="$target" + while [ -L "$current" ]; do + current="$(readlink "$current")" + # Handle relative symlinks + if [[ "$current" != /* ]]; then + current="$(dirname "$target")/$current" + fi + done + echo "$current" + } + + # Get the real path of this script to avoid infinite recursion + SCRIPT_REAL_PATH="$(resolve_symlink_macos "${BASH_SOURCE[0]}")" + + # Comprehensive Mac app search - try standard locations first, then development locations + APP_PATH="" + + # First try standard locations with valid binary check + for TRY_PATH in "/Applications/VibeTunnel.app" "$HOME/Applications/VibeTunnel.app"; do + if [ -d "$TRY_PATH" ] && [ -f "$TRY_PATH/Contents/Resources/vibetunnel" ]; then + VT_SCRIPT="$TRY_PATH/Contents/Resources/vt" + if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then + # Avoid infinite recursion by checking if this is the same script + VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")" + if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then + exec "$VT_SCRIPT" "$@" + fi + fi + APP_PATH="$TRY_PATH" + break + fi + done + + # If not found in standard locations, search for development builds + if [ -z "$APP_PATH" ]; then + # First try DerivedData (for development) + for CANDIDATE in $(find ~/Library/Developer/Xcode/DerivedData -name "VibeTunnel.app" -type d 2>/dev/null | grep -v "\.dSYM" | grep -v "Index\.noindex"); do + if [ -f "$CANDIDATE/Contents/Resources/vibetunnel" ]; then + VT_SCRIPT="$CANDIDATE/Contents/Resources/vt" + if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then + VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")" + if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then + exec "$VT_SCRIPT" "$@" + fi + fi + APP_PATH="$CANDIDATE" + break + fi + done + + # If still not found, use mdfind as last resort + if [ -z "$APP_PATH" ]; then + for CANDIDATE in $(mdfind -name "VibeTunnel.app" 2>/dev/null | grep -v "\.dSYM"); do + if [ -f "$CANDIDATE/Contents/Resources/vibetunnel" ]; then + VT_SCRIPT="$CANDIDATE/Contents/Resources/vt" + if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then + VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")" + if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then + exec "$VT_SCRIPT" "$@" + fi + fi + APP_PATH="$CANDIDATE" + break + fi + done + fi + fi + + # If we found a Mac app but couldn't use its vt script, use its binary directly + if [ -n "$APP_PATH" ]; then + VIBETUNNEL_BIN="$APP_PATH/Contents/Resources/vibetunnel" + if [ -f "$VIBETUNNEL_BIN" ]; then + # Found Mac app bundle - will use this binary + # Silent operation - no message printed + fi + fi +fi + +# If we get here without a Mac app, use the npm-installed vibetunnel +if [ -z "$VIBETUNNEL_BIN" ]; then + # First, try to find vibetunnel in the same directory as this script + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [ -f "$SCRIPT_DIR/vibetunnel" ]; then + VIBETUNNEL_BIN="$SCRIPT_DIR/vibetunnel" + else + # Try to find vibetunnel in PATH + if command -v vibetunnel >/dev/null 2>&1; then + VIBETUNNEL_BIN="$(command -v vibetunnel)" + fi + fi + + if [ -z "$VIBETUNNEL_BIN" ] || [ ! -f "$VIBETUNNEL_BIN" ]; then + echo "Error: vibetunnel binary not found. Please ensure vibetunnel is installed." >&2 + echo "Install with: npm install -g vibetunnel" >&2 + exit 1 + fi +fi + +# Check if we're already inside a VibeTunnel session +if [ -n "$VIBETUNNEL_SESSION_ID" ]; then + # Special case: handle 'vt title' command inside a session + if [[ "$1" == "title" ]]; then + if [[ $# -lt 2 ]]; then + echo "Error: 'vt title' requires a title argument" >&2 + echo "Usage: vt title " >&2 + exit 1 + fi + shift # Remove 'title' from arguments + TITLE="$*" # Get all remaining arguments as the title + + # Use the vibetunnel binary's new --update-title flag + exec "$VIBETUNNEL_BIN" fwd --update-title "$TITLE" --session-id "$VIBETUNNEL_SESSION_ID" + # If exec fails, exit with error + exit 1 + fi + + echo "Error: Already inside a VibeTunnel session (ID: $VIBETUNNEL_SESSION_ID). Recursive VibeTunnel sessions are not supported." >&2 + echo "If you need to run commands, use them directly without the 'vt' prefix." >&2 + exit 1 +fi + +# Function to show help +show_help() { + cat << 'EOF' +vt - VibeTunnel TTY Forward Wrapper + +USAGE: + vt [command] [args...] + vt --shell [args...] + vt -i [args...] + vt --no-shell-wrap [command] [args...] + vt -S [command] [args...] + vt title # Inside a VibeTunnel session only + vt --help + +QUICK VERBOSITY: + -q (quiet), -v (verbose), -vv (extra), -vvv (debug) + +DESCRIPTION: + This wrapper script allows VibeTunnel to see the output of commands by + forwarding TTY data through the vibetunnel utility. When you run commands + through 'vt', VibeTunnel can monitor and display the command's output + in real-time. + + By default, commands are executed through your shell to resolve aliases, + functions, and builtins. Use --no-shell-wrap to execute commands directly. + + Inside a VibeTunnel session, use 'vt title' to update the session name. + +EXAMPLES: + vt top # Watch top with VibeTunnel monitoring + vt python script.py # Run Python script with output forwarding + vt npm test # Run tests with VibeTunnel visibility + vt --shell # Launch current shell (equivalent to vt $SHELL) + vt -i # Launch current shell (short form) + vt -S ls -la # List files without shell alias resolution + vt title "My Project" # Update session title (inside session only) + vt -q npm test # Run with minimal output (errors only) + vt -vv npm run dev # Run with verbose output + +OPTIONS: + --shell, -i Launch current shell (equivalent to vt $SHELL) + --no-shell-wrap, -S Execute command directly without shell wrapper + --title-mode Terminal title mode (none, filter, static, dynamic) + Default: none (dynamic for claude) + --quiet, -q Quiet mode - only show errors + --verbose, -v Verbose mode - show more information + -vv Extra verbose - show all except debug + -vvv Debug mode - show all messages + --help, -h Show this help message and exit + +TITLE MODES: + none No title management - apps control their own titles + filter Block all title changes from applications + static Show working directory and command in title + dynamic Show directory, command, and live activity status (default for web UI) + +VERBOSITY: + By default, only errors are shown. Use verbosity flags to control output: + -q/--quiet Suppress all output except critical errors + -v/--verbose Show errors, warnings, and informational messages + -vv Show everything except debug messages + -vvv Show all messages including debug + + You can also set VIBETUNNEL_LOG_LEVEL environment variable: + export VIBETUNNEL_LOG_LEVEL=error # Default + export VIBETUNNEL_LOG_LEVEL=warn # Show errors and warnings + export VIBETUNNEL_LOG_LEVEL=info # Show errors, warnings, and info + export VIBETUNNEL_LOG_LEVEL=verbose # All except debug + export VIBETUNNEL_LOG_LEVEL=debug # Everything + +NOTE: + This script automatically detects and uses the best available VibeTunnel installation: + - Mac app bundle (preferred on macOS) + - npm package installation (fallback) +EOF + + # Show path and version info + echo + echo "VIBETUNNEL BINARY:" + echo " Path: $VIBETUNNEL_BIN" + if [ -f "$VIBETUNNEL_BIN" ]; then + # Try to get version from binary output first (works for both Mac app and npm) + VERSION_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^VibeTunnel Server" | head -n 1) + BUILD_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^Built:" | head -n 1) + PLATFORM_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^Platform:" | head -n 1) + + if [ -n "$VERSION_INFO" ]; then + echo " Version: ${VERSION_INFO#VibeTunnel Server }" + else + # Fallback to package.json for npm installations + PACKAGE_JSON="$(dirname "$(dirname "$VIBETUNNEL_BIN")")/package.json" + if [ -f "$PACKAGE_JSON" ]; then + VERSION=$(grep '"version"' "$PACKAGE_JSON" | head -1 | sed 's/.*"version".*:.*"\(.*\)".*/\1/') + echo " Version: $VERSION" + fi + fi + + if [ -n "$BUILD_INFO" ]; then + echo " ${BUILD_INFO}" + fi + if [ -n "$PLATFORM_INFO" ]; then + echo " ${PLATFORM_INFO}" + fi + + # Determine installation type + if [[ "$VIBETUNNEL_BIN" == */Applications/VibeTunnel.app/* ]]; then + echo " Status: Mac app bundle" + elif [[ "$VIBETUNNEL_BIN" == */DerivedData/* ]]; then + echo " Status: Development build" + elif [[ "$VIBETUNNEL_BIN" == *npm* ]] || [[ "$VIBETUNNEL_BIN" == */bin/vibetunnel ]]; then + echo " Status: Installed via npm" + else + echo " Status: Unknown installation" + fi + else + echo " Status: Not found" + fi +} + +# Function to resolve command through user's shell +resolve_command() { + local user_shell="${SHELL:-/bin/bash}" + local cmd="$1" + shift + + local shell_name=$(basename "$user_shell") + + # Always try through shell first to handle aliases, functions, and builtins + # The shell will fall back to PATH lookup if no alias/function exists + case "$shell_name" in + zsh) + # For zsh, we need interactive mode to get aliases + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -i -c "$(printf '%q ' "$cmd" "$@")" + ;; + bash) + # For bash, expand aliases in non-interactive mode + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "shopt -s expand_aliases; source ~/.bashrc 2>/dev/null || source ~/.bash_profile 2>/dev/null || true; $(printf '%q ' "$cmd" "$@")" + ;; + *) + # Generic shell handling + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "$(printf '%q ' "$cmd" "$@")" + ;; + esac +} + +# Handle --help or -h option, or no arguments (show help) +if [[ $# -eq 0 || "$1" == "--help" || "$1" == "-h" ]]; then + show_help + exit 0 +fi + +# Handle 'vt title' command when not inside a session +if [[ "$1" == "title" ]]; then + echo "Error: 'vt title' can only be used inside a VibeTunnel session." >&2 + echo "Start a session first with 'vt' or 'vt '" >&2 + exit 1 +fi + +# Handle verbosity flags +VERBOSITY_ARGS="" +if [[ "$1" == "--quiet" || "$1" == "-q" ]]; then + VERBOSITY_ARGS="--verbosity silent" + shift +elif [[ "$1" == "--verbose" || "$1" == "-v" ]]; then + VERBOSITY_ARGS="--verbosity info" + shift +elif [[ "$1" == "-vv" ]]; then + VERBOSITY_ARGS="--verbosity verbose" + shift +elif [[ "$1" == "-vvv" ]]; then + VERBOSITY_ARGS="--verbosity debug" + shift +fi + +# Handle --shell or -i option (launch current shell) +if [[ "$1" == "--shell" || "$1" == "-i" ]]; then + shift + # Execute current shell through vibetunnel + exec "$0" ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} "${SHELL:-/bin/bash}" "$@" +fi + +# Handle --no-shell-wrap or -S option +NO_SHELL_WRAP=false +if [[ "$1" == "--no-shell-wrap" || "$1" == "-S" ]]; then + NO_SHELL_WRAP=true + shift +fi + +# Handle --title-mode option +TITLE_MODE_ARGS="" +if [[ "$1" == "--title-mode" && $# -gt 1 ]]; then + TITLE_MODE_ARGS="--title-mode $2" + shift 2 +fi + +# Check if we have arguments and if the first argument is not an option +if [ $# -gt 0 ] && [[ "$1" != -* ]]; then + if [[ "$NO_SHELL_WRAP" == "true" ]]; then + # Execute directly without shell wrapper + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@" + else + # Check if the first argument is a real binary + if which "$1" >/dev/null 2>&1; then + # It's a real binary, execute directly + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@" + else + # Not a real binary, try alias resolution + resolve_command "$@" + fi + fi +else + # Run with fwd command (original behavior for options) + exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@" +fi \ No newline at end of file diff --git a/web/package/lib/cli.js b/web/package/lib/cli.js new file mode 100755 index 00000000..2cf65260 --- /dev/null +++ b/web/package/lib/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./vibetunnel-cli'); diff --git a/web/package/lib/vibetunnel-cli b/web/package/lib/vibetunnel-cli new file mode 100755 index 00000000..8ed966ce --- /dev/null +++ b/web/package/lib/vibetunnel-cli @@ -0,0 +1,125 @@ +#!/usr/bin/env node +var zo=Object.create;var Vr=Object.defineProperty;var Ho=Object.getOwnPropertyDescriptor;var Wo=Object.getOwnPropertyNames;var qo=Object.getPrototypeOf,Vo=Object.prototype.hasOwnProperty;var Ze=(o,e)=>()=>(o&&(e=o(o=0)),e);var Ko=(o,e)=>()=>(e||o((e={exports:{}}).exports,e),e.exports),Ms=(o,e)=>{for(var t in e)Vr(o,t,{get:e[t],enumerable:!0})},Go=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Wo(e))!Vo.call(o,s)&&s!==t&&Vr(o,s,{get:()=>e[s],enumerable:!(r=Ho(e,s))||r.enumerable});return o};var x=(o,e,t)=>(t=o!=null?zo(qo(o)):{},Go(e||!o||!o.__esModule?Vr(t,"default",{value:o,enumerable:!0}):t,o));function Yo(){let o=new Map;for(let[e,t]of Object.entries(Q)){for(let[r,s]of Object.entries(t))Q[r]={open:`\x1B[${s[0]}m`,close:`\x1B[${s[1]}m`},t[r]=Q[r],o.set(s[0],s[1]);Object.defineProperty(Q,e,{value:t,enumerable:!1})}return Object.defineProperty(Q,"codes",{value:o,enumerable:!1}),Q.color.close="\x1B[39m",Q.bgColor.close="\x1B[49m",Q.color.ansi=Ls(),Q.color.ansi256=Fs(),Q.color.ansi16m=Bs(),Q.bgColor.ansi=Ls(10),Q.bgColor.ansi256=Fs(10),Q.bgColor.ansi16m=Bs(10),Object.defineProperties(Q,{rgbToAnsi256:{value(e,t,r){return e===t&&t===r?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(t/255*5)+Math.round(r/255*5)},enumerable:!1},hexToRgb:{value(e){let t=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!t)return[0,0,0];let[r]=t;r.length===3&&(r=[...r].map(n=>n+n).join(""));let s=Number.parseInt(r,16);return[s>>16&255,s>>8&255,s&255]},enumerable:!1},hexToAnsi256:{value:e=>Q.rgbToAnsi256(...Q.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){if(e<8)return 30+e;if(e<16)return 90+(e-8);let t,r,s;if(e>=232)t=((e-232)*10+8)/255,r=t,s=t;else{e-=16;let a=e%36;t=Math.floor(e/36)/5,r=Math.floor(a/6)/5,s=a%6/5}let n=Math.max(t,r,s)*2;if(n===0)return 30;let i=30+(Math.round(s)<<2|Math.round(r)<<1|Math.round(t));return n===2&&(i+=60),i},enumerable:!1},rgbToAnsi:{value:(e,t,r)=>Q.ansi256ToAnsi(Q.rgbToAnsi256(e,t,r)),enumerable:!1},hexToAnsi:{value:e=>Q.ansi256ToAnsi(Q.hexToAnsi256(e)),enumerable:!1}}),Q}var Ls,Fs,Bs,Q,ha,Jo,Qo,ma,Xo,je,Os=Ze(()=>{Ls=(o=0)=>e=>`\x1B[${e+o}m`,Fs=(o=0)=>e=>`\x1B[${38+o};5;${e}m`,Bs=(o=0)=>(e,t,r)=>`\x1B[${38+o};2;${e};${t};${r}m`,Q={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}},ha=Object.keys(Q.modifier),Jo=Object.keys(Q.color),Qo=Object.keys(Q.bgColor),ma=[...Jo,...Qo];Xo=Yo(),je=Xo});function Ce(o,e=globalThis.Deno?globalThis.Deno.args:or.default.argv){let t=o.startsWith("-")?"":o.length===1?"-":"--",r=e.indexOf(t+o),s=e.indexOf("--");return r!==-1&&(s===-1||r=2,has16m:o>=3}}function ti(o,{streamIsTTY:e,sniffFlags:t=!0}={}){let r=Zo();r!==void 0&&(nr=r);let s=t?nr:r;if(s===0)return 0;if(t){if(Ce("color=16m")||Ce("color=full")||Ce("color=truecolor"))return 3;if(Ce("color=256"))return 2}if("TF_BUILD"in ee&&"AGENT_NAME"in ee)return 1;if(o&&!e&&s===void 0)return 0;let n=s||0;if(ee.TERM==="dumb")return n;if(or.default.platform==="win32"){let i=_s.default.release().split(".");return Number(i[0])>=10&&Number(i[2])>=10586?Number(i[2])>=14931?3:2:1}if("CI"in ee)return["GITHUB_ACTIONS","GITEA_ACTIONS","CIRCLECI"].some(i=>i in ee)?3:["TRAVIS","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(i=>i in ee)||ee.CI_NAME==="codeship"?1:n;if("TEAMCITY_VERSION"in ee)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(ee.TEAMCITY_VERSION)?1:0;if(ee.COLORTERM==="truecolor"||ee.TERM==="xterm-kitty")return 3;if("TERM_PROGRAM"in ee){let i=Number.parseInt((ee.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(ee.TERM_PROGRAM){case"iTerm.app":return i>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(ee.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(ee.TERM)||"COLORTERM"in ee?1:n}function Us(o,e={}){let t=ti(o,{streamIsTTY:o&&o.isTTY,...e});return ei(t)}var or,_s,Kr,ee,nr,ri,zs,Hs=Ze(()=>{or=x(require("node:process"),1),_s=x(require("node:os"),1),Kr=x(require("node:tty"),1);({env:ee}=or.default);Ce("no-color")||Ce("no-colors")||Ce("color=false")||Ce("color=never")?nr=0:(Ce("color")||Ce("colors")||Ce("color=true")||Ce("color=always"))&&(nr=1);ri={stdout:Us({isTTY:Kr.default.isatty(1)}),stderr:Us({isTTY:Kr.default.isatty(2)})},zs=ri});function Ws(o,e,t){let r=o.indexOf(e);if(r===-1)return o;let s=e.length,n=0,i="";do i+=o.slice(n,r)+e+t,n=r+s,r=o.indexOf(e,n);while(r!==-1);return i+=o.slice(n),i}function qs(o,e,t,r){let s=0,n="";do{let i=o[r-1]==="\r";n+=o.slice(s,i?r-1:r)+e+(i?`\r +`:` +`)+t,s=r+1,r=o.indexOf(` +`,s)}while(r!==-1);return n+=o.slice(s),n}var Vs=Ze(()=>{});function Ut(o){return ni(o)}var Ks,Gs,Gr,vt,Ot,Js,wt,si,ni,Jr,oi,ii,Qr,ir,ai,ci,Ta,f,le=Ze(()=>{Os();Hs();Vs();({stdout:Ks,stderr:Gs}=zs),Gr=Symbol("GENERATOR"),vt=Symbol("STYLER"),Ot=Symbol("IS_EMPTY"),Js=["ansi","ansi","ansi256","ansi16m"],wt=Object.create(null),si=(o,e={})=>{if(e.level&&!(Number.isInteger(e.level)&&e.level>=0&&e.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let t=Ks?Ks.level:0;o.level=e.level===void 0?t:e.level},ni=o=>{let e=(...t)=>t.join(" ");return si(e,o),Object.setPrototypeOf(e,Ut.prototype),e};Object.setPrototypeOf(Ut.prototype,Function.prototype);for(let[o,e]of Object.entries(je))wt[o]={get(){let t=ir(this,Qr(e.open,e.close,this[vt]),this[Ot]);return Object.defineProperty(this,o,{value:t}),t}};wt.visible={get(){let o=ir(this,this[vt],!0);return Object.defineProperty(this,"visible",{value:o}),o}};Jr=(o,e,t,...r)=>o==="rgb"?e==="ansi16m"?je[t].ansi16m(...r):e==="ansi256"?je[t].ansi256(je.rgbToAnsi256(...r)):je[t].ansi(je.rgbToAnsi(...r)):o==="hex"?Jr("rgb",e,t,...je.hexToRgb(...r)):je[t][o](...r),oi=["rgb","hex","ansi256"];for(let o of oi){wt[o]={get(){let{level:t}=this;return function(...r){let s=Qr(Jr(o,Js[t],"color",...r),je.color.close,this[vt]);return ir(this,s,this[Ot])}}};let e="bg"+o[0].toUpperCase()+o.slice(1);wt[e]={get(){let{level:t}=this;return function(...r){let s=Qr(Jr(o,Js[t],"bgColor",...r),je.bgColor.close,this[vt]);return ir(this,s,this[Ot])}}}}ii=Object.defineProperties(()=>{},{...wt,level:{enumerable:!0,get(){return this[Gr].level},set(o){this[Gr].level=o}}}),Qr=(o,e,t)=>{let r,s;return t===void 0?(r=o,s=e):(r=t.openAll+o,s=e+t.closeAll),{open:o,close:e,openAll:r,closeAll:s,parent:t}},ir=(o,e,t)=>{let r=(...s)=>ai(r,s.length===1?""+s[0]:s.join(" "));return Object.setPrototypeOf(r,ii),r[Gr]=o,r[vt]=e,r[Ot]=t,r},ai=(o,e)=>{if(o.level<=0||!e)return o[Ot]?"":e;let t=o[vt];if(t===void 0)return e;let{openAll:r,closeAll:s}=t;if(e.includes("\x1B"))for(;t!==void 0;)e=Ws(e,t.close,t.open),t=t.parent;let n=e.indexOf(` +`);return n!==-1&&(e=qs(e,s,r,n)),r+e+s};Object.defineProperties(Ut.prototype,wt);ci=Ut(),Ta=Ut({level:Gs?Gs.level:0}),f=ci});function Ys(o){Le&&(Le.end(),Le=null),kt=o;let e=_t.dirname(kt);Ie.existsSync(e)||Ie.mkdirSync(e,{recursive:!0});try{Le=Ie.createWriteStream(kt,{flags:"a"})}catch(t){console.error("Failed to open log file at new location:",t)}}function ar(o){let e=o.toLowerCase();return li[e]}function cr(o=!1,e){if(Xr=o,e!==void 0?et=e:o&&(et=5),!Le)try{Ie.existsSync(Yr)||Ie.mkdirSync(Yr,{recursive:!0});try{Ie.existsSync(kt)&&Ie.unlinkSync(kt)}catch{}Le=Ie.createWriteStream(kt,{flags:"a"})}catch(t){console.error("Failed to initialize log file:",t)}}function se(){Le&&(Le.end(),Le=null)}function Tt(o,e,t){let r=new Date().toISOString(),s=t.map(c=>{if(typeof c=="object")try{return JSON.stringify(c,null,2)}catch{return String(c)}return String(c)}).join(" "),n,i=f.cyan(`[${e}]`),a=f.gray(r);switch(o){case"ERROR":n=`${a} ${f.red(o)} ${i} ${f.red(s)}`;break;case"WARN":n=`${a} ${f.yellow(o)} ${i} ${f.yellow(s)}`;break;case"DEBUG":n=`${a} ${f.magenta(o)} ${i} ${f.gray(s)}`;break;default:n=`${a} ${f.green(o)} ${i} ${s}`}let u=`${r} ${o.padEnd(5)} [${e}] ${s}`;return{console:n,file:u}}function Et(o){if(Le)try{let e=o.replace(ui,"");Le.write(`${e} +`)}catch{}}function Zr(o){Xr=o,o&&(et=5)}function es(o){et=o,Xr=o>=5}function xt(o){switch(o){case"ERROR":return et>=1;case"WARN":return et>=2;case"LOG":return et>=3;case"DEBUG":return et>=5;default:return!0}}function Xs(o,e,t){let{console:r,file:s}=Tt(o,e,t);if(Et(s),!!xt(o))switch(o){case"ERROR":console.error(r);break;case"WARN":console.warn(r);break;default:console.log(r)}}function k(o){let e=o.startsWith("[")?o:`[SRV] ${o}`;return{log:(...t)=>{let{console:r,file:s}=Tt("LOG",e,t);Et(s),xt("LOG")&&console.log(r)},info:(...t)=>{let{console:r,file:s}=Tt("LOG",e,t);Et(s),xt("LOG")&&console.log(r)},warn:(...t)=>{let{console:r,file:s}=Tt("WARN",e,t);Et(s),xt("WARN")&&console.warn(r)},error:(...t)=>{let{console:r,file:s}=Tt("ERROR",e,t);Et(s),xt("ERROR")&&console.error(r)},debug:(...t)=>{let{console:r,file:s}=Tt("DEBUG",e,t);Et(s),xt("DEBUG")&&console.log(r)},setDebugMode:t=>Zr(t),setVerbosity:t=>es(t)}}var Ie,Qs,_t,Yr,kt,zt,li,et,Xr,Le,ui,L=Ze(()=>{le();Ie=x(require("fs")),Qs=x(require("os")),_t=x(require("path")),Yr=_t.join(Qs.homedir(),".vibetunnel"),kt=_t.join(Yr,"log.txt");zt=(i=>(i[i.SILENT=0]="SILENT",i[i.ERROR=1]="ERROR",i[i.WARN=2]="WARN",i[i.INFO=3]="INFO",i[i.VERBOSE=4]="VERBOSE",i[i.DEBUG=5]="DEBUG",i))(zt||{}),li={silent:0,error:1,warn:2,info:3,verbose:4,debug:5},et=1,Xr=!1;Le=null,ui=/\x1b\[[0-9;]*m/g});var an=Ko((Fa,on)=>{var rs=require("path"),vi=require("fs"),wi=[rs.join(__dirname,"../node-pty"),rs.join(__dirname,"../../node-pty"),rs.join(__dirname,"../node_modules/node-pty"),"node-pty"],Wt,nn;for(let o of wi)try{if((o==="node-pty"||vi.existsSync(o))&&(Wt=require(o)),Wt)break}catch(e){nn=e}if(!Wt)throw new Error(`Failed to load node-pty from any location. Last error: ${nn?.message}`);on.exports=Wt});var En={};Ms(En,{FishHandler:()=>Rt,fishHandler:()=>Mi});var cs,Tn,Rt,Mi,ls=Ze(()=>{cs=require("child_process"),Tn=x(require("path")),Rt=class{async getCompletions(e,t=process.cwd()){return new Promise(r=>{try{let s=(0,cs.spawn)("fish",["-c",`complete -C ${JSON.stringify(e)}`],{cwd:t,stdio:["ignore","pipe","ignore"]}),n="",i=setTimeout(()=>{s.kill("SIGTERM"),r([])},2e3);s.stdout?.on("data",a=>{n+=a.toString()}),s.on("close",a=>{if(clearTimeout(i),a!==0||!n.trim()){r([]);return}let u=n.split(` +`).filter(c=>c.trim()).map(c=>c.split(" ")[0]).filter(c=>c&&c!==e);r(u)}),s.on("error",()=>{clearTimeout(i),r([])})}catch{r([])}})}static isFishShell(e){let t=Tn.default.basename(e);return t==="fish"||/^fish\d*$/.test(t)}static async getFishVersion(){return new Promise(e=>{try{let t=(0,cs.spawn)("fish",["--version"],{stdio:["ignore","pipe","ignore"]}),r="",s=setTimeout(()=>{t.kill("SIGTERM"),e(null)},1e3);t.stdout?.on("data",n=>{r+=n.toString()}),t.on("close",n=>{clearTimeout(s),e(n===0&&r.trim()?r.trim():null)}),t.on("error",()=>{clearTimeout(s),e(null)})}catch{e(null)}})}},Mi=new Rt});function Er(o,e,t,r){return{id:crypto.randomUUID(),type:"request",category:o,action:e,payload:t,sessionId:r}}function Se(o,e,t){return{id:o.id,type:"response",category:o.category,action:o.action,payload:e,sessionId:o.sessionId,error:t}}function jt(o,e,t,r){return{id:crypto.randomUUID(),type:"event",category:o,action:e,payload:t,sessionId:r}}var ms=Ze(()=>{});var go={};Ms(go,{ControlUnixHandler:()=>xr,controlUnixHandler:()=>Be});var fo,Je,po,ys,bs,S,Ss,vs,ws,xr,Be,kr=Ze(()=>{fo=x(require("node:child_process")),Je=x(require("node:fs")),po=x(require("node:net")),ys=x(require("node:path")),bs=require("ws");L();ms();S=k("control-unix"),Ss=class{async handleMessage(e){if(S.log(`Terminal handler: ${e.action}`),e.action==="spawn"){let t=e.payload;try{let r=["launch"];return t.workingDirectory&&r.push("--working-directory",t.workingDirectory),t.command&&r.push("--command",t.command),r.push("--session-id",t.sessionId),t.terminalPreference&&r.push("--terminal",t.terminalPreference),S.log(`Spawning terminal with args: ${r.join(" ")}`),fo.spawn("vibetunnel",r,{detached:!0,stdio:"ignore"}).unref(),Se(e,{success:!0})}catch(r){return S.error("Failed to spawn terminal:",r),Se(e,null,r instanceof Error?r.message:"Failed to spawn terminal")}}return Se(e,null,`Unknown terminal action: ${e.action}`)}},vs=class{constructor(e){this.controlUnixHandler=e}async handleMessage(e){switch(S.log(`System handler: ${e.action}, type: ${e.type}, id: ${e.id}`),e.action){case"repository-path-update":{let t=e.payload;if(S.log(`Repository path update received: ${JSON.stringify(t)}`),!t?.path)return S.error("Missing path in payload"),Se(e,null,"Missing path in payload");try{return S.log(`Calling updateRepositoryPath with: ${t.path}`),await this.controlUnixHandler.updateRepositoryPath(t.path)?(S.log(`Successfully updated repository path to: ${t.path}`),Se(e,{success:!0,path:t.path})):(S.error("updateRepositoryPath returned false"),Se(e,null,"Failed to update repository path"))}catch(r){return S.error("Failed to update repository path:",r),Se(e,null,r instanceof Error?r.message:"Failed to update repository path")}}case"ping":return null;case"ready":return null;default:return S.warn(`Unknown system action: ${e.action}`),Se(e,null,`Unknown action: ${e.action}`)}}},ws=class{constructor(e){this.controlUnixHandler=e}browserSocket=null;userId=null;setBrowserSocket(e,t){this.browserSocket=e,this.userId=t||null,S.log(`\u{1F510} ScreenCaptureHandler userId set to: ${this.userId||"unknown"}`)}isBrowserConnected(){return this.browserSocket!==null&&this.browserSocket.readyState===bs.WebSocket.OPEN}getUserId(){return this.userId}async handleMessage(e){switch(S.log(`Screen capture handler: ${e.action}`),e.sessionId&&this.userId&&S.log(`\u{1F510} Associating sessionId ${e.sessionId} with userId ${this.userId}`),e.action){case"mac-ready":return this.browserSocket&&(this.sendToBrowser(jt("screencap","ready","Mac peer connected")),S.log("\u23F1\uFE0F Scheduling initial data request with 100ms delay"),setTimeout(()=>{S.log("\u23F0 Delay complete, now requesting initial data"),this.requestInitialData()},100)),null;case"api-request":return S.log(`Forwarding API request from browser to Mac: ${e.id}`),null;case"api-response":return this.browserSocket&&(S.log(`Forwarding API response to browser: ${e.id}`),this.sendToBrowser(e)),null;case"offer":case"answer":case"ice-candidate":case"bitrate-adjustment":return this.browserSocket&&(S.log(`Forwarding ${e.action} to browser`),this.sendToBrowser(e)),null;case"start-capture":return S.log("Forwarding start-capture request to Mac app"),null;case"ping":return S.debug("Received ping from Mac, sending pong"),Se(e,{timestamp:Date.now()/1e3});case"state-change":case"display-disconnected":case"window-disconnected":return this.browserSocket&&(S.log(`Forwarding ${e.action} event to browser`),this.sendToBrowser(e)),null;default:return S.warn(`Unknown screen capture action: ${e.action}`),Se(e,null,`Unknown action: ${e.action}`)}}sendToBrowser(e){this.browserSocket&&this.browserSocket.readyState===bs.WebSocket.OPEN&&this.browserSocket.send(JSON.stringify(e))}requestInitialData(){S.log("\u{1F4E4} Requesting initial data from Mac...");let e=Er("screencap","get-initial-data",{});S.log(`\u{1F4DD} Initial data request: ${JSON.stringify(e)}`),this.controlUnixHandler.sendToMac(e),S.log("\u2705 Initial data request sent")}},xr=class{pendingRequests=new Map;macSocket=null;unixServer=null;socketPath;handlers=new Map;screenCaptureHandler;messageBuffer=Buffer.alloc(0);configUpdateCallback=null;currentRepositoryPath=null;constructor(){let e=process.env.HOME||"/tmp",t=ys.join(e,".vibetunnel");try{Je.mkdirSync(t,{recursive:!0})}catch{}this.socketPath=ys.join(t,"control.sock"),this.handlers.set("terminal",new Ss),this.handlers.set("system",new vs(this)),this.screenCaptureHandler=new ws(this),this.handlers.set("screencap",this.screenCaptureHandler)}async start(){S.log("\u{1F680} Starting control Unix socket handler"),S.log(`\u{1F4C2} Socket path: ${this.socketPath}`);try{Je.existsSync(this.socketPath)?(Je.unlinkSync(this.socketPath),S.log("\u{1F9F9} Removed existing stale socket file.")):S.log("\u2705 No existing socket file found")}catch(e){S.warn("\u26A0\uFE0F Failed to remove stale socket file:",e)}this.unixServer=po.createServer(e=>{this.handleMacConnection(e)}),await new Promise((e,t)=>{this.unixServer?.listen(this.socketPath,()=>{S.log(`Control UNIX socket server listening at ${this.socketPath}`),Je.chmod(this.socketPath,384,r=>{r?S.error("Failed to set socket permissions:",r):S.log("Socket permissions set to 0600 (owner read/write only)")}),e()}),this.unixServer?.on("error",r=>{S.error("UNIX socket server error:",r),t(r)})})}stop(){this.macSocket&&(this.macSocket.destroy(),this.macSocket=null),this.unixServer&&(this.unixServer.close(),this.unixServer=null);try{Je.unlinkSync(this.socketPath)}catch{}}isMacAppConnected(){return this.macSocket!==null&&!this.macSocket.destroyed}handleMacConnection(e){S.log("\u{1F50C} New Mac connection via UNIX socket"),S.log(`\u{1F50D} Socket info: local=${e.localAddress}, remote=${e.remoteAddress}`),this.macSocket&&(S.log("\u26A0\uFE0F Closing existing Mac connection"),this.macSocket.destroy()),this.macSocket=e,S.log("\u2705 Mac socket stored"),this.screenCaptureHandler.isBrowserConnected()&&(S.log("\u{1F310} Browser is already connected, sending mac-ready event."),this.screenCaptureHandler.handleMessage(jt("screencap","mac-ready"))),e.setNoDelay(!0),S.log("\u2705 Socket options set: NoDelay=true");let t=1024*1024;try{let r=e;r._readableState&&(r._readableState.highWaterMark=t,S.log(`Set socket receive buffer to ${t} bytes`))}catch(r){S.warn("Failed to set socket buffer size:",r)}e.on("data",r=>{if(this.messageBuffer=Buffer.concat([this.messageBuffer,r]),S.log(`\u{1F4E5} Received from Mac: ${r.length} bytes, buffer size: ${this.messageBuffer.length}`),r.length>0){let s=r.subarray(0,Math.min(r.length,50));S.debug(`\u{1F4CB} Data preview (first ${s.length} bytes):`,s.toString("hex"))}for(;!(this.messageBuffer.length<4);){let s=this.messageBuffer.readUInt32BE(0);if(s<=0){S.error(`Invalid message length: ${s}`),this.messageBuffer=Buffer.alloc(0);break}let n=10*1024*1024;if(s>n){S.error(`Message too large: ${s} bytes (max: ${n})`),this.messageBuffer=Buffer.alloc(0);break}if(this.messageBuffer.length<4+s){S.debug(`Waiting for more data: have ${this.messageBuffer.length}, need ${4+s}`);break}let i=this.messageBuffer.subarray(4,4+s);this.messageBuffer=this.messageBuffer.subarray(4+s);try{let a=i.toString("utf-8");S.debug(`\u{1F4E8} Parsing message (${s} bytes): ${a.substring(0,100)}...`);let u=JSON.parse(a);S.log(`\u2705 Parsed Mac message: category=${u.category}, action=${u.action}, id=${u.id}`),this.handleMacMessage(u)}catch(a){S.error("\u274C Failed to parse Mac message:",a),S.error("Message length:",s),S.error("Raw message buffer:",i.toString("utf-8"))}}}),e.on("error",r=>{S.error("\u274C Mac socket error:",r);let s=r;S.error("Error details:",{code:s.code,syscall:s.syscall,errno:s.errno,message:s.message}),(s.code==="EPIPE"||s.code==="ECONNRESET")&&S.error("\u{1F534} Connection broken - Mac app likely closed the connection")}),e.on("close",r=>{S.log(`\u{1F50C} Mac disconnected (hadError: ${r})`),S.log(`\u{1F4CA} Socket state: destroyed=${e.destroyed}, readable=${e.readable}, writable=${e.writable}`),e===this.macSocket&&(this.macSocket=null,S.log("\u{1F9F9} Cleared Mac socket reference"),this.screenCaptureHandler.setBrowserSocket(null))}),e.on("drain",()=>{S.log("Mac socket drained - ready for more data")}),e.on("end",()=>{S.log("\u{1F4F4} Mac socket received FIN packet (clean close)")}),S.log("\u{1F4E4} Sending initial system:ready event to Mac"),this.sendToMac(jt("system","ready")),S.log("\u2705 system:ready event sent")}handleBrowserConnection(e,t){S.log("\u{1F310} New browser WebSocket connection for control messages"),S.log(`\u{1F464} User ID: ${t||"unknown"}`),S.log(`\u{1F50C} Mac socket status on browser connect: ${this.macSocket?"CONNECTED":"NOT CONNECTED"}`),S.log(`\u{1F5A5}\uFE0F Screen capture handler exists: ${!!this.screenCaptureHandler}`),this.screenCaptureHandler.setBrowserSocket(e,t),this.handlers.set("screencap",this.screenCaptureHandler),S.log("\u2705 Browser socket set in screen capture handler with userId:",t),this.macSocket?(S.log("\u2705 Mac is already connected, sending mac-ready event to trigger initialization"),this.screenCaptureHandler.handleMessage(jt("screencap","mac-ready")).catch(r=>{S.error("\u274C Failed to handle mac-ready event:",r)})):(S.log("\u23F3 Mac app not connected yet, waiting for Mac connection..."),S.log("\u{1F4A1} Make sure the Mac app is running and the Unix socket is connected")),e.on("message",async r=>{try{let s=r.toString();S.log(`\u{1F4E8} Browser message received (${s.length} chars): ${s.substring(0,200)}...`);let n=JSON.parse(s);if(S.log(`\u{1F4E5} Parsed browser message - type: ${n.type}, category: ${n.category}, action: ${n.action}`),n.category==="screencap"){S.log(`\u{1F5A5}\uFE0F Processing screencap message: ${n.action}`),S.log(`\u{1F4CB} Message ID: ${n.id}`),S.log(`\u{1F4CB} Message type: ${n.type}`),S.log("\u{1F4CB} Full message:",JSON.stringify(n));let i={...n,userId:this.screenCaptureHandler.getUserId()||"unknown"};if(S.log(`\u{1F510} Adding userId ${i.userId} to message`),this.macSocket)S.log(`\u{1F4E4} Forwarding ${n.action} to Mac app via Unix socket with auth context`),S.log(`\u{1F50C} Mac socket state: ${this.macSocket.destroyed?"DESTROYED":"ACTIVE"}`),this.sendToMac(i);else if(S.warn("\u274C No Mac connected to handle screen capture request"),S.warn("\u{1F4A1} The Mac app needs to be running and connected via Unix socket"),n.type==="request"){let a=Se(n,null,"Mac app not connected - ensure VibeTunnel Mac app is running");S.log("\u{1F4E4} Sending error response to browser:",a),e.send(JSON.stringify(a))}}else S.warn(`\u26A0\uFE0F Browser sent message for unsupported category: ${n.category}`)}catch(s){S.error("\u274C Failed to parse browser message:",s),e.send(JSON.stringify(jt("system","error",{error:s instanceof Error?s.message:String(s)})))}}),e.on("close",()=>{S.log("Browser disconnected"),this.screenCaptureHandler.setBrowserSocket(null)}),e.on("error",r=>{S.error("Browser WebSocket error:",r)})}async handleMacMessage(e){if(S.log(`Mac message - category: ${e.category}, action: ${e.action}, type: ${e.type}, id: ${e.id}`),e.category==="system"&&e.action==="ping"){let r=Se(e,{status:"ok"});this.sendToMac(r);return}if(e.category==="system"&&e.action==="repository-path-update"&&S.log("\u{1F50D} Repository path update message details:",JSON.stringify(e)),e.type==="response"&&this.pendingRequests.has(e.id)){let r=this.pendingRequests.get(e.id);r&&(S.debug(`Resolving pending request for id: ${e.id}`),this.pendingRequests.delete(e.id),r(e));return}if(e.type==="response"&&e.category!=="screencap"){S.debug(`Ignoring response message that has no pending request: ${e.id}, action: ${e.action}`);return}e.type==="response"&&e.category==="screencap"&&S.log(`\u{1F4E1} Forwarding screencap response to handler: ${e.id}, action: ${e.action}`);let t=this.handlers.get(e.category);if(!t){if(S.warn(`No handler for category: ${e.category}`),e.type==="request"){let r=Se(e,null,`Unknown category: ${e.category}`);this.sendToMac(r)}return}try{let r=await t.handleMessage(e);r&&this.sendToMac(r)}catch(r){if(S.error(`Handler error for ${e.category}:${e.action}:`,r),e.type==="request"){let s=Se(e,null,r instanceof Error?r.message:"Handler error");this.sendToMac(s)}}}async sendControlMessage(e){return new Promise(t=>{this.pendingRequests.set(e.id,t),this.sendToMac(e),setTimeout(()=>{this.pendingRequests.has(e.id)&&(this.pendingRequests.delete(e.id),t(null))},1e4)})}sendToMac(e){if(!this.macSocket){S.warn("\u26A0\uFE0F Cannot send to Mac - no socket connection");return}if(this.macSocket.destroyed){S.warn("\u26A0\uFE0F Cannot send to Mac - socket is destroyed"),this.macSocket=null;return}try{let t=JSON.stringify(e),r=Buffer.from(t,"utf-8"),s=Buffer.allocUnsafe(4);s.writeUInt32BE(r.length,0);let n=Buffer.concat([s,r]);S.log(`\u{1F4E4} Sending to Mac: ${e.category}:${e.action}, header: 4 bytes, payload: ${r.length} bytes, total: ${n.length} bytes`),S.log(`\u{1F4CB} Message ID being sent: ${e.id}`),S.debug(`\u{1F4DD} Message content: ${t.substring(0,200)}...`),(e.category==="system"||e.action==="get-initial-data")&&(S.debug(`\u{1F50D} Length header bytes: ${s.toString("hex")}`),S.debug(`\u{1F50D} First 50 bytes of full data: ${n.subarray(0,Math.min(50,n.length)).toString("hex")}`)),r.length>65536&&S.warn(`\u26A0\uFE0F Large message to Mac: ${r.length} bytes`),this.macSocket.write(n,a=>{a?(S.error("\u274C Error writing to Mac socket:",a),S.error("Error details:",{code:a.code,syscall:a.syscall,message:a.message}),this.macSocket?.destroy(),this.macSocket=null):S.debug("\u2705 Write to Mac socket completed successfully")})?S.debug("\u2705 Write immediate - no backpressure"):S.warn("\u26A0\uFE0F Socket write buffered - backpressure detected")}catch(t){S.error("\u274C Exception while sending to Mac:",t),this.macSocket?.destroy(),this.macSocket=null}}setConfigUpdateCallback(e){this.configUpdateCallback=e}async updateRepositoryPath(e){S.log(`updateRepositoryPath called with path: ${e}`);try{return this.currentRepositoryPath=e,S.log(`Set currentRepositoryPath to: ${this.currentRepositoryPath}`),this.configUpdateCallback?(S.log("Calling configUpdateCallback..."),this.configUpdateCallback({repositoryBasePath:e}),S.log("configUpdateCallback completed successfully"),!0):(S.warn("No config update callback set - is the server initialized?"),!1)}catch(t){return S.error("Failed to update repository path:",t),!1}}getRepositoryPath(){return this.currentRepositoryPath}},Be=new xr});function js(){let o=typeof process<"u"&&process.versions?.node,e=o?global:typeof globalThis<"u"?globalThis:global;if(e.__xtermErrorsSuppressed)return;e.__xtermErrorsSuppressed=!0;let t=console.error,r=console.warn;console.error=(...s)=>{Ds(s)||t.apply(console,s)},console.warn=(...s)=>{Ds(s)||r.apply(console,s)},o&&process.env.VIBETUNNEL_DEBUG==="1"&&r.call(console,"[suppress-xterm-errors] xterm.js error suppression activated")}function Ds(o){if(!o[0]||typeof o[0]!="string")return!1;let e=o[0];return!!(e.includes("xterm.js: Parsing error:")||e.includes("Unable to process character")&&e.includes("xterm"))}le();var Ne=x(require("fs")),us=x(require("os")),Ve=x(require("path"));var $t=(s=>(s.NONE="none",s.FILTER="filter",s.STATIC="static",s.DYNAMIC="dynamic",s))($t||{});var lr=require("events"),tt=x(require("fs")),Zs=x(require("path")),en=require("util");L();var lt=class{queue=Promise.resolve();enqueue(e){this.queue=this.queue.then(()=>e()).catch(t=>{console.error("WriteQueue error:",t)})}async drain(){await this.queue}};var j=class extends Error{constructor(t,r,s){super(t);this.code=r;this.sessionId=s;this.name="PtyError"}};var di=k("AsciinemaWriter"),fi=(0,en.promisify)(tt.fsync),Ht=class o{constructor(e,t){this.filePath=e;this.header=t;this.startTime=new Date;let r=Zs.dirname(e);tt.existsSync(r)||tt.mkdirSync(r,{recursive:!0}),this.writeStream=tt.createWriteStream(e,{flags:"w",encoding:"utf8",highWaterMark:0}),this.writeStream.on("open",s=>{this.fd=s}),this.writeHeader()}writeStream;startTime;utf8Buffer=Buffer.alloc(0);headerWritten=!1;fd=null;writeQueue=new lt;static create(e,t=80,r=24,s,n,i){let a={version:2,width:t,height:r,timestamp:Math.floor(Date.now()/1e3),command:s,title:n,env:i};return new o(e,a)}writeHeader(){this.headerWritten||(this.writeQueue.enqueue(async()=>{let e=JSON.stringify(this.header);this.writeStream.write(`${e} +`)||await(0,lr.once)(this.writeStream,"drain")}),this.headerWritten=!0)}writeOutput(e){this.writeQueue.enqueue(async()=>{let t=this.getElapsedTime(),r=Buffer.concat([this.utf8Buffer,e]),{processedData:s,remainingBuffer:n}=this.processTerminalData(r);if(s.length>0){let i={time:t,type:"o",data:s};await this.writeEvent(i)}this.utf8Buffer=n})}writeInput(e){this.writeQueue.enqueue(async()=>{let r={time:this.getElapsedTime(),type:"i",data:e};await this.writeEvent(r)})}writeResize(e,t){this.writeQueue.enqueue(async()=>{let s={time:this.getElapsedTime(),type:"r",data:`${e}x${t}`};await this.writeEvent(s)})}writeMarker(e){this.writeQueue.enqueue(async()=>{let r={time:this.getElapsedTime(),type:"m",data:e};await this.writeEvent(r)})}writeRawJson(e){this.writeQueue.enqueue(async()=>{let t=JSON.stringify(e);this.writeStream.write(`${t} +`)||await(0,lr.once)(this.writeStream,"drain")})}async writeEvent(e){let t=[e.time,e.type,e.data],r=JSON.stringify(t);if(this.writeStream.write(`${r} +`)||await(0,lr.once)(this.writeStream,"drain"),this.fd!==null)try{await fi(this.fd)}catch(n){di.debug(`fsync failed for ${this.filePath}:`,n)}}processTerminalData(e){let t="",r=0;for(;r0&&(t+=a.toString("utf8")),u=e.length){let l=e.subarray(s+u);if(l.length<=4&&this.mightBeIncompleteUtf8(l))return{processedData:t,remainingBuffer:l}}let c=n.subarray(u);t+=c.toString("latin1")}}return{processedData:t,remainingBuffer:Buffer.alloc(0)}}findEscapeSequenceEnd(e){if(e.length===0||e[0]!==27||e.length<2)return null;switch(e[1]){case 91:{let t=2;for(;t=32&&r<=63)t++;else return r>=64&&r<=126?t+1:t}return null}case 93:{let t=2;for(;t=192){if(t<224)return e.length<2;if(t<240)return e.length<3;if(t<248)return e.length<4}return!1}getElapsedTime(){return(Date.now()-this.startTime.getTime())/1e3}async close(){if(this.utf8Buffer.length>0){let t={time:this.getElapsedTime(),type:"o",data:this.utf8Buffer.toString("latin1")};this.writeQueue.enqueue(async()=>{await this.writeEvent(t)}),this.utf8Buffer=Buffer.alloc(0)}return await this.writeQueue.drain(),new Promise((e,t)=>{this.writeStream.end(r=>{r?t(new j(`Failed to close asciinema writer: ${r.message}`)):e()})})}isOpen(){return!this.writeStream.destroyed}};le();var rt=require("child_process"),ur=x(require("fs")),dr=x(require("os")),ut=x(require("path"));L();var fe=k("process-utils");function pi(o){let e=dr.homedir(),t=ut.basename(o),s={zsh:[".zshrc",".zshenv"],bash:[".bashrc",".bash_profile",".profile"],sh:[".profile"],ksh:[".kshrc",".profile"],fish:[".config/fish/config.fish"],tcsh:[".tcshrc",".cshrc"],csh:[".cshrc"],dash:[".profile"]}[t]||[];for(let i of s){let a=ut.join(e,i);if(tn(a))return a}let n=ut.join(e,".profile");return tn(n)?n:null}function tn(o){try{return ur.accessSync(o,ur.constants.F_OK),!0}catch{return!1}}function ts(o){if(!o||o<=0)return!1;try{return process.platform==="win32"?gi(o):hi(o)}catch(e){return fe.warn(`error checking if process ${o} is running:`,e),!1}}function gi(o){try{fe.debug(`checking windows process ${o} with tasklist`);let e=(0,rt.spawnSync)("tasklist",["/FI",`PID eq ${o}`,"/NH","/FO","CSV"],{encoding:"utf8",windowsHide:!0,timeout:5e3});if(e.status===0&&e.stdout){let t=e.stdout.includes(`"${o}"`);return fe.debug(`process ${o} exists: ${t}`),t}return fe.debug(`tasklist command failed with status ${e.status}`),!1}catch(e){return fe.warn(`windows process check failed for PID ${o}:`,e),!1}}function hi(o){try{return process.kill(o,0),!0}catch(e){return e.code==="EPERM"}}function mi(o){return ts(o)?{pid:o,exists:!0}:null}function yi(o,e="SIGTERM"){if(!o||o<=0)return!1;fe.debug(`attempting to kill process ${o} with signal ${e}`);try{if(process.platform==="win32"){let t=(0,rt.spawnSync)("taskkill",["/PID",o.toString(),"/F"],{windowsHide:!0,timeout:5e3});return t.status===0?(fe.log(f.green(`process ${o} killed successfully`)),!0):(fe.debug(`taskkill failed with status ${t.status}`),!1)}else return process.kill(o,e),fe.log(f.green(`signal ${e} sent to process ${o}`)),!0}catch(t){return fe.warn(`error killing process ${o}:`,t),!1}}async function bi(o,e=5e3){let t=Date.now(),r=100;for(fe.debug(`waiting for process ${o} to exit (timeout: ${e}ms)`);Date.now()-tsetTimeout(s,r))}return fe.log(f.yellow(`process ${o} did not exit within ${e}ms timeout`)),!1}function rn(o,e){if(!["bash","zsh","sh","fish","dash","ksh","tcsh","csh"].some(n=>o===n||o.endsWith(`/${n}`)))return!1;let s=["-i","--interactive","-l","--login"];return e.length===0?!0:e.some(n=>s.includes(n))}function Si(o){if(o.length===0)throw new Error("No command provided");let e=o[0],t=o.slice(1),r=process.platform==="win32"?"where":"which";try{let i=(0,rt.spawnSync)(r,[e],{encoding:"utf8",windowsHide:!0,timeout:2e3});if(i.status===0&&i.stdout&&i.stdout.trim()){if(fe.debug(`Command '${e}' found at: ${i.stdout.trim()}`),rn(e,t)){fe.log(f.cyan(`\u2713 Starting ${e} as login shell to load configuration files`));let a=t.some(d=>d==="-i"||d==="--interactive"),u=t.some(d=>d==="-l"||d==="--login"),c=[...t],l=e==="fish"||e.endsWith("/fish");return a||c.unshift(l?"--interactive":"-i"),u||c.unshift(l?"--login":"-l"),{command:e,args:c,useShell:!1,resolvedFrom:"path",originalCommand:e,isInteractive:!0}}return{command:e,args:t,useShell:!1,resolvedFrom:"path",originalCommand:e}}}catch(i){fe.debug(`Failed to check command existence for '${e}':`,i)}fe.debug(`Command '${e}' not found in PATH, will use shell`);let s=sn(),n=!rn(e,t);return process.platform==="win32"?s.includes("bash")?n?{command:s,args:["-c",o.join(" ")],useShell:!0,resolvedFrom:"shell"}:{command:s,args:["-i","-c",o.join(" ")],useShell:!0,resolvedFrom:"shell",isInteractive:!0}:s.includes("pwsh")||s.includes("powershell")?{command:s,args:["-NoLogo","-Command",o.join(" ")],useShell:!0,resolvedFrom:"shell"}:{command:s,args:["/C",o.join(" ")],useShell:!0,resolvedFrom:"shell"}:n?pi(s)?{command:s,args:["-i","-l","-c",o.join(" ")],useShell:!0,resolvedFrom:"alias"}:{command:s,args:["-c",o.join(" ")],useShell:!0,resolvedFrom:"shell"}:{command:s,args:["-i","-l","-c",o.join(" ")],useShell:!0,resolvedFrom:"shell",isInteractive:!0}}function sn(){if(process.env.SHELL)return process.env.SHELL;if(process.platform==="win32"){try{if((0,rt.spawnSync)("pwsh",["-Command","echo test"],{encoding:"utf8",windowsHide:!0,timeout:1e3}).status===0)return"pwsh"}catch{}let o=ut.join(process.env.SystemRoot||"C:\\Windows","System32","WindowsPowerShell","v1.0","powershell.exe");try{if((0,rt.spawnSync)(o,["-Command","echo test"],{encoding:"utf8",windowsHide:!0,timeout:1e3}).status===0)return o}catch{}let e=["C:\\Program Files\\Git\\bin\\bash.exe","C:\\Program Files (x86)\\Git\\bin\\bash.exe",ut.join(process.env.ProgramFiles||"C:\\Program Files","Git","bin","bash.exe")];for(let t of e)try{if((0,rt.spawnSync)(t,["-c","echo test"],{encoding:"utf8",windowsHide:!0,timeout:1e3}).status===0)return t}catch{}return process.env.ComSpec||"cmd.exe"}else{try{let e=dr.userInfo();if("shell"in e&&e.shell)return e.shell}catch{}let o=["/bin/zsh","/bin/bash","/usr/bin/zsh","/usr/bin/bash","/bin/sh"];for(let e of o)try{if((0,rt.spawnSync)("test",["-x",e],{encoding:"utf8",timeout:500}).status===0)return e}catch{}return"/bin/sh"}}var Ae={isProcessRunning:ts,getProcessInfo:mi,killProcess:yi,waitForProcessExit:bi,resolveCommand:Si,getUserShell:sn};le();var Kt=require("events"),we=x(require("fs")),Gt=x(require("net")),Pn=x(an()),_e=x(require("path"));var pe=[];for(let o=0;o<256;++o)pe.push((o+256).toString(16).slice(1));function cn(o,e=0){return(pe[o[e+0]]+pe[o[e+1]]+pe[o[e+2]]+pe[o[e+3]]+"-"+pe[o[e+4]]+pe[o[e+5]]+"-"+pe[o[e+6]]+pe[o[e+7]]+"-"+pe[o[e+8]]+pe[o[e+9]]+"-"+pe[o[e+10]]+pe[o[e+11]]+pe[o[e+12]]+pe[o[e+13]]+pe[o[e+14]]+pe[o[e+15]]).toLowerCase()}var ln=require("crypto"),pr=new Uint8Array(256),fr=pr.length;function ss(){return fr>pr.length-16&&((0,ln.randomFillSync)(pr),fr=0),pr.slice(fr,fr+=16)}var un=require("crypto"),ns={randomUUID:un.randomUUID};function $i(o,e,t){if(ns.randomUUID&&!e&&!o)return ns.randomUUID();o=o||{};let r=o.random??o.rng?.()??ss();if(r.length<16)throw new Error("Random bytes length must be >= 16");if(r[6]=r[6]&15|64,r[8]=r[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let s=0;s<16;++s)e[t+s]=r[s];return e}return cn(r)}var Ue=$i;var dn=require("child_process"),fn=require("util");L();var G=k("process-tree-analyzer"),os=(0,fn.promisify)(dn.exec),dt=class o{async getProcessTree(e){try{return process.platform==="win32"?await this.getWindowsProcessTree(e):await this.getUnixProcessTree(e)}catch(t){return G.warn("ProcessTreeAnalyzer",`Failed to get process tree for PID ${e}:`,t),[]}}async getUnixProcessTree(e){let t=process.platform==="darwin";G.log("ProcessTreeAnalyzer",`Using recursive child search for ${e} to find all descendants`);try{return await this.getProcessTreeRecursive(e,t)}catch(r){G.warn("ProcessTreeAnalyzer","Recursive process search failed:",r);try{let s=t?`ps -o pid,ppid,pgid,tty,state,lstart,command -p ${e}`:`ps -o pid,ppid,pgid,sid,tty,state,lstart,command -p ${e}`,{stdout:n}=await os(s,{timeout:5e3});return this.parseUnixProcessOutput(n,t)}catch(s){return G.warn("ProcessTreeAnalyzer","Final fallback also failed:",s),[]}}}async getWindowsProcessTree(e){try{let{stdout:t}=await os(`wmic process where "ParentProcessId=${e}" get ProcessId,ParentProcessId,CommandLine /format:csv`,{timeout:5e3});return this.parseWindowsProcessOutput(t,e)}catch(t){return G.warn("ProcessTreeAnalyzer","Windows process query failed:",t),[]}}parseUnixProcessOutput(e,t=!1){let r=e.trim().split(` +`),s=[];for(let n=1;n=10){let u=Number.parseInt(a[0]),c=Number.parseInt(a[1]),l=Number.parseInt(a[2]),d=a[3]==="?"?void 0:a[3],g=a[4],p=a.slice(5,10).join(" "),h=a.slice(10).join(" ");!Number.isNaN(u)&&!Number.isNaN(c)&&!Number.isNaN(l)&&h&&(G.log("ProcessTreeAnalyzer",`Parsed macOS process: PID=${u}, COMMAND="${h.trim()}"`),s.push({pid:u,ppid:c,pgid:l,tty:d,state:g,startTime:p,command:h.trim()}))}}else{let a=i.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s+(.+)$/);if(a){let[,u,c,l,d,g,p,h,b]=a;s.push({pid:Number.parseInt(u),ppid:Number.parseInt(c),pgid:Number.parseInt(l),sid:Number.parseInt(d),tty:g==="?"?void 0:g,state:p,startTime:h,command:b.trim()})}}}catch{G.debug("ProcessTreeAnalyzer",`Failed to parse ps line: ${i}`)}}return s}parseWindowsProcessOutput(e,t){let r=e.trim().split(` +`),s=[];s.push({pid:t,ppid:0,pgid:t,command:"shell"});for(let n=1;n=3){let u=Number.parseInt(a[1]),c=Number.parseInt(a[2]),l=a[3]||"unknown";!Number.isNaN(u)&&!Number.isNaN(c)&&s.push({pid:u,ppid:c,pgid:u,command:l})}}return s}async identifyBellSource(e){let t=await this.getProcessTree(e);if(G.log("ProcessTreeAnalyzer",`Process tree for session ${e}: ${JSON.stringify(t.map(a=>({pid:a.pid,ppid:a.ppid,command:a.command})))}`),t.length===0)return G.warn("ProcessTreeAnalyzer",`No processes found in tree for session ${e}`),null;let r=this.findForegroundProcess(t,e);if(r)return G.debug("ProcessTreeAnalyzer",`Identified foreground process: ${r.command} (PID: ${r.pid})`),r;let s=this.findMostRecentChild(t,e);if(s)return G.debug("ProcessTreeAnalyzer",`Identified recent child process: ${s.command} (PID: ${s.pid})`),s;let n=t.find(a=>a.pid!==e&&!this.isShellProcess(a.command)&&!this.isBackgroundProcess(a.command));if(n)return G.debug("ProcessTreeAnalyzer",`Found non-shell process: ${n.command} (PID: ${n.pid})`),n;let i=t.find(a=>a.pid===e);return i?(G.debug("ProcessTreeAnalyzer",`Defaulting to shell process: ${i.command} (PID: ${i.pid})`),i):null}findForegroundProcess(e,t){let r=e.filter(a=>a.pid!==t&&a.ppid===t&&!this.isShellProcess(a.command)&&!this.isBackgroundProcess(a.command));if(G.log("ProcessTreeAnalyzer",`Direct child candidates: ${JSON.stringify(r.map(a=>({pid:a.pid,command:a.command})))}`),r.length===0&&(r=e.filter(a=>a.pid!==t&&!this.isShellProcess(a.command)&&!this.isBackgroundProcess(a.command)),G.log("ProcessTreeAnalyzer",`Descendant candidates: ${JSON.stringify(r.map(a=>({pid:a.pid,command:a.command})))}`)),r.length===0)return G.log("ProcessTreeAnalyzer","No suitable candidate processes found, bell likely from shell itself"),null;let s=new Date,n=r.filter(a=>{if(!a.startTime)return!0;let u=new Date(a.startTime),c=s.getTime()-u.getTime();return c<100?(G.log("ProcessTreeAnalyzer",`Filtering out very recent process: ${a.command} (age: ${c}ms)`),!1):!0});if(n.length===0)return G.log("ProcessTreeAnalyzer","All candidates were very recent (likely prompt utilities)"),null;let i=n.sort((a,u)=>a.startTime&&u.startTime?new Date(u.startTime).getTime()-new Date(a.startTime).getTime():0);return G.log("ProcessTreeAnalyzer",`Selected foreground candidate: ${i[0].command} (PID: ${i[0].pid})`),i[0]}findMostRecentChild(e,t){let r=e.filter(n=>n.ppid===t&&n.pid!==t&&!this.isShellProcess(n.command));return r.length===0&&(r=e.filter(n=>n.ppid===t&&n.pid!==t)),G.log("ProcessTreeAnalyzer",`Recent child candidates: ${JSON.stringify(r.map(n=>({pid:n.pid,command:n.command})))}`),r.length===0?null:r.sort((n,i)=>n.startTime&&i.startTime?new Date(i.startTime).getTime()-new Date(n.startTime).getTime():0)[0]}isShellProcess(e){let t=["bash","zsh","sh","fish","csh","tcsh","ksh"],r=o.extractProcessName(e);return t.includes(r.toLowerCase())}isBackgroundProcess(e){let t=["ssh-agent","gpg-agent","dbus-daemon","systemd","kworker","ksoftirqd","migration","watchdog"],r=["git status","git branch","hg branch","hg status","svn status","pwd","whoami","hostname","date","ps ","ls -la","df -h"],s=e.toLowerCase();return t.some(n=>s.includes(n))?!0:r.some(n=>s.includes(n))?(G.log("ProcessTreeAnalyzer",`Identified prompt utility: ${e}`),!0):!1}async getProcessTreeRecursive(e,t){let r=[],s=new Set,n=t?"ps -eo pid,ppid,pgid,tty,state,lstart,command":"ps -eo pid,ppid,pgid,sid,tty,state,lstart,command";G.log("ProcessTreeAnalyzer",`Getting all system processes with: ${n}`);let{stdout:i}=await os(n,{timeout:1e4}),a=this.parseUnixProcessOutput(i,t);G.log("ProcessTreeAnalyzer",`Found ${a.length} total system processes`);let u=new Map;for(let d of a){u.has(d.ppid)||u.set(d.ppid,[]);let g=u.get(d.ppid);g&&g.push(d)}let c=u.get(e)||[];G.log("ProcessTreeAnalyzer",`Direct children of ${e}: ${JSON.stringify(c.map(d=>({pid:d.pid,command:d.command})))}`);let l=d=>{if(s.has(d))return;s.add(d);let g=a.find(h=>h.pid===d);g&&r.push(g);let p=u.get(d)||[];for(let h of p)l(h.pid)};return l(e),G.log("ProcessTreeAnalyzer",`Final process tree: ${JSON.stringify(r.map(d=>({pid:d.pid,ppid:d.ppid,command:d.command})))}`),r}async captureProcessSnapshot(e){let t=await this.getProcessTree(e),r=this.findForegroundProcess(t,e),s=await this.identifyBellSource(e);return{sessionPid:e,processTree:t,foregroundProcess:r,suspectedBellSource:s,capturedAt:new Date().toISOString()}}static extractProcessName(e){return e.replace(/^.*\//,"").replace(/\s+.*$/,"").replace(/^sudo\s+/,"").replace(/^exec\s+/,"")||"unknown"}static getProcessDescription(e){if(!e)return"unknown process";let t=o.extractProcessName(e.command);return t==="bash"||t==="zsh"||t==="sh"||t==="fish"?"shell":t}};L();L();var is=k("prompt-patterns"),Ti=/\x1b\[[0-9;]*[a-zA-Z]/g,Ei=/(?])(?:\[[^\]]*\])?[$>#%❯➜]\s*(?:\x1b\[[0-9;]*[a-zA-Z])?$/,xi=/^(?:\[[^\]]*\])?(?]{2})[$>#%❯➜]\s*$/,ki={python:/^>>>\s*$/,pythonContinuation:/^\.\.\.\s*$/,bracketed:/\][#$]\s*$/,root:/#\s*$/,powershell:/^PS.*>\s*$|^>\s*$/,zsh:/[%❯]\s*$/,fish:/[❯➜]\s*$/,bash:/\$\s*$/,sh:/\$\s*$/,withEscape:/[$>#%❯➜]\s*\x1b\[/},ft;(l=>{let o=new Map,e=new Map,t=0,r=1e3;function s(d){if(d.length>1e4)return is.warn("Unusually long prompt input detected",{length:d.length}),!1;let g=d.trim();if(e.has(g))return e.get(g)??!1;let p=xi.test(g);return a(e,g,p),p}l.isPromptOnly=s;function n(d){let g=d.slice(-100);if(o.has(g))return o.get(g)??!1;let p=d.replace(Ti,""),h=Ei.test(p);return a(o,g,h),h&&is.debug("Detected prompt at end of output"),h}l.endsWithPrompt=n;function i(d){let g=d.trim();for(let[p,h]of Object.entries(ki))if(h.test(g))return p;return null}l.getShellType=i;function a(d,g,p){if(t>=r){let h=Math.floor(r*.2),b=d.keys();for(let v=0;vb.includes("interrupt")&&b.includes("tokens"));h&&(Fe("Found status line:",h),Fe("Line length:",h.length),gn&&Array.from(h.substring(0,50)).forEach((v,$)=>{console.log(` [${$}] '${v}' = U+${v.charCodeAt(0).toString(16).padStart(4,"0")}`)}))}return null}let[r,s,n,i,a,u]=t,c=a!==void 0&&u!==void 0;Fe("Claude status MATCHED!"),Fe(`Action: ${n}, Duration: ${i}s, Direction: ${a}, Tokens: ${u}`),Fe(`Indicator: '${s}'`),qt.debug(`Claude status MATCHED! Action: ${n}, Duration: ${i}s, Direction: ${a}, Tokens: ${u}`),qt.debug(`Full match: "${r}"`);let l=e.indexOf(r),d=o;if(l>=0){let p=0,h=0;for(;ho.join(" ").toLowerCase().includes("claude"),parseStatus:Ci}],Pt=class{lastActivityTime=Date.now();currentStatus=null;detector=null;lastStatusTime=0;ACTIVITY_TIMEOUT=5e3;STATUS_TIMEOUT=1e4;MEANINGFUL_OUTPUT_THRESHOLD=5;constructor(e){this.detector=Ii.find(t=>t.detect(e))||null,this.detector?qt.log(`ActivityDetector: Using ${this.detector.name} detector for command: ${e.join(" ")}`):qt.debug(`ActivityDetector: No specific detector found for command: ${e.join(" ")}`)}isJustPrompt(e){return ft.isPromptOnly(e)}processOutput(e){let t=e.trim();if(t.length>this.MEANINGFUL_OUTPUT_THRESHOLD&&!this.isJustPrompt(t)&&(this.lastActivityTime=Date.now()),this.detector&&e.length>10&&Fe(`Processing output with ${this.detector.name} detector (${e.length} chars)`),this.detector){let s=this.detector.parseStatus(e);if(s)return this.currentStatus=s,this.lastStatusTime=Date.now(),this.lastActivityTime=Date.now(),{filteredData:s.filteredData,activity:{isActive:!0,lastActivityTime:this.lastActivityTime,specificStatus:{app:this.detector.name,status:s.displayText}}}}return{filteredData:e,activity:this.getActivityState()}}getActivityState(){let e=Date.now(),t=e-this.lastActivityTimethis.STATUS_TIMEOUT&&(qt.debug("Clearing stale status - not seen for",this.STATUS_TIMEOUT,"ms"),this.currentStatus=null),{isActive:t,lastActivityTime:this.lastActivityTime,specificStatus:this.currentStatus&&this.detector?{app:this.detector.name,status:this.currentStatus.displayText}:void 0}}clearStatus(){this.currentStatus=null}};var gr=class o{buffer="";static COMPLETE_TITLE_REGEX=/\x1b\][0-2];[^\x07\x1b]*(?:\x07|\x1b\\)/g;static PARTIAL_TITLE_REGEX=/\x1b\][0-2];.*\x1b$|\x1b\][0-2];[^\x07]*$|\x1b(?:\](?:[0-2])?)?$/;filter(e){this.buffer+=e;let t=this.buffer.replace(o.COMPLETE_TITLE_REGEX,""),r=t.match(o.PARTIAL_TITLE_REGEX);return r?(this.buffer=r[0],t.slice(0,-r[0].length)):(this.buffer="",t)}};L();var Ct=x(require("os")),qe=x(require("path"));var Ai=/^\s*cd(?:\s+([^;&|\n]+?))?(?:\s*[;&|\n]|$)/;function Vt(o,e,t){let r=Ct.homedir(),s=o.startsWith(r)?o.replace(r,"~"):o,n=e[0]||"shell",i=qe.basename(n);if(t?.trim()){let c=t.trim();if(!(c===`${i} \xB7 ${i}`||c===i||c.match(new RegExp(`^${i}\\s*\\(.*\\)$`))))return`\x1B]2;${c}\x07`}let a=[s,i];if(t?.trim()){let c=t.trim();c===`${i} \xB7 ${i}`||c===i||c.match(new RegExp(`^${i}\\s*\\(.*\\)$`))||a.push(c)}return`\x1B]2;${a.join(" \xB7 ")}\x07`}function hn(o,e){let t=o.match(Ai);if(!t)return null;if(!t[1])return Ct.homedir();let r=t[1].trim();return(r.startsWith('"')&&r.endsWith('"')||r.startsWith("'")&&r.endsWith("'"))&&(r=r.slice(1,-1)),r==="-"?null:!r||r==="~"?Ct.homedir():r.startsWith("~/")?qe.join(Ct.homedir(),r.slice(2)):qe.isAbsolute(r)?r:qe.resolve(e,r)}function mn(o){return ft.endsWithPrompt(o)}function yn(o,e,t,r){let s=Ct.homedir(),n=o.startsWith(s)?o.replace(s,"~"):o,i=e[0]||"shell",a=qe.basename(i);if(r?.trim()){let l=r.trim();if(!(l===`${a} \xB7 ${a}`||l===a||l.match(new RegExp(`^${a}\\s*\\(.*\\)$`))))return t.specificStatus?`\x1B]2;${t.specificStatus.status} \xB7 ${l}\x07`:t.isActive?`\x1B]2;\u25CF ${l}\x07`:`\x1B]2;${l}\x07`}let u=[n,a];return r?.trim()&&u.push(r),t.specificStatus?`\x1B]2;${`${t.specificStatus.status} \xB7 ${u.join(" \xB7 ")}`}\x07`:t.isActive?`\x1B]2;${`\u25CF ${u.join(" \xB7 ")}`}\x07`:`\x1B]2;${u.join(" \xB7 ")}\x07`}le();var bn={name:"vibetunnel",version:"1.0.0-beta.12",description:"Terminal sharing server with web interface - supports macOS, Linux, and headless environments",main:"dist/server/server.js",bin:{vibetunnel:"./bin/vibetunnel"},files:["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"],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:{clean:"node scripts/clean.js",dev:"node scripts/dev.js","dev:server":"tsx watch src/cli.ts --no-auth","dev:client":"node scripts/dev.js --client-only",build:"node scripts/build.js","build:ci":"node scripts/build-ci.js","build:npm":"node scripts/build-npm.js",prepublishOnly:"npm run build:npm",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"',"lint:fix":"biome check src --write","lint:biome":"biome check src",typecheck:'concurrently -n server,client,sw "tsc --noEmit --project tsconfig.server.json" "tsc --noEmit --project tsconfig.client.json" "tsc --noEmit --project tsconfig.sw.json"',pretest:"node scripts/ensure-native-modules.js",test:"vitest","test:ci":"npm run pretest && vitest run --reporter=verbose","test:coverage":"vitest run --coverage","test:client":"vitest run --mode=client","test:server":"vitest run --mode=server","test:client:coverage":"vitest run --mode=client --coverage","test:server:coverage":"vitest run --mode=server --coverage",format:"biome format src --write","format:check":"biome format src",prettier:"prettier --write src --experimental-cli","prettier:check":"prettier --check src --experimental-cli","prettier:fast":"PRETTIER_EXPERIMENTAL_CLI=1 prettier --write src",check:"./scripts/check-all.sh","check:fix":"./scripts/check-fix-sequential.sh",precommit:"pnpm run format && pnpm run lint:fix && pnpm run typecheck","test:e2e":"playwright test","test:e2e:headed":"playwright test --headed","test:e2e:debug":"playwright test --debug","test:e2e:skip-failing":"playwright test --config=playwright.config.skip-failing.ts","test:e2e:ui":"playwright test --ui","test:e2e:report":"playwright show-report","test:e2e:parallel":"PLAYWRIGHT_PARALLEL=true playwright test","test:e2e:parallel:headed":"PLAYWRIGHT_PARALLEL=true playwright test --headed","test:e2e:parallel:workers":"PLAYWRIGHT_PARALLEL=true PLAYWRIGHT_WORKERS=4 playwright test",prepare:"husky"},pnpm:{onlyBuiltDependencies:["authenticate-pam","esbuild","node-pty","puppeteer"]},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-pty":"file:node-pty",postject:"1.0.0-alpha.6","signal-exit":"^4.1.0","web-push":"^3.6.7",ws:"^8.18.3"},devDependencies:{"@biomejs/biome":"^2.1.1","@open-wc/testing":"^4.0.0","@playwright/test":"^1.54.1","@prettier/plugin-oxc":"^0.0.4","@testing-library/dom":"^10.4.0","@types/compression":"^1.8.1","@types/express":"^5.0.3","@types/jsonwebtoken":"^9.0.10","@types/mime-types":"^3.0.1","@types/multer":"^2.0.0","@types/node":"^24.0.13","@types/supertest":"^6.0.3","@types/uuid":"^10.0.0","@types/web-push":"^3.6.4","@types/ws":"^8.18.1","@vitest/coverage-v8":"^3.2.4","@vitest/ui":"^3.2.4",autoprefixer:"^10.4.21",chokidar:"^4.0.3","chokidar-cli":"^3.0.0",concurrently:"^9.2.0",esbuild:"^0.25.6","happy-dom":"^18.0.1",husky:"^9.1.7","lint-staged":"^16.1.2","node-fetch":"^3.3.2",postcss:"^8.5.6",prettier:"^3.6.2",puppeteer:"^24.12.1",supertest:"^7.1.3",tailwindcss:"^3.4.17",tsx:"^4.20.3",typescript:"^5.8.3",uuid:"^11.1.0",vitest:"^3.2.4","ws-mock":"^0.1.0"},keywords:["terminal","multiplexer","websocket","asciinema"],author:"",license:"MIT","lint-staged":{"src/**/*.{ts,tsx,js,jsx}":["biome check --write","bash -c 'tsc --noEmit --project tsconfig.server.json'","bash -c 'tsc --noEmit --project tsconfig.client.json'","bash -c 'tsc --noEmit --project tsconfig.sw.json'"],"../{ios,mac}/**/*.swift":["bash -c 'cd ../ios && ./scripts/lint.sh'","bash -c 'cd ../mac && ./scripts/lint.sh'"]}};L();var st=k("version"),ve=bn.version,At=process.env.BUILD_DATE||new Date().toISOString(),Ni=process.env.BUILD_TIMESTAMP||Date.now(),It=process.env.GIT_COMMIT||"development",Sn=process.version,vn=process.platform,wn=process.arch;function as(){st.debug("gathering version information");let o={version:ve,buildDate:At,buildTimestamp:Ni,gitCommit:It,nodeVersion:Sn,platform:vn,arch:wn,uptime:process.uptime(),pid:process.pid};return st.debug(`version info: ${JSON.stringify(o)}`),o}function $n(){st.log(f.green(`VibeTunnel Server v${ve}`)),st.log(f.gray(`Built: ${At}`)),st.log(f.gray(`Platform: ${vn}/${wn} Node ${Sn}`)),st.log(f.gray(`PID: ${process.pid}`)),It!=="development"&&st.log(f.gray(`Commit: ${It}`)),(It==="development"||!process.env.BUILD_DATE)&&st.log(f.yellow("running in development mode"))}ls();le();var xn=require("child_process"),F=x(require("fs")),kn=x(require("os")),he=x(require("path"));L();var D=k("session-manager"),pt=class o{controlPath;static SESSION_ID_REGEX=/^[a-zA-Z0-9_-]+$/;constructor(e){this.controlPath=e||he.join(kn.homedir(),".vibetunnel","control"),D.debug(`initializing session manager with control path: ${this.controlPath}`),this.ensureControlDirectory()}validateSessionId(e){if(!o.SESSION_ID_REGEX.test(e))throw new j(`Invalid session ID format: "${e}". Session IDs must only contain letters, numbers, hyphens (-), and underscores (_).`,"INVALID_SESSION_ID")}ensureControlDirectory(){F.existsSync(this.controlPath)||(F.mkdirSync(this.controlPath,{recursive:!0}),D.debug(f.green(`control directory created: ${this.controlPath}`)))}getVersionFilePath(){return he.join(this.controlPath,".version")}readLastVersion(){try{let e=this.getVersionFilePath();if(F.existsSync(e)){let t=F.readFileSync(e,"utf8").trim();return D.debug(`read last version from file: ${t}`),t}return null}catch(e){return D.warn(`failed to read version file: ${e}`),null}}writeCurrentVersion(){try{let e=this.getVersionFilePath();F.writeFileSync(e,ve,"utf8"),D.debug(`wrote current version to file: ${ve}`)}catch(e){D.warn(`failed to write version file: ${e}`)}}createSessionDirectory(e){this.validateSessionId(e);let t=he.join(this.controlPath,e);F.existsSync(t)||F.mkdirSync(t,{recursive:!0});let r=this.getSessionPaths(e,!0);if(!r)throw new Error(`Session ${e} not found`);return this.createStdinPipe(r.stdinPath),D.debug(f.green(`session directory created for ${e}`)),r}createStdinPipe(e){try{if(process.platform!=="win32"&&(0,xn.spawnSync)("mkfifo",[e],{stdio:"ignore"}).status===0){D.debug(`FIFO pipe created: ${e}`);return}F.existsSync(e)||F.writeFileSync(e,"")}catch(t){D.debug(`mkfifo failed (${t instanceof Error?t.message:"unknown error"}), creating regular file: ${e}`),F.existsSync(e)||F.writeFileSync(e,"")}}saveSessionInfo(e,t){this.validateSessionId(e);try{let r=he.join(this.controlPath,e),s=he.join(r,"session.json"),n=`${s}.tmp`;F.existsSync(r)||(D.warn(`Session directory ${r} does not exist, creating it`),F.mkdirSync(r,{recursive:!0}));let i=JSON.stringify(t,null,2);if(F.writeFileSync(n,i,"utf8"),!F.existsSync(r))throw D.error(`Session directory ${r} was deleted during save operation`),F.existsSync(n)&&F.unlinkSync(n),new j("Session directory was deleted during save operation","SESSION_DIR_DELETED");F.renameSync(n,s),D.debug(`session.json file saved for session ${e} with name: ${t.name}`)}catch(r){throw r instanceof j?r:new j(`Failed to save session info: ${r instanceof Error?r.message:String(r)}`,"SAVE_SESSION_FAILED")}}loadSessionInfo(e){let t=he.join(this.controlPath,e,"session.json");try{if(!F.existsSync(t))return null;let r=F.readFileSync(t,"utf8");return JSON.parse(r)}catch(r){return D.warn(`failed to load session info for ${e}:`,r),null}}updateSessionStatus(e,t,r,s){let n=this.loadSessionInfo(e);if(!n)throw new j("Session info not found","SESSION_NOT_FOUND");r!==void 0&&(n.pid=r),n.status=t,s!==void 0&&(n.exitCode=s),this.saveSessionInfo(e,n),D.debug(`session ${e} status updated to ${t}${r?` (pid: ${r})`:""}${s!==void 0?` (exit code: ${s})`:""}`)}ensureUniqueName(e,t){let r=this.listSessions(),s=e,n=2;for(;r.some(a=>a.name===s&&a.id!==t);)s=`${e} (${n})`,n++;return s}updateSessionName(e,t){D.debug(`[SessionManager] updateSessionName called for session ${e} with name: ${t}`);let r=this.loadSessionInfo(e);if(!r)throw D.error(`[SessionManager] Session info not found for ${e}`),new j("Session info not found","SESSION_NOT_FOUND");D.debug(`[SessionManager] Current session info: ${JSON.stringify(r)}`);let s=this.ensureUniqueName(t,e);return s!==t&&D.debug(`[SessionManager] Name "${t}" already exists, using "${s}" instead`),r.name=s,D.debug(`[SessionManager] Updated session info: ${JSON.stringify(r)}`),D.debug("[SessionManager] Calling saveSessionInfo"),this.saveSessionInfo(e,r),D.debug(`[SessionManager] session ${e} name updated to: ${s}`),s}listSessions(){try{if(!F.existsSync(this.controlPath))return[];let e=[],t=F.readdirSync(this.controlPath,{withFileTypes:!0});for(let r of t)if(r.isDirectory()){let s=r.name,n=he.join(this.controlPath,s),i=he.join(n,"stdout"),a=this.loadSessionInfo(s);if(a)if(a.status==="running"&&a.pid&&(Ae.isProcessRunning(a.pid)||(D.debug(f.yellow(`process ${a.pid} no longer running for session ${s}`)),a.status="exited",a.exitCode===void 0&&(a.exitCode=1),this.saveSessionInfo(s,a))),F.existsSync(i)){let u=F.statSync(i).mtime.toISOString();e.push({...a,id:s,lastModified:u})}else e.push({...a,id:s,lastModified:a.startedAt})}return e.sort((r,s)=>{let n=r.startedAt?new Date(r.startedAt).getTime():0;return(s.startedAt?new Date(s.startedAt).getTime():0)-n}),D.debug(`listSessions found ${e.length} sessions`),e.forEach(r=>{D.debug(` - Session ${r.id}: name="${r.name}", status="${r.status}"`)}),e}catch(e){throw new j(`Failed to list sessions: ${e instanceof Error?e.message:String(e)}`,"LIST_SESSIONS_FAILED")}}sessionExists(e){let t=he.join(this.controlPath,e),r=he.join(t,"session.json");return F.existsSync(r)}cleanupSession(e){if(!e)throw new j("Session ID is required for cleanup","INVALID_SESSION_ID");try{let t=he.join(this.controlPath,e);if(F.existsSync(t)){D.debug(`Cleaning up session directory: ${t}`);let r=this.loadSessionInfo(e);r&&D.debug(`Cleaning up session ${e} with status: ${r.status}`),F.rmSync(t,{recursive:!0,force:!0}),D.debug(f.green(`session ${e} cleaned up`))}else D.debug(`Session directory ${t} does not exist, nothing to clean up`)}catch(t){throw new j(`Failed to cleanup session ${e}: ${t instanceof Error?t.message:String(t)}`,"CLEANUP_FAILED",e)}}cleanupExitedSessions(){let e=[];try{let t=this.listSessions();for(let r of t)r.status==="exited"&&r.id&&(this.cleanupSession(r.id),e.push(r.id));return e.length>0&&D.debug(f.green(`cleaned up ${e.length} exited sessions`)),e}catch(t){throw new j(`Failed to cleanup exited sessions: ${t instanceof Error?t.message:String(t)}`,"CLEANUP_EXITED_FAILED")}}cleanupOldVersionSessions(){let e=this.readLastVersion(),t=ve;if(!e){D.debug("no previous version found, checking for legacy sessions"),this.updateZombieSessions();let s=0,n=this.listSessions();for(let i of n)i.version||(i.status==="exited"||i.pid&&!Ae.isProcessRunning(i.pid)?(D.debug(`cleaning up legacy zombie session ${i.id} (no version field)`),this.cleanupSession(i.id),s++):D.debug(`preserving active legacy session ${i.id}`));return this.writeCurrentVersion(),{versionChanged:!1,cleanedCount:s}}if(e===t)return D.debug(`version unchanged (${t}), skipping cleanup`),{versionChanged:!1,cleanedCount:0};D.log(f.yellow(`VibeTunnel version changed from ${e} to ${t}`)),D.log(f.yellow("cleaning up zombie sessions from old version...")),this.updateZombieSessions();let r=0;try{let s=this.listSessions();for(let n of s)(!n.version||n.version!==t)&&(n.status==="exited"||n.pid&&!Ae.isProcessRunning(n.pid)?(D.debug(`cleaning up zombie session ${n.id} (version: ${n.version||"unknown"})`),this.cleanupSession(n.id),r++):D.debug(`preserving active session ${n.id} (version: ${n.version||"unknown"})`));return this.writeCurrentVersion(),r>0?D.log(f.green(`cleaned up ${r} zombie sessions from previous version`)):D.log(f.gray("no zombie sessions to clean up (active sessions preserved)")),{versionChanged:!0,cleanedCount:r}}catch(s){return D.error(`failed to cleanup old version sessions: ${s}`),this.writeCurrentVersion(),{versionChanged:!0,cleanedCount:r}}}getSessionPaths(e,t=!1){let r=he.join(this.controlPath,e);return D.debug(`[SessionManager] getSessionPaths for ${e}, sessionDir: ${r}, checkExists: ${t}`),t&&!F.existsSync(r)?(D.debug(`[SessionManager] Session directory does not exist: ${r}`),null):{controlDir:r,stdoutPath:he.join(r,"stdout"),stdinPath:he.join(r,"stdin"),sessionJsonPath:he.join(r,"session.json")}}writeToStdin(e,t){let r=this.getSessionPaths(e);if(!r)throw new j(`Session ${e} not found`,"SESSION_NOT_FOUND",e);try{F.appendFileSync(r.stdinPath,t),D.debug(`wrote ${t.length} bytes to stdin for session ${e}`)}catch(s){throw new j(`Failed to write to stdin for session ${e}: ${s instanceof Error?s.message:String(s)}`,"STDIN_WRITE_FAILED",e)}}updateZombieSessions(){let e=[];try{let t=this.listSessions();for(let r of t)r.status==="running"&&r.pid&&(Ae.isProcessRunning(r.pid)||this.getSessionPaths(r.id)&&(D.debug(f.yellow(`marking zombie process ${r.pid} as exited for session ${r.id}`)),this.updateSessionStatus(r.id,"exited",void 0,1),e.push(r.id)));return e}catch(t){return D.warn("failed to update zombie sessions:",t),[]}}getControlPath(){return this.controlPath}};var nt=require("buffer");function Re(o,e){let t=nt.Buffer.isBuffer(e)?e:nt.Buffer.from(typeof e=="string"?e:JSON.stringify(e),"utf8"),r=nt.Buffer.allocUnsafe(5+t.length);return r[0]=o,r.writeUInt32BE(t.length,1),t.copy(r,5),r}var Nt=class{buffer=nt.Buffer.alloc(0);addData(e){this.buffer=nt.Buffer.concat([this.buffer,e])}*parseMessages(){for(;this.buffer.length>=5;){let e=this.buffer[0],t=this.buffer.readUInt32BE(1);if(this.buffer.length<5+t)break;let r=this.buffer.subarray(5,5+t);this.buffer=this.buffer.subarray(5+t),yield{type:e,payload:r}}}get pendingBytes(){return this.buffer.length}clear(){this.buffer=nt.Buffer.alloc(0)}},ot={stdin(o){return Re(1,o)},resize(o,e){return Re(2,{cmd:"resize",cols:o,rows:e})},kill(o){return Re(2,{cmd:"kill",signal:o})},resetSize(){return Re(2,{cmd:"reset-size"})},updateTitle(o){return Re(2,{cmd:"update-title",title:o})},status(o,e,t){return Re(3,{app:o,status:e,...t})},heartbeat(){return Re(4,nt.Buffer.alloc(0))},error(o,e,t){return Re(5,{code:o,message:e,details:t})}};function hr(o,e){switch(o){case 1:return e.toString("utf8");case 2:case 3:case 5:return JSON.parse(e.toString("utf8"));case 4:return null;default:return e}}var y=k("pty-manager"),ji=1e3,Li=50,Fi=10,gt=class extends Kt.EventEmitter{sessions=new Map;sessionManager;defaultTerm="xterm-256color";inputSocketClients=new Map;lastTerminalSize=null;resizeEventListeners=[];sessionResizeSources=new Map;sessionEventListeners=new Map;lastBellTime=new Map;sessionExitTimes=new Map;processTreeAnalyzer=new dt;activityFileWarningsLogged=new Set;lastWrittenActivityState=new Map;constructor(e){super(),this.sessionManager=new pt(e),this.setupTerminalResizeDetection()}setupTerminalResizeDetection(){if(!process.stdout.isTTY){y.debug("Not a TTY, skipping terminal resize detection");return}this.lastTerminalSize={cols:process.stdout.columns||80,rows:process.stdout.rows||24};let e=()=>{let r=process.stdout.columns||80,s=process.stdout.rows||24;this.handleTerminalResize(r,s)};process.stdout.on("resize",e),this.resizeEventListeners.push(()=>{process.stdout.removeListener("resize",e)});let t=()=>{let r=process.stdout.columns||80,s=process.stdout.rows||24;this.handleTerminalResize(r,s)};process.on("SIGWINCH",t),this.resizeEventListeners.push(()=>{process.removeListener("SIGWINCH",t)})}handleTerminalResize(e,t){if(this.lastTerminalSize&&this.lastTerminalSize.cols===e&&this.lastTerminalSize.rows===t)return;y.log(f.blue(`Terminal resized to ${e}x${t}`)),this.lastTerminalSize={cols:e,rows:t};let r=Date.now();for(let[s,n]of this.sessions)if(n.ptyProcess&&n.sessionInfo.status==="running"){let i=this.sessionResizeSources.get(s);if(!i||i.source==="terminal"||r-i.timestamp>1e3)try{n.ptyProcess.resize(e,t),n.asciinemaWriter?.writeResize(e,t),this.sessionResizeSources.set(s,{cols:e,rows:t,source:"terminal",timestamp:r}),y.debug(`Resized session ${s} to ${e}x${t} from terminal`)}catch(u){y.error(`Failed to resize session ${s}:`,u)}else y.debug(`Skipping terminal resize for session ${s} (browser has precedence)`)}}async createSession(e,t){let r=t.sessionId||Ue(),s=t.name||_e.basename(e[0]),n=t.workingDir||process.cwd(),i=this.defaultTerm,a=t.cols,u=t.rows;y.debug("Session creation parameters:",{sessionId:r,sessionName:s,workingDir:n,term:i,cols:a!==void 0?a:"terminal default",rows:u!==void 0?u:"terminal default"});try{let c=this.sessionManager.createSessionDirectory(r),l=Ae.resolveCommand(e),{command:d,args:g}=l,p=[d,...g];l.resolvedFrom==="alias"?y.log(f.cyan(`Using alias: '${l.originalCommand}' \u2192 '${p.join(" ")}'`)):l.resolvedFrom==="path"&&l.originalCommand?y.log(f.gray(`Resolved '${l.originalCommand}' \u2192 '${d}'`)):l.useShell&&y.debug(`Using shell to execute ${l.resolvedFrom}: ${e.join(" ")}`),y.debug(f.blue(`Creating PTY session with command: ${p.join(" ")}`)),y.debug(`Working directory: ${n}`);let h={id:r,command:p,name:s,workingDir:n,status:"starting",startedAt:new Date().toISOString(),initialCols:a,initialRows:u,lastClearOffset:0,version:ve};this.sessionManager.saveSessionInfo(r,h);let b=Ht.create(c.stdoutPath,a||void 0,u||void 0,e.join(" "),s,this.createEnvVars(i)),v;try{let w={...process.env,TERM:i,VIBETUNNEL_SESSION_ID:r};y.debug("PTY spawn parameters:",{command:d,args:g,options:{name:i,cols:a!==void 0?a:"terminal default",rows:u!==void 0?u:"terminal default",cwd:n,hasEnv:!!w,envKeys:Object.keys(w).length}});let P={name:i,cwd:n,env:w};a!==void 0&&(P.cols=a),u!==void 0&&(P.rows=u),v=Pn.spawn(d,g,P)}catch(w){y.debug("Raw spawn error:",{type:typeof w,isError:w instanceof Error,errorString:String(w),errorKeys:w&&typeof w=="object"?Object.keys(w):[]});let P=w instanceof Error?w.message:String(w),V=w instanceof Error&&"code"in w?w.code:void 0;V==="ENOENT"||P.includes("ENOENT")?P=`Command not found: '${e[0]}'. Please ensure the command exists and is in your PATH.`:V==="EACCES"||P.includes("EACCES")?P=`Permission denied: '${e[0]}'. The command exists but is not executable.`:V==="ENXIO"||P.includes("ENXIO")?P=`Failed to allocate terminal for '${e[0]}'. This may occur if the command doesn't exist or the system cannot create a pseudo-terminal.`:(P.includes("cwd")||P.includes("working directory"))&&(P=`Working directory does not exist: '${n}'`);let _=w instanceof Error?{...w,message:w.message,stack:w.stack,code:w.code}:w;throw y.error(`Failed to spawn PTY for command '${e.join(" ")}':`,_),new j(P,"SPAWN_FAILED")}let $=t.titleMode;$||e.some(P=>P.toLowerCase().includes("claude"))&&($="dynamic",y.log(f.cyan("\u2713 Auto-selected dynamic title mode for Claude")),y.debug(`Detected Claude in command: ${e.join(" ")}`));let E={id:r,sessionInfo:h,ptyProcess:v,asciinemaWriter:b,controlDir:c.controlDir,stdoutPath:c.stdoutPath,stdinPath:c.stdinPath,sessionJsonPath:c.sessionJsonPath,startTime:new Date,titleMode:$||"none",isExternalTerminal:!!t.forwardToStdout,currentWorkingDir:n,titleFilter:new gr};return this.sessions.set(r,E),h.pid=v.pid,h.status="running",this.sessionManager.saveSessionInfo(r,h),t.forwardToStdout&&this.setupSessionWatcher(E),y.debug(f.green(`Session ${r} created successfully (PID: ${v.pid})`)),y.log(f.gray(`Running: ${p.join(" ")} in ${n}`)),this.setupPtyHandlers(E,t.forwardToStdout||!1,t.onExit),{sessionId:r,sessionInfo:h}}catch(c){try{this.sessionManager.cleanupSession(r)}catch(l){y.warn(`Failed to cleanup session ${r} after creation failure:`,l)}throw new j(`Failed to create session: ${c instanceof Error?c.message:String(c)}`,"SESSION_CREATE_FAILED")}}getPtyForSession(e){return this.sessions.get(e)?.ptyProcess||null}getInternalSession(e){return this.sessions.get(e)}setupPtyHandlers(e,t,r){let{ptyProcess:s,asciinemaWriter:n}=e;if(!s){y.error(`No PTY process found for session ${e.id}`);return}let i=t?new lt:null;i&&(e.stdoutQueue=i);let a=new lt;if(e.inputQueue=a,e.titleMode==="dynamic"&&(e.activityDetector=new Pt(e.sessionInfo.command)),e.titleMode!=="none"&&e.titleMode!=="filter"&&t){let u=null;e.titleUpdateInterval=setInterval(()=>{if(e.titleMode==="dynamic"&&e.activityDetector){let c=e.activityDetector.getActivityState();(u===null||c.isActive!==u.isActive||c.specificStatus?.status!==u.specificStatus)&&(u={isActive:c.isActive,specificStatus:c.specificStatus?.status},this.markTitleUpdateNeeded(e),y.debug(`Activity state changed for session ${e.id}: active=${c.isActive}, status=${c.specificStatus?.status||"none"}`)),this.writeActivityState(e,c)}this.checkAndUpdateTitle(e)},ji)}s.onData(u=>{let c=u;if(e.titleMode!==void 0&&e.titleMode!=="none"&&(c=e.titleFilter?e.titleFilter.filter(u):u),e.titleMode==="dynamic"&&e.activityDetector){let{filteredData:l,activity:d}=e.activityDetector.processOutput(c);c=l,d.specificStatus?.status!==e.lastActivityStatus&&(e.lastActivityStatus=d.specificStatus?.status,this.markTitleUpdateNeeded(e))}e.titleMode==="static"&&t&&(!e.initialTitleSent||mn(c))&&(this.markTitleUpdateNeeded(e),e.initialTitleSent||(e.initialTitleSent=!0)),n?.writeOutput(Buffer.from(c,"utf8")),t&&i&&i.enqueue(async()=>{let l=process.stdout.write(c);e.lastWriteTimestamp=Date.now(),l||await(0,Kt.once)(process.stdout,"drain")})}),s.onExit(async({exitCode:u,signal:c})=>{try{if(this.sessionExitTimes.set(e.id,Date.now()),n?.isOpen()&&(n.writeRawJson(["exit",u||0,e.id]),n.close().catch(l=>y.error(`Failed to close asciinema writer for session ${e.id}:`,l))),this.sessionManager.updateSessionStatus(e.id,"exited",void 0,u||(c?128+(typeof c=="number"?c:1):1)),e.stdoutQueue)try{await e.stdoutQueue.drain()}catch(l){y.error(`Failed to drain stdout queue for session ${e.id}:`,l)}this.cleanupSessionResources(e),this.sessions.delete(e.id),this.lastBellTime.delete(e.id),this.sessionExitTimes.delete(e.id),this.emit("sessionExited",e.id),r&&r(u||0,c)}catch(l){y.error(`Failed to handle exit for session ${e.id}:`,l)}}),t&&(e.titleMode==="static"||e.titleMode==="dynamic")&&(this.markTitleUpdateNeeded(e),e.initialTitleSent=!0,y.debug(`Marked initial title update for session ${e.id}`)),this.setupIPCSocket(e)}setupIPCSocket(e){if(!e.ptyProcess){y.error(`No PTY process found for session ${e.id}`);return}let r=_e.join(e.controlDir,"ipc.sock");if(r.length>103){let s=new Error(`Socket path too long: ${r.length} characters`);throw y.error(`Socket path too long (${r.length} chars): ${r}`),y.error("macOS limit is 103 characters. Consider using shorter session IDs or control paths."),s}try{try{we.unlinkSync(r)}catch{}e.connectedClients||(e.connectedClients=new Set);let s=Gt.createServer(n=>{let i=new Nt;n.setNoDelay(!0),e.connectedClients?.add(n),y.debug(`Client connected to session ${e.id}, total clients: ${e.connectedClients?.size}`),n.on("data",a=>{i.addData(a);for(let{type:u,payload:c}of i.parseMessages())this.handleSocketMessage(e,u,c)}),n.on("error",a=>{y.debug(`Client socket error for session ${e.id}:`,a)}),n.on("close",()=>{e.connectedClients?.delete(n),y.debug(`Client disconnected from session ${e.id}, remaining clients: ${e.connectedClients?.size}`)})});s.listen(r,()=>{try{we.chmodSync(r,438)}catch(n){y.debug(`Failed to chmod input socket for session ${e.id}:`,n)}y.debug(`Input socket created for session ${e.id}`)}),e.inputSocketServer=s}catch(s){y.error(`Failed to create input socket for session ${e.id}:`,s)}}setupSessionWatcher(e){let t=_e.join(e.controlDir,"session.json");try{let r=setInterval(()=>{try{let s=this.sessionManager.loadSessionInfo(e.id);if(s&&s.name!==e.sessionInfo.name){let n=e.sessionInfo.name;e.sessionInfo.name=s.name,y.debug(`Session ${e.id} name changed from "${n}" to "${s.name}"`),this.trackAndEmit("sessionNameChanged",e.id,s.name),e.isExternalTerminal&&(e.titleMode==="static"||e.titleMode==="dynamic")&&this.markTitleUpdateNeeded(e)}}catch(s){y.debug(`Failed to read session file for ${e.id}:`,s)}},100);e.sessionJsonInterval=r,y.debug(`Session watcher setup for ${e.id}`)}catch(r){y.error(`Failed to setup session watcher for ${e.id}:`,r)}}handleSocketMessage(e,t,r){try{let s=hr(t,r);switch(t){case 1:{let n=s;e.ptyProcess&&e.inputQueue&&e.inputQueue.enqueue(()=>{e.ptyProcess&&e.ptyProcess.write(n),e.asciinemaWriter?.writeInput(n)});break}case 2:{let n=s;this.handleControlMessage(e,n);break}case 3:{let n=s;if(e.activityStatus||(e.activityStatus={}),e.activityStatus.specificStatus={app:n.app,status:n.status},y.debug(`Updated status for session ${e.id}:`,n),e.connectedClients&&e.connectedClients.size>0){let i=Re(3,n);for(let a of e.connectedClients)try{a.write(i)}catch(u){y.debug("Failed to broadcast status to client:",u)}y.debug(`Broadcasted status update to ${e.connectedClients.size} clients`)}break}case 4:break;default:y.debug(`Unknown message type ${t} for session ${e.id}`)}}catch(s){y.error(`Failed to handle socket message for session ${e.id}:`,s)}}handleControlMessage(e,t){if(t.cmd==="resize"&&typeof t.cols=="number"&&typeof t.rows=="number")try{e.ptyProcess&&(e.ptyProcess.resize(t.cols,t.rows),e.asciinemaWriter?.writeResize(t.cols,t.rows))}catch(r){y.warn(`Failed to resize session ${e.id} to ${t.cols}x${t.rows}:`,r)}else if(t.cmd==="kill"){let r=typeof t.signal=="string"||typeof t.signal=="number"?t.signal:"SIGTERM";try{e.ptyProcess&&e.ptyProcess.kill(r)}catch(s){y.warn(`Failed to kill session ${e.id} with signal ${r}:`,s)}}else if(t.cmd==="reset-size")try{if(e.ptyProcess){let r=process.stdout.columns||80,s=process.stdout.rows||24;e.ptyProcess.resize(r,s),e.asciinemaWriter?.writeResize(r,s),y.debug(`Reset session ${e.id} size to terminal size: ${r}x${s}`)}}catch(r){y.warn(`Failed to reset session ${e.id} size to terminal size:`,r)}else t.cmd==="update-title"&&typeof t.title=="string"&&(y.debug(`[IPC] Received title update for session ${e.id}: "${t.title}"`),y.debug(`[IPC] Current session name before update: "${e.sessionInfo.name}"`),this.updateSessionName(e.id,t.title))}async getFishCompletions(e,t){try{let r=this.sessions.get(e);if(!r)return[];let s=Ae.getUserShell();if(!Rt.isFishShell(s))return[];let{fishHandler:n}=await Promise.resolve().then(()=>(ls(),En)),i=r.currentWorkingDir||process.cwd();return await n.getCompletions(t,i)}catch(r){return y.warn(`Fish completions failed: ${r}`),[]}}sendInput(e,t){try{let r="";if(t.text!==void 0)r=t.text,y.debug(`Received text input: ${JSON.stringify(t.text)} -> sending: ${JSON.stringify(r)}`);else if(t.key!==void 0)r=this.convertSpecialKey(t.key),y.debug(`Received special key: "${t.key}" -> converted to: ${JSON.stringify(r)}`);else throw new j("No text or key specified in input","INVALID_INPUT");let s=this.sessions.get(e);if(s?.ptyProcess&&s.inputQueue){s.inputQueue.enqueue(()=>{if(s.ptyProcess&&s.ptyProcess.write(r),s.asciinemaWriter?.writeInput(r),(s.titleMode==="static"||s.titleMode==="dynamic")&&t.text){let n=hn(t.text,s.currentWorkingDir||s.sessionInfo.workingDir);n&&(s.currentWorkingDir=n,this.markTitleUpdateNeeded(s),y.debug(`Session ${e} changed directory to: ${n}`))}});return}else{let n=this.sessionManager.getSessionPaths(e);if(!n)throw new j(`Session ${e} paths not found`,"SESSION_PATHS_NOT_FOUND",e);let i=_e.join(n.controlDir,"ipc.sock"),a=this.inputSocketClients.get(e);if(!a||a.destroyed)try{a=Gt.createConnection(i),a.setNoDelay(!0),a.setKeepAlive(!0,0),this.inputSocketClients.set(e,a),a.on("error",()=>{this.inputSocketClients.delete(e)}),a.on("close",()=>{this.inputSocketClients.delete(e)})}catch(u){y.debug(`Failed to connect to input socket for session ${e}:`,u),a=void 0}if(a&&!a.destroyed){let u=Re(1,r);a.write(u)||y.debug(`Socket buffer full for session ${e}, data queued`)}else throw new j(`No socket connection available for session ${e}`,"NO_SOCKET_CONNECTION",e)}}catch(r){throw new j(`Failed to send input to session ${e}: ${r instanceof Error?r.message:String(r)}`,"SEND_INPUT_FAILED",e)}}sendControlMessage(e,t){let r=this.sessionManager.getSessionPaths(e);if(!r)return!1;try{let s=_e.join(r.controlDir,"ipc.sock"),n=this.inputSocketClients.get(e);if(!n||n.destroyed)try{n=Gt.createConnection(s),n.setNoDelay(!0),n.setKeepAlive(!0,0),this.inputSocketClients.set(e,n),n.on("error",()=>{this.inputSocketClients.delete(e)}),n.on("close",()=>{this.inputSocketClients.delete(e)})}catch(i){return y.debug(`Failed to connect to control socket for session ${e}:`,i),!1}if(n&&!n.destroyed){let i=Re(2,t);return n.write(i)}}catch(s){y.error(`Failed to send control message to session ${e}:`,s)}return!1}convertSpecialKey(e){let r={arrow_up:"\x1B[A",arrow_down:"\x1B[B",arrow_right:"\x1B[C",arrow_left:"\x1B[D",escape:"\x1B",enter:"\r",ctrl_enter:` +`,shift_enter:`\r +`,backspace:"\x7F",tab:" ",shift_tab:"\x1B[Z",page_up:"\x1B[5~",page_down:"\x1B[6~",home:"\x1B[H",end:"\x1B[F",delete:"\x1B[3~",f1:"\x1BOP",f2:"\x1BOQ",f3:"\x1BOR",f4:"\x1BOS",f5:"\x1B[15~",f6:"\x1B[17~",f7:"\x1B[18~",f8:"\x1B[19~",f9:"\x1B[20~",f10:"\x1B[21~",f11:"\x1B[23~",f12:"\x1B[24~"}[e];if(!r)throw new j(`Unknown special key: ${e}`,"UNKNOWN_KEY");return r}resizeSession(e,t,r){let s=this.sessions.get(e),n=Date.now();try{if(s?.ptyProcess)s.ptyProcess.resize(t,r),s.asciinemaWriter?.writeResize(t,r),this.sessionResizeSources.set(e,{cols:t,rows:r,source:"browser",timestamp:n}),y.debug(`Resized session ${e} to ${t}x${r} from browser`);else{let i={cmd:"resize",cols:t,rows:r};this.sendControlMessage(e,i),this.sessionResizeSources.set(e,{cols:t,rows:r,source:"browser",timestamp:n})}}catch(i){throw new j(`Failed to resize session ${e}: ${i instanceof Error?i.message:String(i)}`,"RESIZE_FAILED",e)}}updateSessionName(e,t){y.debug(`[PtyManager] updateSessionName called for session ${e} with name: ${t}`),y.debug("[PtyManager] Calling sessionManager.updateSessionName");let r=this.sessionManager.updateSessionName(e,t),s=this.sessions.get(e);if(s?.sessionInfo){y.debug("[PtyManager] Found in-memory session, updating...");let n=s.sessionInfo.name;s.sessionInfo.name=r,y.debug("[PtyManager] Session info after update:",{sessionId:s.id,newName:s.sessionInfo.name,oldCurrentTitle:`${s.currentTitle?.substring(0,50)}...`}),s.isExternalTerminal&&s.stdoutQueue&&(y.debug(`[PtyManager] Forcing immediate title update for session ${e}`,{titleMode:s.titleMode,hadCurrentTitle:!!s.currentTitle,titleUpdateNeeded:s.titleUpdateNeeded}),s.currentTitle=void 0,this.updateTerminalTitleForSessionName(s)),y.log(`[PtyManager] Updated session ${e} name from "${n}" to "${r}"`)}else y.debug(`[PtyManager] No in-memory session found for ${e}`,{sessionsMapSize:this.sessions.size,sessionIds:Array.from(this.sessions.keys())});return this.trackAndEmit("sessionNameChanged",e,r),y.debug(`[PtyManager] Updated session ${e} name to: ${r}`),r}resetSessionSize(e){let t=this.sessions.get(e);try{if(t?.ptyProcess)throw new j(`Cannot reset size for in-memory session ${e}`,"INVALID_OPERATION",e);let r={cmd:"reset-size"};if(!this.sendControlMessage(e,r))throw new j(`Failed to send reset-size command to session ${e}`,"CONTROL_MESSAGE_FAILED",e);y.debug(`Sent reset-size command to session ${e}`)}catch(r){throw new j(`Failed to reset session size for ${e}: ${r instanceof Error?r.message:String(r)}`,"RESET_SIZE_FAILED",e)}}async killSession(e,t="SIGTERM"){let r=this.sessions.get(e);try{if(r?.ptyProcess){if(t==="SIGKILL"||t===9){let s=r.ptyProcess.pid;if(r.ptyProcess.kill("SIGKILL"),process.platform!=="win32"&&s)try{process.kill(-s,"SIGKILL"),y.debug(`Sent SIGKILL to process group -${s} for session ${e}`)}catch(n){y.debug(`Failed to SIGKILL process group for session ${e}:`,n)}this.sessions.delete(e),await new Promise(n=>setTimeout(n,100));return}await this.killSessionWithEscalation(e,r)}else{let s={cmd:"kill",signal:t};this.sendControlMessage(e,s)&&await new Promise(a=>setTimeout(a,500));let i=this.sessionManager.loadSessionInfo(e);if(!i)throw new j(`Session ${e} not found`,"SESSION_NOT_FOUND",e);if(i.pid&&Ae.isProcessRunning(i.pid)){if(y.log(f.yellow(`Killing external session ${e} (PID: ${i.pid})`)),t==="SIGKILL"||t===9){if(process.kill(i.pid,"SIGKILL"),process.platform!=="win32")try{process.kill(-i.pid,"SIGKILL"),y.debug(`Sent SIGKILL to process group -${i.pid} for external session ${e}`)}catch(l){y.debug(`Failed to SIGKILL process group for external session ${e}:`,l)}await new Promise(l=>setTimeout(l,100));return}if(process.kill(i.pid,"SIGTERM"),process.platform!=="win32")try{process.kill(-i.pid,"SIGTERM"),y.debug(`Sent SIGTERM to process group -${i.pid} for external session ${e}`)}catch(l){y.debug(`Failed to kill process group for external session ${e}:`,l)}let a=3e3,u=500,c=a/u;for(let l=0;lsetTimeout(d,u)),!Ae.isProcessRunning(i.pid)){y.debug(f.green(`External session ${e} terminated gracefully`));return}if(y.debug(f.yellow(`External session ${e} requires SIGKILL`)),process.kill(i.pid,"SIGKILL"),process.platform!=="win32")try{process.kill(-i.pid,"SIGKILL"),y.debug(`Sent SIGKILL to process group -${i.pid} for external session ${e}`)}catch(l){y.debug(`Failed to SIGKILL process group for external session ${e}:`,l)}await new Promise(l=>setTimeout(l,100))}}}catch(s){throw new j(`Failed to kill session ${e}: ${s instanceof Error?s.message:String(s)}`,"KILL_FAILED",e)}}async killSessionWithEscalation(e,t){if(!t.ptyProcess){this.sessions.delete(e);return}let r=t.ptyProcess.pid;y.debug(f.yellow(`Terminating session ${e} (PID: ${r})`));try{if(t.ptyProcess.kill("SIGTERM"),process.platform!=="win32"&&r)try{process.kill(-r,"SIGTERM"),y.debug(`Sent SIGTERM to process group -${r} for session ${e}`)}catch(a){y.debug(`Failed to kill process group for session ${e}:`,a)}let s=3e3,n=500,i=s/n;for(let a=0;asetTimeout(u,n)),!Ae.isProcessRunning(r)){y.debug(f.green(`Session ${e} terminated gracefully`)),this.sessions.delete(e);return}y.debug(`Session ${e} still running after ${(a+1)*n}ms`)}y.debug(f.yellow(`Session ${e} requires SIGKILL`));try{if(t.ptyProcess.kill("SIGKILL"),process.platform!=="win32"&&r)try{process.kill(-r,"SIGKILL"),y.debug(`Sent SIGKILL to process group -${r} for session ${e}`)}catch(a){y.debug(`Failed to SIGKILL process group for session ${e}:`,a)}await new Promise(a=>setTimeout(a,100))}catch{y.debug(`SIGKILL failed for session ${e} (process already terminated)`)}this.sessions.delete(e),y.debug(f.yellow(`Session ${e} forcefully terminated`))}catch(s){throw this.sessions.delete(e),new j(`Failed to terminate session ${e}: ${s instanceof Error?s.message:String(s)}`,"KILL_FAILED",e)}}listSessions(){let e=this.sessionManager.updateZombieSessions();for(let r of e){let s=this.inputSocketClients.get(r);s&&(s.destroy(),this.inputSocketClients.delete(r))}return this.sessionManager.listSessions().map(r=>{let s=this.sessions.get(r.id);if(s?.activityStatus)return{...r,activityStatus:s.activityStatus};if(s?.activityDetector){let n=s.activityDetector.getActivityState();return{...r,activityStatus:{isActive:n.isActive,specificStatus:n.specificStatus}}}try{let n=this.sessionManager.getSessionPaths(r.id);if(!n)return r;let i=_e.join(n.controlDir,"claude-activity.json");if(we.existsSync(i)){let a=JSON.parse(we.readFileSync(i,"utf-8")),u=Math.abs(Date.now()-new Date(a.timestamp).getTime());if(u<6e4)return y.debug(`Found recent activity for external session ${r.id}:`,{isActive:a.isActive,specificStatus:a.specificStatus}),{...r,activityStatus:{isActive:a.isActive,specificStatus:a.specificStatus}};y.debug(`Activity file for session ${r.id} is stale (time diff: ${u}ms)`)}else this.activityFileWarningsLogged.has(r.id)||(this.activityFileWarningsLogged.add(r.id),y.debug(`No claude-activity.json found for session ${r.id} at ${i}`))}catch(n){y.debug(`Failed to read activity file for session ${r.id}:`,n)}return r})}getSession(e){y.debug(`[PtyManager] getSession called for sessionId: ${e}`);let t=this.sessionManager.getSessionPaths(e,!0);if(!t)return y.debug(`[PtyManager] No session paths found for ${e}`),null;let r=this.sessionManager.loadSessionInfo(e);if(!r)return y.debug(`[PtyManager] No session info found for ${e}`),null;let s={...r,id:e,lastModified:r.startedAt};if(we.existsSync(t.stdoutPath)){let n=we.statSync(t.stdoutPath).mtime.toISOString();s.lastModified=n}return y.debug(`[PtyManager] Found session: ${JSON.stringify(s)}`),s}getSessionPaths(e){return this.sessionManager.getSessionPaths(e)}cleanupSession(e){this.sessions.has(e)&&this.killSession(e).catch(r=>{y.error(`Failed to kill session ${e} during cleanup:`,r)}),this.sessionManager.cleanupSession(e);let t=this.inputSocketClients.get(e);t&&(t.destroy(),this.inputSocketClients.delete(e))}cleanupExitedSessions(){return this.sessionManager.cleanupExitedSessions()}createEnvVars(e){let t={TERM:e},r=["SHELL","LANG","LC_ALL","PATH","USER","HOME"];for(let s of r){let n=process.env[s];n&&(t[s]=n)}return t}getActiveSessionCount(){return this.sessions.size}isSessionActive(e){return this.sessions.has(e)}async captureProcessInfoForBell(e,t){try{let r=e.ptyProcess?.pid;if(!r){y.warn(`Cannot capture process info for session ${e.id}: no PID available`),this.emit("bell",{sessionInfo:e.sessionInfo,timestamp:new Date,bellCount:t});return}y.log(`Capturing process snapshot for bell in session ${e.id} (PID: ${r})`);let s=await this.processTreeAnalyzer.captureProcessSnapshot(r);this.emit("bell",{sessionInfo:e.sessionInfo,timestamp:new Date,bellCount:t,processSnapshot:s,suspectedSource:s.suspectedBellSource}),y.log(`Bell event emitted for session ${e.id} with suspected source: ${s.suspectedBellSource?.command||"unknown"} (PID: ${s.suspectedBellSource?.pid||"unknown"})`)}catch(r){y.warn(`Failed to capture process info for bell in session ${e.id}:`,r),this.emit("bell",{sessionInfo:e.sessionInfo,timestamp:new Date,bellCount:t})}}async shutdown(){for(let[e,t]of Array.from(this.sessions.entries()))try{if(t.ptyProcess){let r=t.ptyProcess.pid;if(t.ptyProcess.kill(),process.platform!=="win32"&&r)try{process.kill(-r,"SIGTERM"),y.debug(`Sent SIGTERM to process group -${r} during shutdown`)}catch(s){y.debug("Failed to kill process group during shutdown:",s)}}t.asciinemaWriter?.isOpen()&&await t.asciinemaWriter.close(),this.cleanupSessionResources(t)}catch(r){y.error(`Failed to cleanup session ${e} during shutdown:`,r)}this.sessions.clear();for(let[e,t]of this.inputSocketClients.entries())try{t.destroy()}catch{}this.inputSocketClients.clear();for(let e of this.resizeEventListeners)try{e()}catch(t){y.error("Failed to remove resize event listener:",t)}this.resizeEventListeners.length=0}getSessionManager(){return this.sessionManager}setupStdinForwarding(e){e.ptyProcess&&y.warn(`setupStdinForwarding called for session ${e.id} - stdin should be handled via IPC socket`)}writeActivityState(e,t){let r=_e.join(e.controlDir,"claude-activity.json"),s={isActive:t.isActive,specificStatus:t.specificStatus,timestamp:new Date().toISOString()},n=JSON.stringify(s);if(this.lastWrittenActivityState.get(e.id)!==n)try{we.writeFileSync(r,JSON.stringify(s,null,2)),this.lastWrittenActivityState.set(e.id,n),e.activityFileWritten||(e.activityFileWritten=!0,y.debug(`Writing activity state to ${r} for session ${e.id}`,{activityState:t,timestamp:s.timestamp}))}catch(a){y.error(`Failed to write activity state for session ${e.id}:`,a)}}trackAndEmit(e,t,...r){let s=this.listeners(e);this.sessionEventListeners.has(t)||this.sessionEventListeners.set(t,new Set);let n=this.sessionEventListeners.get(t);n&&(s.forEach(i=>n.add(i)),this.emit(e,t,...r))}cleanupSessionResources(e){if(this.sessionResizeSources.delete(e.id),e.titleUpdateInterval&&(clearInterval(e.titleUpdateInterval),e.titleUpdateInterval=void 0),e.activityDetector&&(e.activityDetector.clearStatus(),e.activityDetector=void 0),e.titleFilter&&(e.titleFilter=void 0),e.sessionJsonWatcher&&(e.sessionJsonWatcher.close(),e.sessionJsonWatcher=void 0),e.sessionJsonInterval&&(clearInterval(e.sessionJsonInterval),e.sessionJsonInterval=void 0),e.connectedClients){for(let r of e.connectedClients)try{r.destroy()}catch{}e.connectedClients.clear()}if(e.inputSocketServer){e.inputSocketServer.close(),e.inputSocketServer.unref();try{we.unlinkSync(_e.join(e.controlDir,"ipc.sock"))}catch{}}let t=this.sessionEventListeners.get(e.id);t&&(t.forEach(r=>{this.removeListener("sessionNameChanged",r),this.removeListener("watcherError",r),this.removeListener("bell",r)}),this.sessionEventListeners.delete(e.id)),this.lastWrittenActivityState.delete(e.id),e.titleInjectionTimer&&(clearInterval(e.titleInjectionTimer),e.titleInjectionTimer=void 0)}markTitleUpdateNeeded(e){if(y.debug(`[markTitleUpdateNeeded] Called for session ${e.id}`,{titleMode:e.titleMode,sessionName:e.sessionInfo.name,titleUpdateNeeded:e.titleUpdateNeeded}),!e.titleMode||e.titleMode==="none"){y.debug("[markTitleUpdateNeeded] Skipping - title mode is NONE or undefined");return}e.titleUpdateNeeded=!0,y.debug("[markTitleUpdateNeeded] Set titleUpdateNeeded=true, calling checkAndUpdateTitle"),this.checkAndUpdateTitle(e)}updateTerminalTitleForSessionName(e){if(!e.stdoutQueue||!e.isExternalTerminal){y.debug("[updateTerminalTitleForSessionName] Early return - no stdout queue or not external terminal");return}let t=null;!e.titleMode||e.titleMode==="none"||e.titleMode==="filter"?t=Vt(e.currentWorkingDir||e.sessionInfo.workingDir,e.sessionInfo.command,e.sessionInfo.name||"VibeTunnel"):t=this.generateTerminalTitle(e),t&&t!==e.currentTitle&&(y.debug("[updateTerminalTitleForSessionName] Updating title for session name change"),e.pendingTitleToInject=t,e.titleUpdateNeeded=!0,e.titleInjectionTimer||this.startTitleInjectionMonitor(e))}checkAndUpdateTitle(e){if(y.debug(`[checkAndUpdateTitle] Called for session ${e.id}`,{titleUpdateNeeded:e.titleUpdateNeeded,hasStdoutQueue:!!e.stdoutQueue,isExternalTerminal:e.isExternalTerminal,sessionName:e.sessionInfo.name}),!e.titleUpdateNeeded||!e.stdoutQueue||!e.isExternalTerminal){y.debug("[checkAndUpdateTitle] Early return - conditions not met");return}y.debug("[checkAndUpdateTitle] Generating new title...");let t=this.generateTerminalTitle(e);y.debug(`[Title Update] Session ${e.id}:`,{sessionName:e.sessionInfo.name,newTitle:t?`${t.substring(0,50)}...`:null,currentTitle:e.currentTitle?`${e.currentTitle.substring(0,50)}...`:null,titleChanged:t!==e.currentTitle}),t&&t!==e.currentTitle?(y.debug("[checkAndUpdateTitle] Title changed, queueing for injection"),e.pendingTitleToInject=t,e.titleInjectionTimer||(y.debug("[checkAndUpdateTitle] Starting title injection monitor"),this.startTitleInjectionMonitor(e))):y.debug("[checkAndUpdateTitle] Title unchanged or null, skipping injection",{newTitleNull:!t,titlesEqual:t===e.currentTitle}),e.titleUpdateNeeded=!1}startTitleInjectionMonitor(e){e.titleInjectionTimer=setInterval(()=>{if(!e.pendingTitleToInject||!e.stdoutQueue){e.titleInjectionTimer&&(clearInterval(e.titleInjectionTimer),e.titleInjectionTimer=void 0);return}let r=Date.now()-(e.lastWriteTimestamp||0);if(r>=Li&&!e.titleInjectionInProgress){let s=e.pendingTitleToInject;if(!s)return;e.titleInjectionInProgress=!0,e.lastWriteTimestamp=Date.now(),e.stdoutQueue.enqueue(async()=>{try{y.debug(`[Title Injection] Writing title to stdout for session ${e.id}:`,{title:`${s.substring(0,50)}...`}),process.stdout.write(s)||await(0,Kt.once)(process.stdout,"drain"),e.currentTitle=s,y.debug(`[Title Injection] Successfully injected title for session ${e.id}`),e.pendingTitleToInject===s&&(e.pendingTitleToInject=void 0),!e.pendingTitleToInject&&e.titleInjectionTimer&&(clearInterval(e.titleInjectionTimer),e.titleInjectionTimer=void 0)}finally{e.titleInjectionInProgress=!1}}),y.debug(`Injected title during quiet period (${r}ms) for session ${e.id}`)}},Fi)}generateTerminalTitle(e){if(!e.titleMode||e.titleMode==="none")return null;let t=e.currentWorkingDir||e.sessionInfo.workingDir;if(y.debug(`[generateTerminalTitle] Session ${e.id}:`,{titleMode:e.titleMode,sessionName:e.sessionInfo.name,sessionInfoObjectId:e.sessionInfo,currentDir:t,command:e.sessionInfo.command,activityDetectorExists:!!e.activityDetector}),e.titleMode==="static")return Vt(t,e.sessionInfo.command,e.sessionInfo.name);if(e.titleMode==="dynamic"&&e.activityDetector){let r=e.activityDetector.getActivityState();return y.debug("[generateTerminalTitle] Calling generateDynamicTitle with:",{currentDir:t,command:e.sessionInfo.command,sessionName:e.sessionInfo.name,activity:r}),yn(t,e.sessionInfo.command,r,e.sessionInfo.name)}return null}};var Cn=require("events"),In=x(require("net"));L();var ht=k("socket-client"),Jt=class extends Cn.EventEmitter{constructor(t,r={}){super();this.socketPath=t;this.options=r;t.length>103&&ht.warn(`Socket path may be too long (${t.length} chars): ${t}`)}socket;parser=new Nt;connected=!1;reconnectTimer;reconnectDelay=1e3;heartbeatInterval;lastHeartbeat=Date.now();connect(){return new Promise((t,r)=>{if(this.connected){t();return}this.socket=In.createConnection(this.socketPath),this.socket.setNoDelay(!0),this.socket.setKeepAlive(!0,0);let s=()=>{this.connected=!0,this.setupSocketHandlers(),this.emit("connect"),this.startHeartbeat(),i(),t()},n=a=>{i(),this.socket?.destroy(),this.socket=void 0,r(a)},i=()=>{this.socket?.off("connect",s),this.socket?.off("error",n)};this.socket.once("connect",s),this.socket.once("error",n)})}setupSocketHandlers(){this.socket&&(this.socket.on("data",t=>{this.parser.addData(t);for(let{type:r,payload:s}of this.parser.parseMessages())this.handleMessage(r,s)}),this.socket.on("close",()=>{this.handleDisconnect()}),this.socket.on("error",t=>{ht.error(`Socket error on ${this.socketPath}:`,t),this.emit("error",t)}))}handleMessage(t,r){try{let s=hr(t,r);switch(t){case 3:this.emit("status",s);break;case 5:this.emit("serverError",s);break;case 4:this.lastHeartbeat=Date.now(),this.sendHeartbeat();break;default:ht.debug(`Received unexpected message type: ${t}`)}}catch(s){ht.error("Failed to parse message:",s)}}handleDisconnect(t){this.connected=!1,this.stopHeartbeat(),this.emit("disconnect",t),this.options.autoReconnect&&!this.reconnectTimer&&(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.connect().catch(r=>{ht.debug(`Reconnection failed: ${r.message}`),this.handleDisconnect(r)})},this.reconnectDelay))}startHeartbeat(){this.options.heartbeatInterval&&(this.heartbeatInterval=setInterval(()=>{this.sendHeartbeat()},this.options.heartbeatInterval))}stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=void 0)}sendStdin(t){return this.send(ot.stdin(t))}resize(t,r){return this.send(ot.resize(t,r))}kill(t){return this.send(ot.kill(t))}resetSize(){return this.send(ot.resetSize())}updateTitle(t){return this.send(ot.updateTitle(t))}sendStatus(t,r,s){return this.send(ot.status(t,r,s))}sendHeartbeat(){return this.send(ot.heartbeat())}send(t){if(!this.connected||!this.socket)return ht.debug("Cannot send message: not connected"),!1;try{return this.socket.write(t)}catch(r){return ht.error("Failed to send message:",r),!1}}disconnect(){this.options.autoReconnect=!1,this.connected=!1,this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0),this.socket&&(this.socket.destroy(),this.socket=void 0)}isConnected(){return this.connected}getTimeSinceLastHeartbeat(){return Date.now()-this.lastHeartbeat}};var mr=require("child_process"),be=x(require("fs")),Rn=x(require("os")),Mt=x(require("path"));L();var Z=k("claude-patcher"),yr=new Map;function Bi(){for(let[o,e]of yr.entries())try{if(be.default.existsSync(e)){be.default.copyFileSync(e,o),Z.debug(`Restored binary: ${o}`);try{be.default.unlinkSync(e),Z.debug(`Cleaned up backup: ${e}`)}catch(t){Z.debug(`Failed to clean up backup ${e}:`,t)}}}catch(t){Z.error(`Failed to restore binary ${o}:`,t)}yr.clear()}var An=!1;function Oi(){if(An)return;An=!0;let o=()=>{Bi()};process.on("exit",o),process.on("SIGINT",()=>{o(),process.exit(130)}),process.on("SIGTERM",()=>{o(),process.exit(143)})}function Ui(o){if(yr.has(o))return Z.debug(`Binary already patched: ${o}`),o;let e=Mt.default.basename(o),t=Rn.default.tmpdir(),r=Mt.default.join(t,`vibetunnel-claude-backup-${Date.now()}-${e}`);be.default.copyFileSync(o,r),Z.debug(`Created backup at ${r}`);let s=be.default.readFileSync(o,"utf8"),n=[/if\([A-Za-z0-9_$]+\(\)\)process\.exit\(1\);/g,/if\s*\([A-Za-z0-9_$]+\(\)\)\s*process\.exit\(1\);/g,/if\([A-Za-z0-9_$]+\(\)\)process\.exit\(\d+\);/g],i=s,a=!1;for(let u of n){let c=i.replace(u,"if(false)process.exit(1);");c!==i&&(i=c,a=!0,Z.debug(`Applied patch for pattern: ${u}`))}return a?(be.default.writeFileSync(o,i),yr.set(o,r),Oi(),Z.log("Patched Claude binary"),o):(Z.warn("No anti-debugging pattern found - Claude binary may have changed"),o)}function Nn(o){if(o.length===0)return o;let e=o[0];Z.debug(`Checking command: ${e}`);try{let n=process.env.SHELL||"/bin/bash",a=Mt.default.basename(n)==="zsh"?`${n} -i -c "alias ${e} 2>/dev/null"`:`${n} -i -c "alias ${e} 2>&1"`,u=(0,mr.execSync)(a,{encoding:"utf8"}).trim();if(u&&!u.includes("not found")){let c=u.match(/^(?:alias\s+)?[^=]+=["']?(.+?)["']?$/);if(c){let l=c[1].split(" ")[0];Z.debug(`Resolved alias: ${e} \u2192 ${l}`),e=l}}}catch{Z.debug(`No alias found for: ${e}`)}let t=e;if(!Mt.default.isAbsolute(e))try{let n=(0,mr.execSync)(`which "${e}" 2>/dev/null`,{encoding:"utf8"}).trim();if(n)t=n,Z.debug(`Found in PATH: ${t}`);else try{let i=(0,mr.execSync)(`command -v "${e}" 2>/dev/null`,{encoding:"utf8",shell:"/bin/sh"}).trim();i&&i!==e&&(t=i,Z.debug(`Found via command -v: ${t}`))}catch{}}catch{Z.debug(`Could not find ${e} in PATH`)}try{if(be.default.existsSync(t)&&be.default.lstatSync(t).isSymbolicLink()){let n=be.default.realpathSync(t);Z.debug(`Resolved symlink: ${t} \u2192 ${n}`),t=n}}catch(n){Z.debug(`Could not resolve symlink: ${n}`)}if(!be.default.existsSync(t))return Z.debug(`Resolved path does not exist: ${t}`),o;try{let n=be.default.openSync(t,"r"),i=Buffer.alloc(1024),a=be.default.readSync(n,i,0,1024,0);be.default.closeSync(n);let u=i.toString("utf8",0,a);if(!(u.includes("#!/usr/bin/env")&&u.includes("node")&&u.includes("Anthropic PBC")))return Z.debug(`Not a Claude CLI binary: ${Mt.default.basename(t)}`),o;let l=be.default.readFileSync(t,"utf8");if(!(l.includes("process.exit(1)")||l.includes("PF5()")))return Z.debug("Claude CLI detected but no anti-debugging patterns found"),o}catch(n){return Z.debug(`Could not read file to verify Claude binary: ${n}`),o}Z.log(`Detected Claude CLI binary at: ${t}`);let s=[Ui(t),...o.slice(1)];return Z.log(`Using patched command: ${s.join(" ")}`),s}L();var Mn=x(require("os")),Dn=x(require("path"));function _i(o){if(!o)return"";let e=Mn.homedir(),t=o;o.startsWith(e)&&(t=`~${o.slice(e.length)}`),t=t.replace("/Development/","/Dev/").replace("/Documents/","/Docs/").replace("/Applications/","/Apps/");let r=t.split("/").filter(s=>s);return r.length>3?`\u2026/${r.slice(-2).join("/")}`:t}function Qt(o,e){let t=Dn.basename(o[0]),r=_i(e);return r?`${t} (${r})`:t}L();function br(){if(process.env.VIBETUNNEL_LOG_LEVEL){let o=ar(process.env.VIBETUNNEL_LOG_LEVEL);if(o!==void 0)return o;console.warn(`Invalid VIBETUNNEL_LOG_LEVEL: ${process.env.VIBETUNNEL_LOG_LEVEL}`),console.warn("Valid levels: silent, error, warn, info, verbose, debug")}if(process.env.VIBETUNNEL_DEBUG==="1"||process.env.VIBETUNNEL_DEBUG==="true")return 5}var A=k("fwd");function jn(){console.log(f.blue(`VibeTunnel Forward v${ve}`)+f.gray(` (${At})`)),console.log(""),console.log("Usage:"),console.log(" pnpm exec tsx src/fwd.ts [--session-id ] [--title-mode ] [--verbosity ] [args...]"),console.log(""),console.log("Options:"),console.log(" --session-id Use a pre-generated session ID"),console.log(" --title-mode Terminal title mode: none, filter, static, dynamic"),console.log(" (defaults to none for most commands, dynamic for claude)"),console.log(" --update-title Update session title and exit (requires --session-id)"),console.log(" --verbosity <level> Set logging verbosity: silent, error, warn, info, verbose, debug"),console.log(" (defaults to error)"),console.log(" --log-file <path> Override default log file location"),console.log(" (defaults to ~/.vibetunnel/log.txt)"),console.log(""),console.log("Title Modes:"),console.log(" none - No title management (default)"),console.log(" filter - Block all title changes from applications"),console.log(" static - Show working directory and command"),console.log(" dynamic - Show directory, command, and activity (auto-selected for claude)"),console.log(""),console.log("Verbosity Levels:"),console.log(` ${f.gray("silent")} - No output except critical errors`),console.log(` ${f.red("error")} - Only errors ${f.gray("(default)")}`),console.log(` ${f.yellow("warn")} - Errors and warnings`),console.log(` ${f.green("info")} - Errors, warnings, and informational messages`),console.log(` ${f.blue("verbose")} - All messages except debug`),console.log(` ${f.magenta("debug")} - All messages including debug`),console.log(""),console.log(`Quick verbosity: ${f.cyan("-q (quiet), -v (verbose), -vv (extra), -vvv (debug)")}`),console.log(""),console.log("Environment Variables:"),console.log(" VIBETUNNEL_TITLE_MODE=<mode> Set default title mode"),console.log(" VIBETUNNEL_CLAUDE_DYNAMIC_TITLE=1 Force dynamic title for Claude"),console.log(" VIBETUNNEL_LOG_LEVEL=<level> Set default verbosity level"),console.log(" VIBETUNNEL_DEBUG=1 Enable debug mode (legacy)"),console.log(""),console.log("Examples:"),console.log(" pnpm exec tsx src/fwd.ts claude --resume"),console.log(" pnpm exec tsx src/fwd.ts --title-mode static bash -l"),console.log(" pnpm exec tsx src/fwd.ts --title-mode filter vim"),console.log(" pnpm exec tsx src/fwd.ts --session-id abc123 claude"),console.log(' pnpm exec tsx src/fwd.ts --update-title "New Title" --session-id abc123'),console.log(" pnpm exec tsx src/fwd.ts --verbosity silent npm test"),console.log(""),console.log("The command will be spawned in the current working directory"),console.log("and managed through the VibeTunnel PTY infrastructure.")}async function Ln(o){let e=br();e===5&&A.setDebugMode(!0),(o.length===0||o[0]==="--help"||o[0]==="-h")&&(jn(),se(),process.exit(0)),A.debug(f.blue(`VibeTunnel Forward v${ve}`)+f.gray(` (${At})`)),A.debug(`Full command: ${o.join(" ")}`);let t,r="none",s,n,i=o;if(process.env.VIBETUNNEL_TITLE_MODE){let h=process.env.VIBETUNNEL_TITLE_MODE.toLowerCase();Object.values($t).includes(h)&&(r=h,A.debug(`Title mode set from environment: ${r}`))}for((process.env.VIBETUNNEL_CLAUDE_DYNAMIC_TITLE==="1"||process.env.VIBETUNNEL_CLAUDE_DYNAMIC_TITLE==="true")&&(r="dynamic",A.debug("Forced dynamic title mode for Claude via environment variable"));i.length>0;)if(i[0]==="--session-id"&&i.length>1)t=i[1],i=i.slice(2);else if(i[0]==="--update-title"&&i.length>1)s=i[1],i=i.slice(2);else if(i[0]==="--title-mode"&&i.length>1){let h=i[1].toLowerCase();Object.values($t).includes(h)?r=h:(A.error(`Invalid title mode: ${i[1]}`),A.error(`Valid modes: ${Object.values($t).join(", ")}`),se(),process.exit(1)),i=i.slice(2)}else if(i[0]==="--verbosity"&&i.length>1){let h=ar(i[1]);h!==void 0?e=h:(A.error(`Invalid verbosity level: ${i[1]}`),A.error("Valid levels: silent, error, warn, info, verbose, debug"),se(),process.exit(1)),i=i.slice(2)}else if(i[0]==="--log-file"&&i.length>1)n=i[1],i=i.slice(2);else break;if(i[0]==="--"&&i.length>1&&(i=i.slice(1)),n!==void 0&&(Ys(n),A.debug(`Log file path set to: ${n}`)),e!==void 0&&(es(e),e>=3&&A.log(`Verbosity level set to: ${zt[e].toLowerCase()}`)),s!==void 0){t||(A.error("--update-title requires --session-id"),se(),process.exit(1));let h=Ve.join(us.homedir(),".vibetunnel","control"),b=new pt(h);/^[a-zA-Z0-9_-]+$/.test(t)||(A.error(`Invalid session ID format: "${t}". Session IDs must only contain letters, numbers, hyphens (-), and underscores (_).`),se(),process.exit(1));try{let v=b.loadSessionInfo(t);v||(A.error(`Session ${t} not found`),se(),process.exit(1));let $=s.substring(0,256).split("").filter(w=>{let P=w.charCodeAt(0);return P>=32&&P!==127&&(P<128||P>159)}).join(""),E=Ve.join(h,t,"ipc.sock");if(Ne.existsSync(E)){A.debug("IPC socket found, sending title update via IPC");let w=new Jt(E,{autoReconnect:!1});try{await w.connect(),w.updateTitle($)?(A.log(`Session title updated to: ${$}`),w.disconnect(),se(),process.exit(0)):A.warn("Failed to send title update via IPC, falling back to file update"),w.disconnect()}catch(P){A.warn(`IPC connection failed: ${P}, falling back to file update`)}}else A.debug("No IPC socket found, session might not be active");v.name=$,b.saveSessionInfo(t,v),A.log(`Session title updated to: ${$}`),se(),process.exit(0)}catch(v){A.error(`Failed to update session title: ${v instanceof Error?v.message:String(v)}`),se(),process.exit(1)}}let a=i;if(a.length===0&&(A.error("No command specified"),jn(),se(),process.exit(1)),process.env.VIBETUNNEL_DEBUG==="1"||process.env.VIBETUNNEL_DEBUG==="true"){let h=Nn(a);h!==a&&(a=h,A.debug("Command updated after patching"))}r==="none"&&a.some(b=>b.toLowerCase().includes("claude"))&&(r="dynamic",A.log(f.cyan("\u2713 Auto-selected dynamic title mode for Claude")),A.debug(`Detected Claude in command: ${a.join(" ")}`));let u=process.cwd(),c=Ve.join(us.homedir(),".vibetunnel","control");A.debug(`Control path: ${c}`);let l=new gt(c),d=process.env.VIBETUNNEL_SESSION_ID!==void 0,g,p;d?(await new Promise(h=>setTimeout(h,100)),process.stdout.isTTY&&process.stdout.columns&&process.stdout.rows?(g=process.stdout.columns,p=process.stdout.rows,A.debug(`External spawn using actual terminal size: ${g}x${p}`)):A.debug("External spawn: terminal dimensions not available, using terminal defaults")):(g=process.stdout.columns||120,p=process.stdout.rows||40,A.debug(`Regular spawn with dimensions: ${g}x${p}`));try{let h=Qt(a,u),b=t||`fwd_${Date.now()}`;if(A.log(`Creating session for command: ${a.join(" ")}`),A.debug(`Session ID: ${b}, working directory: ${u}`),r!=="none"){let I={filter:"Terminal title changes will be blocked",static:"Terminal title will show path and command",dynamic:"Terminal title will show path, command, and activity"};A.log(f.cyan(`\u2713 ${I[r]}`))}let v,$,E={sessionId:b,name:h,workingDir:u,titleMode:r,forwardToStdout:!0,onExit:async I=>{A.log(f.yellow(` +\u2713 VibeTunnel session ended`)+f.gray(` (exit code: ${I})`)),process.stdout.removeListener("resize",z),process.stdin.isTTY&&(A.debug("Restoring terminal to normal mode"),process.stdin.setRawMode(!1)),process.stdin.pause(),process.stdin.removeAllListeners(),process.stdin.destroy&&process.stdin.destroy(),N&&N(),v&&(v.close(),v=void 0,A.debug("Closed session file watcher")),$&&clearTimeout($),Ne.unwatchFile(Y),A.debug("Shutting down PTY manager"),await l.shutdown(),se(),process.exit(I||0)}};g!==void 0&&p!==void 0&&(E.cols=g,E.rows=p);let w=await l.createSession(a,E);if(!l.getSession(w.sessionId))throw new Error("Session not found after creation");A.log(f.green("\u2713 VibeTunnel session started")+f.gray(` (v${ve})`)),A.log(f.gray("Command:"),a.join(" ")),A.log(f.gray("Control directory:"),Ve.join(c,w.sessionId)),A.log(f.gray("Build:"),`${At} | Commit: ${It}`);let V=Ve.join(c,w.sessionId,"ipc.sock"),_=new Jt(V,{autoReconnect:!0,heartbeatInterval:3e4});try{await _.connect(),A.debug("Connected to session IPC socket")}catch(I){throw A.error("Failed to connect to session socket:",I),I}let z=()=>{let I=process.stdout.columns||80,q=process.stdout.rows||24;A.debug(`Terminal resized to ${I}x${q}`),_.resize(I,q)||A.error("Failed to send resize command")};process.stdout.on("resize",z);let Y=Ve.join(c,w.sessionId,"session.json"),K=w.sessionInfo.name,Te=async(I=0)=>{let ce=500*2**I;try{if(!Ne.existsSync(Y))if(I<5){A.debug(`Session file not found, retrying in ${ce}ms (attempt ${I+1}/5)`),setTimeout(()=>Te(I+1),ce);return}else{A.warn(`Session file not found after 5 attempts: ${Y}`);return}A.log("Setting up file watcher for session name changes");let B=()=>{try{if(!Ne.existsSync(Y))return;let O=Ne.readFileSync(Y,"utf-8"),X=JSON.parse(O);if(X.name!==K){A.debug(`[File Watch] Session name changed from "${K}" to "${X.name}"`),K=X.name;let de;r==="none"||r==="filter"?de=`\x1B]2;${X.name}\x07`:de=Vt(u,a,X.name),process.stdout.write(de),A.log(`Updated terminal title to "${X.name}" via file watcher`)}}catch(O){A.error("Failed to check session.json:",O)}};Ne.watchFile(Y,{interval:500},(O,X)=>{A.debug(`[File Watch] File stats changed - mtime: ${O.mtime} vs ${X.mtime}`),O.mtime!==X.mtime&&B()});try{let O=Ve.dirname(Y);v=Ne.watch(O,(X,de)=>{A.debug(`[File Watch] Directory event: ${X} on ${de||"unknown"}`),de&&(de==="session.json"||de==="session.json.tmp")&&($&&clearTimeout($),$=setTimeout(B,100))})}catch(O){A.warn("Failed to set up fs.watch, relying on fs.watchFile:",O)}A.log("File watcher successfully set up with polling fallback"),v?.on("error",O=>{A.error("File watcher error:",O),v?.close(),v=void 0})}catch(B){A.error("Failed to set up file watcher:",B),I<5&&setTimeout(()=>Te(I+1),ce)}};setTimeout(()=>Te(),500);let C,N;if(r==="dynamic"){C=new Pt(a);let I=process.stdout.write.bind(process.stdout),q=function(ce,B,O){if(typeof B=="function"&&(O=B,B=void 0),C&&typeof ce=="string"){let{filteredData:X,activity:de}=C.processOutput(ce);return de.specificStatus&&_.sendStatus(de.specificStatus.app,de.specificStatus.status),O?I.call(this,ce,B,O):B&&typeof B=="string"?I.call(this,X,B):I.call(this,X)}return O?I.call(this,ce,B,O):B&&typeof B=="string"?I.call(this,ce,B):I.call(this,ce)};process.stdout.write=q,N=()=>{process.stdout.write=I},process.on("exit",N),process.on("SIGINT",N),process.on("SIGTERM",N)}process.stdin.isTTY&&(A.debug("Setting terminal to raw mode for input forwarding"),process.stdin.setRawMode(!0)),process.stdin.resume(),process.stdin.setEncoding("utf8"),process.stdin.on("data",I=>{_.sendStdin(I)||A.error("Failed to send stdin data")}),_.on("disconnect",I=>{A.error("Socket disconnected:",I?.message||"Unknown error"),process.exit(1)}),_.on("error",I=>{A.error("Socket error:",I)})}catch(h){A.error("Failed to create or manage session:",h),se(),process.exit(1)}}le();var Is=x(require("compression")),_r=x(require("express")),zr=x(require("fs")),jo=x(require("helmet")),Lo=require("http"),Hr=x(require("os")),ue=x(require("path"));var Wr=require("ws");L();var Me=k("auth");function zi(o){let e=o.ip||o.socket.remoteAddress||"",r=["127.0.0.1","::1","::ffff:127.0.0.1","localhost"].includes(e),s=!o.headers["x-forwarded-for"],n=!o.headers["x-real-ip"],i=!o.headers["x-forwarded-host"],a=o.hostname==="localhost"||o.hostname==="127.0.0.1"||o.hostname==="[::1]";return Me.debug(`Local request check - IP: ${e}, Host: ${o.hostname}, Forwarded headers: ${!s||!n||!i}`),r&&s&&n&&i&&a}function Fn(o){return(e,t,r)=>{if(e.path.startsWith("/auth")||e.path.startsWith("/logs")||e.path.startsWith("/push"))return r();if(o.noAuth)return e.authMethod="no-auth",e.userId="no-auth-user",r();if(o.allowLocalBypass&&zi(e))if(o.localAuthToken){if(e.headers["x-vibetunnel-local"]===o.localAuthToken)return Me.debug("Local request authenticated with token"),e.authMethod="local-bypass",e.userId="local-user",r();Me.debug("Local request missing or invalid token")}else return Me.debug("Local request authenticated without token"),e.authMethod="local-bypass",e.userId="local-user",r();let s=e.headers.authorization,n=e.query.token;if(s?.startsWith("Bearer ")){let i=s.substring(7);if(o.isHQMode&&o.bearerToken&&i===o.bearerToken)return Me.debug("Valid HQ bearer token authentication"),e.isHQRequest=!0,e.authMethod="hq-bearer",r();if(o.authService&&o.enableSSHKeys){let a=o.authService.verifyToken(i);if(a.valid&&a.userId)return e.userId=a.userId,e.authMethod="ssh-key",r();Me.error("Invalid JWT token")}else if(o.authService){let a=o.authService.verifyToken(i);if(a.valid&&a.userId)return e.userId=a.userId,e.authMethod="password",r();Me.error("Invalid JWT token")}if(!o.isHQMode&&o.bearerToken&&i===o.bearerToken)return Me.debug("Valid remote bearer token authentication"),e.authMethod="hq-bearer",r();Me.error(`Bearer token rejected - HQ mode: ${o.isHQMode}, token matches: ${o.bearerToken===i}`)}if(n&&o.authService){let i=o.authService.verifyToken(n);if(i.valid&&i.userId)return Me.debug(`Valid query token for user: ${i.userId}`),e.userId=i.userId,e.authMethod=o.enableSSHKeys?"ssh-key":"password",r();Me.error("Invalid query token")}Me.error(`Unauthorized request to ${e.method} ${e.path} from ${e.ip}`),t.setHeader("WWW-Authenticate",'Bearer realm="VibeTunnel"'),t.status(401).json({error:"Authentication required"})}}var Bn=require("express"),ds=require("util");function On(o){let e=(0,Bn.Router)(),{authService:t}=o;return e.post("/challenge",async(r,s)=>{try{let{userId:n}=r.body;if(!n)return s.status(400).json({error:"User ID is required"});if(!await t.userExists(n))return s.status(404).json({error:"User not found"});let a=t.createChallenge(n);s.json({challengeId:a.challengeId,challenge:a.challenge,expiresAt:Date.now()+5*60*1e3})}catch(n){console.error("Error creating auth challenge:",n),s.status(500).json({error:"Failed to create authentication challenge"})}}),e.post("/ssh-key",async(r,s)=>{try{let{challengeId:n,publicKey:i,signature:a}=r.body;if(!n||!i||!a)return s.status(400).json({error:"Challenge ID, public key, and signature are required"});let u=await t.authenticateWithSSHKey({challengeId:n,publicKey:i,signature:a});u.success?s.json({success:!0,token:u.token,userId:u.userId,authMethod:"ssh-key"}):s.status(401).json({success:!1,error:u.error})}catch(n){console.error("Error authenticating with SSH key:",n),s.status(500).json({error:"SSH key authentication failed"})}}),e.post("/password",async(r,s)=>{try{let{userId:n,password:i}=r.body;if(!n||!i)return s.status(400).json({error:"User ID and password are required"});let a=await t.authenticateWithPassword(n,i);a.success?s.json({success:!0,token:a.token,userId:a.userId,authMethod:"password"}):s.status(401).json({success:!1,error:a.error})}catch(n){console.error("Error authenticating with password:",n),s.status(500).json({error:"Password authentication failed"})}}),e.get("/verify",(r,s)=>{try{let n=r.headers.authorization;if(!n||!n.startsWith("Bearer "))return s.status(401).json({valid:!1,error:"No token provided"});let i=n.slice(7),a=t.verifyToken(i);a.valid?s.json({valid:!0,userId:a.userId}):s.status(401).json({valid:!1,error:"Invalid or expired token"})}catch(n){console.error("Error verifying token:",n),s.status(500).json({error:"Token verification failed"})}}),e.get("/current-user",(r,s)=>{try{let n=t.getCurrentUser();s.json({userId:n})}catch(n){console.error("Error getting current user:",n),s.status(500).json({error:"Failed to get current user"})}}),e.get("/config",(r,s)=>{try{s.json({enableSSHKeys:o.enableSSHKeys||!1,disallowUserPassword:o.disallowUserPassword||!1,noAuth:o.noAuth||!1})}catch(n){console.error("Error getting auth config:",n),s.status(500).json({error:"Failed to get auth config"})}}),e.get("/avatar/:userId",async(r,s)=>{try{let{userId:n}=r.params;if(!n||!/^[a-zA-Z0-9._-]+$/.test(n))return s.status(400).json({error:"Invalid user ID format"});if(n.length>255)return s.status(400).json({error:"User ID too long"});if(process.platform!=="darwin")return s.json({avatar:null,platform:process.platform});try{let{execFile:i}=await import("child_process"),a=(0,ds.promisify)(i),{stdout:u}=await a("dscl",[".","-read",`/Users/${n}`,"JPEGPhoto"]);if(u.includes("JPEGPhoto:")){let l=u.split(` +`).slice(1).filter(d=>d.trim()&&!d.startsWith("dsAttrTypeNative"));if(l.length>0){let d=l.join("").replace(/\s/g,""),p=Buffer.from(d,"hex").toString("base64");return s.json({avatar:`data:image/jpeg;base64,${p}`,platform:"darwin",source:"dscl"})}}}catch{console.log("No JPEGPhoto found for user, trying Picture attribute")}try{let{execFile:i}=await import("child_process"),a=(0,ds.promisify)(i),{stdout:u}=await a("dscl",[".","-read",`/Users/${n}`,"Picture"]);if(u.includes("Picture:")){let c=u.split("Picture:")[1].trim();if(c&&c!=="Picture:")return s.json({avatar:c,platform:"darwin",source:"picture_path"})}}catch{console.log("No Picture attribute found for user")}s.json({avatar:null,platform:"darwin"})}catch(n){console.error("Error getting user avatar:",n),s.status(500).json({error:"Failed to get user avatar"})}}),e.post("/logout",(r,s)=>{s.json({success:!0,message:"Logged out successfully"})}),e}var _n=require("express");L();var Un=k("config");function zn(o){let e=(0,_n.Router)(),{getRepositoryBasePath:t}=o;return e.get("/config",(r,s)=>{try{let n=t(),i={repositoryBasePath:n||"~/",serverConfigured:n!==null};Un.debug("[GET /api/config] Returning app config:",i),s.json(i)}catch(n){Un.error("[GET /api/config] Error getting app config:",n),s.status(500).json({error:"Failed to get app config"})}}),e}var Hn=require("express"),Dt=x(require("fs")),Ke=require("fs/promises"),Wn=x(require("mime-types")),fs=x(require("multer")),qn=x(require("os")),ie=x(require("path"));L();var mt=k("files"),Hi=process.env.VIBETUNNEL_CONTROL_DIR||ie.join(qn.homedir(),".vibetunnel/control"),ze=ie.join(Hi,"uploads");Dt.existsSync(ze)||(Dt.mkdirSync(ze,{recursive:!0}),mt.log(`Created uploads directory: ${ze}`));var Wi=fs.default.diskStorage({destination:(o,e,t)=>{t(null,ze)},filename:(o,e,t)=>{let r=`${Ue()}${ie.extname(e.originalname)}`;t(null,r)}}),qi=(o,e,t)=>{t(null,!0)},Vi=(0,fs.default)({storage:Wi,fileFilter:qi,limits:{fileSize:100*1024*1024}});function Vn(){let o=(0,Hn.Router)();return o.post("/files/upload",Vi.single("file"),(e,t)=>{try{if(!e.file)return t.status(400).json({error:"No file provided"});let r=ie.relative(process.cwd(),e.file.path),s=e.file.path;mt.log(`File uploaded by user ${e.userId}: ${e.file.filename} (${e.file.size} bytes)`),t.json({success:!0,filename:e.file.filename,originalName:e.file.originalname,size:e.file.size,mimetype:e.file.mimetype,path:s,relativePath:r})}catch(r){mt.error("File upload error:",r),t.status(500).json({error:"Failed to upload file"})}}),o.get("/files/:filename",async(e,t)=>{try{let r=e.params.filename,s=ie.join(ze,r);if(r.includes("..")||r.includes("/")||r.includes("\\")||r.includes("\0")||!/^[a-zA-Z0-9._-]+$/.test(r)||r.startsWith(".")||r.length>255)return t.status(400).json({error:"Invalid filename"});let n=ie.resolve(s),i=ie.resolve(ze);if(!n.startsWith(i+ie.sep)&&n!==i)return t.status(400).json({error:"Invalid file path"});try{await(0,Ke.access)(s)}catch{return t.status(404).json({error:"File not found"})}let a=await(0,Ke.stat)(s),u=Wn.lookup(r)||"application/octet-stream";t.setHeader("Content-Type",u),t.setHeader("Content-Length",a.size),t.setHeader("Cache-Control","public, max-age=86400"),Dt.createReadStream(s).pipe(t)}catch(r){mt.error("File serve error:",r),t.status(500).json({error:"Failed to serve file"})}}),o.get("/files",async(e,t)=>{try{let r=await(0,Ke.readdir)(ze),s=await Promise.all(r.map(async n=>{let i=ie.join(ze,n),a=await(0,Ke.stat)(i);return{filename:n,size:a.size,createdAt:a.birthtime,modifiedAt:a.mtime,url:`/api/files/${n}`,extension:ie.extname(n).toLowerCase()}}));s.sort((n,i)=>i.createdAt.getTime()-n.createdAt.getTime()),t.json({files:s,count:s.length})}catch(r){mt.error("File list error:",r),t.status(500).json({error:"Failed to list files"})}}),o.delete("/files/:filename",async(e,t)=>{try{let r=e.params.filename;if(r.includes("..")||r.includes("/")||r.includes("\\")||r.includes("\0")||!/^[a-zA-Z0-9._-]+$/.test(r)||r.startsWith(".")||r.length>255)return t.status(400).json({error:"Invalid filename"});let s=ie.join(ze,r),n=ie.resolve(s),i=ie.resolve(ze);if(!n.startsWith(i+ie.sep)&&n!==i)return t.status(400).json({error:"Invalid file path"});try{await(0,Ke.unlink)(s),mt.log(`File deleted by user ${e.userId}: ${r}`),t.json({success:!0,message:"File deleted successfully"})}catch{t.status(404).json({error:"File not found"})}}catch(r){mt.error("File deletion error:",r),t.status(500).json({error:"Failed to delete file"})}}),o}le();var Kn=require("child_process"),Gn=require("express"),vr=require("fs"),xe=x(require("fs/promises")),gs=x(require("mime-types")),J=x(require("path")),Jn=require("util");L();var R=k("filesystem"),Yt=(0,Jn.promisify)(Kn.exec);function Qn(){let o=(0,Gn.Router)();function e(s,n){return!0}async function t(s){try{let{stdout:n}=await Yt("git rev-parse --show-toplevel",{cwd:s}),i=n.trim(),{stdout:a}=await Yt("git branch --show-current",{cwd:s}),{stdout:u}=await Yt("git status --porcelain",{cwd:i}),c={isGitRepo:!0,branch:a.trim(),modified:[],added:[],deleted:[],untracked:[]};return u.split(` +`).forEach(l=>{if(!l)return;let d=l.substring(0,2),g=l.substring(3);d===" M"||d==="M "||d==="MM"?c.modified.push(g):d==="A "||d==="AM"?c.added.push(g):d===" D"||d==="D "?c.deleted.push(g):d==="??"&&c.untracked.push(g)}),{status:c,repoRoot:i}}catch{return null}}function r(s,n,i){if(!n)return;let a=J.relative(i,s);return n.modified.includes(a)?"modified":n.added.includes(a)?"added":n.deleted.includes(a)?"deleted":n.untracked.includes(a)?"untracked":"unchanged"}return o.get("/fs/browse",async(s,n)=>{try{let i=s.query.path||".",a=s.query.showHidden==="true",u=s.query.gitFilter;if(i==="~"||i.startsWith("~/")){let $=process.env.HOME||process.env.USERPROFILE;if(!$)return R.error("unable to determine home directory"),n.status(500).json({error:"Unable to determine home directory"});i=i==="~"?$:J.join($,i.slice(2))}if(R.debug(`browsing directory: ${i}, showHidden: ${a}, gitFilter: ${u}`),!e(i,process.cwd()))return R.warn(`access denied for path: ${i}`),n.status(403).json({error:"Access denied"});let c=J.resolve(i),l;try{l=await xe.stat(c)}catch($){if($ instanceof Error&&"code"in $&&$.code==="ENOENT")return R.warn(`directory not found: ${i}`),n.status(404).json({error:"Directory not found"});throw $}if(!l.isDirectory())return R.warn(`path is not a directory: ${i}`),n.status(400).json({error:"Path is not a directory"});let d=Date.now(),g=u!=="none"?await t(c):null,p=g?.status||null,h=g?.repoRoot||"";u!=="none"&&R.debug(`git status check took ${Date.now()-d}ms for ${i}`);let b=[];if(u==="changed"&&p){let $=[...p.modified.map(P=>({path:P,status:"modified"})),...p.added.map(P=>({path:P,status:"added"})),...p.deleted.map(P=>({path:P,status:"deleted"})),...p.untracked.map(P=>({path:P,status:"untracked"}))],E=J.relative(h,c),w=$.filter(P=>c===h?!0:P.path.startsWith(`${E}/`));b=await Promise.all(w.map(async P=>{let V=J.join(h,P.path),_=null,z="file";try{_=await xe.stat(V),z=_.isDirectory()?"directory":"file"}catch{_=null}return{name:J.relative(c,V),path:J.relative(process.cwd(),V),type:z,size:_?.size||0,modified:_?.mtime.toISOString()||new Date().toISOString(),permissions:_?.mode?.toString(8).slice(-3)||"000",isGitTracked:!0,gitStatus:P.status}}))}else{let $=await xe.readdir(c,{withFileTypes:!0});b=await Promise.all($.filter(E=>a||!E.name.startsWith(".")).map(async E=>{let w=J.join(c,E.name);try{let P=await xe.stat(w),V=J.relative(process.cwd(),w),_=E.isSymbolicLink();return{name:E.name,path:V,type:P.isDirectory()?"directory":"file",size:P.size,modified:P.mtime.toISOString(),permissions:P.mode.toString(8).slice(-3),isGitTracked:p?.isGitRepo||!1,gitStatus:r(w,p,h),isSymlink:_}}catch(P){return R.warn(`failed to stat ${w}:`,P),{name:E.name,path:J.relative(process.cwd(),w),type:"file",size:0,modified:new Date().toISOString(),permissions:"000",isGitTracked:!1,gitStatus:void 0}}}))}let v=b;v.sort(($,E)=>$.type!==E.type?$.type==="directory"?-1:1:$.name.localeCompare(E.name)),R.debug(`directory browsed successfully: ${i} (${v.length} items)`),n.json({path:i,fullPath:c,gitStatus:p,files:v})}catch(i){R.error(`failed to browse directory ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.get("/fs/preview",async(s,n)=>{try{let i=s.query.path;if(!i)return n.status(400).json({error:"Path is required"});if(R.debug(`previewing file: ${i}`),!e(i,process.cwd()))return R.warn(`access denied for file preview: ${i}`),n.status(403).json({error:"Access denied"});let a=J.resolve(process.cwd(),i),u=await xe.stat(a);if(u.isDirectory())return R.warn(`cannot preview directory: ${i}`),n.status(400).json({error:"Cannot preview directories"});let c=gs.default.lookup(a)||"application/octet-stream",l=c.startsWith("text/")||c==="application/json"||c==="application/javascript"||c==="application/typescript"||c==="application/xml";if(c.startsWith("image/"))R.log(f.green(`image preview generated: ${i} (${Sr(u.size)})`)),n.json({type:"image",mimeType:c,url:`/api/fs/raw?path=${encodeURIComponent(i)}`,size:u.size});else if(l||u.size<1024*1024){let g=await xe.readFile(a,"utf-8"),p=ps(a);R.log(f.green(`text file preview generated: ${i} (${Sr(u.size)}, ${p})`)),n.json({type:"text",content:g,language:p,mimeType:c,size:u.size})}else R.log(`binary file preview metadata returned: ${i} (${Sr(u.size)})`),n.json({type:"binary",mimeType:c,size:u.size,humanSize:Sr(u.size)})}catch(i){R.error(`failed to preview file ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.get("/fs/raw",(s,n)=>{try{let i=s.query.path;if(!i)return n.status(400).json({error:"Path is required"});if(R.debug(`serving raw file: ${i}`),!e(i,process.cwd()))return R.warn(`access denied for raw file: ${i}`),n.status(403).json({error:"Access denied"});let a=J.resolve(process.cwd(),i);if(!(0,vr.statSync)(a).isFile())return R.warn(`file not found for raw access: ${i}`),n.status(404).json({error:"File not found"});let u=gs.default.lookup(a)||"application/octet-stream";n.setHeader("Content-Type",u);let c=(0,vr.createReadStream)(a);c.pipe(n),c.on("end",()=>{R.log(f.green(`raw file served: ${i}`))})}catch(i){R.error(`failed to serve raw file ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.get("/fs/content",async(s,n)=>{try{let i=s.query.path;if(!i)return n.status(400).json({error:"Path is required"});if(R.debug(`getting file content: ${i}`),!e(i,process.cwd()))return R.warn(`access denied for file content: ${i}`),n.status(403).json({error:"Access denied"});let a=J.resolve(process.cwd(),i),u=await xe.readFile(a,"utf-8");R.log(f.green(`file content retrieved: ${i}`)),n.json({path:i,content:u,language:ps(a)})}catch(i){R.error(`failed to get file content ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.get("/fs/diff",async(s,n)=>{try{let i=s.query.path;if(!i)return n.status(400).json({error:"Path is required"});if(R.debug(`getting git diff: ${i}`),!e(i,process.cwd()))return R.warn(`access denied for git diff: ${i}`),n.status(403).json({error:"Access denied"});let a=J.resolve(process.cwd(),i),u=J.relative(process.cwd(),a),c=Date.now(),{stdout:l}=await Yt(`git diff HEAD -- "${u}"`,{cwd:process.cwd()}),d=Date.now()-c;d>1e3&&R.warn(`slow git diff operation: ${i} took ${d}ms`),R.log(f.green(`git diff retrieved: ${i} (${l.length>0?"has changes":"no changes"})`)),n.json({path:i,diff:l,hasDiff:l.length>0})}catch(i){R.error(`failed to get git diff for ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.get("/fs/diff-content",async(s,n)=>{try{let i=s.query.path;if(!i)return n.status(400).json({error:"Path is required"});if(R.debug(`getting diff content: ${i}`),!e(i,process.cwd()))return R.warn(`access denied for diff content: ${i}`),n.status(403).json({error:"Access denied"});let a=J.resolve(process.cwd(),i),u=J.relative(process.cwd(),a);R.debug(`Getting diff content for: ${i}`),R.debug(`Full path: ${a}`),R.debug(`CWD: ${process.cwd()}`);let c=await xe.readFile(a,"utf-8");R.debug(`Current content length: ${c.length}`);let l="";try{let d=`./${u}`;R.debug(`Getting HEAD version: git show HEAD:"${d}"`);let{stdout:g}=await Yt(`git show HEAD:"${d}"`,{cwd:process.cwd()});l=g,R.debug(`Got HEAD version for ${d}, length: ${l.length}`)}catch(d){if(d instanceof Error&&d.message.includes("does not exist"))l="",R.debug(`File ${i} does not exist in HEAD (new file)`);else{if(R.error(`Failed to get HEAD version of ./${u}:`,d),d instanceof Error&&"stderr"in d){let g=d;g.stderr&&R.error(`Git stderr: ${g.stderr}`)}l=c}}R.log(f.green(`diff content retrieved: ${i}`)),n.json({path:i,originalContent:l,modifiedContent:c,language:ps(a)})}catch(i){R.error(`failed to get diff content for ${s.query.path}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o.post("/fs/mkdir",async(s,n)=>{try{let{path:i,name:a}=s.body;if(!i||!a)return n.status(400).json({error:"Path and name are required"});if(R.log(`creating directory: ${a} in ${i}`),a.includes("/")||a.includes("\\")||a.startsWith("."))return R.warn(`invalid directory name attempted: ${a}`),n.status(400).json({error:"Invalid directory name"});if(!e(i,process.cwd()))return R.warn(`access denied for mkdir: ${i}/${a}`),n.status(403).json({error:"Access denied"});let u=J.resolve(process.cwd(),i,a);await xe.mkdir(u,{recursive:!0}),R.log(f.green(`directory created: ${J.relative(process.cwd(),u)}`)),n.json({success:!0,path:J.relative(process.cwd(),u)})}catch(i){R.error(`failed to create directory ${s.body.path}/${s.body.name}:`,i),n.status(500).json({error:i instanceof Error?i.message:String(i)})}}),o}function ps(o){let e=J.extname(o).toLowerCase();return{".js":"javascript",".jsx":"javascript",".ts":"typescript",".tsx":"typescript",".py":"python",".java":"java",".c":"c",".cpp":"cpp",".cs":"csharp",".php":"php",".rb":"ruby",".go":"go",".rs":"rust",".swift":"swift",".kt":"kotlin",".scala":"scala",".r":"r",".m":"objective-c",".mm":"objective-c",".h":"c",".hpp":"cpp",".sh":"shell",".bash":"shell",".zsh":"shell",".fish":"shell",".ps1":"powershell",".html":"html",".htm":"html",".xml":"xml",".css":"css",".scss":"scss",".sass":"sass",".less":"less",".json":"json",".yaml":"yaml",".yml":"yaml",".toml":"toml",".ini":"ini",".cfg":"ini",".conf":"ini",".sql":"sql",".md":"markdown",".markdown":"markdown",".tex":"latex",".dockerfile":"dockerfile",".makefile":"makefile",".cmake":"cmake",".gradle":"gradle",".vue":"vue",".svelte":"svelte",".elm":"elm",".clj":"clojure",".cljs":"clojure",".ex":"elixir",".exs":"elixir",".erl":"erlang",".hrl":"erlang",".fs":"fsharp",".fsx":"fsharp",".fsi":"fsharp",".ml":"ocaml",".mli":"ocaml",".pas":"pascal",".pp":"pascal",".pl":"perl",".pm":"perl",".t":"perl",".lua":"lua",".dart":"dart",".nim":"nim",".nims":"nim",".zig":"zig",".jl":"julia"}[e]||"plaintext"}function Sr(o,e=2){if(o===0)return"0 Bytes";let t=1024,r=e<0?0:e,s=["Bytes","KB","MB","GB","TB"],n=Math.floor(Math.log(o)/Math.log(t));return`${Number.parseFloat((o/t**n).toFixed(r))} ${s[n]}`}var Yn=require("express"),He=x(require("fs")),wr=x(require("os")),$r=x(require("path"));L();var Xt=k("logs");function Xn(o){let e=(0,Yn.Router)();return e.post("/logs/client",(t,r)=>{try{let{level:s,module:n,args:i}=t.body;if(!s||!n||!Array.isArray(i))return r.status(400).json({error:"Invalid log request. Required: level, module, args[]"});if(!["log","warn","error","debug"].includes(s))return r.status(400).json({error:"Invalid log level. Must be: log, warn, error, or debug"});let a=`[FE] ${n}`,u=s.toUpperCase();Xs(u==="LOG"?"LOG":u,a,i),r.status(204).send()}catch(s){Xt.error("Failed to process client log:",s),r.status(500).json({error:"Failed to process log"})}}),e.get("/logs/raw",(t,r)=>{try{let s=$r.join(wr.homedir(),".vibetunnel","log.txt");if(!He.existsSync(s))return r.setHeader("Content-Type","text/plain; charset=utf-8"),r.send("");r.setHeader("Content-Type","text/plain; charset=utf-8"),He.createReadStream(s).pipe(r)}catch(s){Xt.error("Failed to read log file:",s),r.status(500).json({error:"Failed to read log file"})}}),e.get("/logs/info",(t,r)=>{try{let s=$r.join(wr.homedir(),".vibetunnel","log.txt");if(!He.existsSync(s))return r.json({exists:!1,size:0,lastModified:null,path:s});let n=He.statSync(s);r.json({exists:!0,size:n.size,sizeHuman:Ki(n.size),lastModified:n.mtime,path:s})}catch(s){Xt.error("Failed to get log info:",s),r.status(500).json({error:"Failed to get log info"})}}),e.delete("/logs/clear",(t,r)=>{try{let s=$r.join(wr.homedir(),".vibetunnel","log.txt");He.existsSync(s)&&(He.truncateSync(s,0),Xt.log("Log file cleared")),r.status(204).send()}catch(s){Xt.error("Failed to clear log file:",s),r.status(500).json({error:"Failed to clear log file"})}}),e}function Ki(o){if(o===0)return"0 Bytes";let e=1024,t=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(o)/Math.log(e));return`${Number.parseFloat((o/e**r).toFixed(2))} ${t[r]}`}var Zn=require("express");L();var it=k("push-routes");function eo(o){let{vapidManager:e,pushNotificationService:t}=o,r=(0,Zn.Router)();return r.get("/push/vapid-public-key",(s,n)=>{try{let i=e.getPublicKey();if(!i)return n.status(503).json({error:"Push notifications not configured",message:"VAPID keys not available"});if(!e.isEnabled())return n.status(503).json({error:"Push notifications disabled",message:"VAPID configuration incomplete"});n.json({publicKey:i,enabled:!0})}catch(i){it.error("Failed to get VAPID public key:",i),n.status(500).json({error:"Internal server error",message:"Failed to retrieve VAPID public key"})}}),r.post("/push/subscribe",async(s,n)=>{if(!t)return n.status(503).json({error:"Push notifications not initialized",message:"Push notification service is not available"});try{let{endpoint:i,keys:a}=s.body;if(!i||!a||!a.p256dh||!a.auth)return n.status(400).json({error:"Invalid subscription data",message:"Missing required subscription fields"});let u=await t.addSubscription(i,a);n.json({success:!0,subscriptionId:u,message:"Successfully subscribed to push notifications"}),it.log(`Push subscription created: ${u}`)}catch(i){it.error("Failed to create push subscription:",i),n.status(500).json({error:"Subscription failed",message:"Failed to create push subscription"})}}),r.post("/push/unsubscribe",async(s,n)=>{if(!t)return n.status(503).json({error:"Push notifications not initialized",message:"Push notification service is not available"});try{let{endpoint:i}=s.body;if(!i)return n.status(400).json({error:"Missing endpoint",message:"Endpoint is required for unsubscription"});let u=t.getSubscriptions().find(c=>c.endpoint===i);u&&(await t.removeSubscription(u.id),it.log(`Push subscription removed: ${u.id}`)),n.json({success:!0,message:"Successfully unsubscribed from push notifications"})}catch(i){it.error("Failed to remove push subscription:",i),n.status(500).json({error:"Unsubscription failed",message:"Failed to remove push subscription"})}}),r.post("/push/test",async(s,n)=>{if(!t)return n.status(503).json({error:"Push notifications not initialized",message:"Push notification service is not available"});try{let i=await t.sendNotification({type:"test",title:"\u{1F514} Test Notification",body:"This is a test notification from VibeTunnel",icon:"/apple-touch-icon.png",badge:"/favicon-32.png",tag:"vibetunnel-test",requireInteraction:!1,actions:[{action:"dismiss",title:"Dismiss"}]});n.json({success:i.success,sent:i.sent,failed:i.failed,errors:i.errors,message:`Test notification sent to ${i.sent} subscribers`}),it.log(`Test notification sent: ${i.sent} successful, ${i.failed} failed`)}catch(i){it.error("Failed to send test notification:",i),n.status(500).json({error:"Test notification failed",message:"Failed to send test notification"})}}),r.get("/push/status",(s,n)=>{if(!t)return n.status(503).json({error:"Push notifications not initialized",message:"Push notification service is not available"});try{let i=t.getSubscriptions();n.json({enabled:e.isEnabled(),hasVapidKeys:!!e.getPublicKey(),totalSubscriptions:i.length,activeSubscriptions:i.filter(a=>a.isActive).length})}catch(i){it.error("Failed to get push status:",i),n.status(500).json({error:"Status check failed",message:"Failed to retrieve push notification status"})}}),r}le();var to=require("express");L();var ae=k("remotes");function ro(o){let e=(0,to.Router)(),{remoteRegistry:t,isHQMode:r}=o;return e.get("/remotes",(s,n)=>{if(!r||!t)return ae.debug("remotes list requested but not in HQ mode"),n.status(404).json({error:"Not running in HQ mode"});let i=t.getRemotes();ae.debug(`listing ${i.length} registered remotes`);let a=i.map(u=>({...u,sessionIds:Array.from(u.sessionIds)}));n.json(a)}),e.post("/remotes/register",(s,n)=>{if(!r||!t)return ae.debug("remote registration attempted but not in HQ mode"),n.status(404).json({error:"Not running in HQ mode"});let{id:i,name:a,url:u,token:c}=s.body;if(!i||!a||!u||!c)return ae.warn(`remote registration missing required fields: got id=${!!i}, name=${!!a}, url=${!!u}, token=${!!c}`),n.status(400).json({error:"Missing required fields: id, name, url, token"});ae.debug(`attempting to register remote ${a} (${i}) from ${u}`);try{let l=t.register({id:i,name:a,url:u,token:c});ae.log(f.green(`remote registered: ${a} (${i}) from ${u}`)),n.json({success:!0,remote:l})}catch(l){if(l instanceof Error&&l.message.includes("already registered"))return n.status(409).json({error:l.message});ae.error("failed to register remote:",l),n.status(500).json({error:"Failed to register remote"})}}),e.delete("/remotes/:remoteId",(s,n)=>{if(!r||!t)return ae.debug("remote unregistration attempted but not in HQ mode"),n.status(404).json({error:"Not running in HQ mode"});let i=s.params.remoteId;ae.debug(`attempting to unregister remote ${i}`),t.unregister(i)?(ae.log(f.yellow(`remote unregistered: ${i}`)),n.json({success:!0})):(ae.warn(`attempted to unregister non-existent remote: ${i}`),n.status(404).json({error:"Remote not found"}))}),e.post("/remotes/:remoteName/refresh-sessions",async(s,n)=>{if(!r||!t)return ae.debug("session refresh attempted but not in HQ mode"),n.status(404).json({error:"Not running in HQ mode"});if(ke())return ae.debug("session refresh rejected during shutdown"),n.status(503).json({error:"Server is shutting down"});let i=s.params.remoteName,{action:a,sessionId:u}=s.body;ae.debug(`refreshing sessions for remote ${i} (action: ${a}, sessionId: ${u})`);let l=t.getRemotes().find(d=>d.name===i);if(!l)return ae.warn(`remote not found for session refresh: ${i}`),n.status(404).json({error:"Remote not found"});try{let d=Date.now(),g=await fetch(`${l.url}/api/sessions`,{headers:{Authorization:`Bearer ${l.token}`},signal:AbortSignal.timeout(5e3)});if(g.ok){let h=(await g.json()).map(v=>v.id),b=Date.now()-d;t.updateRemoteSessions(l.id,h),ae.log(f.green(`updated sessions for remote ${l.name}: ${h.length} sessions`)),ae.debug(`session refresh completed in ${b}ms (action: ${a}, sessionId: ${u})`),n.json({success:!0,sessionCount:h.length})}else throw new Error(`Failed to fetch sessions: ${g.status}`)}catch(d){if(ke())return ae.log(f.yellow(`remote ${l.name} refresh failed during shutdown (expected)`)),n.status(503).json({error:"Server is shutting down"});ae.error(`failed to refresh sessions for remote ${l.name}:`,d),n.status(500).json({error:"Failed to refresh sessions"})}}),e}var so=require("express"),Ge=x(require("fs/promises")),hs=x(require("os")),We=x(require("path"));L();var Tr=k("repositories");function no(){let o=(0,so.Router)();return o.get("/repositories/discover",async(e,t)=>{try{let r=e.query.path||"~/",s=Number.parseInt(e.query.maxDepth)||3;Tr.debug(`[GET /repositories/discover] Discovering repositories in: ${r}`);let n=Gi(r),i=await Ji({basePath:n,maxDepth:s});Tr.debug(`[GET /repositories/discover] Found ${i.length} repositories`),t.json(i)}catch(r){Tr.error("[GET /repositories/discover] Error discovering repositories:",r),t.status(500).json({error:"Failed to discover repositories"})}}),o}function Gi(o){return o.startsWith("~/")?We.join(hs.homedir(),o.slice(2)):We.isAbsolute(o)?o:We.resolve(o)}async function Ji(o){let{basePath:e,maxDepth:t=3}=o,r=[];async function s(n,i){if(!(i>t))try{await Ge.access(n,Ge.constants.R_OK);let a=await Ge.readdir(n,{withFileTypes:!0});for(let u of a){if(!u.isDirectory()||u.name.startsWith(".")&&u.name!==".git")continue;let c=We.join(n,u.name),l=We.join(c,".git");try{await Ge.stat(l);let d=await Qi(c);r.push(d)}catch{await s(c,i+1)}}}catch(a){Tr.debug(`Cannot access directory ${n}: ${a}`)}}return await s(e,0),r.sort((n,i)=>n.folderName.localeCompare(i.folderName)),r}async function Qi(o){let e=We.basename(o),t=await Ge.stat(o),r=t.mtime.toISOString(),s=hs.homedir(),n=o.startsWith(s)?`~${o.slice(s.length)}`:o;return{id:`${e}-${t.ino}`,path:o,folderName:e,lastModified:r,relativePath:n}}var io=x(require("node:path")),ao=require("express");L();var oo=k("screencap");async function co(){if(process.platform!=="darwin"){oo.log("\u23ED\uFE0F Skipping screencap initialization (macOS only)");return}oo.log("\u2705 Screencap ready via WebSocket API")}function lo(){let o=(0,ao.Router)(),e=(t,r,s)=>{if(process.platform!=="darwin")return r.status(503).json({error:"Screencap is only available on macOS",platform:process.platform});s()};return o.get("/screencap",e,(t,r)=>{r.sendFile(io.join(process.cwd(),"public","screencap.html"))}),o}le();var ho=require("express"),Ts=x(require("fs")),mo=x(require("os")),Zt=x(require("path"));function Yi(o){let e=[];if(o.fg!==void 0)if(o.fg>=0&&o.fg<=255)e.push(`fg="${o.fg}"`);else{let t=o.fg>>16&255,r=o.fg>>8&255,s=o.fg&255;e.push(`fg="${t},${r},${s}"`)}if(o.bg!==void 0)if(o.bg>=0&&o.bg<=255)e.push(`bg="${o.bg}"`);else{let t=o.bg>>16&255,r=o.bg>>8&255,s=o.bg&255;e.push(`bg="${t},${r},${s}"`)}return o.attributes&&(o.attributes&1&&e.push("bold"),o.attributes&2&&e.push("dim"),o.attributes&4&&e.push("italic"),o.attributes&8&&e.push("underline"),o.attributes&16&&e.push("inverse"),o.attributes&32&&e.push("invisible"),o.attributes&64&&e.push("strikethrough")),e.join(" ")}function uo(o,e=!0){let t=[];for(let r of o){let s="";if(e){let n="",i="",a=()=>{i&&(n?s+=`[style ${n}]${i}[/style]`:s+=i,i="")};for(let u of r){let c=Yi(u);c!==n&&(a(),n=c),i+=u.char}a()}else for(let n of r)s+=n.char;t.push(s.trimEnd())}return t.join(` +`)}L();ms();kr();var T=k("sessions");function $s(o,e){return!o||o.trim()===""?e:o.startsWith("~/")?Zt.join(mo.homedir(),o.slice(2)):Zt.isAbsolute(o)?o:Zt.join(e,o)}function yo(o){let e=(0,ho.Router)(),{ptyManager:t,terminalManager:r,streamWatcher:s,remoteRegistry:n,isHQMode:i,activityMonitor:a}=o;return e.get("/server/status",async(u,c)=>{T.debug("[GET /server/status] Getting server status");try{let l={macAppConnected:Be.isMacAppConnected(),isHQMode:i,version:process.env.VERSION||"unknown"};c.json(l)}catch(l){T.error("Failed to get server status:",l),c.status(500).json({error:"Failed to get server status"})}}),e.get("/sessions",async(u,c)=>{T.debug("[GET /sessions] Listing all sessions");try{let l=[],d=t.listSessions();if(T.debug(`[GET /sessions] Found ${d.length} local sessions`),d.forEach(p=>{T.debug(`[GET /sessions] Session ${p.id}: name="${p.name||"null"}", workingDir="${p.workingDir}"`)}),l=[...d.map(p=>({...p,source:"local"}))],i&&n){let p=n.getRemotes();T.debug(`checking ${p.length} remote servers for sessions`);let h=p.map(async $=>{try{let E=await fetch(`${$.url}/api/sessions`,{headers:{Authorization:`Bearer ${$.token}`},signal:AbortSignal.timeout(5e3)});if(E.ok){let w=await E.json();T.debug(`got ${w.length} sessions from remote ${$.name}`);let P=w.map(V=>V.id);return n.updateRemoteSessions($.id,P),w.map(V=>({...V,source:"remote",remoteId:$.id,remoteName:$.name,remoteUrl:$.url}))}else return T.warn(`failed to get sessions from remote ${$.name}: HTTP ${E.status}`),[]}catch(E){return T.error(`failed to get sessions from remote ${$.name}:`,E),[]}}),v=(await Promise.all(h)).flat();T.debug(`total remote sessions: ${v.length}`),l=[...l,...v]}T.debug(`returning ${l.length} total sessions`),c.json(l)}catch(l){T.error("error listing sessions:",l),c.status(500).json({error:"Failed to list sessions"})}}),e.post("/sessions",async(u,c)=>{let{command:l,workingDir:d,name:g,remoteId:p,spawn_terminal:h,cols:b,rows:v,titleMode:$}=u.body;if(T.debug(`creating new session: command=${JSON.stringify(l)}, remoteId=${p||"local"}, spawn_terminal=${h}, cols=${b}, rows=${v}`),!l||!Array.isArray(l)||l.length===0)return T.warn("session creation failed: invalid command array"),c.status(400).json({error:"Command array is required"});try{if(p&&i&&n){let z=n.getRemote(p);if(!z)return T.warn(`session creation failed: remote ${p} not found`),c.status(404).json({error:"Remote server not found"});T.log(f.blue(`forwarding session creation to remote ${z.name}`));let Y=Date.now(),K=await fetch(`${z.url}/api/sessions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${z.token}`},body:JSON.stringify({command:l,workingDir:d,name:g,spawn_terminal:h,cols:b,rows:v,titleMode:$}),signal:AbortSignal.timeout(1e4)});if(!K.ok){let C=await K.json().catch(()=>({error:"Unknown error"}));return c.status(K.status).json(C)}let Te=await K.json();T.debug(`remote session creation took ${Date.now()-Y}ms`),Te.sessionId&&n.addSessionToRemote(z.id,Te.sessionId),c.json(Te);return}if(h)try{let z=Xi(),Y=g||Qt(l,$s(d,process.cwd()));T.log(f.blue(`requesting terminal spawn with command: ${JSON.stringify(l)}`));let K=await Zi({sessionId:z,sessionName:Y,command:l,workingDir:$s(d,process.cwd()),titleMode:$});if(!K.success)T.warn("terminal spawn failed:",K.error||"Unknown error"),T.debug("falling back to normal web session");else{await new Promise(Te=>setTimeout(Te,500)),T.log(f.green(`terminal spawn requested for session ${z}`)),c.json({sessionId:z,message:"Terminal spawn requested"});return}}catch(z){T.error("error spawning terminal:",z),T.debug("falling back to normal web session")}let E=$s(d,process.cwd());Ts.existsSync(E)||(T.warn(`Working directory '${E}' does not exist, using current directory as fallback`),E=process.cwd());let w=g||Qt(l,E);T.log(f.blue(`creating WEB session: ${l.join(" ")} in ${E} (spawn_terminal=${h})`));let P=await t.createSession(l,{name:w,workingDir:E,cols:b,rows:v,titleMode:$}),{sessionId:V,sessionInfo:_}=P;T.log(f.green(`WEB session ${V} created (PID: ${_.pid})`)),c.json({sessionId:V})}catch(E){T.error("error creating session:",E),E instanceof j?c.status(500).json({error:"Failed to create session",details:E.message}):c.status(500).json({error:"Failed to create session"})}}),e.get("/sessions/activity",async(u,c)=>{T.debug("getting activity status for all sessions");try{let l={},d=a.getActivityStatus();if(Object.assign(l,d),i&&n){let p=n.getRemotes().map(async b=>{try{let v=await fetch(`${b.url}/api/sessions/activity`,{headers:{Authorization:`Bearer ${b.token}`},signal:AbortSignal.timeout(5e3)});if(v.ok){let $=await v.json();return{remote:{id:b.id,name:b.name,url:b.url},activity:$}}}catch(v){T.error(`failed to get activity from remote ${b.name}:`,v)}return null}),h=await Promise.all(p);for(let b of h)b?.activity&&Object.assign(l,b.activity)}c.json(l)}catch(l){T.error("error getting activity status:",l),c.status(500).json({error:"Failed to get activity status"})}}),e.get("/sessions/:sessionId/activity",async(u,c)=>{let l=u.params.sessionId;try{if(i&&n){let g=n.getRemoteBySessionId(l);if(g)try{let p=await fetch(`${g.url}/api/sessions/${l}/activity`,{headers:{Authorization:`Bearer ${g.token}`},signal:AbortSignal.timeout(5e3)});return p.ok?c.json(await p.json()):c.status(p.status).json(await p.json())}catch(p){return T.error(`failed to get activity from remote ${g.name}:`,p),c.status(503).json({error:"Failed to reach remote server"})}}let d=a.getSessionActivityStatus(l);if(!d)return c.status(404).json({error:"Session not found"});c.json(d)}catch(d){T.error(`error getting activity status for session ${l}:`,d),c.status(500).json({error:"Failed to get activity status"})}}),e.get("/sessions/:sessionId",async(u,c)=>{let l=u.params.sessionId;T.debug(`getting info for session ${l}`);try{if(i&&n){let g=n.getRemoteBySessionId(l);if(g)try{let p=await fetch(`${g.url}/api/sessions/${l}`,{headers:{Authorization:`Bearer ${g.token}`},signal:AbortSignal.timeout(5e3)});return p.ok?c.json(await p.json()):c.status(p.status).json(await p.json())}catch(p){return T.error(`failed to get session info from remote ${g.name}:`,p),c.status(503).json({error:"Failed to reach remote server"})}}let d=t.getSession(l);if(!d)return c.status(404).json({error:"Session not found"});c.json(d)}catch(d){T.error("error getting session info:",d),c.status(500).json({error:"Failed to get session info"})}}),e.delete("/sessions/:sessionId",async(u,c)=>{let l=u.params.sessionId;T.debug(`killing session ${l}`);try{if(i&&n){let g=n.getRemoteBySessionId(l);if(g)try{let p=await fetch(`${g.url}/api/sessions/${l}`,{method:"DELETE",headers:{Authorization:`Bearer ${g.token}`},signal:AbortSignal.timeout(1e4)});return p.ok?(n.removeSessionFromRemote(l),T.log(f.yellow(`remote session ${l} killed on ${g.name}`)),c.json(await p.json())):c.status(p.status).json(await p.json())}catch(p){return T.error(`failed to kill session on remote ${g.name}:`,p),c.status(503).json({error:"Failed to reach remote server"})}}if(!t.getSession(l))return c.status(404).json({error:"Session not found"});await t.killSession(l,"SIGTERM"),T.log(f.yellow(`local session ${l} killed`)),c.json({success:!0,message:"Session killed"})}catch(d){T.error("error killing session:",d),d instanceof j?c.status(500).json({error:"Failed to kill session",details:d.message}):c.status(500).json({error:"Failed to kill session"})}}),e.delete("/sessions/:sessionId/cleanup",async(u,c)=>{let l=u.params.sessionId;T.debug(`cleaning up session ${l} files`);try{if(i&&n){let d=n.getRemoteBySessionId(l);if(d)try{let g=await fetch(`${d.url}/api/sessions/${l}/cleanup`,{method:"DELETE",headers:{Authorization:`Bearer ${d.token}`},signal:AbortSignal.timeout(1e4)});return g.ok?(n.removeSessionFromRemote(l),T.log(f.yellow(`remote session ${l} cleaned up on ${d.name}`)),c.json(await g.json())):c.status(g.status).json(await g.json())}catch(g){return T.error(`failed to cleanup session on remote ${d.name}:`,g),c.status(503).json({error:"Failed to reach remote server"})}}t.cleanupSession(l),T.log(f.yellow(`local session ${l} cleaned up`)),c.json({success:!0,message:"Session cleaned up"})}catch(d){T.error("error cleaning up session:",d),d instanceof j?c.status(500).json({error:"Failed to cleanup session",details:d.message}):c.status(500).json({error:"Failed to cleanup session"})}}),e.post("/cleanup-exited",async(u,c)=>{T.log(f.blue("cleaning up all exited sessions"));try{let l=t.cleanupExitedSessions();if(T.log(f.green(`cleaned up ${l.length} local exited sessions`)),i&&n)for(let p of l)n.removeSessionFromRemote(p);let d=l.length,g=[];if(i&&n){let h=n.getRemotes().map(async b=>{try{let v=await fetch(`${b.url}/api/cleanup-exited`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${b.token}`},signal:AbortSignal.timeout(1e4)});if(v.ok){let E=(await v.json()).cleanedSessions||[],w=E.length;d+=w;for(let P of E)n.removeSessionFromRemote(P);g.push({remoteName:b.name,cleaned:w})}else throw new Error(`HTTP ${v.status}`)}catch(v){T.error(`failed to cleanup sessions on remote ${b.name}:`,v),g.push({remoteName:b.name,cleaned:0,error:v instanceof Error?v.message:"Unknown error"})}});await Promise.all(h)}c.json({success:!0,message:`${d} exited sessions cleaned up across all servers`,localCleaned:l.length,remoteResults:g})}catch(l){T.error("error cleaning up exited sessions:",l),l instanceof j?c.status(500).json({error:"Failed to cleanup exited sessions",details:l.message}):c.status(500).json({error:"Failed to cleanup exited sessions"})}}),e.get("/sessions/:sessionId/text",async(u,c)=>{let l=u.params.sessionId,d=u.query.styles!==void 0;T.debug(`getting plain text for session ${l}, styles=${d}`);try{if(i&&n){let b=n.getRemoteBySessionId(l);if(b)try{let v=new URL(`${b.url}/api/sessions/${l}/text`);d&&v.searchParams.set("styles","");let $=await fetch(v.toString(),{headers:{Authorization:`Bearer ${b.token}`},signal:AbortSignal.timeout(5e3)});if(!$.ok)return c.status($.status).json(await $.json());let E=await $.text();return c.setHeader("Content-Type","text/plain"),c.send(E)}catch(v){return T.error(`failed to get text from remote ${b.name}:`,v),c.status(503).json({error:"Failed to reach remote server"})}}if(!t.getSession(l))return c.status(404).json({error:"Session not found"});let p=await r.getBufferSnapshot(l),h=uo(p.cells,d);c.setHeader("Content-Type","text/plain"),c.send(h)}catch(g){T.error("error getting plain text:",g),c.status(500).json({error:"Failed to get terminal text"})}}),e.get("/sessions/:sessionId/buffer",async(u,c)=>{let l=u.params.sessionId;T.debug(`client requesting buffer for session ${l}`);try{if(i&&n){let h=n.getRemoteBySessionId(l);if(h)try{let b=await fetch(`${h.url}/api/sessions/${l}/buffer`,{headers:{Authorization:`Bearer ${h.token}`},signal:AbortSignal.timeout(5e3)});if(!b.ok)return c.status(b.status).json(await b.json());let v=await b.arrayBuffer();return c.setHeader("Content-Type","application/octet-stream"),c.send(Buffer.from(v))}catch(b){return T.error(`failed to get buffer from remote ${h.name}:`,b),c.status(503).json({error:"Failed to reach remote server"})}}if(!t.getSession(l))return T.error(`session ${l} not found`),c.status(404).json({error:"Session not found"});let g=await r.getBufferSnapshot(l),p=r.encodeSnapshot(g);T.debug(`sending buffer for session ${l}: ${p.length} bytes, dimensions: ${g.cols}x${g.rows}, cursor: (${g.cursorX},${g.cursorY})`),c.setHeader("Content-Type","application/octet-stream"),c.send(p)}catch(d){T.error("error getting buffer:",d),c.status(500).json({error:"Failed to get terminal buffer"})}}),e.get("/sessions/:sessionId/stream",async(u,c)=>{let l=u.params.sessionId,d=Date.now();if(T.log(f.blue(`new SSE client connected to session ${l} from ${u.get("User-Agent")?.substring(0,50)||"unknown"}`)),i&&n){let E=n.getRemoteBySessionId(l);if(E)try{let w=new AbortController,P=await fetch(`${E.url}/api/sessions/${l}/stream`,{headers:{Authorization:`Bearer ${E.token}`,Accept:"text/event-stream"},signal:w.signal});if(!P.ok)return c.status(P.status).json(await P.json());c.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"Cache-Control","X-Accel-Buffering":"no"});let V=P.body?.getReader();if(!V)throw new Error("No response body");let _=new TextDecoder,z={count:0};(async()=>{try{for(;;){let{done:K,value:Te}=await V.read();if(K)break;z.count+=Te.length;let C=_.decode(Te,{stream:!0});c.write(C)}}catch(K){T.error(`stream proxy error for remote ${E.name}:`,K)}})(),u.on("close",()=>{T.log(f.yellow(`SSE client disconnected from remote session ${l} (proxied ${z.count} bytes)`)),w.abort()});return}catch(w){return T.error(`failed to stream from remote ${E.name}:`,w),c.status(503).json({error:"Failed to reach remote server"})}}if(!t.getSession(l))return c.status(404).json({error:"Session not found"});let p=t.getSessionPaths(l);if(!p)return c.status(404).json({error:"Session paths not found"});let h=p.stdoutPath;if(!h||!Ts.existsSync(h))return T.warn(`stream path not found for session ${l}`),c.status(404).json({error:"Session stream not found"});c.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"Cache-Control","X-Accel-Buffering":"no","Content-Encoding":"identity"}),c.flushHeaders(),c.write(`:ok + +`),c.flush&&c.flush(),s.addClient(l,h,c),T.debug(`SSE stream setup completed in ${Date.now()-d}ms`);let b=setInterval(()=>{c.write(`:heartbeat + +`),c.flush&&c.flush()},3e4),v=!1,$=()=>{v||(v=!0,T.log(f.yellow(`SSE client disconnected from session ${l}`)),s.removeClient(l,c),clearInterval(b))};u.on("close",$),u.on("error",E=>{T.error(`SSE client error for session ${l}:`,E),$()}),c.on("close",$),c.on("finish",$)}),e.post("/sessions/:sessionId/input",async(u,c)=>{let l=u.params.sessionId,{text:d,key:g}=u.body;if(d===void 0&&g===void 0||d!==void 0&&g!==void 0)return T.warn(`invalid input request for session ${l}: both or neither text/key provided`),c.status(400).json({error:"Either text or key must be provided, but not both"});if(d!==void 0&&typeof d!="string")return T.warn(`invalid input request for session ${l}: text is not a string`),c.status(400).json({error:"Text must be a string"});if(g!==void 0&&typeof g!="string")return T.warn(`invalid input request for session ${l}: key is not a string`),c.status(400).json({error:"Key must be a string"});try{if(i&&n){let b=n.getRemoteBySessionId(l);if(b)try{let v=await fetch(`${b.url}/api/sessions/${l}/input`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${b.token}`},body:JSON.stringify(u.body),signal:AbortSignal.timeout(5e3)});return v.ok?c.json(await v.json()):c.status(v.status).json(await v.json())}catch(v){return T.error(`failed to send input to remote ${b.name}:`,v),c.status(503).json({error:"Failed to reach remote server"})}}let p=t.getSession(l);if(!p)return T.error(`session ${l} not found for input`),c.status(404).json({error:"Session not found"});if(p.status!=="running")return T.error(`session ${l} is not running (status: ${p.status})`),c.status(400).json({error:"Session is not running"});let h=d!==void 0?{text:d}:{key:g};T.debug(`sending input to session ${l}: ${JSON.stringify(h)}`),t.sendInput(l,h),c.json({success:!0})}catch(p){T.error("error sending input:",p),p instanceof j?c.status(500).json({error:"Failed to send input",details:p.message}):c.status(500).json({error:"Failed to send input"})}}),e.post("/sessions/:sessionId/resize",async(u,c)=>{let l=u.params.sessionId,{cols:d,rows:g}=u.body;if(typeof d!="number"||typeof g!="number")return T.warn(`invalid resize request for session ${l}: cols/rows not numbers`),c.status(400).json({error:"Cols and rows must be numbers"});if(d<1||g<1||d>1e3||g>1e3)return T.warn(`invalid resize request for session ${l}: cols=${d}, rows=${g} out of range`),c.status(400).json({error:"Cols and rows must be between 1 and 1000"});T.log(f.blue(`resizing session ${l} to ${d}x${g}`));try{if(i&&n){let h=n.getRemoteBySessionId(l);if(h)try{let b=await fetch(`${h.url}/api/sessions/${l}/resize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${h.token}`},body:JSON.stringify({cols:d,rows:g}),signal:AbortSignal.timeout(5e3)});return b.ok?c.json(await b.json()):c.status(b.status).json(await b.json())}catch(b){return T.error(`failed to resize session on remote ${h.name}:`,b),c.status(503).json({error:"Failed to reach remote server"})}}let p=t.getSession(l);if(!p)return T.warn(`session ${l} not found for resize`),c.status(404).json({error:"Session not found"});if(p.status!=="running")return T.warn(`session ${l} is not running (status: ${p.status})`),c.status(400).json({error:"Session is not running"});t.resizeSession(l,d,g),T.log(f.green(`session ${l} resized to ${d}x${g}`)),c.json({success:!0,cols:d,rows:g})}catch(p){T.error("error resizing session via PTY service:",p),p instanceof j?c.status(500).json({error:"Failed to resize session",details:p.message}):c.status(500).json({error:"Failed to resize session"})}}),e.patch("/sessions/:sessionId",async(u,c)=>{let l=u.params.sessionId;T.log(f.yellow(`[PATCH] Received rename request for session ${l}`)),T.debug("[PATCH] Request body:",u.body),T.debug("[PATCH] Request headers:",u.headers);let{name:d}=u.body;if(typeof d!="string"||d.trim()==="")return T.warn(`[PATCH] Invalid name provided: ${JSON.stringify(d)}`),c.status(400).json({error:"Name must be a non-empty string"});T.log(f.blue(`[PATCH] Updating session ${l} name to: ${d}`));try{if(i&&n){let h=n.getRemoteBySessionId(l);if(h)try{let b=await fetch(`${h.url}/api/sessions/${l}`,{method:"PATCH",headers:{"Content-Type":"application/json",Authorization:`Bearer ${h.token}`},body:JSON.stringify({name:d}),signal:AbortSignal.timeout(5e3)});return b.ok?c.json(await b.json()):c.status(b.status).json(await b.json())}catch(b){return T.error(`failed to update session name on remote ${h.name}:`,b),c.status(503).json({error:"Failed to reach remote server"})}}T.debug("[PATCH] Handling local session update");let g=t.getSession(l);if(!g)return T.warn(`[PATCH] Session ${l} not found for name update`),c.status(404).json({error:"Session not found"});T.debug(`[PATCH] Found session: ${JSON.stringify(g)}`),T.debug(`[PATCH] Calling ptyManager.updateSessionName(${l}, ${d})`);let p=t.updateSessionName(l,d);T.log(f.green(`[PATCH] Session ${l} name updated to: ${p}`)),c.json({success:!0,name:p})}catch(g){T.error("error updating session name:",g),g instanceof j?c.status(500).json({error:"Failed to update session name",details:g.message}):c.status(500).json({error:"Failed to update session name"})}}),e.post("/sessions/:sessionId/reset-size",async(u,c)=>{let{sessionId:l}=u.params;try{if(n){let g=n.getRemoteBySessionId(l);if(g){T.debug(`forwarding reset-size to remote ${g.id}`);let p=await fetch(`${g.url}/api/sessions/${l}/reset-size`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g.token}`}});if(!p.ok){let b=await p.json();return c.status(p.status).json(b)}let h=await p.json();return c.json(h)}}T.log(f.cyan(`resetting terminal size for session ${l}`));let d=t.getSession(l);if(!d)return T.error(`session ${l} not found for reset-size`),c.status(404).json({error:"Session not found"});if(d.status!=="running")return T.error(`session ${l} is not running (status: ${d.status})`),c.status(400).json({error:"Session is not running"});t.resetSessionSize(l),T.log(f.green(`session ${l} size reset to terminal size`)),c.json({success:!0})}catch(d){T.error("error resetting session size via PTY service:",d),d instanceof j?c.status(500).json({error:"Failed to reset session size",details:d.message}):c.status(500).json({error:"Failed to reset session size"})}}),e}function Xi(){let o=new Uint8Array(16);for(let t=0;t<16;t++)o[t]=Math.floor(Math.random()*256);o[6]=o[6]&15|64,o[8]=o[8]&63|128;let e=Array.from(o,t=>t.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-")}async function Zi(o){try{let e=Er("terminal","spawn",{sessionId:o.sessionId,workingDirectory:o.workingDir,command:o.command.join(" "),terminalPreference:null},o.sessionId);T.debug(`requesting terminal spawn via control socket for session ${o.sessionId}`);let t=await Be.sendControlMessage(e);if(!t)return{success:!1,error:"No response from Mac app"};if(t.error)return{success:!1,error:t.error};let r=t.payload?.success===!0;return{success:r,error:r?void 0:"Terminal spawn failed"}}catch(e){return T.error("failed to spawn terminal:",e),{success:!1,error:e instanceof Error?e.message:"Unknown error"}}}var ea=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"},{urls:"stun:stun2.l.google.com:19302"},{urls:"stun:stun3.l.google.com:19302"},{urls:"stun:stun4.l.google.com:19302"}];function bo(){let o={iceServers:[...ea],bundlePolicy:"max-bundle",rtcpMuxPolicy:"require",iceCandidatePoolSize:0};if(typeof globalThis<"u"&&"window"in globalThis){let t=globalThis.window.__WEBRTC_CONFIG__;t?.iceServers&&(o.iceServers=t.iceServers)}if(typeof process<"u"&&process.env){if(process.env.TURN_SERVER_URL){let e={urls:process.env.TURN_SERVER_URL};process.env.TURN_USERNAME&&(e.username=process.env.TURN_USERNAME),process.env.TURN_CREDENTIAL&&(e.credential=process.env.TURN_CREDENTIAL),o.iceServers.push(e)}if(process.env.ADDITIONAL_STUN_SERVERS){let e=process.env.ADDITIONAL_STUN_SERVERS.split(",").map(t=>({urls:t.trim()}));o.iceServers.push(...e)}process.env.ICE_TRANSPORT_POLICY==="relay"&&(o.iceTransportPolicy="relay")}return o}function So(o){return o.iceServers.map(t=>{let r=Array.isArray(t.urls)?t.urls:[t.urls],s=r[0].startsWith("turn:")?"TURN":"STUN",n=t.username&&t.credential;return`${s}: ${r[0]}${n?" (authenticated)":""}`}).join(` +`)}L();var vo=k("webrtc-config");function wo(){let o=require("express").Router();return o.get("/webrtc-config",(e,t)=>{try{let r=bo();vo.log(`Serving WebRTC configuration: +${So(r)}`),t.json({success:!0,config:r})}catch(r){vo.error("Failed to get WebRTC config:",r),t.status(500).json({success:!1,error:"Failed to get WebRTC configuration"})}}),o}L();var ge=k("websocket-input"),Pr=class{ptyManager;terminalManager;activityMonitor;remoteRegistry;authService;isHQMode;remoteConnections=new Map;constructor(e){this.ptyManager=e.ptyManager,this.terminalManager=e.terminalManager,this.activityMonitor=e.activityMonitor,this.remoteRegistry=e.remoteRegistry,this.authService=e.authService,this.isHQMode=e.isHQMode}async connectToRemote(e,t,r){let n=`${e.replace(/^https?:/,a=>a==="https:"?"wss:":"ws:")}/ws/input?sessionId=${t}&token=${encodeURIComponent(r)}`;ge.log(`Establishing proxy connection to remote: ${n}`);let i=new WebSocket(n);return new Promise((a,u)=>{let c=setTimeout(()=>{i.close(),u(new Error("Remote WebSocket connection timeout"))},5e3);i.addEventListener("open",()=>{clearTimeout(c),ge.log(`Remote WebSocket proxy established for session ${t}`),a(i)}),i.addEventListener("error",l=>{clearTimeout(c),ge.error(`Remote WebSocket error for session ${t}:`,l),u(l)})})}async handleConnection(e,t,r){ge.log(`WebSocket input connection established for session ${t}, user ${r}`);let s=null;if(this.isHQMode&&this.remoteRegistry){let n=this.remoteRegistry.getRemoteBySessionId(t);if(n){ge.log(`Session ${t} is on remote ${n.name}, establishing proxy connection`);try{s=await this.connectToRemote(n.url,t,n.token),this.remoteConnections.set(t,s),s.addEventListener("close",()=>{ge.log(`Remote WebSocket closed for session ${t}`),this.remoteConnections.delete(t),e.close()}),s.addEventListener("error",i=>{ge.error(`Remote WebSocket error for session ${t}:`,i),this.remoteConnections.delete(t),e.close()})}catch(i){ge.error(`Failed to establish proxy connection to remote for session ${t}:`,i),e.close();return}}}e.on("message",n=>{try{if(s&&s.readyState===WebSocket.OPEN){n instanceof Buffer?s.send(n):Array.isArray(n)?s.send(Buffer.concat(n)):s.send(n);return}let i=n.toString();if(!i)return;try{let a;if(ge.debug(`Raw WebSocket input: ${JSON.stringify(i)} (length: ${i.length})`),i.startsWith("\0")&&i.endsWith("\0")&&i.length>2){let u=i.slice(1,-1);ge.debug(`Detected special key: "${u}"`),a={key:u},ge.debug(`Mapped to special key: ${JSON.stringify(a)}`)}else a={text:i},ge.debug(`Regular text input: ${JSON.stringify(a)}`);ge.debug(`Sending to PTY manager: ${JSON.stringify(a)}`),this.ptyManager.sendInput(t,a)}catch(a){ge.warn(`Failed to send input to session ${t}:`,a)}}catch(i){ge.error("Error processing WebSocket input message:",i)}}),e.on("close",()=>{ge.log(`WebSocket input connection closed for session ${t}`),s&&(s.close(),this.remoteConnections.delete(t))}),e.on("error",n=>{ge.error(`WebSocket input error for session ${t}:`,n)})}};le();var H=x(require("fs")),Qe=x(require("path"));L();var te=k("activity-monitor"),Cr=class{controlPath;activities=new Map;watchers=new Map;checkInterval=null;ACTIVITY_TIMEOUT=500;CHECK_INTERVAL=100;constructor(e){this.controlPath=e}start(){te.log(f.green("activity monitor started"));let e=this.scanSessions();e>0&&te.log(f.blue(`monitoring ${e} existing sessions`)),this.checkInterval=setInterval(()=>{this.scanSessions(),this.updateActivityStates()},this.CHECK_INTERVAL)}stop(){te.log(f.yellow("stopping activity monitor")),this.checkInterval&&(clearInterval(this.checkInterval),this.checkInterval=null);let e=this.watchers.size;for(let[t,r]of this.watchers)r.close(),this.watchers.delete(t);this.activities.clear(),e>0&&te.log(f.gray(`closed ${e} file watchers`))}scanSessions(){try{if(!H.existsSync(this.controlPath))return 0;let e=H.readdirSync(this.controlPath,{withFileTypes:!0}),t=0;for(let s of e)if(s.isDirectory()){let n=s.name;if(this.activities.has(n))continue;let i=Qe.join(this.controlPath,n,"stdout");H.existsSync(i)&&this.startMonitoringSession(n,i)&&t++}let r=[];for(let[s,n]of this.activities){let i=Qe.join(this.controlPath,s);H.existsSync(i)||r.push(s)}if(r.length>0){te.log(f.yellow(`cleaning up ${r.length} removed sessions`));for(let s of r)this.stopMonitoringSession(s)}return t}catch(e){return te.error("failed to scan sessions:",e),0}}startMonitoringSession(e,t){try{let r=H.statSync(t);this.activities.set(e,{sessionId:e,isActive:!1,lastActivityTime:Date.now(),lastFileSize:r.size});let s=H.watch(t,n=>{n==="change"&&this.handleFileChange(e,t)});return this.watchers.set(e,s),te.debug(`started monitoring session ${e}`),!0}catch(r){return te.error(`failed to start monitor for session ${e}:`,r),!1}}stopMonitoringSession(e){let t=this.watchers.get(e);t&&(t.close(),this.watchers.delete(e)),this.activities.delete(e),te.debug(`stopped monitoring session ${e}`)}handleFileChange(e,t){try{let r=this.activities.get(e);if(!r)return;if(!H.existsSync(t)){this.stopMonitoringSession(e);return}let s=H.statSync(t);if(s.size>r.lastFileSize){let n=r.isActive;r.isActive=!0,r.lastActivityTime=Date.now(),r.lastFileSize=s.size,n||te.debug(`session ${e} became active`),this.writeActivityStatus(e,!0)}}catch(r){r.code==="ENOENT"?this.stopMonitoringSession(e):te.error(`failed to handle file change for session ${e}:`,r)}}updateActivityStates(){let e=Date.now();for(let[t,r]of this.activities)r.isActive&&e-r.lastActivityTime>this.ACTIVITY_TIMEOUT&&(r.isActive=!1,te.debug(`session ${t} became inactive`),this.writeActivityStatus(t,!1))}writeActivityStatus(e,t){try{let r=Qe.join(this.controlPath,e,"activity.json"),s=Qe.join(this.controlPath,e,"session.json"),n={isActive:t,timestamp:new Date().toISOString()};if(H.existsSync(s))try{let i=JSON.parse(H.readFileSync(s,"utf8"));n.session=i}catch{te.debug(`could not read session.json for ${e}`)}H.writeFileSync(r,JSON.stringify(n,null,2))}catch(r){te.error(`failed to write activity status for session ${e}:`,r)}}getActivityStatus(){let e={},t=Date.now();try{if(!H.existsSync(this.controlPath))return e;let r=H.readdirSync(this.controlPath,{withFileTypes:!0});for(let n of r)if(n.isDirectory()){let i=n.name,a=Qe.join(this.controlPath,i,"activity.json"),u=Qe.join(this.controlPath,i,"session.json");if(H.existsSync(a))try{let c=JSON.parse(H.readFileSync(a,"utf8"));e[i]=c}catch{te.debug(`could not read activity.json for ${i}`);let l=this.activities.get(i);if(l){let d={isActive:l.isActive,timestamp:new Date().toISOString()};if(H.existsSync(u))try{let g=JSON.parse(H.readFileSync(u,"utf8"));d.session=g}catch{te.debug(`could not read session.json for ${i} when creating activity`)}e[i]=d}}else if(H.existsSync(u))try{let c=JSON.parse(H.readFileSync(u,"utf8"));e[i]={isActive:!1,timestamp:new Date().toISOString(),session:c}}catch{te.debug(`could not read session.json for ${i}`)}}let s=Date.now()-t;s>100&&te.warn(`activity status scan took ${s}ms for ${Object.keys(e).length} sessions`)}catch(r){te.error("failed to read activity status:",r)}return e}getSessionActivityStatus(e){let t=Qe.join(this.controlPath,e,"session.json");try{let r=Qe.join(this.controlPath,e,"activity.json");if(H.existsSync(r))return JSON.parse(H.readFileSync(r,"utf8"))}catch{te.debug(`could not read activity.json for session ${e}, creating from current state`);let s=this.activities.get(e);if(s){let n={isActive:s.isActive,timestamp:new Date().toISOString()};if(H.existsSync(t))try{let i=JSON.parse(H.readFileSync(t,"utf8"));n.session=i}catch{te.debug(`could not read session.json for ${e} in getSessionActivityStatus`)}return n}}if(H.existsSync(t))try{let r=JSON.parse(H.readFileSync(t,"utf8"));return{isActive:!1,timestamp:new Date().toISOString(),session:r}}catch{te.debug(`could not read session.json for ${e} when creating default activity`)}return null}};var Ye=x(require("crypto")),Ar=x(require("jsonwebtoken"));var er=x(require("fs")),bt=x(require("path"));function $o(o){let e={exports:{}};return process.dlopen(e,o),e.exports}var yt,xo=bt.dirname(process.execPath),To=bt.join(xo,"authenticate_pam.node"),Eo=bt.join(xo,"native","authenticate_pam.node");if(er.existsSync(To)||er.existsSync(Eo)){let o=[To,Eo,bt.join(__dirname,"..","..","..","native","authenticate_pam.node")],e=!1;for(let t of o)if(er.existsSync(t))try{let r=$o(t);if(r.authenticate)yt=r.authenticate;else throw new Error("Module does not export authenticate function");e=!0;break}catch{}e||(console.warn("Warning: authenticate-pam native module not found. PAM authentication will not work."),yt=(t,r,s)=>{s(new Error("PAM authentication not available"))})}else{let o=!1;try{let e=require("authenticate-pam");yt=e.authenticate||e.default||e,o=!0}catch{}if(!o){let e=bt.join(__dirname,"..","..","..","optional-modules","authenticate-pam","build","Release","authenticate_pam.node");if(er.existsSync(e))try{let t=$o(e);t.authenticate&&(yt=t.authenticate,o=!0,console.log("Loaded authenticate-pam from optional-modules location"))}catch{}}o||(console.warn("Warning: authenticate-pam native module not found. PAM authentication will not work."),yt=(e,t,r)=>{r(new Error("PAM authentication not available"))})}var Ir=class{challenges=new Map;jwtSecret;challengeTimeout=5*60*1e3;constructor(){this.jwtSecret=process.env.JWT_SECRET||this.generateSecret(),setInterval(()=>this.cleanupExpiredChallenges(),6e4)}generateSecret(){return Ye.randomBytes(64).toString("hex")}cleanupExpiredChallenges(){let e=Date.now();for(let[t,r]of this.challenges.entries())e-r.timestamp>this.challengeTimeout&&this.challenges.delete(t)}async authenticateWithSSHKey(e){try{let t=this.challenges.get(e.challengeId);if(!t)return{success:!1,error:"Invalid or expired challenge"};let r=Buffer.from(e.signature,"base64");if(!this.verifySSHSignature(t.challenge,r,e.publicKey))return{success:!1,error:"Invalid SSH key signature"};if(!await this.checkSSHKeyAuthorization(t.userId,e.publicKey))return{success:!1,error:"SSH key not authorized for this user"};this.challenges.delete(e.challengeId);let i=this.generateToken(t.userId);return{success:!0,userId:t.userId,token:i}}catch(t){return console.error("SSH key authentication error:",t),{success:!1,error:"SSH key authentication failed"}}}async authenticateWithPassword(e,t){try{let r=process.env.VIBETUNNEL_USERNAME,s=process.env.VIBETUNNEL_PASSWORD;if(r&&s)if(e===r&&t===s){let a=this.generateToken(e);return{success:!0,userId:e,token:a}}else return{success:!1,error:"Invalid username or password"};if(!await this.verifyPAMCredentials(e,t))return{success:!1,error:"Invalid username or password"};let i=this.generateToken(e);return{success:!0,userId:e,token:i}}catch(r){return console.error("PAM authentication error:",r),{success:!1,error:"Authentication failed"}}}createChallenge(e){let t=Ye.randomUUID(),r=Ye.randomBytes(32);return this.challenges.set(t,{challengeId:t,challenge:r,timestamp:Date.now(),userId:e}),{challengeId:t,challenge:r.toString("base64")}}verifyToken(e){try{return{valid:!0,userId:Ar.verify(e,this.jwtSecret).userId}}catch{return{valid:!1}}}generateToken(e){return Ar.sign({userId:e,iat:Math.floor(Date.now()/1e3)},this.jwtSecret,{expiresIn:"24h"})}async verifyPAMCredentials(e,t){return new Promise(r=>{yt(e,t,s=>{s?(console.error("PAM authentication failed:",s.message),r(!1)):r(!0)})})}verifySSHSignature(e,t,r){try{if(!e||!t||!r)return console.error("Missing required parameters for signature verification"),!1;let s=r.trim().split(" ");if(s.length<2)return console.error("Invalid SSH public key format"),!1;let n=s[0],i=s[1];if(n==="ssh-ed25519"){if(t.length!==64)return console.error(`Invalid Ed25519 signature length: ${t.length} (expected 64)`),!1;let a=Buffer.from(i,"base64"),u=0,c=a.readUInt32BE(u);u+=4+c;let l=a.readUInt32BE(u);if(u+=4,l!==32)return console.error(`Invalid Ed25519 key length: ${l} (expected 32)`),!1;let d=a.subarray(u,u+32),g=Ye.createPublicKey({key:Buffer.concat([Buffer.from([48,42]),Buffer.from([48,5]),Buffer.from([6,3,43,101,112]),Buffer.from([3,33,0]),d]),format:"der",type:"spki"}),p=Ye.verify(null,e,g,t);return console.log(`\u{1F510} Ed25519 signature verification: ${p?"PASSED":"FAILED"}`),p}return console.error(`Unsupported key type: ${n}`),!1}catch(s){return console.error("SSH signature verification failed:",s),!1}}async checkSSHKeyAuthorization(e,t){try{let r=require("os"),s=require("fs"),n=require("path"),i=e===process.env.USER?r.homedir():`/home/${e}`,a=n.join(i,".ssh","authorized_keys");if(!s.existsSync(a))return!1;let u=s.readFileSync(a,"utf8"),c=t.trim().split(" "),l=c.length>1?c[1]:c[0];return u.includes(l)}catch(r){return console.error("Error checking SSH key authorization:",r),!1}}getCurrentUser(){return process.env.USER||process.env.USERNAME||"unknown"}async userExists(e){try{let{spawnSync:t}=require("child_process");return t("id",[e],{stdio:"ignore"}).status===0}catch{return!1}}};L();var Xe=k("bell-event-handler"),Rr=class{pushNotificationService=null;constructor(){Xe.debug("BellEventHandler initialized")}setPushNotificationService(e){this.pushNotificationService=e,Xe.debug("Push notification service configured")}async processBellEvent(e){try{if(Xe.debug("Processing bell event",{sessionId:e.sessionInfo.id,timestamp:e.timestamp.toISOString()}),this.pushNotificationService){let t=this.createNotificationPayload(e);await this.sendPushNotification(t)}Xe.debug("Bell event processed successfully",{sessionId:e.sessionInfo.id})}catch(t){Xe.error("Error processing bell event",{sessionId:e.sessionInfo.id,error:t instanceof Error?t.message:String(t)})}}createNotificationPayload(e){let t=e.sessionInfo.name||"Terminal Session",r=e.suspectedSource?dt.extractProcessName(e.suspectedSource.command):null,s=dt.getProcessDescription(e.suspectedSource||null),n="\u{1F514} Terminal Activity",i=r&&r!=="shell"?`${s} in ${t} triggered a bell`:`${t} triggered a bell`,a=`vibetunnel-bell-${e.sessionInfo.id}`;return{type:"bell-event",sessionId:e.sessionInfo.id,sessionName:t,title:n,body:i,icon:"/apple-touch-icon.png",badge:"/favicon-32.png",tag:a,requireInteraction:!1,actions:[{action:"view-session",title:"View Session"},{action:"dismiss",title:"Dismiss"}],data:{sessionId:e.sessionInfo.id,timestamp:e.timestamp.toISOString(),processName:r||void 0,processCommand:e.suspectedSource?.command||void 0,processPid:e.suspectedSource?.pid||void 0}}}async sendPushNotification(e){if(!this.pushNotificationService){Xe.debug("No push notification service configured");return}try{await this.pushNotificationService.sendBellNotification(e),Xe.debug("Push notification sent",{sessionId:e.sessionId,title:e.title})}catch(t){Xe.error("Failed to send push notification",{sessionId:e.sessionId,error:t instanceof Error?t.message:String(t)})}}dispose(){Xe.debug("BellEventHandler disposed")}};le();var St=require("ws");L();var M=k("buffer-aggregator"),Nr=class{config;remoteConnections=new Map;clientSubscriptions=new Map;constructor(e){this.config=e,M.log(`BufferAggregator initialized (HQ mode: ${e.isHQMode})`)}async handleClientConnection(e){M.log(f.blue("New client connected"));let t=`client-${Date.now()}`;M.debug(`Assigned client ID: ${t}`),this.clientSubscriptions.set(e,new Map),e.send(JSON.stringify({type:"connected",version:"1.0"})),M.debug("Sent welcome message to client"),e.on("message",async r=>{try{let s=JSON.parse(r.toString());await this.handleClientMessage(e,s)}catch(s){M.error("Error handling client message:",s),e.send(JSON.stringify({type:"error",message:"Invalid message format"}))}}),e.on("close",()=>{this.handleClientDisconnect(e)}),e.on("error",r=>{M.error("Client WebSocket error:",r)})}async handleClientMessage(e,t){let r=this.clientSubscriptions.get(e);if(r)if(t.type==="subscribe"&&t.sessionId){let s=t.sessionId;if(r.has(s)){let i=r.get(s);i&&i(),r.delete(s)}let n=this.config.isHQMode&&this.config.remoteRegistry&&this.config.remoteRegistry.getRemoteBySessionId(s);n?(M.debug(`Subscribing to remote session ${s} on remote ${n.id}`),await this.subscribeToRemoteSession(e,s,n.id)):(M.debug(`Subscribing to local session ${s}`),await this.subscribeToLocalSession(e,s)),e.send(JSON.stringify({type:"subscribed",sessionId:s})),M.log(f.green(`Client subscribed to session ${s}`))}else if(t.type==="unsubscribe"&&t.sessionId){let s=t.sessionId,n=r.get(s);if(n&&(n(),r.delete(s),M.log(f.yellow(`Client unsubscribed from session ${s}`))),this.config.isHQMode&&this.config.remoteRegistry){let i=this.config.remoteRegistry.getRemoteBySessionId(s);if(i){let a=this.remoteConnections.get(i.id);a&&(a.subscriptions.delete(s),a.ws.readyState===St.WebSocket.OPEN?(a.ws.send(JSON.stringify({type:"unsubscribe",sessionId:s})),M.debug(`Sent unsubscribe request to remote ${a.remoteName} for session ${s}`)):M.debug(`Cannot unsubscribe from remote ${a.remoteName} - WebSocket not open`))}}}else t.type==="ping"&&e.send(JSON.stringify({type:"pong",timestamp:Date.now()}))}async subscribeToLocalSession(e,t){let r=this.clientSubscriptions.get(e);if(r)try{let s=await this.config.terminalManager.subscribeToBufferChanges(t,(d,g)=>{try{let p=this.config.terminalManager.encodeSnapshot(g),h=Buffer.from(d,"utf8"),b=5+h.length+p.length,v=Buffer.allocUnsafe(b),$=0;v.writeUInt8(191,$),$+=1,v.writeUInt32LE(h.length,$),$+=4,h.copy(v,$),$+=h.length,p.copy(v,$),e.readyState===St.WebSocket.OPEN?e.send(v):M.debug("Skipping buffer update - client WebSocket not open")}catch(p){M.error("Error encoding buffer update:",p)}});r.set(t,s),M.debug(`Created subscription for local session ${t}`),M.debug(`Sending initial buffer for session ${t}`);let n=await this.config.terminalManager.getBufferSnapshot(t),i=this.config.terminalManager.encodeSnapshot(n),a=Buffer.from(t,"utf8"),u=5+a.length+i.length,c=Buffer.allocUnsafe(u),l=0;c.writeUInt8(191,l),l+=1,c.writeUInt32LE(a.length,l),l+=4,a.copy(c,l),l+=a.length,i.copy(c,l),e.readyState===St.WebSocket.OPEN?(e.send(c),M.debug(`Sent initial buffer (${c.length} bytes) for session ${t}`)):M.warn("Cannot send initial buffer - client WebSocket not open")}catch(s){M.error(`Error subscribing to local session ${t}:`,s),e.send(JSON.stringify({type:"error",message:"Failed to subscribe to session"}))}}async subscribeToRemoteSession(e,t,r){let s=this.remoteConnections.get(r);if(!s||s.ws.readyState!==St.WebSocket.OPEN){if(M.debug(`No active connection to remote ${r}, establishing new connection`),!await this.connectToRemote(r)){M.warn(`Failed to connect to remote ${r} for session ${t}`),e.send(JSON.stringify({type:"error",message:"Failed to connect to remote server"}));return}s=this.remoteConnections.get(r)}if(!s)return;s.subscriptions.add(t),s.ws.send(JSON.stringify({type:"subscribe",sessionId:t})),M.debug(`Sent subscription request to remote ${s.remoteName} for session ${t}`);let n=this.clientSubscriptions.get(e);n&&n.set(t,()=>{})}async connectToRemote(e){if(M.log(`Connecting to remote ${e}`),!this.config.remoteRegistry)return M.warn("No remote registry available"),!1;let t=this.config.remoteRegistry.getRemote(e);if(!t)return M.warn(`Remote ${e} not found in registry`),!1;try{let r=`${t.url.replace(/^http/,"ws")}/buffers`,s=new St.WebSocket(r,{headers:{Authorization:`Bearer ${t.token}`}});M.debug(`Attempting WebSocket connection to ${r}`),await new Promise((i,a)=>{let u=setTimeout(()=>{M.warn(`Connection to remote ${t.name} timed out after 5s`),a(new Error("Connection timeout"))},5e3);s.on("open",()=>{clearTimeout(u),i()}),s.on("error",c=>{clearTimeout(u),a(c)})});let n={ws:s,remoteId:t.id,remoteName:t.name,subscriptions:new Set};return this.remoteConnections.set(e,n),s.on("message",i=>{this.handleRemoteMessage(e,i)}),M.debug(`Remote ${t.name} connection established with ${n.subscriptions.size} initial subscriptions`),s.on("close",()=>{M.log(f.yellow(`Disconnected from remote ${t.name}`)),this.remoteConnections.delete(e)}),s.on("error",i=>{M.error(`Remote ${t.name} WebSocket error:`,i)}),M.log(f.green(`Connected to remote ${t.name}`)),!0}catch(r){return M.error(`Failed to connect to remote ${e}:`,r),!1}}handleRemoteMessage(e,t){if(t.length>0&&t[0]===191)this.forwardBufferToClients(t);else try{let r=JSON.parse(t.toString());M.debug(`Remote ${e} message:`,r.type)}catch(r){M.error("Failed to parse remote message:",r)}}forwardBufferToClients(e){if(e.length<5)return;let t=e.readUInt32LE(1);if(e.length<5+t)return;let r=e.subarray(5,5+t).toString("utf8"),s=0;for(let[n,i]of this.clientSubscriptions)i.has(r)&&n.readyState===St.WebSocket.OPEN&&(n.send(e),s++);s>0&&M.debug(`Forwarded buffer update for session ${r} to ${s} clients`)}handleClientDisconnect(e){let t=this.clientSubscriptions.get(e);if(t){let r=t.size;for(let[s,n]of t)M.debug(`Cleaning up subscription for session ${s}`),n();t.clear(),M.debug(`Cleaned up ${r} subscriptions`)}this.clientSubscriptions.delete(e),M.log(f.yellow("Client disconnected"))}async onRemoteRegistered(e){M.log(`Remote ${e} registered, establishing connection`),await this.connectToRemote(e)||M.warn(`Failed to establish connection to newly registered remote ${e}`)}onRemoteUnregistered(e){M.log(`Remote ${e} unregistered, closing connection`);let t=this.remoteConnections.get(e);t?(M.debug(`Closing connection to remote ${t.remoteName} with ${t.subscriptions.size} active subscriptions`),t.ws.close(),this.remoteConnections.delete(e)):M.debug(`No active connection found for unregistered remote ${e}`)}destroy(){M.log(f.yellow("Shutting down BufferAggregator"));let e=this.clientSubscriptions.size;for(let[r]of this.clientSubscriptions)r.close();this.clientSubscriptions.clear(),M.debug(`Closed ${e} client connections`);let t=this.remoteConnections.size;for(let[r,s]of this.remoteConnections)s.ws.close();this.remoteConnections.clear(),M.debug(`Closed ${t} remote connections`)}};le();var Pe=x(require("fs")),Es=x(require("path"));L();var ne=k("control-dir-watcher"),Mr=class{watcher=null;config;constructor(e){this.config=e,ne.debug(`Initialized with control dir: ${e.controlDir}, HQ mode: ${e.isHQMode}`)}start(){Pe.existsSync(this.config.controlDir)||(ne.debug(f.yellow(`Control directory ${this.config.controlDir} does not exist, creating it`)),Pe.mkdirSync(this.config.controlDir,{recursive:!0})),this.watcher=Pe.watch(this.config.controlDir,{persistent:!0},async(e,t)=>{e==="rename"&&t&&await this.handleFileChange(t)}),ne.debug(f.green(`Control directory watcher started for ${this.config.controlDir}`))}async handleFileChange(e){let t=Es.join(this.config.controlDir,e),r=Es.join(t,"session.json");try{if(Pe.existsSync(t)&&Pe.statSync(t).isDirectory()){let i=null;for(let a=0;a<5;a++){let u=100*2**a;if(ne.debug(`Attempt ${a+1}/5: Waiting ${u}ms for session.json for ${e}`),await new Promise(c=>setTimeout(c,u)),Pe.existsSync(r))try{let c=Pe.readFileSync(r,"utf8");i=JSON.parse(c),ne.debug(`Successfully read session.json for ${e} on attempt ${a+1}`);break}catch(c){ne.debug(`Failed to read/parse session.json on attempt ${a+1}:`,c)}}if(i){let a=i.id||i.session_id||e;if(ne.debug(f.blue(`Detected new external session: ${a}`)),this.config.ptyManager&&(this.config.ptyManager.getSession(a)||ne.debug(f.green(`Attaching to external session: ${a}`))),this.config.hqClient&&!ke())try{await this.notifyHQAboutSession(a,"created")}catch(u){ne.error(`Failed to notify HQ about new session ${a}:`,u)}}else ne.warn(`Session.json not found for ${e} after 5 retries`)}else if(!Pe.existsSync(t)){let s=e;if(ne.debug(f.yellow(`Detected removed session: ${s}`)),this.config.hqClient&&!ke())try{await this.notifyHQAboutSession(s,"deleted")}catch(n){ke()||ne.error(`Failed to notify HQ about deleted session ${s}:`,n)}this.config.isHQMode&&this.config.remoteRegistry&&(ne.debug(`Removing session ${s} from remote registry`),this.config.remoteRegistry.removeSessionFromRemote(s))}}catch(s){ne.error(`Error handling file change for ${e}:`,s)}}async notifyHQAboutSession(e,t){if(!this.config.hqClient||ke()){ne.debug(`Skipping HQ notification for ${e} (${t}): shutting down or no HQ client`);return}let r=this.config.hqClient.getHQUrl(),s=this.config.hqClient.getHQAuth(),n=this.config.hqClient.getName();ne.debug(`Notifying HQ at ${r} about ${t} session ${e} from remote ${n}`);let i=Date.now(),a=await fetch(`${r}/api/remotes/${n}/refresh-sessions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:s},body:JSON.stringify({action:t,sessionId:e})});if(!a.ok){if(a.status===503&&ke()){ne.debug("Got expected 503 from HQ during shutdown");return}throw new Error(`HQ responded with ${a.status}: ${await a.text()}`)}let u=Date.now()-i;ne.debug(f.green(`Notified HQ about ${t} session ${e} (${u}ms)`))}stop(){this.watcher?(this.watcher.close(),this.watcher=null,ne.debug(f.yellow("Control directory watcher stopped"))):ne.debug("Stop called but watcher was not running")}};le();L();var Oe=k("hq-client"),Dr=class{hqUrl;remoteId;remoteName;token;hqUsername;hqPassword;remoteUrl;constructor(e,t,r,s,n,i){this.hqUrl=e,this.remoteId=Ue(),this.remoteName=s,this.token=i,this.hqUsername=t,this.hqPassword=r,this.remoteUrl=n,Oe.debug("hq client initialized",{hqUrl:e,remoteName:s,remoteId:this.remoteId,remoteUrl:n})}async register(){Oe.log(`registering with hq at ${this.hqUrl}`);try{let e=await fetch(`${this.hqUrl}/api/remotes/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Basic ${Buffer.from(`${this.hqUsername}:${this.hqPassword}`).toString("base64")}`},body:JSON.stringify({id:this.remoteId,name:this.remoteName,url:this.remoteUrl,token:this.token})});if(!e.ok){let t=await e.text();throw Oe.error(`registration failed with status ${e.status}: ${t}`),Oe.debug("registration request details:",{url:`${this.hqUrl}/api/remotes/register`,headers:{"Content-Type":"application/json",Authorization:`Basic ${Buffer.from(`${this.hqUsername}:${this.hqPassword}`).toString("base64")}`},body:{id:this.remoteId,name:this.remoteName,url:this.remoteUrl,token:`${this.token.substring(0,8)}...`}}),new Error(`Registration failed (${e.status}): ${t}`)}Oe.log(f.green(`successfully registered with hq: ${this.remoteName} (${this.remoteId})`)+f.gray(` at ${this.hqUrl}`)),Oe.debug("registration details",{remoteId:this.remoteId,remoteName:this.remoteName,token:`${this.token.substring(0,8)}...`})}catch(e){throw Oe.error("failed to register with hq:",e),e}}async destroy(){Oe.log(f.yellow(`unregistering from hq: ${this.remoteName} (${this.remoteId})`));try{let e=await fetch(`${this.hqUrl}/api/remotes/${this.remoteId}`,{method:"DELETE",headers:{Authorization:`Basic ${Buffer.from(`${this.hqUsername}:${this.hqPassword}`).toString("base64")}`}});e.ok?Oe.debug("successfully unregistered from hq"):Oe.debug(`unregistration returned status ${e.status}`)}catch(e){Oe.debug("error during unregistration:",e)}}getRemoteId(){return this.remoteId}getToken(){return this.token}getHQUrl(){return this.hqUrl}getHQAuth(){return`Basic ${Buffer.from(`${this.hqUsername}:${this.hqPassword}`).toString("base64")}`}getName(){return this.remoteName}};var ko=x(require("node:os"));L();var ta=require("bonjour-service"),at=k("mdns-service"),xs=class{bonjour=null;service=null;isAdvertising=!1;async startAdvertising(e,t){if(this.isAdvertising){at.warn("mDNS service already advertising");return}try{this.bonjour=new ta;let r=t||ko.default.hostname()||"VibeTunnel Server";if(!this.bonjour)throw new Error("Failed to initialize Bonjour");this.service=this.bonjour.publish({name:r,type:"_vibetunnel._tcp",port:e,txt:{version:"1.0",platform:process.platform}}),this.isAdvertising=!0,at.log(`Started mDNS advertisement: ${r} on port ${e}`),this.service&&(this.service.on("up",()=>{at.debug("mDNS service is up")}),this.service.on("error",(...s)=>{at.warn("mDNS service error:",s[0])}))}catch(r){throw at.warn("Failed to start mDNS advertisement:",r),r}}async stopAdvertising(){if(this.isAdvertising)try{this.service&&(await new Promise(e=>{this.service&&typeof this.service.stop=="function"?this.service.stop(()=>{at.debug("mDNS service stopped"),e()}):e()}),this.service=null),this.bonjour&&(this.bonjour.destroy(),this.bonjour=null),this.isAdvertising=!1,at.log("Stopped mDNS advertisement")}catch(e){at.warn("Error stopping mDNS advertisement:",e)}}isActive(){return this.isAdvertising}},jr=new xs;var Lt=x(require("fs/promises")),Po=x(require("os")),tr=x(require("path"));L();var me=k("push-notification-service"),Lr=class{vapidManager;subscriptions=new Map;initialized=!1;subscriptionsFile;constructor(e){this.vapidManager=e;let t=tr.join(Po.homedir(),".vibetunnel/notifications");this.subscriptionsFile=tr.join(t,"subscriptions.json")}async initialize(){if(!this.initialized)try{await Lt.mkdir(tr.dirname(this.subscriptionsFile),{recursive:!0}),await this.loadSubscriptions(),this.initialized=!0,me.log("PushNotificationService initialized")}catch(e){throw me.error("Failed to initialize PushNotificationService:",e),e}}async addSubscription(e,t){let r=this.generateSubscriptionId(e,t),s={id:r,endpoint:e,keys:t,subscribedAt:new Date().toISOString(),isActive:!0};return this.subscriptions.set(r,s),await this.saveSubscriptions(),me.log(`New subscription added: ${r}`),r}async removeSubscription(e){let t=this.subscriptions.delete(e);return t&&(await this.saveSubscriptions(),me.log(`Subscription removed: ${e}`)),t}getSubscriptions(){return Array.from(this.subscriptions.values()).filter(e=>e.isActive)}async sendNotification(e){if(!this.vapidManager.isEnabled())throw new Error("VAPID not properly configured");let t=this.getSubscriptions();if(t.length===0)return{success:!0,sent:0,failed:0,errors:[]};let r=0,s=0,n=[],i=JSON.stringify({title:e.title,body:e.body,icon:e.icon||"/apple-touch-icon.png",badge:e.badge||"/favicon-32.png",tag:e.tag||`vibetunnel-${e.type}`,requireInteraction:e.requireInteraction||!1,actions:e.actions||[],data:{type:e.type,timestamp:new Date().toISOString(),...e.data}});for(let a of t)try{let u={endpoint:a.endpoint,keys:a.keys};await this.vapidManager.sendNotification(u,i),r++,me.debug(`Notification sent to: ${a.id}`)}catch(u){s++;let c=`Failed to send to ${a.id}: ${u}`;if(n.push(c),me.warn(c),this.shouldRemoveSubscription(u)){this.subscriptions.delete(a.id);let d=u;me.log(`Removed expired subscription: ${a.id} (status: ${d.statusCode})`)}else{let d=u;me.debug(`Not removing subscription ${a.id}, error: ${u instanceof Error?u.message:String(u)}, statusCode: ${d.statusCode}`)}}return await this.saveSubscriptions(),me.log(`Notification sent: ${r} successful, ${s} failed`,{type:e.type,title:e.title}),{success:!0,sent:r,failed:s,errors:n}}async sendBellNotification(e){let t={type:"bell",title:e.title,body:e.body,icon:e.icon,badge:e.badge,tag:e.tag,requireInteraction:e.requireInteraction,actions:e.actions,data:e.data};return await this.sendNotification(t)}shouldRemoveSubscription(e){if(!(e instanceof Error))return!1;if(e.statusCode===410||e.message.includes("410")||e.message.includes("Gone"))return!0;let r=e.message.toLowerCase();return r.includes("invalid")||r.includes("expired")||r.includes("no such subscription")||r.includes("unsubscribed")}async cleanupInactiveSubscriptions(){let e=this.subscriptions.size,t=Array.from(this.subscriptions.values()).filter(s=>s.isActive);this.subscriptions.clear();for(let s of t)this.subscriptions.set(s.id,s);let r=e-this.subscriptions.size;return r>0&&(await this.saveSubscriptions(),me.log(`Cleaned up ${r} inactive subscriptions`)),r}async loadSubscriptions(){try{let e=await Lt.readFile(this.subscriptionsFile,"utf8"),t=JSON.parse(e);this.subscriptions.clear();for(let r of t)this.subscriptions.set(r.id,r);me.debug(`Loaded ${t.length} subscriptions`)}catch(e){e.code==="ENOENT"?me.debug("No existing subscriptions file found"):me.error("Failed to load subscriptions:",e)}}async saveSubscriptions(){try{let e=Array.from(this.subscriptions.values());await Lt.writeFile(this.subscriptionsFile,JSON.stringify(e,null,2)),me.debug(`Saved ${e.length} subscriptions`)}catch(e){me.error("Failed to save subscriptions:",e)}}async shutdown(){await this.saveSubscriptions(),me.log("PushNotificationService shutdown")}generateSubscriptionId(e,t){try{let r=new URL(e),s=Buffer.from(t.p256dh).toString("base64").substring(0,8);return`${r.hostname}-${s}`}catch{return Buffer.from(e).toString("base64").substring(0,16)}}};le();L();var ye=k("remote-registry"),Fr=class{remotes=new Map;remotesByName=new Map;sessionToRemote=new Map;healthCheckInterval=null;HEALTH_CHECK_INTERVAL=15e3;HEALTH_CHECK_TIMEOUT=5e3;constructor(){this.startHealthChecker(),ye.debug("remote registry initialized with health check interval",{interval:this.HEALTH_CHECK_INTERVAL,timeout:this.HEALTH_CHECK_TIMEOUT})}register(e){if(this.remotesByName.has(e.name))throw new Error(`Remote with name '${e.name}' is already registered`);let t=new Date,r={...e,registeredAt:t,lastHeartbeat:t,sessionIds:new Set};return this.remotes.set(e.id,r),this.remotesByName.set(e.name,r),ye.log(f.green(`remote registered: ${e.name} (${e.id}) from ${e.url}`)),this.checkRemoteHealth(r),r}unregister(e){let t=this.remotes.get(e);if(t){ye.log(f.yellow(`remote unregistered: ${t.name} (${e})`));for(let r of t.sessionIds)this.sessionToRemote.delete(r);return this.remotesByName.delete(t.name),this.remotes.delete(e)}return!1}getRemote(e){let t=this.remotes.get(e);return t||ye.debug(`remote not found: ${e}`),t}getRemoteByUrl(e){return Array.from(this.remotes.values()).find(t=>t.url===e)}getRemotes(){return Array.from(this.remotes.values())}getRemoteBySessionId(e){let t=this.sessionToRemote.get(e);return t?this.remotes.get(t):void 0}updateRemoteSessions(e,t){let r=this.remotes.get(e);if(!r){ye.debug(`cannot update sessions: remote ${e} not found`);return}let s=r.sessionIds.size;for(let n of r.sessionIds)this.sessionToRemote.delete(n);r.sessionIds=new Set(t);for(let n of t)this.sessionToRemote.set(n,e);ye.debug(`updated sessions for remote ${r.name}`,{oldCount:s,newCount:t.length})}addSessionToRemote(e,t){let r=this.remotes.get(e);if(!r){ye.warn(`cannot add session ${t}: remote ${e} not found`);return}r.sessionIds.add(t),this.sessionToRemote.set(t,e),ye.debug(`session ${t} added to remote ${r.name}`)}removeSessionFromRemote(e){let t=this.sessionToRemote.get(e);if(!t){ye.debug(`session ${e} not mapped to any remote`);return}let r=this.remotes.get(t);r&&(r.sessionIds.delete(e),ye.debug(`session ${e} removed from remote ${r.name}`)),this.sessionToRemote.delete(e)}async checkRemoteHealth(e){if(!ke())try{let t=new AbortController,r=setTimeout(()=>t.abort(),this.HEALTH_CHECK_TIMEOUT),s={Authorization:`Bearer ${e.token}`},n=await fetch(`${e.url}/api/health`,{headers:s,signal:t.signal});if(clearTimeout(r),n.ok)e.lastHeartbeat=new Date,ye.debug(`health check passed for ${e.name}`);else throw new Error(`HTTP ${n.status}`)}catch(t){ke()||(ye.warn(`remote failed health check: ${e.name} (${e.id})`,t),this.unregister(e.id))}}startHealthChecker(){ye.debug("starting health checker"),this.healthCheckInterval=setInterval(()=>{if(ke())return;let e=Array.from(this.remotes.values()).map(t=>this.checkRemoteHealth(t));Promise.all(e).catch(t=>{ye.error("error in health checks:",t)})},this.HEALTH_CHECK_INTERVAL)}destroy(){ye.log(f.yellow("destroying remote registry")),this.healthCheckInterval&&(clearInterval(this.healthCheckInterval),ye.debug("health checker stopped"))}};le();var re=x(require("fs"));L();var U=k("stream-watcher"),ra=4096,sa="\x1B[3J";function Io(o){return Array.isArray(o)&&o.length===3&&o[1]==="o"&&typeof o[0]=="number"}function ks(o){return Array.isArray(o)&&o.length===3&&o[1]==="r"&&typeof o[0]=="number"}function na(o){return Array.isArray(o)&&o[0]==="exit"}function Co(o){return Io(o)&&o[2].includes(sa)}var Br=class{activeWatchers=new Map;sessionManager;constructor(e){this.sessionManager=e,process.on("beforeExit",()=>{this.cleanup()}),U.debug("stream watcher initialized")}addClient(e,t,r){U.debug(`adding client to session ${e}`);let s=Date.now()/1e3,n={response:r,startTime:s},i=this.activeWatchers.get(e);if(i)this.sendExistingContent(e,t,n);else{if(U.log(f.green(`creating new stream watcher for session ${e}`)),i={clients:new Set,lastOffset:0,lastSize:0,lastMtime:0,lineBuffer:""},this.activeWatchers.set(e,i),this.sendExistingContent(e,t,n),re.existsSync(t)){let a=re.statSync(t);i.lastOffset=a.size,i.lastSize=a.size,i.lastMtime=a.mtimeMs,U.debug(`initial file size: ${a.size} bytes`)}else U.debug(`stream file does not exist yet: ${t}`);this.startWatching(e,t,i)}i.clients.add(n),U.log(f.blue(`client connected to stream ${e} (${i.clients.size} total)`))}removeClient(e,t){let r=this.activeWatchers.get(e);if(!r){U.debug(`no watcher found for session ${e}`);return}let s;for(let n of r.clients)if(n.response===t){s=n;break}s&&(r.clients.delete(s),U.log(f.yellow(`client disconnected from stream ${e} (${r.clients.size} remaining)`)),r.clients.size===0&&(U.log(f.yellow(`stopping watcher for session ${e} (no clients)`)),r.watcher&&r.watcher.close(),this.activeWatchers.delete(e)))}sendExistingContent(e,t,r){try{let s=this.sessionManager.loadSessionInfo(e),n=s?.lastClearOffset??0;if(re.existsSync(t)){let v=re.statSync(t);n=Math.min(n,v.size)}let i=null,a=null;try{a=re.openSync(t,"r");let v=Buffer.alloc(ra),$="",E=0,w=re.readSync(a,v,0,v.length,E);for(;!$.includes(` +`)&&w>0;)$+=v.toString("utf8",0,w),E+=w,$.includes(` +`)||(w=re.readSync(a,v,0,v.length,E));let P=$.indexOf(` +`);P!==-1&&(i=JSON.parse($.slice(0,P)))}catch(v){U.debug(`failed to read asciinema header for session ${e}: ${v}`)}finally{if(a!==null)try{re.closeSync(a)}catch(v){U.debug(`failed to close file descriptor: ${v}`)}}let u=re.createReadStream(t,{encoding:"utf8",start:n}),c="",l=[],d=-1,g=null,p=null,h=n,b=n;u.on("data",v=>{c+=v.toString();let $=c.indexOf(` +`);for(;$!==-1;){let E=c.slice(0,$);if(c=c.slice($+1),h+=Buffer.byteLength(E,"utf8")+1,E.trim())try{let w=JSON.parse(E);if(w.version&&w.width&&w.height)i=w;else if(Array.isArray(w)){if(w[0]==="exit")l.push(w);else if(w.length>=3&&typeof w[0]=="number"){let P=w;ks(P)&&(p=P),Co(P)&&(d=l.length,g=p,b=h,U.debug(`found clear sequence at event index ${d}, current resize: ${p?p[2]:"none"}`)),l.push(P)}}}catch(w){U.debug(`skipping invalid JSON line during analysis: ${w}`)}$=c.indexOf(` +`)}}),u.on("end",()=>{if(c.trim())try{let E=JSON.parse(c);if(h+=Buffer.byteLength(c,"utf8"),Array.isArray(E)){if(E[0]==="exit")l.push(E);else if(E.length>=3&&typeof E[0]=="number"){let w=E;ks(w)&&(p=w),Co(w)&&(d=l.length,g=p,b=h,U.debug(`found clear sequence at event index ${d} (last event)`)),l.push(w)}}}catch(E){U.debug(`skipping invalid JSON in line buffer during analysis: ${E}`)}let v=0;if(d>=0&&(v=d+1,U.log(f.green(`pruning stream: skipping ${d+1} events before last clear at offset ${b}`)),s&&(s.lastClearOffset=b,this.sessionManager.saveSessionInfo(e,s))),i){let E={...i};if(d>=0&&g){let w=g[2].split("x");E.width=Number.parseInt(w[0],10),E.height=Number.parseInt(w[1],10)}r.response.write(`data: ${JSON.stringify(E)} + +`)}let $=!1;for(let E=v;E<l.length;E++){let w=l[E];if(na(w))$=!0,r.response.write(`data: ${JSON.stringify(w)} + +`);else if(Io(w)||ks(w)){let P=[0,w[1],w[2]];r.response.write(`data: ${JSON.stringify(P)} + +`)}}$&&(U.log(f.yellow(`session ${r.response.locals?.sessionId||"unknown"} already ended, closing stream`)),r.response.end())}),u.on("error",v=>{U.error("failed to analyze stream for pruning:",v),this.sendExistingContentWithoutPruning(e,t,r)})}catch(s){U.error("failed to create read stream:",s)}}sendExistingContentWithoutPruning(e,t,r){try{let s=re.createReadStream(t,{encoding:"utf8"}),n=!1,i="";s.on("data",a=>{i+=a.toString();let u=i.split(` +`);i=u.pop()||"";for(let c of u)if(c.trim())try{let l=JSON.parse(c);if(l.version&&l.width&&l.height)r.response.write(`data: ${c} + +`);else if(Array.isArray(l)&&l.length>=3)if(l[0]==="exit")n=!0,r.response.write(`data: ${c} + +`);else{let d=[0,l[1],l[2]];r.response.write(`data: ${JSON.stringify(d)} + +`)}}catch(l){U.debug(`skipping invalid JSON line during replay: ${l}`)}}),s.on("end",()=>{if(i.trim())try{let a=JSON.parse(i);if(a.version&&a.width&&a.height)r.response.write(`data: ${i} + +`);else if(Array.isArray(a)&&a.length>=3)if(a[0]==="exit")n=!0,r.response.write(`data: ${i} + +`);else{let u=[0,a[1],a[2]];r.response.write(`data: ${JSON.stringify(u)} + +`)}}catch(a){U.debug(`skipping invalid JSON in line buffer: ${a}`)}n&&(U.log(f.yellow(`session ${r.response.locals?.sessionId||"unknown"} already ended, closing stream`)),r.response.end())}),s.on("error",a=>{U.error("failed to stream existing content:",a)})}catch(s){U.error("failed to create read stream:",s)}}startWatching(e,t,r){U.log(f.green(`started watching stream file for session ${e}`)),r.watcher=re.watch(t,{persistent:!0},s=>{if(s==="change")try{let n=re.statSync(t);if(n.size>r.lastSize||n.mtimeMs>r.lastMtime){let i=n.size-r.lastSize;if(i>0&&U.debug(`file grew by ${i} bytes`),r.lastSize=n.size,r.lastMtime=n.mtimeMs,n.size>r.lastOffset){let a=re.openSync(t,"r"),u=Buffer.alloc(n.size-r.lastOffset);re.readSync(a,u,0,u.length,r.lastOffset),re.closeSync(a),r.lastOffset=n.size;let c=u.toString("utf8");r.lineBuffer+=c;let l=r.lineBuffer.split(` +`);r.lineBuffer=l.pop()||"";for(let d of l)d.trim()&&this.broadcastLine(e,d,r)}}}catch(n){U.error("failed to read file changes:",n)}}),r.watcher.on("error",s=>{U.error(`file watcher error for session ${e}:`,s)})}broadcastLine(e,t,r){let s=null;try{let n=JSON.parse(t);if(n.version&&n.width&&n.height)return;if(Array.isArray(n)&&n.length>=3)if(n[0]==="exit"){U.log(f.yellow(`session ${e} ended with exit code ${n[2]}`)),s=`data: ${JSON.stringify(n)} + +`;for(let i of r.clients)try{i.response.write(s),i.response.end()}catch(a){U.error("failed to send exit event to client:",a)}return}else{for(let i of r.clients){let u=[Date.now()/1e3-i.startTime,n[1],n[2]],c=`data: ${JSON.stringify(u)} + +`;try{i.response.write(c),i.response.flush&&i.response.flush()}catch(l){U.debug(`client write failed (likely disconnected): ${l instanceof Error?l.message:String(l)}`)}}return}}catch{U.debug(`broadcasting raw output line: ${t.substring(0,50)}...`);let n=Date.now()/1e3;for(let i of r.clients){let a=[n-i.startTime,"o",t],u=`data: ${JSON.stringify(a)} + +`;try{i.response.write(u),i.response.flush&&i.response.flush()}catch(c){U.debug(`client write failed (likely disconnected): ${c instanceof Error?c.message:String(c)}`)}}return}}cleanup(){let e=this.activeWatchers.size;if(e>0){U.log(f.yellow(`cleaning up ${e} active watchers`));for(let[t,r]of this.activeWatchers)r.watcher&&r.watcher.close(),U.debug(`closed watcher for session ${t}`);this.activeWatchers.clear()}}};var Ao=require("@xterm/headless");le();var $e=x(require("fs")),Ro=x(require("path"));var rr=class{errorCache=new Map;options;constructor(e={}){this.options={minLogInterval:e.minLogInterval??6e4,summaryInterval:e.summaryInterval??100,maxCacheSize:e.maxCacheSize??100,cacheEntryTTL:e.cacheEntryTTL??3e5,maxKeyLength:e.maxKeyLength??100,keyExtractor:e.keyExtractor??this.defaultKeyExtractor}}shouldLog(e,t){let s=this.options.keyExtractor(e,t).substring(0,this.options.maxKeyLength),n=this.errorCache.get(s),i=Date.now();return n?(n.count++,i-n.lastLogged>=this.options.minLogInterval||n.count%this.options.summaryInterval===0?(n.lastLogged=i,!0):!1):(this.errorCache.set(s,{count:1,lastLogged:i,firstSeen:i}),this.cleanupCacheIfNeeded(),!0)}getErrorStats(e,t){let s=this.options.keyExtractor(e,t).substring(0,this.options.maxKeyLength);return this.errorCache.get(s)}clear(){this.errorCache.clear()}get size(){return this.errorCache.size}defaultKeyExtractor(e,t){let r=e instanceof Error?e.message:String(e),s=t?`:${t.substring(0,30)}`:"";return`${r}${s}`}cleanupCacheIfNeeded(){if(this.errorCache.size<=this.options.maxCacheSize)return;let t=Date.now()-this.options.cacheEntryTTL,r=[];for(let[s,n]of this.errorCache.entries())n.lastLogged<t&&r.push(s);for(let s of r)this.errorCache.delete(s);if(this.errorCache.size>this.options.maxCacheSize){let n=Array.from(this.errorCache.entries()).sort((i,a)=>i[1].lastLogged-a[1].lastLogged).slice(0,this.errorCache.size-this.options.maxCacheSize);for(let[i]of n)this.errorCache.delete(i)}}};function Ps(o,e,t){let r=o instanceof Error?o.message:String(o),s=t?` (context: ${t})`:"",n=Date.now()-e.firstSeen,i=oa(n);return`Repeated error: ${e.count} occurrences over ${i}${s} - ${r}`}function oa(o){return o<1e3?`${o}ms`:o<6e4?`${Math.round(o/1e3)}s`:o<36e5?`${Math.round(o/6e4)}m`:`${Math.round(o/36e5)}h`}var ku=new rr;L();var W=k("terminal-manager"),Ft={highWatermark:.8,lowWatermark:.5,checkInterval:100,maxPendingLines:1e4,maxPauseTime:5*60*1e3,bufferCheckInterval:100},Or=class{terminals=new Map;controlDir;bufferListeners=new Map;changeTimers=new Map;writeQueues=new Map;writeTimers=new Map;errorDeduplicator=new rr({keyExtractor:(e,t)=>{let r=e instanceof Error?e.message:String(e);return`${t}:${r}`}});originalConsoleWarn;flowControlTimer;constructor(e){this.controlDir=e,this.originalConsoleWarn=console.warn,console.warn=(...t)=>{let r=t[0];typeof r=="string"&&(r.includes("xterm.js parsing error")||r.includes("Unable to process character")||r.includes("Cannot read properties of undefined"))||this.originalConsoleWarn.apply(console,t)},this.startFlowControlTimer()}async getTerminal(e){let t=this.terminals.get(e);if(!t){let r=new Ao.Terminal({cols:80,rows:24,scrollback:1e4,allowProposedApi:!0,convertEol:!0});t={terminal:r,lastUpdate:Date.now()},this.terminals.set(e,t),W.log(f.green(`Terminal created for session ${e} (${r.cols}x${r.rows})`)),await this.watchStreamFile(e)}return t.lastUpdate=Date.now(),t.terminal}async watchStreamFile(e){let t=this.terminals.get(e);if(!t)return;let r=Ro.join(this.controlDir,e,"stdout"),s=t.lastFileOffset||0,n=t.lineBuffer||"";if(!$e.existsSync(r)){W.error(`Stream file does not exist for session ${e}: ${r}`);return}try{let i=$e.readFileSync(r,"utf8");s=Buffer.byteLength(i,"utf8");let a=i.split(` +`);for(let u of a)u.trim()&&this.handleStreamLine(e,t,u);t.watcher=$e.watch(r,u=>{if(u==="change")try{let c=$e.statSync(r);if(c.size>s){let l=$e.openSync(r,"r"),d=Buffer.alloc(c.size-s);$e.readSync(l,d,0,d.length,s),$e.closeSync(l),s=c.size,t.lastFileOffset=s;let g=d.toString("utf8");n+=g;let p=n.split(` +`);n=p.pop()||"",t.lineBuffer=n;for(let h of p)h.trim()&&this.handleStreamLine(e,t,h)}}catch(c){W.error(`Error reading stream file for session ${e}:`,c)}}),W.log(f.green(`Watching stream file for session ${e}`))}catch(i){throw W.error(`Failed to watch stream file for session ${e}:`,i),i}}startFlowControlTimer(){let e=0,t=[];this.flowControlTimer=setInterval(()=>{if(e===0){t.length=0;for(let[r,s]of this.terminals)s.isPaused&&t.push(r)}if(t.length>0){let r=t[e%t.length],s=this.terminals.get(r);s?.isPaused&&(s.pausedAt&&Date.now()-s.pausedAt>Ft.maxPauseTime?(W.warn(f.red(`Session ${r} has been paused for too long. Dropping ${s.pendingLines?.length||0} pending lines.`)),s.isPaused=!1,s.pendingLines=[],s.pausedAt=void 0,this.resumeFileWatcher(r).catch(n=>{W.error(`Failed to resume file watcher for session ${r} after timeout:`,n)})):this.checkBufferPressure(r)),e=(e+1)%Math.max(t.length,1)}},Ft.checkInterval)}checkBufferPressure(e){let t=this.terminals.get(e);if(!t)return!1;let r=t.terminal,s=r.buffer.active,n=r.options.scrollback||1e4,i=s.length,a=i/n,u=t.isPaused||!1;if(!u&&a>Ft.highWatermark)return t.isPaused=!0,t.pendingLines=[],t.pausedAt=Date.now(),t.watcher&&(t.watcher.close(),t.watcher=void 0),W.warn(f.yellow(`Buffer pressure high for session ${e}: ${Math.round(a*100)}% (${i}/${n} lines). Pausing file watcher.`)),!0;if(u&&a<Ft.lowWatermark){if(t.pendingLines&&t.pendingLines.length>0&&!t.isProcessingPending){t.isProcessingPending=!0;let c=t.pendingLines.length;W.log(f.green(`Buffer pressure normalized for session ${e}: ${Math.round(a*100)}% (${i}/${n} lines). Processing ${c} pending lines.`)),setImmediate(()=>{let l=t.pendingLines||[];t.pendingLines=[],t.isPaused=!1,t.pausedAt=void 0,t.isProcessingPending=!1;for(let d of l)this.processStreamLine(e,t,d);this.resumeFileWatcher(e).catch(d=>{W.error(`Failed to resume file watcher for session ${e}:`,d)})})}else(!t.pendingLines||t.pendingLines.length===0)&&(t.isPaused=!1,t.pausedAt=void 0,this.resumeFileWatcher(e).catch(c=>{W.error(`Failed to resume file watcher for session ${e}:`,c)}),W.log(f.green(`Buffer pressure normalized for session ${e}: ${Math.round(a*100)}% (${i}/${n} lines). Resuming file watcher.`)));return!1}return u}handleStreamLine(e,t,r){t.linesProcessedSinceCheck===void 0&&(t.linesProcessedSinceCheck=0);let s=t.isPaused||!1;if(!s&&t.linesProcessedSinceCheck>=Ft.bufferCheckInterval&&(s=this.checkBufferPressure(e),t.linesProcessedSinceCheck=0),s){t.pendingLines||(t.pendingLines=[]),t.pendingLines.length<Ft.maxPendingLines?t.pendingLines.push(r):W.warn(f.red(`Pending lines limit reached for session ${e}. Dropping new data to prevent memory overflow.`));return}t.linesProcessedSinceCheck++,this.processStreamLine(e,t,r)}processStreamLine(e,t,r){try{let s=JSON.parse(r);if(s.version&&s.width&&s.height){t.terminal.resize(s.width,s.height),this.notifyBufferChange(e);return}if(Array.isArray(s)&&s.length>=3){let[n,i,a]=s;if(n==="exit"){W.log(f.yellow(`Session ${e} exited with code ${s[1]}`)),t.watcher&&t.watcher.close();return}if(i==="o")this.queueTerminalWrite(e,t,a),this.scheduleBufferChangeNotification(e);else if(i==="r"){let u=a.match(/^(\d+)x(\d+)$/);if(u){let c=Number.parseInt(u[1],10),l=Number.parseInt(u[2],10);t.terminal.resize(c,l),this.notifyBufferChange(e)}}}}catch(s){let n=`${e}:parse-stream-line`;if(this.errorDeduplicator.shouldLog(s,n)){let i=this.errorDeduplicator.getErrorStats(s,n);if(i&&i.count>1)W.warn(Ps(s,i,`session ${e}`));else{let a=r.length>100?`${r.substring(0,100)}...`:r;W.error(`Failed to parse stream line for session ${e}: ${a}`),s instanceof Error&&s.stack&&W.debug(`Parse error details: ${s.message}`)}}}}async getBufferStats(e){let t=await this.getTerminal(e),r=t.buffer.active,s=this.terminals.get(e);W.debug(`Getting buffer stats for session ${e}: ${r.length} total rows`);let n=t.options.scrollback||1e4,i=r.length/n;return{totalRows:r.length,cols:t.cols,rows:t.rows,viewportY:r.viewportY,cursorX:r.cursorX,cursorY:r.cursorY,scrollback:t.options.scrollback||0,isPaused:s?.isPaused||!1,pendingLines:s?.pendingLines?.length||0,bufferUtilization:Math.round(i*100),maxBufferLines:n}}async getBufferSnapshot(e){let t=Date.now(),r=await this.getTerminal(e),s=r.buffer.active,n=Math.max(0,s.length-r.rows),a=s.length-n,u=s.cursorX,c=s.cursorY+s.viewportY-n,l=[],d=s.getNullCell();for(let b=0;b<a;b++){let v=s.getLine(n+b),$=[];if(v){for(let w=0;w<r.cols;w++){v.getCell(w,d);let P=d.getChars()||" ",V=d.getWidth();if(V===0)continue;let _=0;d.isBold()&&(_|=1),d.isItalic()&&(_|=2),d.isUnderline()&&(_|=4),d.isDim()&&(_|=8),d.isInverse()&&(_|=16),d.isInvisible()&&(_|=32),d.isStrikethrough()&&(_|=64);let z={char:P,width:V},Y=d.getFgColor(),K=d.getBgColor();Y!==void 0&&Y!==-1&&(z.fg=Y),K!==void 0&&K!==-1&&(z.bg=K),_!==0&&(z.attributes=_),$.push(z)}let E=$.length-1;for(;E>=0;){let w=$[E];if(w.char!==" "||w.fg!==void 0||w.bg!==void 0||w.attributes!==void 0)break;E--}E<$.length-1&&$.splice(Math.max(1,E+1))}else $.push({char:" ",width:1});l.push($)}let g=l.length-1;for(;g>=0&&!l[g].some($=>$.char!==" "||$.fg!==void 0||$.bg!==void 0||$.attributes!==void 0);)g--;let p=l.slice(0,Math.max(1,g+1)),h=Date.now()-t;return h>10&&W.debug(`Buffer snapshot for session ${e} took ${h}ms (${p.length} rows)`),{cols:r.cols,rows:p.length,viewportY:n,cursorX:u,cursorY:c,cells:p}}encodeSnapshot(e){let t=Date.now(),{cols:r,rows:s,viewportY:n,cursorX:i,cursorY:a,cells:u}=e,c=32;for(let h=0;h<u.length;h++){let b=u[h];if(b.length===0||b.length===1&&b[0].char===" "&&!b[0].fg&&!b[0].bg&&!b[0].attributes)c+=2;else{c+=3;for(let v of b)c+=this.calculateCellSize(v)}}let l=Buffer.allocUnsafe(c),d=0;l.writeUInt16LE(22100,d),d+=2,l.writeUInt8(1,d),d+=1,l.writeUInt8(0,d),d+=1,l.writeUInt32LE(r,d),d+=4,l.writeUInt32LE(s,d),d+=4,l.writeInt32LE(n,d),d+=4,l.writeInt32LE(i,d),d+=4,l.writeInt32LE(a,d),d+=4,l.writeUInt32LE(0,d),d+=4;for(let h=0;h<u.length;h++){let b=u[h];if(b.length===0||b.length===1&&b[0].char===" "&&!b[0].fg&&!b[0].bg&&!b[0].attributes)l.writeUInt8(254,d++),l.writeUInt8(1,d++);else{l.writeUInt8(253,d++),l.writeUInt16LE(b.length,d),d+=2;for(let v of b)d=this.encodeCell(l,d,v)}}let g=l.subarray(0,d),p=Date.now()-t;return p>5&&W.debug(`Encoded snapshot: ${g.length} bytes in ${p}ms (${s} rows)`),g}calculateCellSize(e){let t=e.char===" ",r=e.attributes&&e.attributes!==0,s=e.fg!==void 0,n=e.bg!==void 0,i=e.char.charCodeAt(0)<=127;if(t&&!r&&!s&&!n)return 1;let a=1;if(i)a+=1;else{let u=Buffer.byteLength(e.char,"utf8");a+=1+u}return(r||s||n)&&(a+=1,s&&e.fg!==void 0&&(a+=e.fg>255?3:1),n&&e.bg!==void 0&&(a+=e.bg>255?3:1)),a}encodeCell(e,t,r){let s=r.char===" ",n=r.attributes&&r.attributes!==0,i=r.fg!==void 0,a=r.bg!==void 0,u=r.char.charCodeAt(0)<=127;if(s&&!n&&!i&&!a)return e.writeUInt8(0,t++),t;let c=0;if((n||i||a)&&(c|=128),u?s||(c|=1):(c|=64,c|=2),i&&r.fg!==void 0&&(c|=32,r.fg>255&&(c|=8)),a&&r.bg!==void 0&&(c|=16,r.bg>255&&(c|=4)),e.writeUInt8(c,t++),u)s||e.writeUInt8(r.char.charCodeAt(0),t++);else{let l=Buffer.from(r.char,"utf8");e.writeUInt8(l.length,t++),l.copy(e,t),t+=l.length}return c&128&&(n&&r.attributes!==void 0?e.writeUInt8(r.attributes,t++):(i||a)&&e.writeUInt8(0,t++),i&&r.fg!==void 0&&(r.fg>255?(e.writeUInt8(r.fg>>16&255,t++),e.writeUInt8(r.fg>>8&255,t++),e.writeUInt8(r.fg&255,t++)):e.writeUInt8(r.fg,t++)),a&&r.bg!==void 0&&(r.bg>255?(e.writeUInt8(r.bg>>16&255,t++),e.writeUInt8(r.bg>>8&255,t++),e.writeUInt8(r.bg&255,t++)):e.writeUInt8(r.bg,t++))),t}closeTerminal(e){let t=this.terminals.get(e);if(t){t.watcher&&t.watcher.close(),t.terminal.dispose(),this.terminals.delete(e);let r=this.writeTimers.get(e);r&&(clearTimeout(r),this.writeTimers.delete(e)),this.writeQueues.delete(e),W.log(f.yellow(`Terminal closed for session ${e}`))}}cleanup(e=30*60*1e3){let t=Date.now(),r=[];for(let[s,n]of this.terminals)t-n.lastUpdate>e&&r.push(s);for(let s of r)W.log(f.yellow(`Cleaning up stale terminal for session ${s}`)),this.closeTerminal(s);r.length>0&&W.log(f.gray(`Cleaned up ${r.length} stale terminals`))}queueTerminalWrite(e,t,r){let s=this.writeQueues.get(e);s||(s=[],this.writeQueues.set(e,s)),s.push(r),this.writeTimers.has(e)||this.processWriteQueue(e,t)}processWriteQueue(e,t){let r=this.writeQueues.get(e);if(!r||r.length===0){this.writeTimers.delete(e);return}let i=r.splice(0,10).join("");try{t.terminal.write(i)}catch(a){let u=`${e}:terminal-write`;if(this.errorDeduplicator.shouldLog(a,u)){let c=this.errorDeduplicator.getErrorStats(a,u);if(c&&c.count>1)W.warn(Ps(a,c,`terminal write for session ${e}`));else{let l=a instanceof Error?a.message:String(a);W.warn(`Terminal write error for session ${e}: ${l}`),a instanceof Error&&a.stack&&W.debug(`Write error stack: ${a.stack}`)}}}if(r.length>0){let a=setTimeout(()=>{this.processWriteQueue(e,t)},10);this.writeTimers.set(e,a)}else this.writeTimers.delete(e)}getActiveTerminals(){return Array.from(this.terminals.keys())}async subscribeToBufferChanges(e,t){await this.getTerminal(e),this.bufferListeners.has(e)||this.bufferListeners.set(e,new Set);let r=this.bufferListeners.get(e);return r&&(r.add(t),W.log(f.blue(`Buffer listener subscribed for session ${e} (${r.size} total)`))),()=>{let s=this.bufferListeners.get(e);s&&(s.delete(t),W.log(f.yellow(`Buffer listener unsubscribed for session ${e} (${s.size} remaining)`)),s.size===0&&this.bufferListeners.delete(e))}}scheduleBufferChangeNotification(e){let t=this.changeTimers.get(e);t&&clearTimeout(t);let r=setTimeout(()=>{this.changeTimers.delete(e),this.notifyBufferChange(e)},50);this.changeTimers.set(e,r)}async notifyBufferChange(e){let t=this.bufferListeners.get(e);if(!(!t||t.size===0)){W.debug(`Notifying ${t.size} buffer change listeners for session ${e}`);try{let r=await this.getBufferSnapshot(e);t.forEach(s=>{try{s(e,r)}catch(n){W.error(`Error notifying buffer change listener for ${e}:`,n)}})}catch(r){W.error(`Error getting buffer snapshot for notification ${e}:`,r)}}}async resumeFileWatcher(e){let t=this.terminals.get(e);!t||t.watcher||await this.watchStreamFile(e)}destroy(){for(let e of this.terminals.keys())this.closeTerminal(e);for(let e of this.changeTimers.values())clearTimeout(e);this.changeTimers.clear();for(let e of this.writeTimers.values())clearTimeout(e);this.writeTimers.clear(),this.writeQueues.clear(),this.flowControlTimer&&(clearInterval(this.flowControlTimer),this.flowControlTimer=void 0),console.warn=this.originalConsoleWarn}};L();var De=x(require("fs")),No=x(require("os")),Cs=x(require("path")),Ur=x(require("web-push"));L();var oe=k("vapid-manager"),sr=class{config=null;vapidDir;keysFilePath;constructor(e){this.vapidDir=e||Cs.join(No.homedir(),".vibetunnel/vapid"),this.keysFilePath=Cs.join(this.vapidDir,"keys.json")}async initialize(e){let{contactEmail:t,publicKey:r,privateKey:s,generateIfMissing:n=!0}=e;if(r&&s)return oe.log("Using provided VAPID keys"),this.config={keyPair:{publicKey:r,privateKey:s},contactEmail:t||"noreply@vibetunnel.local",enabled:!0},await this.saveKeys(this.config.keyPair),this.configureWebPush(),this.config;let i=await this.loadKeys();if(i)return oe.log("Using existing VAPID keys"),this.config={keyPair:i,contactEmail:t||"noreply@vibetunnel.local",enabled:!0},this.configureWebPush(),this.config;if(n){oe.log("Generating new VAPID keys");let a=this.generateKeys();return this.config={keyPair:a,contactEmail:t||"noreply@vibetunnel.local",enabled:!0},await this.saveKeys(this.config.keyPair),this.configureWebPush(),this.config}return oe.warn("No VAPID keys available and generation disabled"),this.config={keyPair:{publicKey:"",privateKey:""},contactEmail:t||"noreply@vibetunnel.local",enabled:!1},this.config}generateKeys(){oe.debug("Generating VAPID key pair");let e=Ur.default.generateVAPIDKeys();return{publicKey:e.publicKey,privateKey:e.privateKey}}async rotateKeys(e){oe.log("Rotating VAPID keys");let t=this.generateKeys();return this.config={keyPair:t,contactEmail:e||this.config?.contactEmail||"noreply@vibetunnel.local",enabled:!0},await this.saveKeys(t),this.configureWebPush(),oe.log("VAPID keys rotated successfully"),t}getConfig(){return this.config}getPublicKey(){return this.config?.keyPair.publicKey||null}isEnabled(){return this.config?.enabled===!0&&!!this.config.keyPair.publicKey&&!!this.config.keyPair.privateKey}validateConfig(){let e=[];return this.config?(this.config.keyPair.publicKey||e.push("Missing VAPID public key"),this.config.keyPair.privateKey||e.push("Missing VAPID private key"),this.config.contactEmail||e.push("Missing contact email for VAPID"),this.config.contactEmail&&!this.isValidEmail(this.config.contactEmail)&&e.push("Invalid contact email format"),this.config.keyPair.publicKey&&!this.isValidVapidKey(this.config.keyPair.publicKey)&&e.push("Invalid VAPID public key format"),this.config.keyPair.privateKey&&!this.isValidVapidKey(this.config.keyPair.privateKey)&&e.push("Invalid VAPID private key format"),{valid:e.length===0,errors:e}):(e.push("VAPID manager not initialized"),{valid:!1,errors:e})}async saveKeys(e){try{De.existsSync(this.vapidDir)||(De.mkdirSync(this.vapidDir,{recursive:!0}),oe.debug(`Created VAPID directory: ${this.vapidDir}`));let t={publicKey:e.publicKey,privateKey:e.privateKey,generated:new Date().toISOString()};De.writeFileSync(this.keysFilePath,JSON.stringify(t,null,2),{mode:384}),oe.debug("VAPID keys saved to disk")}catch(t){throw oe.error("Failed to save VAPID keys:",t),new Error(`Failed to save VAPID keys: ${t}`)}}async loadKeys(){try{if(!De.existsSync(this.keysFilePath))return oe.debug("No existing VAPID keys file found"),null;let e=JSON.parse(De.readFileSync(this.keysFilePath,"utf8"));return!e.publicKey||!e.privateKey?(oe.warn("Invalid VAPID keys file format"),null):(oe.debug("VAPID keys loaded from disk"),{publicKey:e.publicKey,privateKey:e.privateKey})}catch(e){return oe.error("Failed to load VAPID keys:",e),null}}configureWebPush(){if(!this.config||!this.isEnabled()){oe.debug("Skipping web-push configuration - VAPID not enabled");return}try{Ur.default.setVapidDetails(`mailto:${this.config.contactEmail}`,this.config.keyPair.publicKey,this.config.keyPair.privateKey),oe.debug("Web-push library configured with VAPID details")}catch(e){throw oe.error("Failed to configure web-push library:",e),new Error(`Failed to configure web-push: ${e}`)}}isValidEmail(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}isValidVapidKey(e){return typeof e=="string"&&e.length>20&&/^[A-Za-z0-9_-]+$/.test(e)}getKeysDirectory(){return this.vapidDir}async removeKeys(){try{De.existsSync(this.keysFilePath)&&(De.unlinkSync(this.keysFilePath),oe.log("VAPID keys removed from disk")),this.config=null}catch(e){throw oe.error("Failed to remove VAPID keys:",e),new Error(`Failed to remove VAPID keys: ${e}`)}}async sendNotification(e,t,r){if(!this.isEnabled())throw new Error("VAPID not properly configured");try{return await Ur.default.sendNotification(e,t,r)}catch(s){throw oe.error("Failed to send push notification:",s),s}}},Mu=new sr;kr();var m=k("server"),Fo=!1;function ke(){return Fo}function ia(o){Fo=o}function aa(){console.log(` +VibeTunnel Server - Terminal Multiplexer + +Usage: vibetunnel-server [options] + +Options: + --help Show this help message + --version Show version information + --port <number> Server port (default: 4020 or PORT env var) + --bind <address> Bind address (default: 0.0.0.0, all interfaces) + --enable-ssh-keys Enable SSH key authentication UI and functionality + --disallow-user-password Disable password auth, SSH keys only (auto-enables --enable-ssh-keys) + --no-auth Disable authentication (auto-login as current user) + --allow-local-bypass Allow localhost connections to bypass authentication + --local-auth-token <token> Token for localhost authentication bypass + --repository-base-path <path> Base path for repository discovery (default: ~/) + --debug Enable debug logging + +Push Notification Options: + --push-enabled Enable push notifications (default: enabled) + --push-disabled Disable push notifications + --vapid-email <email> Contact email for VAPID (or PUSH_CONTACT_EMAIL env var) + --generate-vapid-keys Generate new VAPID keys if none exist + +Network Discovery Options: + --no-mdns Disable mDNS/Bonjour advertisement (enabled by default) + +HQ Mode Options: + --hq Run as HQ (headquarters) server + +Remote Server Options: + --hq-url <url> HQ server URL to register with + --hq-username <user> Username for HQ authentication + --hq-password <pass> Password for HQ authentication + --name <name> Unique name for this remote server + --allow-insecure-hq Allow HTTP URLs for HQ (default: HTTPS only) + --no-hq-auth Disable HQ authentication (for testing only) + +Environment Variables: + PORT Default port if --port not specified + VIBETUNNEL_USERNAME Default username if --username not specified + VIBETUNNEL_PASSWORD Default password if --password not specified + VIBETUNNEL_CONTROL_DIR Control directory for session data + PUSH_CONTACT_EMAIL Contact email for VAPID configuration + +Examples: + # Run a simple server with authentication + vibetunnel-server --username admin --password secret + + # Run as HQ server + vibetunnel-server --hq --username hq-admin --password hq-secret + + # Run as remote server registering with HQ + vibetunnel-server --username local --password local123 \\ + --hq-url https://hq.example.com \\ + --hq-username hq-admin --hq-password hq-secret \\ + --name remote-1 +`)}function ca(){let o=process.argv.slice(2),e={port:null,bind:null,enableSSHKeys:!1,disallowUserPassword:!1,noAuth:!1,isHQMode:!1,hqUrl:null,hqUsername:null,hqPassword:null,remoteName:null,allowInsecureHQ:!1,showHelp:!1,showVersion:!1,debug:!1,pushEnabled:!0,vapidEmail:null,generateVapidKeys:!0,bellNotificationsEnabled:!0,allowLocalBypass:!1,localAuthToken:null,noHqAuth:!1,enableMDNS:!0,repositoryBasePath:null};if(o.includes("--help")||o.includes("-h"))return e.showHelp=!0,e;if(o.includes("--version")||o.includes("-v"))return e.showVersion=!0,e;for(let t=0;t<o.length;t++)o[t]==="--port"&&t+1<o.length?(e.port=Number.parseInt(o[t+1],10),t++):o[t]==="--bind"&&t+1<o.length?(e.bind=o[t+1],t++):o[t]==="--enable-ssh-keys"?e.enableSSHKeys=!0:o[t]==="--disallow-user-password"?(e.disallowUserPassword=!0,e.enableSSHKeys=!0):o[t]==="--no-auth"?e.noAuth=!0:o[t]==="--hq"?e.isHQMode=!0:o[t]==="--hq-url"&&t+1<o.length?(e.hqUrl=o[t+1],t++):o[t]==="--hq-username"&&t+1<o.length?(e.hqUsername=o[t+1],t++):o[t]==="--hq-password"&&t+1<o.length?(e.hqPassword=o[t+1],t++):o[t]==="--name"&&t+1<o.length?(e.remoteName=o[t+1],t++):o[t]==="--allow-insecure-hq"?e.allowInsecureHQ=!0:o[t]==="--debug"?e.debug=!0:o[t]==="--push-enabled"?e.pushEnabled=!0:o[t]==="--push-disabled"?e.pushEnabled=!1:o[t]==="--vapid-email"&&t+1<o.length?(e.vapidEmail=o[t+1],t++):o[t]==="--generate-vapid-keys"?e.generateVapidKeys=!0:o[t]==="--allow-local-bypass"?e.allowLocalBypass=!0:o[t]==="--local-auth-token"&&t+1<o.length?(e.localAuthToken=o[t+1],t++):o[t]==="--no-hq-auth"?e.noHqAuth=!0:o[t]==="--no-mdns"?e.enableMDNS=!1:o[t]==="--repository-base-path"&&t+1<o.length?(e.repositoryBasePath=o[t+1],t++):o[t].startsWith("--")&&(m.error(`Unknown argument: ${o[t]}`),m.error("Use --help to see available options"),process.exit(1));return!e.vapidEmail&&process.env.PUSH_CONTACT_EMAIL&&(e.vapidEmail=process.env.PUSH_CONTACT_EMAIL),e}function la(o){o.noAuth&&(o.enableSSHKeys||o.disallowUserPassword)&&m.warn("--no-auth overrides all other authentication settings (authentication is disabled)"),o.disallowUserPassword&&!o.enableSSHKeys&&(m.warn("--disallow-user-password requires SSH keys, auto-enabling --enable-ssh-keys"),o.enableSSHKeys=!0),o.hqUrl&&(!o.hqUsername||!o.hqPassword)&&!o.noHqAuth&&(m.error("HQ username and password required when --hq-url is specified"),m.error("Use --hq-username and --hq-password with --hq-url"),m.error("Or use --no-hq-auth for testing without authentication"),process.exit(1)),o.hqUrl&&!o.remoteName&&(m.error("Remote name required when --hq-url is specified"),m.error("Use --name to specify a unique name for this remote server"),process.exit(1)),o.hqUrl&&!o.hqUrl.startsWith("https://")&&!o.allowInsecureHQ&&(m.error("HQ URL must use HTTPS protocol"),m.error("Use --allow-insecure-hq to allow HTTP for testing"),process.exit(1)),(o.hqUrl||o.hqUsername||o.hqPassword)&&(!o.hqUrl||!o.hqUsername||!o.hqPassword)&&!o.noHqAuth&&(m.error("All HQ parameters required: --hq-url, --hq-username, --hq-password"),m.error("Or use --no-hq-auth for testing without authentication"),process.exit(1)),o.isHQMode&&o.hqUrl&&(m.error("Cannot use --hq and --hq-url together"),m.error("Use --hq to run as HQ server, or --hq-url to register with an HQ"),process.exit(1)),o.noHqAuth&&o.hqUrl&&(m.warn("--no-hq-auth is enabled: Remote servers can register without authentication"),m.warn("This should only be used for testing!"))}var Mo=!1;async function ua(){if(Mo)throw m.error("App already created, preventing duplicate instance"),new Error("Duplicate app creation detected");Mo=!0;let o=ca();if(o.showHelp&&(aa(),process.exit(0)),o.showVersion){let C=as();console.log(`VibeTunnel Server v${C.version}`),console.log(`Built: ${C.buildDate}`),console.log(`Platform: ${C.platform}/${C.arch}`),console.log(`Node: ${C.nodeVersion}`),process.exit(0)}$n(),la(o),m.log("Initializing VibeTunnel server components");let e=(0,_r.default)(),t=(0,Lo.createServer)(e),r=new Wr.WebSocketServer({noServer:!0,perMessageDeflate:!0});e.use((0,jo.default)({contentSecurityPolicy:!1,crossOriginEmbedderPolicy:!1})),m.debug("Configured security headers with helmet"),e.use((0,Is.default)({filter:(C,N)=>C.path.match(/\/api\/sessions\/[^/]+\/stream$/)?!1:Is.default.filter(C,N),level:6})),m.debug("Configured compression middleware (with asciicast exclusion)"),e.use(_r.default.json({limit:"10mb"})),m.debug("Configured express middleware");let s=process.env.VIBETUNNEL_CONTROL_DIR||ue.join(Hr.homedir(),".vibetunnel/control");zr.existsSync(s)?m.debug(`Using existing control directory: ${s}`):(zr.mkdirSync(s,{recursive:!0}),m.log(f.green(`Created control directory: ${s}`)));let n=new gt(s);m.debug("Initialized PTY manager");let i=n.getSessionManager(),a=i.cleanupOldVersionSessions();a.versionChanged?m.log(f.yellow(`Version change detected - cleaned up ${a.cleanedCount} sessions from previous version`)):a.cleanedCount>0&&m.log(f.yellow(`Cleaned up ${a.cleanedCount} legacy sessions without version information`));let u=new Or(s);m.debug("Initialized terminal manager");let c=new Br(i);m.debug("Initialized stream watcher");let l=new Cr(s);m.debug("Initialized activity monitor");let d=null,g=null,p=null;if(o.pushEnabled)try{m.log("Initializing push notification services"),d=new sr,await d.initialize({contactEmail:o.vapidEmail||"noreply@vibetunnel.local",generateIfMissing:!0}),m.log("VAPID keys initialized successfully"),g=new Lr(d),await g.initialize(),p=new Rr,p.setPushNotificationService(g),m.log(f.green("Push notification services initialized"))}catch(C){m.error("Failed to initialize push notification services:",C),m.warn("Continuing without push notifications"),d=null,g=null,p=null}else m.debug("Push notifications disabled");let h=null,b=null,v=null,$=null,E=null;o.isHQMode?(h=new Fr,m.log(f.green("Running in HQ mode")),m.debug("Initialized remote registry for HQ mode")):o.hqUrl&&o.remoteName&&(o.noHqAuth||o.hqUsername&&o.hqPassword)&&(E=Ue(),m.debug(`Generated bearer token for remote server: ${o.remoteName}`));let w=new Ir;m.debug("Initialized authentication service"),$=new Nr({terminalManager:u,remoteRegistry:h,isHQMode:o.isHQMode}),m.debug("Initialized buffer aggregator");let P=new Pr({ptyManager:n,terminalManager:u,activityMonitor:l,remoteRegistry:h,authService:w,isHQMode:o.isHQMode});m.debug("Initialized WebSocket input handler");let V=Fn({enableSSHKeys:o.enableSSHKeys,disallowUserPassword:o.disallowUserPassword,noAuth:o.noAuth,isHQMode:o.isHQMode,bearerToken:E||void 0,authService:w,allowLocalBypass:o.allowLocalBypass,localAuthToken:o.localAuthToken||void 0}),z=(()=>{let C=(()=>{if(__filename.includes(ue.join("node_modules","vibetunnel","lib"))||__filename.includes("node_modules\\vibetunnel\\lib"))return!0;if(ue.basename(__dirname)==="lib"){let N=ue.dirname(__dirname),I=ue.join(N,"package.json");try{return require(I).name==="vibetunnel"}catch{return!1}}return!1})();if(process.env.VIBETUNNEL_BUNDLED==="true"||process.env.BUILD_DATE||C){let N=__dirname;ue.basename(N)==="dist"&&(N=ue.dirname(N)),ue.basename(N)==="lib"&&(N=ue.dirname(N));let I=ue.join(N,"public"),q=ue.join(I,"index.html");return require("fs").existsSync(q)?I:ue.join(__dirname,"..","public")}else return ue.join(process.cwd(),"public")})(),Y=!process.env.BUILD_DATE||process.env.NODE_ENV==="development";e.use(_r.default.static(z,{extensions:["html"],maxAge:Y?0:"1d",etag:!Y,lastModified:!Y,setHeaders:(C,N)=>{Y?(C.setHeader("Cache-Control","no-cache, no-store, must-revalidate"),C.setHeader("Pragma","no-cache"),C.setHeader("Expires","0")):N.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)?C.setHeader("Cache-Control","public, max-age=31536000, immutable"):N.endsWith(".html")&&C.setHeader("Cache-Control","public, max-age=3600")}})),m.debug(`Serving static files from: ${z} ${Y?"with caching disabled (dev mode)":"with caching headers"}`),e.get("/api/health",(C,N)=>{let I=as();N.json({status:"ok",timestamp:new Date().toISOString(),mode:o.isHQMode?"hq":"remote",version:I.version,buildDate:I.buildDate,uptime:I.uptime,pid:I.pid})}),p&&(n.on("bell",C=>{p.processBellEvent(C).catch(N=>{m.error("Failed to process bell event:",N)})}),m.debug("Connected bell event handler to PTY manager")),e.use("/api/auth",On({authService:w,enableSSHKeys:o.enableSSHKeys,disallowUserPassword:o.disallowUserPassword,noAuth:o.noAuth})),m.debug("Mounted authentication routes"),e.use("/api",V),m.debug("Applied authentication middleware to /api routes"),e.use("/api",yo({ptyManager:n,terminalManager:u,streamWatcher:c,remoteRegistry:h,isHQMode:o.isHQMode,activityMonitor:l})),m.debug("Mounted session routes"),e.use("/api",ro({remoteRegistry:h,isHQMode:o.isHQMode})),m.debug("Mounted remote routes"),e.use("/api",Qn()),m.debug("Mounted filesystem routes"),e.use("/api",Xn()),m.debug("Mounted log routes"),e.use("/api",Vn()),m.debug("Mounted file routes"),e.use("/api",no()),m.debug("Mounted repository routes"),e.use("/api",zn({getRepositoryBasePath:()=>o.repositoryBasePath})),m.debug("Mounted config routes"),d&&(e.use("/api",eo({vapidManager:d,pushNotificationService:g,bellEventHandler:p??void 0})),m.debug("Mounted push notification routes")),e.use("/",lo()),m.debug("Mounted screencap routes"),e.use("/api",wo()),m.debug("Mounted WebRTC config routes");try{await co(),Be.setConfigUpdateCallback(C=>{o.repositoryBasePath=C.repositoryBasePath;let N=JSON.stringify({type:"config",data:{repositoryBasePath:C.repositoryBasePath,serverConfigured:!0}});K.forEach(I=>{I.readyState===Wr.WebSocket.OPEN&&I.send(N)}),m.log(`Broadcast config update to ${K.size} clients`)}),await Be.start(),m.log(f.green("Control UNIX socket: READY"))}catch(C){m.error("Failed to initialize screencap or control socket:",C),m.warn("Screen capture and Mac control features will not be available.")}t.on("upgrade",async(C,N,I)=>{let q=new URL(C.url||"",`http://${C.headers.host||"localhost"}`);if(q.pathname!=="/buffers"&&q.pathname!=="/ws/input"&&q.pathname!=="/ws/screencap-signal"&&q.pathname!=="/ws/config"){N.write(`HTTP/1.1 404 Not Found\r +\r +`),N.destroy();return}let ce=await new Promise(B=>{let O=!1,X=Ee=>{O||(O=!0,B(Ee))},de={};q.searchParams.forEach((Ee,Ns)=>{de[Ns]=Ee});let ct={...C,url:C.url,path:q.pathname,userId:void 0,authMethod:void 0,query:de,headers:C.headers,ip:C.socket.remoteAddress||"",socket:C.socket,hostname:C.headers.host?.split(":")[0]||"localhost",get:Ee=>C.headers[Ee.toLowerCase()],header:Ee=>C.headers[Ee.toLowerCase()],accepts:()=>!1,acceptsCharsets:()=>!1,acceptsEncodings:()=>!1,acceptsLanguages:()=>!1},qr=!1,Uo={status:Ee=>(Ee>=400&&(qr=!0,X({authenticated:!1})),{json:()=>{},send:()=>{},end:()=>{}}),setHeader:()=>{},send:()=>{},json:()=>{},end:()=>{}},_o=Ee=>{X({authenticated:!Ee&&!qr,userId:ct.userId,authMethod:ct.authMethod})},Rs=setTimeout(()=>{m.error("WebSocket auth timeout - auth middleware did not complete in time"),X({authenticated:!1})},5e3);Promise.resolve(V(ct,Uo,_o)).then(()=>{clearTimeout(Rs)}).catch(Ee=>{clearTimeout(Rs),m.error("Auth middleware error:",Ee),X({authenticated:!1})})});if(!ce.authenticated){m.debug("WebSocket connection rejected: unauthorized"),N.write(`HTTP/1.1 401 Unauthorized\r +\r +`),N.destroy();return}r.handleUpgrade(C,N,I,B=>{let O=C;O.pathname=q.pathname,O.searchParams=q.searchParams,O.userId=ce.userId,O.authMethod=ce.authMethod,r.emit("connection",B,O)})});let K=new Set;return r.on("connection",(C,N)=>{let I=N,q=I.pathname,ce=I.searchParams;if(m.log(`\u{1F50C} WebSocket connection to path: ${q}`),m.log(`\u{1F464} User ID: ${I.userId||"unknown"}`),m.log(`\u{1F510} Auth method: ${I.authMethod||"unknown"}`),q==="/buffers")m.log("\u{1F4CA} Handling buffer WebSocket connection"),$?$.handleClientConnection(C):(m.error("BufferAggregator not initialized for WebSocket connection"),C.close());else if(q==="/ws/input"){m.log("\u2328\uFE0F Handling input WebSocket connection");let B=ce?.get("sessionId");if(!B){m.error("WebSocket input connection missing sessionId parameter"),C.close();return}let O=I.userId||"unknown";P.handleConnection(C,B,O)}else if(q==="/ws/screencap-signal"){m.log("\u{1F5A5}\uFE0F Handling screencap WebSocket connection");let B=I.userId||"unknown";if(m.log(`\u{1F5A5}\uFE0F Screencap WebSocket user: ${B}`),!Be){m.error("\u274C controlUnixHandler not initialized!"),C.close();return}m.log("\u2705 Passing connection to controlUnixHandler with userId:",B),Be.handleBrowserConnection(C,B)}else q==="/ws/config"?(m.log("\u2699\uFE0F Handling config WebSocket connection"),K.add(C),C.send(JSON.stringify({type:"config",data:{repositoryBasePath:o.repositoryBasePath||"~/",serverConfigured:o.repositoryBasePath!==null}})),C.on("message",async B=>{try{let O=JSON.parse(B.toString());if(O.type==="update-repository-path"){let X=O.path;if(m.log(`Received repository path update from web: ${X}`),Be){let de={id:Ue(),type:"request",category:"system",action:"repository-path-update",payload:{path:X,source:"web"}},ct=await Be.sendControlMessage(de);ct&&ct.type==="response"?ct.payload?.success?m.log(`Mac app confirmed repository path update: ${X}`):m.error("Mac app failed to update repository path"):m.error("No response from Mac app for repository path update")}else m.warn("No control Unix handler available, cannot forward path update to Mac")}}catch(O){m.error("Failed to handle config WebSocket message:",O)}}),C.on("close",()=>{K.delete(C),m.log("Config WebSocket client disconnected")}),C.on("error",B=>{m.error("Config WebSocket error:",B),K.delete(C)})):(m.error(`\u274C Unknown WebSocket path: ${q}`),C.close())}),e.get("/",(C,N)=>{N.sendFile(ue.join(z,"index.html"))}),e.use((C,N)=>{C.path.startsWith("/api/")?N.status(404).json({error:"API endpoint not found"}):N.status(404).sendFile(ue.join(z,"404.html"),I=>{I&&N.status(404).send("404 - Page not found")})}),{app:e,server:t,wss:r,startServer:()=>{let C=o.port!==null?o.port:Number(process.env.PORT)||4020;m.log(`Starting server on port ${C}`),t.removeAllListeners("error"),t.on("error",I=>{I.code==="EADDRINUSE"?(m.error(`Port ${C} is already in use`),!process.env.BUILD_DATE||process.env.NODE_ENV==="development"?(m.error(f.yellow(` +Development mode options:`)),m.error(` 1. Run server on different port: ${f.cyan("pnpm run dev:server --port 4021")}`),m.error(` 2. Use environment variable: ${f.cyan("PORT=4021 pnpm run dev")}`),m.error(" 3. Stop the existing server (check Activity Monitor for vibetunnel processes)")):m.error("Please use a different port with --port <number> or stop the existing server"),process.exit(9)):(m.error("Server error:",I),process.exit(1))});let N=o.bind||"0.0.0.0";t.listen(C,N,()=>{let I=t.address(),q=typeof I=="string"?C:I?.port||C,ce=N==="0.0.0.0"?"localhost":N;if(m.log(f.green(`VibeTunnel Server running on http://${ce}:${q}`)),o.noAuth?(m.warn(f.yellow("Authentication: DISABLED (--no-auth)")),m.warn("Anyone can access this server without authentication")):o.disallowUserPassword?(m.log(f.green("Authentication: SSH KEYS ONLY (--disallow-user-password)")),m.log(f.gray("Password authentication is disabled"))):(m.log(f.green("Authentication: SYSTEM USER PASSWORD")),o.enableSSHKeys?m.log(f.green("SSH Key Authentication: ENABLED")):m.log(f.gray("SSH Key Authentication: DISABLED (use --enable-ssh-keys to enable)"))),o.hqUrl&&o.remoteName&&(o.noHqAuth||o.hqUsername&&o.hqPassword)){let B=N;N==="0.0.0.0"&&(B=Hr.hostname());let O=`http://${B}:${q}`;b=new Dr(o.hqUrl,o.hqUsername||"no-auth",o.hqPassword||"no-auth",o.remoteName,O,E||""),o.noHqAuth?m.log(f.yellow(`Remote mode: ${o.remoteName} registering WITHOUT HQ authentication (--no-hq-auth)`)):(m.log(f.green(`Remote mode: ${o.remoteName} will accept Bearer token for HQ access`)),m.debug(`Bearer token: ${b.getToken()}`))}process.send&&!process.env.VITEST&&process.send({type:"server-started",port:q}),b&&(m.log(`Registering with HQ at ${o.hqUrl}`),b.register().catch(B=>{m.error("Failed to register with HQ:",B)})),v=new Mr({controlDir:s,remoteRegistry:h,isHQMode:o.isHQMode,hqClient:b,ptyManager:n}),v.start(),m.debug("Started control directory watcher"),l.start(),m.debug("Started activity monitor"),o.enableMDNS?jr.startAdvertising(q).catch(B=>{m.warn("Failed to start mDNS advertisement:",B)}):m.debug("mDNS advertisement disabled")})},config:o,ptyManager:n,terminalManager:u,streamWatcher:c,remoteRegistry:h,hqClient:b,controlDirWatcher:v,bufferAggregator:$,activityMonitor:l,pushNotificationService:g}}var Do=!1;async function Bo(){cr(),Do&&(m.error("Server already started, preventing duplicate instance"),m.error("This should not happen - duplicate server startup detected"),process.exit(1)),Do=!0,m.debug("Creating VibeTunnel application instance");let o=await ua(),{startServer:e,server:t,terminalManager:r,remoteRegistry:s,hqClient:n,controlDirWatcher:i,activityMonitor:a,config:u}=o;(u.debug||process.env.DEBUG==="true")&&(Zr(!0),m.log(f.gray("Debug logging enabled"))),e();let c=setInterval(()=>{r.cleanup(5*60*1e3)},5*60*1e3);m.debug("Started terminal cleanup interval (5 minutes)");let l=null;o.pushNotificationService&&(l=setInterval(()=>{o.pushNotificationService?.cleanupInactiveSubscriptions().catch(p=>{m.error("Failed to cleanup inactive subscriptions:",p)})},30*60*1e3),m.debug("Started subscription cleanup interval (30 minutes)"));let d=!1,g=async()=>{d&&(m.warn("Force exit..."),process.exit(1)),d=!0,ia(!0),m.log(f.yellow(` +Shutting down...`));try{clearInterval(c),l&&clearInterval(l),m.debug("Cleared cleanup intervals"),a.stop(),m.debug("Stopped activity monitor"),jr.isActive()&&(await jr.stopAdvertising(),m.debug("Stopped mDNS advertisement")),i&&(i.stop(),m.debug("Stopped control directory watcher"));try{let{controlUnixHandler:p}=await Promise.resolve().then(()=>(kr(),go));p.stop(),m.debug("Stopped UNIX socket server")}catch{}n&&(m.debug("Destroying HQ client connection"),await n.destroy()),s&&(m.debug("Destroying remote registry"),s.destroy()),t.close(()=>{m.log(f.green("Server closed successfully")),se(),process.exit(0)}),setTimeout(()=>{m.warn("Graceful shutdown timeout, forcing exit..."),se(),process.exit(1)},5e3)}catch(p){m.error("Error during shutdown:",p),se(),process.exit(1)}};process.on("SIGINT",g),process.on("SIGTERM",g),m.debug("Registered signal handlers for graceful shutdown")}L();js();var As=br(),da=process.env.VIBETUNNEL_DEBUG==="1"||process.env.VIBETUNNEL_DEBUG==="true";cr(da,As);var Bt=k("cli"),Oo=global;Oo.__vibetunnelStarted&&process.exit(0);Oo.__vibetunnelStarted=!0;process.on("uncaughtException",o=>{Bt.error("Uncaught exception:",o),Bt.error("Stack trace:",o.stack),se(),process.exit(1)});process.on("unhandledRejection",(o,e)=>{Bt.error("Unhandled rejection at:",e,"reason:",o),o instanceof Error&&Bt.error("Stack trace:",o.stack),se(),process.exit(1)});var fa=!module.parent&&(require.main===module||require.main===void 0||(require.main?.filename?.endsWith("/vibetunnel-cli")??!1));fa&&(process.argv[2]==="version"?(console.log(`VibeTunnel Server v${ve}`),process.exit(0)):process.argv[2]==="fwd"?Ln(process.argv.slice(3)).catch(o=>{Bt.error("Fatal error:",o),se(),process.exit(1)}):(As!==void 0&&As>=3&&Bt.log("Starting VibeTunnel server..."),Bo())); diff --git a/web/package/node-pty/README.md b/web/package/node-pty/README.md new file mode 100644 index 00000000..df810c28 --- /dev/null +++ b/web/package/node-pty/README.md @@ -0,0 +1,47 @@ +# Vendored PTY + +This is a vendored fork of [node-pty](https://github.com/microsoft/node-pty) v1.1.0-beta34 with the threading and shared pipe architecture removed. + +## Why? + +The original node-pty uses a shared pipe/socket architecture through `ConoutSocketWorker` that causes issues when used heavily: +- All PTY instances write to the same shared pipe +- This can overwhelm other Electron processes (like VS Code) that are also listening on the pipe +- Heavy usage from VibeTunnel causes crashes in other applications + +## What's Changed? + +1. **Removed ConoutSocketWorker** - No more worker threads for socket management +2. **Removed shared pipe architecture** - Each PTY instance uses direct file descriptors +3. **Simplified Windows implementation** - Direct socket connections without intermediary workers +4. **Kept core functionality** - The essential PTY spawn/resize/kill operations remain unchanged + +## Building + +```bash +npm install +npm run build +``` + +The native modules will be compiled during installation. + +## Usage + +The API remains compatible with node-pty: + +```javascript +const pty = require('node-pty'); +const ptyProcess = pty.spawn('bash', [], { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: process.env +}); + +ptyProcess.on('data', function(data) { + console.log(data); +}); + +ptyProcess.write('ls\r'); +``` \ No newline at end of file diff --git a/web/package/node-pty/binding.gyp b/web/package/node-pty/binding.gyp new file mode 100644 index 00000000..1ec5c1cd --- /dev/null +++ b/web/package/node-pty/binding.gyp @@ -0,0 +1,63 @@ +{ + 'targets': [{ + 'target_name': 'pty', + 'include_dirs': [ + 'src/', + '<!@(node -p "require(\'node-addon-api\').include")' + ], + 'defines': [ 'NAPI_CPP_EXCEPTIONS' ], + 'cflags!': [ '-fno-exceptions' ], + 'cflags_cc!': [ '-fno-exceptions' ], + 'xcode_settings': { + 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', + 'CLANG_CXX_LIBRARY': 'libc++', + 'MACOSX_DEPLOYMENT_TARGET': '14.0', + }, + 'msvs_settings': { + 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, + }, + 'conditions': [ + ['OS=="win"', { + 'sources': [ + 'src/win/conpty.cc', + 'src/win/path_util.cc' + ], + 'libraries': [ + '-lkernel32.lib', + '-luser32.lib', + '-lshell32.lib', + '-ladvapi32.lib' + ], + 'defines': [ + '_WIN32_WINNT=0x0600', + 'NTDDI_VERSION=0x06000000' + ] + }], + ['OS!="win"', { + 'sources': [ + 'src/unix/pty.cc' + ], + 'libraries': [ + '-lutil' + ], + 'conditions': [ + ['OS=="mac"', { + 'xcode_settings': { + 'MACOSX_DEPLOYMENT_TARGET': '14.0' + } + }] + ] + }] + ] + }, { + 'target_name': 'spawn-helper', + 'type': 'executable', + 'conditions': [ + ['OS!="win"', { + 'sources': [ + 'src/unix/spawn-helper.cc' + ], + }] + ] + }] +} \ No newline at end of file diff --git a/web/package/node-pty/lib/eventEmitter2.d.ts b/web/package/node-pty/lib/eventEmitter2.d.ts new file mode 100644 index 00000000..22c364af --- /dev/null +++ b/web/package/node-pty/lib/eventEmitter2.d.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2019, Microsoft Corporation (MIT License). + */ +import { IDisposable } from './types'; +export interface IEvent<T> { + (listener: (e: T) => any): IDisposable; +} +export declare class EventEmitter2<T> { + private _listeners; + private _event?; + get event(): IEvent<T>; + fire(data: T): void; +} diff --git a/web/package/node-pty/lib/eventEmitter2.js b/web/package/node-pty/lib/eventEmitter2.js new file mode 100644 index 00000000..4944085a --- /dev/null +++ b/web/package/node-pty/lib/eventEmitter2.js @@ -0,0 +1,40 @@ +"use strict"; +/** + * Copyright (c) 2019, Microsoft Corporation (MIT License). + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EventEmitter2 = void 0; +class EventEmitter2 { + constructor() { + this._listeners = []; + } + get event() { + if (!this._event) { + this._event = (listener) => { + this._listeners.push(listener); + const disposable = { + dispose: () => { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + } + }; + return disposable; + }; + } + return this._event; + } + fire(data) { + const queue = []; + for (let i = 0; i < this._listeners.length; i++) { + queue.push(this._listeners[i]); + } + for (let i = 0; i < queue.length; i++) { + queue[i].call(undefined, data); + } + } +} +exports.EventEmitter2 = EventEmitter2; diff --git a/web/package/node-pty/lib/index.d.ts b/web/package/node-pty/lib/index.d.ts new file mode 100644 index 00000000..4015fc7d --- /dev/null +++ b/web/package/node-pty/lib/index.d.ts @@ -0,0 +1,15 @@ +/** + * Minimal PTY implementation without threading + * Vendored from node-pty, simplified to remove shared pipe architecture + */ +import { ITerminal, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; +/** + * Forks a process as a pseudoterminal. + */ +export declare function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal; +export declare const fork: typeof spawn; +export declare const createTerminal: typeof spawn; +export * from './interfaces'; +export * from './types'; +export type IPty = ITerminal; diff --git a/web/package/node-pty/lib/index.js b/web/package/node-pty/lib/index.js new file mode 100644 index 00000000..398eb125 --- /dev/null +++ b/web/package/node-pty/lib/index.js @@ -0,0 +1,41 @@ +"use strict"; +/** + * Minimal PTY implementation without threading + * Vendored from node-pty, simplified to remove shared pipe architecture + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createTerminal = exports.fork = void 0; +exports.spawn = spawn; +let terminalCtor; +if (process.platform === 'win32') { + terminalCtor = require('./windowsTerminal').WindowsTerminal; +} +else { + terminalCtor = require('./unixTerminal').UnixTerminal; +} +/** + * Forks a process as a pseudoterminal. + */ +function spawn(file, args, opt) { + return new terminalCtor(file, args, opt); +} +// Deprecated aliases +exports.fork = spawn; +exports.createTerminal = spawn; +// Re-export types +__exportStar(require("./interfaces"), exports); +__exportStar(require("./types"), exports); diff --git a/web/package/node-pty/lib/interfaces.d.ts b/web/package/node-pty/lib/interfaces.d.ts new file mode 100644 index 00000000..e55483ce --- /dev/null +++ b/web/package/node-pty/lib/interfaces.d.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +export interface IProcessEnv { + [key: string]: string | undefined; +} +import type { IExitEvent } from './types'; +import type { IEvent } from './eventEmitter2'; +export interface ITerminal { + /** + * Gets the name of the process. + */ + process: string; + /** + * Gets the process ID. + */ + pid: number; + /** + * The data event. + */ + readonly onData: IEvent<string>; + /** + * The exit event. + */ + readonly onExit: IEvent<IExitEvent>; + /** + * Writes data to the socket. + * @param data The data to write. + */ + write(data: string): void; + /** + * Resize the pty. + * @param cols The number of columns. + * @param rows The number of rows. + */ + resize(cols: number, rows: number): void; + /** + * Clears the pty's internal representation of its buffer. This is a no-op + * unless on Windows/ConPTY. + */ + clear(): void; + /** + * Close, kill and destroy the socket. + */ + destroy(): void; + /** + * Kill the pty. + * @param signal The signal to send, by default this is SIGHUP. This is not + * supported on Windows. + */ + kill(signal?: string): void; + /** + * Set the pty socket encoding. + */ + setEncoding(encoding: string | null): void; + /** + * Resume the pty socket. + */ + resume(): void; + /** + * Pause the pty socket. + */ + pause(): void; + /** + * Alias for ITerminal.on(eventName, listener). + */ + addListener(eventName: string, listener: (...args: any[]) => any): void; + /** + * Adds the listener function to the end of the listeners array for the event + * named eventName. + * @param eventName The event name. + * @param listener The callback function + */ + on(eventName: string, listener: (...args: any[]) => any): void; + /** + * Returns a copy of the array of listeners for the event named eventName. + */ + listeners(eventName: string): Function[]; + /** + * Removes the specified listener from the listener array for the event named + * eventName. + */ + removeListener(eventName: string, listener: (...args: any[]) => any): void; + /** + * Removes all listeners, or those of the specified eventName. + */ + removeAllListeners(eventName: string): void; + /** + * Adds a one time listener function for the event named eventName. The next + * time eventName is triggered, this listener is removed and then invoked. + */ + once(eventName: string, listener: (...args: any[]) => any): void; +} +interface IBasePtyForkOptions { + name?: string; + cols?: number; + rows?: number; + cwd?: string; + env?: IProcessEnv; + encoding?: string | null; + handleFlowControl?: boolean; + flowControlPause?: string; + flowControlResume?: string; +} +export interface IPtyForkOptions extends IBasePtyForkOptions { + uid?: number; + gid?: number; +} +export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { + useConpty?: boolean; + useConptyDll?: boolean; + conptyInheritCursor?: boolean; +} +export interface IPtyOpenOptions { + cols?: number; + rows?: number; + encoding?: string | null; +} +export {}; diff --git a/web/package/node-pty/lib/interfaces.js b/web/package/node-pty/lib/interfaces.js new file mode 100644 index 00000000..be902097 --- /dev/null +++ b/web/package/node-pty/lib/interfaces.js @@ -0,0 +1,6 @@ +"use strict"; +/** + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/web/package/node-pty/lib/terminal.d.ts b/web/package/node-pty/lib/terminal.d.ts new file mode 100644 index 00000000..04f92894 --- /dev/null +++ b/web/package/node-pty/lib/terminal.d.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +import { Socket } from 'net'; +import { EventEmitter } from 'events'; +import { ITerminal, IPtyForkOptions, IProcessEnv } from './interfaces'; +import { IEvent } from './eventEmitter2'; +import { IExitEvent } from './types'; +export declare const DEFAULT_COLS: number; +export declare const DEFAULT_ROWS: number; +export declare abstract class Terminal implements ITerminal { + protected _socket: Socket; + protected _pid: number; + protected _fd: number; + protected _pty: any; + protected _file: string; + protected _name: string; + protected _cols: number; + protected _rows: number; + protected _readable: boolean; + protected _writable: boolean; + protected _internalee: EventEmitter; + private _flowControlPause; + private _flowControlResume; + handleFlowControl: boolean; + private _onData; + get onData(): IEvent<string>; + private _onExit; + get onExit(): IEvent<IExitEvent>; + get pid(): number; + get cols(): number; + get rows(): number; + constructor(opt?: IPtyForkOptions); + protected abstract _write(data: string): void; + write(data: string): void; + protected _forwardEvents(): void; + protected _checkType<T>(name: string, value: T | undefined, type: string, allowArray?: boolean): void; + /** See net.Socket.end */ + end(data: string): void; + /** See stream.Readable.pipe */ + pipe(dest: any, options: any): any; + /** See net.Socket.pause */ + pause(): Socket; + /** See net.Socket.resume */ + resume(): Socket; + /** See net.Socket.setEncoding */ + setEncoding(encoding: string | null): void; + addListener(eventName: string, listener: (...args: any[]) => any): void; + on(eventName: string, listener: (...args: any[]) => any): void; + emit(eventName: string, ...args: any[]): any; + listeners(eventName: string): Function[]; + removeListener(eventName: string, listener: (...args: any[]) => any): void; + removeAllListeners(eventName: string): void; + once(eventName: string, listener: (...args: any[]) => any): void; + abstract resize(cols: number, rows: number): void; + abstract clear(): void; + abstract destroy(): void; + abstract kill(signal?: string): void; + abstract get process(): string; + abstract get master(): Socket | undefined; + abstract get slave(): Socket | undefined; + protected _close(): void; + protected _parseEnv(env: IProcessEnv): string[]; +} diff --git a/web/package/node-pty/lib/terminal.js b/web/package/node-pty/lib/terminal.js new file mode 100644 index 00000000..4a4a212f --- /dev/null +++ b/web/package/node-pty/lib/terminal.js @@ -0,0 +1,162 @@ +"use strict"; +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Terminal = exports.DEFAULT_ROWS = exports.DEFAULT_COLS = void 0; +const events_1 = require("events"); +const eventEmitter2_1 = require("./eventEmitter2"); +exports.DEFAULT_COLS = 80; +exports.DEFAULT_ROWS = 24; +/** + * Default messages to indicate PAUSE/RESUME for automatic flow control. + * To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh), + * the sequences can be customized in `IPtyForkOptions`. + */ +const FLOW_CONTROL_PAUSE = '\x13'; // defaults to XOFF +const FLOW_CONTROL_RESUME = '\x11'; // defaults to XON +class Terminal { + get onData() { return this._onData.event; } + get onExit() { return this._onExit.event; } + get pid() { return this._pid; } + get cols() { return this._cols; } + get rows() { return this._rows; } + constructor(opt) { + this._pid = 0; + this._fd = 0; + this._cols = 0; + this._rows = 0; + this._readable = false; + this._writable = false; + this._onData = new eventEmitter2_1.EventEmitter2(); + this._onExit = new eventEmitter2_1.EventEmitter2(); + // for 'close' + this._internalee = new events_1.EventEmitter(); + // setup flow control handling + this.handleFlowControl = !!(opt?.handleFlowControl); + this._flowControlPause = opt?.flowControlPause || FLOW_CONTROL_PAUSE; + this._flowControlResume = opt?.flowControlResume || FLOW_CONTROL_RESUME; + if (!opt) { + return; + } + // Do basic type checks here in case node-pty is being used within JavaScript. If the wrong + // types go through to the C++ side it can lead to hard to diagnose exceptions. + this._checkType('name', opt.name ? opt.name : undefined, 'string'); + this._checkType('cols', opt.cols ? opt.cols : undefined, 'number'); + this._checkType('rows', opt.rows ? opt.rows : undefined, 'number'); + this._checkType('cwd', opt.cwd ? opt.cwd : undefined, 'string'); + this._checkType('env', opt.env ? opt.env : undefined, 'object'); + this._checkType('uid', opt.uid ? opt.uid : undefined, 'number'); + this._checkType('gid', opt.gid ? opt.gid : undefined, 'number'); + this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string'); + } + write(data) { + if (this.handleFlowControl) { + // PAUSE/RESUME messages are not forwarded to the pty + if (data === this._flowControlPause) { + this.pause(); + return; + } + if (data === this._flowControlResume) { + this.resume(); + return; + } + } + // everything else goes to the real pty + this._write(data); + } + _forwardEvents() { + this.on('data', e => this._onData.fire(e)); + this.on('exit', (exitCode, signal) => this._onExit.fire({ exitCode, signal })); + } + _checkType(name, value, type, allowArray = false) { + if (value === undefined) { + return; + } + if (allowArray) { + if (Array.isArray(value)) { + value.forEach((v, i) => { + if (typeof v !== type) { + throw new Error(`${name}[${i}] must be a ${type} (not a ${typeof v})`); + } + }); + return; + } + } + if (typeof value !== type) { + throw new Error(`${name} must be a ${type} (not a ${typeof value})`); + } + } + /** See net.Socket.end */ + end(data) { + this._socket.end(data); + } + /** See stream.Readable.pipe */ + pipe(dest, options) { + return this._socket.pipe(dest, options); + } + /** See net.Socket.pause */ + pause() { + return this._socket.pause(); + } + /** See net.Socket.resume */ + resume() { + return this._socket.resume(); + } + /** See net.Socket.setEncoding */ + setEncoding(encoding) { + if (this._socket._decoder) { + delete this._socket._decoder; + } + if (encoding) { + this._socket.setEncoding(encoding); + } + } + addListener(eventName, listener) { this.on(eventName, listener); } + on(eventName, listener) { + if (eventName === 'close') { + this._internalee.on('close', listener); + return; + } + this._socket.on(eventName, listener); + } + emit(eventName, ...args) { + if (eventName === 'close') { + return this._internalee.emit.apply(this._internalee, arguments); + } + return this._socket.emit.apply(this._socket, arguments); + } + listeners(eventName) { + return this._socket.listeners(eventName); + } + removeListener(eventName, listener) { + this._socket.removeListener(eventName, listener); + } + removeAllListeners(eventName) { + this._socket.removeAllListeners(eventName); + } + once(eventName, listener) { + this._socket.once(eventName, listener); + } + _close() { + this._socket.readable = false; + this.write = () => { }; + this.end = () => { }; + this._writable = false; + this._readable = false; + } + _parseEnv(env) { + const keys = Object.keys(env || {}); + const pairs = []; + for (let i = 0; i < keys.length; i++) { + if (keys[i] === undefined) { + continue; + } + pairs.push(keys[i] + '=' + env[keys[i]]); + } + return pairs; + } +} +exports.Terminal = Terminal; diff --git a/web/package/node-pty/lib/types.d.ts b/web/package/node-pty/lib/types.d.ts new file mode 100644 index 00000000..0de7327b --- /dev/null +++ b/web/package/node-pty/lib/types.d.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +export type ArgvOrCommandLine = string[] | string; +export interface IExitEvent { + exitCode: number; + signal: number | undefined; +} +export interface IDisposable { + dispose(): void; +} diff --git a/web/package/node-pty/lib/types.js b/web/package/node-pty/lib/types.js new file mode 100644 index 00000000..a00668a6 --- /dev/null +++ b/web/package/node-pty/lib/types.js @@ -0,0 +1,6 @@ +"use strict"; +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/web/package/node-pty/lib/unixTerminal.d.ts b/web/package/node-pty/lib/unixTerminal.d.ts new file mode 100644 index 00000000..2981edc5 --- /dev/null +++ b/web/package/node-pty/lib/unixTerminal.d.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +import * as net from 'net'; +import { Terminal } from './terminal'; +import { IPtyForkOptions, IPtyOpenOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; +export declare class UnixTerminal extends Terminal { + protected _fd: number; + protected _pty: string; + protected _file: string; + protected _name: string; + protected _readable: boolean; + protected _writable: boolean; + private _boundClose; + private _emittedClose; + private _master; + private _slave; + get master(): net.Socket | undefined; + get slave(): net.Socket | undefined; + constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions); + protected _write(data: string): void; + get fd(): number; + get ptsName(): string; + /** + * openpty + */ + static open(opt: IPtyOpenOptions): UnixTerminal; + destroy(): void; + kill(signal?: string): void; + /** + * Gets the name of the process. + */ + get process(): string; + /** + * TTY + */ + resize(cols: number, rows: number): void; + clear(): void; + private _sanitizeEnv; +} diff --git a/web/package/node-pty/lib/unixTerminal.js b/web/package/node-pty/lib/unixTerminal.js new file mode 100644 index 00000000..7629c46d --- /dev/null +++ b/web/package/node-pty/lib/unixTerminal.js @@ -0,0 +1,300 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnixTerminal = void 0; +const path = __importStar(require("path")); +const tty = __importStar(require("tty")); +const terminal_1 = require("./terminal"); +const utils_1 = require("./utils"); +let pty; +let helperPath; +// Check if running in SEA (Single Executable Application) context +if (process.env.VIBETUNNEL_SEA) { + // In SEA mode, load native module using process.dlopen + const fs = require('fs'); + const execDir = path.dirname(process.execPath); + const ptyPath = path.join(execDir, 'pty.node'); + if (fs.existsSync(ptyPath)) { + const module = { exports: {} }; + process.dlopen(module, ptyPath); + pty = module.exports; + } + else { + throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`); + } + // Set spawn-helper path for macOS only (Linux doesn't use it) + if (process.platform === 'darwin') { + helperPath = path.join(execDir, 'spawn-helper'); + if (!fs.existsSync(helperPath)) { + console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`); + } + } +} +else { + // Standard Node.js loading + try { + pty = require('../build/Release/pty.node'); + helperPath = '../build/Release/spawn-helper'; + } + catch (outerError) { + try { + pty = require('../build/Debug/pty.node'); + helperPath = '../build/Debug/spawn-helper'; + } + catch (innerError) { + console.error('innerError', innerError); + throw outerError; + } + } + helperPath = path.resolve(__dirname, helperPath); + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); +} +const DEFAULT_FILE = 'sh'; +const DEFAULT_NAME = 'xterm'; +const DESTROY_SOCKET_TIMEOUT_MS = 200; +class UnixTerminal extends terminal_1.Terminal { + get master() { return this._master; } + get slave() { return this._slave; } + constructor(file, args, opt) { + super(opt); + this._boundClose = false; + this._emittedClose = false; + if (typeof args === 'string') { + throw new Error('args as a string is not supported on unix.'); + } + // Initialize arguments + args = args || []; + file = file || DEFAULT_FILE; + opt = opt || {}; + opt.env = opt.env || process.env; + this._cols = opt.cols || terminal_1.DEFAULT_COLS; + this._rows = opt.rows || terminal_1.DEFAULT_ROWS; + const uid = opt.uid ?? -1; + const gid = opt.gid ?? -1; + const env = (0, utils_1.assign)({}, opt.env); + if (opt.env === process.env) { + this._sanitizeEnv(env); + } + const cwd = opt.cwd || process.cwd(); + env.PWD = cwd; + const name = opt.name || env.TERM || DEFAULT_NAME; + env.TERM = name; + const parsedEnv = this._parseEnv(env); + const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); + const onexit = (code, signal) => { + // XXX Sometimes a data event is emitted after exit. Wait til socket is + // destroyed. + if (!this._emittedClose) { + if (this._boundClose) { + return; + } + this._boundClose = true; + // From macOS High Sierra 10.13.2 sometimes the socket never gets + // closed. A timeout is applied here to avoid the terminal never being + // destroyed when this occurs. + let timeout = setTimeout(() => { + timeout = null; + // Destroying the socket now will cause the close event to fire + this._socket.destroy(); + }, DESTROY_SOCKET_TIMEOUT_MS); + this.once('close', () => { + if (timeout !== null) { + clearTimeout(timeout); + } + this.emit('exit', code, signal); + }); + return; + } + this.emit('exit', code, signal); + }; + // fork + const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit); + this._socket = new tty.ReadStream(term.fd); + if (encoding !== null) { + this._socket.setEncoding(encoding); + } + // setup + this._socket.on('error', (err) => { + // NOTE: fs.ReadStream gets EAGAIN twice at first: + if (err.code) { + if (~err.code.indexOf('EAGAIN')) { + return; + } + } + // close + this._close(); + // EIO on exit from fs.ReadStream: + if (!this._emittedClose) { + this._emittedClose = true; + this.emit('close'); + } + // EIO, happens when someone closes our child process: the only process in + // the terminal. + // node < 0.6.14: errno 5 + // node >= 0.6.14: read EIO + if (err.code) { + if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) { + return; + } + } + // throw anything else + if (this.listeners('error').length < 2) { + throw err; + } + }); + this._pid = term.pid; + this._fd = term.fd; + this._pty = term.pty; + this._file = file; + this._name = name; + this._readable = true; + this._writable = true; + this._socket.on('close', () => { + if (this._emittedClose) { + return; + } + this._emittedClose = true; + this._close(); + this.emit('close'); + }); + this._forwardEvents(); + } + _write(data) { + this._socket.write(data); + } + /* Accessors */ + get fd() { return this._fd; } + get ptsName() { return this._pty; } + /** + * openpty + */ + static open(opt) { + const self = Object.create(UnixTerminal.prototype); + opt = opt || {}; + if (arguments.length > 1) { + opt = { + cols: arguments[1], + rows: arguments[2] + }; + } + const cols = opt.cols || terminal_1.DEFAULT_COLS; + const rows = opt.rows || terminal_1.DEFAULT_ROWS; + const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); + // open + const term = pty.open(cols, rows); + self._master = new tty.ReadStream(term.master); + if (encoding !== null) { + self._master.setEncoding(encoding); + } + self._master.resume(); + self._slave = new tty.ReadStream(term.slave); + if (encoding !== null) { + self._slave.setEncoding(encoding); + } + self._slave.resume(); + self._socket = self._master; + self._pid = -1; + self._fd = term.master; + self._pty = term.pty; + self._file = process.argv[0] || 'node'; + self._name = process.env.TERM || ''; + self._readable = true; + self._writable = true; + self._socket.on('error', err => { + self._close(); + if (self.listeners('error').length < 2) { + throw err; + } + }); + self._socket.on('close', () => { + self._close(); + }); + return self; + } + destroy() { + this._close(); + // Need to close the read stream so node stops reading a dead file + // descriptor. Then we can safely SIGHUP the shell. + this._socket.once('close', () => { + this.kill('SIGHUP'); + }); + this._socket.destroy(); + } + kill(signal) { + try { + process.kill(this.pid, signal || 'SIGHUP'); + } + catch (e) { /* swallow */ } + } + /** + * Gets the name of the process. + */ + get process() { + if (process.platform === 'darwin') { + const title = pty.process(this._fd); + return (title !== 'kernel_task') ? title : this._file; + } + return pty.process(this._fd, this._pty) || this._file; + } + /** + * TTY + */ + resize(cols, rows) { + if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) { + throw new Error('resizing must be done using positive cols and rows'); + } + pty.resize(this._fd, cols, rows); + this._cols = cols; + this._rows = rows; + } + clear() { + } + _sanitizeEnv(env) { + // Make sure we didn't start our server from inside tmux. + delete env['TMUX']; + delete env['TMUX_PANE']; + // Make sure we didn't start our server from inside screen. + // http://web.mit.edu/gnu/doc/html/screen_20.html + delete env['STY']; + delete env['WINDOW']; + // Delete some variables that might confuse our terminal. + delete env['WINDOWID']; + delete env['TERMCAP']; + delete env['COLUMNS']; + delete env['LINES']; + } +} +exports.UnixTerminal = UnixTerminal; diff --git a/web/package/node-pty/lib/utils.d.ts b/web/package/node-pty/lib/utils.d.ts new file mode 100644 index 00000000..46d51c3a --- /dev/null +++ b/web/package/node-pty/lib/utils.d.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +export declare function assign(target: any, ...sources: any[]): any; diff --git a/web/package/node-pty/lib/utils.js b/web/package/node-pty/lib/utils.js new file mode 100644 index 00000000..d996bda4 --- /dev/null +++ b/web/package/node-pty/lib/utils.js @@ -0,0 +1,11 @@ +"use strict"; +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.assign = assign; +function assign(target, ...sources) { + sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key])); + return target; +} diff --git a/web/package/node-pty/lib/windowsTerminal.d.ts b/web/package/node-pty/lib/windowsTerminal.d.ts new file mode 100644 index 00000000..132ed661 --- /dev/null +++ b/web/package/node-pty/lib/windowsTerminal.d.ts @@ -0,0 +1,34 @@ +/** + * Simplified Windows terminal implementation without threading + * Removed ConoutSocketWorker and shared pipe architecture + */ +import { Socket } from 'net'; +import { Terminal } from './terminal'; +import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; +export declare class WindowsTerminal extends Terminal { + private _isReady; + protected _pid: number; + private _innerPid; + private _ptyNative; + protected _pty: number; + private _inSocket; + private _outSocket; + private _exitCode; + private _useConptyDll; + get master(): Socket | undefined; + get slave(): Socket | undefined; + constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions); + private _setupDirectSockets; + protected _write(data: string): void; + resize(cols: number, rows: number): void; + clear(): void; + kill(signal?: string): void; + protected _close(): void; + private _generatePipeName; + private _argsToCommandLine; + static open(options?: IPtyOpenOptions): void; + get process(): string; + get pid(): number; + destroy(): void; +} diff --git a/web/package/node-pty/lib/windowsTerminal.js b/web/package/node-pty/lib/windowsTerminal.js new file mode 100644 index 00000000..4bbd75bc --- /dev/null +++ b/web/package/node-pty/lib/windowsTerminal.js @@ -0,0 +1,200 @@ +"use strict"; +/** + * Simplified Windows terminal implementation without threading + * Removed ConoutSocketWorker and shared pipe architecture + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WindowsTerminal = void 0; +const fs = __importStar(require("fs")); +const net_1 = require("net"); +const terminal_1 = require("./terminal"); +const utils_1 = require("./utils"); +const DEFAULT_FILE = 'cmd.exe'; +const DEFAULT_NAME = 'Windows Shell'; +let conptyNative; +class WindowsTerminal extends terminal_1.Terminal { + get master() { return this._outSocket; } + get slave() { return this._inSocket; } + constructor(file, args, opt) { + super(opt); + this._isReady = false; + this._pid = 0; + this._innerPid = 0; + this._useConptyDll = false; + // Load native module + if (!conptyNative) { + try { + conptyNative = require('../build/Release/conpty.node'); + } + catch (outerError) { + try { + conptyNative = require('../build/Debug/conpty.node'); + } + catch (innerError) { + throw outerError; + } + } + } + this._ptyNative = conptyNative; + // Initialize arguments + args = args || []; + file = file || DEFAULT_FILE; + opt = opt || {}; + opt.env = opt.env || process.env; + const env = (0, utils_1.assign)({}, opt.env); + this._cols = opt.cols || terminal_1.DEFAULT_COLS; + this._rows = opt.rows || terminal_1.DEFAULT_ROWS; + const cwd = opt.cwd || process.cwd(); + const parsedEnv = this._parseEnv(env); + // Compose command line + const commandLine = this._argsToCommandLine(file, args); + // Start ConPTY process + const pipeName = this._generatePipeName(); + const term = this._ptyNative.startProcess(file, this._cols, this._rows, false, pipeName, false, this._useConptyDll); + this._pty = term.pty; + this._fd = term.fd; + // Create direct socket connections without worker threads + this._setupDirectSockets(term); + // Connect the process + const connect = this._ptyNative.connect(this._pty, commandLine, cwd, parsedEnv, this._useConptyDll, (exitCode) => { + this._exitCode = exitCode; + this.emit('exit', exitCode); + this._close(); + }); + this._innerPid = connect.pid; + this._pid = connect.pid; + this._file = file; + this._name = opt.name || env.TERM || DEFAULT_NAME; + this._readable = true; + this._writable = true; + this._forwardEvents(); + } + _setupDirectSockets(term) { + // Setup output socket - read directly from conout + const outFd = fs.openSync(term.conout, 'r'); + this._outSocket = new net_1.Socket({ fd: outFd, readable: true, writable: false }); + this._outSocket.setEncoding('utf8'); + this._socket = this._outSocket; + // Setup input socket - write directly to conin + const inFd = fs.openSync(term.conin, 'w'); + this._inSocket = new net_1.Socket({ fd: inFd, readable: false, writable: true }); + this._inSocket.setEncoding('utf8'); + // Forward events directly + this._outSocket.on('data', (data) => { + if (!this._isReady) { + this._isReady = true; + } + this.emit('data', data); + }); + this._outSocket.on('error', (err) => { + if (err.code && (err.code.includes('EPIPE') || err.code.includes('EIO'))) { + // Expected errors when process exits + return; + } + this.emit('error', err); + }); + this._outSocket.on('close', () => { + if (this._exitCode === undefined) { + this.emit('exit', 0); + } + this._close(); + }); + } + _write(data) { + if (this._inSocket && this._inSocket.writable) { + this._inSocket.write(data); + } + } + resize(cols, rows) { + if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) { + throw new Error('resizing must be done using positive cols and rows'); + } + if (this._exitCode !== undefined) { + throw new Error('Cannot resize a pty that has already exited'); + } + this._cols = cols; + this._rows = rows; + this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll); + } + clear() { + this._ptyNative.clear(this._pty, this._useConptyDll); + } + kill(signal) { + this._close(); + try { + process.kill(this._pid); + } + catch (e) { + // Ignore if process cannot be found + } + this._ptyNative.kill(this._pty, this._useConptyDll); + } + _close() { + if (this._inSocket) { + this._inSocket.destroy(); + } + if (this._outSocket) { + this._outSocket.destroy(); + } + } + _generatePipeName() { + return `\\\\.\\pipe\\conpty-${Date.now()}-${Math.random()}`; + } + _argsToCommandLine(file, args) { + if (typeof args === 'string') { + return `${file} ${args}`; + } + const argv = [file]; + if (args) { + argv.push(...args); + } + return argv.map(arg => { + if (arg.includes(' ') || arg.includes('\t')) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }).join(' '); + } + static open(options) { + throw new Error('open() not supported on windows, use spawn() instead.'); + } + get process() { return this._name; } + get pid() { return this._pid; } + destroy() { + this.kill(); + } +} +exports.WindowsTerminal = WindowsTerminal; diff --git a/web/package/node-pty/package.json b/web/package/node-pty/package.json new file mode 100644 index 00000000..a4123bf9 --- /dev/null +++ b/web/package/node-pty/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-pty", + "version": "1.0.0", + "description": "Minimal PTY implementation without threading - vendored from node-pty", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc && node-gyp rebuild", + "clean": "rimraf lib build", + "install": "node-gyp rebuild" + }, + "dependencies": { + "node-addon-api": "^7.1.0" + }, + "devDependencies": { + "@types/node": "^24.0.3", + "node-gyp": "^11.0.0", + "prebuild": "^13.0.1", + "rimraf": "^5.0.5", + "typescript": "^5.8.3" + }, + "gypfile": true, + "license": "MIT" +} \ No newline at end of file diff --git a/web/package/node-pty/src/eventEmitter2.ts b/web/package/node-pty/src/eventEmitter2.ts new file mode 100644 index 00000000..6779d0cc --- /dev/null +++ b/web/package/node-pty/src/eventEmitter2.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2019, Microsoft Corporation (MIT License). + */ + +import { IDisposable } from './types'; + +interface IListener<T> { + (e: T): void; +} + +export interface IEvent<T> { + (listener: (e: T) => any): IDisposable; +} + +export class EventEmitter2<T> { + private _listeners: IListener<T>[] = []; + private _event?: IEvent<T>; + + public get event(): IEvent<T> { + if (!this._event) { + this._event = (listener: (e: T) => any) => { + this._listeners.push(listener); + const disposable = { + dispose: () => { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + } + }; + return disposable; + }; + } + return this._event; + } + + public fire(data: T): void { + const queue: IListener<T>[] = []; + for (let i = 0; i < this._listeners.length; i++) { + queue.push(this._listeners[i]); + } + for (let i = 0; i < queue.length; i++) { + queue[i].call(undefined, data); + } + } +} diff --git a/web/package/node-pty/src/index.ts b/web/package/node-pty/src/index.ts new file mode 100644 index 00000000..2032d89f --- /dev/null +++ b/web/package/node-pty/src/index.ts @@ -0,0 +1,32 @@ +/** + * Minimal PTY implementation without threading + * Vendored from node-pty, simplified to remove shared pipe architecture + */ + +import { ITerminal, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; + +let terminalCtor: any; +if (process.platform === 'win32') { + terminalCtor = require('./windowsTerminal').WindowsTerminal; +} else { + terminalCtor = require('./unixTerminal').UnixTerminal; +} + +/** + * Forks a process as a pseudoterminal. + */ +export function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal { + return new terminalCtor(file, args, opt); +} + +// Deprecated aliases +export const fork = spawn; +export const createTerminal = spawn; + +// Re-export types +export * from './interfaces'; +export * from './types'; + +// Alias for compatibility +export type IPty = ITerminal; \ No newline at end of file diff --git a/web/package/node-pty/src/interfaces.ts b/web/package/node-pty/src/interfaces.ts new file mode 100644 index 00000000..6f0610e6 --- /dev/null +++ b/web/package/node-pty/src/interfaces.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +export interface IProcessEnv { + [key: string]: string | undefined; +} + +import type { IExitEvent } from './types'; +import type { IEvent } from './eventEmitter2'; + +export interface ITerminal { + /** + * Gets the name of the process. + */ + process: string; + + /** + * Gets the process ID. + */ + pid: number; + + /** + * The data event. + */ + readonly onData: IEvent<string>; + + /** + * The exit event. + */ + readonly onExit: IEvent<IExitEvent>; + + /** + * Writes data to the socket. + * @param data The data to write. + */ + write(data: string): void; + + /** + * Resize the pty. + * @param cols The number of columns. + * @param rows The number of rows. + */ + resize(cols: number, rows: number): void; + + /** + * Clears the pty's internal representation of its buffer. This is a no-op + * unless on Windows/ConPTY. + */ + clear(): void; + + /** + * Close, kill and destroy the socket. + */ + destroy(): void; + + /** + * Kill the pty. + * @param signal The signal to send, by default this is SIGHUP. This is not + * supported on Windows. + */ + kill(signal?: string): void; + + /** + * Set the pty socket encoding. + */ + setEncoding(encoding: string | null): void; + + /** + * Resume the pty socket. + */ + resume(): void; + + /** + * Pause the pty socket. + */ + pause(): void; + + /** + * Alias for ITerminal.on(eventName, listener). + */ + addListener(eventName: string, listener: (...args: any[]) => any): void; + + /** + * Adds the listener function to the end of the listeners array for the event + * named eventName. + * @param eventName The event name. + * @param listener The callback function + */ + on(eventName: string, listener: (...args: any[]) => any): void; + + /** + * Returns a copy of the array of listeners for the event named eventName. + */ + listeners(eventName: string): Function[]; + + /** + * Removes the specified listener from the listener array for the event named + * eventName. + */ + removeListener(eventName: string, listener: (...args: any[]) => any): void; + + /** + * Removes all listeners, or those of the specified eventName. + */ + removeAllListeners(eventName: string): void; + + /** + * Adds a one time listener function for the event named eventName. The next + * time eventName is triggered, this listener is removed and then invoked. + */ + once(eventName: string, listener: (...args: any[]) => any): void; +} + +interface IBasePtyForkOptions { + name?: string; + cols?: number; + rows?: number; + cwd?: string; + env?: IProcessEnv; + encoding?: string | null; + handleFlowControl?: boolean; + flowControlPause?: string; + flowControlResume?: string; +} + +export interface IPtyForkOptions extends IBasePtyForkOptions { + uid?: number; + gid?: number; +} + +export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { + useConpty?: boolean; + useConptyDll?: boolean; + conptyInheritCursor?: boolean; +} + +export interface IPtyOpenOptions { + cols?: number; + rows?: number; + encoding?: string | null; +} diff --git a/web/package/node-pty/src/native.d.ts b/web/package/node-pty/src/native.d.ts new file mode 100644 index 00000000..c53e086b --- /dev/null +++ b/web/package/node-pty/src/native.d.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +interface IConptyNative { + startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess; + connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; + resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; + clear(ptyId: number, useConptyDll: boolean): void; + kill(ptyId: number, useConptyDll: boolean): void; +} + +interface IWinptyNative { + startProcess(file: string, commandLine: string, env: string[], cwd: string, cols: number, rows: number, debug: boolean): IWinptyProcess; + resize(pid: number, cols: number, rows: number): void; + kill(pid: number, innerPid: number): void; + getProcessList(pid: number): number[]; + getExitCode(innerPid: number): number; +} + +interface IUnixNative { + fork(file: string, args: string[], parsedEnv: string[], cwd: string, cols: number, rows: number, uid: number, gid: number, useUtf8: boolean, helperPath: string, onExitCallback: (code: number, signal: number) => void): IUnixProcess; + open(cols: number, rows: number): IUnixOpenProcess; + process(fd: number, pty?: string): string; + resize(fd: number, cols: number, rows: number): void; +} + +interface IConptyProcess { + pty: number; + fd: number; + conin: string; + conout: string; +} + +interface IWinptyProcess { + pty: number; + fd: number; + conin: string; + conout: string; + pid: number; + innerPid: number; +} + +interface IUnixProcess { + fd: number; + pid: number; + pty: string; +} + +interface IUnixOpenProcess { + master: number; + slave: number; + pty: string; +} diff --git a/web/package/node-pty/src/terminal.ts b/web/package/node-pty/src/terminal.ts new file mode 100644 index 00000000..5bee9a79 --- /dev/null +++ b/web/package/node-pty/src/terminal.ts @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +import { Socket } from 'net'; +import { EventEmitter } from 'events'; +import { ITerminal, IPtyForkOptions, IProcessEnv } from './interfaces'; +import { EventEmitter2, IEvent } from './eventEmitter2'; +import { IExitEvent } from './types'; + +export const DEFAULT_COLS: number = 80; +export const DEFAULT_ROWS: number = 24; + +/** + * Default messages to indicate PAUSE/RESUME for automatic flow control. + * To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh), + * the sequences can be customized in `IPtyForkOptions`. + */ +const FLOW_CONTROL_PAUSE = '\x13'; // defaults to XOFF +const FLOW_CONTROL_RESUME = '\x11'; // defaults to XON + +export abstract class Terminal implements ITerminal { + protected _socket!: Socket; // HACK: This is unsafe + protected _pid: number = 0; + protected _fd: number = 0; + protected _pty: any; + + protected _file!: string; // HACK: This is unsafe + protected _name!: string; // HACK: This is unsafe + protected _cols: number = 0; + protected _rows: number = 0; + + protected _readable: boolean = false; + protected _writable: boolean = false; + + protected _internalee: EventEmitter; + private _flowControlPause: string; + private _flowControlResume: string; + public handleFlowControl: boolean; + + private _onData = new EventEmitter2<string>(); + public get onData(): IEvent<string> { return this._onData.event; } + private _onExit = new EventEmitter2<IExitEvent>(); + public get onExit(): IEvent<IExitEvent> { return this._onExit.event; } + + public get pid(): number { return this._pid; } + public get cols(): number { return this._cols; } + public get rows(): number { return this._rows; } + + constructor(opt?: IPtyForkOptions) { + // for 'close' + this._internalee = new EventEmitter(); + + // setup flow control handling + this.handleFlowControl = !!(opt?.handleFlowControl); + this._flowControlPause = opt?.flowControlPause || FLOW_CONTROL_PAUSE; + this._flowControlResume = opt?.flowControlResume || FLOW_CONTROL_RESUME; + + if (!opt) { + return; + } + + // Do basic type checks here in case node-pty is being used within JavaScript. If the wrong + // types go through to the C++ side it can lead to hard to diagnose exceptions. + this._checkType('name', opt.name ? opt.name : undefined, 'string'); + this._checkType('cols', opt.cols ? opt.cols : undefined, 'number'); + this._checkType('rows', opt.rows ? opt.rows : undefined, 'number'); + this._checkType('cwd', opt.cwd ? opt.cwd : undefined, 'string'); + this._checkType('env', opt.env ? opt.env : undefined, 'object'); + this._checkType('uid', opt.uid ? opt.uid : undefined, 'number'); + this._checkType('gid', opt.gid ? opt.gid : undefined, 'number'); + this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string'); + } + + protected abstract _write(data: string): void; + + public write(data: string): void { + if (this.handleFlowControl) { + // PAUSE/RESUME messages are not forwarded to the pty + if (data === this._flowControlPause) { + this.pause(); + return; + } + if (data === this._flowControlResume) { + this.resume(); + return; + } + } + // everything else goes to the real pty + this._write(data); + } + + protected _forwardEvents(): void { + this.on('data', e => this._onData.fire(e)); + this.on('exit', (exitCode, signal) => this._onExit.fire({ exitCode, signal })); + } + + protected _checkType<T>(name: string, value: T | undefined, type: string, allowArray: boolean = false): void { + if (value === undefined) { + return; + } + if (allowArray) { + if (Array.isArray(value)) { + value.forEach((v, i) => { + if (typeof v !== type) { + throw new Error(`${name}[${i}] must be a ${type} (not a ${typeof v})`); + } + }); + return; + } + } + if (typeof value !== type) { + throw new Error(`${name} must be a ${type} (not a ${typeof value})`); + } + } + + /** See net.Socket.end */ + public end(data: string): void { + this._socket.end(data); + } + + /** See stream.Readable.pipe */ + public pipe(dest: any, options: any): any { + return this._socket.pipe(dest, options); + } + + /** See net.Socket.pause */ + public pause(): Socket { + return this._socket.pause(); + } + + /** See net.Socket.resume */ + public resume(): Socket { + return this._socket.resume(); + } + + /** See net.Socket.setEncoding */ + public setEncoding(encoding: string | null): void { + if ((this._socket as any)._decoder) { + delete (this._socket as any)._decoder; + } + if (encoding) { + this._socket.setEncoding(encoding as BufferEncoding); + } + } + + public addListener(eventName: string, listener: (...args: any[]) => any): void { this.on(eventName, listener); } + public on(eventName: string, listener: (...args: any[]) => any): void { + if (eventName === 'close') { + this._internalee.on('close', listener); + return; + } + this._socket.on(eventName, listener); + } + + public emit(eventName: string, ...args: any[]): any { + if (eventName === 'close') { + return this._internalee.emit.apply(this._internalee, arguments as any); + } + return this._socket.emit.apply(this._socket, arguments as any); + } + + public listeners(eventName: string): Function[] { + return this._socket.listeners(eventName); + } + + public removeListener(eventName: string, listener: (...args: any[]) => any): void { + this._socket.removeListener(eventName, listener); + } + + public removeAllListeners(eventName: string): void { + this._socket.removeAllListeners(eventName); + } + + public once(eventName: string, listener: (...args: any[]) => any): void { + this._socket.once(eventName, listener); + } + + public abstract resize(cols: number, rows: number): void; + public abstract clear(): void; + public abstract destroy(): void; + public abstract kill(signal?: string): void; + + public abstract get process(): string; + public abstract get master(): Socket| undefined; + public abstract get slave(): Socket | undefined; + + protected _close(): void { + this._socket.readable = false; + this.write = () => {}; + this.end = () => {}; + this._writable = false; + this._readable = false; + } + + protected _parseEnv(env: IProcessEnv): string[] { + const keys = Object.keys(env || {}); + const pairs = []; + + for (let i = 0; i < keys.length; i++) { + if (keys[i] === undefined) { + continue; + } + pairs.push(keys[i] + '=' + env[keys[i]]); + } + + return pairs; + } +} diff --git a/web/package/node-pty/src/types.ts b/web/package/node-pty/src/types.ts new file mode 100644 index 00000000..94c2ac74 --- /dev/null +++ b/web/package/node-pty/src/types.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +export type ArgvOrCommandLine = string[] | string; + +export interface IExitEvent { + exitCode: number; + signal: number | undefined; +} + +export interface IDisposable { + dispose(): void; +} diff --git a/web/package/node-pty/src/unix/pty.cc b/web/package/node-pty/src/unix/pty.cc new file mode 100644 index 00000000..7c308d26 --- /dev/null +++ b/web/package/node-pty/src/unix/pty.cc @@ -0,0 +1,816 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2017, Daniel Imms (MIT License) + * + * pty.cc: + * This file is responsible for starting processes + * with pseudo-terminal file descriptors. + * + * See: + * man pty + * man tty_ioctl + * man termios + * man forkpty + */ + +/** + * Includes + */ + +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include <napi.h> +#include <assert.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <thread> + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <signal.h> + +/* forkpty */ +/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */ +#if defined(__linux__) +#include <pty.h> +#elif defined(__APPLE__) +#include <util.h> +#elif defined(__FreeBSD__) +#include <libutil.h> +#include <termios.h> +#endif + +/* Some platforms name VWERASE and VDISCARD differently */ +#if !defined(VWERASE) && defined(VWERSE) +#define VWERASE VWERSE +#endif +#if !defined(VDISCARD) && defined(VDISCRD) +#define VDISCARD VDISCRD +#endif + +/* for pty_getproc */ +#if defined(__linux__) +#include <stdio.h> +#include <stdint.h> +#elif defined(__APPLE__) +#include <libproc.h> +#include <os/availability.h> +#include <paths.h> +#include <spawn.h> +#include <sys/event.h> +#include <sys/sysctl.h> +#include <termios.h> +#endif + +/* NSIG - macro for highest signal + 1, should be defined */ +#ifndef NSIG +#define NSIG 32 +#endif + +/* macOS 10.14 back does not define this constant */ +#ifndef POSIX_SPAWN_SETSID + #define POSIX_SPAWN_SETSID 1024 +#endif + +/* environ for execvpe */ +/* node/src/node_child_process.cc */ +#if !defined(__APPLE__) +extern char **environ; +#endif + +#if defined(__APPLE__) +extern "C" { +// Changes the current thread's directory to a path or directory file +// descriptor. libpthread only exposes a syscall wrapper starting in +// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes, +// the syscall is issued directly. +int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12)); +int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12)); +} + +#define HANDLE_EINTR(x) ({ \ + int eintr_wrapper_counter = 0; \ + decltype(x) eintr_wrapper_result; \ + do { \ + eintr_wrapper_result = (x); \ + } while (eintr_wrapper_result == -1 && errno == EINTR && \ + eintr_wrapper_counter++ < 100); \ + eintr_wrapper_result; \ +}) +#endif + +struct ExitEvent { + int exit_code = 0, signal_code = 0; +}; + +void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) { + std::thread *th = new std::thread; + // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. + auto tsfn = Napi::ThreadSafeFunction::New( + env, + cb, // JavaScript function called asynchronously + "SetupExitCallback_resource", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + [th](Napi::Env) { // Finalizer used to clean threads up + th->join(); + delete th; + }); + *th = std::thread([tsfn = std::move(tsfn), pid] { + auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) { + cb.Call({Napi::Number::New(env, exit_event->exit_code), + Napi::Number::New(env, exit_event->signal_code)}); + delete exit_event; + }; + + int ret; + int stat_loc; +#if defined(__APPLE__) + // Based on + // https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69? + int kq = HANDLE_EINTR(kqueue()); + struct kevent change = {0}; + EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); + ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL)); + if (ret == -1) { + if (errno == ESRCH) { + // At this point, one of the following has occurred: + // 1. The process has died but has not yet been reaped. + // 2. The process has died and has already been reaped. + // 3. The process is in the process of dying. It's no longer + // kqueueable, but it may not be waitable yet either. Mark calls + // this case the "zombie death race". + ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG)); + if (ret == 0) { + ret = kill(pid, SIGKILL); + if (ret != -1) { + HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); + } + } + } + } else { + struct kevent event = {0}; + ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL)); + if (ret == 1) { + if ((event.fflags & NOTE_EXIT) && + (event.ident == static_cast<uintptr_t>(pid))) { + // The process is dead or dying. This won't block for long, if at + // all. + HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); + } + } + } +#else + while (true) { + errno = 0; + if ((ret = waitpid(pid, &stat_loc, 0)) != pid) { + if (ret == -1 && errno == EINTR) { + continue; + } + if (ret == -1 && errno == ECHILD) { + // XXX node v0.8.x seems to have this problem. + // waitpid is already handled elsewhere. + ; + } else { + assert(false); + } + } + break; + } +#endif + ExitEvent *exit_event = new ExitEvent; + if (WIFEXITED(stat_loc)) { + exit_event->exit_code = WEXITSTATUS(stat_loc); // errno? + } + if (WIFSIGNALED(stat_loc)) { + exit_event->signal_code = WTERMSIG(stat_loc); + } + auto status = tsfn.BlockingCall(exit_event, callback); // In main thread + switch (status) { + case napi_closing: + break; + + case napi_queue_full: + Napi::Error::Fatal("SetupExitCallback", "Queue was full"); + + case napi_ok: + if (tsfn.Release() != napi_ok) { + Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed"); + } + break; + + default: + Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed"); + } + }); +} + +/** + * Methods + */ + +Napi::Value PtyFork(const Napi::CallbackInfo& info); +Napi::Value PtyOpen(const Napi::CallbackInfo& info); +Napi::Value PtyResize(const Napi::CallbackInfo& info); +Napi::Value PtyGetProc(const Napi::CallbackInfo& info); + +/** + * Functions + */ + +static int +pty_nonblock(int); + +#if defined(__APPLE__) +static char * +pty_getproc(int); +#else +static char * +pty_getproc(int, char *); +#endif + +#if defined(__APPLE__) || defined(__OpenBSD__) +static void +pty_posix_spawn(char** argv, char** env, + const struct termios *termp, + const struct winsize *winp, + int* master, + pid_t* pid, + int* err); +#endif + +struct DelBuf { + int len; + DelBuf(int len) : len(len) {} + void operator()(char **p) { + if (p == nullptr) + return; + for (int i = 0; i < len; i++) + free(p[i]); + delete[] p; + } +}; + +Napi::Value PtyFork(const Napi::CallbackInfo& info) { + Napi::Env napiEnv(info.Env()); + Napi::HandleScope scope(napiEnv); + + if (info.Length() != 11 || + !info[0].IsString() || + !info[1].IsArray() || + !info[2].IsArray() || + !info[3].IsString() || + !info[4].IsNumber() || + !info[5].IsNumber() || + !info[6].IsNumber() || + !info[7].IsNumber() || + !info[8].IsBoolean() || + !info[9].IsString() || + !info[10].IsFunction()) { + throw Napi::Error::New(napiEnv, "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)"); + } + + // file + std::string file = info[0].As<Napi::String>(); + + // args + Napi::Array argv_ = info[1].As<Napi::Array>(); + + // env + Napi::Array env_ = info[2].As<Napi::Array>(); + int envc = env_.Length(); + std::unique_ptr<char *, DelBuf> env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1)); + char **env = env_unique_ptr.get(); + env[envc] = NULL; + for (int i = 0; i < envc; i++) { + std::string pair = env_.Get(i).As<Napi::String>(); + env[i] = strdup(pair.c_str()); + } + + // cwd + std::string cwd_ = info[3].As<Napi::String>(); + + // size + struct winsize winp; + winp.ws_col = info[4].As<Napi::Number>().Int32Value(); + winp.ws_row = info[5].As<Napi::Number>().Int32Value(); + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + +#if !defined(__APPLE__) + // uid / gid + int uid = info[6].As<Napi::Number>().Int32Value(); + int gid = info[7].As<Napi::Number>().Int32Value(); +#endif + + // termios + struct termios t = termios(); + struct termios *term = &t; + term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; + if (info[8].As<Napi::Boolean>().Value()) { +#if defined(IUTF8) + term->c_iflag |= IUTF8; +#endif + } + term->c_oflag = OPOST | ONLCR; + term->c_cflag = CREAD | CS8 | HUPCL; + term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL; + + term->c_cc[VEOF] = 4; + term->c_cc[VEOL] = -1; + term->c_cc[VEOL2] = -1; + term->c_cc[VERASE] = 0x7f; + term->c_cc[VWERASE] = 23; + term->c_cc[VKILL] = 21; + term->c_cc[VREPRINT] = 18; + term->c_cc[VINTR] = 3; + term->c_cc[VQUIT] = 0x1c; + term->c_cc[VSUSP] = 26; + term->c_cc[VSTART] = 17; + term->c_cc[VSTOP] = 19; + term->c_cc[VLNEXT] = 22; + term->c_cc[VDISCARD] = 15; + term->c_cc[VMIN] = 1; + term->c_cc[VTIME] = 0; + + #if (__APPLE__) + term->c_cc[VDSUSP] = 25; + term->c_cc[VSTATUS] = 20; + #endif + + cfsetispeed(term, B38400); + cfsetospeed(term, B38400); + + // helperPath + std::string helper_path = info[9].As<Napi::String>(); + + pid_t pid; + int master; +#if defined(__APPLE__) + int argc = argv_.Length(); + int argl = argc + 4; + std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl)); + char **argv = argv_unique_ptr.get(); + argv[0] = strdup(helper_path.c_str()); + argv[1] = strdup(cwd_.c_str()); + argv[2] = strdup(file.c_str()); + argv[argl - 1] = NULL; + for (int i = 0; i < argc; i++) { + std::string arg = argv_.Get(i).As<Napi::String>(); + argv[i + 3] = strdup(arg.c_str()); + } + + int err = -1; + pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err); + if (err != 0) { + throw Napi::Error::New(napiEnv, "posix_spawnp failed."); + } + if (pty_nonblock(master) == -1) { + throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); + } +#else + int argc = argv_.Length(); + int argl = argc + 2; + std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl)); + char** argv = argv_unique_ptr.get(); + argv[0] = strdup(file.c_str()); + argv[argl - 1] = NULL; + for (int i = 0; i < argc; i++) { + std::string arg = argv_.Get(i).As<Napi::String>(); + argv[i + 1] = strdup(arg.c_str()); + } + + sigset_t newmask, oldmask; + struct sigaction sig_action; + // temporarily block all signals + // this is needed due to a race condition in openpty + // and to avoid running signal handlers in the child + // before exec* happened + sigfillset(&newmask); + pthread_sigmask(SIG_SETMASK, &newmask, &oldmask); + + pid = forkpty(&master, nullptr, static_cast<termios*>(term), static_cast<winsize*>(&winp)); + + if (!pid) { + // remove all signal handler from child + sig_action.sa_handler = SIG_DFL; + sig_action.sa_flags = 0; + sigemptyset(&sig_action.sa_mask); + for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1 + sigaction(i, &sig_action, NULL); + } + } + + // reenable signals + pthread_sigmask(SIG_SETMASK, &oldmask, NULL); + + switch (pid) { + case -1: + throw Napi::Error::New(napiEnv, "forkpty(3) failed."); + case 0: + if (strlen(cwd_.c_str())) { + if (chdir(cwd_.c_str()) == -1) { + perror("chdir(2) failed."); + _exit(1); + } + } + + if (uid != -1 && gid != -1) { + if (setgid(gid) == -1) { + perror("setgid(2) failed."); + _exit(1); + } + if (setuid(uid) == -1) { + perror("setuid(2) failed."); + _exit(1); + } + } + + { + char **old = environ; + environ = env; + execvp(argv[0], argv); + environ = old; + perror("execvp(3) failed."); + _exit(1); + } + default: + if (pty_nonblock(master) == -1) { + throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); + } + } +#endif + + Napi::Object obj = Napi::Object::New(napiEnv); + obj.Set("fd", Napi::Number::New(napiEnv, master)); + obj.Set("pid", Napi::Number::New(napiEnv, pid)); +#if defined(__APPLE__) + // Use TIOCPTYGNAME instead of ptsname() to avoid threading problems. + char slave_pty_name[128]; + if (ioctl(master, TIOCPTYGNAME, slave_pty_name) != -1) { + obj.Set("pty", Napi::String::New(napiEnv, slave_pty_name)); + } else { + obj.Set("pty", Napi::String::New(napiEnv, "")); + } +#else + obj.Set("pty", Napi::String::New(napiEnv, ptsname(master))); +#endif + + // Set up process exit callback. + Napi::Function cb = info[10].As<Napi::Function>(); + SetupExitCallback(napiEnv, cb, pid); + return obj; +} + +Napi::Value PtyOpen(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 2 || + !info[0].IsNumber() || + !info[1].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.open(cols, rows)"); + } + + // size + struct winsize winp; + winp.ws_col = info[0].As<Napi::Number>().Int32Value(); + winp.ws_row = info[1].As<Napi::Number>().Int32Value(); + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + + // pty + int master, slave; + int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize*>(&winp)); + + if (ret == -1) { + throw Napi::Error::New(env, "openpty(3) failed."); + } + + if (pty_nonblock(master) == -1) { + throw Napi::Error::New(env, "Could not set master fd to nonblocking."); + } + + if (pty_nonblock(slave) == -1) { + throw Napi::Error::New(env, "Could not set slave fd to nonblocking."); + } + + Napi::Object obj = Napi::Object::New(env); + obj.Set("master", Napi::Number::New(env, master)); + obj.Set("slave", Napi::Number::New(env, slave)); +#if defined(__APPLE__) + // Use TIOCPTYGNAME instead of ptsname() to avoid threading problems. + char slave_pty_name[128]; + if (ioctl(master, TIOCPTYGNAME, slave_pty_name) != -1) { + obj.Set("pty", Napi::String::New(env, slave_pty_name)); + } else { + obj.Set("pty", Napi::String::New(env, "")); + } +#else + obj.Set("pty", Napi::String::New(env, ptsname(master))); +#endif + + return obj; +} + +Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 3 || + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.resize(fd, cols, rows)"); + } + + int fd = info[0].As<Napi::Number>().Int32Value(); + + struct winsize winp; + winp.ws_col = info[1].As<Napi::Number>().Int32Value(); + winp.ws_row = info[2].As<Napi::Number>().Int32Value(); + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + + if (ioctl(fd, TIOCSWINSZ, &winp) == -1) { + switch (errno) { + case EBADF: + throw Napi::Error::New(env, "ioctl(2) failed, EBADF"); + case EFAULT: + throw Napi::Error::New(env, "ioctl(2) failed, EFAULT"); + case EINVAL: + throw Napi::Error::New(env, "ioctl(2) failed, EINVAL"); + case ENOTTY: + throw Napi::Error::New(env, "ioctl(2) failed, ENOTTY"); + } + throw Napi::Error::New(env, "ioctl(2) failed"); + } + + return env.Undefined(); +} + +/** + * Foreground Process Name + */ +Napi::Value PtyGetProc(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + +#if defined(__APPLE__) + if (info.Length() != 1 || + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.process(pid)"); + } + + int fd = info[0].As<Napi::Number>().Int32Value(); + char *name = pty_getproc(fd); +#else + if (info.Length() != 2 || + !info[0].IsNumber() || + !info[1].IsString()) { + throw Napi::Error::New(env, "Usage: pty.process(fd, tty)"); + } + + int fd = info[0].As<Napi::Number>().Int32Value(); + + std::string tty_ = info[1].As<Napi::String>(); + char *tty = strdup(tty_.c_str()); + char *name = pty_getproc(fd, tty); + free(tty); +#endif + + if (name == NULL) { + return env.Undefined(); + } + + Napi::String name_ = Napi::String::New(env, name); + free(name); + return name_; +} + +/** + * Nonblocking FD + */ + +static int +pty_nonblock(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +/** + * pty_getproc + * Taken from tmux. + */ + +// Taken from: tmux (http://tmux.sourceforge.net/) +// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net> +// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org> +// Copyright (c) 2009 Todd Carson <toc@daybefore.net> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER +// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#if defined(__linux__) + +static char * +pty_getproc(int fd, char *tty) { + FILE *f; + char *path, *buf; + size_t len; + int ch; + pid_t pgrp; + int r; + + if ((pgrp = tcgetpgrp(fd)) == -1) { + return NULL; + } + + r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp); + if (r == -1 || path == NULL) return NULL; + + if ((f = fopen(path, "r")) == NULL) { + free(path); + return NULL; + } + + free(path); + + len = 0; + buf = NULL; + while ((ch = fgetc(f)) != EOF) { + if (ch == '\0') break; + buf = (char *)realloc(buf, len + 2); + if (buf == NULL) return NULL; + buf[len++] = ch; + } + + if (buf != NULL) { + buf[len] = '\0'; + } + + fclose(f); + return buf; +} + +#elif defined(__APPLE__) + +static char * +pty_getproc(int fd) { + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 }; + size_t size; + struct kinfo_proc kp; + + if ((mib[3] = tcgetpgrp(fd)) == -1) { + return NULL; + } + + size = sizeof kp; + if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) { + return NULL; + } + + if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') { + return NULL; + } + + return strdup(kp.kp_proc.p_comm); +} + +#else + +static char * +pty_getproc(int fd, char *tty) { + return NULL; +} + +#endif + +#if defined(__APPLE__) +static void +pty_posix_spawn(char** argv, char** env, + const struct termios *termp, + const struct winsize *winp, + int* master, + pid_t* pid, + int* err) { + int low_fds[3]; + size_t count = 0; + + for (; count < 3; count++) { + low_fds[count] = posix_openpt(O_RDWR); + if (low_fds[count] >= STDERR_FILENO) + break; + } + + int flags = POSIX_SPAWN_CLOEXEC_DEFAULT | + POSIX_SPAWN_SETSIGDEF | + POSIX_SPAWN_SETSIGMASK | + POSIX_SPAWN_SETSID; + *master = posix_openpt(O_RDWR); + if (*master == -1) { + return; + } + + int res = grantpt(*master) || unlockpt(*master); + if (res == -1) { + return; + } + + // Use TIOCPTYGNAME instead of ptsname() to avoid threading problems. + int slave; + char slave_pty_name[128]; + res = ioctl(*master, TIOCPTYGNAME, slave_pty_name); + if (res == -1) { + return; + } + + slave = open(slave_pty_name, O_RDWR | O_NOCTTY); + if (slave == -1) { + return; + } + + if (termp) { + res = tcsetattr(slave, TCSANOW, termp); + if (res == -1) { + return; + }; + } + + if (winp) { + res = ioctl(slave, TIOCSWINSZ, winp); + if (res == -1) { + return; + } + } + + posix_spawn_file_actions_t acts; + posix_spawn_file_actions_init(&acts); + posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO); + posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO); + posix_spawn_file_actions_addclose(&acts, slave); + posix_spawn_file_actions_addclose(&acts, *master); + + posix_spawnattr_t attrs; + posix_spawnattr_init(&attrs); + *err = posix_spawnattr_setflags(&attrs, flags); + if (*err != 0) { + goto done; + } + + sigset_t signal_set; + /* Reset all signal the child to their default behavior */ + sigfillset(&signal_set); + *err = posix_spawnattr_setsigdefault(&attrs, &signal_set); + if (*err != 0) { + goto done; + } + + /* Reset the signal mask for all signals */ + sigemptyset(&signal_set); + *err = posix_spawnattr_setsigmask(&attrs, &signal_set); + if (*err != 0) { + goto done; + } + + do + *err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env); + while (*err == EINTR); +done: + posix_spawn_file_actions_destroy(&acts); + posix_spawnattr_destroy(&attrs); + + for (; count > 0; count--) { + close(low_fds[count]); + } +} +#endif + +/** + * Init + */ + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("fork", Napi::Function::New(env, PtyFork)); + exports.Set("open", Napi::Function::New(env, PtyOpen)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("process", Napi::Function::New(env, PtyGetProc)); + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/web/package/node-pty/src/unixTerminal.ts b/web/package/node-pty/src/unixTerminal.ts new file mode 100644 index 00000000..300c60d1 --- /dev/null +++ b/web/package/node-pty/src/unixTerminal.ts @@ -0,0 +1,332 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ +import * as net from 'net'; +import * as path from 'path'; +import * as tty from 'tty'; +import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal'; +import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; +import { assign } from './utils'; + +let pty: IUnixNative; +let helperPath: string; + +// Check if running in SEA (Single Executable Application) context +if (process.env.VIBETUNNEL_SEA) { + // In SEA mode, load native module using process.dlopen + const fs = require('fs'); + const execDir = path.dirname(process.execPath); + const ptyPath = path.join(execDir, 'pty.node'); + + if (fs.existsSync(ptyPath)) { + const module = { exports: {} }; + process.dlopen(module, ptyPath); + pty = module.exports as IUnixNative; + } else { + throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`); + } + + // Set spawn-helper path for macOS only (Linux doesn't use it) + if (process.platform === 'darwin') { + helperPath = path.join(execDir, 'spawn-helper'); + if (!fs.existsSync(helperPath)) { + console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`); + } + } +} else { + // Standard Node.js loading + try { + pty = require('../build/Release/pty.node'); + helperPath = '../build/Release/spawn-helper'; + } catch (outerError) { + try { + pty = require('../build/Debug/pty.node'); + helperPath = '../build/Debug/spawn-helper'; + } catch (innerError) { + console.error('innerError', innerError); + throw outerError; + } + } + + helperPath = path.resolve(__dirname, helperPath); + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); +} + +const DEFAULT_FILE = 'sh'; +const DEFAULT_NAME = 'xterm'; +const DESTROY_SOCKET_TIMEOUT_MS = 200; + +export class UnixTerminal extends Terminal { + protected _fd: number; + protected _pty: string; + + protected _file: string; + protected _name: string; + + protected _readable: boolean; + protected _writable: boolean; + + private _boundClose: boolean = false; + private _emittedClose: boolean = false; + private _master: net.Socket | undefined; + private _slave: net.Socket | undefined; + + public get master(): net.Socket | undefined { return this._master; } + public get slave(): net.Socket | undefined { return this._slave; } + + constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) { + super(opt); + + if (typeof args === 'string') { + throw new Error('args as a string is not supported on unix.'); + } + + // Initialize arguments + args = args || []; + file = file || DEFAULT_FILE; + opt = opt || {}; + opt.env = opt.env || process.env; + + this._cols = opt.cols || DEFAULT_COLS; + this._rows = opt.rows || DEFAULT_ROWS; + const uid = opt.uid ?? -1; + const gid = opt.gid ?? -1; + const env: IProcessEnv = assign({}, opt.env); + + if (opt.env === process.env) { + this._sanitizeEnv(env); + } + + const cwd = opt.cwd || process.cwd(); + env.PWD = cwd; + const name = opt.name || env.TERM || DEFAULT_NAME; + env.TERM = name; + const parsedEnv = this._parseEnv(env); + + const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); + + const onexit = (code: number, signal: number): void => { + // XXX Sometimes a data event is emitted after exit. Wait til socket is + // destroyed. + if (!this._emittedClose) { + if (this._boundClose) { + return; + } + this._boundClose = true; + // From macOS High Sierra 10.13.2 sometimes the socket never gets + // closed. A timeout is applied here to avoid the terminal never being + // destroyed when this occurs. + let timeout: NodeJS.Timeout | null = setTimeout(() => { + timeout = null; + // Destroying the socket now will cause the close event to fire + this._socket.destroy(); + }, DESTROY_SOCKET_TIMEOUT_MS); + this.once('close', () => { + if (timeout !== null) { + clearTimeout(timeout); + } + this.emit('exit', code, signal); + }); + return; + } + this.emit('exit', code, signal); + }; + + // fork + const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit); + + this._socket = new tty.ReadStream(term.fd); + if (encoding !== null) { + this._socket.setEncoding(encoding as BufferEncoding); + } + + // setup + this._socket.on('error', (err: any) => { + // NOTE: fs.ReadStream gets EAGAIN twice at first: + if (err.code) { + if (~err.code.indexOf('EAGAIN')) { + return; + } + } + + // close + this._close(); + // EIO on exit from fs.ReadStream: + if (!this._emittedClose) { + this._emittedClose = true; + this.emit('close'); + } + + // EIO, happens when someone closes our child process: the only process in + // the terminal. + // node < 0.6.14: errno 5 + // node >= 0.6.14: read EIO + if (err.code) { + if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) { + return; + } + } + + // throw anything else + if (this.listeners('error').length < 2) { + throw err; + } + }); + + this._pid = term.pid; + this._fd = term.fd; + this._pty = term.pty; + + this._file = file; + this._name = name; + + this._readable = true; + this._writable = true; + + this._socket.on('close', () => { + if (this._emittedClose) { + return; + } + this._emittedClose = true; + this._close(); + this.emit('close'); + }); + + this._forwardEvents(); + } + + protected _write(data: string): void { + this._socket.write(data); + } + + /* Accessors */ + get fd(): number { return this._fd; } + get ptsName(): string { return this._pty; } + + /** + * openpty + */ + + public static open(opt: IPtyOpenOptions): UnixTerminal { + const self: UnixTerminal = Object.create(UnixTerminal.prototype); + opt = opt || {}; + + if (arguments.length > 1) { + opt = { + cols: arguments[1], + rows: arguments[2] + }; + } + + const cols = opt.cols || DEFAULT_COLS; + const rows = opt.rows || DEFAULT_ROWS; + const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); + + // open + const term: IUnixOpenProcess = pty.open(cols, rows); + + self._master = new tty.ReadStream(term.master); + if (encoding !== null) { + self._master.setEncoding(encoding as BufferEncoding); + } + self._master.resume(); + + self._slave = new tty.ReadStream(term.slave); + if (encoding !== null) { + self._slave.setEncoding(encoding as BufferEncoding); + } + self._slave.resume(); + + self._socket = self._master; + self._pid = -1; + self._fd = term.master; + self._pty = term.pty; + + self._file = process.argv[0] || 'node'; + self._name = process.env.TERM || ''; + + self._readable = true; + self._writable = true; + + self._socket.on('error', err => { + self._close(); + if (self.listeners('error').length < 2) { + throw err; + } + }); + + self._socket.on('close', () => { + self._close(); + }); + + return self; + } + + public destroy(): void { + this._close(); + + // Need to close the read stream so node stops reading a dead file + // descriptor. Then we can safely SIGHUP the shell. + this._socket.once('close', () => { + this.kill('SIGHUP'); + }); + + this._socket.destroy(); + } + + public kill(signal?: string): void { + try { + process.kill(this.pid, signal || 'SIGHUP'); + } catch (e) { /* swallow */ } + } + + /** + * Gets the name of the process. + */ + public get process(): string { + if (process.platform === 'darwin') { + const title = pty.process(this._fd); + return (title !== 'kernel_task' ) ? title : this._file; + } + + return pty.process(this._fd, this._pty) || this._file; + } + + /** + * TTY + */ + + public resize(cols: number, rows: number): void { + if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) { + throw new Error('resizing must be done using positive cols and rows'); + } + pty.resize(this._fd, cols, rows); + this._cols = cols; + this._rows = rows; + } + + public clear(): void { + + } + + private _sanitizeEnv(env: IProcessEnv): void { + // Make sure we didn't start our server from inside tmux. + delete env['TMUX']; + delete env['TMUX_PANE']; + + // Make sure we didn't start our server from inside screen. + // http://web.mit.edu/gnu/doc/html/screen_20.html + delete env['STY']; + delete env['WINDOW']; + + // Delete some variables that might confuse our terminal. + delete env['WINDOWID']; + delete env['TERMCAP']; + delete env['COLUMNS']; + delete env['LINES']; + } +} diff --git a/web/package/node-pty/src/utils.ts b/web/package/node-pty/src/utils.ts new file mode 100644 index 00000000..6306c8ba --- /dev/null +++ b/web/package/node-pty/src/utils.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +export function assign(target: any, ...sources: any[]): any { + sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key])); + return target; +} diff --git a/web/package/node-pty/src/win/conpty.cc b/web/package/node-pty/src/win/conpty.cc new file mode 100644 index 00000000..7b286d3d --- /dev/null +++ b/web/package/node-pty/src/win/conpty.cc @@ -0,0 +1,583 @@ +/** + * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + * + * pty.cc: + * This file is responsible for starting processes + * with pseudo-terminal file descriptors. + */ + +#define _WIN32_WINNT 0x600 + +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include <node_api.h> +#include <assert.h> +#include <Shlwapi.h> // PathCombine, PathIsRelative +#include <sstream> +#include <iostream> +#include <string> +#include <thread> +#include <vector> +#include <Windows.h> +#include <strsafe.h> +#include "path_util.h" +#include "conpty.h" + +// Taken from the RS5 Windows SDK, but redefined here in case we're targeting <= 17134 +#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE +#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \ + ProcThreadAttributeValue(22, FALSE, TRUE, FALSE) + +typedef VOID* HPCON; +typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE hOut, DWORD dwFlags, HPCON* phpcon); +typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize); +typedef HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc); +typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc); +typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc); + +#endif + +struct pty_baton { + int id; + HANDLE hIn; + HANDLE hOut; + HPCON hpc; + + HANDLE hShell; + + pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {}; +}; + +static std::vector<std::unique_ptr<pty_baton>> ptyHandles; +static volatile LONG ptyCounter; + +static pty_baton* get_pty_baton(int id) { + auto it = std::find_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) { + return ptyHandle->id == id; + }); + if (it != ptyHandles.end()) { + return it->get(); + } + return nullptr; +} + +static bool remove_pty_baton(int id) { + auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) { + return ptyHandle->id == id; + }); + if (it != ptyHandles.end()) { + ptyHandles.erase(it); + return true; + } + return false; +} + +struct ExitEvent { + int exit_code = 0; +}; + +void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { + std::thread *th = new std::thread; + // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. + auto tsfn = Napi::ThreadSafeFunction::New( + env, + cb, // JavaScript function called asynchronously + "SetupExitCallback_resource", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + [th](Napi::Env) { // Finalizer used to clean threads up + th->join(); + delete th; + }); + *th = std::thread([tsfn = std::move(tsfn), baton] { + auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) { + cb.Call({Napi::Number::New(env, exit_event->exit_code)}); + delete exit_event; + }; + + ExitEvent *exit_event = new ExitEvent; + // Wait for process to complete. + WaitForSingleObject(baton->hShell, INFINITE); + // Get process exit code. + GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); + // Clean up handles + CloseHandle(baton->hShell); + assert(remove_pty_baton(baton->id)); + + auto status = tsfn.BlockingCall(exit_event, callback); // In main thread + switch (status) { + case napi_closing: + break; + + case napi_queue_full: + Napi::Error::Fatal("SetupExitCallback", "Queue was full"); + + case napi_ok: + if (tsfn.Release() != napi_ok) { + Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed"); + } + break; + + default: + Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed"); + } + }); +} + +Napi::Error errorWithCode(const Napi::CallbackInfo& info, const char* text) { + std::stringstream errorText; + errorText << text; + errorText << ", error code: " << GetLastError(); + return Napi::Error::New(info.Env(), errorText.str()); +} + +// Returns a new server named pipe. It has not yet been connected. +bool createDataServerPipe(bool write, + std::wstring kind, + HANDLE* hServer, + std::wstring &name, + const std::wstring &pipeName) +{ + *hServer = INVALID_HANDLE_VALUE; + + name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind; + + const DWORD winOpenMode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE/* | FILE_FLAG_OVERLAPPED */; + + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + + *hServer = CreateNamedPipeW( + name.c_str(), + /*dwOpenMode=*/winOpenMode, + /*dwPipeMode=*/PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + /*nMaxInstances=*/1, + /*nOutBufferSize=*/128 * 1024, + /*nInBufferSize=*/128 * 1024, + /*nDefaultTimeOut=*/30000, + &sa); + + return *hServer != INVALID_HANDLE_VALUE; +} + +HANDLE LoadConptyDll(const Napi::CallbackInfo& info, + const bool useConptyDll) +{ + if (!useConptyDll) { + return LoadLibraryExW(L"kernel32.dll", 0, 0); + } + wchar_t currentDir[MAX_PATH]; + HMODULE hModule = GetModuleHandleA("conpty.node"); + if (hModule == NULL) { + throw errorWithCode(info, "Failed to get conpty.node module handle"); + } + DWORD result = GetModuleFileNameW(hModule, currentDir, MAX_PATH); + if (result == 0) { + throw errorWithCode(info, "Failed to get conpty.node module file name"); + } + PathRemoveFileSpecW(currentDir); + wchar_t conptyDllPath[MAX_PATH]; + PathCombineW(conptyDllPath, currentDir, L"conpty\\conpty.dll"); + if (!path_util::file_exists(conptyDllPath)) { + std::wstring errorMessage = L"Cannot find conpty.dll at " + std::wstring(conptyDllPath); + std::string errorMessageStr = path_util::wstring_to_string(errorMessage); + throw errorWithCode(info, errorMessageStr.c_str()); + } + + return LoadLibraryW(conptyDllPath); +} + +HRESULT CreateNamedPipesAndPseudoConsole(const Napi::CallbackInfo& info, + COORD size, + DWORD dwFlags, + HANDLE *phInput, + HANDLE *phOutput, + HPCON* phPC, + std::wstring& inName, + std::wstring& outName, + const std::wstring& pipeName, + const bool useConptyDll) +{ + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + DWORD error = GetLastError(); + bool fLoadedDll = hLibrary != nullptr; + if (fLoadedDll) + { + PFNCREATEPSEUDOCONSOLE const pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, + useConptyDll ? "ConptyCreatePseudoConsole" : "CreatePseudoConsole"); + if (pfnCreate) + { + if (phPC == NULL || phInput == NULL || phOutput == NULL) + { + return E_INVALIDARG; + } + + bool success = createDataServerPipe(true, L"in", phInput, inName, pipeName); + if (!success) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + success = createDataServerPipe(false, L"out", phOutput, outName, pipeName); + if (!success) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC); + } + else + { + // Failed to find CreatePseudoConsole in kernel32. This is likely because + // the user is not running a build of Windows that supports that API. + // We should fall back to winpty in this case. + return HRESULT_FROM_WIN32(GetLastError()); + } + } else { + throw errorWithCode(info, "Failed to load conpty.dll"); + } + + // Failed to find kernel32. This is realy unlikely - honestly no idea how + // this is even possible to hit. But if it does happen, fall back to winpty. + return HRESULT_FROM_WIN32(GetLastError()); +} + +static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + Napi::Object marshal; + std::wstring inName, outName; + BOOL fSuccess = FALSE; + std::unique_ptr<wchar_t[]> mutableCommandline; + PROCESS_INFORMATION _piClient{}; + + if (info.Length() != 7 || + !info[0].IsString() || + !info[1].IsNumber() || + !info[2].IsNumber() || + !info[3].IsBoolean() || + !info[4].IsString() || + !info[5].IsBoolean() || + !info[6].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.startProcess(file, cols, rows, debug, pipeName, inheritCursor, useConptyDll)"); + } + + const std::wstring filename(path_util::to_wstring(info[0].As<Napi::String>())); + const SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value()); + const SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value()); + const bool debug = info[3].As<Napi::Boolean>().Value(); + const std::wstring pipeName(path_util::to_wstring(info[4].As<Napi::String>())); + const bool inheritCursor = info[5].As<Napi::Boolean>().Value(); + const bool useConptyDll = info[6].As<Napi::Boolean>().Value(); + + // use environment 'Path' variable to determine location of + // the relative path that we have recieved (e.g cmd.exe) + std::wstring shellpath; + if (::PathIsRelativeW(filename.c_str())) { + shellpath = path_util::get_shell_path(filename.c_str()); + } else { + shellpath = filename; + } + + if (shellpath.empty() || !path_util::file_exists(shellpath)) { + std::string why; + why += "File not found: "; + why += path_util::wstring_to_string(shellpath); + throw Napi::Error::New(env, why); + } + + HANDLE hIn, hOut; + HPCON hpc; + HRESULT hr = CreateNamedPipesAndPseudoConsole(info, {cols, rows}, inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0, &hIn, &hOut, &hpc, inName, outName, pipeName, useConptyDll); + + // Restore default handling of ctrl+c + SetConsoleCtrlHandler(NULL, FALSE); + + // Set return values + marshal = Napi::Object::New(env); + + if (SUCCEEDED(hr)) { + // We were able to instantiate a conpty + const int ptyId = InterlockedIncrement(&ptyCounter); + marshal.Set("pty", Napi::Number::New(env, ptyId)); + ptyHandles.emplace_back( + std::make_unique<pty_baton>(ptyId, hIn, hOut, hpc)); + } else { + throw Napi::Error::New(env, "Cannot launch conpty"); + } + + std::string inNameStr = path_util::wstring_to_string(inName); + if (inNameStr.empty()) { + throw Napi::Error::New(env, "Failed to initialize conpty conin"); + } + std::string outNameStr = path_util::wstring_to_string(outName); + if (outNameStr.empty()) { + throw Napi::Error::New(env, "Failed to initialize conpty conout"); + } + + marshal.Set("fd", Napi::Number::New(env, -1)); + marshal.Set("conin", Napi::String::New(env, inNameStr)); + marshal.Set("conout", Napi::String::New(env, outNameStr)); + return marshal; +} + +static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + // If we're working with conpty's we need to call ConnectNamedPipe here AFTER + // the Socket has attempted to connect to the other end, then actually + // spawn the process here. + + std::stringstream errorText; + BOOL fSuccess = FALSE; + + if (info.Length() != 6 || + !info[0].IsNumber() || + !info[1].IsString() || + !info[2].IsString() || + !info[3].IsArray() || + !info[4].IsBoolean() || + !info[5].IsFunction()) { + throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, useConptyDll, exitCallback)"); + } + + const int id = info[0].As<Napi::Number>().Int32Value(); + const std::wstring cmdline(path_util::to_wstring(info[1].As<Napi::String>())); + const std::wstring cwd(path_util::to_wstring(info[2].As<Napi::String>())); + const Napi::Array envValues = info[3].As<Napi::Array>(); + const bool useConptyDll = info[4].As<Napi::Boolean>().Value(); + Napi::Function exitCallback = info[5].As<Napi::Function>(); + + // Fetch pty handle from ID and start process + pty_baton* handle = get_pty_baton(id); + if (!handle) { + throw Napi::Error::New(env, "Invalid pty handle"); + } + + // Prepare command line + std::unique_ptr<wchar_t[]> mutableCommandline = std::make_unique<wchar_t[]>(cmdline.length() + 1); + HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1, cmdline.c_str()); + + // Prepare cwd + std::unique_ptr<wchar_t[]> mutableCwd = std::make_unique<wchar_t[]>(cwd.length() + 1); + hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str()); + + // Prepare environment + std::wstring envStr; + if (!envValues.IsEmpty()) { + std::wstring envBlock; + for(uint32_t i = 0; i < envValues.Length(); i++) { + envBlock += path_util::to_wstring(envValues.Get(i).As<Napi::String>()); + envBlock += L'\0'; + } + envBlock += L'\0'; + envStr = std::move(envBlock); + } + std::vector<wchar_t> envV(envStr.cbegin(), envStr.cend()); + LPWSTR envArg = envV.empty() ? nullptr : envV.data(); + + ConnectNamedPipe(handle->hIn, nullptr); + ConnectNamedPipe(handle->hOut, nullptr); + + // Attach the pseudoconsole to the client application we're creating + STARTUPINFOEXW siEx{0}; + siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW); + siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + siEx.StartupInfo.hStdError = nullptr; + siEx.StartupInfo.hStdInput = nullptr; + siEx.StartupInfo.hStdOutput = nullptr; + + SIZE_T size = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &size); + BYTE *attrList = new BYTE[size]; + siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList); + + fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size); + if (!fSuccess) { + throw errorWithCode(info, "InitializeProcThreadAttributeList failed"); + } + fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + handle->hpc, + sizeof(HPCON), + NULL, + NULL); + if (!fSuccess) { + throw errorWithCode(info, "UpdateProcThreadAttribute failed"); + } + + PROCESS_INFORMATION piClient{}; + fSuccess = !!CreateProcessW( + nullptr, + mutableCommandline.get(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles VERY IMPORTANT that this is false + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags + envArg, // lpEnvironment + mutableCwd.get(), // lpCurrentDirectory + &siEx.StartupInfo, // lpStartupInfo + &piClient // lpProcessInformation + ); + if (!fSuccess) { + throw errorWithCode(info, "Cannot create process"); + } + + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + bool fLoadedDll = hLibrary != nullptr; + if (useConptyDll && fLoadedDll) + { + PFNRELEASEPSEUDOCONSOLE const pfnReleasePseudoConsole = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, "ConptyReleasePseudoConsole"); + if (pfnReleasePseudoConsole) + { + pfnReleasePseudoConsole(handle->hpc); + } + } + + // Update handle + handle->hShell = piClient.hProcess; + + // Close the thread handle to avoid resource leak + CloseHandle(piClient.hThread); + // Close the input read and output write handle of the pseudoconsole + CloseHandle(handle->hIn); + CloseHandle(handle->hOut); + + SetupExitCallback(env, exitCallback, handle); + + // Return + auto marshal = Napi::Object::New(env); + marshal.Set("pid", Napi::Number::New(env, piClient.dwProcessId)); + return marshal; +} + +static Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 4 || + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber() || + !info[3].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.resize(id, cols, rows, useConptyDll)"); + } + + int id = info[0].As<Napi::Number>().Int32Value(); + SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value()); + SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value()); + const bool useConptyDll = info[3].As<Napi::Boolean>().Value(); + + const pty_baton* handle = get_pty_baton(id); + + if (handle != nullptr) { + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + bool fLoadedDll = hLibrary != nullptr; + if (fLoadedDll) + { + PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, + useConptyDll ? "ConptyResizePseudoConsole" : "ResizePseudoConsole"); + if (pfnResizePseudoConsole) + { + COORD size = {cols, rows}; + pfnResizePseudoConsole(handle->hpc, size); + } + } + } + + return env.Undefined(); +} + +static Napi::Value PtyClear(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 2 || + !info[0].IsNumber() || + !info[1].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.clear(id, useConptyDll)"); + } + + int id = info[0].As<Napi::Number>().Int32Value(); + const bool useConptyDll = info[1].As<Napi::Boolean>().Value(); + + // This API is only supported for conpty.dll as it was introduced in a later version of Windows. + // We could hook it up to point at >= a version of Windows only, but the future is conpty.dll + // anyway. + if (!useConptyDll) { + return env.Undefined(); + } + + const pty_baton* handle = get_pty_baton(id); + + if (handle != nullptr) { + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + bool fLoadedDll = hLibrary != nullptr; + if (fLoadedDll) + { + PFNCLEARPSEUDOCONSOLE const pfnClearPseudoConsole = (PFNCLEARPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ConptyClearPseudoConsole"); + if (pfnClearPseudoConsole) + { + pfnClearPseudoConsole(handle->hpc); + } + } + } + + return env.Undefined(); +} + +static Napi::Value PtyKill(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 2 || + !info[0].IsNumber() || + !info[1].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.kill(id, useConptyDll)"); + } + + int id = info[0].As<Napi::Number>().Int32Value(); + const bool useConptyDll = info[1].As<Napi::Boolean>().Value(); + + const pty_baton* handle = get_pty_baton(id); + + if (handle != nullptr) { + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + bool fLoadedDll = hLibrary != nullptr; + if (fLoadedDll) + { + PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, + useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole"); + if (pfnClosePseudoConsole) + { + pfnClosePseudoConsole(handle->hpc); + } + } + if (useConptyDll) { + TerminateProcess(handle->hShell, 1); + } + } + + return env.Undefined(); +} + +/** +* Init +*/ + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess)); + exports.Set("connect", Napi::Function::New(env, PtyConnect)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("clear", Napi::Function::New(env, PtyClear)); + exports.Set("kill", Napi::Function::New(env, PtyKill)); + return exports; +}; + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/web/package/node-pty/src/win/conpty.h b/web/package/node-pty/src/win/conpty.h new file mode 100644 index 00000000..4cef31c4 --- /dev/null +++ b/web/package/node-pty/src/win/conpty.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This header prototypes the Pseudoconsole symbols from conpty.lib with their original names. +// This is required because we cannot import __imp_CreatePseudoConsole from a static library +// as it doesn't produce an import lib. +// We can't use an /ALTERNATENAME trick because it seems that that name is only resolved when the +// linker cannot otherwise find the symbol. + +#pragma once + +#include <consoleapi.h> + +#ifndef CONPTY_IMPEXP +#define CONPTY_IMPEXP __declspec(dllimport) +#endif + +#ifndef CONPTY_EXPORT +#ifdef __cplusplus +#define CONPTY_EXPORT extern "C" CONPTY_IMPEXP +#else +#define CONPTY_EXPORT extern CONPTY_IMPEXP +#endif +#endif + +#define PSEUDOCONSOLE_RESIZE_QUIRK (2u) +#define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u) + +CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); +CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); + +CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size); +CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC); +CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show); +CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent); +CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC); + +CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC); +CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleTimeout(HPCON hPC, DWORD dwMilliseconds); + +CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC); diff --git a/web/package/node-pty/src/win/conpty_console_list.cc b/web/package/node-pty/src/win/conpty_console_list.cc new file mode 100644 index 00000000..4c8ab393 --- /dev/null +++ b/web/package/node-pty/src/win/conpty_console_list.cc @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2019, Microsoft Corporation (MIT License). + */ + +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include <napi.h> +#include <windows.h> + +static Napi::Value ApiConsoleProcessList(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + if (info.Length() != 1 || + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: getConsoleProcessList(shellPid)"); + } + + const DWORD pid = info[0].As<Napi::Number>().Uint32Value(); + + if (!FreeConsole()) { + throw Napi::Error::New(env, "FreeConsole failed"); + } + if (!AttachConsole(pid)) { + throw Napi::Error::New(env, "AttachConsole failed"); + } + auto processList = std::vector<DWORD>(64); + auto processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size())); + if (processList.size() < processCount) { + processList.resize(processCount); + processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size())); + } + FreeConsole(); + + Napi::Array result = Napi::Array::New(env); + for (DWORD i = 0; i < processCount; i++) { + result.Set(i, Napi::Number::New(env, processList[i])); + } + return result; +} + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("getConsoleProcessList", Napi::Function::New(env, ApiConsoleProcessList)); + return exports; +}; + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/web/package/node-pty/src/win/path_util.cc b/web/package/node-pty/src/win/path_util.cc new file mode 100644 index 00000000..726e36b6 --- /dev/null +++ b/web/package/node-pty/src/win/path_util.cc @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +#include <stdexcept> +#include <Shlwapi.h> // PathCombine +#include <Windows.h> +#include "path_util.h" + +namespace path_util { + +std::wstring to_wstring(const Napi::String& str) { + const std::u16string & u16 = str.Utf16Value(); + return std::wstring(u16.begin(), u16.end()); +} + +std::string wstring_to_string(const std::wstring &wide_string) { + if (wide_string.empty()) { + return ""; + } + const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) { + return ""; + } + std::string result(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), &result.at(0), size_needed, nullptr, nullptr); + return result; +} + +std::string from_wstring(const wchar_t* wstr) { + int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); + if (bufferSize <= 0) { + return ""; + } + std::string result(bufferSize - 1, 0); // -1 to exclude null terminator + int status = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &result[0], bufferSize, NULL, NULL); + if (status == 0) { + return ""; + } + return result; +} + +bool file_exists(std::wstring filename) { + DWORD attr = ::GetFileAttributesW(filename.c_str()); + if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) { + return false; + } + return true; +} + +// cmd.exe -> C:\Windows\system32\cmd.exe +std::wstring get_shell_path(std::wstring filename) { + std::wstring shellpath; + + if (file_exists(filename)) { + return shellpath; + } + + wchar_t* buffer_ = new wchar_t[MAX_ENV]; + int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV); + if (read) { + std::wstring delimiter = L";"; + size_t pos = 0; + std::vector<std::wstring> paths; + std::wstring buffer(buffer_); + while ((pos = buffer.find(delimiter)) != std::wstring::npos) { + paths.push_back(buffer.substr(0, pos)); + buffer.erase(0, pos + delimiter.length()); + } + + const wchar_t *filename_ = filename.c_str(); + + for (size_t i = 0; i < paths.size(); ++i) { + std::wstring path = paths[i]; + wchar_t searchPath[MAX_PATH]; + ::PathCombineW(searchPath, const_cast<wchar_t*>(path.c_str()), filename_); + + if (searchPath == NULL) { + continue; + } + + if (file_exists(searchPath)) { + shellpath = searchPath; + break; + } + } + } + + delete[] buffer_; + return shellpath; +} + +} // namespace path_util diff --git a/web/package/node-pty/src/win/path_util.h b/web/package/node-pty/src/win/path_util.h new file mode 100644 index 00000000..85d119f9 --- /dev/null +++ b/web/package/node-pty/src/win/path_util.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +#ifndef NODE_PTY_PATH_UTIL_H_ +#define NODE_PTY_PATH_UTIL_H_ + +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include <napi.h> +#include <string> + +#define MAX_ENV 65536 + +namespace path_util { + +std::wstring to_wstring(const Napi::String& str); +std::string wstring_to_string(const std::wstring &wide_string); +std::string from_wstring(const wchar_t* wstr); +bool file_exists(std::wstring filename); +std::wstring get_shell_path(std::wstring filename); + +} // namespace path_util + +#endif // NODE_PTY_PATH_UTIL_H_ diff --git a/web/package/node-pty/src/win/winpty.cc b/web/package/node-pty/src/win/winpty.cc new file mode 100644 index 00000000..147e2ac0 --- /dev/null +++ b/web/package/node-pty/src/win/winpty.cc @@ -0,0 +1,333 @@ +/** + * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + * + * pty.cc: + * This file is responsible for starting processes + * with pseudo-terminal file descriptors. + */ + +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include <napi.h> +#include <iostream> +#include <assert.h> +#include <map> +#include <Shlwapi.h> // PathCombine, PathIsRelative +#include <sstream> +#include <stdlib.h> +#include <string.h> +#include <string> +#include <vector> +#include <winpty.h> + +#include "path_util.h" + +/** +* Misc +*/ +#define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG") + +/** +* winpty +*/ +static std::vector<winpty_t *> ptyHandles; +static volatile LONG ptyCounter; + +/** +* Helpers +*/ + +/** Keeps track of the handles created by PtyStartProcess */ +static std::map<DWORD, HANDLE> createdHandles; + +static winpty_t *get_pipe_handle(DWORD pid) { + for (size_t i = 0; i < ptyHandles.size(); ++i) { + winpty_t *ptyHandle = ptyHandles[i]; + HANDLE current = winpty_agent_process(ptyHandle); + if (GetProcessId(current) == pid) { + return ptyHandle; + } + } + return nullptr; +} + +static bool remove_pipe_handle(DWORD pid) { + for (size_t i = 0; i < ptyHandles.size(); ++i) { + winpty_t *ptyHandle = ptyHandles[i]; + HANDLE current = winpty_agent_process(ptyHandle); + if (GetProcessId(current) == pid) { + winpty_free(ptyHandle); + ptyHandles.erase(ptyHandles.begin() + i); + ptyHandle = nullptr; + return true; + } + } + return false; +} + +Napi::Error error_with_winpty_msg(const char *generalMsg, winpty_error_ptr_t error_ptr, Napi::Env env) { + std::string why; + why += generalMsg; + why += ": "; + why += path_util::wstring_to_string(winpty_error_msg(error_ptr)); + winpty_error_free(error_ptr); + return Napi::Error::New(env, why); +} + +static Napi::Value PtyGetExitCode(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 1 || + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.getExitCode(pid)"); + } + + DWORD pid = info[0].As<Napi::Number>().Uint32Value(); + HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (handle == NULL) { + return Napi::Number::New(env, -1); + } + + DWORD exitCode = 0; + BOOL success = GetExitCodeProcess(handle, &exitCode); + if (success == FALSE) { + exitCode = -1; + } + + CloseHandle(handle); + return Napi::Number::New(env, exitCode); +} + +static Napi::Value PtyGetProcessList(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 1 || + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.getProcessList(pid)"); + } + + DWORD pid = info[0].As<Napi::Number>().Uint32Value(); + winpty_t *pc = get_pipe_handle(pid); + if (pc == nullptr) { + return Napi::Number::New(env, 0); + } + int processList[64]; + const int processCount = 64; + int actualCount = winpty_get_console_process_list(pc, processList, processCount, nullptr); + if (actualCount <= 0) { + return Napi::Number::New(env, 0); + } + Napi::Array result = Napi::Array::New(env, actualCount); + for (int i = 0; i < actualCount; i++) { + result.Set(i, Napi::Number::New(env, processList[i])); + } + return result; +} + +static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 7 || + !info[0].IsString() || + !info[1].IsString() || + !info[2].IsArray() || + !info[3].IsString() || + !info[4].IsNumber() || + !info[5].IsNumber() || + !info[6].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.startProcess(file, cmdline, env, cwd, cols, rows, debug)"); + } + + std::wstring filename(path_util::to_wstring(info[0].As<Napi::String>())); + std::wstring cmdline(path_util::to_wstring(info[1].As<Napi::String>())); + std::wstring cwd(path_util::to_wstring(info[3].As<Napi::String>())); + + // create environment block + std::wstring envStr; + const Napi::Array envValues = info[2].As<Napi::Array>(); + if (!envValues.IsEmpty()) { + std::wstring envBlock; + for(uint32_t i = 0; i < envValues.Length(); i++) { + envBlock += path_util::to_wstring(envValues.Get(i).As<Napi::String>()); + envBlock += L'\0'; + } + envStr = std::move(envBlock); + } + + // use environment 'Path' variable to determine location of + // the relative path that we have recieved (e.g cmd.exe) + std::wstring shellpath; + if (::PathIsRelativeW(filename.c_str())) { + shellpath = path_util::get_shell_path(filename); + } else { + shellpath = filename; + } + + if (shellpath.empty() || !path_util::file_exists(shellpath)) { + std::string why; + why += "File not found: "; + why += path_util::wstring_to_string(shellpath); + throw Napi::Error::New(env, why); + } + + int cols = info[4].As<Napi::Number>().Int32Value(); + int rows = info[5].As<Napi::Number>().Int32Value(); + bool debug = info[6].As<Napi::Boolean>().Value(); + + // Enable/disable debugging + SetEnvironmentVariable(WINPTY_DBG_VARIABLE, debug ? "1" : NULL); // NULL = deletes variable + + // Create winpty config + winpty_error_ptr_t error_ptr = nullptr; + winpty_config_t* winpty_config = winpty_config_new(0, &error_ptr); + if (winpty_config == nullptr) { + throw error_with_winpty_msg("Error creating WinPTY config", error_ptr, env); + } + winpty_error_free(error_ptr); + + // Set pty size on config + winpty_config_set_initial_size(winpty_config, cols, rows); + + // Start the pty agent + winpty_t *pc = winpty_open(winpty_config, &error_ptr); + winpty_config_free(winpty_config); + if (pc == nullptr) { + throw error_with_winpty_msg("Error launching WinPTY agent", error_ptr, env); + } + winpty_error_free(error_ptr); + + // Create winpty spawn config + winpty_spawn_config_t* config = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, shellpath.c_str(), cmdline.c_str(), cwd.c_str(), envStr.c_str(), &error_ptr); + if (config == nullptr) { + winpty_free(pc); + throw error_with_winpty_msg("Error creating WinPTY spawn config", error_ptr, env); + } + winpty_error_free(error_ptr); + + // Spawn the new process + HANDLE handle = nullptr; + BOOL spawnSuccess = winpty_spawn(pc, config, &handle, nullptr, nullptr, &error_ptr); + winpty_spawn_config_free(config); + if (!spawnSuccess) { + if (handle) { + CloseHandle(handle); + } + winpty_free(pc); + throw error_with_winpty_msg("Unable to start terminal process", error_ptr, env); + } + winpty_error_free(error_ptr); + + LPCWSTR coninPipeName = winpty_conin_name(pc); + std::string coninPipeNameStr = path_util::from_wstring(coninPipeName); + if (coninPipeNameStr.empty()) { + CloseHandle(handle); + winpty_free(pc); + throw Napi::Error::New(env, "Failed to initialize winpty conin"); + } + + LPCWSTR conoutPipeName = winpty_conout_name(pc); + std::string conoutPipeNameStr = path_util::from_wstring(conoutPipeName); + if (conoutPipeNameStr.empty()) { + CloseHandle(handle); + winpty_free(pc); + throw Napi::Error::New(env, "Failed to initialize winpty conout"); + } + + DWORD innerPid = GetProcessId(handle); + if (createdHandles[innerPid]) { + CloseHandle(handle); + winpty_free(pc); + std::stringstream why; + why << "There is already a process with innerPid " << innerPid; + throw Napi::Error::New(env, why.str()); + } + createdHandles[innerPid] = handle; + + // Save pty struct for later use + ptyHandles.push_back(pc); + + DWORD pid = GetProcessId(winpty_agent_process(pc)); + Napi::Object marshal = Napi::Object::New(env); + marshal.Set("innerPid", Napi::Number::New(env, (int)innerPid)); + marshal.Set("pid", Napi::Number::New(env, (int)pid)); + marshal.Set("pty", Napi::Number::New(env, InterlockedIncrement(&ptyCounter))); + marshal.Set("fd", Napi::Number::New(env, -1)); + marshal.Set("conin", Napi::String::New(env, coninPipeNameStr)); + marshal.Set("conout", Napi::String::New(env, conoutPipeNameStr)); + + return marshal; +} + +static Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 3 || + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.resize(pid, cols, rows)"); + } + + DWORD pid = info[0].As<Napi::Number>().Uint32Value(); + int cols = info[1].As<Napi::Number>().Int32Value(); + int rows = info[2].As<Napi::Number>().Int32Value(); + + winpty_t *pc = get_pipe_handle(pid); + + if (pc == nullptr) { + throw Napi::Error::New(env, "The pty doesn't appear to exist"); + } + BOOL success = winpty_set_size(pc, cols, rows, nullptr); + if (!success) { + throw Napi::Error::New(env, "The pty could not be resized"); + } + + return env.Undefined(); +} + +static Napi::Value PtyKill(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 2 || + !info[0].IsNumber() || + !info[1].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.kill(pid, innerPid)"); + } + + DWORD pid = info[0].As<Napi::Number>().Uint32Value(); + DWORD innerPid = info[1].As<Napi::Number>().Uint32Value(); + + winpty_t *pc = get_pipe_handle(pid); + if (pc == nullptr) { + throw Napi::Error::New(env, "Pty seems to have been killed already"); + } + + assert(remove_pipe_handle(pid)); + + HANDLE innerPidHandle = createdHandles[innerPid]; + createdHandles.erase(innerPid); + CloseHandle(innerPidHandle); + + return env.Undefined(); +} + +/** +* Init +*/ + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("kill", Napi::Function::New(env, PtyKill)); + exports.Set("getExitCode", Napi::Function::New(env, PtyGetExitCode)); + exports.Set("getProcessList", Napi::Function::New(env, PtyGetProcessList)); + return exports; +}; + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/web/package/node-pty/src/windowsTerminal.ts b/web/package/node-pty/src/windowsTerminal.ts new file mode 100644 index 00000000..49db483e --- /dev/null +++ b/web/package/node-pty/src/windowsTerminal.ts @@ -0,0 +1,214 @@ +/** + * Simplified Windows terminal implementation without threading + * Removed ConoutSocketWorker and shared pipe architecture + */ + +import * as fs from 'fs'; +import { Socket } from 'net'; +import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal'; +import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces'; +import { ArgvOrCommandLine } from './types'; +import { assign } from './utils'; + +const DEFAULT_FILE = 'cmd.exe'; +const DEFAULT_NAME = 'Windows Shell'; + +// Native module interfaces +interface IConptyProcess { + pty: number; + fd: number; + conin: string; + conout: string; +} + +interface IConptyNative { + startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, inheritCursor: boolean, useConptyDll: boolean): IConptyProcess; + connect(pty: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExit: (exitCode: number) => void): { pid: number }; + resize(pty: number, cols: number, rows: number, useConptyDll: boolean): void; + clear(pty: number, useConptyDll: boolean): void; + kill(pty: number, useConptyDll: boolean): void; +} + +let conptyNative: IConptyNative; + +export class WindowsTerminal extends Terminal { + private _isReady: boolean = false; + protected _pid: number = 0; + private _innerPid: number = 0; + private _ptyNative: IConptyNative; + protected _pty: number; + private _inSocket!: Socket; + private _outSocket!: Socket; + private _exitCode: number | undefined; + private _useConptyDll: boolean = false; + + public get master(): Socket | undefined { return this._outSocket; } + public get slave(): Socket | undefined { return this._inSocket; } + + constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions) { + super(opt); + + // Load native module + if (!conptyNative) { + try { + conptyNative = require('../build/Release/conpty.node'); + } catch (outerError) { + try { + conptyNative = require('../build/Debug/conpty.node'); + } catch (innerError) { + throw outerError; + } + } + } + this._ptyNative = conptyNative; + + // Initialize arguments + args = args || []; + file = file || DEFAULT_FILE; + opt = opt || {}; + opt.env = opt.env || process.env; + + const env = assign({}, opt.env); + this._cols = opt.cols || DEFAULT_COLS; + this._rows = opt.rows || DEFAULT_ROWS; + const cwd = opt.cwd || process.cwd(); + const parsedEnv = this._parseEnv(env); + + // Compose command line + const commandLine = this._argsToCommandLine(file, args); + + // Start ConPTY process + const pipeName = this._generatePipeName(); + const term = this._ptyNative.startProcess(file, this._cols, this._rows, false, pipeName, false, this._useConptyDll); + + this._pty = term.pty; + this._fd = term.fd; + + // Create direct socket connections without worker threads + this._setupDirectSockets(term); + + // Connect the process + const connect = this._ptyNative.connect(this._pty, commandLine, cwd, parsedEnv, this._useConptyDll, (exitCode) => { + this._exitCode = exitCode; + this.emit('exit', exitCode); + this._close(); + }); + this._innerPid = connect.pid; + this._pid = connect.pid; + + this._file = file; + this._name = opt.name || env.TERM || DEFAULT_NAME; + this._readable = true; + this._writable = true; + + this._forwardEvents(); + } + + private _setupDirectSockets(term: IConptyProcess): void { + // Setup output socket - read directly from conout + const outFd = fs.openSync(term.conout, 'r'); + this._outSocket = new Socket({ fd: outFd, readable: true, writable: false }); + this._outSocket.setEncoding('utf8'); + this._socket = this._outSocket; + + // Setup input socket - write directly to conin + const inFd = fs.openSync(term.conin, 'w'); + this._inSocket = new Socket({ fd: inFd, readable: false, writable: true }); + this._inSocket.setEncoding('utf8'); + + // Forward events directly + this._outSocket.on('data', (data) => { + if (!this._isReady) { + this._isReady = true; + } + this.emit('data', data); + }); + + this._outSocket.on('error', (err) => { + if ((err as any).code && ((err as any).code.includes('EPIPE') || (err as any).code.includes('EIO'))) { + // Expected errors when process exits + return; + } + this.emit('error', err); + }); + + this._outSocket.on('close', () => { + if (this._exitCode === undefined) { + this.emit('exit', 0); + } + this._close(); + }); + } + + protected _write(data: string): void { + if (this._inSocket && this._inSocket.writable) { + this._inSocket.write(data); + } + } + + public resize(cols: number, rows: number): void { + if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) { + throw new Error('resizing must be done using positive cols and rows'); + } + if (this._exitCode !== undefined) { + throw new Error('Cannot resize a pty that has already exited'); + } + this._cols = cols; + this._rows = rows; + this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll); + } + + public clear(): void { + this._ptyNative.clear(this._pty, this._useConptyDll); + } + + public kill(signal?: string): void { + this._close(); + try { + process.kill(this._pid); + } catch (e) { + // Ignore if process cannot be found + } + this._ptyNative.kill(this._pty, this._useConptyDll); + } + + protected _close(): void { + if (this._inSocket) { + this._inSocket.destroy(); + } + if (this._outSocket) { + this._outSocket.destroy(); + } + } + + private _generatePipeName(): string { + return `\\\\.\\pipe\\conpty-${Date.now()}-${Math.random()}`; + } + + private _argsToCommandLine(file: string, args: ArgvOrCommandLine): string { + if (typeof args === 'string') { + return `${file} ${args}`; + } + const argv = [file]; + if (args) { + argv.push(...args); + } + return argv.map(arg => { + if (arg.includes(' ') || arg.includes('\t')) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }).join(' '); + } + + public static open(options?: IPtyOpenOptions): void { + throw new Error('open() not supported on windows, use spawn() instead.'); + } + + public get process(): string { return this._name; } + public get pid(): number { return this._pid; } + + public destroy(): void { + this.kill(); + } +} \ No newline at end of file diff --git a/web/package/package.json b/web/package/package.json new file mode 100644 index 00000000..9eb07d88 --- /dev/null +++ b/web/package/package.json @@ -0,0 +1,78 @@ +{ + "name": "vibetunnel", + "version": "1.0.0-beta.12", + "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", + "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", + "postject": "1.0.0-alpha.6", + "signal-exit": "^4.1.0", + "web-push": "^3.6.7", + "ws": "^8.18.3" + }, + "optionalDependencies": { + "authenticate-pam": "^1.0.5" + }, + "keywords": [ + "terminal", + "multiplexer", + "websocket", + "asciinema" + ], + "author": "", + "license": "MIT" +} diff --git a/web/package/scripts/node-pty-plugin.js b/web/package/scripts/node-pty-plugin.js new file mode 100644 index 00000000..2284a43b --- /dev/null +++ b/web/package/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/package/scripts/postinstall.js b/web/package/scripts/postinstall.js new file mode 100755 index 00000000..ddde226c --- /dev/null +++ b/web/package/scripts/postinstall.js @@ -0,0 +1,257 @@ +#!/usr/bin/env node + +/** + * Postinstall script for npm package + * 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...'); + +// Check for npm_config_prefix conflict with NVM +if (process.env.npm_config_prefix && process.env.NVM_DIR) { + const nvmNodeVersion = process.execPath; + const npmPrefix = process.env.npm_config_prefix; + + // Check if npm_config_prefix conflicts with NVM path + if (!nvmNodeVersion.includes(npmPrefix) && nvmNodeVersion.includes('.nvm')) { + console.warn('⚠️ Detected npm_config_prefix conflict with NVM'); + console.warn(` npm_config_prefix: ${npmPrefix}`); + console.warn(` NVM Node path: ${nvmNodeVersion}`); + console.warn(' This may cause npm global installs to fail or install in wrong location.'); + console.warn(' Run: unset npm_config_prefix'); + console.warn(' Then reinstall VibeTunnel for proper NVM compatibility.'); + } +} + +// Check if we're in development (has src directory) or npm install +const isDevelopment = fs.existsSync(path.join(__dirname, '..', 'src')); + +if (isDevelopment) { + // In development, run the existing ensure-native-modules script + require('./ensure-native-modules.js'); + return; +} + +// For npm package, node-pty is bundled in the package root +// No need to create symlinks as it's accessed directly + +// 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_build_from_source: 'false' } + }); + + return true; + } catch (error) { + console.log(` prebuild-install failed for ${moduleName}, will try manual extraction`); + return false; + } +}; + +// Function to manually extract prebuild +const extractPrebuild = (name, version, targetDir, skipDirCheck = false) => { + 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; + } + + // For optional dependencies like authenticate-pam, check if the module exists + // If not, extract to a different location + let extractDir = targetDir; + if (skipDirCheck && name === 'authenticate-pam' && !fs.existsSync(targetDir)) { + // Extract to a controlled location since node_modules/authenticate-pam doesn't exist + extractDir = path.join(__dirname, '..', 'optional-modules', name); + } + + // Create the parent directory + fs.mkdirSync(extractDir, { recursive: true }); + + try { + // Extract directly into the module directory - the tar already contains build/Release structure + execSync(`tar -xzf "${prebuildFile}" -C "${extractDir}"`, { 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, node-addon-api is included as a dependency in its package.json + // npm should handle it automatically during source compilation + + 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, // Optional - falls back to other auth methods + platforms: ['linux', 'darwin'], // Needed on Linux and macOS + skipDirCheck: true // Don't check if dir exists since it's optional + } +]; + +let hasErrors = false; + +for (const module of modules) { + 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)) { + if (module.skipDirCheck) { + // For optional modules, we'll try to extract the prebuild anyway + console.log(` ${module.name} not installed via npm (optional dependency), will extract prebuild`); + } else { + console.warn(` Warning: ${module.name} directory not found at ${module.dir}`); + if (module.essential) { + hasErrors = true; + } + 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) - skip if directory doesn't exist + if (fs.existsSync(module.dir)) { + success = tryPrebuildInstall(module.name, module.dir); + } + + // Method 2: Manual prebuild extraction + if (!success) { + success = extractPrebuild(module.name, module.version, module.dir, module.skipDirCheck); + } + + // Method 3: Compile from source (skip if directory doesn't exist) + if (!success && fs.existsSync(module.dir) && 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); +} 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-npm-docker-verbose.sh b/web/scripts/test-npm-docker-verbose.sh new file mode 100755 index 00000000..a501c4bb --- /dev/null +++ b/web/scripts/test-npm-docker-verbose.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Test VibeTunnel npm package installation with verbose output + +VERSION=${1:-latest} + +echo "Testing VibeTunnel npm package version: $VERSION" +echo "================================================" + +# Create test directory +TMP_DIR=$(mktemp -d) +echo "Test directory: $TMP_DIR" + +# Create Dockerfile +cat > "$TMP_DIR/Dockerfile" << 'EOF' +FROM node:20-slim + +WORKDIR /app + +# Test installation and PAM extraction +RUN echo "=== Installing VibeTunnel ===" && \ + npm install -g vibetunnel@VERSION && \ + echo "=== Installation complete ===" && \ + echo "=== Checking for node_modules/authenticate-pam ===" && \ + ls -la /usr/local/lib/node_modules/vibetunnel/node_modules/ | grep -E "(authenticate|optional)" || echo "No authenticate-pam in node_modules" && \ + echo "=== Checking for optional-modules ===" && \ + ls -la /usr/local/lib/node_modules/vibetunnel/optional-modules/ 2>/dev/null || echo "No optional-modules directory" && \ + echo "=== Checking postinstall output ===" && \ + cd /usr/local/lib/node_modules/vibetunnel && \ + npm run postinstall || echo "Postinstall failed" && \ + echo "=== Final check ===" && \ + find /usr/local/lib/node_modules/vibetunnel -name "authenticate_pam.node" -type f 2>/dev/null || echo "No authenticate_pam.node found" +EOF + +# Replace VERSION placeholder +sed -i.bak "s/VERSION/$VERSION/g" "$TMP_DIR/Dockerfile" + +# Build and run +echo "Building Docker image..." +docker build -t vibetunnel-npm-test-verbose "$TMP_DIR" + +# Cleanup +rm -rf "$TMP_DIR" + +echo "✅ Test complete!" \ No newline at end of file diff --git a/web/scripts/test-npm-docker.sh b/web/scripts/test-npm-docker.sh new file mode 100755 index 00000000..d8434ff1 --- /dev/null +++ b/web/scripts/test-npm-docker.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Test VibeTunnel npm package installation in Docker +# Usage: ./scripts/test-npm-docker.sh [version] +# Example: ./scripts/test-npm-docker.sh 1.0.0-beta.11.4 + +VERSION=${1:-latest} + +echo "Testing VibeTunnel npm package version: $VERSION" +echo "================================================" + +# Create temporary dockerfile +TEMP_DIR=$(mktemp -d) +DOCKERFILE="$TEMP_DIR/Dockerfile" + +cat > "$DOCKERFILE" << EOF +FROM node:20-slim + +# Test 1: Install without PAM headers (should succeed) +RUN echo "=== Test 1: Installing without PAM headers ===" && \ + npm install -g vibetunnel@$VERSION && \ + vibetunnel --version && \ + node -e "try { require('authenticate-pam'); console.log('PAM available'); } catch { console.log('PAM not available - this is expected'); }" + +# Test 2: Install PAM headers and check if module compiles +RUN echo "=== Test 2: Installing PAM headers ===" && \ + apt-get update && apt-get install -y libpam0g-dev && \ + echo "PAM headers installed" + +# Test 3: Verify VibeTunnel still works +RUN echo "=== Test 3: Verifying VibeTunnel functionality ===" && \ + vibetunnel --version && \ + vibetunnel --help > /dev/null && \ + echo "VibeTunnel is working correctly!" + +CMD ["echo", "All tests passed successfully!"] +EOF + +# Build and run the test +echo "Building Docker image..." +docker build -f "$DOCKERFILE" -t vibetunnel-npm-test . || { + echo "❌ Docker build failed!" + rm -rf "$TEMP_DIR" + exit 1 +} + +echo "" +echo "Running tests..." +docker run --rm vibetunnel-npm-test || { + echo "❌ Tests failed!" + rm -rf "$TEMP_DIR" + exit 1 +} + +# Cleanup +rm -rf "$TEMP_DIR" +docker rmi vibetunnel-npm-test > /dev/null 2>&1 + +echo "" +echo "✅ All tests passed! VibeTunnel $VERSION installs correctly on Linux." \ No newline at end of file diff --git a/web/src/server/services/authenticate-pam-loader.ts b/web/src/server/services/authenticate-pam-loader.ts index d1b01e29..908396b5 100644 --- a/web/src/server/services/authenticate-pam-loader.ts +++ b/web/src/server/services/authenticate-pam-loader.ts @@ -64,7 +64,7 @@ if (fs.existsSync(seaPamPath) || fs.existsSync(seaNativePamPath)) { } else { // Development mode - use regular require let loaded = false; - + // First, try the normal require path try { const pamModule = require('authenticate-pam'); @@ -74,20 +74,10 @@ if (fs.existsSync(seaPamPath) || fs.existsSync(seaNativePamPath)) { } catch (_error) { // Module not found via normal require } - + // If normal require failed, try the optional-modules location if (!loaded) { - const optionalModulePath = path.join( - __dirname, - '..', - '..', - '..', - 'optional-modules', - 'authenticate-pam', - 'build', - 'Release', - 'authenticate_pam.node' - ); + const optionalModulePath = path.join(__dirname, '..', '..', '..', 'optional-modules', 'authenticate-pam', 'build', 'Release', 'authenticate_pam.node'); if (fs.existsSync(optionalModulePath)) { try { const nativeModule = loadNativeModule(optionalModulePath); @@ -101,7 +91,7 @@ if (fs.existsSync(seaPamPath) || fs.existsSync(seaNativePamPath)) { } } } - + if (!loaded) { console.warn( 'Warning: authenticate-pam native module not found. PAM authentication will not work.' diff --git a/web/test-vibetunnel-simple.dockerfile b/web/test-vibetunnel-simple.dockerfile new file mode 100644 index 00000000..fca383f8 --- /dev/null +++ b/web/test-vibetunnel-simple.dockerfile @@ -0,0 +1,29 @@ +# Test VibeTunnel npm package with prebuilds +FROM node:22 + +# Create a test user +RUN useradd -m -s /bin/bash testuser + +# Install dependencies +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Install vibetunnel globally +RUN npm install -g --ignore-scripts vibetunnel@latest + +# Test script +RUN echo '#!/bin/bash\n\ +echo "Testing VibeTunnel npm package..."\n\ +echo "Node version: $(node --version)"\n\ +echo "npm version: $(npm --version)"\n\ +echo ""\n\ +echo "Testing vibetunnel command..."\n\ +which vibetunnel && echo "✅ vibetunnel command found" || echo "❌ vibetunnel command not found"\n\ +echo ""\n\ +echo "Checking version..."\n\ +vibetunnel --version 2>&1 || echo "Note: Version check may fail if native modules are missing"\n\ +echo ""\n\ +echo "Checking help..."\n\ +vibetunnel --help 2>&1 | head -20\n\ +' > /test.sh && chmod +x /test.sh + +CMD ["/test.sh"] \ No newline at end of file diff --git a/web/test-vibetunnel-with-postinstall.dockerfile b/web/test-vibetunnel-with-postinstall.dockerfile new file mode 100644 index 00000000..cd7b3493 --- /dev/null +++ b/web/test-vibetunnel-with-postinstall.dockerfile @@ -0,0 +1,26 @@ +# Test VibeTunnel npm package with postinstall +FROM node:22 + +# Install dependencies +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Install vibetunnel globally (with postinstall) +RUN npm install -g vibetunnel@latest + +# Test script +RUN echo '#!/bin/bash\n\ +echo "Testing VibeTunnel npm package..."\n\ +echo "Node version: $(node --version)"\n\ +echo ""\n\ +echo "Checking installed files..."\n\ +ls -la /usr/local/lib/node_modules/vibetunnel/node-pty/build/ 2>/dev/null || echo "No build directory"\n\ +ls -la /usr/local/lib/node_modules/vibetunnel/prebuilds/ 2>/dev/null || echo "No prebuilds directory"\n\ +echo ""\n\ +echo "Testing vibetunnel command..."\n\ +vibetunnel --version\n\ +echo ""\n\ +echo "Starting server test..."\n\ +timeout 5 vibetunnel --port 4021 --no-auth || echo "Server test completed"\n\ +' > /test.sh && chmod +x /test.sh + +CMD ["/test.sh"] \ No newline at end of file diff --git a/web/test-vibetunnel.dockerfile b/web/test-vibetunnel.dockerfile new file mode 100644 index 00000000..b3492e9e --- /dev/null +++ b/web/test-vibetunnel.dockerfile @@ -0,0 +1,43 @@ +# Test VibeTunnel npm package on different Node.js versions +FROM node:22-slim + +# Install dependencies for terminal functionality and building native modules +RUN apt-get update && apt-get install -y \ + curl \ + procps \ + python3 \ + build-essential \ + libpam0g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install vibetunnel globally as root +RUN npm install -g vibetunnel@latest + +# Create a test user +RUN useradd -m -s /bin/bash testuser + +# Switch to test user +USER testuser +WORKDIR /home/testuser + +# Create a test script +RUN echo '#!/bin/bash\n\ +echo "Testing VibeTunnel npm package..."\n\ +echo "Node version: $(node --version)"\n\ +echo "npm version: $(npm --version)"\n\ +echo "VibeTunnel version:"\n\ +vibetunnel --version\n\ +echo ""\n\ +echo "Starting VibeTunnel server..."\n\ +vibetunnel --port 4021 --no-auth &\n\ +SERVER_PID=$!\n\ +sleep 3\n\ +echo ""\n\ +echo "Testing if server is running..."\n\ +curl -s http://localhost:4021 > /dev/null && echo "✅ Server is responding" || echo "❌ Server not responding"\n\ +echo ""\n\ +echo "Stopping server..."\n\ +kill $SERVER_PID\n\ +' > test.sh && chmod +x test.sh + +CMD ["./test.sh"] \ No newline at end of file