summaryrefslogtreecommitdiffstats
path: root/cmd/ship/root.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-25 09:57:24 -0800
committerbndw <ben@bdw.to>2026-01-25 09:57:24 -0800
commit92189aed1a4789e13e275caec4492aac04b7a3a2 (patch)
tree65329be02ba1e99668ba7deaf51a634ac787fe89 /cmd/ship/root.go
parent1704c7d9dd88ebdae78217ea9b1a5941dc10f998 (diff)
Preserve existing config when updating apps and static sites
Previously, updating an app or static site without specifying all flags would clear existing configuration (e.g., custom domains would be lost if --domain wasn't provided on update). Now all config is merged in runDeploy before calling deploy functions: - Load state once instead of 2-3 times - Check for existing app once to determine create vs update - Build merged DeployOptions with existing config + CLI overrides - Deploy functions receive fully-merged config and just execute This ensures existing configuration is preserved unless explicitly overridden by CLI flags.
Diffstat (limited to 'cmd/ship/root.go')
-rw-r--r--cmd/ship/root.go395
1 files changed, 215 insertions, 180 deletions
diff --git a/cmd/ship/root.go b/cmd/ship/root.go
index c46491f..24eed1e 100644
--- a/cmd/ship/root.go
+++ b/cmd/ship/root.go
@@ -16,100 +16,223 @@ import (
16 16
17// DeployOptions contains all options for deploying or updating an app 17// DeployOptions contains all options for deploying or updating an app
18type DeployOptions struct { 18type DeployOptions struct {
19 Host string 19 Host string
20 Domain string 20 Domain string
21 Name string 21 Name string
22 Binary string 22 Binary string
23 Port int 23 Dir string // for static sites
24 EnvVars []string 24 Port int
25 EnvFile string 25 Args string
26 Args string 26 Files []string
27 Files []string 27 Memory string
28 Memory string 28 CPU string
29 CPU string 29 Env map[string]string // merged env vars
30 IsUpdate bool
30} 31}
31 32
32func runDeploy(cmd *cobra.Command, args []string) error { 33func runDeploy(cmd *cobra.Command, args []string) error {
33 flags := cmd.Flags() 34 flags := cmd.Flags()
34 35
35 opts := DeployOptions{} 36 // Parse CLI flags
36 opts.Binary, _ = flags.GetString("binary") 37 binary, _ := flags.GetString("binary")
37 static, _ := flags.GetBool("static") 38 static, _ := flags.GetBool("static")
38 dir, _ := flags.GetString("dir") 39 dir, _ := flags.GetString("dir")
39 opts.Domain, _ = flags.GetString("domain") 40 domain, _ := flags.GetString("domain")
40 opts.Name, _ = flags.GetString("name") 41 name, _ := flags.GetString("name")
41 opts.Port, _ = flags.GetInt("port") 42 port, _ := flags.GetInt("port")
42 opts.EnvVars, _ = flags.GetStringArray("env") 43 envVars, _ := flags.GetStringArray("env")
43 opts.EnvFile, _ = flags.GetString("env-file") 44 envFile, _ := flags.GetString("env-file")
44 opts.Args, _ = flags.GetString("args") 45 argsFlag, _ := flags.GetString("args")
45 opts.Files, _ = flags.GetStringArray("file") 46 files, _ := flags.GetStringArray("file")
46 opts.Memory, _ = flags.GetString("memory") 47 memory, _ := flags.GetString("memory")
47 opts.CPU, _ = flags.GetString("cpu") 48 cpu, _ := flags.GetString("cpu")
48 49
49 // Get host from flag or state default 50 // Get host from flag or state default
50 opts.Host = hostFlag 51 host := hostFlag
51 if opts.Host == "" { 52 if host == "" {
52 st, err := state.Load() 53 st, err := state.Load()
53 if err != nil { 54 if err != nil {
54 return fmt.Errorf("error loading state: %w", err) 55 return fmt.Errorf("error loading state: %w", err)
55 } 56 }
56 opts.Host = st.GetDefaultHost() 57 host = st.GetDefaultHost()
57 } 58 }
58 59
59 // If no flags provided, show help 60 // If no flags provided, show help
60 if opts.Domain == "" && opts.Binary == "" && !static && opts.Name == "" { 61 if domain == "" && binary == "" && !static && name == "" {
61 return cmd.Help() 62 return cmd.Help()
62 } 63 }
63 64
64 if opts.Host == "" { 65 if host == "" {
65 return fmt.Errorf("--host is required") 66 return fmt.Errorf("--host is required")
66 } 67 }
67 68
68 // Load state to check base domain 69 // Load state once - this will be used throughout
69 st, err := state.Load() 70 st, err := state.Load()
70 if err != nil { 71 if err != nil {
71 return fmt.Errorf("error loading state: %w", err) 72 return fmt.Errorf("error loading state: %w", err)
72 } 73 }
73 hostState := st.GetHost(opts.Host) 74 hostState := st.GetHost(host)
74 75
75 // Config update mode: --name provided without --binary or --static 76 // Config update mode: --name provided without --binary or --static
76 if opts.Name != "" && opts.Binary == "" && !static { 77 if name != "" && binary == "" && !static {
77 return updateAppConfig(opts) 78 existingApp, err := st.GetApp(host, name)
78 } 79 if err != nil {
80 return fmt.Errorf("app %s not found (use --binary to deploy a new app)", name)
81 }
79 82
80 if opts.Domain == "" && hostState.BaseDomain == "" { 83 // Build merged config starting from existing app
81 return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") 84 opts := DeployOptions{
85 Host: host,
86 Name: name,
87 Port: existingApp.Port,
88 Args: existingApp.Args,
89 Files: existingApp.Files,
90 Memory: existingApp.Memory,
91 CPU: existingApp.CPU,
92 Env: make(map[string]string),
93 }
94 for k, v := range existingApp.Env {
95 opts.Env[k] = v
96 }
97
98 // Override with CLI flags if provided
99 if argsFlag != "" {
100 opts.Args = argsFlag
101 }
102 if len(files) > 0 {
103 opts.Files = files
104 }
105 if memory != "" {
106 opts.Memory = memory
107 }
108 if cpu != "" {
109 opts.CPU = cpu
110 }
111
112 // Merge env vars (CLI overrides existing)
113 for _, e := range envVars {
114 parts := strings.SplitN(e, "=", 2)
115 if len(parts) == 2 {
116 opts.Env[parts[0]] = parts[1]
117 }
118 }
119 if envFile != "" {
120 fileEnv, err := parseEnvFile(envFile)
121 if err != nil {
122 return fmt.Errorf("error reading env file: %w", err)
123 }
124 for k, v := range fileEnv {
125 opts.Env[k] = v
126 }
127 }
128
129 return updateAppConfig(st, opts)
82 } 130 }
83 131
84 // Infer name early so we can use it for subdomain generation 132 // Infer name early so we can use it for subdomain generation and existing app lookup
85 if opts.Name == "" { 133 if name == "" {
86 if static { 134 if static {
87 opts.Name = opts.Domain 135 name = domain
88 if opts.Name == "" && hostState.BaseDomain != "" { 136 if name == "" && hostState.BaseDomain != "" {
89 opts.Name = filepath.Base(dir) 137 name = filepath.Base(dir)
90 } 138 }
91 } else { 139 } else {
92 opts.Name = filepath.Base(opts.Binary) 140 name = filepath.Base(binary)
93 } 141 }
94 } 142 }
95 143
96 // Generate subdomain if base domain configured 144 // Check if this is an update to an existing app/site
145 existingApp, _ := st.GetApp(host, name)
146 isUpdate := existingApp != nil
147
148 // For new deployments, require domain or base domain
149 if !isUpdate && domain == "" && hostState.BaseDomain == "" {
150 return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')")
151 }
152
153 // Build merged config, starting from existing app if updating
154 opts := DeployOptions{
155 Host: host,
156 Name: name,
157 Binary: binary,
158 Dir: dir,
159 IsUpdate: isUpdate,
160 }
161
162 // Merge domain: auto-subdomain + (user-provided or existing custom domain)
97 var domains []string 163 var domains []string
98 if hostState.BaseDomain != "" { 164 if hostState.BaseDomain != "" {
99 domains = append(domains, opts.Name+"."+hostState.BaseDomain) 165 domains = append(domains, name+"."+hostState.BaseDomain)
100 } 166 }
101 if opts.Domain != "" { 167 if domain != "" {
102 domains = append(domains, opts.Domain) 168 domains = append(domains, domain)
169 } else if isUpdate && existingApp.Domain != "" {
170 for _, d := range strings.Split(existingApp.Domain, ",") {
171 d = strings.TrimSpace(d)
172 if d != "" && (hostState.BaseDomain == "" || !strings.HasSuffix(d, "."+hostState.BaseDomain)) {
173 domains = append(domains, d)
174 }
175 }
103 } 176 }
104 opts.Domain = strings.Join(domains, ", ") 177 opts.Domain = strings.Join(domains, ", ")
105 178
179 // For apps, merge all config fields
180 if !static {
181 // Start with existing values if updating
182 if isUpdate {
183 opts.Port = existingApp.Port
184 opts.Args = existingApp.Args
185 opts.Files = existingApp.Files
186 opts.Memory = existingApp.Memory
187 opts.CPU = existingApp.CPU
188 opts.Env = make(map[string]string)
189 for k, v := range existingApp.Env {
190 opts.Env[k] = v
191 }
192 } else {
193 opts.Port = port
194 opts.Env = make(map[string]string)
195 }
196
197 // Override with CLI flags if provided
198 if argsFlag != "" {
199 opts.Args = argsFlag
200 }
201 if len(files) > 0 {
202 opts.Files = files
203 }
204 if memory != "" {
205 opts.Memory = memory
206 }
207 if cpu != "" {
208 opts.CPU = cpu
209 }
210
211 // Merge env vars (CLI overrides existing)
212 for _, e := range envVars {
213 parts := strings.SplitN(e, "=", 2)
214 if len(parts) == 2 {
215 opts.Env[parts[0]] = parts[1]
216 }
217 }
218 if envFile != "" {
219 fileEnv, err := parseEnvFile(envFile)
220 if err != nil {
221 return fmt.Errorf("error reading env file: %w", err)
222 }
223 for k, v := range fileEnv {
224 opts.Env[k] = v
225 }
226 }
227 }
228
106 if static { 229 if static {
107 return deployStatic(opts.Host, opts.Domain, opts.Name, dir) 230 return deployStatic(st, opts)
108 } 231 }
109 return deployApp(opts) 232 return deployApp(st, opts)
110} 233}
111 234
112func deployApp(opts DeployOptions) error { 235func deployApp(st *state.State, opts DeployOptions) error {
113 if opts.Binary == "" { 236 if opts.Binary == "" {
114 return fmt.Errorf("--binary is required") 237 return fmt.Errorf("--binary is required")
115 } 238 }
@@ -122,67 +245,19 @@ func deployApp(opts DeployOptions) error {
122 fmt.Printf(" Domain(s): %s\n", opts.Domain) 245 fmt.Printf(" Domain(s): %s\n", opts.Domain)
123 fmt.Printf(" Binary: %s\n", opts.Binary) 246 fmt.Printf(" Binary: %s\n", opts.Binary)
124 247
125 st, err := state.Load() 248 // Allocate port for new apps
126 if err != nil { 249 port := opts.Port
127 return fmt.Errorf("error loading state: %w", err) 250 if opts.IsUpdate {
128 }
129
130 existingApp, _ := st.GetApp(opts.Host, opts.Name)
131 var port int
132 if existingApp != nil {
133 port = existingApp.Port
134 fmt.Printf(" Updating existing deployment (port %d)\n", port) 251 fmt.Printf(" Updating existing deployment (port %d)\n", port)
135 } else { 252 } else {
136 if opts.Port > 0 { 253 if port == 0 {
137 port = opts.Port
138 } else {
139 port = st.AllocatePort(opts.Host) 254 port = st.AllocatePort(opts.Host)
140 } 255 }
141 fmt.Printf(" Allocated port: %d\n", port) 256 fmt.Printf(" Allocated port: %d\n", port)
142 } 257 }
143 258
144 // Merge with existing config 259 // Add PORT to env
145 args := opts.Args 260 opts.Env["PORT"] = strconv.Itoa(port)
146 files := opts.Files
147 memory := opts.Memory
148 cpu := opts.CPU
149 env := make(map[string]string)
150 if existingApp != nil {
151 for k, v := range existingApp.Env {
152 env[k] = v
153 }
154 if args == "" && existingApp.Args != "" {
155 args = existingApp.Args
156 }
157 if len(files) == 0 && len(existingApp.Files) > 0 {
158 files = existingApp.Files
159 }
160 if memory == "" && existingApp.Memory != "" {
161 memory = existingApp.Memory
162 }
163 if cpu == "" && existingApp.CPU != "" {
164 cpu = existingApp.CPU
165 }
166 }
167
168 for _, e := range opts.EnvVars {
169 parts := strings.SplitN(e, "=", 2)
170 if len(parts) == 2 {
171 env[parts[0]] = parts[1]
172 }
173 }
174
175 if opts.EnvFile != "" {
176 fileEnv, err := parseEnvFile(opts.EnvFile)
177 if err != nil {
178 return fmt.Errorf("error reading env file: %w", err)
179 }
180 for k, v := range fileEnv {
181 env[k] = v
182 }
183 }
184
185 env["PORT"] = strconv.Itoa(port)
186 261
187 client, err := ssh.Connect(opts.Host) 262 client, err := ssh.Connect(opts.Host)
188 if err != nil { 263 if err != nil {
@@ -217,9 +292,9 @@ func deployApp(opts DeployOptions) error {
217 return fmt.Errorf("error making binary executable: %w", err) 292 return fmt.Errorf("error making binary executable: %w", err)
218 } 293 }
219 294
220 if len(files) > 0 { 295 if len(opts.Files) > 0 {
221 fmt.Println("-> Uploading config files...") 296 fmt.Println("-> Uploading config files...")
222 for _, file := range files { 297 for _, file := range opts.Files {
223 if _, err := os.Stat(file); err != nil { 298 if _, err := os.Stat(file); err != nil {
224 return fmt.Errorf("config file not found: %s", file) 299 return fmt.Errorf("config file not found: %s", file)
225 } 300 }
@@ -246,7 +321,7 @@ func deployApp(opts DeployOptions) error {
246 fmt.Println("-> Creating environment file...") 321 fmt.Println("-> Creating environment file...")
247 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) 322 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name)
248 envContent := "" 323 envContent := ""
249 for k, v := range env { 324 for k, v := range opts.Env {
250 envContent += fmt.Sprintf("%s=%s\n", k, v) 325 envContent += fmt.Sprintf("%s=%s\n", k, v)
251 } 326 }
252 if err := client.WriteSudoFile(envFilePath, envContent); err != nil { 327 if err := client.WriteSudoFile(envFilePath, envContent); err != nil {
@@ -267,9 +342,9 @@ func deployApp(opts DeployOptions) error {
267 "BinaryPath": binaryDest, 342 "BinaryPath": binaryDest,
268 "Port": strconv.Itoa(port), 343 "Port": strconv.Itoa(port),
269 "EnvFile": envFilePath, 344 "EnvFile": envFilePath,
270 "Args": args, 345 "Args": opts.Args,
271 "Memory": memory, 346 "Memory": opts.Memory,
272 "CPU": cpu, 347 "CPU": opts.CPU,
273 }) 348 })
274 if err != nil { 349 if err != nil {
275 return fmt.Errorf("error generating systemd unit: %w", err) 350 return fmt.Errorf("error generating systemd unit: %w", err)
@@ -316,11 +391,11 @@ func deployApp(opts DeployOptions) error {
316 Type: "app", 391 Type: "app",
317 Domain: opts.Domain, 392 Domain: opts.Domain,
318 Port: port, 393 Port: port,
319 Env: env, 394 Env: opts.Env,
320 Args: args, 395 Args: opts.Args,
321 Files: files, 396 Files: opts.Files,
322 Memory: memory, 397 Memory: opts.Memory,
323 CPU: cpu, 398 CPU: opts.CPU,
324 }) 399 })
325 if err := st.Save(); err != nil { 400 if err := st.Save(); err != nil {
326 return fmt.Errorf("error saving state: %w", err) 401 return fmt.Errorf("error saving state: %w", err)
@@ -334,12 +409,7 @@ func deployApp(opts DeployOptions) error {
334 return nil 409 return nil
335} 410}
336 411
337func updateAppConfig(opts DeployOptions) error { 412func updateAppConfig(st *state.State, opts DeployOptions) error {
338 st, err := state.Load()
339 if err != nil {
340 return fmt.Errorf("error loading state: %w", err)
341 }
342
343 existingApp, err := st.GetApp(opts.Host, opts.Name) 413 existingApp, err := st.GetApp(opts.Host, opts.Name)
344 if err != nil { 414 if err != nil {
345 return fmt.Errorf("app %s not found (use --binary to deploy a new app)", opts.Name) 415 return fmt.Errorf("app %s not found (use --binary to deploy a new app)", opts.Name)
@@ -351,41 +421,8 @@ func updateAppConfig(opts DeployOptions) error {
351 421
352 fmt.Printf("Updating config: %s\n", opts.Name) 422 fmt.Printf("Updating config: %s\n", opts.Name)
353 423
354 // Merge with existing values 424 // Add PORT to env
355 args := opts.Args 425 opts.Env["PORT"] = strconv.Itoa(existingApp.Port)
356 if args == "" {
357 args = existingApp.Args
358 }
359 memory := opts.Memory
360 if memory == "" {
361 memory = existingApp.Memory
362 }
363 cpu := opts.CPU
364 if cpu == "" {
365 cpu = existingApp.CPU
366 }
367
368 // Merge env vars
369 env := make(map[string]string)
370 for k, v := range existingApp.Env {
371 env[k] = v
372 }
373 for _, e := range opts.EnvVars {
374 parts := strings.SplitN(e, "=", 2)
375 if len(parts) == 2 {
376 env[parts[0]] = parts[1]
377 }
378 }
379 if opts.EnvFile != "" {
380 fileEnv, err := parseEnvFile(opts.EnvFile)
381 if err != nil {
382 return fmt.Errorf("error reading env file: %w", err)
383 }
384 for k, v := range fileEnv {
385 env[k] = v
386 }
387 }
388 env["PORT"] = strconv.Itoa(existingApp.Port)
389 426
390 client, err := ssh.Connect(opts.Host) 427 client, err := ssh.Connect(opts.Host)
391 if err != nil { 428 if err != nil {
@@ -397,7 +434,7 @@ func updateAppConfig(opts DeployOptions) error {
397 fmt.Println("-> Updating environment file...") 434 fmt.Println("-> Updating environment file...")
398 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) 435 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name)
399 envContent := "" 436 envContent := ""
400 for k, v := range env { 437 for k, v := range opts.Env {
401 envContent += fmt.Sprintf("%s=%s\n", k, v) 438 envContent += fmt.Sprintf("%s=%s\n", k, v)
402 } 439 }
403 if err := client.WriteSudoFile(envFilePath, envContent); err != nil { 440 if err := client.WriteSudoFile(envFilePath, envContent); err != nil {
@@ -415,9 +452,9 @@ func updateAppConfig(opts DeployOptions) error {
415 "BinaryPath": binaryDest, 452 "BinaryPath": binaryDest,
416 "Port": strconv.Itoa(existingApp.Port), 453 "Port": strconv.Itoa(existingApp.Port),
417 "EnvFile": envFilePath, 454 "EnvFile": envFilePath,
418 "Args": args, 455 "Args": opts.Args,
419 "Memory": memory, 456 "Memory": opts.Memory,
420 "CPU": cpu, 457 "CPU": opts.CPU,
421 }) 458 })
422 if err != nil { 459 if err != nil {
423 return fmt.Errorf("error generating systemd unit: %w", err) 460 return fmt.Errorf("error generating systemd unit: %w", err)
@@ -439,10 +476,10 @@ func updateAppConfig(opts DeployOptions) error {
439 } 476 }
440 477
441 // Update state 478 // Update state
442 existingApp.Args = args 479 existingApp.Args = opts.Args
443 existingApp.Memory = memory 480 existingApp.Memory = opts.Memory
444 existingApp.CPU = cpu 481 existingApp.CPU = opts.CPU
445 existingApp.Env = env 482 existingApp.Env = opts.Env
446 if err := st.Save(); err != nil { 483 if err := st.Save(); err != nil {
447 return fmt.Errorf("error saving state: %w", err) 484 return fmt.Errorf("error saving state: %w", err)
448 } 485 }
@@ -451,27 +488,26 @@ func updateAppConfig(opts DeployOptions) error {
451 return nil 488 return nil
452} 489}
453 490
454func deployStatic(host, domain, name, dir string) error { 491func deployStatic(st *state.State, opts DeployOptions) error {
455 if _, err := os.Stat(dir); err != nil { 492 if _, err := os.Stat(opts.Dir); err != nil {
456 return fmt.Errorf("directory not found: %s", dir) 493 return fmt.Errorf("directory not found: %s", opts.Dir)
457 } 494 }
458 495
459 fmt.Printf("Deploying static site: %s\n", name) 496 fmt.Printf("Deploying static site: %s\n", opts.Name)
460 fmt.Printf(" Domain(s): %s\n", domain) 497 fmt.Printf(" Domain(s): %s\n", opts.Domain)
461 fmt.Printf(" Directory: %s\n", dir) 498 fmt.Printf(" Directory: %s\n", opts.Dir)
462 499
463 st, err := state.Load() 500 if opts.IsUpdate {
464 if err != nil { 501 fmt.Println(" Updating existing deployment")
465 return fmt.Errorf("error loading state: %w", err)
466 } 502 }
467 503
468 client, err := ssh.Connect(host) 504 client, err := ssh.Connect(opts.Host)
469 if err != nil { 505 if err != nil {
470 return fmt.Errorf("error connecting to VPS: %w", err) 506 return fmt.Errorf("error connecting to VPS: %w", err)
471 } 507 }
472 defer client.Close() 508 defer client.Close()
473 509
474 remoteDir := fmt.Sprintf("/var/www/%s", name) 510 remoteDir := fmt.Sprintf("/var/www/%s", opts.Name)
475 fmt.Println("-> Creating remote directory...") 511 fmt.Println("-> Creating remote directory...")
476 if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { 512 if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil {
477 return fmt.Errorf("error creating remote directory: %w", err) 513 return fmt.Errorf("error creating remote directory: %w", err)
@@ -488,7 +524,7 @@ func deployStatic(host, domain, name, dir string) error {
488 } 524 }
489 525
490 fmt.Println("-> Uploading files...") 526 fmt.Println("-> Uploading files...")
491 if err := client.UploadDir(dir, remoteDir); err != nil { 527 if err := client.UploadDir(opts.Dir, remoteDir); err != nil {
492 return fmt.Errorf("error uploading files: %w", err) 528 return fmt.Errorf("error uploading files: %w", err)
493 } 529 }
494 530
@@ -505,14 +541,14 @@ func deployStatic(host, domain, name, dir string) error {
505 541
506 fmt.Println("-> Configuring Caddy...") 542 fmt.Println("-> Configuring Caddy...")
507 caddyContent, err := templates.StaticCaddy(map[string]string{ 543 caddyContent, err := templates.StaticCaddy(map[string]string{
508 "Domain": domain, 544 "Domain": opts.Domain,
509 "RootDir": remoteDir, 545 "RootDir": remoteDir,
510 }) 546 })
511 if err != nil { 547 if err != nil {
512 return fmt.Errorf("error generating Caddy config: %w", err) 548 return fmt.Errorf("error generating Caddy config: %w", err)
513 } 549 }
514 550
515 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) 551 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name)
516 if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { 552 if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil {
517 return fmt.Errorf("error creating Caddy config: %w", err) 553 return fmt.Errorf("error creating Caddy config: %w", err)
518 } 554 }
@@ -522,17 +558,16 @@ func deployStatic(host, domain, name, dir string) error {
522 return fmt.Errorf("error reloading Caddy: %w", err) 558 return fmt.Errorf("error reloading Caddy: %w", err)
523 } 559 }
524 560
525 st.AddApp(host, name, &state.App{ 561 st.AddApp(opts.Host, opts.Name, &state.App{
526 Type: "static", 562 Type: "static",
527 Domain: domain, 563 Domain: opts.Domain,
528 }) 564 })
529 if err := st.Save(); err != nil { 565 if err := st.Save(); err != nil {
530 return fmt.Errorf("error saving state: %w", err) 566 return fmt.Errorf("error saving state: %w", err)
531 } 567 }
532 568
533 fmt.Printf("\n Static site deployed successfully!\n") 569 fmt.Printf("\n Static site deployed successfully!\n")
534 // Show first domain in the URL message 570 primaryDomain := strings.Split(opts.Domain, ",")[0]
535 primaryDomain := strings.Split(domain, ",")[0]
536 primaryDomain = strings.TrimSpace(primaryDomain) 571 primaryDomain = strings.TrimSpace(primaryDomain)
537 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) 572 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain)
538 return nil 573 return nil