grokkit/cmd/lint.go

224 lines
6.6 KiB
Go
Raw Normal View History

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 <file>",
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:
- 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},
}
}