feat(cmd): add AI documentation generation and command tests
- Implemented `grokkit docs` command for generating language-specific documentation comments (godoc, PEP 257, Doxygen, etc.) with previews, backups, and auto-apply option - Extracted message builder functions for commit, history, pr-describe, and review commands - Added comprehensive unit tests for all command message builders (commit_test.go, docs_test.go, history_test.go, lint_test.go, prdescribe_test.go, review_test.go) - Enforced 70% test coverage threshold in CI workflow - Added .golangci.yml configuration with linters like govet, errcheck, staticcheck - Updated Makefile to include -race in tests and add help target - Updated README.md with new docs command details, workflows, and quality features - Added .claude/ to .gitignore - Configured default model for docs command in config.go
This commit is contained in:
parent
6e3a52728e
commit
0aa806be70
@ -40,9 +40,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go tool cover -func=coverage.out | tail -1
|
go tool cover -func=coverage.out | tail -1
|
||||||
|
|
||||||
|
- name: Enforce coverage threshold
|
||||||
|
run: |
|
||||||
|
PCT=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%')
|
||||||
|
awk "BEGIN { exit ($PCT < 70) }" || (echo "Coverage ${PCT}% is below 70%" && exit 1)
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-gitea
|
runs-on: ubuntu-gitea
|
||||||
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -60,6 +66,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-gitea
|
runs-on: ubuntu-gitea
|
||||||
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.junie/
|
.junie/
|
||||||
|
.claude/
|
||||||
build/
|
build/
|
||||||
grokkit
|
grokkit
|
||||||
*.bak
|
*.bak
|
||||||
|
|||||||
22
.golangci.yml
Normal file
22
.golangci.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- govet
|
||||||
|
- errcheck
|
||||||
|
- staticcheck
|
||||||
|
- ineffassign
|
||||||
|
- unused
|
||||||
|
- gosimple
|
||||||
|
- typecheck
|
||||||
|
- misspell
|
||||||
|
- gofmt
|
||||||
|
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
check-blank: false
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
4
Makefile
4
Makefile
@ -1,11 +1,11 @@
|
|||||||
.PHONY: test test-cover test-agent lint build install clean
|
.PHONY: test test-cover test-agent lint build install clean help
|
||||||
|
|
||||||
VERSION ?= dev-$(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
VERSION ?= dev-$(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
||||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)
|
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./... -v
|
go test ./... -v -race
|
||||||
|
|
||||||
test-cover:
|
test-cover:
|
||||||
@mkdir -p build
|
@mkdir -p build
|
||||||
|
|||||||
74
README.md
74
README.md
@ -37,6 +37,15 @@ grokkit version
|
|||||||
## 📋 Table of Contents
|
## 📋 Table of Contents
|
||||||
|
|
||||||
- [Commands](#commands)
|
- [Commands](#commands)
|
||||||
|
- [chat](#-grokkit-chat)
|
||||||
|
- [edit](#-grokkit-edit-file-instruction)
|
||||||
|
- [commit / commitmsg](#-grokkit-commitmsg)
|
||||||
|
- [review](#-grokkit-review)
|
||||||
|
- [pr-describe](#-grokkit-pr-describe)
|
||||||
|
- [history](#-grokkit-history)
|
||||||
|
- [lint](#-grokkit-lint-file)
|
||||||
|
- [docs](#-grokkit-docs-file)
|
||||||
|
- [agent](#-grokkit-agent)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Workflows](#workflows)
|
- [Workflows](#workflows)
|
||||||
- [Shell Completions](#shell-completions)
|
- [Shell Completions](#shell-completions)
|
||||||
@ -148,6 +157,41 @@ Summarize recent git commits.
|
|||||||
grokkit history # Last 10 commits
|
grokkit history # Last 10 commits
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 📖 `grokkit docs <file> [file...]`
|
||||||
|
Generate language-appropriate documentation comments using Grok AI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview and confirm
|
||||||
|
grokkit docs main.go
|
||||||
|
|
||||||
|
# Auto-apply without confirmation
|
||||||
|
grokkit docs handlers.go models.go --auto-apply
|
||||||
|
|
||||||
|
# Document multiple files at once
|
||||||
|
grokkit docs cmd/*.go --auto-apply
|
||||||
|
|
||||||
|
# Use specific model
|
||||||
|
grokkit docs app.py -m grok-4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported doc styles by language:**
|
||||||
|
|
||||||
|
| Language | Style |
|
||||||
|
|----------|-------|
|
||||||
|
| Go | godoc (`// FuncName does...`) |
|
||||||
|
| Python | PEP 257 docstrings (`"""Summary\n\nArgs:..."""`) |
|
||||||
|
| C / C++ | Doxygen (`/** @brief ... @param ... @return ... */`) |
|
||||||
|
| JavaScript / TypeScript | JSDoc (`/** @param {type} name ... */`) |
|
||||||
|
| Rust | rustdoc (`/// Summary\n/// # Arguments`) |
|
||||||
|
| Ruby | YARD (`# @param [Type] name`) |
|
||||||
|
| Java | Javadoc (`/** @param ... @return ... */`) |
|
||||||
|
| Shell | Shell comments (`# function: desc, # Args: ...`) |
|
||||||
|
|
||||||
|
**Safety features:**
|
||||||
|
- Creates `.bak` backup before any changes
|
||||||
|
- Shows first 50 lines of documented code as preview
|
||||||
|
- Requires confirmation (unless `--auto-apply`)
|
||||||
|
|
||||||
### 🤖 `grokkit agent`
|
### 🤖 `grokkit agent`
|
||||||
Multi-file agent for complex refactoring (experimental).
|
Multi-file agent for complex refactoring (experimental).
|
||||||
|
|
||||||
@ -315,6 +359,23 @@ grokkit review
|
|||||||
grokkit commit
|
grokkit commit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Documentation Generation Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Preview docs for a single file
|
||||||
|
grokkit docs internal/api/handler.go
|
||||||
|
|
||||||
|
# 2. Batch-document a package
|
||||||
|
grokkit docs cmd/*.go --auto-apply
|
||||||
|
|
||||||
|
# 3. Document across languages in one pass
|
||||||
|
grokkit docs lib/utils.py src/helpers.ts --auto-apply
|
||||||
|
|
||||||
|
# 4. Review and commit
|
||||||
|
grokkit review
|
||||||
|
grokkit commit
|
||||||
|
```
|
||||||
|
|
||||||
## Shell Completions
|
## Shell Completions
|
||||||
|
|
||||||
Generate shell completions for faster command entry:
|
Generate shell completions for faster command entry:
|
||||||
@ -369,10 +430,13 @@ grokkit review -v
|
|||||||
- ✅ **Safe file editing** - Automatic backups, preview, confirmation
|
- ✅ **Safe file editing** - Automatic backups, preview, confirmation
|
||||||
- ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions
|
- ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions
|
||||||
- ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes
|
- ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes
|
||||||
|
- ✅ **AI documentation generation** - 8 doc styles (godoc, PEP 257, Doxygen, JSDoc, rustdoc, YARD, Javadoc, shell)
|
||||||
|
|
||||||
### Quality & Testing
|
### Quality & Testing
|
||||||
- ✅ **Test coverage 72%** - Comprehensive unit tests
|
- ✅ **Test coverage 72%+** - Comprehensive unit tests including all command message builders
|
||||||
- ✅ **CI/CD with Gitea Actions** - Automated testing and builds
|
- ✅ **Coverage gate in CI** - Builds fail if coverage drops below 70%
|
||||||
|
- ✅ **CI/CD with Gitea Actions** - Tests must pass before lint and build jobs run
|
||||||
|
- ✅ **Golangci-lint configured** - `.golangci.yml` with govet, errcheck, staticcheck, and more
|
||||||
- ✅ **Interface-based design** - Testable and maintainable
|
- ✅ **Interface-based design** - Testable and maintainable
|
||||||
- ✅ **Zero external dependencies** - Only stdlib + well-known libs
|
- ✅ **Zero external dependencies** - Only stdlib + well-known libs
|
||||||
|
|
||||||
@ -413,7 +477,7 @@ make test-agent
|
|||||||
make test-cover
|
make test-cover
|
||||||
open build/coverage.html
|
open build/coverage.html
|
||||||
|
|
||||||
# Linting (matches CI)
|
# Linting (matches CI, uses .golangci.yml config)
|
||||||
make lint
|
make lint
|
||||||
# Install golangci-lint if needed:
|
# Install golangci-lint if needed:
|
||||||
# go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
# go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
@ -436,6 +500,9 @@ make clean
|
|||||||
```
|
```
|
||||||
grokkit/
|
grokkit/
|
||||||
├── cmd/ # CLI commands (Cobra)
|
├── cmd/ # CLI commands (Cobra)
|
||||||
|
│ ├── docs.go # grokkit docs — AI documentation generation
|
||||||
|
│ ├── lint.go # grokkit lint — AI-powered linting
|
||||||
|
│ └── ... # chat, edit, commit, review, history, pr-describe, agent
|
||||||
├── config/ # Viper configuration
|
├── config/ # Viper configuration
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
├── todo/ # TODO tracking: queued/ (pending) and completed/ (historical record)
|
├── todo/ # TODO tracking: queued/ (pending) and completed/ (historical record)
|
||||||
@ -451,6 +518,7 @@ grokkit/
|
|||||||
├── main.go # Application entrypoint
|
├── main.go # Application entrypoint
|
||||||
├── go.mod # Dependencies
|
├── go.mod # Dependencies
|
||||||
├── Makefile # Build automation
|
├── Makefile # Build automation
|
||||||
|
├── .golangci.yml # Golangci-lint configuration
|
||||||
└── scripts/ # Install scripts
|
└── scripts/ # Install scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -28,10 +28,7 @@ var commitCmd = &cobra.Command{
|
|||||||
model := config.GetModel("commit", modelFlag)
|
model := config.GetModel("commit", modelFlag)
|
||||||
|
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
messages := []map[string]string{
|
messages := buildCommitMessages(diff)
|
||||||
{"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."},
|
|
||||||
{"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)},
|
|
||||||
}
|
|
||||||
color.Yellow("Generating commit message...")
|
color.Yellow("Generating commit message...")
|
||||||
msg := client.Stream(messages, model)
|
msg := client.Stream(messages, model)
|
||||||
|
|
||||||
@ -54,3 +51,10 @@ var commitCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildCommitMessages(diff string) []map[string]string {
|
||||||
|
return []map[string]string{
|
||||||
|
{"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."},
|
||||||
|
{"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
52
cmd/commit_test.go
Normal file
52
cmd/commit_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildCommitMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
diff string
|
||||||
|
sysCheck string
|
||||||
|
usrCheck []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal diff",
|
||||||
|
diff: "diff --git a/foo.go b/foo.go\n+func bar() {}",
|
||||||
|
sysCheck: "conventional commit",
|
||||||
|
usrCheck: []string{"diff --git a/foo.go", "Staged changes:"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty diff",
|
||||||
|
diff: "",
|
||||||
|
sysCheck: "conventional commit",
|
||||||
|
usrCheck: []string{"Staged changes:"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msgs := buildCommitMessages(tt.diff)
|
||||||
|
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||||
|
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
for _, check := range tt.usrCheck {
|
||||||
|
if !strings.Contains(msgs[1]["content"], check) {
|
||||||
|
t.Errorf("user prompt missing %q", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
185
cmd/docs.go
Normal file
185
cmd/docs.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"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 autoApply bool
|
||||||
|
|
||||||
|
var docsCmd = &cobra.Command{
|
||||||
|
Use: "docs <file> [file...]",
|
||||||
|
Short: "Generate documentation comments for source files",
|
||||||
|
Long: `Detects the programming language of each file and uses Grok AI to generate
|
||||||
|
language-appropriate documentation comments.
|
||||||
|
|
||||||
|
Supported doc styles:
|
||||||
|
Go godoc (// FuncName does...)
|
||||||
|
Python PEP 257 docstrings
|
||||||
|
C/C++ Doxygen (/** @brief ... */)
|
||||||
|
JS/TS JSDoc (/** @param ... */)
|
||||||
|
Rust rustdoc (/// Summary)
|
||||||
|
Ruby YARD (# @param ...)
|
||||||
|
Java Javadoc (/** @param ... */)
|
||||||
|
Shell Shell comments (# function: ...)
|
||||||
|
|
||||||
|
Safety features:
|
||||||
|
- Creates .bak backup before modifying files
|
||||||
|
- Shows preview of changes before applying
|
||||||
|
- Requires confirmation unless --auto-apply is used`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: runDocs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
docsCmd.Flags().BoolVar(&autoApply, "auto-apply", false, "Apply documentation without confirmation")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDocs(cmd *cobra.Command, args []string) {
|
||||||
|
modelFlag, _ := cmd.Flags().GetString("model")
|
||||||
|
model := config.GetModel("docs", modelFlag)
|
||||||
|
|
||||||
|
client := grok.NewClient()
|
||||||
|
for _, filePath := range args {
|
||||||
|
processDocsFile(client, model, filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDocsFile(client *grok.Client, model, filePath string) {
|
||||||
|
logger.Info("starting docs operation", "file", filePath)
|
||||||
|
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
logger.Error("file not found", "file", filePath)
|
||||||
|
color.Red("❌ File not found: %s", filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lang, err := linter.DetectLanguage(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("unsupported language", "file", filePath, "error", err)
|
||||||
|
color.Yellow("⚠️ Skipping %s: %v", filePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
originalContent, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to read file", "file", filePath, "error", err)
|
||||||
|
color.Red("❌ Failed to read file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Cyan("📝 Generating %s docs for: %s", lang.Name, filePath)
|
||||||
|
logger.Info("requesting AI documentation", "file", filePath, "language", lang.Name)
|
||||||
|
|
||||||
|
messages := buildDocsMessages(lang.Name, string(originalContent))
|
||||||
|
response := client.StreamSilent(messages, model)
|
||||||
|
|
||||||
|
if response == "" {
|
||||||
|
logger.Error("AI returned empty response", "file", filePath)
|
||||||
|
color.Red("❌ Failed to get AI response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
documented := grok.CleanCodeResponse(response)
|
||||||
|
logger.Info("received AI documentation", "file", filePath, "size", len(documented))
|
||||||
|
|
||||||
|
// Show preview
|
||||||
|
fmt.Println()
|
||||||
|
color.Cyan("📋 Preview of documented code:")
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
lines := strings.Split(documented, "\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()
|
||||||
|
|
||||||
|
if !autoApply {
|
||||||
|
color.Yellow("Apply documentation to %s? (y/N): ", filePath)
|
||||||
|
var confirm string
|
||||||
|
if _, err := fmt.Scanln(&confirm); err != nil {
|
||||||
|
color.Yellow("❌ Cancelled. No changes made to: %s", filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirm = strings.ToLower(strings.TrimSpace(confirm))
|
||||||
|
if confirm != "y" && confirm != "yes" {
|
||||||
|
logger.Info("user cancelled docs application", "file", filePath)
|
||||||
|
color.Yellow("❌ Cancelled. No changes made to: %s", filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
backupPath := filePath + ".bak"
|
||||||
|
if err := os.WriteFile(backupPath, originalContent, 0644); err != nil {
|
||||||
|
logger.Error("failed to create backup", "file", filePath, "error", err)
|
||||||
|
color.Red("❌ Failed to create backup: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("backup created", "backup", backupPath)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil {
|
||||||
|
logger.Error("failed to write documented file", "file", filePath, "error", err)
|
||||||
|
color.Red("❌ Failed to write file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("documentation applied successfully", "file", filePath)
|
||||||
|
color.Green("✅ Documentation applied: %s", filePath)
|
||||||
|
color.Cyan("💾 Original saved to: %s", backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDocsMessages(language, code string) []map[string]string {
|
||||||
|
style := docStyle(language)
|
||||||
|
systemPrompt := fmt.Sprintf(
|
||||||
|
"You are a documentation expert. Add %s documentation comments to the provided code. "+
|
||||||
|
"Return ONLY the documented code with no explanations, markdown, or extra text. "+
|
||||||
|
"Do NOT include markdown code fences. Document all public functions, methods, types, and constants.",
|
||||||
|
style,
|
||||||
|
)
|
||||||
|
|
||||||
|
userPrompt := fmt.Sprintf("Add documentation comments to the following %s code:\n\n%s", language, code)
|
||||||
|
|
||||||
|
return []map[string]string{
|
||||||
|
{"role": "system", "content": systemPrompt},
|
||||||
|
{"role": "user", "content": userPrompt},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func docStyle(language string) string {
|
||||||
|
switch strings.ToLower(language) {
|
||||||
|
case "go":
|
||||||
|
return "godoc"
|
||||||
|
case "python":
|
||||||
|
return "PEP 257 docstring"
|
||||||
|
case "c", "c++":
|
||||||
|
return "Doxygen"
|
||||||
|
case "javascript", "typescript":
|
||||||
|
return "JSDoc"
|
||||||
|
case "rust":
|
||||||
|
return "rustdoc"
|
||||||
|
case "ruby":
|
||||||
|
return "YARD"
|
||||||
|
case "java":
|
||||||
|
return "Javadoc"
|
||||||
|
case "shell", "bash":
|
||||||
|
return "shell comment"
|
||||||
|
default:
|
||||||
|
return "standard documentation comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
78
cmd/docs_test.go
Normal file
78
cmd/docs_test.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildDocsMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
language string
|
||||||
|
code string
|
||||||
|
styleCheck string
|
||||||
|
}{
|
||||||
|
{language: "Go", code: "package main\nfunc Foo() {}", styleCheck: "godoc"},
|
||||||
|
{language: "Python", code: "def foo():\n pass", styleCheck: "PEP 257"},
|
||||||
|
{language: "C", code: "int foo(void) { return 0; }", styleCheck: "Doxygen"},
|
||||||
|
{language: "C++", code: "int foo() { return 0; }", styleCheck: "Doxygen"},
|
||||||
|
{language: "JavaScript", code: "function foo() {}", styleCheck: "JSDoc"},
|
||||||
|
{language: "TypeScript", code: "function foo(): void {}", styleCheck: "JSDoc"},
|
||||||
|
{language: "Rust", code: "pub fn foo() {}", styleCheck: "rustdoc"},
|
||||||
|
{language: "Ruby", code: "def foo; end", styleCheck: "YARD"},
|
||||||
|
{language: "Java", code: "public void foo() {}", styleCheck: "Javadoc"},
|
||||||
|
{language: "Shell", code: "foo() { echo hello; }", styleCheck: "shell comment"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.language, func(t *testing.T) {
|
||||||
|
msgs := buildDocsMessages(tt.language, tt.code)
|
||||||
|
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(msgs[0]["content"], tt.styleCheck) {
|
||||||
|
t.Errorf("system prompt missing %q for language %s; got: %s",
|
||||||
|
tt.styleCheck, tt.language, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
if !strings.Contains(msgs[1]["content"], tt.code) {
|
||||||
|
t.Errorf("user prompt missing code for language %s", tt.language)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocStyle(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
language string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"go", "godoc"},
|
||||||
|
{"Go", "godoc"},
|
||||||
|
{"python", "PEP 257 docstring"},
|
||||||
|
{"c", "Doxygen"},
|
||||||
|
{"c++", "Doxygen"},
|
||||||
|
{"javascript", "JSDoc"},
|
||||||
|
{"typescript", "JSDoc"},
|
||||||
|
{"rust", "rustdoc"},
|
||||||
|
{"ruby", "YARD"},
|
||||||
|
{"java", "Javadoc"},
|
||||||
|
{"shell", "shell comment"},
|
||||||
|
{"bash", "shell comment"},
|
||||||
|
{"unknown", "standard documentation comment"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.language, func(t *testing.T) {
|
||||||
|
got := docStyle(tt.language)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("docStyle(%q) = %q, want %q", tt.language, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,11 +26,15 @@ var historyCmd = &cobra.Command{
|
|||||||
model := config.GetModel("history", modelFlag)
|
model := config.GetModel("history", modelFlag)
|
||||||
|
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
messages := []map[string]string{
|
messages := buildHistoryMessages(log)
|
||||||
{"role": "system", "content": "Summarize the recent git history in 3-5 bullet points."},
|
|
||||||
{"role": "user", "content": log},
|
|
||||||
}
|
|
||||||
color.Yellow("Summarizing recent commits...")
|
color.Yellow("Summarizing recent commits...")
|
||||||
client.Stream(messages, model)
|
client.Stream(messages, model)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildHistoryMessages(log string) []map[string]string {
|
||||||
|
return []map[string]string{
|
||||||
|
{"role": "system", "content": "Summarize the recent git history in 3-5 bullet points."},
|
||||||
|
{"role": "user", "content": log},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
53
cmd/history_test.go
Normal file
53
cmd/history_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildHistoryMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
log string
|
||||||
|
sysCheck string
|
||||||
|
usrCheck string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with recent commits",
|
||||||
|
log: "abc1234 feat: add new feature\ndef5678 fix: resolve bug",
|
||||||
|
sysCheck: "git history",
|
||||||
|
usrCheck: "abc1234 feat: add new feature",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty log",
|
||||||
|
log: "",
|
||||||
|
sysCheck: "git history",
|
||||||
|
usrCheck: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msgs := buildHistoryMessages(tt.log)
|
||||||
|
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||||
|
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
if tt.usrCheck != "" && !strings.Contains(msgs[1]["content"], tt.usrCheck) {
|
||||||
|
t.Errorf("user content missing %q", tt.usrCheck)
|
||||||
|
}
|
||||||
|
if msgs[1]["content"] != tt.log {
|
||||||
|
t.Errorf("user content = %q, want %q", msgs[1]["content"], tt.log)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
68
cmd/lint_test.go
Normal file
68
cmd/lint_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gmgauthier.com/grokkit/internal/linter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildLintFixMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
result *linter.LintResult
|
||||||
|
code string
|
||||||
|
wantLen int
|
||||||
|
sysCheck string
|
||||||
|
usrCheck []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "go file with issues",
|
||||||
|
result: &linter.LintResult{
|
||||||
|
Language: "Go",
|
||||||
|
LinterUsed: "golangci-lint",
|
||||||
|
Output: "line 5: unused variable x",
|
||||||
|
},
|
||||||
|
code: "package main\nfunc main() { x := 1 }",
|
||||||
|
wantLen: 2,
|
||||||
|
sysCheck: "code quality",
|
||||||
|
usrCheck: []string{"Go", "golangci-lint", "unused variable x", "package main"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "python file with issues",
|
||||||
|
result: &linter.LintResult{
|
||||||
|
Language: "Python",
|
||||||
|
LinterUsed: "flake8",
|
||||||
|
Output: "E501 line too long",
|
||||||
|
},
|
||||||
|
code: "def foo():\n pass",
|
||||||
|
wantLen: 2,
|
||||||
|
sysCheck: "code quality",
|
||||||
|
usrCheck: []string{"Python", "flake8", "E501 line too long", "def foo"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msgs := buildLintFixMessages(tt.result, tt.code)
|
||||||
|
|
||||||
|
if len(msgs) != tt.wantLen {
|
||||||
|
t.Fatalf("expected %d messages, got %d", tt.wantLen, len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||||
|
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
for _, check := range tt.usrCheck {
|
||||||
|
if !strings.Contains(msgs[1]["content"], check) {
|
||||||
|
t.Errorf("user prompt missing %q", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,11 +30,15 @@ var prDescribeCmd = &cobra.Command{
|
|||||||
model := config.GetModel("prdescribe", modelFlag)
|
model := config.GetModel("prdescribe", modelFlag)
|
||||||
|
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
messages := []map[string]string{
|
messages := buildPRDescribeMessages(diff)
|
||||||
{"role": "system", "content": "Write a professional GitHub PR title + detailed body (changes, motivation, testing notes)."},
|
|
||||||
{"role": "user", "content": fmt.Sprintf("Diff:\n%s", diff)},
|
|
||||||
}
|
|
||||||
color.Yellow("Writing PR description...")
|
color.Yellow("Writing PR description...")
|
||||||
client.Stream(messages, model)
|
client.Stream(messages, model)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildPRDescribeMessages(diff string) []map[string]string {
|
||||||
|
return []map[string]string{
|
||||||
|
{"role": "system", "content": "Write a professional GitHub PR title + detailed body (changes, motivation, testing notes)."},
|
||||||
|
{"role": "user", "content": fmt.Sprintf("Diff:\n%s", diff)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
52
cmd/prdescribe_test.go
Normal file
52
cmd/prdescribe_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildPRDescribeMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
diff string
|
||||||
|
sysCheck string
|
||||||
|
usrCheck []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "branch with changes",
|
||||||
|
diff: "diff --git a/cmd/new.go b/cmd/new.go\n+package cmd",
|
||||||
|
sysCheck: "PR",
|
||||||
|
usrCheck: []string{"Diff:", "diff --git a/cmd/new.go"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty diff",
|
||||||
|
diff: "",
|
||||||
|
sysCheck: "PR",
|
||||||
|
usrCheck: []string{"Diff:"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msgs := buildPRDescribeMessages(tt.diff)
|
||||||
|
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(msgs[0]["content"], tt.sysCheck) {
|
||||||
|
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
for _, check := range tt.usrCheck {
|
||||||
|
if !strings.Contains(msgs[1]["content"], check) {
|
||||||
|
t.Errorf("user prompt missing %q", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,11 +29,15 @@ var reviewCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := []map[string]string{
|
messages := buildReviewMessages(status, diff)
|
||||||
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
|
||||||
{"role": "user", "content": fmt.Sprintf("Git status:\n%s\n\nGit diff:\n%s", status, diff)},
|
|
||||||
}
|
|
||||||
color.Yellow("Grok is reviewing the repo...")
|
color.Yellow("Grok is reviewing the repo...")
|
||||||
client.Stream(messages, model)
|
client.Stream(messages, model)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildReviewMessages(status, diff string) []map[string]string {
|
||||||
|
return []map[string]string{
|
||||||
|
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
||||||
|
{"role": "user", "content": fmt.Sprintf("Git status:\n%s\n\nGit diff:\n%s", status, diff)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
55
cmd/review_test.go
Normal file
55
cmd/review_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildReviewMessages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
diff string
|
||||||
|
sysCheck string
|
||||||
|
usrCheck []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with status and diff",
|
||||||
|
status: "M main.go",
|
||||||
|
diff: "diff --git a/main.go b/main.go\n+func foo() {}",
|
||||||
|
sysCheck: "code reviewer",
|
||||||
|
usrCheck: []string{"M main.go", "diff --git a/main.go"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty diff",
|
||||||
|
status: "",
|
||||||
|
diff: "",
|
||||||
|
sysCheck: "code reviewer",
|
||||||
|
usrCheck: []string{"Git status:", "Git diff:"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msgs := buildReviewMessages(tt.status, tt.diff)
|
||||||
|
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if msgs[0]["role"] != "system" {
|
||||||
|
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||||
|
}
|
||||||
|
if msgs[1]["role"] != "user" {
|
||||||
|
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||||
|
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||||
|
}
|
||||||
|
for _, check := range tt.usrCheck {
|
||||||
|
if !strings.Contains(msgs[1]["content"], check) {
|
||||||
|
t.Errorf("user prompt missing %q", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,6 +56,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(agentCmd)
|
rootCmd.AddCommand(agentCmd)
|
||||||
rootCmd.AddCommand(completionCmd)
|
rootCmd.AddCommand(completionCmd)
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
rootCmd.AddCommand(docsCmd)
|
||||||
|
|
||||||
// Add model flag to all commands
|
// Add model flag to all commands
|
||||||
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
||||||
|
|||||||
@ -35,6 +35,7 @@ func Load() {
|
|||||||
viper.SetDefault("commands.lint.model", "grok-4-1-fast-non-reasoning")
|
viper.SetDefault("commands.lint.model", "grok-4-1-fast-non-reasoning")
|
||||||
viper.SetDefault("commands.prdescribe.model", "grok-4")
|
viper.SetDefault("commands.prdescribe.model", "grok-4")
|
||||||
viper.SetDefault("commands.review.model", "grok-4")
|
viper.SetDefault("commands.review.model", "grok-4")
|
||||||
|
viper.SetDefault("commands.docs.model", "grok-4")
|
||||||
|
|
||||||
// Config file is optional, so we ignore read errors
|
// Config file is optional, so we ignore read errors
|
||||||
_ = viper.ReadInConfig()
|
_ = viper.ReadInConfig()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user