2026-03-06 18:35:58 +00:00
package recipe
2026-03-06 20:32:04 +00:00
import (
2026-03-06 21:01:01 +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:01:01 +00:00
// Special case: Apply or patch step is handled by the CLI (like edit/scaffold)
if strings . Contains ( strings . ToLower ( step . Title ) , "apply" ) || strings . Contains ( strings . ToLower ( step . Title ) , "patch" ) {
r . handleApplyStep ( previousResults )
continue
}
// Normal LLM step
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 20:46:30 +00:00
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" ) ,
2026-03-06 20:32:04 +00:00
step . Objective ,
step . Instructions ,
step . Expected )
messages := [ ] map [ string ] string {
2026-03-06 21:01:01 +00:00
{ "role" : "system" , "content" : "You are Grok, built by xAI. You are a precise, expert Go 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:01:01 +00:00
// handleApplyStep parses the refactored code blocks from the previous step and does the real apply/patch with confirmation
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
// Extract all labelled code blocks from the last LLM step (Step 2)
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:01:01 +00:00
// Dry-run or real apply?
dryRun := true // TODO: read from parameters once we add --param support
if dryRun {
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." )
return
}
// Real apply with confirmation (exactly like edit/scaffold)
fmt . Print ( "\nApply these changes to disk? (y/N) " )
scanner := bufio . NewScanner ( os . Stdin )
if scanner . Scan ( ) {
answer := strings . ToLower ( strings . TrimSpace ( scanner . Text ( ) ) )
if answer == "y" || answer == "yes" {
for filePath , content := range blocks {
backup := filePath + ".bak"
if err := os . Rename ( filePath , backup ) ; err == nil {
fmt . Printf ( " 📦 Backed up: %s\n" , backup )
}
if err := os . WriteFile ( filePath , [ ] byte ( content ) , 0644 ) ; err != nil {
fmt . Printf ( " ❌ Failed to write %s: %v\n" , filePath , err )
continue
}
fmt . Printf ( " ✅ Applied: %s\n" , filePath )
}
} else {
fmt . Println ( " ❌ Cancelled — no changes made." )
}
}
}
// Simple parser for the labelled blocks Grok outputs: "// /path/to/file.go\n```go\ncode\n```"
var regStr = "`(?s)//\\s*(.+?\\.go)\\n```go\\n(.*?)\\n````"
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 {
// Very simple unified patch for now — can be improved later with real diff
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
}