package main import ( "flag" "fmt" "os" "os/exec" "github.com/bdw/deploy/internal/ssh" "github.com/bdw/deploy/internal/state" ) func runVPS(args []string) { fs := flag.NewFlagSet("vps", flag.ExitOnError) host := fs.String("host", "", "VPS host (SSH config alias or user@host)") fs.Parse(args) // Load state st, err := state.Load() if err != nil { fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) os.Exit(1) } // Get host from flag or state default if *host == "" { *host = st.GetDefaultHost() } if *host == "" { fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") fs.Usage() os.Exit(1) } fmt.Printf("Connecting to %s...\n\n", *host) // Connect to VPS client, err := ssh.Connect(*host) if err != nil { fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) os.Exit(1) } defer client.Close() // Uptime fmt.Println("UPTIME") if output, err := client.Run("uptime -p"); err == nil { fmt.Printf(" %s", output) } if output, err := client.Run("uptime -s"); err == nil { fmt.Printf(" Since: %s", output) } fmt.Println() // Load average fmt.Println("LOAD") if output, err := client.Run("cat /proc/loadavg | awk '{print $1, $2, $3}'"); err == nil { fmt.Printf(" 1m, 5m, 15m: %s", output) } fmt.Println() // Memory fmt.Println("MEMORY") if output, err := client.Run("free -h | awk 'NR==2 {print \" Used: \" $3 \" / \" $2}'"); err == nil { fmt.Print(output) } if output, err := client.Run("free -h | awk 'NR==2 {printf \" Available: %s\\n\", $7}'"); err == nil { fmt.Print(output) } fmt.Println() // Disk fmt.Println("DISK") if output, err := client.Run("df -h / | awk 'NR==2 {print \" Used: \" $3 \" / \" $2 \" (\" $5 \")\"}'"); err == nil { fmt.Print(output) } if output, err := client.Run("df -h / | awk 'NR==2 {print \" Available: \" $4}'"); err == nil { fmt.Print(output) } fmt.Println() // Updates available fmt.Println("UPDATES") if output, err := client.Run("[ -f /var/lib/update-notifier/updates-available ] && cat /var/lib/update-notifier/updates-available | head -2 || echo ' (update info not available)'"); err == nil { fmt.Print(output) } fmt.Println() // Services fmt.Println("SERVICES") if output, err := client.Run("systemctl is-active caddy 2>/dev/null && echo ' Caddy: active' || echo ' Caddy: inactive'"); err == nil { // Parse the output if output == "active\n" { fmt.Println(" Caddy: active") } else { fmt.Println(" Caddy: inactive") } } // Count deployed apps that are running hostState := st.GetHost(*host) if hostState != nil && len(hostState.Apps) > 0 { activeCount := 0 for name, app := range hostState.Apps { if app.Type == "app" { if output, err := client.Run(fmt.Sprintf("systemctl is-active %s 2>/dev/null", name)); err == nil && output == "active\n" { activeCount++ } } } appCount := 0 for _, app := range hostState.Apps { if app.Type == "app" { appCount++ } } fmt.Printf(" Deployed apps: %d/%d active\n", activeCount, appCount) } } func runUpdate(args []string) { fs := flag.NewFlagSet("vps-update", flag.ExitOnError) host := fs.String("host", "", "VPS host (SSH config alias or user@host)") yes := fs.Bool("y", false, "Skip confirmation prompt") fs.Parse(args) // Load state st, err := state.Load() if err != nil { fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) os.Exit(1) } // Get host from flag or state default if *host == "" { *host = st.GetDefaultHost() } if *host == "" { fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") fs.Usage() os.Exit(1) } // Confirm unless -y flag is set if !*yes { fmt.Printf("This will run apt update && apt upgrade on %s\n", *host) fmt.Print("Continue? [y/N]: ") var response string fmt.Scanln(&response) if response != "y" && response != "Y" { fmt.Println("Aborted.") return } } fmt.Printf("Connecting to %s...\n", *host) // Connect to VPS client, err := ssh.Connect(*host) if err != nil { fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) os.Exit(1) } defer client.Close() // Run apt update fmt.Println("\n→ Running apt update...") if err := client.RunSudoStream("apt update"); err != nil { fmt.Fprintf(os.Stderr, "Error running apt update: %v\n", err) os.Exit(1) } // Run apt upgrade fmt.Println("\n→ Running apt upgrade...") if err := client.RunSudoStream("DEBIAN_FRONTEND=noninteractive apt upgrade -y"); err != nil { fmt.Fprintf(os.Stderr, "Error running apt upgrade: %v\n", err) os.Exit(1) } // Check if reboot is required fmt.Println() if output, err := client.Run("[ -f /var/run/reboot-required ] && echo 'yes' || echo 'no'"); err == nil { if output == "yes\n" { fmt.Println("Note: A reboot is required to complete the update.") } } fmt.Println("āœ“ Update complete") } func runSSH(args []string) { fs := flag.NewFlagSet("vps-ssh", flag.ExitOnError) host := fs.String("host", "", "VPS host (SSH config alias or user@host)") fs.Parse(args) // Load state st, err := state.Load() if err != nil { fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) os.Exit(1) } // Get host from flag or state default if *host == "" { *host = st.GetDefaultHost() } if *host == "" { fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") fs.Usage() os.Exit(1) } // Launch interactive SSH session cmd := exec.Command("ssh", *host) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) } fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } }