Go · 8101 bytes Raw Blame History
1 package cmd
2
3 import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "parrot/internal/config"
11
12 "github.com/spf13/cobra"
13 )
14
15 var configureCmd = &cobra.Command{
16 Use: "configure",
17 Short: "Interactively configure parrot backends and preferences",
18 Long: "Walk through interactive setup to configure API keys, models, and preferences",
19 Run: runConfigure,
20 }
21
22 func init() {
23 rootCmd.AddCommand(configureCmd)
24 }
25
26 func runConfigure(cmd *cobra.Command, args []string) {
27 fmt.Println("🦜 Parrot Configuration Wizard")
28 fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
29 fmt.Println()
30
31 reader := bufio.NewReader(os.Stdin)
32
33 // Load existing config or defaults
34 cfg, err := config.LoadConfig()
35 if err != nil {
36 cfg = config.DefaultConfig()
37 }
38
39 // 1. Choose config location
40 configPath := chooseConfigLocation(reader)
41
42 // 2. Configure API backend
43 fmt.Println("🌐 API Backend Configuration")
44 fmt.Println("────────────────────────────")
45 cfg.API.Enabled = askYesNo(reader, "Enable API backend? (recommended)", cfg.API.Enabled)
46
47 if cfg.API.Enabled {
48 cfg.API.Provider = askChoice(reader, "API Provider", []string{"openai", "anthropic", "custom"}, cfg.API.Provider)
49
50 if cfg.API.Provider == "custom" {
51 cfg.API.Endpoint = askString(reader, "API Endpoint URL", cfg.API.Endpoint)
52 } else {
53 // Set default endpoints
54 switch cfg.API.Provider {
55 case "openai":
56 cfg.API.Endpoint = "https://api.openai.com/v1"
57 case "anthropic":
58 cfg.API.Endpoint = "https://api.anthropic.com/v1"
59 }
60 }
61
62 cfg.API.APIKey = askString(reader, "API Key", cfg.API.APIKey)
63 cfg.API.Model = askString(reader, "Model name", cfg.API.Model)
64 }
65
66 // 3. Configure Local backend
67 fmt.Println("\n🖥️ Local Backend Configuration")
68 fmt.Println("────────────────────────────")
69 cfg.Local.Enabled = askYesNo(reader, "Enable local Ollama backend?", cfg.Local.Enabled)
70
71 if cfg.Local.Enabled {
72 cfg.Local.Endpoint = askString(reader, "Ollama endpoint", cfg.Local.Endpoint)
73 availableModels := []string{"phi3.5:3.8b", "llama3.2:3b", "qwen2.5:0.5b", "custom"}
74 selectedModel := askChoice(reader, "Local model", availableModels, cfg.Local.Model)
75
76 if selectedModel == "custom" {
77 cfg.Local.Model = askString(reader, "Custom model name", cfg.Local.Model)
78 } else {
79 cfg.Local.Model = selectedModel
80 }
81 }
82
83 // 4. General preferences
84 fmt.Println("\n⚙️ General Preferences")
85 fmt.Println("────────────────────────")
86 personalities := []string{"mild", "sarcastic", "savage"}
87 if cfg.General.Personality == "" {
88 cfg.General.Personality = "sarcastic"
89 }
90 cfg.General.Personality = askChoice(reader, "Personality level", personalities, cfg.General.Personality)
91 cfg.General.Debug = askYesNo(reader, "Enable debug mode?", cfg.General.Debug)
92 cfg.General.FallbackMode = askYesNo(reader, "Use only fallback responses? (disable AI)", cfg.General.FallbackMode)
93
94 // 5. Save configuration
95 fmt.Println("\n💾 Saving Configuration...")
96 if err := config.CreateSampleConfig(configPath); err != nil {
97 fmt.Printf("❌ Error creating config template: %v\n", err)
98 return
99 }
100
101 // Load the template and update with user values
102 if err := saveConfig(cfg, configPath); err != nil {
103 fmt.Printf("❌ Error saving configuration: %v\n", err)
104 return
105 }
106
107 fmt.Printf("✅ Configuration saved to: %s\n", configPath)
108
109 // 6. Next steps
110 fmt.Println("\n🎯 Next Steps:")
111 if cfg.API.Enabled && cfg.API.APIKey != "" {
112 fmt.Println(" • Test API backend: parrot status")
113 }
114 if cfg.Local.Enabled {
115 fmt.Printf(" • Ensure model is available: ollama pull %s\n", cfg.Local.Model)
116 }
117 fmt.Println(" • Test parrot: parrot mock \"git push\" \"1\"")
118 fmt.Println(" • Install shell hooks: parrot install")
119 }
120
121 func chooseConfigLocation(reader *bufio.Reader) string {
122 fmt.Println("📁 Configuration Location")
123 fmt.Println("─────────────────────────")
124
125 paths := config.GetConfigPaths()
126 fmt.Println("Choose where to save your configuration:")
127 for i, path := range paths {
128 fmt.Printf("%d. %s\n", i+1, path)
129 }
130
131 for {
132 fmt.Print("Choice [1]: ")
133 input, _ := reader.ReadString('\n')
134 input = strings.TrimSpace(input)
135
136 if input == "" {
137 return paths[0] // Default to first option
138 }
139
140 choice := 0
141 if _, err := fmt.Sscanf(input, "%d", &choice); err == nil && choice >= 1 && choice <= len(paths) {
142 return paths[choice-1]
143 }
144
145 fmt.Println("❌ Invalid choice. Please enter a number between 1 and", len(paths))
146 }
147 }
148
149 func askString(reader *bufio.Reader, prompt, defaultValue string) string {
150 if defaultValue != "" {
151 fmt.Printf("%s [%s]: ", prompt, defaultValue)
152 } else {
153 fmt.Printf("%s: ", prompt)
154 }
155
156 input, _ := reader.ReadString('\n')
157 input = strings.TrimSpace(input)
158
159 if input == "" {
160 return defaultValue
161 }
162 return input
163 }
164
165 func askYesNo(reader *bufio.Reader, prompt string, defaultValue bool) bool {
166 defaultStr := "n"
167 if defaultValue {
168 defaultStr = "y"
169 }
170
171 fmt.Printf("%s [%s]: ", prompt, defaultStr)
172 input, _ := reader.ReadString('\n')
173 input = strings.ToLower(strings.TrimSpace(input))
174
175 if input == "" {
176 return defaultValue
177 }
178
179 return input == "y" || input == "yes"
180 }
181
182 func askChoice(reader *bufio.Reader, prompt string, choices []string, defaultValue string) string {
183 fmt.Printf("%s:\n", prompt)
184 for i, choice := range choices {
185 marker := " "
186 if choice == defaultValue {
187 marker = "*"
188 }
189 fmt.Printf("%s %d. %s\n", marker, i+1, choice)
190 }
191
192 for {
193 fmt.Print("Choice: ")
194 input, _ := reader.ReadString('\n')
195 input = strings.TrimSpace(input)
196
197 if input == "" && defaultValue != "" {
198 return defaultValue
199 }
200
201 choice := 0
202 if _, err := fmt.Sscanf(input, "%d", &choice); err == nil && choice >= 1 && choice <= len(choices) {
203 return choices[choice-1]
204 }
205
206 // Allow text input too
207 for _, validChoice := range choices {
208 if strings.EqualFold(input, validChoice) {
209 return validChoice
210 }
211 }
212
213 fmt.Printf("❌ Invalid choice. Please enter 1-%d or the option name.\n", len(choices))
214 }
215 }
216
217 func saveConfig(cfg *config.Config, path string) error {
218 // Ensure directory exists
219 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
220 return err
221 }
222
223 content := generateConfigContent(cfg)
224 return os.WriteFile(path, []byte(content), 0644)
225 }
226
227 func generateConfigContent(cfg *config.Config) string {
228 var content strings.Builder
229
230 content.WriteString("# Parrot Configuration File\n")
231 content.WriteString("# Generated by: parrot configure\n\n")
232
233 // General section
234 content.WriteString("[general]\n")
235 content.WriteString(fmt.Sprintf("personality = \"%s\"\n", cfg.General.Personality))
236 content.WriteString(fmt.Sprintf("fallback_mode = %t\n", cfg.General.FallbackMode))
237 content.WriteString(fmt.Sprintf("debug = %t\n", cfg.General.Debug))
238 content.WriteString("\n")
239
240 // API section
241 content.WriteString("[api]\n")
242 content.WriteString(fmt.Sprintf("enabled = %t\n", cfg.API.Enabled))
243 content.WriteString(fmt.Sprintf("provider = \"%s\"\n", cfg.API.Provider))
244 content.WriteString(fmt.Sprintf("endpoint = \"%s\"\n", cfg.API.Endpoint))
245 if cfg.API.APIKey != "" {
246 content.WriteString(fmt.Sprintf("api_key = \"%s\"\n", cfg.API.APIKey))
247 } else {
248 content.WriteString("# api_key = \"your-api-key-here\"\n")
249 }
250 content.WriteString(fmt.Sprintf("model = \"%s\"\n", cfg.API.Model))
251 content.WriteString(fmt.Sprintf("timeout = %d\n", cfg.API.Timeout))
252 content.WriteString("\n")
253
254 // Local section
255 content.WriteString("[local]\n")
256 content.WriteString(fmt.Sprintf("enabled = %t\n", cfg.Local.Enabled))
257 content.WriteString(fmt.Sprintf("provider = \"%s\"\n", cfg.Local.Provider))
258 content.WriteString(fmt.Sprintf("endpoint = \"%s\"\n", cfg.Local.Endpoint))
259 content.WriteString(fmt.Sprintf("model = \"%s\"\n", cfg.Local.Model))
260 content.WriteString(fmt.Sprintf("timeout = %d\n", cfg.Local.Timeout))
261
262 return content.String()
263 }