package main import ( "fmt" "strconv" "strings" "time" "github.com/bdw/ship/internal/output" "github.com/bdw/ship/internal/ssh" "github.com/bdw/ship/internal/state" "github.com/spf13/cobra" ) // listV2Cmd lists all deployments var listV2Cmd = &cobra.Command{ Use: "list", Short: "List all deployments", RunE: runListV2, } func runListV2(cmd *cobra.Command, args []string) error { st, err := state.Load() if err != nil { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error())) } hostName := hostFlag if hostName == "" { hostName = st.DefaultHost } if hostName == "" { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified")) } hostConfig := st.GetHost(hostName) client, err := ssh.Connect(hostName) if err != nil { output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error())) } defer client.Close() var deploys []output.DeployInfo // Get all deployed services by checking /etc/ship/ports and /var/www // Check ports (apps and docker) portsOut, _ := client.Run("ls /etc/ship/ports/ 2>/dev/null || true") for _, name := range strings.Fields(portsOut) { if name == "" { continue } info := output.DeployInfo{ Name: name, URL: fmt.Sprintf("https://%s.%s", name, hostConfig.BaseDomain), } // Check if it's docker or binary dockerOut, _ := client.Run(fmt.Sprintf("docker inspect %s 2>/dev/null && echo docker", name)) if strings.Contains(dockerOut, "docker") { info.Type = "docker" } else { info.Type = "binary" } // Check if running statusOut, _ := client.RunSudo(fmt.Sprintf("systemctl is-active %s 2>/dev/null || echo inactive", name)) info.Running = strings.TrimSpace(statusOut) == "active" // Check TTL ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name)) if ttlOut != "" { if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil { info.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339) } } deploys = append(deploys, info) } // Check static sites in /var/www wwwOut, _ := client.Run("ls -d /var/www/*/ 2>/dev/null | xargs -n1 basename 2>/dev/null || true") for _, name := range strings.Fields(wwwOut) { if name == "" || name == "html" { continue } // Skip if already in ports (would be an app, not static) found := false for _, d := range deploys { if d.Name == name { found = true break } } if found { continue } info := output.DeployInfo{ Name: name, URL: fmt.Sprintf("https://%s.%s", name, hostConfig.BaseDomain), Type: "static", Running: true, // Static sites are always "running" } // Check TTL ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name)) if ttlOut != "" { if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil { info.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339) } } deploys = append(deploys, info) } output.PrintAndExit(&output.ListResponse{ Status: "ok", Deploys: deploys, }) return nil } // statusV2Cmd shows status for a single deployment var statusV2Cmd = &cobra.Command{ Use: "status NAME", Short: "Check status of a deployment", Args: cobra.ExactArgs(1), RunE: runStatusV2, } func runStatusV2(cmd *cobra.Command, args []string) error { name := args[0] st, err := state.Load() if err != nil { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error())) } hostName := hostFlag if hostName == "" { hostName = st.DefaultHost } if hostName == "" { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified")) } hostConfig := st.GetHost(hostName) client, err := ssh.Connect(hostName) if err != nil { output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error())) } defer client.Close() // Check if deployment exists portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name)) wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name)) if strings.TrimSpace(portOut) == "" && strings.TrimSpace(wwwExists) == "" { output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name)) } resp := &output.StatusResponse{ Status: "ok", Name: name, URL: fmt.Sprintf("https://%s.%s", name, hostConfig.BaseDomain), } // Determine type and get details if portOut != "" { port, _ := strconv.Atoi(strings.TrimSpace(portOut)) resp.Port = port // Check if docker dockerOut, _ := client.Run(fmt.Sprintf("docker inspect %s 2>/dev/null && echo docker", name)) if strings.Contains(dockerOut, "docker") { resp.Type = "docker" } else { resp.Type = "binary" } // Check if running statusOut, _ := client.RunSudo(fmt.Sprintf("systemctl is-active %s 2>/dev/null || echo inactive", name)) resp.Running = strings.TrimSpace(statusOut) == "active" } else { resp.Type = "static" resp.Running = true } // Check TTL ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name)) if ttlOut != "" { if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil { resp.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339) } } output.PrintAndExit(resp) return nil } // logsV2Cmd shows logs for a deployment var logsV2Cmd = &cobra.Command{ Use: "logs NAME", Short: "View logs for a deployment", Args: cobra.ExactArgs(1), RunE: runLogsV2, } func init() { logsV2Cmd.Flags().Int("lines", 50, "Number of log lines to show") } func runLogsV2(cmd *cobra.Command, args []string) error { name := args[0] lines, _ := cmd.Flags().GetInt("lines") st, err := state.Load() if err != nil { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error())) } hostName := hostFlag if hostName == "" { hostName = st.DefaultHost } if hostName == "" { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified")) } client, err := ssh.Connect(hostName) if err != nil { output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error())) } defer client.Close() // Check if it's a static site (no logs) portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name)) if strings.TrimSpace(portOut) == "" { // Check if static site exists wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name)) if strings.TrimSpace(wwwExists) == "" { output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name)) } // Static site - check Caddy access logs logsOut, err := client.RunSudo(fmt.Sprintf("journalctl -u caddy -n %d --no-pager 2>/dev/null | grep %s || echo 'No logs found'", lines*2, name)) if err != nil { output.PrintAndExit(output.Err(output.ErrServiceFailed, err.Error())) } logLines := strings.Split(strings.TrimSpace(logsOut), "\n") output.PrintAndExit(&output.LogsResponse{ Status: "ok", Name: name, Lines: logLines, }) return nil } // Get journalctl logs logsOut, err := client.RunSudo(fmt.Sprintf("journalctl -u %s -n %d --no-pager 2>/dev/null || echo 'No logs found'", name, lines)) if err != nil { output.PrintAndExit(output.Err(output.ErrServiceFailed, err.Error())) } logLines := strings.Split(strings.TrimSpace(logsOut), "\n") output.PrintAndExit(&output.LogsResponse{ Status: "ok", Name: name, Lines: logLines, }) return nil } // removeV2Cmd removes a deployment var removeV2Cmd = &cobra.Command{ Use: "remove NAME", Short: "Remove a deployment", Args: cobra.ExactArgs(1), RunE: runRemoveV2, } func runRemoveV2(cmd *cobra.Command, args []string) error { name := args[0] st, err := state.Load() if err != nil { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error())) } hostName := hostFlag if hostName == "" { hostName = st.DefaultHost } if hostName == "" { output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified")) } client, err := ssh.Connect(hostName) if err != nil { output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error())) } defer client.Close() // Check if deployment exists portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name)) wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name)) if strings.TrimSpace(portOut) == "" && strings.TrimSpace(wwwExists) == "" { output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name)) } // Stop and disable service client.RunSudo(fmt.Sprintf("systemctl stop %s 2>/dev/null || true", name)) client.RunSudo(fmt.Sprintf("systemctl disable %s 2>/dev/null || true", name)) // Remove files client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name)) client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name)) client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name)) client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name)) client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name)) client.RunSudo(fmt.Sprintf("rm -f /etc/ship/env/%s.env", name)) client.RunSudo(fmt.Sprintf("rm -f /etc/ship/ports/%s", name)) client.RunSudo(fmt.Sprintf("rm -f /etc/ship/ttl/%s", name)) // Remove docker container and image client.Run(fmt.Sprintf("docker rm -f %s 2>/dev/null || true", name)) client.Run(fmt.Sprintf("docker rmi %s 2>/dev/null || true", name)) // Reload services client.RunSudo("systemctl daemon-reload") client.RunSudo("systemctl reload caddy") output.PrintAndExit(&output.RemoveResponse{ Status: "ok", Name: name, Removed: true, }) return nil }