//! Hermes Setup — Tauri entrypoint. //! //! Spawns a single window pointed at the React frontend (apps/bootstrap-installer/src/). //! All install-time work lives in `bootstrap.rs` and is invoked through the Tauri //! commands registered at the bottom of `run()`. //! //! The Windows-subsystem strip lives on the binary crate (src/main.rs), not //! here — a crate-level attribute on a lib doesn't propagate to the linker //! flags of the executable that consumes it. mod bootstrap; mod events; mod install_script; mod powershell; mod paths; mod update; use std::sync::Arc; use tokio::sync::Mutex; /// How the installer was invoked. Resolved once from the process args in /// `run()` and exposed to the frontend via `get_mode` so it can route to the /// install flow (first-run onboarding) or the update flow (driven by the /// desktop app handing off via `Hermes-Setup.exe --update`). /// /// Bare launch (double-click, first-run) => Install. /// `--update` (spawned by the desktop's "Update" button) => Update. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum AppMode { Install, Update, } impl AppMode { /// Resolve the mode from an argument iterator. Anything containing the /// `--update` flag selects Update; otherwise Install. Kept arg-iterator /// generic (not reading `std::env` directly) so it's unit-testable. pub fn from_args(args: I) -> Self where I: IntoIterator, S: AsRef, { for a in args { if a.as_ref() == "--update" { return AppMode::Update; } } AppMode::Install } } /// 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 /// of these per window. `Arc>` lets command handlers grab it /// without lifetime gymnastics. pub struct AppState { pub bootstrap: Mutex>, /// How this process was launched (install vs update). Immutable for the /// lifetime of the process; read by the `get_mode` command. pub mode: AppMode, } impl AppState { fn new(mode: AppMode) -> Self { Self { bootstrap: Mutex::new(None), mode, } } } /// Frontend → Rust: which flow should the UI render? #[tauri::command] fn get_mode(state: tauri::State<'_, Arc>) -> AppMode { state.mode } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Tracing → bootstrap-installer.log under HERMES_HOME/logs/ so install // failures leave a trail for support. Console output also goes here in // debug builds. let _guard = paths::init_logging(); let mode = AppMode::from_args(std::env::args().skip(1)); // 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()) .plugin(tauri_plugin_opener::init()) .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, // Bootstrap lifecycle bootstrap::start_bootstrap, bootstrap::cancel_bootstrap, bootstrap::get_bootstrap_status, // Update lifecycle update::start_update, // Hand-off bootstrap::launch_hermes_desktop, // Diagnostics paths::get_log_path, paths::get_hermes_home, paths::open_log_dir, ]) .run(tauri::generate_context!()) .expect("error while running Hermes Setup"); } #[cfg(test)] mod tests { use super::{force_setup_from_args, AppMode}; #[test] fn bare_args_are_install() { assert_eq!(AppMode::from_args(Vec::::new()), AppMode::Install); assert_eq!(AppMode::from_args(["--foo", "bar"]), AppMode::Install); } #[test] fn update_flag_selects_update() { assert_eq!(AppMode::from_args(["--update"]), AppMode::Update); assert_eq!( AppMode::from_args(["--something", "--update", "--else"]), 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 ); } }