summaryrefslogtreecommitdiffstats
path: root/cmd/deploy/manage.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
committerbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
commit98b9af372025595e8a4255538e2836e019311474 (patch)
tree0a26fa5a8a19ea8565da6d63e1f19c21fc170d12 /cmd/deploy/manage.go
parent7fcb9dfa87310e91b527829ece9989decb6fda64 (diff)
Add deploy command and fix static site naming
Static sites now default to using the domain as the name instead of the source directory basename, preventing conflicts when multiple sites use the same directory name (e.g., dist). Also fixes .gitignore to not exclude cmd/deploy/ directory.
Diffstat (limited to 'cmd/deploy/manage.go')
-rw-r--r--cmd/deploy/manage.go327
1 files changed, 327 insertions, 0 deletions
diff --git a/cmd/deploy/manage.go b/cmd/deploy/manage.go
new file mode 100644
index 0000000..3cee1f4
--- /dev/null
+++ b/cmd/deploy/manage.go
@@ -0,0 +1,327 @@
1package main
2
3import (
4 "flag"
5 "fmt"
6 "os"
7
8 "github.com/bdw/deploy/internal/config"
9 "github.com/bdw/deploy/internal/ssh"
10 "github.com/bdw/deploy/internal/state"
11)
12
13func runRemove(args []string) {
14 fs := flag.NewFlagSet("remove", flag.ExitOnError)
15 host := fs.String("host", "", "VPS host (SSH config alias or user@host)")
16 fs.Parse(args)
17
18 if len(fs.Args()) == 0 {
19 fmt.Fprintf(os.Stderr, "Error: app name is required\n")
20 fmt.Fprintf(os.Stderr, "Usage: deploy remove <app-name> --host user@vps-ip\n")
21 os.Exit(1)
22 }
23
24 name := fs.Args()[0]
25
26 // Get host from flag or config
27 if *host == "" {
28 cfg, err := config.Load()
29 if err != nil {
30 fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
31 os.Exit(1)
32 }
33 *host = cfg.Host
34 }
35
36 if *host == "" {
37 fmt.Fprintf(os.Stderr, "Error: --host is required\n")
38 fs.Usage()
39 os.Exit(1)
40 }
41
42 // Load state
43 st, err := state.Load()
44 if err != nil {
45 fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
46 os.Exit(1)
47 }
48
49 // Get app info
50 app, err := st.GetApp(*host, name)
51 if err != nil {
52 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
53 os.Exit(1)
54 }
55
56 fmt.Printf("Removing deployment: %s\n", name)
57
58 // Connect to VPS
59 client, err := ssh.Connect(*host)
60 if err != nil {
61 fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err)
62 os.Exit(1)
63 }
64 defer client.Close()
65
66 if app.Type == "app" {
67 // Stop and disable service
68 fmt.Println("→ Stopping service...")
69 client.RunSudo(fmt.Sprintf("systemctl stop %s", name))
70 client.RunSudo(fmt.Sprintf("systemctl disable %s", name))
71
72 // Remove systemd unit
73 client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name))
74 client.RunSudo("systemctl daemon-reload")
75
76 // Remove binary
77 client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name))
78
79 // Remove working directory
80 client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name))
81
82 // Remove env file
83 client.RunSudo(fmt.Sprintf("rm -f /etc/deploy/env/%s.env", name))
84
85 // Remove user
86 client.RunSudo(fmt.Sprintf("userdel %s", name))
87 } else {
88 // Remove static site files
89 fmt.Println("→ Removing files...")
90 client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name))
91 }
92
93 // Remove Caddy config
94 fmt.Println("→ Removing Caddy config...")
95 client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name))
96
97 // Reload Caddy
98 fmt.Println("→ Reloading Caddy...")
99 if _, err := client.RunSudo("systemctl reload caddy"); err != nil {
100 fmt.Fprintf(os.Stderr, "Warning: Error reloading Caddy: %v\n", err)
101 }
102
103 // Update state
104 if err := st.RemoveApp(*host, name); err != nil {
105 fmt.Fprintf(os.Stderr, "Error updating state: %v\n", err)
106 os.Exit(1)
107 }
108 if err := st.Save(); err != nil {
109 fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err)
110 os.Exit(1)
111 }
112
113 fmt.Printf("✓ Deployment removed successfully\n")
114}
115
116func runLogs(args []string) {
117 fs := flag.NewFlagSet("logs", flag.ExitOnError)
118 host := fs.String("host", "", "VPS host (SSH config alias or user@host)")
119 follow := fs.Bool("f", false, "Follow logs")
120 lines := fs.Int("n", 50, "Number of lines to show")
121 fs.Parse(args)
122
123 if len(fs.Args()) == 0 {
124 fmt.Fprintf(os.Stderr, "Error: app name is required\n")
125 fmt.Fprintf(os.Stderr, "Usage: deploy logs <app-name> --host user@vps-ip\n")
126 os.Exit(1)
127 }
128
129 name := fs.Args()[0]
130
131 // Get host from flag or config
132 if *host == "" {
133 cfg, err := config.Load()
134 if err != nil {
135 fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
136 os.Exit(1)
137 }
138 *host = cfg.Host
139 }
140
141 if *host == "" {
142 fmt.Fprintf(os.Stderr, "Error: --host is required\n")
143 fs.Usage()
144 os.Exit(1)
145 }
146
147 // Load state to verify app exists
148 st, err := state.Load()
149 if err != nil {
150 fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
151 os.Exit(1)
152 }
153
154 app, err := st.GetApp(*host, name)
155 if err != nil {
156 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
157 os.Exit(1)
158 }
159
160 if app.Type != "app" {
161 fmt.Fprintf(os.Stderr, "Error: logs are only available for apps, not static sites\n")
162 os.Exit(1)
163 }
164
165 // Connect to VPS
166 client, err := ssh.Connect(*host)
167 if err != nil {
168 fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err)
169 os.Exit(1)
170 }
171 defer client.Close()
172
173 // Build journalctl command
174 cmd := fmt.Sprintf("journalctl -u %s -n %d", name, *lines)
175 if *follow {
176 cmd += " -f"
177 }
178
179 // Run command
180 if *follow {
181 // Stream output for follow mode (no sudo needed for journalctl)
182 if err := client.RunStream(cmd); err != nil {
183 fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err)
184 os.Exit(1)
185 }
186 } else {
187 // Buffer output for non-follow mode (no sudo needed for journalctl)
188 output, err := client.Run(cmd)
189 if err != nil {
190 fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err)
191 os.Exit(1)
192 }
193 fmt.Print(output)
194 }
195}
196
197func runStatus(args []string) {
198 fs := flag.NewFlagSet("status", flag.ExitOnError)
199 host := fs.String("host", "", "VPS host (SSH config alias or user@host)")
200 fs.Parse(args)
201
202 if len(fs.Args()) == 0 {
203 fmt.Fprintf(os.Stderr, "Error: app name is required\n")
204 fmt.Fprintf(os.Stderr, "Usage: deploy status <app-name> --host user@vps-ip\n")
205 os.Exit(1)
206 }
207
208 name := fs.Args()[0]
209
210 // Get host from flag or config
211 if *host == "" {
212 cfg, err := config.Load()
213 if err != nil {
214 fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
215 os.Exit(1)
216 }
217 *host = cfg.Host
218 }
219
220 if *host == "" {
221 fmt.Fprintf(os.Stderr, "Error: --host is required\n")
222 fs.Usage()
223 os.Exit(1)
224 }
225
226 // Load state to verify app exists
227 st, err := state.Load()
228 if err != nil {
229 fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
230 os.Exit(1)
231 }
232
233 app, err := st.GetApp(*host, name)
234 if err != nil {
235 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
236 os.Exit(1)
237 }
238
239 if app.Type != "app" {
240 fmt.Fprintf(os.Stderr, "Error: status is only available for apps, not static sites\n")
241 os.Exit(1)
242 }
243
244 // Connect to VPS
245 client, err := ssh.Connect(*host)
246 if err != nil {
247 fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err)
248 os.Exit(1)
249 }
250 defer client.Close()
251
252 // Get status
253 output, err := client.RunSudo(fmt.Sprintf("systemctl status %s", name))
254 if err != nil {
255 // systemctl status returns non-zero for non-active services
256 // but we still want to show the output
257 fmt.Print(output)
258 return
259 }
260
261 fmt.Print(output)
262}
263
264func runRestart(args []string) {
265 fs := flag.NewFlagSet("restart", flag.ExitOnError)
266 host := fs.String("host", "", "VPS host (SSH config alias or user@host)")
267 fs.Parse(args)
268
269 if len(fs.Args()) == 0 {
270 fmt.Fprintf(os.Stderr, "Error: app name is required\n")
271 fmt.Fprintf(os.Stderr, "Usage: deploy restart <app-name> --host user@vps-ip\n")
272 os.Exit(1)
273 }
274
275 name := fs.Args()[0]
276
277 // Get host from flag or config
278 if *host == "" {
279 cfg, err := config.Load()
280 if err != nil {
281 fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
282 os.Exit(1)
283 }
284 *host = cfg.Host
285 }
286
287 if *host == "" {
288 fmt.Fprintf(os.Stderr, "Error: --host is required\n")
289 fs.Usage()
290 os.Exit(1)
291 }
292
293 // Load state to verify app exists
294 st, err := state.Load()
295 if err != nil {
296 fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
297 os.Exit(1)
298 }
299
300 app, err := st.GetApp(*host, name)
301 if err != nil {
302 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
303 os.Exit(1)
304 }
305
306 if app.Type != "app" {
307 fmt.Fprintf(os.Stderr, "Error: restart is only available for apps, not static sites\n")
308 os.Exit(1)
309 }
310
311 // Connect to VPS
312 client, err := ssh.Connect(*host)
313 if err != nil {
314 fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err)
315 os.Exit(1)
316 }
317 defer client.Close()
318
319 // Restart service
320 fmt.Printf("Restarting %s...\n", name)
321 if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil {
322 fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err)
323 os.Exit(1)
324 }
325
326 fmt.Println("✓ Service restarted successfully")
327}