返回 DeepSeek-Reasonix
shell_support.go
根目录 / desktop / shell_support.go
1 package main
2
3 import (
4 "fmt"
5 "runtime"
6 "strings"
7
8 "reasonix/internal/config"
9 "reasonix/internal/control"
10 "reasonix/internal/sandbox"
11 )
12
13 // Shell support discovery and repair guidance. The Git.Git winget manifest may
14 // elevate even with user scope, so Reasonix never launches that installer and
15 // Windows exposes only the official manual link.
16
17 // shellInstallActionGitForWindows is the single install action id hosts may
18 // request today; unknown ids are rejected as errors rather than no-ops so a
19 // frontend typo cannot silently do nothing.
20 const shellInstallActionGitForWindows = "git-for-windows"
21
22 // GitForWindowsManualURL is the official download page handed to Windows users.
23 const GitForWindowsManualURL = "https://git-scm.com/download/win"
24
25 // Structured outcomes retained by the desktop bridge contract. Invalid action ids remain
26 // errors; supported Windows requests always return manual_required.
27 const (
28 shellInstallStatusManualRequired = "manual_required"
29 shellInstallStatusUnsupported = "unsupported_platform"
30 )
31
32 // ShellInstallResult is the structured outcome of InstallShellSupport.
33 type ShellInstallResult struct {
34 Status string `json:"status"`
35 Path string `json:"path,omitempty"`
36 Reason string `json:"reason,omitempty"`
37 ManualURL string `json:"manualUrl,omitempty"`
38 }
39
40 // ShellCapabilityView is one discovered interpreter for the settings surface:
41 // whether it is usable, where it lives, how it was found, and why not when
42 // unavailable. Purely informational — resolution goes through ResolveShell.
43 type ShellCapabilityView struct {
44 ID string `json:"id"`
45 Variant string `json:"variant,omitempty"`
46 Available bool `json:"available"`
47 Path string `json:"path,omitempty"`
48 Source string `json:"source,omitempty"`
49 Reason string `json:"reason,omitempty"`
50 }
51
52 // ShellInstallActionView exposes manual Git for Windows repair guidance.
53 // It never authorizes an automatic installer launch.
54 type ShellInstallActionView struct {
55 ID string `json:"id"`
56 Mode string `json:"mode"`
57 Available bool `json:"available"`
58 ManualURL string `json:"manualUrl,omitempty"`
59 }
60
61 // SandboxView is the Settings panel's sandbox surface. The shell fields
62 // separate three states: the configured preference (Shell), what the live
63 // controller bound (EffectiveShell), and what a reload would pick now
64 // (ResolvedShell) — ShellReloadRequired marks the divergence.
65 type SandboxView struct {
66 Bash string `json:"bash"`
67 Network bool `json:"network"`
68 WorkspaceRoot string `json:"workspaceRoot"`
69 AllowWrite []string `json:"allowWrite"`
70 EffectiveWorkspaceRoot string `json:"effectiveWorkspaceRoot"`
71 EffectiveWriteRoots []string `json:"effectiveWriteRoots"`
72 Shell string `json:"shell"` // [tools.shell] prefer: auto|bash|powershell|pwsh
73 EffectiveShell string `json:"effectiveShell,omitempty"`
74 ResolvedShell string `json:"resolvedShell,omitempty"`
75 ShellReloadRequired bool `json:"shellReloadRequired"`
76 ShellCapabilities []ShellCapabilityView `json:"shellCapabilities"`
77 GitCapability *ShellCapabilityView `json:"gitCapability,omitempty"`
78 ShellInstallAction *ShellInstallActionView `json:"shellInstallAction,omitempty"`
79 ShellRepairGuidance *RepairGuidanceView `json:"shellRepairGuidance,omitempty"`
80 GitRepairGuidance *RepairGuidanceView `json:"gitRepairGuidance,omitempty"`
81 }
82
83 // InstallShellSupport retains the existing bridge method while enforcing the
84 // manual-only policy. It never probes for or launches a package manager.
85 func (a *App) InstallShellSupport(id string) (ShellInstallResult, error) {
86 return installShellSupportForGOOS(runtime.GOOS, id)
87 }
88
89 func installShellSupportForGOOS(goos, id string) (ShellInstallResult, error) {
90 if strings.TrimSpace(id) != shellInstallActionGitForWindows {
91 return ShellInstallResult{}, fmt.Errorf("unknown shell support action %q", id)
92 }
93 if goos != "windows" {
94 return ShellInstallResult{
95 Status: shellInstallStatusUnsupported,
96 Reason: "shell helper install is only available on Windows",
97 }, nil
98 }
99 return ShellInstallResult{
100 Status: shellInstallStatusManualRequired,
101 Reason: "automatic installation is disabled because Git for Windows cannot reliably honor user scope",
102 ManualURL: GitForWindowsManualURL,
103 }, nil
104 }
105
106 // CancelShellInstall is retained as an idempotent compatibility no-op. No
107 // installer can be running under the manual-only policy.
108 func (a *App) CancelShellInstall() {}
109
110 // SetShellPreference updates only [tools.shell] prefer, preserving the
111 // configured shell path and every other sandbox field, so switching the
112 // interpreter never rewrites unrelated settings.
113 func (a *App) SetShellPreference(prefer string) error {
114 prefer = strings.TrimSpace(prefer)
115 switch strings.ToLower(prefer) {
116 case "", "auto", "bash", "powershell", "pwsh":
117 default:
118 return fmt.Errorf("invalid shell preference %q (use auto, bash, powershell, or pwsh)", prefer)
119 }
120 return a.applyConfigChange(func(c *config.Config) error {
121 c.Tools.Shell.Prefer = prefer
122 return nil
123 })
124 }
125
126 // shellInstallActionViewForGOOS exposes the official manual link on Windows.
127 // Other platforms use their native detect-and-guide policy.
128 func shellInstallActionViewForGOOS(goos string) *ShellInstallActionView {
129 if goos != "windows" {
130 return nil
131 }
132 return &ShellInstallActionView{
133 ID: shellInstallActionGitForWindows,
134 Mode: "manual",
135 Available: false,
136 ManualURL: GitForWindowsManualURL,
137 }
138 }
139
140 // sandboxViewFor builds the Settings panel's SandboxView shell surface.
141 // effectiveShell is what the live controller actually bound at build time;
142 // resolvedShell is what a reload would pick from the current config and
143 // machine state. They can diverge after a manual repair or an unreloaded config
144 // edit, so the surface shows both plus an explicit reload button.
145 func (a *App) sandboxViewFor(cfg *config.Config, ctrl control.SessionAPI, writeRoots []string, effectiveWorkspaceRoot string) SandboxView {
146 shell := cfg.Tools.Shell.Prefer
147 if shell == "" {
148 shell = "auto"
149 }
150 resolved := sandbox.ResolveShell(cfg.Tools.Shell.Prefer, cfg.Tools.Shell.Path, nil)
151 bound := resolved
152 if ctrl != nil {
153 if sh := ctrl.BoundShell(); sh.Path != "" {
154 bound = sh
155 }
156 }
157 return SandboxView{
158 Bash: cfg.BashMode(), Network: cfg.Sandbox.Network,
159 WorkspaceRoot: cfg.Sandbox.WorkspaceRoot, AllowWrite: nonNil(cfg.Sandbox.AllowWrite),
160 EffectiveWorkspaceRoot: effectiveWorkspaceRoot, EffectiveWriteRoots: nonNil(writeRoots),
161 Shell: shell, EffectiveShell: sandboxEffectiveShellView(bound),
162 ResolvedShell: sandboxEffectiveShellView(resolved),
163 ShellReloadRequired: bound != resolved,
164 ShellCapabilities: sandboxCapabilityViews(cfg.Tools.Shell.Prefer, cfg.Tools.Shell.Path),
165 GitCapability: gitCapabilityView(cfg.Tools.Shell.Prefer, cfg.Tools.Shell.Path),
166 ShellInstallAction: shellInstallActionViewForGOOS(runtime.GOOS),
167 ShellRepairGuidance: shellRepairGuidanceForGOOS(runtime.GOOS),
168 GitRepairGuidance: gitRepairGuidanceForGOOS(runtime.GOOS),
169 }
170 }
171
172 func sandboxEffectiveShellView(sh sandbox.Shell) string {
173 if sh.Kind == sandbox.ShellZsh {
174 return "zsh"
175 }
176 if sh.Kind == sandbox.ShellSh {
177 return "sh"
178 }
179 if sh.Kind == sandbox.ShellPowerShell {
180 if sh.SupportsChaining() {
181 return "pwsh"
182 }
183 return "powershell"
184 }
185 path := strings.ToLower(strings.ReplaceAll(sh.Path, "\\", "/"))
186 if strings.Contains(path, "/git/") && strings.HasSuffix(path, "bash.exe") {
187 return "git-bash"
188 }
189 return "bash"
190 }
191
192 func gitCapabilityView(prefer, configPath string) *ShellCapabilityView {
193 capability := sandbox.GitCapabilityForConfig(prefer, configPath)
194 return &ShellCapabilityView{
195 ID: capability.ID, Available: capability.Available, Path: capability.Path,
196 Source: capability.Source, Reason: capability.Reason,
197 }
198 }
199
200 // sandboxCapabilityViews projects the discovered shell inventory for the
201 // settings surface. The slice is never nil so the desktop binding always
202 // encodes a JSON array, not null.
203 func sandboxCapabilityViews(prefer, configPath string) []ShellCapabilityView {
204 caps := sandbox.ShellCapabilitiesForConfig(prefer, configPath)
205 out := make([]ShellCapabilityView, 0, len(caps))
206 for _, cap := range caps {
207 out = append(out, ShellCapabilityView{
208 ID: cap.ID,
209 Variant: cap.Variant,
210 Available: cap.Available,
211 Path: cap.Path,
212 Source: cap.Source,
213 Reason: cap.Reason,
214 })
215 }
216 return out
217 }
218
218 lines GO