builtins.go

  1package hooks
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"log/slog"
  8
  9	"mvdan.cc/sh/v3/interp"
 10)
 11
 12// crushGetInput reads a field from the hook context JSON.
 13// Usage: VALUE=$(crush_get_input "field_name")
 14func crushGetInput(ctx context.Context, args []string) error {
 15	hc := interp.HandlerCtx(ctx)
 16
 17	if len(args) != 2 {
 18		fmt.Fprintln(hc.Stderr, "Usage: crush_get_input <field_name>")
 19		return interp.ExitStatus(1)
 20	}
 21
 22	fieldName := args[1]
 23	stdin := hc.Env.Get("_CRUSH_STDIN").Str
 24
 25	var data map[string]any
 26	if err := json.Unmarshal([]byte(stdin), &data); err != nil {
 27		fmt.Fprintf(hc.Stderr, "crush_get_input: failed to parse JSON: %v\n", err)
 28		return interp.ExitStatus(1)
 29	}
 30
 31	if value, ok := data[fieldName]; ok && value != nil {
 32		fmt.Fprint(hc.Stdout, formatJSONValue(value))
 33	}
 34
 35	return nil
 36}
 37
 38// crushGetToolInput reads a tool input parameter from the hook context JSON.
 39// Usage: COMMAND=$(crush_get_tool_input "command")
 40func crushGetToolInput(ctx context.Context, args []string) error {
 41	hc := interp.HandlerCtx(ctx)
 42	if len(args) != 2 {
 43		fmt.Fprintln(hc.Stderr, "Usage: crush_get_tool_input <param_name>")
 44		return interp.ExitStatus(1)
 45	}
 46
 47	paramName := args[1]
 48	stdin := hc.Env.Get("_CRUSH_STDIN").Str
 49
 50	var data map[string]any
 51	if err := json.Unmarshal([]byte(stdin), &data); err != nil {
 52		fmt.Fprintf(hc.Stderr, "crush_get_tool_input: failed to parse JSON: %v\n", err)
 53		return interp.ExitStatus(1)
 54	}
 55
 56	toolInput, ok := data["tool_input"].(map[string]any)
 57	if !ok {
 58		return nil
 59	}
 60
 61	if value, ok := toolInput[paramName]; ok && value != nil {
 62		fmt.Fprint(hc.Stdout, formatJSONValue(value))
 63	}
 64
 65	return nil
 66}
 67
 68// crushGetPrompt reads the user prompt from the hook context JSON.
 69// Usage: PROMPT=$(crush_get_prompt)
 70func crushGetPrompt(ctx context.Context, args []string) error {
 71	hc := interp.HandlerCtx(ctx)
 72
 73	stdin := hc.Env.Get("_CRUSH_STDIN").Str
 74
 75	var data map[string]any
 76	if err := json.Unmarshal([]byte(stdin), &data); err != nil {
 77		fmt.Fprintf(hc.Stderr, "crush_get_prompt: failed to parse JSON: %v\n", err)
 78		return interp.ExitStatus(1)
 79	}
 80
 81	if prompt, ok := data["prompt"]; ok && prompt != nil {
 82		fmt.Fprint(hc.Stdout, formatJSONValue(prompt))
 83	}
 84
 85	return nil
 86}
 87
 88// crushLog writes a log message using slog.Debug.
 89// Usage: crush_log "debug message"
 90func crushLog(ctx context.Context, args []string) error {
 91	if len(args) < 2 {
 92		return nil
 93	}
 94
 95	slog.Debug(joinArgs(args[1:]))
 96	return nil
 97}
 98
 99// formatJSONValue converts a JSON value to a string suitable for shell output.
100func formatJSONValue(value any) string {
101	switch v := value.(type) {
102	case string:
103		return v
104	case float64:
105		// JSON numbers are float64 by default
106		if v == float64(int64(v)) {
107			return fmt.Sprintf("%d", int64(v))
108		}
109		return fmt.Sprintf("%v", v)
110	case bool:
111		return fmt.Sprintf("%t", v)
112	case nil:
113		return ""
114	default:
115		// For complex types (arrays, objects), return JSON representation
116		b, err := json.Marshal(v)
117		if err != nil {
118			return fmt.Sprintf("%v", v)
119		}
120		return string(b)
121	}
122}
123
124// joinArgs joins arguments with spaces.
125func joinArgs(args []string) string {
126	if len(args) == 0 {
127		return ""
128	}
129	result := args[0]
130	for _, arg := range args[1:] {
131		result += " " + arg
132	}
133	return result
134}
135
136// RegisterBuiltins returns an ExecHandlerFunc that registers all Crush hook builtins.
137func RegisterBuiltins(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
138	builtins := map[string]func(context.Context, []string) error{
139		"crush_get_input":      crushGetInput,
140		"crush_get_tool_input": crushGetToolInput,
141		"crush_get_prompt":     crushGetPrompt,
142		"crush_log":            crushLog,
143	}
144
145	return func(ctx context.Context, args []string) error {
146		if len(args) == 0 {
147			return next(ctx, args)
148		}
149
150		if fn, ok := builtins[args[0]]; ok {
151			return fn(ctx, args)
152		}
153
154		return next(ctx, args)
155	}
156}