From 4aa30699c97e6ce781f0609e21ee1d7530dfd752 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Thu, 24 Sep 2026 12:50:05 -0400 Subject: [PATCH] Install the update itself, instead of asking for a drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build 115. Downloading and then telling the operator to quit, mount and drag was the friction the updater was supposed to remove. An app cannot replace itself while running, so the swap is done by a small script that outlives the process: it waits for us to exit, moves the old bundle aside, copies the new one in, clears the quarantine flag and relaunches. Moved aside, not deleted. If the copy fails half way — a full disk, a truncated download, the volume going away — the old app goes straight back and is reopened. The failure this is written against is a campus Mac with no working app and a service in an hour, which is far worse than not updating. Tested in a sandbox across all three paths: a clean swap, a copy that fails mid-way (old app restored, no leftovers), and a disk image with no app in it (aborts before touching the installed one). It refuses while translation is live: quitting stops every listener's captions and no update is worth that. Stop translating first. No sudo — /Applications is admin-writable and the bundle belongs to the operator. If that is ever untrue the copy fails and the rollback runs. Co-Authored-By: Claude Opus 5 --- app/engine.js | 28 ++++++++++----- app/index.html | 2 +- app/main.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ app/package.json | 4 +-- app/preload.js | 1 + 5 files changed, 115 insertions(+), 11 deletions(-) diff --git a/app/engine.js b/app/engine.js index 0e2f2cf..b23fa2b 100644 --- a/app/engine.js +++ b/app/engine.js @@ -2983,14 +2983,26 @@ async function downloadUpdate() { try { const r = await window.engineHost.updateDownload(updFound.asset.url, updFound.asset.name); if (!r.ok) { $('updProgress').textContent = r.error || 'Download failed.'; btn.disabled = false; return; } - $('updProgress').textContent = 'Saved to Downloads.'; - /* Say what is left to do. Nothing installs itself here on purpose — see - the note on the main-process side. */ - $('updWhat').textContent = 'Downloaded ' + updFound.tag + ' (' + fmtMB(r.bytes) + '). ' + - 'Quit Church Translate, open the disk image, and drag the app over the one in ' + - 'Applications. Your settings, models and voices are untouched.'; - log('update ' + updFound.tag + ' downloaded to ' + r.path, 'ok'); - try { await window.engineHost.updateReveal(r.path); } catch (_) {} + $('updProgress').textContent = 'Downloaded. Installing\u2026'; + log('update ' + updFound.tag + ' downloaded to ' + r.path, 'ok'); + + /* + * Straight into the install. The app quits, a helper swaps the bundle and + * reopens it — so from here the window closing IS the update working, and + * saying so before it happens stops that looking like a crash. + */ + $('updWhat').textContent = 'Installing ' + updFound.tag + '. Church Translate will close and ' + + 'reopen by itself in a few seconds. Your settings, models and voices are untouched.'; + const ins = await window.engineHost.updateInstall(r.path); + if (!ins || !ins.ok) { + /* Still useful: the file is downloaded, so fall back to the manual route + rather than leaving them with nothing. */ + $('updProgress').textContent = (ins && ins.error) || 'Could not install.'; + $('updWhat').textContent = 'Downloaded, but it could not install itself. Quit Church ' + + 'Translate, open the disk image in Downloads, and drag the app over the one in Applications.'; + try { await window.engineHost.updateReveal(r.path); } catch (_) {} + btn.disabled = false; + } } catch (e) { $('updProgress').textContent = 'Download failed: ' + e.message; btn.disabled = false; diff --git a/app/index.html b/app/index.html index 8266e8a..50625c0 100644 --- a/app/index.html +++ b/app/index.html @@ -391,7 +391,7 @@