Introduce ResolvedParams field to Recipe struct for storing resolved parameter values from defaults and user overrides. Update loader to populate and use it for template rendering. Adjust runner to use ResolvedParams for root path and generalize file discovery.
234 lines
5.8 KiB
Go
234 lines
5.8 KiB
Go
package recipe
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"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
|
|
var refactorJSONs []string
|
|
|
|
for _, step := range r.Recipe.Steps {
|
|
fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title)
|
|
|
|
titleLower := strings.ToLower(step.Title)
|
|
|
|
switch {
|
|
case strings.Contains(titleLower, "discover") || strings.Contains(titleLower, "find"):
|
|
files := r.discoverFiles()
|
|
result := strings.Join(files, "\n")
|
|
previousResults = append(previousResults, "Discovered files:\n"+result)
|
|
fmt.Println(result)
|
|
|
|
case strings.Contains(titleLower, "refactor"):
|
|
r.refactorFiles(previousResults, &refactorJSONs)
|
|
continue
|
|
|
|
case strings.Contains(titleLower, "apply") || strings.Contains(titleLower, "patch"):
|
|
r.handleApplyStep(refactorJSONs)
|
|
continue
|
|
|
|
default:
|
|
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. Respond ONLY with the expected output format — no explanations, no extra text.`,
|
|
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. Precise expert programmer and refactoring assistant."},
|
|
{"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
|
|
}
|
|
|
|
// discoverFiles — now fully generic using recipe metadata
|
|
func (r *Runner) discoverFiles() []string {
|
|
var files []string
|
|
|
|
// Get root from --param or default
|
|
root := "."
|
|
if v, ok := r.Recipe.ResolvedParams["package_path"]; ok {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
root = s
|
|
}
|
|
}
|
|
|
|
// Build allowed extensions from recipe frontmatter
|
|
allowedExt := make(map[string]bool)
|
|
for _, lang := range r.Recipe.ProjectLanguages {
|
|
if exts, ok := r.Recipe.Extensions[lang]; ok {
|
|
for _, ext := range exts {
|
|
allowedExt[ext] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
if allowedExt[filepath.Ext(path)] {
|
|
b, _ := os.ReadFile(path)
|
|
if strings.Contains(string(b), "if err != nil") {
|
|
files = append(files, path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if len(files) == 0 {
|
|
files = append(files, "No files found matching the criteria.")
|
|
}
|
|
return files
|
|
}
|
|
|
|
// refactorFiles — one file at a time
|
|
func (r *Runner) refactorFiles(previousResults []string, refactorJSONs *[]string) {
|
|
discoveredLine := previousResults[len(previousResults)-1]
|
|
lines := strings.Split(discoveredLine, "\n")
|
|
|
|
for _, line := range lines {
|
|
filePath := strings.TrimSpace(line)
|
|
if filePath == "" || strings.HasPrefix(filePath, "Discovered") || filePath == "No files found matching the criteria." {
|
|
continue
|
|
}
|
|
|
|
fmt.Printf(" Refactoring %s...\n", filePath)
|
|
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
fmt.Printf(" ❌ Could not read %s\n", filePath)
|
|
continue
|
|
}
|
|
|
|
prompt := fmt.Sprintf(`Refactor the following file to use Result[T] instead of naked errors.
|
|
Follow existing style and preserve all comments.
|
|
Return ONLY this exact JSON (no extra text, no markdown):
|
|
|
|
{
|
|
"file": "%s",
|
|
"content": "the complete refactored file here"
|
|
}
|
|
|
|
Original file:
|
|
%s`, filePath, string(content))
|
|
|
|
messages := []map[string]string{
|
|
{"role": "system", "content": "You are Grok, built by xAI. Precise expert programmer and refactoring assistant."},
|
|
{"role": "user", "content": prompt},
|
|
}
|
|
|
|
response := r.Client.Stream(messages, r.Model)
|
|
fmt.Println()
|
|
|
|
*refactorJSONs = append(*refactorJSONs, response)
|
|
}
|
|
}
|
|
|
|
type FileChange struct {
|
|
File string `json:"file"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func (r *Runner) handleApplyStep(refactorJSONs []string) {
|
|
if len(refactorJSONs) == 0 {
|
|
fmt.Println(" ⚠️ No refactored files to apply — skipping.")
|
|
return
|
|
}
|
|
|
|
var allChanges []FileChange
|
|
for _, jsonStr := range refactorJSONs {
|
|
start := strings.Index(jsonStr, "{")
|
|
end := strings.LastIndex(jsonStr, "}") + 1
|
|
if start == -1 {
|
|
continue
|
|
}
|
|
|
|
var ch FileChange
|
|
if err := json.Unmarshal([]byte(jsonStr[start:end]), &ch); err == nil && ch.File != "" {
|
|
allChanges = append(allChanges, ch)
|
|
}
|
|
}
|
|
|
|
if len(allChanges) == 0 {
|
|
fmt.Println(" ⚠️ No valid file changes found — skipping.")
|
|
return
|
|
}
|
|
|
|
fmt.Println(" 📄 Dry-run mode: creating patch file...")
|
|
patchPath := filepath.Join(".", "recipe-refactor.patch")
|
|
if err := createUnifiedPatch(allChanges, 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.")
|
|
}
|
|
|
|
func createUnifiedPatch(changes []FileChange, patchPath string) error {
|
|
f, err := os.Create(patchPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func(f *os.File) {
|
|
err := f.Close()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}(f)
|
|
|
|
for _, ch := range changes {
|
|
_, err := fmt.Fprintf(f, "--- %s\n+++ %s\n@@ -0,0 +1,%d @@\n", ch.File, ch.File, strings.Count(ch.Content, "\n")+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, line := range strings.Split(ch.Content, "\n") {
|
|
_, err := fmt.Fprintf(f, "+%s\n", line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|