Add support for executing recipe steps via Grok API streaming, including parameter defaults from YAML, model selection via flags/config, previous step context, and a final summary prompt. Update runner to handle client and model, and refine loader to apply user params with fallbacks.
124 lines
3.0 KiB
Go
124 lines
3.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gmgauthier.com/grokkit/config"
|
|
"gmgauthier.com/grokkit/internal/grok"
|
|
"gmgauthier.com/grokkit/internal/recipe"
|
|
)
|
|
|
|
var recipeCmd = &cobra.Command{
|
|
Use: "recipe",
|
|
Short: "Run a recipe (transactional sous-chef mode)",
|
|
}
|
|
|
|
var runCmd = &cobra.Command{
|
|
Use: "run [recipe-name|recipe.md]",
|
|
Short: "Execute a recipe",
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: runRecipe,
|
|
}
|
|
|
|
func init() {
|
|
recipeCmd.AddCommand(runCmd)
|
|
rootCmd.AddCommand(recipeCmd)
|
|
}
|
|
|
|
func runRecipe(cmd *cobra.Command, args []string) error {
|
|
nameOrPath := args[0]
|
|
|
|
recipePath, err := resolveRecipePath(nameOrPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: add --param support later; for now YAML defaults work
|
|
params := make(map[string]any)
|
|
|
|
r, err := recipe.Load(recipePath, params)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load recipe: %w", err)
|
|
}
|
|
|
|
// respect -m/--model flag + config exactly like every other command
|
|
flagModel, _ := cmd.Flags().GetString("model")
|
|
model := config.GetModel("recipe", flagModel)
|
|
|
|
client := grok.NewClient()
|
|
|
|
runner := recipe.NewRunner(r, client, model)
|
|
return runner.Run()
|
|
}
|
|
|
|
// resolveRecipePath implements exactly the rules you specified
|
|
func resolveRecipePath(nameOrPath string) (string, error) {
|
|
// explicit path wins immediately
|
|
if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".md") {
|
|
if _, err := os.Stat(nameOrPath); err == nil {
|
|
return nameOrPath, nil
|
|
}
|
|
return "", fmt.Errorf("recipe file not found: %s", nameOrPath)
|
|
}
|
|
|
|
// normalise to .md
|
|
if !strings.HasSuffix(nameOrPath, ".md") {
|
|
nameOrPath += ".md"
|
|
}
|
|
|
|
// 1. Project-local first (primary source of truth)
|
|
projectRoot, err := findProjectRoot()
|
|
if err == nil {
|
|
local := filepath.Join(projectRoot, ".grokkit", "recipes", nameOrPath)
|
|
if _, err := os.Stat(local); err == nil {
|
|
return local, nil
|
|
}
|
|
}
|
|
|
|
// 2. Global XDG fallback with confirmation
|
|
global := filepath.Join(os.Getenv("HOME"), ".local", "share", "grokkit", "recipes", nameOrPath)
|
|
if _, err := os.Stat(global); err == nil {
|
|
fmt.Printf("Recipe %q not found in project.\nFound globally at %s\nUse this one? [y/N] ", nameOrPath, global)
|
|
var answer string
|
|
if _, err := fmt.Scanln(&answer); err != nil {
|
|
return "", err
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(answer), "y") {
|
|
return global, nil
|
|
}
|
|
return "", fmt.Errorf("user declined global recipe")
|
|
}
|
|
|
|
return "", fmt.Errorf("recipe %q not found in project or global store", nameOrPath)
|
|
}
|
|
|
|
// tiny helper used by almost every command already
|
|
func findProjectRoot() (string, error) {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
|
|
return dir, nil
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir, nil
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, ".grokkit")); err == nil {
|
|
return dir, nil
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
return "", fmt.Errorf("not in a project")
|
|
}
|