package cmd import ( "fmt" "os" "path/filepath" "strings" "github.com/fatih/color" "github.com/spf13/cobra" "gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/linter" "gmgauthier.com/grokkit/internal/logger" ) var ( dryRun bool autoFix bool ) var lintCmd = &cobra.Command{ Use: "lint ", Short: "Lint a file and optionally apply AI-suggested fixes", Long: `Automatically detects the programming language of a file, runs the appropriate linter, and optionally uses Grok AI to apply suggested fixes. The command will: 1. Detect the file's programming language 2. Check for available linters 3. Run the linter and report issues 4. (Optional) Use Grok AI to generate and apply fixes Safety features: - Creates backup before modifying files - Shows preview of changes before applying - Requires confirmation unless --auto-fix is used`, Args: cobra.ExactArgs(1), Run: runLint, } func init() { rootCmd.AddCommand(lintCmd) lintCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show linting issues without fixing") lintCmd.Flags().BoolVar(&autoFix, "auto-fix", false, "Apply fixes without confirmation") } func runLint(cmd *cobra.Command, args []string) { filePath := args[0] logger.Info("starting lint operation", "file", filePath, "dry_run", dryRun, "auto_fix", autoFix) // Check file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { logger.Error("file not found", "file", filePath, "error", err) color.Red("āŒ File not found: %s", filePath) return } // Get absolute path for better logging absPath, err := filepath.Abs(filePath) if err != nil { logger.Warn("could not get absolute path", "file", filePath, "error", err) absPath = filePath } // Run linter color.Cyan("šŸ” Detecting language and running linter...") result, err := linter.LintFile(absPath) if err != nil { // Check if it's because no linter is available if result != nil && !result.LinterExists { logger.Warn("no linter available", "file", absPath, "language", result.Language) color.Yellow("āš ļø %s", result.Output) return } logger.Error("linting failed", "file", absPath, "error", err) color.Red("āŒ Failed to lint file: %v", err) return } // Display results color.Cyan("šŸ“‹ Language: %s", result.Language) color.Cyan("šŸ”§ Linter: %s", result.LinterUsed) fmt.Println() if !result.HasIssues { logger.Info("no linting issues found", "file", absPath, "linter", result.LinterUsed) color.Green("āœ… No issues found!") return } // Show linter output logger.Info("linting issues found", "file", absPath, "exit_code", result.ExitCode) color.Yellow("āš ļø Issues found:") fmt.Println(strings.TrimSpace(result.Output)) fmt.Println() // Stop here if dry-run if dryRun { logger.Info("dry-run mode, skipping fixes", "file", absPath) return } // Read original file content originalContent, err := os.ReadFile(absPath) if err != nil { logger.Error("failed to read file", "file", absPath, "error", err) color.Red("āŒ Failed to read file: %v", err) return } // Use Grok AI to generate fixes color.Cyan("šŸ¤– Asking Grok AI for fixes...") logger.Info("requesting AI fixes", "file", absPath, "original_size", len(originalContent)) modelFlag, _ := cmd.Flags().GetString("model") model := config.GetModel("lint", modelFlag) client := newGrokClient() messages := buildLintFixMessages(result, string(originalContent)) response := client.StreamSilent(messages, model) if response == "" { logger.Error("AI returned empty response", "file", absPath) color.Red("āŒ Failed to get AI response") return } // Clean the response fixedCode := grok.CleanCodeResponse(response) logger.Info("received AI fixes", "file", absPath, "fixed_size", len(fixedCode)) // Show preview if not auto-fix if !autoFix { fmt.Println() color.Cyan("šŸ“ Preview of fixes:") fmt.Println(strings.Repeat("-", 80)) // Show first 50 lines of fixed code lines := strings.Split(fixedCode, "\n") previewLines := 50 if len(lines) < previewLines { previewLines = len(lines) } for i := 0; i < previewLines; i++ { fmt.Println(lines[i]) } if len(lines) > previewLines { fmt.Printf("... (%d more lines)\n", len(lines)-previewLines) } fmt.Println(strings.Repeat("-", 80)) fmt.Println() // Ask for confirmation color.Yellow("Apply these fixes? (y/N): ") var response string if _, err := fmt.Scanln(&response); err != nil { logger.Info("failed to read user input", "file", absPath, "error", err) color.Yellow("āŒ Cancelled. No changes made.") return } response = strings.ToLower(strings.TrimSpace(response)) if response != "y" && response != "yes" { logger.Info("user cancelled fix application", "file", absPath) color.Yellow("āŒ Cancelled. No changes made.") return } } // Apply fixes if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil { logger.Error("failed to write fixed file", "file", absPath, "error", err) color.Red("āŒ Failed to write file: %v", err) return } logger.Info("fixes applied successfully", "file", absPath) color.Green("āœ… Fixes applied successfully!") // Optionally run linter again to verify color.Cyan("\nšŸ” Re-running linter to verify fixes...") verifyResult, err := linter.LintFile(absPath) if err == nil && !verifyResult.HasIssues { logger.Info("verification successful, no issues remain", "file", absPath) color.Green("āœ… Verification passed! No issues remaining.") } else if err == nil && verifyResult.HasIssues { logger.Warn("verification found remaining issues", "file", absPath, "exit_code", verifyResult.ExitCode) color.Yellow("āš ļø Some issues may remain:") fmt.Println(strings.TrimSpace(verifyResult.Output)) } } func buildLintFixMessages(result *linter.LintResult, originalCode string) []map[string]string { systemPrompt := "You are a code quality expert. Fix linter issues and return only the corrected code with no explanations, markdown, or extra text." userPrompt := fmt.Sprintf(`A linter has found issues in the following %s code. Linter: %s Issues found: %s Original code: %s Please fix all the issues identified by the linter. Return ONLY the corrected code without any explanations, markdown formatting, or code fences. The output should be ready to write directly to the file. Important: - Fix ALL issues reported by the linter - Preserve all functionality - Maintain the original code style as much as possible - Do NOT include markdown code fences - Do NOT include explanations or comments about the changes`, result.Language, result.LinterUsed, strings.TrimSpace(result.Output), originalCode) return []map[string]string{ {"role": "system", "content": systemPrompt}, {"role": "user", "content": userPrompt}, } }