Go · 8061 bytes Raw Blame History
1 package cmd
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "strings"
8 "time"
9
10 "parrot/internal/colors"
11 "parrot/internal/config"
12 "parrot/internal/llm"
13 "parrot/internal/prompts"
14
15 "github.com/spf13/cobra"
16 )
17
18 var rootCmd = &cobra.Command{
19 Use: "parrot",
20 Version: "1.8.2",
21 Short: "A sassy CLI that mocks your failed commands",
22 Long: "Parrot listens for failed commands and responds with intelligent insults and mockery.",
23 Run: func(cmd *cobra.Command, args []string) {
24 fmt.Println("🦜 Parrot is watching... waiting for you to mess up!")
25 },
26 }
27
28 func Execute() {
29 if err := rootCmd.Execute(); err != nil {
30 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
31 os.Exit(1)
32 }
33 }
34
35 // CLI flags
36 var spicyMode bool
37
38 var mockCmd = &cobra.Command{
39 Use: "mock [command] [exit_code]",
40 Short: "Mock a failed command",
41 Long: "Called by shell hooks when a command fails",
42 Args: cobra.MinimumNArgs(2),
43 Run: mockCommand,
44 }
45
46 func init() {
47 rootCmd.AddCommand(mockCmd)
48
49 // Add --spicy flag for quality mode (default is snappy/fast)
50 mockCmd.Flags().BoolVar(&spicyMode, "spicy", false, "Use spicy mode (richer responses, slightly slower)")
51 }
52
53 func mockCommand(cmd *cobra.Command, args []string) {
54 failedCmd := args[0]
55 exitCode := args[1]
56
57 // Basic command type detection
58 cmdType := detectCommandType(failedCmd)
59
60 // Show immediate feedback to user
61 fmt.Print("🦜 ")
62
63 // Generate a smart mock response
64 response, cfg := generateSmartResponse(cmdType, failedCmd, exitCode)
65
66 // Clear the loading indicator and show response
67 fmt.Print("\r") // Clear current line
68
69 // Format output with colors and personality
70 if cfg.General.Colors {
71 fmt.Println(colors.FormatParrotOutput(cfg.General.Personality, response, cfg.General.Enhanced))
72 } else {
73 fmt.Printf("🦜 %s\n", response)
74 }
75 }
76
77 func detectCommandType(command string) string {
78 parts := strings.Fields(command)
79 if len(parts) == 0 {
80 return "generic"
81 }
82
83 cmd := parts[0]
84
85 // Check for common command patterns
86 switch cmd {
87 // Version control
88 case "git":
89 return "git"
90
91 // Node.js ecosystem
92 case "npm", "yarn", "pnpm", "node", "npx", "bun", "deno":
93 return "nodejs"
94
95 // Containers
96 case "docker", "docker-compose", "podman":
97 return "docker"
98
99 // Kubernetes
100 case "kubectl", "k9s", "helm", "kustomize", "k3s", "minikube":
101 return "kubernetes"
102
103 // HTTP/Network
104 case "curl", "wget", "http", "https", "httpie":
105 return "http_errors"
106
107 // SSH/Remote
108 case "ssh", "scp", "sftp", "rsync":
109 return "ssh_expanded"
110
111 // Shell scripting
112 case "bash", "zsh", "fish", "sh", "ksh", "csh":
113 return "shell_scripting"
114
115 // Navigation
116 case "cd", "pushd", "popd":
117 return "navigation"
118
119 // Python - check for ML frameworks first
120 case "python", "python3", "pip", "pip3", "poetry", "pipenv", "conda":
121 // Check if this is an AI/ML command
122 if strings.Contains(command, "torch") || strings.Contains(command, "tensorflow") ||
123 strings.Contains(command, "keras") || strings.Contains(command, "sklearn") ||
124 strings.Contains(command, "pytorch") || strings.Contains(command, "transformers") ||
125 strings.Contains(command, "cuda") || strings.Contains(command, "gpu") ||
126 strings.Contains(command, "train") || strings.Contains(command, "model") {
127 return "ai_ml"
128 }
129 return "python_expanded"
130
131 // Rust
132 case "cargo", "rustc", "rustup":
133 return "rust_expanded"
134
135 // Go
136 case "go":
137 return "golang_expanded"
138
139 // Java
140 case "java", "javac", "mvn", "gradle":
141 return "java_expanded"
142
143 // C/C++
144 case "gcc", "g++", "clang", "clang++", "cc", "c++":
145 return "c_expanded"
146
147 // Ruby
148 case "ruby", "gem", "bundle", "rake", "rails":
149 return "ruby_expanded"
150
151 // PHP
152 case "php", "composer":
153 return "php_expanded"
154
155 // Build systems (check for C files to categorize properly)
156 case "make", "cmake", "ninja", "ant", "bazel":
157 if strings.Contains(command, ".c ") || strings.HasSuffix(command, ".c") {
158 return "c"
159 }
160 return "build"
161
162 // Databases
163 case "mysql", "psql", "postgres", "mongo", "mongosh", "redis-cli", "sqlite3":
164 return "database"
165
166 // Testing tools
167 case "jest", "vitest", "pytest", "mocha", "jasmine", "karma", "cypress", "playwright", "rspec", "phpunit", "junit":
168 return "testing"
169
170 // Security tools
171 case "nmap", "nikto", "burpsuite", "metasploit", "nessus", "wireshark", "tcpdump", "openssl", "gpg":
172 return "security"
173
174 // Performance tools
175 case "perf", "valgrind", "gprof", "strace", "ltrace", "top", "htop", "iotop":
176 return "performance"
177
178 // AI/ML tools
179 case "nvidia-smi", "nvcc", "tensorboard", "mlflow", "wandb", "jupyter", "ipython":
180 return "ai_ml"
181
182 // Terraform/IaC
183 case "terraform", "pulumi", "cdktf", "terragrunt":
184 return "terraform"
185
186 // Cloud providers
187 case "aws", "gcloud", "az", "cloudformation", "cdk":
188 return "cloud"
189
190 // DevOps tools
191 case "ansible", "ansible-playbook", "puppet", "chef", "jenkins", "circleci", "travis":
192 return "devops"
193
194 // Monitoring tools
195 case "prometheus", "grafana", "datadog", "newrelic", "splunk", "elastic", "kibana", "logstash":
196 return "monitoring"
197
198 // Permission-related commands
199 case "chmod", "chown", "chgrp", "sudo":
200 return "permissions"
201
202 default:
203 return "generic"
204 }
205 }
206
207 func generateSmartResponse(cmdType, command, exitCode string) (string, *config.Config) {
208 // Load configuration
209 cfg, err := config.LoadConfig()
210 if err != nil {
211 // If config loading fails, use fallback with default config
212 defaultCfg := config.DefaultConfig()
213 return getFallbackResponse(cmdType), defaultCfg
214 }
215
216 // Override mode if --spicy flag is set
217 if spicyMode {
218 cfg.General.GenerationMode = "spicy"
219 }
220
221 // Initialize LLM manager
222 manager := llm.NewLLMManager(cfg)
223
224 // Build context-aware prompt with personality
225 prompt := prompts.BuildPrompt(cmdType, command, exitCode, cfg.General.Personality)
226
227 // Set timeout based on generation mode
228 // Snappy: 4s max (3s LLM + 1s buffer), Spicy: 6s max (5s LLM + 1s buffer)
229 maxTimeout := 4 * time.Second
230 if cfg.General.GenerationMode == "spicy" {
231 maxTimeout = 6 * time.Second
232 }
233 ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
234 defer cancel()
235
236 // Create a channel for the response
237 responseChan := make(chan struct {
238 response string
239 backend llm.Backend
240 }, 1)
241
242 // Start generation in a goroutine
243 go func() {
244 // Use GenerateWithContext for intelligent fallbacks
245 response, backend := manager.GenerateWithContext(ctx, prompt, cmdType, command, exitCode)
246 select {
247 case responseChan <- struct {
248 response string
249 backend llm.Backend
250 }{response, backend}:
251 case <-ctx.Done():
252 }
253 }()
254
255 // Show progress indicator for anything longer than 500ms
256 progressTimer := time.NewTimer(500 * time.Millisecond)
257 defer progressTimer.Stop()
258
259 select {
260 case result := <-responseChan:
261 progressTimer.Stop()
262 // Add backend indicator in debug mode
263 if cfg.General.Debug {
264 switch result.backend {
265 case llm.BackendAPI:
266 fmt.Printf("🌐 API backend used\n")
267 case llm.BackendLocal:
268 fmt.Printf("🖥️ Local backend used\n")
269 case llm.BackendFallback:
270 fmt.Printf("🔄 Fallback backend used\n")
271 }
272 }
273 return result.response, cfg
274 case <-progressTimer.C:
275 // Show thinking indicator after 500ms
276 fmt.Print("💭")
277 select {
278 case result := <-responseChan:
279 // Add backend indicator in debug mode
280 if cfg.General.Debug {
281 switch result.backend {
282 case llm.BackendAPI:
283 fmt.Printf("\n🌐 API backend used\n")
284 case llm.BackendLocal:
285 fmt.Printf("\n🖥️ Local backend used\n")
286 case llm.BackendFallback:
287 fmt.Printf("\n🔄 Fallback backend used\n")
288 }
289 }
290 return result.response, cfg
291 case <-ctx.Done():
292 // Fallback to instant response if timeout reached
293 return getFallbackResponse(cmdType), cfg
294 }
295 case <-ctx.Done():
296 // Fallback to instant response if timeout reached
297 return getFallbackResponse(cmdType), cfg
298 }
299 }
300
301 func getFallbackResponse(cmdType string) string {
302 // Use the expanded fallback database with hundreds of brutal insults
303 // This is only called on config load failure, so we don't have full context
304 return llm.GetExpandedFallback(cmdType, "")
305 }