feat(installer): drive in-app updates through the Tauri installer

Converge update on the same principle as bootstrap: one driver owns all
repo mutation. The desktop becomes a pure consumer that hands off to
Hermes-Setup.exe --update instead of re-implementing git/pip in Electron.

- hermes desktop --build-only: build without launching, so the installer
  owns the post-update launch (CLI keeps build logic single-sourced).
- Installer AppMode {Install,Update} from argv; get_mode exposed to the UI.
- Installer self-copies to HERMES_HOME/hermes-setup.exe on install success
  (no-op guard during --update re-invocation to avoid the locked-exe copy).
- Installer --update flow (update.rs): wait for the desktop to release the
  venv shim, run 'hermes update --yes --gateway' (branch on exit 0/2/other),
  then 'hermes desktop --build-only', then launch the rebuilt desktop. Reuses
  the bootstrap event channel + progress UI via a synthetic two-stage manifest.
- Desktop applyUpdates() gutted (~105 lines of git/stash/pull/pyproject/pip
  removed) -> thin handoff: spawn updater, app.quit() to free the shim.
  Detection (checkUpdates, commit changelog, behind-count) kept intact.
- install.ps1 creates Start Menu + Desktop shortcuts to the packed Hermes.exe
  (never bare 'hermes desktop', which would rebuild every launch).
This commit is contained in:
emozilla
2026-05-28 23:48:21 -04:00
parent a4cfc8b740
commit 6381e70448
9 changed files with 709 additions and 128 deletions
+25
View File
@@ -6770,6 +6770,26 @@ def cmd_gui(args):
sys.exit(build_result.returncode or 1)
packaged_executable = _desktop_packaged_executable(desktop_dir)
# --build-only: produce the artifact but do NOT launch. The installer's
# --update flow drives the rebuild headlessly and then launches the desktop
# itself (detached, after the old exe has exited), so the launch must NOT
# happen here — it would block the installer and, on Windows, the old exe
# is still being replaced. Verify the expected artifact exists so a silent
# "built nothing" can't slip past, then return success.
if getattr(args, "build_only", False):
if source_mode:
if not _desktop_dist_exists(desktop_dir):
print(f"✗ --build-only --source produced no dist at: {desktop_dir / 'dist'}")
sys.exit(1)
print(f"✓ Desktop source build ready at {desktop_dir / 'dist'} (not launching; --build-only)")
elif packaged_executable is None:
print(f"✗ --build-only produced no launchable app at: {desktop_dir / 'release'}")
print(" Expected an unpacked Electron app for the current OS.")
sys.exit(1)
else:
print(f"✓ Desktop packaged app ready: {packaged_executable} (not launching; --build-only)")
return
if source_mode:
print("→ Launching Hermes Desktop from source build...")
launch_result = subprocess.run([npm, "exec", "--", "electron", "."], cwd=desktop_dir, env=env, check=False)
@@ -14165,6 +14185,11 @@ Examples:
action="store_true",
help="Launch via `electron .` against apps/desktop/dist instead of the packaged app",
)
gui_parser.add_argument(
"--build-only",
action="store_true",
help="Build the desktop app but do not launch it (used by the installer's --update flow)",
)
gui_parser.add_argument(
"--fake-boot",
action="store_true",