From 25543cb7ba22ccf4c767c2fe4ff81f710039099f Mon Sep 17 00:00:00 2001 From: MiTHRAL Date: Tue, 21 Apr 2026 13:19:57 -0400 Subject: [PATCH] feat: auto-download and install updates in the background Downloads the platform zip, extracts over the install dir, then prompts to restart with a single click. Co-Authored-By: Claude Sonnet 4.6 --- src/native/updater.ts | 72 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/src/native/updater.ts b/src/native/updater.ts index 2c95c01..111dbde 100644 --- a/src/native/updater.ts +++ b/src/native/updater.ts @@ -1,32 +1,84 @@ -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 = "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() { try { const res = await fetch(RELEASES_URL); 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 current = app.getVersion(); if (!isNewer(latest, current)) return; - const notification = new Notification({ - title: "Update Available", - body: `Version ${latest} is available. Click to download.`, - silent: true, - }); + const asset = findAsset(release.assets); + if (!asset) return; - notification.on("click", () => shell.openExternal(release.html_url)); - notification.show(); + notify("Update Downloading", `Sanctum ${latest} is downloading in the background…`); + await downloadAndInstall(asset.browser_download_url, latest); } catch { - // non-critical — silently ignore network/parse errors + // 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[0]), createWriteStream(zipPath)); + + const installDir = dirname(process.execPath); + + await new Promise((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()), + ); + }); + + const n = notify("Update Ready", `Sanctum ${version} is installed — click to restart.`); + n.on("click", () => { app.relaunch(); app.exit(0); }); +} + +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 { const parse = (v: string) => v.split(".").map(Number); const [lA, lB, lC] = parse(latest);