| 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 | } |