From 98b9af372025595e8a4255538e2836e019311474 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 20:54:46 -0800 Subject: 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. --- cmd/deploy/manage.go | 327 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 cmd/deploy/manage.go (limited to 'cmd/deploy/manage.go') 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 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/bdw/deploy/internal/config" + "github.com/bdw/deploy/internal/ssh" + "github.com/bdw/deploy/internal/state" +) + +func runRemove(args []string) { + fs := flag.NewFlagSet("remove", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + fs.Parse(args) + + if len(fs.Args()) == 0 { + fmt.Fprintf(os.Stderr, "Error: app name is required\n") + fmt.Fprintf(os.Stderr, "Usage: deploy remove --host user@vps-ip\n") + os.Exit(1) + } + + name := fs.Args()[0] + + // Get host from flag or config + if *host == "" { + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + *host = cfg.Host + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required\n") + fs.Usage() + os.Exit(1) + } + + // Load state + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + // Get app info + app, err := st.GetApp(*host, name) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Removing deployment: %s\n", name) + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + if app.Type == "app" { + // Stop and disable service + fmt.Println("→ Stopping service...") + client.RunSudo(fmt.Sprintf("systemctl stop %s", name)) + client.RunSudo(fmt.Sprintf("systemctl disable %s", name)) + + // Remove systemd unit + client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name)) + client.RunSudo("systemctl daemon-reload") + + // Remove binary + client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name)) + + // Remove working directory + client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name)) + + // Remove env file + client.RunSudo(fmt.Sprintf("rm -f /etc/deploy/env/%s.env", name)) + + // Remove user + client.RunSudo(fmt.Sprintf("userdel %s", name)) + } else { + // Remove static site files + fmt.Println("→ Removing files...") + client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name)) + } + + // Remove Caddy config + fmt.Println("→ Removing Caddy config...") + client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name)) + + // Reload Caddy + fmt.Println("→ Reloading Caddy...") + if _, err := client.RunSudo("systemctl reload caddy"); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Error reloading Caddy: %v\n", err) + } + + // Update state + if err := st.RemoveApp(*host, name); err != nil { + fmt.Fprintf(os.Stderr, "Error updating state: %v\n", err) + os.Exit(1) + } + if err := st.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err) + os.Exit(1) + } + + fmt.Printf("✓ Deployment removed successfully\n") +} + +func runLogs(args []string) { + fs := flag.NewFlagSet("logs", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + follow := fs.Bool("f", false, "Follow logs") + lines := fs.Int("n", 50, "Number of lines to show") + fs.Parse(args) + + if len(fs.Args()) == 0 { + fmt.Fprintf(os.Stderr, "Error: app name is required\n") + fmt.Fprintf(os.Stderr, "Usage: deploy logs --host user@vps-ip\n") + os.Exit(1) + } + + name := fs.Args()[0] + + // Get host from flag or config + if *host == "" { + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + *host = cfg.Host + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required\n") + fs.Usage() + os.Exit(1) + } + + // Load state to verify app exists + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + app, err := st.GetApp(*host, name) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if app.Type != "app" { + fmt.Fprintf(os.Stderr, "Error: logs are only available for apps, not static sites\n") + os.Exit(1) + } + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + // Build journalctl command + cmd := fmt.Sprintf("journalctl -u %s -n %d", name, *lines) + if *follow { + cmd += " -f" + } + + // Run command + if *follow { + // Stream output for follow mode (no sudo needed for journalctl) + if err := client.RunStream(cmd); err != nil { + fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err) + os.Exit(1) + } + } else { + // Buffer output for non-follow mode (no sudo needed for journalctl) + output, err := client.Run(cmd) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err) + os.Exit(1) + } + fmt.Print(output) + } +} + +func runStatus(args []string) { + fs := flag.NewFlagSet("status", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + fs.Parse(args) + + if len(fs.Args()) == 0 { + fmt.Fprintf(os.Stderr, "Error: app name is required\n") + fmt.Fprintf(os.Stderr, "Usage: deploy status --host user@vps-ip\n") + os.Exit(1) + } + + name := fs.Args()[0] + + // Get host from flag or config + if *host == "" { + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + *host = cfg.Host + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required\n") + fs.Usage() + os.Exit(1) + } + + // Load state to verify app exists + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + app, err := st.GetApp(*host, name) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if app.Type != "app" { + fmt.Fprintf(os.Stderr, "Error: status is only available for apps, not static sites\n") + os.Exit(1) + } + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + // Get status + output, err := client.RunSudo(fmt.Sprintf("systemctl status %s", name)) + if err != nil { + // systemctl status returns non-zero for non-active services + // but we still want to show the output + fmt.Print(output) + return + } + + fmt.Print(output) +} + +func runRestart(args []string) { + fs := flag.NewFlagSet("restart", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + fs.Parse(args) + + if len(fs.Args()) == 0 { + fmt.Fprintf(os.Stderr, "Error: app name is required\n") + fmt.Fprintf(os.Stderr, "Usage: deploy restart --host user@vps-ip\n") + os.Exit(1) + } + + name := fs.Args()[0] + + // Get host from flag or config + if *host == "" { + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + *host = cfg.Host + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required\n") + fs.Usage() + os.Exit(1) + } + + // Load state to verify app exists + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + app, err := st.GetApp(*host, name) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if app.Type != "app" { + fmt.Fprintf(os.Stderr, "Error: restart is only available for apps, not static sites\n") + os.Exit(1) + } + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + // Restart service + fmt.Printf("Restarting %s...\n", name) + if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { + fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err) + os.Exit(1) + } + + fmt.Println("✓ Service restarted successfully") +} -- cgit v1.2.3