summaryrefslogtreecommitdiffstats
path: root/internal/output/output.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/output/output.go')
-rw-r--r--internal/output/output.go226
1 files changed, 226 insertions, 0 deletions
diff --git a/internal/output/output.go b/internal/output/output.go
new file mode 100644
index 0000000..a9a1036
--- /dev/null
+++ b/internal/output/output.go
@@ -0,0 +1,226 @@
1// Package output provides JSON response types for ship v2.
2// All commands output JSON by default. Human-readable output is opt-in via --pretty.
3package output
4
5import (
6 "encoding/json"
7 "fmt"
8 "os"
9)
10
11// Response is the base interface for all output types
12type Response interface {
13 IsError() bool
14}
15
16// DeployResponse is returned on successful deploy
17type DeployResponse struct {
18 Status string `json:"status"`
19 Name string `json:"name"`
20 URL string `json:"url"`
21 Type string `json:"type"` // "static", "docker", "binary"
22 TookMs int64 `json:"took_ms"`
23 Health *HealthResult `json:"health,omitempty"`
24 Expires string `json:"expires,omitempty"` // ISO 8601, only if TTL set
25}
26
27func (r DeployResponse) IsError() bool { return false }
28
29// HealthResult is the health check outcome
30type HealthResult struct {
31 Endpoint string `json:"endpoint"`
32 Status int `json:"status"`
33 LatencyMs int64 `json:"latency_ms"`
34}
35
36// ListResponse is returned by ship list
37type ListResponse struct {
38 Status string `json:"status"`
39 Deploys []DeployInfo `json:"deploys"`
40}
41
42func (r ListResponse) IsError() bool { return false }
43
44// DeployInfo is a single deploy in a list
45type DeployInfo struct {
46 Name string `json:"name"`
47 URL string `json:"url"`
48 Type string `json:"type"`
49 Running bool `json:"running"`
50 Expires string `json:"expires,omitempty"`
51}
52
53// StatusResponse is returned by ship status
54type StatusResponse struct {
55 Status string `json:"status"`
56 Name string `json:"name"`
57 URL string `json:"url"`
58 Type string `json:"type"`
59 Running bool `json:"running"`
60 Port int `json:"port,omitempty"`
61 Expires string `json:"expires,omitempty"`
62 Memory string `json:"memory,omitempty"`
63 CPU string `json:"cpu,omitempty"`
64}
65
66func (r StatusResponse) IsError() bool { return false }
67
68// LogsResponse is returned by ship logs
69type LogsResponse struct {
70 Status string `json:"status"`
71 Name string `json:"name"`
72 Lines []string `json:"lines"`
73}
74
75func (r LogsResponse) IsError() bool { return false }
76
77// RemoveResponse is returned by ship remove
78type RemoveResponse struct {
79 Status string `json:"status"`
80 Name string `json:"name"`
81 Removed bool `json:"removed"`
82}
83
84func (r RemoveResponse) IsError() bool { return false }
85
86// HostInitResponse is returned by ship host init
87type HostInitResponse struct {
88 Status string `json:"status"`
89 Host string `json:"host"`
90 Domain string `json:"domain"`
91 Installed []string `json:"installed"`
92}
93
94func (r HostInitResponse) IsError() bool { return false }
95
96// ErrorResponse is returned on any failure
97type ErrorResponse struct {
98 Status string `json:"status"` // always "error"
99 Code string `json:"code"`
100 Message string `json:"message"`
101 Name string `json:"name,omitempty"`
102 URL string `json:"url,omitempty"`
103}
104
105func (r ErrorResponse) IsError() bool { return true }
106
107// Error implements the error interface for compatibility with v1 code
108func (r *ErrorResponse) Error() string { return r.Message }
109
110// Error codes
111const (
112 ErrInvalidPath = "INVALID_PATH"
113 ErrInvalidArgs = "INVALID_ARGS"
114 ErrUnknownProjectType = "UNKNOWN_PROJECT_TYPE"
115 ErrSSHConnectFailed = "SSH_CONNECT_FAILED"
116 ErrSSHAuthFailed = "SSH_AUTH_FAILED"
117 ErrUploadFailed = "UPLOAD_FAILED"
118 ErrBuildFailed = "BUILD_FAILED"
119 ErrServiceFailed = "SERVICE_FAILED"
120 ErrCaddyFailed = "CADDY_FAILED"
121 ErrHealthCheckFailed = "HEALTH_CHECK_FAILED"
122 ErrHealthCheckTimeout = "HEALTH_CHECK_TIMEOUT"
123 ErrNotFound = "NOT_FOUND"
124 ErrConflict = "CONFLICT"
125 ErrHostNotConfigured = "HOST_NOT_CONFIGURED"
126 ErrInvalidTTL = "INVALID_TTL"
127 ErrInvalidName = "INVALID_NAME"
128 ErrPortExhausted = "PORT_EXHAUSTED"
129)
130
131// Exit codes
132const (
133 ExitSuccess = 0
134 ExitDeployFailed = 1
135 ExitInvalidArgs = 2
136 ExitSSHFailed = 3
137 ExitHealthFailed = 4
138)
139
140// Pretty controls human-readable output
141var Pretty bool
142
143// Print outputs the response as JSON (or pretty if enabled)
144func Print(r Response) {
145 if Pretty {
146 printPretty(r)
147 return
148 }
149 enc := json.NewEncoder(os.Stdout)
150 enc.Encode(r)
151}
152
153// PrintAndExit outputs the response and exits with appropriate code
154func PrintAndExit(r Response) {
155 Print(r)
156 if r.IsError() {
157 os.Exit(exitCodeForError(r.(*ErrorResponse).Code))
158 }
159 os.Exit(ExitSuccess)
160}
161
162// Err creates an ErrorResponse
163func Err(code, message string) *ErrorResponse {
164 return &ErrorResponse{
165 Status: "error",
166 Code: code,
167 Message: message,
168 }
169}
170
171// ErrWithName creates an ErrorResponse with name context
172func ErrWithName(code, message, name string) *ErrorResponse {
173 return &ErrorResponse{
174 Status: "error",
175 Code: code,
176 Message: message,
177 Name: name,
178 }
179}
180
181func exitCodeForError(code string) int {
182 switch code {
183 case ErrSSHConnectFailed, ErrSSHAuthFailed:
184 return ExitSSHFailed
185 case ErrHealthCheckFailed, ErrHealthCheckTimeout:
186 return ExitHealthFailed
187 case ErrInvalidPath, ErrInvalidTTL, ErrInvalidName, ErrHostNotConfigured, ErrInvalidArgs:
188 return ExitInvalidArgs
189 default:
190 return ExitDeployFailed
191 }
192}
193
194func printPretty(r Response) {
195 switch v := r.(type) {
196 case *DeployResponse:
197 fmt.Printf("✓ Deployed to %s (%.1fs)\n", v.URL, float64(v.TookMs)/1000)
198 case *ListResponse:
199 if len(v.Deploys) == 0 {
200 fmt.Println("No deployments")
201 return
202 }
203 fmt.Printf("%-20s %-40s %-8s %s\n", "NAME", "URL", "TYPE", "STATUS")
204 for _, d := range v.Deploys {
205 status := "running"
206 if !d.Running {
207 status = "stopped"
208 }
209 if d.Expires != "" {
210 status += " (expires " + d.Expires + ")"
211 }
212 fmt.Printf("%-20s %-40s %-8s %s\n", d.Name, d.URL, d.Type, status)
213 }
214 case *RemoveResponse:
215 fmt.Printf("✓ Removed %s\n", v.Name)
216 case *ErrorResponse:
217 fmt.Printf("✗ %s: %s\n", v.Code, v.Message)
218 case *HostInitResponse:
219 fmt.Printf("✓ Initialized %s with domain %s\n", v.Host, v.Domain)
220 default:
221 // Fallback to JSON
222 enc := json.NewEncoder(os.Stdout)
223 enc.SetIndent("", " ")
224 enc.Encode(r)
225 }
226}