grokkit/cmd/analyze.go

185 lines
5.2 KiB
Go
Raw Normal View History

package cmd
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/git"
"gmgauthier.com/grokkit/internal/grok"
"gmgauthier.com/grokkit/internal/linter"
"gmgauthier.com/grokkit/internal/logger"
"gmgauthier.com/grokkit/internal/prompts"
)
var analyzeCmd = &cobra.Command{
Use: "analyze [flags]",
Short: "Deep project analysis → educational Markdown report",
Long: `Analyzes the repository root and generates a didactic Markdown report.
Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grokkit/prompts/.`,
Run: func(cmd *cobra.Command, args []string) {
dir := viper.GetString("dir")
if dir == "" {
dir = "."
}
output := viper.GetString("output")
if output == "" {
output = filepath.Join(".grokkit", "analysis.md")
}
model := config.GetModel("analyze", viper.GetString("model"))
yes := viper.GetBool("yes")
if !git.IsRepo() {
logger.Warn("Not inside a git repository. Git metadata in report will be limited.")
}
files, err := DiscoverSourceFiles(dir)
if err != nil {
logger.Error("Failed to discover source files", "dir", dir, "error", err)
os.Exit(1)
}
if len(files) == 0 {
fmt.Printf("No supported source files found in %s\n", dir)
os.Exit(1)
}
lang := linter.DetectPrimaryLanguage(files)
if lang == "" {
lang = "unknown"
}
promptPath, promptContent, err := prompts.LoadAnalysisPrompt(dir, lang)
if err != nil {
fmt.Printf("Error: Could not find analysis prompt for language '%s'.\n\n", lang)
fmt.Println("Create one of the following files:")
fmt.Printf(" %s\n", filepath.Join(dir, ".grokkit", "prompts", lang+".md"))
fmt.Printf(" %s\n", filepath.Join(os.Getenv("HOME"), ".config", "grokkit", "prompts", lang+".md"))
fmt.Println("\nExample starter content:")
fmt.Println(`You are an expert ` + lang + ` educator...`)
os.Exit(1)
}
logger.Info("Loaded analysis prompt", "language", lang, "path", promptPath)
projectName := filepath.Base(dir)
if projectName == "." {
projectName = "Current Project"
}
promptContent = strings.Replace(promptContent, "[Inferred Project Name]", projectName, 1)
context := BuildProjectContext(dir, files)
messages := []map[string]string{
{"role": "system", "content": promptContent},
{"role": "user", "content": fmt.Sprintf("Analyze this %s project and generate the full educational Markdown report now:\n\n%s", lang, context)},
}
report := grok.NewClient().StreamSilent(messages, model)
if !yes {
fmt.Println("\n=== Proposed Analysis Report Preview (first 60 lines) ===")
previewLines(report, 60)
fmt.Printf("\nWrite report to %s? (y/N): ", output)
var confirm string
_, err := fmt.Scanln(&confirm)
if err != nil {
return
}
if !strings.HasPrefix(strings.ToLower(confirm), "y") {
fmt.Println("Analysis cancelled by user.")
return
}
}
if output == "-" {
fmt.Println(report)
return
}
if err := os.MkdirAll(".grokkit", 0755); err != nil {
logger.Error("Failed to create .grokkit directory", "error", err)
os.Exit(1)
}
if err := os.WriteFile(output, []byte(report), 0644); err != nil {
logger.Error("Failed to write report", "file", output, "error", err)
os.Exit(1)
}
fmt.Printf("✅ Educational analysis report written to %s\n", output)
fmt.Println("Review it with: less " + output)
},
}
func init() {
analyzeCmd.Flags().String("dir", ".", "Repository root to analyze")
analyzeCmd.Flags().StringP("output", "o", "analyze.md", "Output file (use - for stdout)")
analyzeCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
// Export analysis functions for MCP use
// mcp.RegisterAnalyzeHelpers(DiscoverSourceFiles, BuildProjectContext)
}
func DiscoverSourceFiles(root string) ([]string, error) {
var files []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
name := info.Name()
if info.IsDir() {
if strings.HasPrefix(name, ".") && name != "." && name != ".." ||
name == "node_modules" || name == "vendor" || name == "build" || name == "dist" {
return filepath.SkipDir
}
return nil
}
if _, detectErr := linter.DetectLanguage(path); detectErr == nil {
files = append(files, path)
}
return nil
})
return files, err
}
func previewLines(content string, n int) {
scanner := bufio.NewScanner(strings.NewReader(content))
for i := 0; i < n && scanner.Scan(); i++ {
fmt.Println(scanner.Text())
}
if scanner.Err() != nil {
fmt.Println("... (preview truncated)")
}
}
func BuildProjectContext(dir string, files []string) string {
var sb strings.Builder
sb.WriteString("Project Root: " + dir + "\n\n")
_, err := fmt.Fprintf(&sb, "Total source files discovered: %d\n\n", len(files))
if err != nil {
return ""
}
sb.WriteString("Key files (top-level view):\n")
for _, f := range files {
rel, _ := filepath.Rel(dir, f)
if strings.Count(rel, string(filepath.Separator)) <= 2 {
sb.WriteString(" - " + rel + "\n")
}
if sb.Len() > 2500 {
break
}
}
if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" {
sb.WriteString("\nGit Remotes:\n" + out + "\n")
}
return sb.String()
}