2026-03-06 18:45:03 +00:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
"gmgauthier.com/grokkit/config"
|
|
|
|
|
"gmgauthier.com/grokkit/internal/grok"
|
2026-03-06 19:00:16 +00:00
|
|
|
"gmgauthier.com/grokkit/internal/recipe"
|
2026-03-06 18:45:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
2026-03-06 19:00:16 +00:00
|
|
|
rootCmd.AddCommand(recipeCmd)
|
2026-03-06 18:45:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runRecipe(cmd *cobra.Command, args []string) error {
|
|
|
|
|
nameOrPath := args[0]
|
|
|
|
|
|
|
|
|
|
recipePath, err := resolveRecipePath(nameOrPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
// TODO: add --param support later; for now YAML defaults work
|
2026-03-06 18:45:03 +00:00
|
|
|
params := make(map[string]any)
|
|
|
|
|
|
|
|
|
|
r, err := recipe.Load(recipePath, params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load recipe: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
// respect -m/--model flag + config exactly like every other command
|
|
|
|
|
flagModel, _ := cmd.Flags().GetString("model")
|
|
|
|
|
model := config.GetModel("recipe", flagModel)
|
|
|
|
|
|
|
|
|
|
client := grok.NewClient()
|
2026-03-06 18:45:03 +00:00
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
runner := recipe.NewRunner(r, client, model)
|
|
|
|
|
return runner.Run()
|
2026-03-06 18:45:03 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
// resolveRecipePath implements exactly the rules you specified
|
2026-03-06 18:45:03 +00:00
|
|
|
func resolveRecipePath(nameOrPath string) (string, error) {
|
2026-03-06 20:32:04 +00:00
|
|
|
// explicit path wins immediately
|
2026-03-06 18:45:03 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
// normalise to .md
|
2026-03-06 18:45:03 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:32:04 +00:00
|
|
|
// 2. Global XDG fallback with confirmation
|
2026-03-06 18:45:03 +00:00
|
|
|
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
|
2026-03-06 19:00:16 +00:00
|
|
|
if _, err := fmt.Scanln(&answer); err != nil {
|
2026-03-06 18:49:16 +00:00
|
|
|
return "", err
|
|
|
|
|
}
|
2026-03-06 18:45:03 +00:00
|
|
|
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")
|
|
|
|
|
}
|