feat(cmd): add recipe run command
Implement the `recipe` command with `run` subcommand for executing transactional recipes. - Resolve recipe paths: explicit, project-local (.grokkit/recipes), or global (~/.local/share/grokkit/recipes) with user prompt. - Load and parse recipes using internal/recipe package. - Integrate with root command and project root detection.
This commit is contained in:
parent
6bd72aad25
commit
8e414faa10
120
cmd/recipe.go
Normal file
120
cmd/recipe.go
Normal file
@ -0,0 +1,120 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"repos.gmgauthier.com/gmgauthier/grokkit/internal/recipe" // adjust if your module name differs
|
||||
)
|
||||
|
||||
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) // this is how every other command is wired in your project
|
||||
}
|
||||
|
||||
func runRecipe(cmd *cobra.Command, args []string) error {
|
||||
nameOrPath := args[0]
|
||||
|
||||
// 1. Resolve recipe path (your exact rules)
|
||||
recipePath, err := resolveRecipePath(nameOrPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. For now we pass empty params (we'll add --param later)
|
||||
params := make(map[string]any)
|
||||
|
||||
// 3. Load & parse
|
||||
r, err := recipe.Load(recipePath, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load recipe: %w", err)
|
||||
}
|
||||
|
||||
// 4. Run it
|
||||
runner := recipe.NewRunner(r)
|
||||
if err := runner.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRecipePath implements exactly what you asked for
|
||||
func resolveRecipePath(nameOrPath string) (string, error) {
|
||||
// explicit path?
|
||||
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 name
|
||||
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
|
||||
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
|
||||
fmt.Scanln(&answer)
|
||||
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")
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user