Files
codex-market-app/electron/lib/purge.js
T

183 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 codexDesktop = require("./codex_desktop");
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 可能返回多行)
for (const item of codexDesktop.desktopExecutableCandidates()) {
add(item.label || "ChatGPT/Codex 桌面程序", "bin", item.path);
}
// 3) 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) {
/* 断链/权限:忽略 */
}
}
}
// 4) 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"));
}
// 5) 各平台常见固定位置兜底
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 };