2026-03-06 18:35:58 +00:00
package recipe
2026-03-06 20:32:04 +00:00
import (
2026-03-06 21:36:31 +00:00
_ "bufio"
2026-03-06 20:32:04 +00:00
"fmt"
2026-03-06 21:01:01 +00:00
"os"
"path/filepath"
"regexp"
2026-03-06 20:32:04 +00:00
"strings"
"gmgauthier.com/grokkit/internal/grok"
)
2026-03-06 18:35:58 +00:00
type Runner struct {
Recipe * Recipe
2026-03-06 20:32:04 +00:00
Client * grok . Client
Model string
2026-03-06 18:35:58 +00:00
}
2026-03-06 20:32:04 +00:00
func NewRunner ( r * Recipe , client * grok . Client , model string ) * Runner {
return & Runner { Recipe : r , Client : client , Model : model }
2026-03-06 18:35:58 +00:00
}
func ( r * Runner ) Run ( ) error {
2026-03-06 19:00:16 +00:00
fmt . Printf ( "🍳 Starting recipe: %s v%s\n\n" , r . Recipe . Name , r . Recipe . Version )
2026-03-06 20:32:04 +00:00
var previousResults [ ] string
2026-03-06 18:35:58 +00:00
for _ , step := range r . Recipe . Steps {
fmt . Printf ( "Step %d/%d: %s\n" , step . Number , len ( r . Recipe . Steps ) , step . Title )
2026-03-06 20:32:04 +00:00
2026-03-06 21:36:31 +00:00
// Only special-case the Apply/Patch step (this is the only place the CLI needs to touch disk)
2026-03-06 21:01:01 +00:00
if strings . Contains ( strings . ToLower ( step . Title ) , "apply" ) || strings . Contains ( strings . ToLower ( step . Title ) , "patch" ) {
r . handleApplyStep ( previousResults )
continue
}
2026-03-06 21:36:31 +00:00
// Everything else is pure LLM — the recipe defines exactly what to do
2026-03-06 20:46:30 +00:00
prompt := fmt . Sprintf ( ` Recipe Overview :
2026-03-06 20:32:04 +00:00
% s
Previous step results ( for context ) :
% s
== = CURRENT STEP == =
Objective : % s
Instructions : % s
Expected output format : % s
2026-03-06 21:36:31 +00:00
Execute this step now . ` ,
2026-03-06 20:46:30 +00:00
r . Recipe . Overview ,
strings . Join ( previousResults , "\n\n---\n\n" ) ,
2026-03-06 20:32:04 +00:00
step . Objective ,
step . Instructions ,
step . Expected )
messages := [ ] map [ string ] string {
2026-03-06 21:36:31 +00:00
{ "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." } ,
2026-03-06 20:32:04 +00:00
{ "role" : "user" , "content" : prompt } ,
}
response := r . Client . Stream ( messages , r . Model )
2026-03-06 20:46:30 +00:00
fmt . Println ( )
2026-03-06 20:32:04 +00:00
previousResults = append ( previousResults , fmt . Sprintf ( "Step %d result:\n%s" , step . Number , response ) )
}
2026-03-06 21:01:01 +00:00
fmt . Println ( "\n✅ Recipe complete." )
return nil
}
2026-03-06 20:32:04 +00:00
2026-03-06 21:36:31 +00:00
// handleApplyStep is the ONLY place we touch the filesystem (exactly like edit/scaffold)
2026-03-06 21:01:01 +00:00
func ( r * Runner ) handleApplyStep ( previousResults [ ] string ) {
if len ( previousResults ) == 0 {
fmt . Println ( " ⚠️ No previous results to apply — skipping." )
return
}
2026-03-06 20:32:04 +00:00
2026-03-06 21:01:01 +00:00
lastResult := previousResults [ len ( previousResults ) - 1 ]
blocks := extractCodeBlocks ( lastResult )
2026-03-06 20:32:04 +00:00
2026-03-06 21:01:01 +00:00
if len ( blocks ) == 0 {
fmt . Println ( " ⚠️ No code blocks found to apply — skipping." )
return
2026-03-06 18:35:58 +00:00
}
2026-03-06 19:00:16 +00:00
2026-03-06 21:36:31 +00:00
// 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 )
2026-03-06 21:01:01 +00:00
return
}
2026-03-06 21:36:31 +00:00
fmt . Printf ( " ✅ Patch created: %s\n" , patchPath )
fmt . Println ( " Review it, then run with dry_run=false to apply." )
2026-03-06 21:01:01 +00:00
}
2026-03-06 21:36:31 +00:00
// Simple regex for the format the recipe asks Grok to return
var regStr = "`(?s)^//\\s*(.+?\\.go)\\n```go\\n(.*?)\\n````"
2026-03-06 21:01:01 +00:00
var blockRe = regexp . MustCompile ( regStr )
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
}
}
}
2026-03-06 18:35:58 +00:00
return nil
}