Add new vt binary that supports both rust and go

This commit is contained in:
Peter Steinberger 2025-06-20 00:48:33 +02:00
parent ae270d66f8
commit 943410fc1c
7 changed files with 346 additions and 190 deletions

6
.gitignore vendored
View file

@ -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

View file

@ -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 */

View file

@ -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

36
linux/build-vt-universal.sh Executable file
View file

@ -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

1
linux/claude Executable file
View file

@ -0,0 +1 @@
echo "Claude called with args: $@"

277
linux/cmd/vt/main.go Normal file
View file

@ -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 ""
}

2
linux/test-vt.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
VT_DEBUG=1 ./vt claude --dangerously-skip-permissions