feat: Introduce analyze Command for Deep Educational Codebase Analysis
#6
31
.grokkit/prompts/go.md
Normal file
31
.grokkit/prompts/go.md
Normal file
@ -0,0 +1,31 @@
|
||||
You are an expert Go educator and code archaeologist.
|
||||
Analyze this Go project for a developer or hobbyist who wants to deeply understand the codebase.
|
||||
|
||||
Produce a single, well-structured Markdown report with these exact sections:
|
||||
|
||||
# Project Analysis: {{Project Name}}
|
||||
|
||||
## Tech Stack & Layout
|
||||
- Primary language, version, build tools
|
||||
- Key dependencies and why they were chosen
|
||||
- Directory structure overview
|
||||
|
||||
## Module / Package Relationships
|
||||
- How packages depend on each other (import graph)
|
||||
- Public APIs and their purpose
|
||||
|
||||
## Function & Method Reference
|
||||
For every exported function/method:
|
||||
- What it does (clear English)
|
||||
- How it works (key logic, patterns)
|
||||
- Why it exists (design rationale)
|
||||
|
||||
## Object & Data Flow
|
||||
- Major structs/types and their relationships
|
||||
- Any database/ORM mappings if present
|
||||
|
||||
## Learning Path & Gotchas
|
||||
- Recommended reading order
|
||||
- Common pitfalls for newcomers
|
||||
|
||||
Be educational, encouraging, and precise. Use code snippets only when they clarify.
|
||||
175
cmd/analyze.go
175
cmd/analyze.go
@ -1,19 +1,176 @@
|
||||
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" // new package (see below)
|
||||
)
|
||||
|
||||
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") // or flag
|
||||
dir := viper.GetString("dir")
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
output := viper.GetString("output")
|
||||
if output == "" {
|
||||
output = "analyze.md"
|
||||
}
|
||||
model := config.GetModel(viper.GetString("model"))
|
||||
yes := viper.GetBool("yes")
|
||||
|
||||
// 1. Discover files via internal/linter
|
||||
files, err := linter.DiscoverFiles(dir)
|
||||
// 2. Build context (tree, git remote, go.mod, etc.)
|
||||
log := logger.Get()
|
||||
|
||||
// Safety: note if not in git repo
|
||||
if !git.IsRepo(dir) {
|
||||
log.Warn("Not inside a git repository. Git metadata in report will be limited.")
|
||||
}
|
||||
|
||||
// 1. Discover source files
|
||||
files, err := discoverSourceFiles(dir)
|
||||
if err != nil {
|
||||
log.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 (extend linter as needed)
|
||||
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)
|
||||
}
|
||||
log.Info("Loaded analysis prompt", "language", lang, "path", promptPath)
|
||||
|
||||
// 4. Build rich project context (tree, key files, git info, etc.)
|
||||
context := buildProjectContext(dir, files)
|
||||
// 3. Send to Grok (silent stream)
|
||||
report := grokClient.StreamSilent(buildAnalyzePrompt(context), model)
|
||||
// 4. Clean + preview (unified diff style against existing analyze.md if present)
|
||||
// 5. Confirm unless --yes
|
||||
// 6. Write or print
|
||||
|
||||
// 5. Call Grok (silent stream for full report)
|
||||
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},
|
||||
}
|
||||
|
||||
report, err := grok.GetClient().StreamSilent(messages, model)
|
||||
if err != nil {
|
||||
log.Error("Grok analysis failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 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
|
||||
fmt.Scanln(&confirm)
|
||||
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 {
|
||||
log.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")
|
||||
// model flag is handled globally via config.GetModel
|
||||
|
||||
rootCmd.AddCommand(analyzeCmd)
|
||||
}
|
||||
|
||||
// discoverSourceFiles walks the directory and collects supported source files.
|
||||
// Extend this if you want deeper recursion or ignore patterns.
|
||||
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
|
||||
}
|
||||
if info.IsDir() && (strings.HasPrefix(info.Name(), ".") || info.Name() == "node_modules" || info.Name() == "vendor") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if _, err := linter.DetectLanguage(path); err == nil {
|
||||
files = append(files, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
||||
// previewLines prints the first N lines of the report.
|
||||
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 harvests useful context (tree, go.mod, git remote, etc.).
|
||||
// Expand this as needed for other languages.
|
||||
func buildProjectContext(dir string, files []string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Project Directory: " + dir + "\n\n")
|
||||
|
||||
// Simple tree summary (or call tree/git ls-files if desired)
|
||||
sb.WriteString("Source files found: " + fmt.Sprintf("%d\n\n", len(files)))
|
||||
|
||||
// Git info
|
||||
if remotes, err := git.Run([]string{"remote", "-v"}); err == nil {
|
||||
sb.WriteString("Git Remotes:\n" + remotes + "\n\n")
|
||||
}
|
||||
|
||||
// Add more (go.mod, package.json, Cargo.toml, etc.) here for richer context
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ func init() {
|
||||
rootCmd.AddCommand(testgenCmd)
|
||||
rootCmd.AddCommand(scaffoldCmd)
|
||||
rootCmd.AddCommand(queryCmd)
|
||||
rootCmd.AddCommand(analyzeCmd)
|
||||
|
||||
// Add model flag to all commands
|
||||
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
||||
|
||||
4
internal/linter/language.go
Normal file
4
internal/linter/language.go
Normal file
@ -0,0 +1,4 @@
|
||||
package linter
|
||||
|
||||
// DetectPrimaryLanguage returns the most common language or falls back to "go"
|
||||
func DetectPrimaryLanguage(files []string) string { ... }
|
||||
32
internal/prompts/analyze.go
Normal file
32
internal/prompts/analyze.go
Normal file
@ -0,0 +1,32 @@
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LoadAnalysisPrompt returns the path and content of the best matching prompt.
|
||||
// Search order: {projectRoot}/.grokkit/prompts/{language}.md → ~/.config/grokkit/prompts/{language}.md
|
||||
func LoadAnalysisPrompt(projectRoot, language string) (path, content string, err error) {
|
||||
if language == "" {
|
||||
language = "unknown"
|
||||
}
|
||||
|
||||
// 1. Project-local (preferred)
|
||||
localPath := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md")
|
||||
if data, readErr := os.ReadFile(localPath); readErr == nil {
|
||||
return localPath, string(data), nil
|
||||
}
|
||||
|
||||
// 2. Global config
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
home = "/root" // fallback for containers
|
||||
}
|
||||
globalPath := filepath.Join(home, ".config", "grokkit", "prompts", language+".md")
|
||||
if data, readErr := os.ReadFile(globalPath); readErr == nil {
|
||||
return globalPath, string(data), nil
|
||||
}
|
||||
|
||||
return "", "", os.ErrNotExist
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user