From 6fd639e4d03bf8e15b3b72017f9b1403ad6328c1 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Mon, 15 Jun 2026 18:13:07 +0800 Subject: [PATCH] fix(updater): per-user NSIS updates, skip auto-update in dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows auto-updater shipped a per-machine MSI installed silently (installMode "quiet"). Per-machine MSI upgrades need elevation; "quiet" suppresses the UAC prompt and apps run non-elevated by default, so the silent install failed (1603 / "must be Administrator") for ordinary users — and since the app quits to apply the update, it looked like a crash/loop. This affected any non-elevated user on auto-update, not just dev. - bundle.targets: drop "msi" (was "all"). Windows now ships NSIS only, which is already configured per-user (nsis.installMode "currentUser") — so updates install without elevation. The updater manifest will reference the NSIS "-setup.exe"; the api.mcpmux.com resolver rewrites URLs filename-agnostically, so no worker change is needed. - updater.windows.installMode: "quiet" -> "passive". Shows a small progress UI and, crucially, surfaces a UAC prompt if elevation is ever needed instead of failing invisibly. With per-user NSIS the normal path still needs no elevation. - App.tsx: skip the startup auto-update entirely when import.meta.env.DEV (i.e. under `pnpm dev`) — otherwise a dev build detects a newer published release, installs it over the dev build, and relaunches, so local changes never appear. Production builds are unaffected. Tests: App update-banner suite stubs DEV=false to keep exercising the production flow; all 12 pass. Signed-off-by: Mohammod Al Amin Ashik --- apps/desktop/src-tauri/tauri.conf.json | 11 +++++++++-- apps/desktop/src/App.tsx | 6 ++++++ tests/ts/components/App.test.tsx | 5 +++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index b93e1e59..3caf0961 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -26,7 +26,7 @@ ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdCQUZGMEVCMEZBOTk5RTcKUldUbm1ha1A2L0N2ZTlOZjN5T3pGOHBHRUlibytMY2tPeWJkQ01heDJzdTJqK3B3a2lBdDZ1T1oK", "windows": { - "installMode": "quiet" + "installMode": "passive" } } }, @@ -50,7 +50,14 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": "all", + "targets": [ + "app", + "dmg", + "deb", + "rpm", + "appimage", + "nsis" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 3741edb4..b0d64656 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -104,6 +104,12 @@ function AppContent() { // into the new version — so a restart picks up updates with no clicks. // Otherwise just surface the dismissible banner for a manual install. useEffect(() => { + // Never auto-update under `pnpm dev`. A dev build would otherwise detect a + // newer published release, install it over this build, and relaunch — so + // your local changes would vanish before you could see them. Production + // builds (import.meta.env.DEV === false) are unaffected. + if (import.meta.env.DEV) return; + const checkForUpdates = async () => { try { const { checkForUpdate } = await import('@/lib/updates'); diff --git a/tests/ts/components/App.test.tsx b/tests/ts/components/App.test.tsx index d653ce5c..0fd35105 100644 --- a/tests/ts/components/App.test.tsx +++ b/tests/ts/components/App.test.tsx @@ -288,6 +288,10 @@ describe('App – dynamic gateway URL display', () => { describe('App – update banner', () => { beforeEach(() => { + // The startup auto-update check is gated behind `!import.meta.env.DEV` + // (it must never run under `pnpm dev`). Vitest runs in dev mode, so stub + // DEV=false here to exercise the production update flow. + vi.stubEnv('DEV', false); vi.useFakeTimers(); gatewayEventCallbacks = []; setupInvoke({ get_version: '0.1.2' }); @@ -296,6 +300,7 @@ describe('App – update banner', () => { afterEach(() => { vi.useRealTimers(); + vi.unstubAllEnvs(); }); it('should show update banner when update is available', async () => {