feat: auto-download and install updates in the background
All checks were successful
Build & Release / build (push) Successful in 2m28s

Downloads the platform zip, extracts over the install dir, then
prompts to restart with a single click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MiTHRAL 2026-04-21 13:19:57 -04:00
parent e259d9b63c
commit 25543cb7ba

View file

@ -1,30 +1,82 @@
import { Notification, app, shell } from "electron"; import { Notification, app } from "electron";
import { exec } from "child_process";
import { createWriteStream, mkdirSync } from "fs";
import { dirname, join } from "path";
import { tmpdir } from "os";
import { pipeline } from "stream/promises";
import { Readable } from "stream";
const RELEASES_URL = const RELEASES_URL =
"https://git.mithraic.cloud/api/v1/repos/ad3laid3/sanctum/releases/latest"; "https://git.mithraic.cloud/api/v1/repos/ad3laid3/sanctum/releases/latest";
interface Asset {
name: string;
browser_download_url: string;
}
interface Release {
tag_name: string;
html_url: string;
assets: Asset[];
}
export async function checkForUpdates() { export async function checkForUpdates() {
try { try {
const res = await fetch(RELEASES_URL); const res = await fetch(RELEASES_URL);
if (!res.ok) return; if (!res.ok) return;
const release = (await res.json()) as { tag_name: string; html_url: string }; const release = (await res.json()) as Release;
const latest = release.tag_name.replace(/^v/, ""); const latest = release.tag_name.replace(/^v/, "");
const current = app.getVersion(); const current = app.getVersion();
if (!isNewer(latest, current)) return; if (!isNewer(latest, current)) return;
const notification = new Notification({ const asset = findAsset(release.assets);
title: "Update Available", if (!asset) return;
body: `Version ${latest} is available. Click to download.`,
silent: true, notify("Update Downloading", `Sanctum ${latest} is downloading in the background…`);
await downloadAndInstall(asset.browser_download_url, latest);
} catch {
// non-critical
}
}
function findAsset(assets: Asset[]): Asset | undefined {
if (process.platform === "linux")
return assets.find((a) => a.name.includes("linux") && a.name.endsWith(".zip"));
if (process.platform === "win32")
return assets.find((a) => a.name.includes("win32") && a.name.endsWith(".zip"));
}
async function downloadAndInstall(url: string, version: string) {
const tmpDir = join(tmpdir(), `sanctum-update-${version}`);
mkdirSync(tmpDir, { recursive: true });
const zipPath = join(tmpDir, "update.zip");
const extractDir = join(tmpDir, "extracted");
const res = await fetch(url);
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
await pipeline(Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]), createWriteStream(zipPath));
const installDir = dirname(process.execPath);
await new Promise<void>((resolve, reject) => {
exec(
`unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`,
{ shell: "/bin/bash" },
(err) => (err ? reject(err) : resolve()),
);
}); });
notification.on("click", () => shell.openExternal(release.html_url)); const n = notify("Update Ready", `Sanctum ${version} is installed — click to restart.`);
notification.show(); n.on("click", () => { app.relaunch(); app.exit(0); });
} catch {
// non-critical — silently ignore network/parse errors
} }
function notify(title: string, body: string) {
const n = new Notification({ title, body, silent: true });
n.show();
return n;
} }
function isNewer(latest: string, current: string): boolean { function isNewer(latest: string, current: string): boolean {