mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-20 13:45:54 +00:00
743 lines
24 KiB
Go
743 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/vibetunnel/linux/pkg/api"
|
|
"github.com/vibetunnel/linux/pkg/config"
|
|
"github.com/vibetunnel/linux/pkg/server"
|
|
"github.com/vibetunnel/linux/pkg/session"
|
|
)
|
|
|
|
var (
|
|
// Version injected at build time
|
|
version = "dev"
|
|
|
|
// Session management flags
|
|
controlPath string
|
|
sessionName string
|
|
listSessions bool
|
|
sendKey string
|
|
sendText string
|
|
signalCmd string
|
|
stopSession bool
|
|
killSession bool
|
|
cleanupExited bool
|
|
detachedSessionID string
|
|
|
|
// Server flags
|
|
serve bool
|
|
staticPath string
|
|
|
|
// Network and access configuration
|
|
port string
|
|
bindAddr string
|
|
localhost bool
|
|
network bool
|
|
|
|
// Security flags
|
|
password string
|
|
passwordEnabled bool
|
|
|
|
// TLS/HTTPS flags (optional, defaults to HTTP like Rust version)
|
|
tlsEnabled bool
|
|
tlsPort string
|
|
tlsDomain string
|
|
tlsSelfSigned bool
|
|
tlsCertPath string
|
|
tlsKeyPath string
|
|
tlsAutoRedirect bool
|
|
|
|
// ngrok integration
|
|
ngrokEnabled bool
|
|
ngrokToken string
|
|
|
|
// Advanced options
|
|
debugMode bool
|
|
cleanupStartup bool
|
|
serverMode string
|
|
updateChannel string
|
|
noSpawn bool
|
|
doNotAllowColumnSet bool
|
|
|
|
// Configuration file
|
|
configFile string
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "vibetunnel",
|
|
Short: "VibeTunnel - Remote terminal access for Linux",
|
|
Long: `VibeTunnel allows you to access your Linux terminal from any web browser.
|
|
This is the Linux implementation compatible with the macOS VibeTunnel app.`,
|
|
RunE: run,
|
|
// Allow positional arguments after flags (for command execution)
|
|
Args: cobra.ArbitraryArgs,
|
|
}
|
|
|
|
func init() {
|
|
homeDir, _ := os.UserHomeDir()
|
|
defaultControlPath := filepath.Join(homeDir, ".vibetunnel", "control")
|
|
defaultConfigPath := filepath.Join(homeDir, ".vibetunnel", "config.yaml")
|
|
|
|
// Session management flags
|
|
rootCmd.Flags().StringVar(&controlPath, "control-path", defaultControlPath, "Control directory path")
|
|
rootCmd.Flags().StringVar(&sessionName, "session-name", "", "Session name")
|
|
rootCmd.Flags().BoolVar(&listSessions, "list-sessions", false, "List all sessions")
|
|
rootCmd.Flags().StringVar(&sendKey, "send-key", "", "Send key to session")
|
|
rootCmd.Flags().StringVar(&sendText, "send-text", "", "Send text to session")
|
|
rootCmd.Flags().StringVar(&signalCmd, "signal", "", "Send signal to session")
|
|
rootCmd.Flags().BoolVar(&stopSession, "stop", false, "Stop session (SIGTERM)")
|
|
rootCmd.Flags().BoolVar(&killSession, "kill", false, "Kill session (SIGKILL)")
|
|
rootCmd.Flags().BoolVar(&cleanupExited, "cleanup-exited", false, "Clean up exited sessions")
|
|
rootCmd.Flags().StringVar(&detachedSessionID, "detached-session", "", "Run as detached session with given ID")
|
|
|
|
// Server flags
|
|
rootCmd.Flags().BoolVar(&serve, "serve", false, "Start HTTP server")
|
|
rootCmd.Flags().StringVar(&staticPath, "static-path", "", "Path for static files")
|
|
|
|
// Network and access configuration (compatible with VibeTunnel settings)
|
|
rootCmd.Flags().StringVarP(&port, "port", "p", "4020", "Server port (default matches VibeTunnel)")
|
|
rootCmd.Flags().StringVar(&bindAddr, "bind", "", "Bind address (auto-detected if empty)")
|
|
rootCmd.Flags().BoolVar(&localhost, "localhost", false, "Bind to localhost only (127.0.0.1)")
|
|
rootCmd.Flags().BoolVar(&network, "network", false, "Bind to all interfaces (0.0.0.0)")
|
|
|
|
// Security flags (compatible with VibeTunnel dashboard settings)
|
|
rootCmd.Flags().StringVar(&password, "password", "", "Dashboard password for Basic Auth")
|
|
rootCmd.Flags().BoolVar(&passwordEnabled, "password-enabled", false, "Enable password protection")
|
|
|
|
// TLS/HTTPS flags (optional enhancement, defaults to HTTP like Rust version)
|
|
rootCmd.Flags().BoolVar(&tlsEnabled, "tls", false, "Enable HTTPS/TLS support")
|
|
rootCmd.Flags().StringVar(&tlsPort, "tls-port", "4443", "HTTPS port")
|
|
rootCmd.Flags().StringVar(&tlsDomain, "tls-domain", "", "Domain for Let's Encrypt (optional)")
|
|
rootCmd.Flags().BoolVar(&tlsSelfSigned, "tls-self-signed", true, "Use self-signed certificates (default)")
|
|
rootCmd.Flags().StringVar(&tlsCertPath, "tls-cert", "", "Custom TLS certificate path")
|
|
rootCmd.Flags().StringVar(&tlsKeyPath, "tls-key", "", "Custom TLS key path")
|
|
rootCmd.Flags().BoolVar(&tlsAutoRedirect, "tls-redirect", false, "Redirect HTTP to HTTPS")
|
|
|
|
// ngrok integration (compatible with VibeTunnel ngrok service)
|
|
rootCmd.Flags().BoolVar(&ngrokEnabled, "ngrok", false, "Enable ngrok tunnel")
|
|
rootCmd.Flags().StringVar(&ngrokToken, "ngrok-token", "", "ngrok auth token")
|
|
|
|
// Advanced options (compatible with VibeTunnel advanced settings)
|
|
rootCmd.Flags().BoolVar(&debugMode, "debug", false, "Enable debug mode")
|
|
rootCmd.Flags().BoolVar(&cleanupStartup, "cleanup-startup", false, "Clean up sessions on startup")
|
|
rootCmd.Flags().StringVar(&serverMode, "server-mode", "native", "Server mode (native, rust)")
|
|
rootCmd.Flags().StringVar(&updateChannel, "update-channel", "stable", "Update channel (stable, prerelease)")
|
|
rootCmd.Flags().BoolVar(&noSpawn, "no-spawn", false, "Disable terminal spawning")
|
|
rootCmd.Flags().BoolVar(&doNotAllowColumnSet, "do-not-allow-column-set", true, "Disable terminal resizing for all sessions (spawned and detached)")
|
|
|
|
// HQ mode flags
|
|
rootCmd.Flags().Bool("hq", false, "Run as HQ (headquarters) server")
|
|
rootCmd.Flags().String("hq-url", "", "HQ server URL (for remote mode)")
|
|
rootCmd.Flags().String("hq-token", "", "HQ server token (for remote mode)")
|
|
rootCmd.Flags().String("bearer-token", "", "Bearer token for authentication (in HQ mode)")
|
|
|
|
// Configuration file
|
|
rootCmd.Flags().StringVarP(&configFile, "config", "c", defaultConfigPath, "Configuration file path")
|
|
|
|
// Add version command
|
|
rootCmd.AddCommand(&cobra.Command{
|
|
Use: "version",
|
|
Short: "Show version information",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
fmt.Printf("VibeTunnel Linux v%s\n", version)
|
|
fmt.Println("Compatible with VibeTunnel macOS app")
|
|
},
|
|
})
|
|
|
|
// Add config command
|
|
rootCmd.AddCommand(&cobra.Command{
|
|
Use: "config",
|
|
Short: "Show configuration",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cfg := config.LoadConfig(configFile)
|
|
cfg.Print()
|
|
},
|
|
})
|
|
}
|
|
|
|
func run(cmd *cobra.Command, args []string) error {
|
|
// Load configuration from file and merge with CLI flags
|
|
cfg := config.LoadConfig(configFile)
|
|
cfg.MergeFlags(cmd.Flags())
|
|
|
|
// Apply configuration
|
|
if cfg.ControlPath != "" {
|
|
controlPath = cfg.ControlPath
|
|
}
|
|
if cfg.Server.Port != "" {
|
|
port = cfg.Server.Port
|
|
}
|
|
|
|
// Handle detached session mode
|
|
if detachedSessionID != "" {
|
|
// We're running as a detached session
|
|
// TODO: Implement RunDetachedSession
|
|
return fmt.Errorf("detached session mode not yet implemented")
|
|
}
|
|
|
|
manager := session.NewManager(controlPath)
|
|
|
|
// Handle cleanup on startup if enabled
|
|
if cfg.Advanced.CleanupStartup || cleanupStartup {
|
|
fmt.Println("Updating session statuses on startup...")
|
|
// Only update statuses, don't remove sessions (matching Rust behavior)
|
|
if err := manager.UpdateAllSessionStatuses(); err != nil {
|
|
fmt.Printf("Warning: status update failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Handle session management operations
|
|
if listSessions {
|
|
sessions, err := manager.ListSessions()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list sessions: %w", err)
|
|
}
|
|
fmt.Printf("ID\t\tName\t\tStatus\t\tCommand\n")
|
|
for _, s := range sessions {
|
|
fmt.Printf("%s\t%s\t\t%s\t\t%s\n", s.ID[:8], s.Name, s.Status, s.Cmdline)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if cleanupExited {
|
|
// Match Rust behavior: actually remove dead sessions on manual cleanup
|
|
return manager.RemoveExitedSessions()
|
|
}
|
|
|
|
// Handle session input/control operations
|
|
if sessionName != "" && (sendKey != "" || sendText != "" || signalCmd != "" || stopSession || killSession) {
|
|
sess, err := manager.FindSession(sessionName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find session: %w", err)
|
|
}
|
|
|
|
if sendKey != "" {
|
|
return sess.SendKey(sendKey)
|
|
}
|
|
if sendText != "" {
|
|
return sess.SendText(sendText)
|
|
}
|
|
if signalCmd != "" {
|
|
return sess.Signal(signalCmd)
|
|
}
|
|
if stopSession {
|
|
return sess.Stop()
|
|
}
|
|
if killSession {
|
|
return sess.Kill()
|
|
}
|
|
}
|
|
|
|
// Handle server mode
|
|
if serve {
|
|
return startServer(cmd, cfg, manager)
|
|
}
|
|
|
|
// Handle direct command execution (create new session)
|
|
if len(args) == 0 {
|
|
// Show comprehensive help when no arguments provided
|
|
showHelp()
|
|
return nil
|
|
}
|
|
|
|
sess, err := manager.CreateSession(session.Config{
|
|
Name: sessionName,
|
|
Cmdline: args,
|
|
Cwd: ".",
|
|
IsSpawned: false, // Command line sessions are detached, not spawned
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Created session: %s (%s)\n", sess.ID, sess.ID[:8])
|
|
return sess.Attach()
|
|
}
|
|
|
|
func startServer(cmd *cobra.Command, cfg *config.Config, manager *session.Manager) error {
|
|
// Terminal spawning behavior:
|
|
// 1. When spawn_terminal=true in API requests, we first try to connect to the Mac app's socket
|
|
// 2. If Mac app is running, it handles the terminal spawn via TerminalSpawnService
|
|
// 3. If Mac app is not running, we fall back to native terminal spawning (osascript on macOS)
|
|
// This matches the Rust implementation's behavior.
|
|
|
|
// Use static path from command line or config
|
|
if staticPath == "" {
|
|
staticPath = cfg.Server.StaticPath
|
|
}
|
|
|
|
// When running from Mac app, static path should always be provided via --static-path
|
|
// When running standalone, user must provide the path
|
|
if staticPath == "" {
|
|
return fmt.Errorf("static path not specified. Use --static-path flag or configure in config file")
|
|
}
|
|
|
|
// Determine password
|
|
serverPassword := password
|
|
if cfg.Security.PasswordEnabled && cfg.Security.Password != "" {
|
|
serverPassword = cfg.Security.Password
|
|
}
|
|
|
|
// Determine bind address
|
|
bindAddress := determineBind(cfg)
|
|
|
|
// Convert port to int
|
|
portInt, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid port: %w", err)
|
|
}
|
|
|
|
// Set the resize flag on the manager
|
|
manager.SetDoNotAllowColumnSet(doNotAllowColumnSet)
|
|
|
|
// Check HQ mode flags
|
|
isHQMode, _ := cmd.Flags().GetBool("hq")
|
|
hqURL, _ := cmd.Flags().GetString("hq-url")
|
|
hqToken, _ := cmd.Flags().GetString("hq-token")
|
|
bearerToken, _ := cmd.Flags().GetString("bearer-token")
|
|
|
|
// Create server
|
|
srv := server.NewServerWithHQMode(manager, staticPath, serverPassword, portInt, isHQMode, bearerToken)
|
|
srv.SetNoSpawn(noSpawn)
|
|
srv.SetDoNotAllowColumnSet(doNotAllowColumnSet)
|
|
|
|
// If HQ URL and token are provided, register with HQ
|
|
if hqURL != "" && hqToken != "" && !isHQMode {
|
|
if err := srv.RegisterWithHQ(hqURL, hqToken); err != nil {
|
|
fmt.Printf("Warning: Failed to register with HQ server: %v\n", err)
|
|
} else {
|
|
fmt.Printf("Registered with HQ server at %s\n", hqURL)
|
|
}
|
|
}
|
|
|
|
// Configure ngrok if enabled
|
|
var ngrokURL string
|
|
if cfg.Ngrok.Enabled || ngrokEnabled {
|
|
authToken := ngrokToken
|
|
if authToken == "" && cfg.Ngrok.AuthToken != "" {
|
|
authToken = cfg.Ngrok.AuthToken
|
|
}
|
|
if authToken != "" {
|
|
// Start ngrok through the server's service
|
|
if err := srv.StartNgrok(authToken); err != nil {
|
|
fmt.Printf("Warning: ngrok failed to start: %v\n", err)
|
|
} else {
|
|
fmt.Printf("Ngrok tunnel starting...\n")
|
|
}
|
|
} else {
|
|
fmt.Printf("Warning: ngrok enabled but no auth token provided\n")
|
|
}
|
|
}
|
|
|
|
// Check if TLS is enabled
|
|
if tlsEnabled {
|
|
// Convert TLS port to int
|
|
tlsPortInt, err := strconv.Atoi(tlsPort)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid TLS port: %w", err)
|
|
}
|
|
|
|
// Create TLS configuration
|
|
tlsConfig := &api.TLSConfig{
|
|
Enabled: true,
|
|
Port: tlsPortInt,
|
|
Domain: tlsDomain,
|
|
SelfSigned: tlsSelfSigned,
|
|
CertPath: tlsCertPath,
|
|
KeyPath: tlsKeyPath,
|
|
AutoRedirect: tlsAutoRedirect,
|
|
}
|
|
|
|
// Create TLS server
|
|
tlsServer := server.NewTLSServer(srv, tlsConfig)
|
|
|
|
// Print startup information for TLS
|
|
fmt.Printf("Starting VibeTunnel HTTPS server on %s:%s\n", bindAddress, tlsPort)
|
|
if tlsAutoRedirect {
|
|
fmt.Printf("HTTP redirect server on %s:%s -> HTTPS\n", bindAddress, port)
|
|
}
|
|
fmt.Printf("Serving web UI from: %s\n", staticPath)
|
|
fmt.Printf("Control directory: %s\n", controlPath)
|
|
|
|
if tlsSelfSigned {
|
|
fmt.Printf("TLS: Using self-signed certificates for localhost\n")
|
|
} else if tlsDomain != "" {
|
|
fmt.Printf("TLS: Using Let's Encrypt for domain: %s\n", tlsDomain)
|
|
} else if tlsCertPath != "" && tlsKeyPath != "" {
|
|
fmt.Printf("TLS: Using custom certificates\n")
|
|
}
|
|
|
|
if serverPassword != "" {
|
|
fmt.Printf("Basic auth enabled with username: admin\n")
|
|
}
|
|
|
|
if ngrokURL != "" {
|
|
fmt.Printf("ngrok tunnel: %s\n", ngrokURL)
|
|
}
|
|
|
|
if cfg.Advanced.DebugMode || debugMode {
|
|
fmt.Printf("Debug mode enabled\n")
|
|
}
|
|
|
|
// Start TLS server
|
|
httpAddr := ""
|
|
if tlsAutoRedirect {
|
|
httpAddr = fmt.Sprintf("%s:%s", bindAddress, port)
|
|
}
|
|
httpsAddr := fmt.Sprintf("%s:%s", bindAddress, tlsPort)
|
|
|
|
return tlsServer.StartTLS(httpAddr, httpsAddr)
|
|
}
|
|
|
|
// Default HTTP behavior (like Rust version)
|
|
fmt.Printf("Starting VibeTunnel server on %s:%s\n", bindAddress, port)
|
|
fmt.Printf("Serving web UI from: %s\n", staticPath)
|
|
fmt.Printf("Control directory: %s\n", controlPath)
|
|
|
|
if serverPassword != "" {
|
|
fmt.Printf("Basic auth enabled with username: admin\n")
|
|
}
|
|
|
|
if ngrokURL != "" {
|
|
fmt.Printf("ngrok tunnel: %s\n", ngrokURL)
|
|
}
|
|
|
|
if cfg.Advanced.DebugMode || debugMode {
|
|
fmt.Printf("Debug mode enabled\n")
|
|
}
|
|
|
|
return srv.Start(fmt.Sprintf("%s:%s", bindAddress, port))
|
|
}
|
|
|
|
func determineBind(cfg *config.Config) string {
|
|
// CLI flags take precedence
|
|
if localhost {
|
|
return "127.0.0.1"
|
|
}
|
|
if network {
|
|
return "0.0.0.0"
|
|
}
|
|
|
|
// Check configuration
|
|
switch cfg.Server.AccessMode {
|
|
case "localhost":
|
|
return "127.0.0.1"
|
|
case "network":
|
|
return "0.0.0.0"
|
|
default:
|
|
// Default to localhost for security
|
|
return "127.0.0.1"
|
|
}
|
|
}
|
|
|
|
func showHelp() {
|
|
fmt.Println("USAGE:")
|
|
fmt.Println(" vt [command] [args...]")
|
|
fmt.Println(" vt --claude [args...]")
|
|
fmt.Println(" vt --claude-yolo [args...]")
|
|
fmt.Println(" vt --shell [args...]")
|
|
fmt.Println(" vt -i [args...]")
|
|
fmt.Println(" vt --no-shell-wrap [command] [args...]")
|
|
fmt.Println(" vt --show-session-info")
|
|
fmt.Println(" vt --show-session-id")
|
|
fmt.Println(" vt -S [command] [args...]")
|
|
fmt.Println(" vt --help")
|
|
fmt.Println()
|
|
fmt.Println("DESCRIPTION:")
|
|
fmt.Println(" This wrapper script allows VibeTunnel to see the output of commands by")
|
|
fmt.Println(" forwarding TTY data through the tty-fwd utility. When you run commands")
|
|
fmt.Println(" through 'vt', VibeTunnel can monitor and display the command's output")
|
|
fmt.Println(" in real-time.")
|
|
fmt.Println()
|
|
fmt.Println(" By default, commands are executed through your shell to resolve aliases,")
|
|
fmt.Println(" functions, and builtins. Use --no-shell-wrap to execute commands directly.")
|
|
fmt.Println()
|
|
fmt.Println("EXAMPLES:")
|
|
fmt.Println(" vt top # Watch top with VibeTunnel monitoring")
|
|
fmt.Println(" vt python script.py # Run Python script with output forwarding")
|
|
fmt.Println(" vt npm test # Run tests with VibeTunnel visibility")
|
|
fmt.Println(" vt --claude # Auto-locate and run Claude")
|
|
fmt.Println(" vt --claude --help # Run Claude with --help option")
|
|
fmt.Println(" vt --claude-yolo # Run Claude with --dangerously-skip-permissions")
|
|
fmt.Println(" vt --shell # Launch current shell (equivalent to vt $SHELL)")
|
|
fmt.Println(" vt -i # Launch current shell (short form)")
|
|
fmt.Println(" vt -S ls -la # List files without shell alias resolution")
|
|
fmt.Println()
|
|
fmt.Println("OPTIONS:")
|
|
fmt.Println(" --claude Auto-locate Claude executable and run it")
|
|
fmt.Println(" --claude-yolo Auto-locate Claude and run with --dangerously-skip-permissions")
|
|
fmt.Println(" --shell, -i Launch current shell (equivalent to vt $SHELL)")
|
|
fmt.Println(" --no-shell-wrap, -S Execute command directly without shell wrapper")
|
|
fmt.Println(" --help, -h Show this help message and exit")
|
|
fmt.Println(" --show-session-info Show current session info")
|
|
fmt.Println(" --show-session-id Show current session ID only")
|
|
fmt.Println()
|
|
fmt.Println("NOTE:")
|
|
fmt.Println(" This script automatically uses the tty-fwd executable bundled with")
|
|
fmt.Println(" VibeTunnel from the Resources folder.")
|
|
}
|
|
|
|
func handleVTMode() {
|
|
// VT mode provides simplified command execution
|
|
// All arguments are treated as a command to execute
|
|
|
|
// Special handling for common shortcuts
|
|
if len(os.Args) > 1 {
|
|
switch os.Args[1] {
|
|
case "--claude":
|
|
// vt --claude -> vibetunnel -- claude
|
|
os.Args = append([]string{"vibetunnel", "--"}, append([]string{"claude"}, os.Args[2:]...)...)
|
|
case "--claude-yolo":
|
|
// vt --claude-yolo -> vibetunnel -- claude --dangerously-skip-permissions
|
|
claudeArgs := []string{"claude", "--dangerously-skip-permissions"}
|
|
claudeArgs = append(claudeArgs, os.Args[2:]...)
|
|
os.Args = append([]string{"vibetunnel", "--"}, claudeArgs...)
|
|
case "--shell", "-i":
|
|
// vt --shell or vt -i -> vibetunnel -- $SHELL
|
|
shell := os.Getenv("SHELL")
|
|
if shell == "" {
|
|
shell = "/bin/bash"
|
|
}
|
|
os.Args = []string{"vibetunnel", "--", shell}
|
|
case "--help", "-h":
|
|
// Show vt-specific help
|
|
showHelp()
|
|
return
|
|
case "--show-session-info", "--show-session-id":
|
|
// Pass through to vibetunnel
|
|
os.Args = append([]string{"vibetunnel"}, os.Args[1:]...)
|
|
case "--no-shell-wrap", "-S":
|
|
// Direct command execution without shell wrapper
|
|
if len(os.Args) > 2 {
|
|
os.Args = append([]string{"vibetunnel", "--"}, os.Args[2:]...)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Error: --no-shell-wrap requires a command\n")
|
|
os.Exit(1)
|
|
}
|
|
default:
|
|
// Regular command: vt <cmd> -> vibetunnel -- <cmd>
|
|
os.Args = append([]string{"vibetunnel", "--"}, os.Args[1:]...)
|
|
}
|
|
} else {
|
|
// No args - open interactive shell
|
|
shell := os.Getenv("SHELL")
|
|
if shell == "" {
|
|
shell = "/bin/bash"
|
|
}
|
|
os.Args = []string{"vibetunnel", "--", shell}
|
|
}
|
|
|
|
// Always add column set restriction for vt mode
|
|
os.Args = append(os.Args, "--do-not-allow-column-set=true")
|
|
|
|
// Execute root command with modified args
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
// Check if we're running as "vt" via symlink
|
|
execName := filepath.Base(os.Args[0])
|
|
if execName == "vt" {
|
|
// VT mode - simplified command execution
|
|
handleVTMode()
|
|
return
|
|
}
|
|
|
|
// Check if we're being run with TTY_SESSION_ID (spawned by Mac app)
|
|
if sessionID := os.Getenv("TTY_SESSION_ID"); sessionID != "" {
|
|
// We're running in a terminal spawned by the Mac app
|
|
// Redirect logs to avoid polluting the terminal
|
|
logFile, err := os.OpenFile("/tmp/vibetunnel-session.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err == nil {
|
|
log.SetOutput(logFile)
|
|
defer func() {
|
|
if err := logFile.Close(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to close log file: %v\n", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Use the existing session ID instead of creating a new one
|
|
homeDir, _ := os.UserHomeDir()
|
|
defaultControlPath := filepath.Join(homeDir, ".vibetunnel", "control")
|
|
cfg := config.LoadConfig(filepath.Join(homeDir, ".vibetunnel", "config.yaml"))
|
|
if cfg.ControlPath != "" {
|
|
defaultControlPath = cfg.ControlPath
|
|
}
|
|
|
|
manager := session.NewManager(defaultControlPath)
|
|
|
|
// Wait for the session to be created by the API server
|
|
// The server creates the session before sending the spawn request
|
|
var sess *session.Session
|
|
for i := 0; i < 50; i++ { // Try for up to 5 seconds
|
|
sess, err = manager.GetSession(sessionID)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: Session %s not found\n", sessionID)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// For spawned sessions, we need to execute the command and connect I/O
|
|
// The session was already created by the server, we just need to run the command
|
|
info := sess.GetInfo()
|
|
if info == nil {
|
|
fmt.Fprintf(os.Stderr, "Error: Failed to get session info\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Execute the command that was stored in the session
|
|
if len(info.Args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Error: No command specified in session\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Create a new PTY and attach it to the existing session
|
|
if err := sess.AttachSpawnedSession(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check for special case: if we have args but no recognized VibeTunnel flags,
|
|
// treat everything as a command to execute (compatible with old Rust behavior)
|
|
if len(os.Args) > 1 {
|
|
// Parse flags without executing to check what we have
|
|
rootCmd.DisableFlagParsing = true
|
|
if err := rootCmd.ParseFlags(os.Args[1:]); err != nil {
|
|
// Parse errors are expected at this stage during command detection
|
|
_ = err // Explicitly ignore the error
|
|
}
|
|
rootCmd.DisableFlagParsing = false
|
|
|
|
// Get the command and check if first arg is a subcommand
|
|
args := os.Args[1:]
|
|
if len(args) > 0 && (args[0] == "version" || args[0] == "config") {
|
|
// This is a subcommand, let Cobra handle it normally
|
|
} else {
|
|
// Check if we have a -- separator (everything after it is the command)
|
|
dashDashIndex := -1
|
|
for i, arg := range args {
|
|
if arg == "--" {
|
|
dashDashIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if dashDashIndex >= 0 {
|
|
// We have a -- separator, everything after it is the command to execute
|
|
cmdArgs := args[dashDashIndex+1:]
|
|
if len(cmdArgs) > 0 {
|
|
homeDir, _ := os.UserHomeDir()
|
|
defaultControlPath := filepath.Join(homeDir, ".vibetunnel", "control")
|
|
cfg := config.LoadConfig(filepath.Join(homeDir, ".vibetunnel", "config.yaml"))
|
|
if cfg.ControlPath != "" {
|
|
defaultControlPath = cfg.ControlPath
|
|
}
|
|
|
|
manager := session.NewManager(defaultControlPath)
|
|
sess, err := manager.CreateSession(session.Config{
|
|
Name: "",
|
|
Cmdline: cmdArgs,
|
|
Cwd: ".",
|
|
IsSpawned: false, // Command line sessions are detached
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Attach to the session
|
|
if err := sess.Attach(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
} else {
|
|
// No -- separator, check if any args look like VibeTunnel flags
|
|
hasVibeTunnelFlags := false
|
|
for _, arg := range args {
|
|
if strings.HasPrefix(arg, "-") {
|
|
// Check if this is one of our known flags
|
|
flag := strings.TrimLeft(arg, "-")
|
|
flag = strings.Split(flag, "=")[0] // Handle --flag=value format
|
|
|
|
knownFlags := []string{
|
|
"serve", "port", "p", "bind", "localhost", "network",
|
|
"password", "password-enabled", "tls", "tls-port", "tls-domain",
|
|
"tls-self-signed", "tls-cert", "tls-key", "tls-redirect",
|
|
"ngrok", "ngrok-token", "debug", "cleanup-startup",
|
|
"server-mode", "update-channel", "config", "c",
|
|
"control-path", "session-name", "list-sessions",
|
|
"send-key", "send-text", "signal", "stop", "kill",
|
|
"cleanup-exited", "detached-session", "static-path", "help", "h",
|
|
}
|
|
|
|
for _, known := range knownFlags {
|
|
if flag == known {
|
|
hasVibeTunnelFlags = true
|
|
break
|
|
}
|
|
}
|
|
if hasVibeTunnelFlags {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no VibeTunnel flags found, treat everything as a command
|
|
if !hasVibeTunnelFlags && len(args) > 0 {
|
|
homeDir, _ := os.UserHomeDir()
|
|
defaultControlPath := filepath.Join(homeDir, ".vibetunnel", "control")
|
|
cfg := config.LoadConfig(filepath.Join(homeDir, ".vibetunnel", "config.yaml"))
|
|
if cfg.ControlPath != "" {
|
|
defaultControlPath = cfg.ControlPath
|
|
}
|
|
|
|
manager := session.NewManager(defaultControlPath)
|
|
sess, err := manager.CreateSession(session.Config{
|
|
Name: "",
|
|
Cmdline: args,
|
|
Cwd: ".",
|
|
IsSpawned: false, // Command line sessions are detached
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Attach to the session
|
|
if err := sess.Attach(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to Cobra command handling for flags and structured commands
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|