// Package output provides JSON response types for ship v2. // All commands output JSON by default. Human-readable output is opt-in via --pretty. package output import ( "encoding/json" "fmt" "os" ) // Response is the base interface for all output types type Response interface { IsError() bool } // DeployResponse is returned on successful deploy type DeployResponse struct { Status string `json:"status"` Name string `json:"name"` URL string `json:"url"` Type string `json:"type"` // "static", "docker", "binary" TookMs int64 `json:"took_ms"` Health *HealthResult `json:"health,omitempty"` Expires string `json:"expires,omitempty"` // ISO 8601, only if TTL set } func (r DeployResponse) IsError() bool { return false } // HealthResult is the health check outcome type HealthResult struct { Endpoint string `json:"endpoint"` Status int `json:"status"` LatencyMs int64 `json:"latency_ms"` } // ListResponse is returned by ship list type ListResponse struct { Status string `json:"status"` Deploys []DeployInfo `json:"deploys"` } func (r ListResponse) IsError() bool { return false } // DeployInfo is a single deploy in a list type DeployInfo struct { Name string `json:"name"` URL string `json:"url"` Type string `json:"type"` Running bool `json:"running"` Expires string `json:"expires,omitempty"` } // StatusResponse is returned by ship status type StatusResponse struct { Status string `json:"status"` Name string `json:"name"` URL string `json:"url"` Type string `json:"type"` Running bool `json:"running"` Port int `json:"port,omitempty"` Expires string `json:"expires,omitempty"` Memory string `json:"memory,omitempty"` CPU string `json:"cpu,omitempty"` } func (r StatusResponse) IsError() bool { return false } // LogsResponse is returned by ship logs type LogsResponse struct { Status string `json:"status"` Name string `json:"name"` Lines []string `json:"lines"` } func (r LogsResponse) IsError() bool { return false } // RemoveResponse is returned by ship remove type RemoveResponse struct { Status string `json:"status"` Name string `json:"name"` Removed bool `json:"removed"` } func (r RemoveResponse) IsError() bool { return false } // HostInitResponse is returned by ship host init type HostInitResponse struct { Status string `json:"status"` Host string `json:"host"` Domain string `json:"domain"` Installed []string `json:"installed"` } func (r HostInitResponse) IsError() bool { return false } // ErrorResponse is returned on any failure type ErrorResponse struct { Status string `json:"status"` // always "error" Code string `json:"code"` Message string `json:"message"` Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` } func (r ErrorResponse) IsError() bool { return true } // Error codes const ( ErrInvalidPath = "INVALID_PATH" ErrInvalidArgs = "INVALID_ARGS" ErrUnknownProjectType = "UNKNOWN_PROJECT_TYPE" ErrSSHConnectFailed = "SSH_CONNECT_FAILED" ErrSSHAuthFailed = "SSH_AUTH_FAILED" ErrUploadFailed = "UPLOAD_FAILED" ErrBuildFailed = "BUILD_FAILED" ErrServiceFailed = "SERVICE_FAILED" ErrCaddyFailed = "CADDY_FAILED" ErrHealthCheckFailed = "HEALTH_CHECK_FAILED" ErrHealthCheckTimeout = "HEALTH_CHECK_TIMEOUT" ErrNotFound = "NOT_FOUND" ErrConflict = "CONFLICT" ErrHostNotConfigured = "HOST_NOT_CONFIGURED" ErrInvalidTTL = "INVALID_TTL" ErrInvalidName = "INVALID_NAME" ErrPortExhausted = "PORT_EXHAUSTED" ) // Exit codes const ( ExitSuccess = 0 ExitDeployFailed = 1 ExitInvalidArgs = 2 ExitSSHFailed = 3 ExitHealthFailed = 4 ) // Pretty controls human-readable output var Pretty bool // Print outputs the response as JSON (or pretty if enabled) func Print(r Response) { if Pretty { printPretty(r) return } enc := json.NewEncoder(os.Stdout) enc.Encode(r) } // PrintAndExit outputs the response and exits with appropriate code func PrintAndExit(r Response) { Print(r) if r.IsError() { os.Exit(exitCodeForError(r.(*ErrorResponse).Code)) } os.Exit(ExitSuccess) } // Err creates an ErrorResponse func Err(code, message string) *ErrorResponse { return &ErrorResponse{ Status: "error", Code: code, Message: message, } } // ErrWithName creates an ErrorResponse with name context func ErrWithName(code, message, name string) *ErrorResponse { return &ErrorResponse{ Status: "error", Code: code, Message: message, Name: name, } } func exitCodeForError(code string) int { switch code { case ErrSSHConnectFailed, ErrSSHAuthFailed: return ExitSSHFailed case ErrHealthCheckFailed, ErrHealthCheckTimeout: return ExitHealthFailed case ErrInvalidPath, ErrInvalidTTL, ErrInvalidName, ErrHostNotConfigured, ErrInvalidArgs: return ExitInvalidArgs default: return ExitDeployFailed } } func printPretty(r Response) { switch v := r.(type) { case *DeployResponse: fmt.Printf("✓ Deployed to %s (%.1fs)\n", v.URL, float64(v.TookMs)/1000) case *ListResponse: if len(v.Deploys) == 0 { fmt.Println("No deployments") return } fmt.Printf("%-20s %-40s %-8s %s\n", "NAME", "URL", "TYPE", "STATUS") for _, d := range v.Deploys { status := "running" if !d.Running { status = "stopped" } if d.Expires != "" { status += " (expires " + d.Expires + ")" } fmt.Printf("%-20s %-40s %-8s %s\n", d.Name, d.URL, d.Type, status) } case *RemoveResponse: fmt.Printf("✓ Removed %s\n", v.Name) case *ErrorResponse: fmt.Printf("✗ %s: %s\n", v.Code, v.Message) case *HostInitResponse: fmt.Printf("✓ Initialized %s with domain %s\n", v.Host, v.Domain) default: // Fallback to JSON enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(r) } }