Add built-in auto-updater with tab-bar update pill

Wires electrobun's Updater to the /dl download host. The packaged app checks
https://miniscp.com/dl on launch + every 6h; when a newer build exists, an
accent-colored "update tool" appears next to Connections in the tab bar. It
announces the update, becomes its own inline progress bar while downloading,
then flips to "Restart to apply" (two-step: download, then user-triggered
relaunch).

- electrobun.config.ts: release.baseUrl = https://miniscp.com/dl (baked into
  version.json at build; miniscp.com fronted by an external proxy for TLS).
- rpc.ts: checkForUpdate/downloadUpdate/applyUpdate requests + updateStatus push.
- rpc-handlers.ts + index.ts: call Updater; poll on launch+6h; stream
  onStatusChange progress. checkForUpdate self-guards the dev channel.
- rpc.service.ts + new updater.service.ts: signal-based state
  (available -> downloading -> ready, ready derived from downloadUpdate ok).
- tab-bar.ts: single bg-primary pill; progress fills the pill itself.
- icons.ts: register mynaDownload. i18n: tabBar + menu keys across all locales.
- menu.ts + app.ts: "Check for Updates…" app-menu item -> UpdaterService.check().
- docs: build-and-release + architecture updated.

Unsigned builds still update (applyUpdate strips the macOS quarantine xattr).
Verified: tsc --noEmit (exit 0) + build:ui AOT (exit 0). Live pill + full
download/apply flow require a two-build test on the host.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:43:40 +02:00
co-authored by Claude
parent 43f1168d6a
commit a40433b99e
19 changed files with 261 additions and 4 deletions
+22
View File
@@ -250,4 +250,26 @@ ApplicationMenu.on("application-menu-clicked", (event) => {
rpc.send.menuAction({ action });
});
// ---- auto-updater: check on launch + every 6h, stream status to the tab bar ----
// Updater.checkForUpdate() self-guards the dev channel (returns updateAvailable:
// false), so the pill never appears during `bun run dev`.
Updater.onStatusChange((entry) => {
if (entry.status === "download-progress") {
rpc.send.updateStatus({ phase: "downloading", progress: entry.details?.progress ?? 0 });
} else if (entry.status === "error") {
rpc.send.updateStatus({ phase: "error", error: entry.message });
}
});
async function runUpdateCheck(): Promise<void> {
try {
const r = await Updater.checkForUpdate();
if (r.updateAvailable) rpc.send.updateStatus({ phase: "available", version: r.version });
} catch {
// offline / feed unreachable — stay quiet and retry on the next tick
}
}
setTimeout(() => void runUpdateCheck(), 5_000); // let the window settle first
setInterval(() => void runUpdateCheck(), 6 * 60 * 60 * 1000);
console.log(`${BRANDING.appName} started (${url})`);
+2
View File
@@ -36,6 +36,8 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
submenu: [
{ label: t("menu.about", { app: BRANDING.appName }), action: "about" },
{ type: "separator" },
{ label: t("menu.checkForUpdates"), action: "check-for-updates" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "showAll" },
+20 -1
View File
@@ -1,4 +1,4 @@
import { BrowserView, Utils } from "electrobun/bun";
import { BrowserView, Updater, Utils } from "electrobun/bun";
import type { AppRPC, EditorOpenContext } from "../shared/rpc";
import { effectiveConcurrency, type AppSettings, type Result } from "../shared/types";
import type { PanelContextMenu } from "./context-menu";
@@ -322,6 +322,25 @@ export function createAppRPC({
}),
listDrives: () => wrap(async () => ({ drives: listLocalDrives() })),
// ---- auto-updater ----
checkForUpdate: () =>
wrap(async () => {
const r = await Updater.checkForUpdate();
return { updateAvailable: r.updateAvailable, version: r.version };
}),
downloadUpdate: () =>
wrap(async () => {
await Updater.downloadUpdate();
return {};
}),
applyUpdate: () =>
wrap(async () => {
await Updater.applyUpdate(); // relaunches + quits; may never resolve
return {};
}),
},
},
});
+5
View File
@@ -24,6 +24,10 @@ export const de = {
newTab: "Neuer Tab ({mod}T)",
closeTab: "Tab schließen ({mod}W)",
options: "Optionen",
update: "Update verfügbar",
updating: "Aktualisiere",
restartToUpdate: "Zum Anwenden neu starten",
updateTooltip: "Update verfügbar: miniSCP {version}",
},
options: {
title: "Optionen",
@@ -245,6 +249,7 @@ export const de = {
},
menu: {
about: "Über {app}",
checkForUpdates: "Nach Updates suchen…",
quit: "Beenden",
file: "Datei",
newTab: "Neuer Tab",
+5
View File
@@ -28,6 +28,10 @@ export const en = {
newTab: "New tab ({mod}T)",
closeTab: "Close tab ({mod}W)",
options: "Options",
update: "Update available",
updating: "Updating",
restartToUpdate: "Restart to apply",
updateTooltip: "Update available: miniSCP {version}",
},
options: {
title: "Options",
@@ -249,6 +253,7 @@ export const en = {
},
menu: {
about: "About {app}",
checkForUpdates: "Check for Updates…",
quit: "Quit",
file: "File",
newTab: "New Tab",
+5
View File
@@ -24,6 +24,10 @@ export const es = {
newTab: "Nueva pestaña ({mod}T)",
closeTab: "Cerrar pestaña ({mod}W)",
options: "Opciones",
update: "Actualización disponible",
updating: "Actualizando",
restartToUpdate: "Reiniciar para aplicar",
updateTooltip: "Actualización disponible: miniSCP {version}",
},
options: {
title: "Opciones",
@@ -245,6 +249,7 @@ export const es = {
},
menu: {
about: "Acerca de {app}",
checkForUpdates: "Buscar actualizaciones…",
quit: "Salir",
file: "Archivo",
newTab: "Nueva pestaña",
+5
View File
@@ -26,6 +26,10 @@ export const eu = {
newTab: "Fitxa berria ({mod}T)",
closeTab: "Itxi fitxa ({mod}W)",
options: "Aukerak",
update: "Eguneratzea eskuragarri",
updating: "Eguneratzen",
restartToUpdate: "Berrabiarazi aplikatzeko",
updateTooltip: "Eguneratzea eskuragarri: miniSCP {version}",
},
options: {
title: "Aukerak",
@@ -247,6 +251,7 @@ export const eu = {
},
menu: {
about: "{app} (r)i buruz",
checkForUpdates: "Bilatu eguneratzeak…",
quit: "Irten",
file: "Fitxategia",
newTab: "Fitxa berria",
+5
View File
@@ -24,6 +24,10 @@ export const fr = {
newTab: "Nouvel onglet ({mod}T)",
closeTab: "Fermer l'onglet ({mod}W)",
options: "Options",
update: "Mise à jour disponible",
updating: "Mise à jour",
restartToUpdate: "Redémarrer pour appliquer",
updateTooltip: "Mise à jour disponible : miniSCP {version}",
},
options: {
title: "Options",
@@ -245,6 +249,7 @@ export const fr = {
},
menu: {
about: "À propos de {app}",
checkForUpdates: "Rechercher des mises à jour…",
quit: "Quitter",
file: "Fichier",
newTab: "Nouvel onglet",
+5
View File
@@ -25,6 +25,10 @@ export const ja = {
newTab: "新しいタブ ({mod}T)",
closeTab: "タブを閉じる ({mod}W)",
options: "オプション",
update: "アップデートあり",
updating: "更新中",
restartToUpdate: "再起動して適用",
updateTooltip: "アップデートがあります: miniSCP {version}",
},
options: {
title: "オプション",
@@ -245,6 +249,7 @@ export const ja = {
},
menu: {
about: "{app} について",
checkForUpdates: "アップデートを確認…",
quit: "終了",
file: "ファイル",
newTab: "新しいタブ",
+5
View File
@@ -25,6 +25,10 @@ export const zh = {
newTab: "新建标签页 ({mod}T)",
closeTab: "关闭标签页 ({mod}W)",
options: "选项",
update: "有可用更新",
updating: "正在更新",
restartToUpdate: "重启以应用",
updateTooltip: "有可用更新:miniSCP {version}",
},
options: {
title: "选项",
@@ -245,6 +249,7 @@ export const zh = {
},
menu: {
about: "关于 {app}",
checkForUpdates: "检查更新…",
quit: "退出",
file: "文件",
newTab: "新建标签页",
+20
View File
@@ -157,6 +157,16 @@ export type AppRPC = {
managerConnect: { params: { id: string }; response: Result };
managerOpenStack: { params: { id: string }; response: Result };
managerClose: { params: Record<string, never>; response: Result };
// auto-updater (electrobun Updater)
/** Main window: re-run the update check on demand (menu item). */
checkForUpdate: {
params: Record<string, never>;
response: Result<{ updateAvailable: boolean; version: string }>;
};
/** Download the pending update (patch-chain, else full). Progress arrives via updateStatus. */
downloadUpdate: { params: Record<string, never>; response: Result };
/** Swap in the downloaded update and relaunch — quits the app, so may not resolve. */
applyUpdate: { params: Record<string, never>; response: Result };
};
messages: {
/** Editor → bun draft mirror; safety net for the non-cancellable native close. */
@@ -179,6 +189,16 @@ export type AppRPC = {
managerState: { open: boolean };
/** bun → main: the manager window changed the connection store. */
connectionsChanged: Record<string, never>;
/**
* bun → main: auto-updater lifecycle, drives the tab-bar update pill.
* "ready" is not a phase — the UI derives it from downloadUpdate resolving ok.
*/
updateStatus: {
phase: "available" | "downloading" | "error";
version?: string;
progress?: number;
error?: string;
};
};
};
};