mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-19 15:18:03 +00:00
Hermes-Setup.exe is a small signed Rust+Tauri binary that drives
scripts/install.ps1 stage-by-stage with a native UI matching the
desktop's design language. Replaces the chicken-and-egg pattern of
shipping a 200MB Electron app whose first launch existed only to
run install.ps1.
The architecture:
Rust backend (src-tauri/):
bootstrap.rs orchestrator -- Tauri commands, stage iteration
install_script.rs resolve install.ps1 (dev checkout, cache, GitHub raw)
powershell.rs spawn powershell, line-stream stdout/stderr, parse JSON
events.rs BootstrapEvent types -- mirror bootstrap-runner.cjs
paths.rs HERMES_HOME resolution + tracing log setup
build.rs bakes BUILD_PIN_COMMIT / BUILD_PIN_BRANCH from
'git rev-parse HEAD' at compile time
React frontend (src/):
Tauri webview rendering 4 screens (welcome / progress / success /
failure), driven by nanostores subscribing to the Rust event stream.
Visual layer reuses the desktop's styles.css wholesale via @import
so the installer and desktop never drift visually.
Distribution:
targets = ['app', 'dmg', 'appimage'] -- no NSIS/MSI wrapper. The
raw target/release/Hermes-Setup.exe IS the artifact on Windows;
.dmg + .app on macOS; AppImage on Linux. One file, double-click,
no installer-installing-an-installer pattern.
Compile-time pinning:
build.rs reads 'git rev-parse HEAD' and emits
cargo:rustc-env=BUILD_PIN_COMMIT=<sha> + BUILD_PIN_BRANCH=<branch>.
bootstrap.rs's option_env!() picks these up so the binary fetches
install.ps1 from the exact SHA it was tested against. CI / release
builds can override via HERMES_BUILD_PIN_COMMIT env var.
Windows manifest:
hermes-setup.manifest declares level='asInvoker' so the
productName 'Hermes Setup' doesn't trip Windows's installer-
detection heuristic and refuse to launch without elevation.
Also declares PerMonitorV2 DPI + UTF-8 active code page + Common
Controls v6.
Limitations of this initial version:
* No code signing -- Windows SmartScreen will warn once on Hermes-Setup.exe
('More info -> Run anyway'). The downstream binaries it produces
(Hermes.exe in win-unpacked/, the hermes CLI) are locally-built and
therefore don't carry MOTW, so they launch without SmartScreen
intervention. Cert procurement tracked separately.
* macOS and Linux build paths defined but untested -- Windows-only V1.
99 lines
3.2 KiB
Rust
99 lines
3.2 KiB
Rust
//! Event types streamed from Rust → React.
|
|
//!
|
|
//! These mirror `apps/desktop/electron/bootstrap-runner.cjs`'s event shape
|
|
//! 1:1 so the React installer code can be roughly identical to the Electron
|
|
//! install-overlay we'll replace.
|
|
//!
|
|
//! The Tauri event channel name is `"bootstrap"` for all of these — the
|
|
//! `type` discriminator on each payload is how the frontend routes.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Stage definition as reported by `install.ps1 -Manifest`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StageInfo {
|
|
pub name: String,
|
|
pub title: String,
|
|
pub category: String,
|
|
/// `needs_user_input=true` stages run with -NonInteractive and emit
|
|
/// skipped=true; the post-install wizard takes over for those.
|
|
#[serde(rename = "needs_user_input", alias = "needsUserInput")]
|
|
pub needs_user_input: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Manifest {
|
|
pub stages: Vec<StageInfo>,
|
|
#[serde(rename = "protocol_version", alias = "protocolVersion", default)]
|
|
pub protocol_version: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StageResultPayload {
|
|
pub stage: String,
|
|
pub ok: bool,
|
|
#[serde(default)]
|
|
pub skipped: bool,
|
|
#[serde(default)]
|
|
pub reason: Option<String>,
|
|
/// install.ps1 may attach stage-specific structured data here.
|
|
#[serde(default)]
|
|
pub data: Option<serde_json::Value>,
|
|
}
|
|
|
|
/// Run-state for a single stage as we transition through it.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum StageState {
|
|
Running,
|
|
Succeeded,
|
|
Skipped,
|
|
Failed,
|
|
}
|
|
|
|
/// The single event channel `bootstrap` emits these. `type` discriminates.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(tag = "type", rename_all = "lowercase")]
|
|
pub enum BootstrapEvent {
|
|
/// Sent once at the start with the full stage list.
|
|
Manifest {
|
|
stages: Vec<StageInfo>,
|
|
#[serde(rename = "protocolVersion")]
|
|
protocol_version: Option<u32>,
|
|
},
|
|
/// Stage state transition. `result` populated only on terminal states.
|
|
Stage {
|
|
name: String,
|
|
state: StageState,
|
|
#[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
|
|
duration_ms: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
result: Option<StageResultPayload>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
error: Option<String>,
|
|
},
|
|
/// Raw stdout/stderr line from install.ps1 (or our wrapper).
|
|
Log {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
stage: Option<String>,
|
|
line: String,
|
|
},
|
|
/// Sent once when all stages complete successfully.
|
|
Complete {
|
|
#[serde(rename = "installRoot")]
|
|
install_root: String,
|
|
marker: Option<serde_json::Value>,
|
|
},
|
|
/// Sent once if the run aborts.
|
|
Failed {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
stage: Option<String>,
|
|
error: String,
|
|
},
|
|
}
|
|
|
|
impl BootstrapEvent {
|
|
/// Tauri event name. Single channel for all bootstrap events; the
|
|
/// `type` tag tells the renderer how to interpret the payload.
|
|
pub const CHANNEL: &'static str = "bootstrap";
|
|
}
|