// purge.js —— 检测本机 Codex 的所有安装位置,并「强制清除」。 // 跨平台(macOS / Windows / Linux)自动识别:数据目录、PATH 里的可执行文件、 // npm 全局包、Homebrew 安装等。用于「更新后中转失效 → 彻底清干净再重装」。 const fs = require("fs"); const path = require("path"); const os = require("os"); const { exec } = require("child_process"); const codex = require("./codex"); const IS_WIN = process.platform === "win32"; // 跑一条 shell 命令,带超时;失败返回 null(用于探测,绝不抛错)。 function run(command) { return new Promise((resolve) => { try { exec(command, { timeout: 8000, windowsHide: true }, (err, stdout) => { if (err) return resolve(null); resolve(String(stdout || "").trim() || null); }); } catch (e) { resolve(null); } }); } // 某个路径存在吗?(含符号链接本身) function lstat(p) { try { return fs.lstatSync(p); } catch (e) { return null; } } // 求路径体积:目录递归、文件取 size、符号链接算 0。 function sizeOf(p, st) { if (!st) return 0; if (st.isSymbolicLink()) return 0; if (st.isDirectory()) return codex.dirSize(p); try { return fs.statSync(p).size; } catch (e) { return 0; } } // 从可执行文件的真实路径回溯出 @openai/codex 包根目录。 // 例:.../@openai/codex/bin/codex.js -> .../@openai/codex function pkgRootFromBin(realBin) { let dir = path.dirname(realBin); for (let i = 0; i < 6; i++) { if ( path.basename(dir) === "codex" && path.basename(path.dirname(dir)) === "@openai" ) { return dir; } const parent = path.dirname(dir); if (parent === dir) break; dir = parent; } return null; } // 探测所有可能的 Codex 安装位置。返回去重后的候选清单(含 exists / size)。 async function detectInstalls() { const home = os.homedir(); // key(绝对路径)-> 候选 const found = new Map(); const add = (label, kind, p) => { if (!p) return; let abs; try { abs = path.resolve(p); } catch (e) { return; } if (found.has(abs)) return; found.set(abs, { label, kind, path: abs }); }; // 1) 数据目录(配置 / 登录凭证 / 会话 / 插件 / 技能)——始终列出 add("Codex 数据目录(配置 · 登录 · 会话 · 插件)", "data", path.join(home, ".codex")); // 2) PATH 里的可执行文件(which / where 可能返回多行) const whichOut = IS_WIN ? await run("where codex") : await run("which -a codex"); if (whichOut) { for (const line of whichOut.split(/\r?\n/)) { const bin = line.trim(); if (!bin) continue; add("Codex 可执行文件(PATH)", "bin", bin); // 顺着符号链接找到真实文件,再回溯 npm 包根 try { const real = fs.realpathSync(bin); if (real !== bin) add("Codex 可执行文件(真实位置)", "bin", real); const root = pkgRootFromBin(real); if (root) add("@openai/codex 安装包", "npm-package", root); } catch (e) { /* 断链/权限:忽略 */ } } } // 3) npm 全局包与 npm 全局 bin const npmRoot = await run(IS_WIN ? "npm.cmd root -g" : "npm root -g"); if (npmRoot) add("@openai/codex 安装包(npm 全局)", "npm-package", path.join(npmRoot, "@openai", "codex")); const npmPrefix = await run(IS_WIN ? "npm.cmd prefix -g" : "npm prefix -g"); if (npmPrefix) { const binDir = IS_WIN ? npmPrefix : path.join(npmPrefix, "bin"); for (const n of ["codex", "codex.cmd", "codex.ps1", "codex.exe"]) { add("Codex 启动脚本(npm bin)", "bin", path.join(binDir, n)); } add("@openai/codex 安装包(npm 全局)", "npm-package", path.join(npmPrefix, IS_WIN ? "node_modules" : "lib/node_modules", "@openai", "codex")); } // 4) 各平台常见固定位置兜底 if (IS_WIN) { const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming"); for (const n of ["codex", "codex.cmd", "codex.ps1"]) { add("Codex 启动脚本(%APPDATA%\\npm)", "bin", path.join(appdata, "npm", n)); } add("@openai/codex 安装包(%APPDATA%\\npm)", "npm-package", path.join(appdata, "npm", "node_modules", "@openai", "codex")); } else { // macOS / Linux:Homebrew 与常见全局前缀 for (const prefix of ["/opt/homebrew", "/usr/local", path.join(home, ".npm-global")]) { add("Codex 可执行文件(" + prefix + ")", "bin", path.join(prefix, "bin", "codex")); add("@openai/codex 安装包(" + prefix + ")", "npm-package", path.join(prefix, "lib", "node_modules", "@openai", "codex")); } } // 收尾:补 exists / size,只保留真实存在的项 const items = []; for (const c of found.values()) { const st = lstat(c.path); if (!st) continue; // 不存在的不展示 items.push({ ...c, exists: true, isSymlink: st.isSymbolicLink(), isDir: st.isDirectory() && !st.isSymbolicLink(), size: sizeOf(c.path, st), sizeText: codex.humanSize(sizeOf(c.path, st)), }); } // 数据目录排最前,其余按体积降序 items.sort((a, b) => { if (a.kind === "data") return -1; if (b.kind === "data") return 1; return b.size - a.size; }); return items; } // 强制删除给定路径。为安全起见,只删除「当前探测得到」的路径, // 防止渲染进程传入任意路径。返回每一项的删除结果。 async function purge(requestedPaths) { const wanted = new Set((requestedPaths || []).map((p) => path.resolve(p))); const detected = await detectInstalls(); const allowed = detected.filter((d) => wanted.has(d.path)); const results = []; for (const item of allowed) { try { fs.rmSync(item.path, { recursive: true, force: true }); results.push({ path: item.path, label: item.label, ok: true, stillExists: !!lstat(item.path) }); } catch (e) { results.push({ path: item.path, label: item.label, ok: false, error: String(e && e.message || e) }); } } return results; } module.exports = { detectInstalls, purge };