first commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// This file implements hook matching: given an event type and (for tool
|
||||
// events) a tool name, MatchHooks returns the flat, ordered list of hooks that
|
||||
// should run. Matcher semantics follow Claude Code to keep the learning curve
|
||||
// low: empty or "*" matches all, an exact tool name matches only that tool, a
|
||||
// "|"-separated list matches any listed tool, and anything else is compiled as
|
||||
// a Go regexp against the tool name.
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MatchHooks returns every valid hook under eventType whose matcher matches
|
||||
// toolName, preserving config order (layer order + declaration order within a
|
||||
// layer). For events that do not carry a tool name (toolName == ""), matchers
|
||||
// are ignored and all hooks under the event fire. Invalid hooks (see
|
||||
// HookConfig.Validate) are skipped; a matcher whose regexp fails to compile is
|
||||
// skipped with a warning on warnLog (when non-nil).
|
||||
func (s HookSet) MatchHooks(eventType, toolName string, warnLog io.Writer) []HookConfig {
|
||||
matchers := s[eventType]
|
||||
if len(matchers) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []HookConfig
|
||||
for _, m := range matchers {
|
||||
if !matcherApplies(m.Matcher, toolName, warnLog) {
|
||||
continue
|
||||
}
|
||||
for _, h := range m.Hooks {
|
||||
if err := h.Validate(); err != nil {
|
||||
warnf(warnLog, "pigo: hooks: skipping invalid hook: %v\n", err)
|
||||
continue
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matcherApplies reports whether a matcher pattern matches the given tool name.
|
||||
// An empty tool name (event without a tool) always matches, so tool-agnostic
|
||||
// events fire every hook regardless of matcher.
|
||||
func matcherApplies(pattern, toolName string, warnLog io.Writer) bool {
|
||||
if toolName == "" {
|
||||
return true
|
||||
}
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" || pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// "|"-separated multi-value: any exact tool name matches. This also covers
|
||||
// the single-exact-name case (no "|").
|
||||
parts := strings.Split(pattern, "|")
|
||||
exactCandidate := true
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == toolName {
|
||||
return true
|
||||
}
|
||||
if !isPlainToolName(p) {
|
||||
exactCandidate = false
|
||||
}
|
||||
}
|
||||
// If every alternative was a plain tool name, this was an exact/multi-value
|
||||
// matcher that simply did not match — do not fall through to regexp.
|
||||
if exactCandidate {
|
||||
return false
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
warnf(warnLog, "pigo: hooks: skipping matcher with invalid regexp %q: %v\n", pattern, err)
|
||||
return false
|
||||
}
|
||||
return re.MatchString(toolName)
|
||||
}
|
||||
|
||||
// isPlainToolName reports whether s looks like a literal tool name rather than
|
||||
// a regexp — i.e. it contains no regexp metacharacters. Used to decide whether
|
||||
// a "|"-split alternative should be treated as an exact match or as part of a
|
||||
// regexp alternation.
|
||||
func isPlainToolName(s string) bool {
|
||||
return !strings.ContainsAny(s, ".*+?()[]{}^$\\")
|
||||
}
|
||||
Reference in New Issue
Block a user