Go · 6996 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.7.1",
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 var mockCmd = &cobra.Command{
36 Use: "mock [command] [exit_code]",
37 Short: "Mock a failed command",
38 Long: "Called by shell hooks when a command fails",
39 Args: cobra.MinimumNArgs(2),
40 Run: mockCommand,
41 }
42
43 func init() {
44 rootCmd.AddCommand(mockCmd)
45 }
46
47 func mockCommand(cmd *cobra.Command, args []string) {
48 failedCmd := args[0]
49 exitCode := args[1]
50
51 // Basic command type detection
52 cmdType := detectCommandType(failedCmd)
53
54 // Show immediate feedback to user
55 fmt.Print("🦜 ")
56
57 // Generate a smart mock response
58 response, cfg := generateSmartResponse(cmdType, failedCmd, exitCode)
59
60 // Clear the loading indicator and show response
61 fmt.Print("\r") // Clear current line
62
63 // Format output with colors and personality
64 if cfg.General.Colors {
65 fmt.Println(colors.FormatParrotOutput(cfg.General.Personality, response, cfg.General.Enhanced))
66 } else {
67 fmt.Printf("🦜 %s\n", response)
68 }
69 }
70
71 func detectCommandType(command string) string {
72 parts := strings.Fields(command)
73 if len(parts) == 0 {
74 return "generic"
75 }
76
77 cmd := parts[0]
78
79 // Check for common command patterns
80 switch cmd {
81 // Version control
82 case "git":
83 return "git"
84
85 // Node.js ecosystem
86 case "npm", "yarn", "pnpm", "node", "npx", "bun", "deno":
87 return "nodejs"
88
89 // Containers
90 case "docker", "docker-compose", "podman":
91 return "docker"
92
93 // Kubernetes
94 case "kubectl", "k9s", "helm", "kustomize", "k3s", "minikube":
95 return "kubernetes"
96
97 // HTTP/Network
98 case "curl", "wget", "http", "https":
99 return "http"
100
101 // SSH/Remote
102 case "ssh", "scp", "sftp", "rsync":
103 return "ssh_expanded"
104
105 // Shell scripting
106 case "bash", "zsh", "fish", "sh", "ksh", "csh":
107 return "shell_scripting"
108
109 // Navigation
110 case "cd", "pushd", "popd":
111 return "navigation"
112
113 // Python
114 case "python", "python3", "pip", "pip3", "poetry", "pipenv", "conda":
115 return "python_expanded"
116
117 // Rust
118 case "cargo", "rustc", "rustup":
119 return "rust_expanded"
120
121 // Go
122 case "go":
123 return "golang_expanded"
124
125 // Java
126 case "java", "javac", "mvn", "gradle":
127 return "java_expanded"
128
129 // C/C++
130 case "gcc", "g++", "clang", "clang++", "cc", "c++":
131 return "c_expanded"
132
133 // Ruby
134 case "ruby", "gem", "bundle", "rake", "rails":
135 return "ruby_expanded"
136
137 // PHP
138 case "php", "composer":
139 return "php_expanded"
140
141 // Build systems (check for C files to categorize properly)
142 case "make", "cmake", "ninja", "ant", "bazel":
143 if strings.Contains(command, ".c ") || strings.HasSuffix(command, ".c") {
144 return "c"
145 }
146 return "build"
147
148 // Databases
149 case "mysql", "psql", "postgres", "mongo", "mongosh", "redis-cli", "sqlite3":
150 return "database"
151
152 // Testing tools
153 case "jest", "vitest", "pytest", "mocha", "jasmine", "karma", "cypress", "playwright", "rspec", "phpunit", "junit":
154 return "testing"
155
156 // Security tools
157 case "nmap", "nikto", "burpsuite", "metasploit", "nessus", "wireshark", "tcpdump", "openssl", "gpg":
158 return "security"
159
160 // Performance tools
161 case "perf", "valgrind", "gprof", "strace", "ltrace", "top", "htop", "iotop":
162 return "performance"
163
164 // Cloud providers
165 case "aws", "gcloud", "az", "terraform", "pulumi", "cloudformation":
166 return "cloud"
167
168 // DevOps tools
169 case "ansible", "ansible-playbook", "puppet", "chef", "jenkins", "circleci", "travis":
170 return "devops"
171
172 // Monitoring tools
173 case "prometheus", "grafana", "datadog", "newrelic", "splunk", "elastic", "kibana", "logstash":
174 return "monitoring"
175
176 // Permission-related commands
177 case "chmod", "chown", "chgrp", "sudo":
178 return "permissions"
179
180 default:
181 return "generic"
182 }
183 }
184
185 func generateSmartResponse(cmdType, command, exitCode string) (string, *config.Config) {
186 // Load configuration
187 cfg, err := config.LoadConfig()
188 if err != nil {
189 // If config loading fails, use fallback with default config
190 defaultCfg := config.DefaultConfig()
191 return getFallbackResponse(cmdType), defaultCfg
192 }
193
194 // Initialize LLM manager
195 manager := llm.NewLLMManager(cfg)
196
197 // Build context-aware prompt with personality
198 prompt := prompts.BuildPrompt(cmdType, command, exitCode, cfg.General.Personality)
199
200 // Use a reasonable timeout for LLM responses (6 seconds max)
201 // With optimized Ollama options, responses should be under 2 seconds when warm
202 maxTimeout := 6 * time.Second
203 ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
204 defer cancel()
205
206 // Create a channel for the response
207 responseChan := make(chan struct {
208 response string
209 backend llm.Backend
210 }, 1)
211
212 // Start generation in a goroutine
213 go func() {
214 // Use GenerateWithContext for intelligent fallbacks
215 response, backend := manager.GenerateWithContext(ctx, prompt, cmdType, command, exitCode)
216 select {
217 case responseChan <- struct {
218 response string
219 backend llm.Backend
220 }{response, backend}:
221 case <-ctx.Done():
222 }
223 }()
224
225 // Show progress indicator for anything longer than 500ms
226 progressTimer := time.NewTimer(500 * time.Millisecond)
227 defer progressTimer.Stop()
228
229 select {
230 case result := <-responseChan:
231 progressTimer.Stop()
232 // Add backend indicator in debug mode
233 if cfg.General.Debug {
234 switch result.backend {
235 case llm.BackendAPI:
236 fmt.Printf("🌐 API backend used\n")
237 case llm.BackendLocal:
238 fmt.Printf("🖥️ Local backend used\n")
239 case llm.BackendFallback:
240 fmt.Printf("🔄 Fallback backend used\n")
241 }
242 }
243 return result.response, cfg
244 case <-progressTimer.C:
245 // Show thinking indicator after 500ms
246 fmt.Print("💭")
247 select {
248 case result := <-responseChan:
249 // Add backend indicator in debug mode
250 if cfg.General.Debug {
251 switch result.backend {
252 case llm.BackendAPI:
253 fmt.Printf("\n🌐 API backend used\n")
254 case llm.BackendLocal:
255 fmt.Printf("\n🖥️ Local backend used\n")
256 case llm.BackendFallback:
257 fmt.Printf("\n🔄 Fallback backend used\n")
258 }
259 }
260 return result.response, cfg
261 case <-ctx.Done():
262 // Fallback to instant response if timeout reached
263 return getFallbackResponse(cmdType), cfg
264 }
265 case <-ctx.Done():
266 // Fallback to instant response if timeout reached
267 return getFallbackResponse(cmdType), cfg
268 }
269 }
270
271 func getFallbackResponse(cmdType string) string {
272 // Use the expanded fallback database with hundreds of brutal insults
273 // This is only called on config load failure, so we don't have full context
274 return llm.GetExpandedFallback(cmdType, "")
275 }