返回 DeepSeek-Reasonix
hooks_settings_app.go
根目录 / desktop / hooks_settings_app.go
1 package main
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9
10 fileencoding "reasonix/internal/fileutil/encoding"
11 "reasonix/internal/hook"
12 )
13
14 type HookConfigView struct {
15 Event string `json:"event"`
16 Match string `json:"match,omitempty"`
17 Command string `json:"command"`
18 Description string `json:"description,omitempty"`
19 Timeout int `json:"timeout,omitempty"`
20 Cwd string `json:"cwd,omitempty"`
21 }
22
23 type HooksSettingsView struct {
24 Scope string `json:"scope"`
25 Path string `json:"path"`
26 ProjectRoot string `json:"projectRoot"`
27 Trusted bool `json:"trusted"`
28 Hooks []HookConfigView `json:"hooks"`
29 Events []string `json:"events"`
30 }
31
32 func (a *App) HooksSettings(scope string) HooksSettingsView {
33 s, path, root := normalizeHooksScope(scope, a.activeHookProjectRoot())
34 view := HooksSettingsView{
35 Scope: s,
36 Path: path,
37 ProjectRoot: root,
38 // Retained for older Wails clients. Both scopes are enabled by default.
39 Trusted: true,
40 Hooks: []HookConfigView{},
41 Events: hookEventNames(),
42 }
43 settings, err := readHooksSettingsFile(path)
44 if err != nil || settings.Hooks == nil {
45 return view
46 }
47 for _, event := range hook.Events {
48 for _, cfg := range settings.Hooks[event] {
49 if strings.TrimSpace(cfg.Command) == "" {
50 continue
51 }
52 view.Hooks = append(view.Hooks, hookConfigView(event, cfg))
53 }
54 }
55 return view
56 }
57
58 func (a *App) SaveHooksSettings(scope string, hooks []HookConfigView) error {
59 return a.SaveHooksSettingsForRoot(scope, a.activeHookProjectRoot(), hooks)
60 }
61
62 func (a *App) SaveHooksSettingsForRoot(scope, projectRoot string, hooks []HookConfigView) error {
63 s, path, _ := normalizeHooksScope(scope, projectRoot)
64 settings := hook.Settings{Hooks: map[hook.Event][]hook.HookConfig{}}
65 for _, h := range hooks {
66 event := hook.Event(strings.TrimSpace(h.Event))
67 if !validHookEvent(event) {
68 return fmt.Errorf("unknown hook event %q", h.Event)
69 }
70 cmd := strings.TrimSpace(h.Command)
71 if cmd == "" {
72 continue
73 }
74 cmd = hook.NormalizeCommand(cmd)
75 settings.Hooks[event] = append(settings.Hooks[event], hook.HookConfig{
76 Match: strings.TrimSpace(h.Match),
77 Command: cmd,
78 Description: strings.TrimSpace(h.Description),
79 Timeout: h.Timeout,
80 Cwd: strings.TrimSpace(h.Cwd),
81 })
82 }
83 if s == string(hook.ScopeProject) && strings.TrimSpace(path) == "" {
84 return fmt.Errorf("no active project workspace")
85 }
86 return writeHooksSettingsFile(path, settings)
87 }
88
89 func (a *App) TrustProjectHooks() error {
90 // Retained for older generated Wails clients. Project hooks are enabled by
91 // default, so there is no trust state to mutate.
92 return nil
93 }
94
95 func (a *App) TrustProjectHooksForRoot(root string) error {
96 // Retained for older generated Wails clients. Project hooks are enabled by
97 // default, so there is no trust state to mutate.
98 return nil
99 }
100
101 func (a *App) activeHookProjectRoot() string {
102 a.mu.RLock()
103 defer a.mu.RUnlock()
104 if tab := a.activeTabLocked(); tab != nil && tab.Scope == "project" {
105 return strings.TrimSpace(tab.WorkspaceRoot)
106 }
107 return ""
108 }
109
110 func normalizeHooksScope(scope, projectRoot string) (string, string, string) {
111 if strings.EqualFold(strings.TrimSpace(scope), string(hook.ScopeProject)) {
112 root := strings.TrimSpace(projectRoot)
113 if root == "" {
114 return string(hook.ScopeProject), "", ""
115 }
116 return string(hook.ScopeProject), hook.ProjectSettingsPath(root), root
117 }
118 return string(hook.ScopeGlobal), hook.GlobalSettingsPath(""), ""
119 }
120
121 func hookEventNames() []string {
122 out := make([]string, 0, len(hook.Events))
123 for _, event := range hook.Events {
124 out = append(out, string(event))
125 }
126 return out
127 }
128
129 func validHookEvent(event hook.Event) bool {
130 for _, e := range hook.Events {
131 if event == e {
132 return true
133 }
134 }
135 return false
136 }
137
138 func hookConfigView(event hook.Event, cfg hook.HookConfig) HookConfigView {
139 return HookConfigView{
140 Event: string(event),
141 Match: cfg.Match,
142 Command: cfg.Command,
143 Description: cfg.Description,
144 Timeout: cfg.Timeout,
145 Cwd: cfg.Cwd,
146 }
147 }
148
149 func readHooksSettingsFile(path string) (hook.Settings, error) {
150 var settings hook.Settings
151 body, err := fileencoding.ReadFileUTF8(path)
152 if err != nil {
153 return settings, err
154 }
155 if err := json.Unmarshal(body, &settings); err != nil {
156 return settings, err
157 }
158 if settings.Hooks == nil {
159 settings.Hooks = map[hook.Event][]hook.HookConfig{}
160 }
161 return settings, nil
162 }
163
164 func writeHooksSettingsFile(path string, settings hook.Settings) error {
165 if strings.TrimSpace(path) == "" {
166 return fmt.Errorf("empty hooks settings path")
167 }
168 raw := map[string]json.RawMessage{}
169 if body, err := fileencoding.ReadFileUTF8(path); err == nil {
170 if err := json.Unmarshal(body, &raw); err != nil {
171 return err
172 }
173 }
174 hooksJSON, err := json.Marshal(settings.Hooks)
175 if err != nil {
176 return err
177 }
178 raw["hooks"] = hooksJSON
179 body, err := json.MarshalIndent(raw, "", " ")
180 if err != nil {
181 return err
182 }
183 body = append(body, '\n')
184 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
185 return err
186 }
187 return os.WriteFile(path, body, 0o644)
188 }
189
189 lines GO