返回 DeepSeek-Reasonix
hook_machine.go
根目录 / internal / cli / hook_machine.go
1 package cli
2
3 import (
4 "fmt"
5 "io"
6 "os"
7 "sort"
8 "strings"
9
10 "reasonix/internal/hook"
11 )
12
13 type machineHook struct {
14 Event string `json:"event"`
15 Match string `json:"match,omitempty"`
16 Scope string `json:"scope"`
17 Status string `json:"status"`
18 }
19
20 type machineHookSource struct {
21 Scope string `json:"scope"`
22 Status string `json:"status"`
23 HookCount int `json:"hook_count"`
24 }
25
26 type machineHookList struct {
27 SchemaVersion int `json:"schema_version"`
28 Command string `json:"command"`
29 Hooks []machineHook `json:"hooks"`
30 }
31
32 type machineHookStatus struct {
33 SchemaVersion int `json:"schema_version"`
34 Command string `json:"command"`
35 TrustedProject bool `json:"trusted_project"`
36 ProjectDefines bool `json:"project_defines"`
37 Sources []machineHookSource `json:"sources"`
38 }
39
40 type hookMachineOptions struct {
41 projectRoot string
42 homeDir string
43 json bool
44 }
45
46 func hookCommand(args []string) int {
47 return runHookCommand(args, os.Stdout)
48 }
49
50 func runHookCommand(args []string, out io.Writer) int {
51 command := "hook"
52 if len(args) == 0 {
53 return writeMachineError(out, command, "invalid_argument", "a hook operation is required")
54 }
55 operation := args[0]
56 command = "hook." + operation
57 if operation != "list" && operation != "status" {
58 return writeMachineError(out, command, "unknown_command", fmt.Sprintf("unknown hook operation %q; expected list or status", operation))
59 }
60 options, code, message := parseHookMachineOptions(args[1:])
61 if code != "" {
62 return writeMachineError(out, command, code, message)
63 }
64 if !options.json {
65 return writeMachineError(out, command, "invalid_argument", "--json is required")
66 }
67 if options.projectRoot == "" {
68 options.projectRoot, _ = os.Getwd()
69 }
70 // hook.Inspect's HomeDir is an OS user home directory: a non-empty value
71 // has .reasonix appended inside the hook package, and passing
72 // config.ReasonixHomeDir() here double-appends it. Leave it empty so the
73 // hook package resolves the platform Reasonix home itself (correct on
74 // every OS, including Windows where the home is %AppData%\Roaming\reasonix
75 // rather than ~/.reasonix) (#7420).
76 inspection := hook.Inspect(hook.LoadOptions{
77 ProjectRoot: options.projectRoot,
78 HomeDir: options.homeDir,
79 })
80 if operation == "list" {
81 items := make([]machineHook, 0, len(inspection.Entries))
82 for _, entry := range inspection.Entries {
83 status := machineHookEntryStatus(entry)
84 match := entry.Match
85 if match == "" {
86 match = "*"
87 }
88 items = append(items, machineHook{Event: string(entry.Event), Match: match, Scope: string(entry.Scope), Status: status})
89 }
90 sort.SliceStable(items, func(i, j int) bool {
91 if items[i].Event != items[j].Event {
92 return items[i].Event < items[j].Event
93 }
94 if items[i].Scope != items[j].Scope {
95 return items[i].Scope < items[j].Scope
96 }
97 return items[i].Match < items[j].Match
98 })
99 return writeMachineJSON(out, machineHookList{SchemaVersion: machineSchemaVersion, Command: command, Hooks: items})
100 }
101 sources := make([]machineHookSource, 0, len(inspection.Sources))
102 for _, source := range inspection.Sources {
103 sources = append(sources, machineHookSource{Scope: string(source.Scope), Status: source.Status, HookCount: source.HookCount})
104 }
105 sort.SliceStable(sources, func(i, j int) bool { return sources[i].Scope < sources[j].Scope })
106 return writeMachineJSON(out, machineHookStatus{
107 SchemaVersion: machineSchemaVersion,
108 Command: command,
109 TrustedProject: inspection.TrustedProject,
110 ProjectDefines: inspection.ProjectDefines,
111 Sources: sources,
112 })
113 }
114
115 func machineHookEntryStatus(entry hook.Entry) string {
116 if !hook.IsKnownEvent(string(entry.Event)) {
117 return "invalid"
118 }
119 command := strings.TrimSpace(entry.Command)
120 contextFile := strings.TrimSpace(entry.ContextFile)
121 if entry.Scope == hook.ScopePlugin {
122 if command == "" && contextFile == "" {
123 return "invalid"
124 }
125 if contextFile != "" && !hook.ContextFileUsable(contextFile) {
126 return "invalid"
127 }
128 } else if command == "" {
129 // Native project/global loading requires a command; contextFile is an
130 // internal plugin-only execution path.
131 return "invalid"
132 }
133 if hook.UsesToolMatcher(entry.Event) && hook.ValidateMatcher(entry.Match) != "" {
134 return "invalid"
135 }
136 return "active"
137 }
138
139 func parseHookMachineOptions(args []string) (hookMachineOptions, string, string) {
140 var options hookMachineOptions
141 for i := 0; i < len(args); i++ {
142 switch args[i] {
143 case "--json":
144 options.json = true
145 case "--project-root", "--dir":
146 if i+1 >= len(args) || strings.TrimSpace(args[i+1]) == "" {
147 return options, "invalid_argument", "project root requires a value"
148 }
149 i++
150 options.projectRoot = args[i]
151 case "--home-dir":
152 if i+1 >= len(args) || strings.TrimSpace(args[i+1]) == "" {
153 return options, "invalid_argument", "home directory requires a value"
154 }
155 i++
156 options.homeDir = args[i]
157 case "--help", "-h":
158 return options, "invalid_argument", "use the documented machine interface"
159 default:
160 return options, "invalid_argument", "unknown hook option"
161 }
162 }
163 return options, "", ""
164 }
165
165 lines GO