vibetunnel/web/bin/vt
Peter Steinberger 2b20fa9555 Refactor socket client API: Remove HTTP fallback and add type-safe message handling
- Remove HTTP fallback and PID file management from socket-api-client
- Delete ServerPidManager utility class
- Add type-safe sendMessage and sendMessageWithResponse methods to VibeTunnelSocketClient
- Add MessagePayloadMap for compile-time type safety of message payloads
- Refactor SocketApiClient to use clean API instead of brittle type casting
- Remove backwards compatibility code - only emit events with MessageType enum names
- Simplify message handling and response listeners
- Update buildMessage to properly handle CONTROL_CMD messages
2025-07-28 11:40:56 +02:00

560 lines
No EOL
22 KiB
Bash
Executable file

#!/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 - order depends on VIBETUNNEL_PREFER_DERIVED_DATA
APP_PATH=""
if [ -n "$VIBETUNNEL_PREFER_DERIVED_DATA" ]; then
# When preference is set, try DerivedData first
for CANDIDATE in $(find ~/Library/Developer/Xcode/DerivedData -name "VibeTunnel.app" -type d 2>/dev/null | grep -v "\.dSYM" | grep -v "Index\.noindex" | sort -r); 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
# If not found yet, try standard locations
if [ -z "$APP_PATH" ]; then
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
fi
# If not found in standard locations and VIBETUNNEL_PREFER_DERIVED_DATA wasn't set, search development builds
if [ -z "$APP_PATH" ] && [ -z "$VIBETUNNEL_PREFER_DERIVED_DATA" ]; then
# 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
true # No-op command to fix syntax error
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
# Log VibeTunnel binary info when VIBETUNNEL_PREFER_DERIVED_DATA is set
if [ -n "$VIBETUNNEL_PREFER_DERIVED_DATA" ] && [ -n "$VIBETUNNEL_BIN" ]; then
# Get version and build info
VERSION_OUTPUT=$("$VIBETUNNEL_BIN" --version 2>&1)
VERSION_LINE=$(echo "$VERSION_OUTPUT" | grep "^VibeTunnel Server" | head -n 1)
BUILD_LINE=$(echo "$VERSION_OUTPUT" | grep "^Built:" | head -n 1)
# Always log this info regardless of verbosity level
echo "[VibeTunnel] Using binary: $VIBETUNNEL_BIN"
if [ -n "$VERSION_LINE" ]; then
echo "[VibeTunnel] Version: ${VERSION_LINE#VibeTunnel Server }"
fi
if [ -n "$BUILD_LINE" ]; then
echo "[VibeTunnel] ${BUILD_LINE}"
fi
fi
# Handle safe commands first that work both inside and outside sessions
# This must come BEFORE the session check to avoid the recursive session error
if [[ "$1" == "status" || "$1" == "version" || "$1" == "--version" ]]; then
# These commands can run safely inside or outside a session
exec "$VIBETUNNEL_BIN" "$@"
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 <new 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
# For all other commands, block recursive sessions
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 get git repository root
get_git_root() {
git rev-parse --show-toplevel 2>/dev/null
}
# Function to escape strings for JSON
json_escape() {
local str="$1"
# Escape backslashes first, then quotes, then other special characters
str="${str//\\/\\\\}"
str="${str//\"/\\\"}"
str="${str//$'\n'/\\n}"
str="${str//$'\r'/\\r}"
str="${str//$'\t'/\\t}"
printf '%s' "$str"
}
# Function to convert absolute paths to use ~
prettify_path() {
local path="$1"
local home="$HOME"
if [[ "$path" == "$home"* ]]; then
echo "~${path#$home}"
else
echo "$path"
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 <new title> # Inside a VibeTunnel session only
vt status # Show server status and follow mode
vt follow [branch] # Enable follow mode for current or specified branch
vt unfollow # Disable follow mode
vt git event # Git hook notifications
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.
Follow mode automatically switches your VibeTunnel terminal to the Git
worktree that matches the branch you're working on in your editor/IDE.
When you switch branches in your editor, VibeTunnel follows along.
The 'vt git event' command is used by Git hooks to notify VibeTunnel
of repository changes for automatic worktree switching.
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
# Server status:
vt status # Check if server is running and follow mode status
# Git follow mode:
vt follow # Enable follow mode for current branch
vt follow main # Switch to main branch and enable follow mode
vt unfollow # Disable follow mode
# Git event command (typically called by Git hooks):
vt git event # Notify VibeTunnel of Git changes
OPTIONS:
--shell, -i Launch current shell (equivalent to vt $SHELL)
--no-shell-wrap, -S Execute command directly without shell wrapper
--title-mode <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 <command>'" >&2
exit 1
fi
# Handle 'vt follow' command
if [[ "$1" == "follow" ]]; then
# Detect if we're in a worktree
IS_WORKTREE=$(git rev-parse --is-inside-work-tree 2>/dev/null)
COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null)
if [[ "$IS_WORKTREE" == "true" ]] && [[ "$COMMON_DIR" != ".git" ]]; then
# We're in a worktree
if [[ -n "$2" ]]; then
# Error if trying to specify path/branch from worktree
WORKTREE_PATH=$(git rev-parse --show-toplevel)
echo "Error: Cannot specify arguments when running from a worktree."
echo "To enable follow mode for this worktree ($(prettify_path "$WORKTREE_PATH")):"
echo " vt follow"
exit 1
fi
WORKTREE_PATH=$(git rev-parse --show-toplevel)
# Extract main repo path from git common dir
MAIN_REPO=$(dirname "$COMMON_DIR")
echo "Enabling follow mode for worktree: $(prettify_path "$WORKTREE_PATH")"
echo "Main repository ($(prettify_path "$MAIN_REPO")) will track this worktree"
# Use vibetunnel CLI with worktree context
exec "$VIBETUNNEL_BIN" follow --from-worktree --worktree-path "$WORKTREE_PATH" --main-repo "$MAIN_REPO"
else
# We're in main repo
MAIN_REPO=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ -z "$MAIN_REPO" ]]; then
echo "Error: Not in a git repository" >&2
exit 1
fi
ARG="$2"
if [[ -z "$ARG" ]]; then
# No argument - try to be smart
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
if [[ -z "$CURRENT_BRANCH" ]]; then
# Detached HEAD
echo "Error: Not on a branch (detached HEAD state)."
echo "Available worktrees:"
git worktree list | tail -n +2 | while read -r line; do
WPATH=$(echo "$line" | awk '{print $1}')
WBRANCH=$(echo "$line" | grep -oE '\[[^]]+\]' | tr -d '[]')
echo " $WBRANCH -> $(prettify_path "$WPATH")"
done
echo ""
echo "To follow a worktree, use one of:"
echo " vt follow <branch-name>"
echo " vt follow <worktree-path>"
exit 1
fi
# Check if current branch has a worktree
WORKTREE_PATH=$(git worktree list --porcelain | grep -B2 "branch refs/heads/$CURRENT_BRANCH" | grep "worktree" | cut -d' ' -f2 | grep -v "^$MAIN_REPO$" | head -n1)
if [[ -z "$WORKTREE_PATH" ]]; then
# No worktree for current branch
echo "Error: Current branch '$CURRENT_BRANCH' has no associated worktree."
echo "Available worktrees:"
git worktree list | tail -n +2 | while read -r line; do
WPATH=$(echo "$line" | awk '{print $1}')
WBRANCH=$(echo "$line" | grep -oE '\[[^]]+\]' | tr -d '[]')
echo " $WBRANCH -> $(prettify_path "$WPATH")"
done
echo ""
echo "To follow a worktree, use one of:"
echo " vt follow <branch-name>"
echo " vt follow <worktree-path>"
exit 1
fi
# Success - current branch has a worktree
echo "Enabling follow mode for branch: $CURRENT_BRANCH"
echo "Following worktree: $(prettify_path "$WORKTREE_PATH")"
echo "Main repository: $(prettify_path "$MAIN_REPO")"
exec "$VIBETUNNEL_BIN" follow --worktree-path "$WORKTREE_PATH" --main-repo "$MAIN_REPO"
elif [[ -d "$ARG" ]] || [[ "$ARG" == /* ]] || [[ "$ARG" == ../* ]]; then
# Path argument
WORKTREE_PATH=$(realpath "$ARG" 2>/dev/null)
if [[ -z "$WORKTREE_PATH" ]] || [[ ! -d "$WORKTREE_PATH" ]]; then
echo "Error: Invalid path: $ARG" >&2
exit 1
fi
echo "Enabling follow mode for worktree: $(prettify_path "$WORKTREE_PATH")"
echo "Main repository: $(prettify_path "$MAIN_REPO")"
exec "$VIBETUNNEL_BIN" follow --worktree-path "$WORKTREE_PATH" --main-repo "$MAIN_REPO"
else
# Branch argument
WORKTREE_PATH=$(git worktree list --porcelain | grep -B2 "branch refs/heads/$ARG" | grep "worktree" | cut -d' ' -f2 | grep -v "^$MAIN_REPO$" | head -n1)
if [[ -z "$WORKTREE_PATH" ]]; then
echo "Error: No worktree found for branch '$ARG'"
echo "Create a worktree first: git worktree add ../${ARG//\//-} $ARG"
exit 1
fi
echo "Enabling follow mode for branch: $ARG"
echo "Following worktree: $(prettify_path "$WORKTREE_PATH")"
echo "Main repository: $(prettify_path "$MAIN_REPO")"
exec "$VIBETUNNEL_BIN" follow --worktree-path "$WORKTREE_PATH" --main-repo "$MAIN_REPO"
fi
fi
fi
# Handle 'vt unfollow' command
if [[ "$1" == "unfollow" ]]; then
# Use vibetunnel CLI to disable follow mode via socket
exec "$VIBETUNNEL_BIN" unfollow
fi
# Handle 'vt git event' command
if [[ "$1" == "git" && "$2" == "event" ]]; then
# Use vibetunnel CLI to send git event via socket
exec "$VIBETUNNEL_BIN" git-event
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