diff --git a/.gitignore b/.gitignore index b7825993..94258fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,9 @@ Workspace.xcworkspace/ # Sparkle private keys - NEVER commit these private/ + +# Built binaries (should be built during build process) +linux/vibetunnel +linux/vt +VibeTunnel/Resources/vt +VibeTunnel/Resources/vibetunnel diff --git a/VibeTunnel.xcodeproj/project.pbxproj b/VibeTunnel.xcodeproj/project.pbxproj index 675c7b80..73091ae7 100644 --- a/VibeTunnel.xcodeproj/project.pbxproj +++ b/VibeTunnel.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ B2C3D4E5F6A7B8C9D0E1F234 /* Build Web Frontend */, A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */, C2D3E4F5A6B7C8D9E0F1A234 /* Build Go vibetunnel Universal Binary */, + D3E4F5A6B7C8D9E0F1A2B345 /* Build VT Universal Binary */, ); buildRules = ( ); @@ -283,6 +284,29 @@ shellPath = /bin/sh; shellScript = "# Build Go vibetunnel universal binary\necho \"Building Go vibetunnel universal binary...\"\n\n# Get the project directory\nPROJECT_DIR=\"${SRCROOT}\"\nLINUX_DIR=\"${PROJECT_DIR}/linux\"\nBUILD_SCRIPT=\"${LINUX_DIR}/build-universal.sh\"\n\n# Source Go environment\n[ -f \"$HOME/.profile\" ] && . \"$HOME/.profile\"\n[ -f \"$HOME/.zprofile\" ] && . \"$HOME/.zprofile\"\n\n# Check if go is available\nif ! command -v go &> /dev/null; then\n echo \"warning: go could not be found in PATH. Skipping Go binary build.\"\n echo \"PATH is: $PATH\"\n echo \"To enable Go server support, please install Go and ensure it's in your PATH\"\n # Create a dummy file so the build doesn't fail\n mkdir -p \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources\"\n touch \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/vibetunnel.disabled\"\n exit 0\nfi\n\nSOURCE_BINARY=\"${LINUX_DIR}/build/vibetunnel-universal\"\nDEST_BINARY=\"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/vibetunnel\"\n\n# Check if build script exists\nif [ ! -f \"${BUILD_SCRIPT}\" ]; then\n echo \"error: Build script not found at ${BUILD_SCRIPT}\"\n exit 1\nfi\n\n# Make build script executable\nchmod +x \"${BUILD_SCRIPT}\"\n\n# Change to linux directory and run build\ncd \"${LINUX_DIR}\"\n./build-universal.sh\n\n# Check if build succeeded\nif [ ! -f \"${SOURCE_BINARY}\" ]; then\n echo \"error: Universal binary not found at ${SOURCE_BINARY}\"\n exit 1\nfi\n\n# Create Resources directory if it doesn't exist\nmkdir -p \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources\"\n\n# Copy the binary\ncp \"${SOURCE_BINARY}\" \"${DEST_BINARY}\"\nchmod +x \"${DEST_BINARY}\"\n\n# Sign the binary\necho \"Signing Go vibetunnel binary...\"\ncodesign --force --sign - \"${DEST_BINARY}\"\n\necho \"Go vibetunnel universal binary copied and signed to ${DEST_BINARY}\"\n"; }; + D3E4F5A6B7C8D9E0F1A2B345 /* Build VT Universal Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/linux/cmd/vt/main.go", + "$(SRCROOT)/linux/go.mod", + "$(SRCROOT)/linux/build-vt-universal.sh", + ); + name = "Build VT Universal Binary"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Resources/vt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/bash\nset -e\n\necho \"Building VT universal binary...\"\n\n# Get the project directory\nPROJECT_DIR=\"${SRCROOT}\"\nLINUX_DIR=\"${PROJECT_DIR}/linux\"\n\n# Source Go environment\n[ -f \"$HOME/.profile\" ] && . \"$HOME/.profile\"\n[ -f \"$HOME/.zprofile\" ] && . \"$HOME/.zprofile\"\n\n# Check if go is available\nif ! command -v go &> /dev/null; then\n echo \"warning: go could not be found in PATH. Skipping VT binary build.\"\n echo \"PATH is: $PATH\"\n echo \"To enable VT CLI support, please install Go and ensure it's in your PATH\"\n # Don't fail the build, just skip VT\n exit 0\nfi\n\n# Build the VT binary\ncd \"$LINUX_DIR\"\n./build-vt-universal.sh \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Resources/vt\"\n\n# Sign the binary\nif [ -f \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Resources/vt\" ]; then\n echo \"Signing VT binary...\"\n codesign --force --sign - \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Resources/vt\"\n echo \"VT binary built and signed successfully\"\nelse\n echo \"error: VT binary was not created\"\n exit 1\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/VibeTunnel/Resources/vt b/VibeTunnel/Resources/vt deleted file mode 100755 index 57068fc8..00000000 --- a/VibeTunnel/Resources/vt +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash - -# vt - VibeTunnel TTY Forward Wrapper -# This script wraps tty-fwd to enable VibeTunnel to see command output - -# Function to find Claude executable in common locations -find_claude() { - local claude_paths=( - "$HOME/.claude/local/claude" - "$HOME/.claude/local/node_modules/.bin/claude" - "/opt/homebrew/bin/claude" - "/usr/local/bin/claude" - "/usr/bin/claude" - "$(which claude 2>/dev/null)" - ) - - for path in "${claude_paths[@]}"; do - if [[ -n "$path" && -x "$path" ]]; then - echo "$path" - return 0 - fi - done - - echo "Error: Claude executable not found in any of the following locations:" >&2 - printf " %s\n" "${claude_paths[@]}" >&2 - echo "Please ensure Claude is installed or specify the full path manually." >&2 - return 1 -} - -# Get the real path of this script, resolving any symlinks -SCRIPT_REAL_PATH="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || realpath "${BASH_SOURCE[0]}" 2>/dev/null)" -if [[ -z "$SCRIPT_REAL_PATH" ]]; then - # Fallback for systems without readlink -f, greadlink, or realpath - SCRIPT_REAL_PATH="${BASH_SOURCE[0]}" - while [[ -L "$SCRIPT_REAL_PATH" ]]; do - SCRIPT_REAL_PATH="$(readlink "$SCRIPT_REAL_PATH")" - done -fi - -# Get the directory where this script is actually located (Resources folder) -SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_REAL_PATH")" && pwd)" - -# Path to tty-fwd executable in the same Resources directory -TTY_FWD="$SCRIPT_DIR/tty-fwd" - -# Check if tty-fwd exists there, otherwise use the one from ../tty-fwd/target/debug/tty-fwd -if [[ ! -x "$TTY_FWD" ]]; then - TTY_FWD="$SCRIPT_DIR/../tty-fwd/target/debug/tty-fwd" - if [[ ! -x "$TTY_FWD" ]]; then - echo "Error: tty-fwd executable not found at $TTY_FWD" >&2 - exit 1 - fi -fi - -# Handle --claude option -if [[ "$1" == "--claude" ]]; then - shift - claude_path="$(find_claude)" - if [[ $? -ne 0 ]]; then - exit 1 - fi - - # Re-execute vt with claude and remaining arguments - exec "$0" "$claude_path" "$@" -fi - -# Handle --claude-yolo option -if [[ "$1" == "--claude-yolo" ]]; then - shift - claude_path="$(find_claude)" - if [[ $? -ne 0 ]]; then - exit 1 - fi - - # Re-execute vt with claude --dangerously-skip-permissions and remaining arguments - exec "$0" "$claude_path" --dangerously-skip-permissions "$@" -fi - -# Handle --show-session-info option -if [[ "$1" == "--show-session-info" ]]; then - shift - exec "$TTY_FWD" --show-session-info "$@" -fi - -# Handle --show-session-id option -if [[ "$1" == "--show-session-id" ]]; then - shift - exec "$TTY_FWD" --show-session-id "$@" -fi - -# Handle --shell or -i option (launch current shell) -if [[ "$1" == "--shell" || "$1" == "-i" ]]; then - shift - # Execute current shell through tty-fwd - exec "$0" "${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 - -if [[ $# -eq 0 || "$1" == "--help" || "$1" == "-h" ]]; then - cat << 'EOF' -vt - VibeTunnel TTY Forward Wrapper - -USAGE: - vt [command] [args...] - vt --claude [args...] - vt --claude-yolo [args...] - vt --shell [args...] - vt -i [args...] - vt --no-shell-wrap [command] [args...] - vt --show-session-info - vt --show-session-id - vt -S [command] [args...] - vt --help - -DESCRIPTION: - This wrapper script allows VibeTunnel to see the output of commands by - forwarding TTY data through the tty-fwd 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. - -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 --claude # Auto-locate and run Claude - vt --claude --help # Run Claude with --help option - vt --claude-yolo # Run Claude with --dangerously-skip-permissions - 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 - -OPTIONS: - --claude Auto-locate Claude executable and run it - --claude-yolo Auto-locate Claude and run with --dangerously-skip-permissions - --shell, -i Launch current shell (equivalent to vt $SHELL) - --no-shell-wrap, -S Execute command directly without shell wrapper - --help, -h Show this help message and exit - --show-session-info Show current session info - --show-session-id Show current session ID only - -NOTE: - This script automatically uses the tty-fwd executable bundled with - VibeTunnel from the Resources folder. -EOF - exit 0 -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 "$TTY_FWD" -- "$user_shell" -i -c "$(printf '%q ' "$cmd" "$@")" - ;; - bash) - # For bash, expand aliases in non-interactive mode - exec "$TTY_FWD" -- "$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 "$TTY_FWD" -- "$user_shell" -c "$(printf '%q ' "$cmd" "$@")" - ;; - esac -} - -# Resolve and execute the command -if [[ "$NO_SHELL_WRAP" == "true" ]]; then - # Execute directly without shell wrapper - exec "$TTY_FWD" -- "$@" -else - # Use shell wrapper to resolve aliases/functions/builtins - resolve_command "$@" -fi diff --git a/linux/build-vt-universal.sh b/linux/build-vt-universal.sh new file mode 100755 index 00000000..d16e4080 --- /dev/null +++ b/linux/build-vt-universal.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +# Build universal vt binary for macOS + +echo "Building vt universal binary..." + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +# Build for x86_64 +echo "Building vt for x86_64..." +GOOS=darwin GOARCH=amd64 go build -o vt-x86_64 ./cmd/vt + +# Build for arm64 +echo "Building vt for arm64..." +GOOS=darwin GOARCH=arm64 go build -o vt-arm64 ./cmd/vt + +# Create universal binary +echo "Creating universal binary..." +lipo -create -output vt vt-x86_64 vt-arm64 + +# Clean up architecture-specific binaries +rm vt-x86_64 vt-arm64 + +# Make it executable +chmod +x vt + +echo "vt universal binary built successfully at: $SCRIPT_DIR/vt" + +# Copy to target location if provided +if [ -n "$1" ]; then + echo "Copying vt to $1" + cp vt "$1" +fi \ No newline at end of file diff --git a/linux/claude b/linux/claude new file mode 100755 index 00000000..6fc7b880 --- /dev/null +++ b/linux/claude @@ -0,0 +1 @@ +echo "Claude called with args: $@" diff --git a/linux/cmd/vt/main.go b/linux/cmd/vt/main.go new file mode 100644 index 00000000..026aac5c --- /dev/null +++ b/linux/cmd/vt/main.go @@ -0,0 +1,277 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" +) + +const Version = "1.0.0" + +type Config struct { + Server string `json:"server,omitempty"` +} + +func main() { + // Debug incoming args + if os.Getenv("VT_DEBUG") != "" { + fmt.Fprintf(os.Stderr, "VT Debug: args = %v\n", os.Args) + } + + // Handle version flag only if it's the only argument + if len(os.Args) == 2 && (os.Args[1] == "--version" || os.Args[1] == "-v") { + fmt.Printf("vt version %s\n", Version) + os.Exit(0) + } + + // Get preferred server + server := getPreferredServer() + + // Forward to appropriate server + var err error + switch server { + case "rust": + err = forwardToRustServer(os.Args[1:]) + case "go": + err = forwardToGoServer(os.Args[1:]) + default: + err = forwardToGoServer(os.Args[1:]) + } + + if err != nil { + // If the command exited with a specific code, preserve it + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) + } + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "vt: %v\n", err) + os.Exit(1) + } +} + +func getPreferredServer() string { + // Check environment variable first + if server := os.Getenv("VT_SERVER"); server != "" { + if server == "rust" || server == "go" { + return server + } + } + + // Read from config file + configPath := filepath.Join(os.Getenv("HOME"), ".vibetunnel", "config.json") + data, err := os.ReadFile(configPath) + if err != nil { + return "go" // default + } + + var config Config + if err := json.Unmarshal(data, &config); err != nil { + return "go" // default on parse error + } + + if config.Server == "rust" || config.Server == "go" { + return config.Server + } + return "go" // default for invalid values +} + +func forwardToGoServer(args []string) error { + // Find vibetunnel binary + vibetunnelPath := findVibetunnelBinary() + if vibetunnelPath == "" { + return fmt.Errorf("vibetunnel binary not found") + } + + // Check if this is a special VT command + var finalArgs []string + if len(args) > 0 && isVTSpecialCommand(args[0]) { + // Translate special VT commands + finalArgs = translateVTToGoArgs(args) + } else { + // For regular commands, just prepend -- to tell vibetunnel to stop parsing + finalArgs = append([]string{"--"}, args...) + } + + // Debug: print what we're executing + if os.Getenv("VT_DEBUG") != "" { + fmt.Fprintf(os.Stderr, "VT Debug: executing %s with args: %v\n", vibetunnelPath, finalArgs) + } + + // Create command + cmd := exec.Command(vibetunnelPath, finalArgs...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Run and return + return cmd.Run() +} + +func isVTSpecialCommand(arg string) bool { + switch arg { + case "--claude", "--claude-yolo", "--shell", "-i", + "--no-shell-wrap", "-S", "--show-session-info", "--show-session-id": + return true + } + return false +} + +func forwardToRustServer(args []string) error { + // Find tty-fwd binary + ttyFwdPath := findTtyFwdBinary() + if ttyFwdPath == "" { + return fmt.Errorf("tty-fwd binary not found") + } + + // Create command with original args (tty-fwd already understands vt args) + cmd := exec.Command(ttyFwdPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Run and return + return cmd.Run() +} + +func translateVTToGoArgs(args []string) []string { + if len(args) == 0 { + return args + } + + // Check for special vt-only flags + switch args[0] { + case "--claude": + // Find Claude and run it with any additional arguments + claudePath := findClaude() + if claudePath != "" { + // Pass all remaining args to claude + result := []string{"--", claudePath} + if len(args) > 1 { + result = append(result, args[1:]...) + } + return result + } + // Fallback + result := []string{"--", "claude"} + if len(args) > 1 { + result = append(result, args[1:]...) + } + return result + + case "--claude-yolo": + // Find Claude and run with permissions skip + claudePath := findClaude() + if claudePath != "" { + return []string{"--", claudePath, "--dangerously-skip-permissions"} + } + return []string{"--", "claude", "--dangerously-skip-permissions"} + + case "--shell", "-i": + // Launch interactive shell + shell := os.Getenv("SHELL") + if shell == "" { + shell = "/bin/bash" + } + return []string{"--", shell, "-i"} + + case "--no-shell-wrap", "-S": + // Direct execution without shell - skip the flag and pass rest + if len(args) > 1 { + return append([]string{"--"}, args[1:]...) + } + return []string{} + + case "--show-session-info": + return []string{"--list-sessions"} + + case "--show-session-id": + // This needs special handling - just pass through for now + return args + + default: + // This shouldn't happen since we check isVTSpecialCommand first + return args + } +} + +func findVibetunnelBinary() string { + // Check common locations + paths := []string{ + // App bundle location + "/Applications/VibeTunnel.app/Contents/Resources/vibetunnel", + // Development locations + "./vibetunnel", + "../vibetunnel", + "../../linux/vibetunnel", + // Installed location + "/usr/local/bin/vibetunnel", + } + + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + return path + } + } + + // Try to find in PATH + if path, err := exec.LookPath("vibetunnel"); err == nil { + return path + } + + return "" +} + +func findTtyFwdBinary() string { + // Check common locations + paths := []string{ + // App bundle location + "/Applications/VibeTunnel.app/Contents/Resources/tty-fwd", + // Development locations + "./tty-fwd", + "../tty-fwd", + "../../tty-fwd/target/release/tty-fwd", + // Installed location + "/usr/local/bin/tty-fwd", + } + + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + return path + } + } + + // Try to find in PATH + if path, err := exec.LookPath("tty-fwd"); err == nil { + return path + } + + return "" +} + +func findClaude() string { + // Check common Claude installation paths + claudePaths := []string{ + filepath.Join(os.Getenv("HOME"), ".claude", "local", "claude"), + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", + "/usr/bin/claude", + } + + for _, path := range claudePaths { + if _, err := os.Stat(path); err == nil { + return path + } + } + + // Try PATH + if path, err := exec.LookPath("claude"); err == nil { + return path + } + + return "" +} \ No newline at end of file diff --git a/linux/test-vt.sh b/linux/test-vt.sh new file mode 100755 index 00000000..9e84042b --- /dev/null +++ b/linux/test-vt.sh @@ -0,0 +1,2 @@ +#!/bin/bash +VT_DEBUG=1 ./vt claude --dangerously-skip-permissions \ No newline at end of file