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