File size: 3,209 Bytes
e089492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package shellconfig

import (
	"context"
	"fmt"
	"io"
	"log/slog"
)

// handleHook implements the `hook` builtin.
//
// Usage:
//
//	hook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]
//	hook remove <event> [--name NAME]   (alias: rm)
//
// "add" appends a hook to the event's list; multiple hooks per event
// accumulate. "remove" drops the named hook(s) from the event, or clears the
// whole event when no --name is given. Only named hooks can be removed
// individually; give a hook a --name if you intend to remove it later.
func handleHook(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
	b := configBuilderFromCtx(ctx)
	if b == nil {
		return nil
	}
	if len(args) < 2 {
		return usage(stderr, "usage: hook add <event> --command CMD [flags] | hook remove <event> [--name NAME]")
	}

	switch args[1] {
	case "add":
		return hookAdd(b, args, stderr)
	case "remove", "rm":
		return hookRemove(b, args, stderr)
	default:
		return usage(stderr, fmt.Sprintf("hook: unknown subcommand %q (expected add or remove)", args[1]))
	}
}

// hookAddFlags is the declarative flag surface for `hook add`.
var hookAddFlags = []flagSpec{
	{name: "--command", jsonKey: "command", kind: flagString, op: opSet},
	{name: "--matcher", jsonKey: "matcher", kind: flagString, op: opSet},
	{name: "--timeout", jsonKey: "timeout", kind: flagInt, op: opSet},
	{name: "--name", jsonKey: "name", kind: flagString, op: opSet},
}

func hookAdd(b *ConfigBuilder, args []string, stderr io.Writer) error {
	if len(args) < 3 {
		return usage(stderr, "usage: hook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]")
	}
	event := args[2]
	slog.Info("Hook defined in shell config", "event", event)
	h := map[string]any{}

	if err := applyFlags(hookAddFlags, args, 3, h, "hook add", stderr); err != nil {
		return err
	}

	if _, ok := h["command"]; !ok {
		return usage(stderr, "hook add: --command is required")
	}

	hooks := b.section("hooks")
	arr, _ := hooks[event].([]any)
	hooks[event] = append(arr, h)

	slog.Debug("Hook recorded", "event", event)
	return nil
}

// hookRemoveFlags is the declarative flag surface for `hook remove`.
var hookRemoveFlags = []flagSpec{
	{name: "--name", jsonKey: "name", kind: flagString, op: opSet},
}

func hookRemove(b *ConfigBuilder, args []string, stderr io.Writer) error {
	if len(args) < 3 {
		return usage(stderr, "usage: hook remove <event> [--name NAME]")
	}
	event := args[2]

	flags := map[string]any{}
	if err := applyFlags(hookRemoveFlags, args, 3, flags, "hook remove", stderr); err != nil {
		return err
	}
	name, _ := flags["name"].(string)

	hooks := b.section("hooks")

	// No name: clear every hook for the event.
	if name == "" {
		delete(hooks, event)
		slog.Info("Hooks cleared in shell config", "event", event)
		return nil
	}

	// Name given: drop matching hooks, keeping the rest.
	arr, _ := hooks[event].([]any)
	kept := make([]any, 0, len(arr))
	for _, item := range arr {
		if m, ok := item.(map[string]any); ok && m["name"] == name {
			continue
		}
		kept = append(kept, item)
	}
	hooks[event] = kept

	slog.Info("Hook removed in shell config", "event", event, "name", name)
	return nil
}