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}