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