first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+85
View File
@@ -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, ".*+?()[]{}^$\\")
}