177 lines
6.1 KiB
JavaScript
177 lines
6.1 KiB
JavaScript
|
|
// 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 };
|