|
| 1 | +package command |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "os/exec" |
| 7 | + "runtime" |
| 8 | + |
| 9 | + "github.com/owenthereal/upterm/utils" |
| 10 | + "github.com/spf13/cobra" |
| 11 | + "github.com/spf13/viper" |
| 12 | +) |
| 13 | + |
| 14 | +func configCmd() *cobra.Command { |
| 15 | + configPath := utils.UptermConfigFilePath() |
| 16 | + cmd := &cobra.Command{ |
| 17 | + Use: "config", |
| 18 | + Short: "Manage upterm configuration", |
| 19 | + Long: fmt.Sprintf(`Manage upterm configuration file. |
| 20 | +
|
| 21 | +Config file: %s |
| 22 | +
|
| 23 | +This follows the XDG Base Directory Specification. |
| 24 | +
|
| 25 | +Configuration priority (highest to lowest): |
| 26 | + 1. Command-line flags |
| 27 | + 2. Environment variables (UPTERM_ prefix) |
| 28 | + 3. Config file |
| 29 | + 4. Default values`, configPath), |
| 30 | + } |
| 31 | + |
| 32 | + cmd.AddCommand(configPathCmd()) |
| 33 | + cmd.AddCommand(configViewCmd()) |
| 34 | + cmd.AddCommand(configEditCmd()) |
| 35 | + |
| 36 | + return cmd |
| 37 | +} |
| 38 | + |
| 39 | +func configPathCmd() *cobra.Command { |
| 40 | + configPath := utils.UptermConfigFilePath() |
| 41 | + cmd := &cobra.Command{ |
| 42 | + Use: "path", |
| 43 | + Short: "Show the path to the config file", |
| 44 | + Long: fmt.Sprintf(`Show the path to the config file. |
| 45 | +
|
| 46 | +Config file: %s |
| 47 | +
|
| 48 | +The config file is optional and created manually by users.`, configPath), |
| 49 | + Example: ` # Show config file path: |
| 50 | + upterm config path |
| 51 | +
|
| 52 | + # Create config file directory: |
| 53 | + mkdir -p "$(dirname "$(upterm config path)")"`, |
| 54 | + RunE: configPathRunE, |
| 55 | + } |
| 56 | + |
| 57 | + return cmd |
| 58 | +} |
| 59 | + |
| 60 | +func configViewCmd() *cobra.Command { |
| 61 | + configPath := utils.UptermConfigFilePath() |
| 62 | + cmd := &cobra.Command{ |
| 63 | + Use: "view", |
| 64 | + Short: "View the config file contents", |
| 65 | + Long: fmt.Sprintf(`View the config file contents. |
| 66 | +
|
| 67 | +Config file: %s |
| 68 | +
|
| 69 | +If the config file exists, this command displays its contents. If it doesn't |
| 70 | +exist, this command shows an example config file that you can use as a template.`, configPath), |
| 71 | + Example: ` # View current config: |
| 72 | + upterm config view |
| 73 | +
|
| 74 | + # View and save as new config: |
| 75 | + upterm config view > "$(upterm config path)"`, |
| 76 | + RunE: configViewRunE, |
| 77 | + } |
| 78 | + |
| 79 | + return cmd |
| 80 | +} |
| 81 | + |
| 82 | +func configEditCmd() *cobra.Command { |
| 83 | + configPath := utils.UptermConfigFilePath() |
| 84 | + cmd := &cobra.Command{ |
| 85 | + Use: "edit", |
| 86 | + Short: "Edit the config file", |
| 87 | + Long: fmt.Sprintf(`Edit the config file in your default editor. |
| 88 | +
|
| 89 | +Config file: %s |
| 90 | +
|
| 91 | +This command opens the config file in your editor (determined by $VISUAL, $EDITOR, |
| 92 | +or a sensible default). If the config file doesn't exist, it creates a template |
| 93 | +with example settings and comments. |
| 94 | +
|
| 95 | +The config directory is created automatically if it doesn't exist.`, configPath), |
| 96 | + Example: ` # Edit config file: |
| 97 | + upterm config edit |
| 98 | +
|
| 99 | + # Use a specific editor: |
| 100 | + EDITOR=nano upterm config edit`, |
| 101 | + RunE: configEditRunE, |
| 102 | + } |
| 103 | + |
| 104 | + return cmd |
| 105 | +} |
| 106 | + |
| 107 | +func configPathRunE(c *cobra.Command, args []string) error { |
| 108 | + configPath := utils.UptermConfigFilePath() |
| 109 | + fmt.Println(configPath) |
| 110 | + return nil |
| 111 | +} |
| 112 | + |
| 113 | +func configViewRunE(c *cobra.Command, args []string) error { |
| 114 | + configPath := utils.UptermConfigFilePath() |
| 115 | + |
| 116 | + // Check if file exists |
| 117 | + if _, err := os.Stat(configPath); os.IsNotExist(err) { |
| 118 | + // Show example config |
| 119 | + fmt.Println("# Config file does not exist. Example config:") |
| 120 | + fmt.Println() |
| 121 | + fmt.Print(exampleConfig()) |
| 122 | + return nil |
| 123 | + } |
| 124 | + |
| 125 | + // Read and display file |
| 126 | + content, err := os.ReadFile(configPath) |
| 127 | + if err != nil { |
| 128 | + return fmt.Errorf("failed to read config file: %w", err) |
| 129 | + } |
| 130 | + |
| 131 | + fmt.Print(string(content)) |
| 132 | + return nil |
| 133 | +} |
| 134 | + |
| 135 | +func configEditRunE(c *cobra.Command, args []string) error { |
| 136 | + configPath := utils.UptermConfigFilePath() |
| 137 | + configDir := utils.UptermConfigDir() |
| 138 | + |
| 139 | + // Create config directory if it doesn't exist |
| 140 | + if err := os.MkdirAll(configDir, 0755); err != nil { |
| 141 | + return fmt.Errorf("failed to create config directory: %w", err) |
| 142 | + } |
| 143 | + |
| 144 | + // Create example config if file doesn't exist |
| 145 | + if _, err := os.Stat(configPath); os.IsNotExist(err) { |
| 146 | + if err := os.WriteFile(configPath, []byte(exampleConfig()), 0600); err != nil { |
| 147 | + return fmt.Errorf("failed to create config file: %w", err) |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + // Determine editor to use |
| 152 | + editor := getEditor() |
| 153 | + |
| 154 | + // Open editor |
| 155 | + cmd := exec.Command(editor, configPath) |
| 156 | + cmd.Stdin = os.Stdin |
| 157 | + cmd.Stdout = os.Stdout |
| 158 | + cmd.Stderr = os.Stderr |
| 159 | + |
| 160 | + if err := cmd.Run(); err != nil { |
| 161 | + return fmt.Errorf("failed to open editor: %w", err) |
| 162 | + } |
| 163 | + |
| 164 | + // Validate config after editing |
| 165 | + if err := validateConfig(configPath); err != nil { |
| 166 | + fmt.Fprintf(os.Stderr, "Warning: config file has syntax errors: %v\n", err) |
| 167 | + fmt.Fprintf(os.Stderr, "Edit again with 'upterm config edit' or view with 'upterm config view'.\n") |
| 168 | + } |
| 169 | + |
| 170 | + return nil |
| 171 | +} |
| 172 | + |
| 173 | +// getEditor returns the editor to use, checking $VISUAL, $EDITOR, then defaults. |
| 174 | +func getEditor() string { |
| 175 | + // Check $VISUAL first (for full-screen editors) |
| 176 | + if editor := os.Getenv("VISUAL"); editor != "" { |
| 177 | + return editor |
| 178 | + } |
| 179 | + |
| 180 | + // Check $EDITOR (for line editors) |
| 181 | + if editor := os.Getenv("EDITOR"); editor != "" { |
| 182 | + return editor |
| 183 | + } |
| 184 | + |
| 185 | + // Platform-specific defaults |
| 186 | + switch runtime.GOOS { |
| 187 | + case "windows": |
| 188 | + return "notepad" |
| 189 | + default: |
| 190 | + // Unix-like systems: prefer nano for better UX, fall back to vi |
| 191 | + if _, err := exec.LookPath("nano"); err == nil { |
| 192 | + return "nano" |
| 193 | + } |
| 194 | + return "vi" |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +// validateConfig validates the config file by attempting to parse it. |
| 199 | +func validateConfig(path string) error { |
| 200 | + v := viper.New() |
| 201 | + v.SetConfigFile(path) |
| 202 | + return v.ReadInConfig() |
| 203 | +} |
| 204 | + |
| 205 | +// exampleConfig returns an example config file with comments. |
| 206 | +func exampleConfig() string { |
| 207 | + return `# Upterm Configuration File |
| 208 | +# |
| 209 | +# This file follows the XDG Base Directory Specification. |
| 210 | +# Settings here are overridden by environment variables (UPTERM_*) and command-line flags. |
| 211 | +# |
| 212 | +# Configuration priority (highest to lowest): |
| 213 | +# 1. Command-line flags |
| 214 | +# 2. Environment variables (UPTERM_* prefix) |
| 215 | +# 3. This config file |
| 216 | +# 4. Default values |
| 217 | +
|
| 218 | +# Debug logging (default: false) |
| 219 | +# When enabled, writes debug-level logs to the log file. |
| 220 | +# debug: true |
| 221 | +
|
| 222 | +# Default server address for hosting sessions (default: ssh://uptermd.upterm.dev:22) |
| 223 | +# Supported protocols: ssh, ws, wss |
| 224 | +# server: ssh://uptermd.upterm.dev:22 |
| 225 | +
|
| 226 | +# Force a specific command for clients (default: none) |
| 227 | +# When set, clients cannot run arbitrary commands. |
| 228 | +# Use YAML array syntax: ["command", "arg1", "arg2"] |
| 229 | +# force-command: ["/bin/bash", "-l"] |
| 230 | +
|
| 231 | +# Path to authorized_keys file for client authentication (default: none) |
| 232 | +# authorized-keys: /path/to/authorized_keys |
| 233 | +
|
| 234 | +# Paths to private key files (default: generates ephemeral key) |
| 235 | +# private-key: |
| 236 | +# - /path/to/private/key1 |
| 237 | +# - /path/to/private/key2 |
| 238 | +
|
| 239 | +# Read-only mode (default: false) |
| 240 | +# When enabled, clients can view but not interact with the session. |
| 241 | +# read-only: false |
| 242 | +
|
| 243 | +# Auto-accept clients without confirmation (default: false) |
| 244 | +# WARNING: Only use this in trusted environments. |
| 245 | +# accept: false |
| 246 | +
|
| 247 | +# Hide client IP addresses from logs and display (default: false) |
| 248 | +# hide-client-ip: false |
| 249 | +` |
| 250 | +} |
0 commit comments