Files

287 lines
7.7 KiB
JavaScript

import fs from "node:fs";
import { promises as fsp } from "node:fs";
import path from "node:path";
import * as core from "./core.js";
import * as hostctl from "./host.js";
import * as rules from "./rules.js";
import * as updater from "./update.js";
import { clearDrillGrant } from "./drill-grant.js";
export function wasApplied(state) {
if (!state) return false;
if (state.has_backup) return true;
if (Number(state.patches_applied) > 0) return true;
return [state.shim_cmd, state.shim_ps1, state.shim_bin].includes("patched");
}
export function shouldDeletePluginTree(dir, dshHome = core.findDshHome()) {
if (!dir) return false;
let real;
try {
real = fs.realpathSync(dir);
} catch {
real = path.resolve(dir);
}
const n = real.replace(/[/\\]+/g, "/").toLowerCase();
if (n.includes("/node_modules/dsh-purge")) return true;
let home;
try {
home = fs.realpathSync(dshHome);
} catch {
home = path.resolve(dshHome);
}
const homeN = home.replace(/[/\\]+/g, "/").toLowerCase();
return n.startsWith(`${homeN}/profiles/`);
}
export function stripPluginFromPackage(pkg) {
if (!pkg || typeof pkg !== "object") return false;
let changed = false;
for (const key of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
if (pkg[key] && Object.prototype.hasOwnProperty.call(pkg[key], "dsh-purge")) {
delete pkg[key]["dsh-purge"];
changed = true;
}
}
for (const key of ["bundles", "plugins"]) {
if (!Array.isArray(pkg[key])) continue;
const next = pkg[key].filter((item) => {
if (typeof item === "string") return item !== "dsh-purge" && !item.startsWith("dsh-purge@");
const id = item && (item.id || item.name);
return id !== "dsh-purge";
});
if (next.length !== pkg[key].length) {
pkg[key] = next;
changed = true;
}
}
return changed;
}
function readJson(fp) {
try {
return JSON.parse(fs.readFileSync(fp, "utf8"));
} catch {
return null;
}
}
function writeJson(fp, value) {
fs.writeFileSync(fp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function pkgMentionsPurge(pkg) {
if (!pkg) return false;
if (pkg.dependencies?.["dsh-purge"] || pkg.devDependencies?.["dsh-purge"]) return true;
for (const key of ["bundles", "plugins"]) {
if (!Array.isArray(pkg[key])) continue;
if (pkg[key].some((item) => {
if (typeof item === "string") return item === "dsh-purge" || item.startsWith("dsh-purge@");
return item && (item.id === "dsh-purge" || item.name === "dsh-purge");
})) return true;
}
return false;
}
export function listInstalledProfiles(dshHome = core.findDshHome()) {
const root = path.join(dshHome, "profiles");
if (!fs.existsSync(root)) return [];
let names = [];
try {
names = fs.readdirSync(root);
} catch {
return [];
}
const out = [];
for (const name of names) {
const dir = path.join(root, name);
const pkgPath = path.join(dir, "package.json");
const pkg = readJson(pkgPath);
const nm = path.join(dir, "node_modules", "dsh-purge");
if (!pkgMentionsPurge(pkg) && !fs.existsSync(nm)) continue;
const spec = pkg?.dependencies?.["dsh-purge"] || pkg?.devDependencies?.["dsh-purge"] || "";
out.push({ name, dir, spec: String(spec || ""), pkgPath, hasModule: fs.existsSync(nm) });
}
return out;
}
async function unlinkIfExists(fp) {
try {
await fsp.unlink(fp);
return true;
} catch (e) {
if (e && e.code === "ENOENT") return false;
throw e;
}
}
async function rmIfExists(fp) {
try {
await fsp.rm(fp, { recursive: true, force: true });
return true;
} catch {
return false;
}
}
async function removeOverride(dshHome) {
const fp = core.findOverrideFile(dshHome);
const ok = await unlinkIfExists(fp);
return { path: fp, removed: ok };
}
async function resetRules(dshHome) {
const reset = await rules.resetToOriginal(dshHome);
const dir = rules.rulesDir(dshHome);
const removedDir = await rmIfExists(dir);
return { ...reset, rules_dir: dir, rules_dir_removed: removedDir };
}
function collectCleanupDirs(profiles, dshHome) {
const seen = new Set();
const cleanup = [];
const remember = (dir) => {
if (!dir) return;
let real;
try {
real = fs.realpathSync(dir);
} catch {
real = path.resolve(dir);
}
const key = real.toLowerCase();
if (seen.has(key)) return;
seen.add(key);
cleanup.push(real);
};
for (const p of profiles) {
const nm = path.join(p.dir, "node_modules", "dsh-purge");
if (!fs.existsSync(nm)) continue;
let real;
try {
real = fs.realpathSync(nm);
} catch {
real = path.resolve(nm);
}
if (shouldDeletePluginTree(real, dshHome)) remember(real);
if (path.resolve(nm).toLowerCase() !== real.toLowerCase()) remember(nm);
}
const dataDir = path.join(dshHome, "dsh-purge");
remember(dataDir);
const root = updater.pluginRoot();
if (shouldDeletePluginTree(root, dshHome)) remember(root);
return cleanup;
}
function unregisterProfiles(profiles) {
const stripped = [];
const errors = [];
for (const p of profiles) {
try {
const pkg = readJson(p.pkgPath) || {};
if (stripPluginFromPackage(pkg)) {
writeJson(p.pkgPath, pkg);
stripped.push(p.name);
}
} catch (e) {
errors.push({ profile: p.name, error: String(e && e.message ? e.message : e) });
}
}
return { stripped, errors };
}
export async function uninstallPurge() {
const state = await core.gatherState();
const applied = wasApplied(state);
const dshHome = state.dsh_home || core.findDshHome();
const report = {
ok: true,
applied,
reverted: false,
needRestart: true,
patches: null,
shim: null,
flash: null,
override: null,
rules: null,
profiles: [],
cleanup: [],
errors: [],
};
try {
if (state.ai_base) {
report.patches = await core.revertAll(state.ai_base);
if (applied) report.reverted = true;
}
} catch (e) {
report.errors.push(`revert patches: ${e.message || e}`);
}
if (applied && (report.patches?.errors?.length || report.errors.some((e) => /^revert patches:/.test(e)))) {
report.ok = false;
report.needRestart = false;
report.cleanup = [];
return report;
}
try {
report.shim = await core.revertAllShims();
if (state.shim_cmd === "patched" || state.shim_ps1 === "patched" || state.shim_bin === "patched") {
report.reverted = true;
}
} catch (e) {
report.errors.push(`revert shims: ${e.message || e}`);
}
try {
report.flash = core.revertCmdFlash(state.ai_base, dshHome);
} catch (e) {
report.errors.push(`revert flash: ${e.message || e}`);
}
try {
report.override = await removeOverride(dshHome);
} catch (e) {
report.errors.push(`remove override: ${e.message || e}`);
}
try {
report.rules = await resetRules(dshHome);
} catch (e) {
report.errors.push(`reset rules: ${e.message || e}`);
}
const profiles = listInstalledProfiles(dshHome);
report.profiles = profiles.map((p) => p.name);
const unreg = unregisterProfiles(profiles);
report.stripped = unreg.stripped;
report.errors.push(...unreg.errors.map((x) => `${x.profile}: ${x.error}`));
if (unreg.errors.length) {
report.ok = false;
report.needRestart = false;
report.cleanup = [];
report.drillGrantCleared = false;
return report;
}
try {
report.drillGrantCleared = clearDrillGrant(dshHome);
} catch (e) {
report.ok = false;
report.needRestart = false;
report.cleanup = [];
report.errors.push(`clear drill grant: ${e.message || e}`);
return report;
}
report.cleanup = collectCleanupDirs(profiles, dshHome);
return report;
}
export function scheduleCleanupRestart(opts = {}) {
hostctl.scheduleCleanupRestart(opts);
}