133 lines
3.4 KiB
JavaScript
133 lines
3.4 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const repoRoot = path.join(__dirname, "..", "..");
|
|
const recoveryDir = path.join(repoRoot, "data", "recovery");
|
|
const markerPath = path.join(recoveryDir, "update-marker.json");
|
|
const safeModeFlagPath = path.join(recoveryDir, "safe-mode.flag");
|
|
|
|
function ensureRecoveryDir() {
|
|
fs.mkdirSync(recoveryDir, { recursive: true });
|
|
}
|
|
|
|
function readRecoveryMarker() {
|
|
try {
|
|
if (!fs.existsSync(markerPath)) return null;
|
|
const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
return marker && typeof marker === "object" ? marker : null;
|
|
} catch {
|
|
return {
|
|
id: "unreadable",
|
|
status: "failed",
|
|
error: "Recovery marker could not be parsed.",
|
|
marker_path: markerPath
|
|
};
|
|
}
|
|
}
|
|
|
|
function writeRecoveryMarker(marker) {
|
|
ensureRecoveryDir();
|
|
const next = {
|
|
id: marker.id || crypto.randomUUID(),
|
|
status: marker.status || "pending",
|
|
updated_at: new Date().toISOString(),
|
|
...marker
|
|
};
|
|
fs.writeFileSync(markerPath, JSON.stringify(next, null, 2), "utf8");
|
|
return next;
|
|
}
|
|
|
|
function createRecoveryMarker(details) {
|
|
return writeRecoveryMarker({
|
|
id: crypto.randomUUID(),
|
|
status: "pending",
|
|
created_at: new Date().toISOString(),
|
|
...details
|
|
});
|
|
}
|
|
|
|
function updateRecoveryMarker(values) {
|
|
const current = readRecoveryMarker() || {};
|
|
return writeRecoveryMarker({ ...current, ...values });
|
|
}
|
|
|
|
function markRecoveryMarkerFailed(error) {
|
|
return updateRecoveryMarker({
|
|
status: "failed",
|
|
error: error?.message || String(error || "Update failed.")
|
|
});
|
|
}
|
|
|
|
function markRecoveryMarkerComplete(values = {}) {
|
|
return updateRecoveryMarker({
|
|
...values,
|
|
status: "completed",
|
|
completed_at: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
function clearRecoveryMarker() {
|
|
try {
|
|
fs.rmSync(markerPath, { force: true });
|
|
} catch {
|
|
// ignore cleanup failures
|
|
}
|
|
}
|
|
|
|
function markStartupVerification() {
|
|
const marker = readRecoveryMarker();
|
|
if (!marker) return null;
|
|
if (marker.status === "completed") {
|
|
clearRecoveryMarker();
|
|
return { ...marker, cleared: true };
|
|
}
|
|
if (["pending", "applying", "verifying"].includes(marker.status)) {
|
|
return writeRecoveryMarker({
|
|
...marker,
|
|
status: "stale",
|
|
stale_at: new Date().toISOString(),
|
|
error: marker.error || "Previous update did not complete startup verification."
|
|
});
|
|
}
|
|
return marker;
|
|
}
|
|
|
|
function isSafeModeRequested(argv = process.argv, env = process.env) {
|
|
return env.LUMI_SAFE_MODE === "1" ||
|
|
env.SAFE_MODE === "1" ||
|
|
argv.includes("--safe-mode") ||
|
|
fs.existsSync(safeModeFlagPath);
|
|
}
|
|
|
|
function safeModeStatus() {
|
|
const marker = readRecoveryMarker();
|
|
const requested = isSafeModeRequested();
|
|
const incomplete = marker && ["pending", "applying", "verifying", "failed", "stale"].includes(marker.status);
|
|
return {
|
|
requested,
|
|
active: process.env.SAFE_MODE === "1" || process.env.LUMI_SAFE_MODE === "1",
|
|
marker,
|
|
has_incomplete_marker: Boolean(incomplete),
|
|
safe_mode_flag: fs.existsSync(safeModeFlagPath),
|
|
marker_path: markerPath,
|
|
flag_path: safeModeFlagPath
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
recoveryDir,
|
|
markerPath,
|
|
safeModeFlagPath,
|
|
readRecoveryMarker,
|
|
writeRecoveryMarker,
|
|
createRecoveryMarker,
|
|
updateRecoveryMarker,
|
|
markRecoveryMarkerFailed,
|
|
markRecoveryMarkerComplete,
|
|
clearRecoveryMarker,
|
|
markStartupVerification,
|
|
isSafeModeRequested,
|
|
safeModeStatus
|
|
};
|