@@ -49,6 +49,29 @@ func (s *server) uninstallEntrypoints(args map[string]any) commandResult {
4949 return installActionResult ("ok" , "入口已卸载。" )
5050}
5151
52+ func (s * server ) uninstallCodexTools (args map [string ]any ) commandResult {
53+ payload := codexToolsUninstallPayload ()
54+ if runtime .GOOS != "windows" {
55+ return failed ("Codex++ 卸载功能仅支持 Windows 安装包。" , payload )
56+ }
57+ options := mapArg (args , "options" )
58+ removeOwnedData := boolArg (options , "removeOwnedData" )
59+ removeWindowsWatcherInstall ()
60+ cleanupWindowsCodexToolsEntrypoints ()
61+ if removeOwnedData {
62+ _ = os .RemoveAll (stateDir ())
63+ }
64+ uninstaller := windowsCodexToolsUninstallerPath ()
65+ payload = codexToolsUninstallPayload ()
66+ if uninstaller == "" {
67+ return ok ("未找到 Windows 安装器卸载程序;已移除入口和 watcher。若使用便携版,请手动删除当前 CodexTools 文件夹。" , payload )
68+ }
69+ if err := startWindowsCodexToolsUninstaller (uninstaller ); err != nil {
70+ return failed ("启动 Windows 卸载程序失败:" + err .Error (), payload )
71+ }
72+ return ok ("已启动 Windows 卸载程序,请按提示完成卸载。" , payload )
73+ }
74+
5275func installActionResult (status , message string ) commandResult {
5376 return commandResult {
5477 "status" : status ,
@@ -92,6 +115,132 @@ func uninstallEntrypoints() error {
92115 return firstErr
93116}
94117
118+ const windowsCodexToolsUninstallKey = `HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\CodexTools`
119+
120+ func codexToolsUninstallPayload () map [string ]any {
121+ uninstaller := windowsCodexToolsUninstallerPath ()
122+ return map [string ]any {
123+ "platform" : runtime .GOOS ,
124+ "supported" : runtime .GOOS == "windows" ,
125+ "uninstallerPath" : uninstaller ,
126+ "installerFound" : uninstaller != "" ,
127+ }
128+ }
129+
130+ func cleanupWindowsCodexToolsEntrypoints () {
131+ _ = uninstallEntrypoints ()
132+ if runtime .GOOS != "windows" {
133+ return
134+ }
135+ if startMenu := windowsCodexToolsStartMenuDir (); startMenu != "" {
136+ _ = os .RemoveAll (startMenu )
137+ }
138+ }
139+
140+ func removeWindowsWatcherInstall () {
141+ if runtime .GOOS != "windows" {
142+ return
143+ }
144+ _ = windowsRegDeleteCurrentUserValue (watcherRunKey , watcherRunName )
145+ if shortcut := watcherStartupShortcutPath (); shortcut != "" {
146+ _ = os .Remove (shortcut )
147+ }
148+ }
149+
150+ func windowsCodexToolsStartMenuDir () string {
151+ appdata := os .Getenv ("APPDATA" )
152+ if appdata == "" {
153+ return ""
154+ }
155+ return filepath .Join (appdata , "Microsoft" , "Windows" , "Start Menu" , "Programs" , "CodexTools" )
156+ }
157+
158+ func windowsCodexToolsUninstallerPath () string {
159+ if runtime .GOOS != "windows" {
160+ return ""
161+ }
162+ var candidates []string
163+ add := func (path string ) {
164+ path = strings .TrimSpace (path )
165+ if path != "" {
166+ candidates = append (candidates , path )
167+ }
168+ }
169+ if executable , err := os .Executable (); err == nil {
170+ add (filepath .Join (filepath .Dir (executable ), "Uninstall.exe" ))
171+ }
172+ if localAppData := os .Getenv ("LOCALAPPDATA" ); localAppData != "" {
173+ add (filepath .Join (localAppData , "CodexTools" , "Uninstall.exe" ))
174+ }
175+ if installLocation := windowsRegistryString (windowsCodexToolsUninstallKey , "InstallLocation" ); installLocation != "" {
176+ add (filepath .Join (strings .Trim (installLocation , `"` ), "Uninstall.exe" ))
177+ }
178+ if uninstallString := windowsRegistryString (windowsCodexToolsUninstallKey , "UninstallString" ); uninstallString != "" {
179+ add (windowsExecutableFromCommand (uninstallString ))
180+ }
181+ for _ , candidate := range candidates {
182+ if fileExists (candidate ) {
183+ return candidate
184+ }
185+ }
186+ return ""
187+ }
188+
189+ func windowsRegistryString (key , name string ) string {
190+ if runtime .GOOS != "windows" {
191+ return ""
192+ }
193+ cmd := exec .Command ("reg" , "query" , key , "/v" , name )
194+ hideSubprocessWindow (cmd )
195+ output , err := cmd .Output ()
196+ if err != nil {
197+ return ""
198+ }
199+ return parseWindowsRegQueryValue (string (output ), name )
200+ }
201+
202+ func parseWindowsRegQueryValue (output , name string ) string {
203+ for _ , line := range strings .Split (output , "\n " ) {
204+ trimmed := strings .TrimSpace (line )
205+ if ! strings .HasPrefix (strings .ToLower (trimmed ), strings .ToLower (name )) {
206+ continue
207+ }
208+ parts := strings .SplitN (trimmed , "REG_SZ" , 2 )
209+ if len (parts ) != 2 {
210+ continue
211+ }
212+ return strings .TrimSpace (parts [1 ])
213+ }
214+ return ""
215+ }
216+
217+ func windowsExecutableFromCommand (command string ) string {
218+ trimmed := strings .TrimSpace (command )
219+ if trimmed == "" {
220+ return ""
221+ }
222+ if strings .HasPrefix (trimmed , `"` ) {
223+ end := strings .Index (trimmed [1 :], `"` )
224+ if end >= 0 {
225+ return trimmed [1 : end + 1 ]
226+ }
227+ }
228+ fields := strings .Fields (trimmed )
229+ if len (fields ) == 0 {
230+ return ""
231+ }
232+ return fields [0 ]
233+ }
234+
235+ func startWindowsCodexToolsUninstaller (path string ) error {
236+ if runtime .GOOS != "windows" {
237+ return errors .New ("Codex++ 卸载程序只支持 Windows" )
238+ }
239+ cmd := exec .Command (path )
240+ cmd .Dir = filepath .Dir (path )
241+ return cmd .Start ()
242+ }
243+
95244func writeMacOSAppBundle (manager bool ) error {
96245 appPath := entrypointPath (manager )
97246 contents := filepath .Join (appPath , "Contents" )
0 commit comments