Skip to content

Commit b4c6da8

Browse files
committed
Add Windows Codex++ uninstall flow
1 parent 1523859 commit b4c6da8

12 files changed

Lines changed: 218 additions & 23 deletions
508 Bytes
Binary file not shown.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
8a16b6f2f67b4cb361f8fc18b1f8ed2fa67427a925f3f405d636d3475b6b60f2 CodexTools-1.1.20-windows-arm64-setup.exe
2-
ce9abaea41343c06ed685cd03cd82a52894cdbe9ca9d354e3edf97b24e319455 CodexTools-1.1.20-windows-arm64.zip
1+
f79bd4ad55643b3637bd101ec4d90f93d9c950df4a615cd0182d655fe5cdd15d CodexTools-1.1.20-windows-arm64-setup.exe
2+
10cd5dc6d775bdb4a0faf1b72a29103aa5e422a59d9fcb55f538cd1c2b340194 CodexTools-1.1.20-windows-arm64.zip
-260 Bytes
Binary file not shown.
478 Bytes
Binary file not shown.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
d14016db990c5d57ab1b49311db3e3e7af0d5ffba3082f58c9cd60c785a2d73d CodexTools-1.1.20-windows-x64-setup.exe
2-
c4c3285cb9bace46452381970b00d4c1011b45cc713334140a6bda225081c435 CodexTools-1.1.20-windows-x64.zip
1+
bbea05955200c7c83e1fd3342b48597167f93546b454e803072b11ab125341f2 CodexTools-1.1.20-windows-x64-setup.exe
2+
4bb88312eea2e8bd6c113ba78c5fbb7ea501d63683769e819464e7cb7b7adb5b CodexTools-1.1.20-windows-x64.zip
775 Bytes
Binary file not shown.

entrypoints.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5275
func 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+
95244
func writeMacOSAppBundle(manager bool) error {
96245
appPath := entrypointPath(manager)
97246
contents := filepath.Join(appPath, "Contents")

launcher.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ func buildCodexExecutable(appPath string) string {
269269
func startCodexApp(appPath string, debugPort uint16, extraArgs []string) (codexLaunchHandle, error) {
270270
command := buildCodexLaunchCommand(appPath, debugPort, extraArgs)
271271
if runtime.GOOS == "windows" {
272+
if len(command) > 0 && strings.TrimSpace(command[0]) != "" && fileExists(command[0]) {
273+
handle, err := startCodexProcess(command)
274+
if err == nil {
275+
return handle, nil
276+
}
277+
if buildWindowsPackagedActivation(appPath, debugPort, extraArgs) == nil {
278+
return nil, err
279+
}
280+
appendDiagnosticLog("launcher.windows_direct_start_failed", map[string]any{"command": safeCommandForLog(command), "error": err.Error()})
281+
}
272282
if activation := buildWindowsPackagedActivation(appPath, debugPort, extraArgs); activation != nil {
273283
processID, activationErr := activateWindowsPackagedAppWithEnvironment(activation.appUserModelID, activation.arguments, codexLaunchEnvironment())
274284
if activationErr == nil {
@@ -288,13 +298,6 @@ func startCodexApp(appPath string, debugPort uint16, extraArgs []string) (codexL
288298
}
289299
return nil, fmt.Errorf("MSIX 激活 %s 失败:%v;explorer 兜底失败:%v;直接启动 %s 也失败:%w", activation.appUserModelID, activationErr, explorerErr, command[0], err)
290300
}
291-
if len(command) > 0 && strings.TrimSpace(command[0]) != "" && fileExists(command[0]) {
292-
handle, err := startCodexProcess(command)
293-
if err == nil {
294-
return handle, nil
295-
}
296-
return nil, err
297-
}
298301
}
299302
if len(command) == 0 || strings.TrimSpace(command[0]) == "" {
300303
return nil, fmt.Errorf("未找到 Codex.exe:%s", appPath)

main_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ func TestBuildWatcherInstallPlanMatchesOriginalWindowsShape(t *testing.T) {
4242
}
4343
}
4444

45+
func TestParseWindowsUninstallRegistryValue(t *testing.T) {
46+
output := `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\CodexTools
47+
DisplayName REG_SZ CodexTools
48+
UninstallString REG_SZ "C:\Users\A\AppData\Local\CodexTools\Uninstall.exe"
49+
`
50+
value := parseWindowsRegQueryValue(output, "UninstallString")
51+
if value != `"C:\Users\A\AppData\Local\CodexTools\Uninstall.exe"` {
52+
t.Fatalf("uninstall registry value mismatch: %q", value)
53+
}
54+
}
55+
56+
func TestWindowsExecutableFromCommandParsesQuotedPath(t *testing.T) {
57+
command := `"C:\Users\A\AppData\Local\CodexTools\Uninstall.exe" /S`
58+
if got := windowsExecutableFromCommand(command); got != `C:\Users\A\AppData\Local\CodexTools\Uninstall.exe` {
59+
t.Fatalf("quoted command path mismatch: %q", got)
60+
}
61+
}
62+
4563
func TestNormalizeSettingsLanguage(t *testing.T) {
4664
settings := normalizeSettings(backendSettings{Language: "ja"})
4765

@@ -914,18 +932,18 @@ func TestBuildWindowsPackagedActivationArguments(t *testing.T) {
914932
}
915933
}
916934

917-
func TestCodexLaunchPayloadPrefersPackagedActivationWhenReadable(t *testing.T) {
935+
func TestCodexLaunchPayloadPrefersDirectExecutableWhenReadable(t *testing.T) {
918936
if runtime.GOOS != "windows" {
919-
t.Skip("Windows packaged activation preference only applies on Windows")
937+
t.Skip("Windows executable preference only applies on Windows")
920938
}
921939
appDir := filepath.Join(t.TempDir(), "OpenAI.Codex_26.519.11010.0_x64__2p2nqsd0c76g0", "app")
922940
exe := filepath.Join(appDir, "Codex.exe")
923941
writeTestFile(t, exe, "binary")
924942

925943
payload := codexLaunchPayload(appDir)
926944

927-
if got := stringFromAny(payload["method"]); got != "packaged_activation" {
928-
t.Fatalf("readable MSIX app dir should prefer packaged activation: %#v", payload)
945+
if got := stringFromAny(payload["method"]); got != "executable" {
946+
t.Fatalf("readable MSIX app dir should prefer direct executable launch: %#v", payload)
929947
}
930948
if got := stringFromAny(payload["executable"]); got != exe {
931949
t.Fatalf("executable mismatch: %q", got)

manager.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ func (s *server) dispatch(ctx context.Context, command string, args map[string]a
195195
return s.installEntrypoints()
196196
case "uninstall_entrypoints":
197197
return s.uninstallEntrypoints(args)
198+
case "uninstall_codextools":
199+
return s.uninstallCodexTools(args)
198200
case "repair_codex_app":
199201
return s.repairCodexApp()
200202
case "repair_backend":
@@ -972,6 +974,14 @@ func codexLaunchPayload(appPath string) map[string]any {
972974
return payload
973975
}
974976
if runtime.GOOS == "windows" {
977+
if executable := buildCodexExecutable(appPath); strings.TrimSpace(executable) != "" && fileExists(executable) {
978+
payload["ready"] = true
979+
payload["method"] = "executable"
980+
payload["methodLabel"] = "可执行文件启动"
981+
payload["executable"] = executable
982+
payload["message"] = "将直接启动 Codex.exe 并附加调试端口参数。"
983+
return payload
984+
}
975985
if appUserModelID := packagedWindowsAppUserModelID(appPath); appUserModelID != "" {
976986
payload["ready"] = true
977987
payload["method"] = "packaged_activation"
@@ -981,14 +991,6 @@ func codexLaunchPayload(appPath string) map[string]any {
981991
payload["message"] = "将通过 AppUserModelID 激活 Windows Store/MSIX 版。"
982992
return payload
983993
}
984-
if executable := buildCodexExecutable(appPath); strings.TrimSpace(executable) != "" && fileExists(executable) {
985-
payload["ready"] = true
986-
payload["method"] = "executable"
987-
payload["methodLabel"] = "可执行文件启动"
988-
payload["executable"] = executable
989-
payload["message"] = "将按 1.1.12 的方式直接启动 Codex.exe。"
990-
return payload
991-
}
992994
}
993995
executable := buildCodexExecutable(appPath)
994996
if strings.TrimSpace(executable) == "" {

0 commit comments

Comments
 (0)