From 87752492d0dc7df3cf78011d5ce315a3eb0cad51 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 21:52:50 -0800 Subject: Restructure CLI with Cobra Replace custom switch-based routing with Cobra for cleaner command hierarchy. Reorganize commands into logical groups: - Root command handles deployment (--binary, --static, --domain, etc.) - App management at top level: list, logs, status, restart, remove - env subcommand group: list, set, unset - host subcommand group: init, status, update, ssh - Standalone: ui (renamed from webui), version Add version command with ldflags support for build info. --- cmd/deploy/env/env.go | 17 +++++++ cmd/deploy/env/list.go | 69 +++++++++++++++++++++++++ cmd/deploy/env/set.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/deploy/env/unset.go | 92 +++++++++++++++++++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 cmd/deploy/env/env.go create mode 100644 cmd/deploy/env/list.go create mode 100644 cmd/deploy/env/set.go create mode 100644 cmd/deploy/env/unset.go (limited to 'cmd/deploy/env') diff --git a/cmd/deploy/env/env.go b/cmd/deploy/env/env.go new file mode 100644 index 0000000..489353a --- /dev/null +++ b/cmd/deploy/env/env.go @@ -0,0 +1,17 @@ +package env + +import ( + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "env", + Short: "Manage environment variables", + Long: "Manage environment variables for deployed applications", +} + +func init() { + Cmd.AddCommand(listCmd) + Cmd.AddCommand(setCmd) + Cmd.AddCommand(unsetCmd) +} diff --git a/cmd/deploy/env/list.go b/cmd/deploy/env/list.go new file mode 100644 index 0000000..af92171 --- /dev/null +++ b/cmd/deploy/env/list.go @@ -0,0 +1,69 @@ +package env + +import ( + "fmt" + "strings" + + "github.com/bdw/deploy/internal/state" + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list ", + Short: "List environment variables for an app", + Args: cobra.ExactArgs(1), + RunE: runList, +} + +func runList(cmd *cobra.Command, args []string) error { + name := args[0] + + st, err := state.Load() + if err != nil { + return fmt.Errorf("error loading state: %w", err) + } + + host, _ := cmd.Flags().GetString("host") + if host == "" { + host = st.GetDefaultHost() + } + + if host == "" { + return fmt.Errorf("--host is required") + } + + app, err := st.GetApp(host, name) + if err != nil { + return err + } + + if app.Type != "app" { + return fmt.Errorf("env is only available for apps, not static sites") + } + + fmt.Printf("Environment variables for %s:\n\n", name) + if len(app.Env) == 0 { + fmt.Println(" (none)") + } else { + for k, v := range app.Env { + display := v + if isSensitive(k) { + display = "***" + } + fmt.Printf(" %s=%s\n", k, display) + } + } + + return nil +} + +func isSensitive(key string) bool { + key = strings.ToLower(key) + sensitiveWords := []string{"key", "secret", "password", "token", "api"} + for _, word := range sensitiveWords { + if strings.Contains(key, word) { + return true + } + } + return false +} diff --git a/cmd/deploy/env/set.go b/cmd/deploy/env/set.go new file mode 100644 index 0000000..35d77ff --- /dev/null +++ b/cmd/deploy/env/set.go @@ -0,0 +1,132 @@ +package env + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/bdw/deploy/internal/ssh" + "github.com/bdw/deploy/internal/state" + "github.com/spf13/cobra" +) + +var setCmd = &cobra.Command{ + Use: "set KEY=VALUE...", + Short: "Set environment variable(s)", + Long: "Set one or more environment variables for an app. Variables are specified as KEY=VALUE pairs.", + Args: cobra.MinimumNArgs(2), + RunE: runSet, +} + +func init() { + setCmd.Flags().StringP("file", "f", "", "Load environment from file") +} + +func runSet(cmd *cobra.Command, args []string) error { + name := args[0] + envVars := args[1:] + + st, err := state.Load() + if err != nil { + return fmt.Errorf("error loading state: %w", err) + } + + host, _ := cmd.Flags().GetString("host") + if host == "" { + host = st.GetDefaultHost() + } + + if host == "" { + return fmt.Errorf("--host is required") + } + + app, err := st.GetApp(host, name) + if err != nil { + return err + } + + if app.Type != "app" { + return fmt.Errorf("env is only available for apps, not static sites") + } + + if app.Env == nil { + app.Env = make(map[string]string) + } + + // Set variables from args + for _, e := range envVars { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + app.Env[parts[0]] = parts[1] + fmt.Printf("Set %s\n", parts[0]) + } else { + return fmt.Errorf("invalid format: %s (expected KEY=VALUE)", e) + } + } + + // Set variables from file if provided + envFile, _ := cmd.Flags().GetString("file") + if envFile != "" { + fileEnv, err := parseEnvFile(envFile) + if err != nil { + return fmt.Errorf("error reading env file: %w", err) + } + for k, v := range fileEnv { + app.Env[k] = v + fmt.Printf("Set %s\n", k) + } + } + + if err := st.Save(); err != nil { + return fmt.Errorf("error saving state: %w", err) + } + + client, err := ssh.Connect(host) + if err != nil { + return fmt.Errorf("error connecting to VPS: %w", err) + } + defer client.Close() + + fmt.Println("-> Updating environment file on VPS...") + envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) + envContent := "" + for k, v := range app.Env { + envContent += fmt.Sprintf("%s=%s\n", k, v) + } + if err := client.WriteSudoFile(envFilePath, envContent); err != nil { + return fmt.Errorf("error updating env file: %w", err) + } + + fmt.Println("-> Restarting service...") + if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { + return fmt.Errorf("error restarting service: %w", err) + } + + fmt.Println("Environment variables updated successfully") + return nil +} + +func parseEnvFile(path string) (map[string]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + env := make(map[string]string) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + env[parts[0]] = parts[1] + } + } + + return env, scanner.Err() +} diff --git a/cmd/deploy/env/unset.go b/cmd/deploy/env/unset.go new file mode 100644 index 0000000..65a8986 --- /dev/null +++ b/cmd/deploy/env/unset.go @@ -0,0 +1,92 @@ +package env + +import ( + "fmt" + + "github.com/bdw/deploy/internal/ssh" + "github.com/bdw/deploy/internal/state" + "github.com/spf13/cobra" +) + +var unsetCmd = &cobra.Command{ + Use: "unset KEY...", + Short: "Unset environment variable(s)", + Long: "Remove one or more environment variables from an app.", + Args: cobra.MinimumNArgs(2), + RunE: runUnset, +} + +func runUnset(cmd *cobra.Command, args []string) error { + name := args[0] + keys := args[1:] + + st, err := state.Load() + if err != nil { + return fmt.Errorf("error loading state: %w", err) + } + + host, _ := cmd.Flags().GetString("host") + if host == "" { + host = st.GetDefaultHost() + } + + if host == "" { + return fmt.Errorf("--host is required") + } + + app, err := st.GetApp(host, name) + if err != nil { + return err + } + + if app.Type != "app" { + return fmt.Errorf("env is only available for apps, not static sites") + } + + if app.Env == nil { + return fmt.Errorf("no environment variables set") + } + + changed := false + for _, key := range keys { + if _, exists := app.Env[key]; exists { + delete(app.Env, key) + changed = true + fmt.Printf("Unset %s\n", key) + } else { + fmt.Printf("Warning: %s not found\n", key) + } + } + + if !changed { + return nil + } + + if err := st.Save(); err != nil { + return fmt.Errorf("error saving state: %w", err) + } + + client, err := ssh.Connect(host) + if err != nil { + return fmt.Errorf("error connecting to VPS: %w", err) + } + defer client.Close() + + fmt.Println("-> Updating environment file on VPS...") + envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) + envContent := "" + for k, v := range app.Env { + envContent += fmt.Sprintf("%s=%s\n", k, v) + } + if err := client.WriteSudoFile(envFilePath, envContent); err != nil { + return fmt.Errorf("error updating env file: %w", err) + } + + fmt.Println("-> Restarting service...") + if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { + return fmt.Errorf("error restarting service: %w", err) + } + + fmt.Println("Environment variables updated successfully") + return nil +} -- cgit v1.2.3