//! Filesystem paths + logging setup. //! //! Mirrors `hermes_constants.get_hermes_home()` from the Python CLI: //! Windows: %LOCALAPPDATA%\hermes //! macOS: ~/.hermes //! Linux: ~/.hermes (override via $HERMES_HOME) //! //! NOTE (macOS): Python's get_hermes_home(), scripts/install.sh, and the //! Electron desktop's resolveHermesHome() ALL use ~/.hermes on macOS — there //! is no ~/Library/Application Support branch anywhere else. An earlier //! version of this file used Application Support, which drifted from every //! other component: the installer wrote the install to one dir and the //! desktop looked for it in another, so first launch never found the backend. //! //! IMPORTANT: this must match exactly. Drift here means install.ps1 //! writes to one place and the installer reads from another, breaking //! the bootstrap-complete check. use std::path::{Path, PathBuf}; use tracing_appender::non_blocking::WorkerGuard; /// Returns the canonical Hermes home directory, respecting $HERMES_HOME if set. pub fn hermes_home() -> PathBuf { if let Ok(override_path) = std::env::var("HERMES_HOME") { if !override_path.trim().is_empty() { return PathBuf::from(override_path); } } #[cfg(target_os = "windows")] { // %LOCALAPPDATA%\hermes — matches scripts/install.ps1's $HermesHome. if let Some(local_app_data) = dirs::data_local_dir() { return local_app_data.join("hermes"); } } // macOS + Linux + fallback: ~/.hermes (matches Python get_hermes_home(), // install.sh, and the Electron desktop's resolveHermesHome()). if let Some(home) = dirs::home_dir() { return home.join(".hermes"); } // Last resort — current dir, almost certainly wrong but at least // doesn't panic. PathBuf::from(".hermes") } pub fn log_dir() -> PathBuf { hermes_home().join("logs") } pub fn log_path() -> PathBuf { log_dir().join("bootstrap-installer.log") } pub fn bootstrap_cache_dir() -> PathBuf { hermes_home().join("bootstrap-cache") } /// Stable location the installer copies itself to after a successful install. /// The desktop app re-invokes this with `--update`, and the start-menu / /// desktop shortcuts can point users back to it. Lives directly under /// HERMES_HOME so it survives repo checkout deletion (unlike anything under /// hermes-agent/). /// /// On Windows this is `%LOCALAPPDATA%\hermes\hermes-setup.exe`; on other /// platforms the extension differs but the directory is the same. pub fn installer_dest() -> PathBuf { let name = if cfg!(target_os = "windows") { "hermes-setup.exe" } else { "hermes-setup" }; hermes_home().join(name) } /// Copy the currently-running installer binary to `installer_dest()` so it's /// available for future `--update` runs and shortcut launches. /// /// No-ops (returns Ok) when the running exe is ALREADY the destination — which /// is exactly the case during an `--update` run (the desktop launched us FROM /// that path), where copying onto ourselves would be a Windows sharing /// violation. Best-effort: a failure here must not fail the install, so the /// caller logs and continues. pub fn copy_self_to_hermes_home() -> std::io::Result<()> { let src = std::env::current_exe()?; let dest = installer_dest(); // Skip if we're already running from the destination (update re-invocation // or a prior copy). canonicalize both so symlinks / 8.3 short paths / case // differences don't trick us into a self-copy. let same = match (src.canonicalize(), dest.canonicalize()) { (Ok(a), Ok(b)) => a == b, _ => src == dest, }; if same { tracing::info!(?dest, "installer already at destination; skipping self-copy"); return Ok(()); } if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent)?; } std::fs::copy(&src, &dest)?; tracing::info!(?src, ?dest, "copied installer to HERMES_HOME"); Ok(()) } /// Where install.ps1 writes the bootstrap-complete marker (existence-only file /// the Electron app also checks). Per main.cjs: /// const BOOTSTRAP_COMPLETE_MARKER = path.join(ACTIVE_HERMES_ROOT, '.hermes-bootstrap-complete') /// We don't always know ACTIVE_HERMES_ROOT until install.ps1 reports it, so /// this is a probe helper, not a definitive path. pub fn likely_bootstrap_marker(install_root: &Path) -> PathBuf { install_root.join(".hermes-bootstrap-complete") } /// Initializes tracing to bootstrap-installer.log under HERMES_HOME/logs/. /// Returns a guard that flushes the appender on drop — keep it alive for /// the lifetime of the process. pub fn init_logging() -> Option { let dir = log_dir(); if let Err(err) = std::fs::create_dir_all(&dir) { // No log dir → log to stderr only. Don't panic; the installer // should still be usable on an exotic filesystem. eprintln!("[hermes-setup] could not create log dir {dir:?}: {err}"); return None; } let file_appender = tracing_appender::rolling::never(&dir, "bootstrap-installer.log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); let env_filter = tracing_subscriber::EnvFilter::try_from_env("HERMES_BOOTSTRAP_LOG") .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); tracing_subscriber::fmt() .with_env_filter(env_filter) .with_writer(non_blocking) .with_ansi(false) .with_target(true) .init(); Some(guard) } // --------------------------------------------------------------------------- // Tauri commands // --------------------------------------------------------------------------- #[tauri::command] pub fn get_log_path() -> String { log_path().to_string_lossy().into_owned() } #[tauri::command] pub fn get_hermes_home() -> String { hermes_home().to_string_lossy().into_owned() } #[tauri::command] pub fn open_log_dir(app: tauri::AppHandle) -> Result<(), String> { use tauri_plugin_opener::OpenerExt; let path = log_dir(); app.opener() .open_path(path.to_string_lossy(), None::<&str>) .map_err(|e| e.to_string()) }