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" // note: we use package-level funcs "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 = "analyze.md" } // Fixed: config.GetModel takes (commandName, flagModel) model := config.GetModel("analyze", viper.GetString("model")) yes := viper.GetBool("yes") // Logger (use the package-level logger as done in other commands) // (remove the old "log := logger.Get()") // Safety check if !git.IsRepo() { // IsRepo takes no arguments logger.Warn("Not inside a git repository. Git metadata in report will be limited.") } // 1. Discover source files 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) } // 2. Detect primary language lang := linter.DetectPrimaryLanguage(files) if lang == "" { lang = "unknown" } // 3. Load language-specific prompt (project → global) 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) // 4. Build rich project context context := buildProjectContext(dir, files) // 5. Call Grok — use the exact working pattern you provided messages := []map[string]string{ {"role": "system", "content": promptContent}, {"role": "user", "content": "Analyze the following project and generate the full educational Markdown report:\n\n" + context}, } // Fixed: NewClient() + Stream() (or StreamSilent if you prefer no live output) // For a long report, StreamSilent is usually better (no live printing) report := grok.NewClient().StreamSilent(messages, model) // 6. Transactional preview + confirmation 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 } } // 7. Output if output == "-" { fmt.Println(report) return } 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") rootCmd.AddCommand(analyzeCmd) } // discoverSourceFiles walks the directory and collects all supported source files. // It skips common noise directories but DOES descend into cmd/, internal/, etc. 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 } // Skip common hidden/noise directories but still allow descent into source dirs name := info.Name() if info.IsDir() { if strings.HasPrefix(name, ".") && name != "." && name != ".." || // skip .git, .grokkit (except we want .grokkit/prompts? but for source we skip) name == "node_modules" || name == "vendor" || name == "build" || name == "dist" { return filepath.SkipDir } return nil } // Check if this is a supported source file if _, detectErr := linter.DetectLanguage(path); detectErr == nil { files = append(files, path) } return nil }) return files, err } // previewLines prints the first N lines of the report (unchanged) 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)") } } // buildProjectContext — small improvement for better context (optional but nice) func buildProjectContext(dir string, files []string) string { var sb strings.Builder sb.WriteString("Project Root: " + dir + "\n\n") sb.WriteString(fmt.Sprintf("Total source files discovered: %d\n\n", len(files))) 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 } } // Git remotes if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" { sb.WriteString("\nGit Remotes:\n" + out + "\n") } return sb.String() }