diff --git a/apps/bootstrap-installer/index.html b/apps/bootstrap-installer/index.html
index 1b34980a9..f9f7da034 100644
--- a/apps/bootstrap-installer/index.html
+++ b/apps/bootstrap-installer/index.html
@@ -3,7 +3,7 @@
- Hermes Setup
+ Hermes
diff --git a/apps/bootstrap-installer/src-tauri/src/bootstrap.rs b/apps/bootstrap-installer/src-tauri/src/bootstrap.rs
index 529b3b447..9b5897168 100644
--- a/apps/bootstrap-installer/src-tauri/src/bootstrap.rs
+++ b/apps/bootstrap-installer/src-tauri/src/bootstrap.rs
@@ -208,7 +208,7 @@ pub async fn launch_hermes_desktop(
/// Walks the well-known electron-builder unpacked-app paths under
/// `install_root`. Mirrors the resolver in `cmd_gui` (apps/desktop/release/
/// -unpacked/).
-fn resolve_hermes_desktop_exe(install_root: &std::path::Path) -> Option {
+pub(crate) fn resolve_hermes_desktop_exe(install_root: &std::path::Path) -> Option {
let release_dir = install_root.join("apps").join("desktop").join("release");
let candidates: &[(&str, &str)] = if cfg!(target_os = "windows") {
&[
@@ -232,6 +232,35 @@ fn resolve_hermes_desktop_exe(install_root: &std::path::Path) -> Option
None
}
+/// True when a prior install completed (bootstrap-complete marker present) AND a
+/// launchable desktop app exists on disk. Used by the installer's launcher fast
+/// path so a bare re-open just opens Hermes instead of re-running setup.
+pub(crate) fn hermes_is_installed(install_root: &std::path::Path) -> bool {
+ install_root.join(".hermes-bootstrap-complete").exists()
+ && resolve_hermes_desktop_exe(install_root).is_some()
+}
+
+/// Spawn the already-built desktop app, detached. Returns Err if no built app
+/// exists or the spawn fails, so the caller can fall back to showing the
+/// installer UI.
+pub(crate) fn spawn_installed_desktop(install_root: &std::path::Path) -> std::io::Result<()> {
+ let exe = resolve_hermes_desktop_exe(install_root).ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::NotFound, "no built Hermes desktop app")
+ })?;
+ let mut cmd = std::process::Command::new(&exe);
+ cmd.current_dir(exe.parent().unwrap_or(install_root));
+ #[cfg(target_os = "windows")]
+ {
+ use std::os::windows::process::CommandExt;
+ // DETACHED_PROCESS = 0x00000008 — keep the desktop alive after the
+ // installer exits, mirroring launch_hermes_desktop. Kept correct here
+ // even though the only caller is macOS-gated today, so future reuse on
+ // Windows doesn't reintroduce the relaunch race.
+ cmd.creation_flags(0x0000_0008);
+ }
+ cmd.spawn().map(|_child| ())
+}
+
// ---------------------------------------------------------------------------
// Bootstrap implementation
// ---------------------------------------------------------------------------
diff --git a/apps/bootstrap-installer/src-tauri/src/lib.rs b/apps/bootstrap-installer/src-tauri/src/lib.rs
index a710ce9b5..bed06b971 100644
--- a/apps/bootstrap-installer/src-tauri/src/lib.rs
+++ b/apps/bootstrap-installer/src-tauri/src/lib.rs
@@ -50,6 +50,20 @@ impl AppMode {
}
}
+/// Returns true when the args request a forced installer UI (repair/reinstall)
+/// via `--reinstall` or `--repair`, which overrides the macOS launcher
+/// fast-path so a broken install can be repaired. Arg-iterator generic so it's
+/// unit-testable, mirroring `AppMode::from_args`. Independent of mode selection:
+/// these flags never flip Install<->Update.
+pub fn force_setup_from_args(args: I) -> bool
+where
+ I: IntoIterator- ,
+ S: AsRef,
+{
+ args.into_iter()
+ .any(|a| a.as_ref() == "--reinstall" || a.as_ref() == "--repair")
+}
+
/// Process-wide install state, shared across Tauri commands.
///
/// The bootstrap is a one-shot, single-tenant process — we only need one
@@ -85,7 +99,11 @@ pub fn run() {
let _guard = paths::init_logging();
let mode = AppMode::from_args(std::env::args().skip(1));
- tracing::info!(?mode, "Hermes Setup starting");
+ // Escape hatch: `--reinstall`/`--repair` forces the installer UI even when
+ // Hermes is already installed, so users can re-run setup to repair a broken
+ // install instead of the launcher fast path silently relaunching the app.
+ let force_setup = force_setup_from_args(std::env::args().skip(1));
+ tracing::info!(?mode, force_setup, "Hermes installer starting");
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
@@ -93,6 +111,60 @@ pub fn run() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.manage(Arc::new(AppState::new(mode)))
+ .setup(move |app| {
+ use tauri::Manager;
+ // Launcher fast path (macOS only): a bare ("Install") launch when
+ // Hermes is already installed should NOT show the installer or
+ // rebuild — it should just open the app, so the /Applications
+ // "Hermes" doubles as a normal launcher (first run installs, every
+ // later run launches instantly). The window is kept hidden until
+ // here via `"visible": false` so this path never flashes a window.
+ //
+ // Gated to macOS deliberately: on Windows/Linux the installer keeps
+ // its existing behavior (Windows users relaunch via the Start
+ // Menu/Desktop "Hermes" shortcuts that install.ps1 creates, and a
+ // reliable detached relaunch there needs the DETACHED_PROCESS +
+ // startup-grace handling used by launch_hermes_desktop — out of
+ // scope here). So this is a pure no-op on non-macOS.
+ //
+ // `--reinstall`/`--repair` opts out so a broken install can be
+ // repaired by re-running setup instead of launching the bad app.
+ if cfg!(target_os = "macos") && mode == AppMode::Install && !force_setup {
+ let install_root = paths::hermes_home().join("hermes-agent");
+ if bootstrap::hermes_is_installed(&install_root) {
+ match bootstrap::spawn_installed_desktop(&install_root) {
+ Ok(()) => {
+ // Brief grace so the spawned app is registered
+ // before we exit (mirrors launch_hermes_desktop).
+ std::thread::sleep(std::time::Duration::from_millis(200));
+ tracing::info!(
+ "hermes already installed — relaunched desktop; exiting installer"
+ );
+ app.handle().exit(0);
+ return Ok(());
+ }
+ Err(err) => {
+ tracing::warn!(
+ ?err,
+ "relaunch of installed desktop failed; showing installer UI"
+ );
+ }
+ }
+ }
+ }
+ // First run / repair install, or Update mode: reveal the UI.
+ match app.get_webview_window("main") {
+ Some(win) => {
+ if let Err(err) = win.show() {
+ tracing::error!(?err, "failed to show main installer window");
+ }
+ }
+ None => {
+ tracing::error!("main installer window not found; installer UI will not appear");
+ }
+ }
+ Ok(())
+ })
.invoke_handler(tauri::generate_handler![
// Mode (install vs update)
get_mode,
@@ -115,7 +187,7 @@ pub fn run() {
#[cfg(test)]
mod tests {
- use super::AppMode;
+ use super::{force_setup_from_args, AppMode};
#[test]
fn bare_args_are_install() {
@@ -131,4 +203,30 @@ mod tests {
AppMode::Update
);
}
+
+ #[test]
+ fn reinstall_and_repair_flags_force_setup() {
+ assert!(force_setup_from_args(["--reinstall"]));
+ assert!(force_setup_from_args(["--repair"]));
+ assert!(force_setup_from_args(["--foo", "--repair", "--bar"]));
+ }
+
+ #[test]
+ fn bare_or_unrelated_args_do_not_force_setup() {
+ assert!(!force_setup_from_args(Vec::::new()));
+ assert!(!force_setup_from_args(["--foo", "bar"]));
+ // --update must not be mistaken for a force-setup flag.
+ assert!(!force_setup_from_args(["--update"]));
+ }
+
+ #[test]
+ fn force_setup_flags_do_not_affect_mode_selection() {
+ // The repair flags must never flip Install<->Update.
+ assert_eq!(AppMode::from_args(["--reinstall"]), AppMode::Install);
+ assert_eq!(AppMode::from_args(["--repair"]), AppMode::Install);
+ assert_eq!(
+ AppMode::from_args(["--update", "--reinstall"]),
+ AppMode::Update
+ );
+ }
}
diff --git a/apps/bootstrap-installer/src-tauri/tauri.conf.json b/apps/bootstrap-installer/src-tauri/tauri.conf.json
index daf4d2c6e..35ad53bc0 100644
--- a/apps/bootstrap-installer/src-tauri/tauri.conf.json
+++ b/apps/bootstrap-installer/src-tauri/tauri.conf.json
@@ -1,6 +1,6 @@
{
"$schema": "https://schema.tauri.app/config/2",
- "productName": "Hermes Setup",
+ "productName": "Hermes",
"version": "0.0.1",
"identifier": "com.nousresearch.hermes.setup",
"build": {
@@ -13,7 +13,7 @@
"windows": [
{
"label": "main",
- "title": "Hermes Setup",
+ "title": "Hermes",
"width": 880,
"height": 620,
"minWidth": 720,
@@ -22,7 +22,8 @@
"fullscreen": false,
"decorations": true,
"transparent": false,
- "center": true
+ "center": true,
+ "visible": false
}
],
"security": {
@@ -33,7 +34,7 @@
"bundle": {
"active": true,
"category": "DeveloperTool",
- "shortDescription": "Hermes Setup",
+ "shortDescription": "Hermes",
"longDescription": "Installs Hermes Agent on your machine. Drives scripts/install.ps1 (Windows) and scripts/install.sh (macOS/Linux).",
"publisher": "Nous Research",
"copyright": "Copyright © 2026 Nous Research",