Update the regex in runner.go to use string concatenation for better readability. Add test output files as fixtures for test verification.
141 lines
3.6 KiB
Go
141 lines
3.6 KiB
Go
package recipe
|
|
|
|
import (
|
|
_ "bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gmgauthier.com/grokkit/internal/grok"
|
|
)
|
|
|
|
type Runner struct {
|
|
Recipe *Recipe
|
|
Client *grok.Client
|
|
Model string
|
|
}
|
|
|
|
func NewRunner(r *Recipe, client *grok.Client, model string) *Runner {
|
|
return &Runner{Recipe: r, Client: client, Model: model}
|
|
}
|
|
|
|
func (r *Runner) Run() error {
|
|
fmt.Printf("🍳 Starting recipe: %s v%s\n\n", r.Recipe.Name, r.Recipe.Version)
|
|
|
|
var previousResults []string
|
|
|
|
for _, step := range r.Recipe.Steps {
|
|
fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title)
|
|
|
|
// Only special-case the Apply/Patch step (this is the only place the CLI needs to touch disk)
|
|
if strings.Contains(strings.ToLower(step.Title), "apply") || strings.Contains(strings.ToLower(step.Title), "patch") {
|
|
r.handleApplyStep(previousResults)
|
|
continue
|
|
}
|
|
|
|
// Everything else is pure LLM — the recipe defines exactly what to do
|
|
prompt := fmt.Sprintf(`Recipe Overview:
|
|
%s
|
|
|
|
Previous step results (for context):
|
|
%s
|
|
|
|
=== CURRENT STEP ===
|
|
Objective: %s
|
|
Instructions: %s
|
|
Expected output format: %s
|
|
|
|
Execute this step now.`,
|
|
r.Recipe.Overview,
|
|
strings.Join(previousResults, "\n\n---\n\n"),
|
|
step.Objective,
|
|
step.Instructions,
|
|
step.Expected)
|
|
|
|
messages := []map[string]string{
|
|
{"role": "system", "content": "You are Grok, built by xAI. You are a precise, expert programmer and refactoring assistant. Always follow the user's instructions exactly for legitimate coding tasks."},
|
|
{"role": "user", "content": prompt},
|
|
}
|
|
|
|
response := r.Client.Stream(messages, r.Model)
|
|
fmt.Println()
|
|
|
|
previousResults = append(previousResults, fmt.Sprintf("Step %d result:\n%s", step.Number, response))
|
|
}
|
|
|
|
fmt.Println("\n✅ Recipe complete.")
|
|
return nil
|
|
}
|
|
|
|
// handleApplyStep is the ONLY place we touch the filesystem (exactly like edit/scaffold)
|
|
func (r *Runner) handleApplyStep(previousResults []string) {
|
|
if len(previousResults) == 0 {
|
|
fmt.Println(" ⚠️ No previous results to apply — skipping.")
|
|
return
|
|
}
|
|
|
|
lastResult := previousResults[len(previousResults)-1]
|
|
blocks := extractCodeBlocks(lastResult)
|
|
|
|
if len(blocks) == 0 {
|
|
fmt.Println(" ⚠️ No code blocks found to apply — skipping.")
|
|
return
|
|
}
|
|
|
|
// Dry-run by default (we'll wire parameters later)
|
|
fmt.Println(" 📄 Dry-run mode: creating patch file...")
|
|
patchPath := filepath.Join(".", "recipe-refactor.patch")
|
|
if err := createUnifiedPatch(blocks, patchPath); err != nil {
|
|
fmt.Printf(" ❌ Failed to create patch: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf(" ✅ Patch created: %s\n", patchPath)
|
|
fmt.Println(" Review it, then run with dry_run=false to apply.")
|
|
}
|
|
|
|
// Simple regex for the format the recipe asks Grok to return
|
|
var blockRe = regexp.MustCompile(`(?s)//\s*(.+?\.go)\n` + "```" + `go\n(.*?)\n` + "```")
|
|
|
|
func extractCodeBlocks(text string) map[string]string {
|
|
blocks := make(map[string]string)
|
|
matches := blockRe.FindAllStringSubmatch(text, -1)
|
|
for _, m := range matches {
|
|
if len(m) == 3 {
|
|
blocks[m[1]] = m[2]
|
|
}
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
func createUnifiedPatch(blocks map[string]string, patchPath string) error {
|
|
f, err := os.Create(patchPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func(f *os.File) {
|
|
err := f.Close()
|
|
if err != nil {
|
|
_, err := fmt.Fprintf(f, "+%s\n", err.Error())
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}(f)
|
|
|
|
for path, content := range blocks {
|
|
_, err := fmt.Fprintf(f, "--- %s\n+++ %s\n@@ -0,0 +1,%d @@\n", path, path, strings.Count(content, "\n")+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, line := range strings.Split(content, "\n") {
|
|
_, err := fmt.Fprintf(f, "+%s\n", line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|