- Handle errors from fmt.Scanln in cmd/edit.go and cmd/lint.go to prevent crashes on input failures, providing user feedback and preserving backups. - Update .gitea/workflows/release.yml to use 'ubuntu-gitea' runner for CI consistency.
236 lines
7.1 KiB
Go
236 lines
7.1 KiB
Go
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:
|
|
- 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(modelFlag)
|
|
|
|
client := grok.NewClient()
|
|
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))
|
|
|
|
// Create backup
|
|
backupPath := absPath + ".bak"
|
|
if err := os.WriteFile(backupPath, originalContent, 0644); err != nil {
|
|
logger.Error("failed to create backup", "file", absPath, "error", err)
|
|
color.Red("❌ Failed to create backup: %v", err)
|
|
return
|
|
}
|
|
logger.Info("backup created", "backup", backupPath)
|
|
color.Green("💾 Backup created: %s", backupPath)
|
|
|
|
// 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. Backup preserved at: %s", backupPath)
|
|
return
|
|
}
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
|
|
if response != "y" && response != "yes" {
|
|
logger.Info("user cancelled fix application", "file", absPath)
|
|
color.Yellow("❌ Cancelled. Backup preserved at: %s", backupPath)
|
|
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!")
|
|
color.Cyan("💾 Original saved to: %s", backupPath)
|
|
|
|
// 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},
|
|
}
|
|
}
|