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, } var paramFlags []string func init() { runCmd.Flags().StringSliceVarP(¶mFlags, "param", "p", nil, "key=value parameters for the recipe (can be repeated)") 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 } // Parse --param key=value into map params := make(map[string]any) for _, p := range paramFlags { if kv := strings.SplitN(p, "=", 2); len(kv) == 2 { key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) // simple bool detection if value == "true" { params[key] = true } else if value == "false" { params[key] = false } else { params[key] = value } } } r, err := recipe.Load(recipePath, params) if err != nil { return fmt.Errorf("failed to load recipe: %w", err) } flagModel, _ := cmd.Flags().GetString("model") model := config.GetModel("recipe", flagModel) client := grok.NewClient() runner := recipe.NewRunner(r, client, model) return runner.Run() } // resolveRecipePath and findProjectRoot stay exactly as you already have them func resolveRecipePath(nameOrPath string) (string, error) { 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) } if !strings.HasSuffix(nameOrPath, ".md") { nameOrPath += ".md" } projectRoot, err := findProjectRoot() if err == nil { local := filepath.Join(projectRoot, ".grokkit", "recipes", nameOrPath) if _, err := os.Stat(local); err == nil { return local, nil } } 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) } 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, ".grokkit")); err == nil { return dir, nil } if _, err := os.Stat(filepath.Join(dir, ".gitignore")); err == nil { return dir, nil } if _, err := os.Stat(filepath.Join(dir, "pyproject.toml")); err == nil { return dir, nil } if _, err := os.Stat(filepath.Join(dir, "CmakeLists.txt")); err == nil { return dir, nil } if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return "", fmt.Errorf("not in a project") }