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