diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 1cd3e6a6e6a..849cbf5096e 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -22,6 +22,22 @@ If you're hacking on Hermes from a clone outside `HERMES_HOME/hermes-agent`, poi HERMES_DESKTOP_HERMES_ROOT=/path/to/your/clone npm run dev ``` +### Runtime prerequisites + +Hermes Desktop needs: + +- **Python 3.11+** — for the agent runtime, dashboard backend, and tool execution. +- **Git for Windows** (Windows only) — provides Git Bash, which Hermes' terminal tool calls directly. Linux and macOS already ship a system bash. + +The packaged Windows installer (`Hermes-*.exe`) detects both at install time. If either is missing it offers to install them via `winget install -e --id Python.Python.3.11` and `winget install -e --id Git.Git`. If `winget` isn't available the installer shows manual download URLs and lets you continue. The MSI installer (`Hermes-*.msi`) doesn't run the prereq page — enterprise deploys are expected to handle prereqs out-of-band. + +For dev (`npm run dev`) the same checks happen at first launch via the Electron bootstrapper, which throws a clear error if either prereq is missing. Manual install commands you can run yourself: + +```powershell +winget install -e --id Python.Python.3.11 +winget install -e --id Git.Git +``` + ## Development ```bash diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index aeb93effe1e..56430ef6600 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -440,6 +440,46 @@ function findSystemPython() { return null } +// findGitBash — locate bash.exe on Windows. Hermes' terminal tool requires +// bash (POSIX shell), and on Windows that's almost always Git for Windows' +// bundled Git Bash. We check the same set of locations tools/environments/ +// local.py:_find_bash() checks at runtime, so a positive result here means +// the agent will be able to start a terminal too. +// +// On non-Windows hosts bash is part of the OS and this just returns the +// first bash on PATH. +function findGitBash() { + if (!IS_WINDOWS) { + return findOnPath('bash') + } + + // install.ps1 drops PortableGit at %LOCALAPPDATA%\hermes\git\... — checked + // first so users who installed via install.ps1 are detected before we + // start probing system-wide locations. + const localAppData = process.env.LOCALAPPDATA || '' + const candidates = [] + if (localAppData) { + candidates.push(path.join(localAppData, 'hermes', 'git', 'bin', 'bash.exe')) + candidates.push(path.join(localAppData, 'hermes', 'git', 'usr', 'bin', 'bash.exe')) + } + + // Standard Git for Windows install locations. + candidates.push(path.join(process.env['ProgramFiles'] || 'C:\\Program Files', 'Git', 'bin', 'bash.exe')) + candidates.push(path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe')) + if (localAppData) { + candidates.push(path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe')) + } + + for (const candidate of candidates) { + if (fileExists(candidate)) return candidate + } + + // Last resort — bash on PATH (covers WSL bash, MSYS2, custom installs). + // On WSL hosts findOnPath itself filters out Windows-binary paths via + // isWindowsBinaryPathInWsl, so we won't hand back a wsl.exe shim either. + return findOnPath('bash') +} + function getVenvPython(venvRoot) { return path.join(venvRoot, IS_WINDOWS ? path.join('Scripts', 'python.exe') : path.join('bin', 'python')) } @@ -953,6 +993,22 @@ async function ensureRuntime(backend) { await runProcess(systemPython, ['-m', 'venv', VENV_ROOT]) } + // Step 2b: On Windows, preflight Git Bash. Hermes' terminal tool calls + // bash.exe directly (tools/environments/local.py); without it the agent + // can't run a terminal command. We surface this here as a clear, actionable + // error rather than letting the user discover it on their first chat + // ("hey, run `ls`" → opaque tool failure). The NSIS prereq page handles + // this for installer users; this check catches everyone else (.msi users, + // npm run dev with a fresh checkout, manual installs, etc.). + if (IS_WINDOWS && !findGitBash()) { + throw new Error( + 'Git for Windows is required for Hermes on Windows (provides Git Bash, ' + + "which the agent's terminal tool uses). Install it from " + + 'https://git-scm.com/download/win or run `winget install -e --id Git.Git`, ' + + 'then relaunch Hermes.' + ) + } + // Step 3: Ensure deps are installed. We compare a marker against the // active pyproject.toml's hash and only run pip when something changed — // keeps `npm run dev` boots fast on a stable repo. diff --git a/apps/desktop/installer/prereq-check.nsh b/apps/desktop/installer/prereq-check.nsh new file mode 100644 index 00000000000..fe0a80b05d9 --- /dev/null +++ b/apps/desktop/installer/prereq-check.nsh @@ -0,0 +1,293 @@ +; ============================================================================ +; Hermes Desktop installer — prerequisite detection page +; ============================================================================ +; +; A native NSIS Wizard page (using nsDialogs) inserted between the directory +; selection page and the install-files page. Detects Python 3.11+ and Git +; for Windows; offers to install missing prereqs via winget. +; +; Page sequence: +; Welcome → Directory → [PrereqPage] → InstFiles → Finish +; +; Hooks used: +; customPageAfterChangeDir — page declaration (electron-builder's hook for +; inserting a page between Directory and InstFiles) +; customInstall — execute winget for any prereqs the user +; checked on the page +; +; The Function declarations live at top-level in this file so they're parsed +; at include time; the customPageAfterChangeDir macro references them via +; the Page directive so the optimizer doesn't strip them. customInstall has +; a defensive runtime reference too, in case the customPageAfterChangeDir +; hook isn't expanded by some future electron-builder version. +; +; UAC behavior: +; Python is installed with --scope user (no UAC prompt). +; Git for Windows always installs per-machine and triggers a UAC prompt. +; We pre-warn the user via the page footer; the UAC dialog may appear +; behind the installer, so BringToFront is called after each winget run. +; +; Detection: +; Python: try `py -3.11`, `py -3.12`, `py -3.13`, `py -3.14` in order. +; The Python launcher returns exit 0 only when that specific version is +; installed. The Microsoft Store "Python stub" doesn't install py.exe, +; so users with only the stub get correctly classified as not-installed. +; +; Git: `where git` returns exit 0 if git is on PATH. +; +; winget: `where winget` returns exit 0 on Win11 / Win10 1809+ with App +; Installer. If unavailable, the page shows manual download URLs. +; +; Skip behaviors: +; - Both prereqs already installed → page is auto-skipped via Abort +; - Silent install (/S) → customInstall winget block skips +; - User unchecks both checkboxes → page advances without running winget +; ============================================================================ + +!include "LogicLib.nsh" +!include "nsDialogs.nsh" +!include "WinMessages.nsh" + +Var HermesDialog +Var HermesPyStatusLabel +Var HermesPyCheckbox +Var HermesGitStatusLabel +Var HermesGitCheckbox +Var HermesFooterLabel +Var HermesHasWinget +Var HermesHasPython +Var HermesHasGit +Var HermesInstallPython +Var HermesInstallGit + +; ---------------------------------------------------------------------------- +; HermesDetectPrereqs — populates $HermesHasWinget / $HermesHasPython / +; $HermesHasGit with "0" or "1". Called from the page-create function. +; ---------------------------------------------------------------------------- +Function HermesDetectPrereqs + ; --- winget --- + nsExec::Exec 'cmd.exe /c where winget >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasWinget "1" + ${Else} + StrCpy $HermesHasWinget "0" + ${EndIf} + + ; --- Python 3.11+ --- + ; The py launcher returns exit 0 only when that specific version is + ; installed. We probe each version Hermes' pyproject.toml accepts. + StrCpy $HermesHasPython "0" + nsExec::Exec 'cmd.exe /c py -3.11 --version >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasPython "1" + ${Else} + nsExec::Exec 'cmd.exe /c py -3.12 --version >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasPython "1" + ${Else} + nsExec::Exec 'cmd.exe /c py -3.13 --version >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasPython "1" + ${Else} + nsExec::Exec 'cmd.exe /c py -3.14 --version >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasPython "1" + ${EndIf} + ${EndIf} + ${EndIf} + ${EndIf} + + ; --- Git --- + nsExec::Exec 'cmd.exe /c where git >nul 2>&1' + Pop $0 + ${If} $0 == 0 + StrCpy $HermesHasGit "1" + ${Else} + StrCpy $HermesHasGit "0" + ${EndIf} +FunctionEnd + +; ---------------------------------------------------------------------------- +; HermesPrereqPageCreate — builds the prereq page UI. If both prereqs are +; already installed we Abort, which causes NSIS to skip directly to the next +; page in the sequence (InstFiles). +; ---------------------------------------------------------------------------- +Function HermesPrereqPageCreate + Call HermesDetectPrereqs + + ${If} $HermesHasPython == "1" + ${AndIf} $HermesHasGit == "1" + Abort + ${EndIf} + + nsDialogs::Create 1018 + Pop $HermesDialog + ${If} $HermesDialog == error + Abort + ${EndIf} + + StrCpy $HermesInstallPython "0" + StrCpy $HermesInstallGit "0" + + ; Page title (bold) and subtitle. We can't use MUI_HEADER_TEXT here — + ; electron-builder's NSIS template configures MUI internally but doesn't + ; expose the header-text macros to user includes. So we render our own + ; title in the page body using a label with bold font. + ${NSD_CreateLabel} 0u 0u 100% 10u "System Requirements" + Pop $0 + CreateFont $1 "$(^Font)" "10" "700" + SendMessage $0 ${WM_SETFONT} $1 0 + + ${NSD_CreateLabel} 0u 12u 100% 18u "Hermes Agent needs Python 3.11+ and Git for Windows to run. Items already installed are listed as detected; missing items can be installed automatically." + Pop $0 + + ; --- Python panel --- + ${NSD_CreateGroupBox} 0u 34u 100% 32u "Python 3.11+" + Pop $0 + ${If} $HermesHasPython == "1" + ${NSD_CreateLabel} 8u 46u 95% 12u "Detected on your system." + Pop $HermesPyStatusLabel + ${Else} + ${If} $HermesHasWinget == "1" + ${NSD_CreateLabel} 8u 44u 95% 9u "Not detected." + Pop $HermesPyStatusLabel + ${NSD_CreateCheckbox} 8u 54u 95% 10u "Install Python 3.11 (per-user install, no admin prompt)" + Pop $HermesPyCheckbox + ${NSD_Check} $HermesPyCheckbox + ${Else} + ${NSD_CreateLabel} 8u 44u 95% 20u "Not detected. Install manually from https://www.python.org/downloads/ and re-run this installer." + Pop $HermesPyStatusLabel + ${EndIf} + ${EndIf} + + ; --- Git panel --- + ${NSD_CreateGroupBox} 0u 70u 100% 32u "Git for Windows (provides Git Bash)" + Pop $0 + ${If} $HermesHasGit == "1" + ${NSD_CreateLabel} 8u 82u 95% 12u "Detected on your system." + Pop $HermesGitStatusLabel + ${Else} + ${If} $HermesHasWinget == "1" + ${NSD_CreateLabel} 8u 80u 95% 9u "Not detected. Required by Hermes' terminal tool." + Pop $HermesGitStatusLabel + ${NSD_CreateCheckbox} 8u 90u 95% 10u "Install Git for Windows (administrator approval required)" + Pop $HermesGitCheckbox + ${NSD_Check} $HermesGitCheckbox + ${Else} + ${NSD_CreateLabel} 8u 80u 95% 20u "Not detected. Install manually from https://git-scm.com/download/win and re-run this installer." + Pop $HermesGitStatusLabel + ${EndIf} + ${EndIf} + + ; --- Footer (UAC notice when Git install will run) --- + ${If} $HermesHasGit == "0" + ${AndIf} $HermesHasWinget == "1" + ${NSD_CreateLabel} 0u 108u 100% 30u "Note: installing Git for Windows requires administrator approval. The User Account Control prompt may appear behind this window — use the taskbar to find it if needed." + Pop $HermesFooterLabel + ${EndIf} + + nsDialogs::Show +FunctionEnd + +; ---------------------------------------------------------------------------- +; HermesPrereqPageLeave — read checkbox states when the user clicks Next. +; Variables stay at "0" if a checkbox doesn't exist (because the +; corresponding prereq is already installed or winget isn't available). +; ---------------------------------------------------------------------------- +Function HermesPrereqPageLeave + ${If} $HermesHasPython == "0" + ${AndIf} $HermesHasWinget == "1" + ${NSD_GetState} $HermesPyCheckbox $HermesInstallPython + ${EndIf} + ${If} $HermesHasGit == "0" + ${AndIf} $HermesHasWinget == "1" + ${NSD_GetState} $HermesGitCheckbox $HermesInstallGit + ${EndIf} +FunctionEnd + +; ---------------------------------------------------------------------------- +; Page declaration — inserted between the Directory page and InstFiles via +; the customPageAfterChangeDir hook (defined in +; node_modules/app-builder-lib/templates/nsis/assistedInstaller.nsh, included +; whenever build.nsis.oneClick=false). +; +; Note: NSIS's optimizer emits "warning 6010: install function ... not +; referenced" for these functions because Page custom directives don't count +; as references in the optimizer's reference-tracking pass. We set +; build.nsis.warningsAsErrors=false in package.json so this warning doesn't +; fail the build. The functions ARE actually called by NSIS at page-display +; time — the optimizer just can't see it statically. +; ---------------------------------------------------------------------------- +!macro customPageAfterChangeDir + Page custom HermesPrereqPageCreate HermesPrereqPageLeave +!macroend + +; ---------------------------------------------------------------------------- +; customInstall — runs the actual winget commands for whatever prereqs the +; user checked on the page. Output streams to the install progress log. +; ---------------------------------------------------------------------------- +!macro customInstall + ; Skip on silent installs (managed deploys handle prereqs out-of-band). + IfSilent hermes_prereq_install_done + + ${If} $HermesInstallPython == "1" + ; Python with --scope user installs to %LOCALAPPDATA%\Programs\Python\ + ; — no UAC, no foreground chain to preserve. nsExec::ExecToLog gives + ; us live output streaming to the install log. + DetailPrint "Installing Python 3.11+ via winget (silent per-user install, no admin prompt)..." + nsExec::ExecToLog 'winget install -e --id Python.Python.3.11 --scope user --silent --disable-interactivity --accept-package-agreements --accept-source-agreements' + Pop $0 + ${If} $0 != 0 + DetailPrint "Python install via winget exited with code $0." + MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "Python install via winget did not complete successfully (exit code $0).$\r$\n$\r$\nYou can install Python 3.11+ manually from https://www.python.org/downloads/ after Hermes setup finishes. Hermes will not run until Python is installed." + ${Else} + DetailPrint "Python 3.11+ installed successfully." + ${EndIf} + ${EndIf} + + ${If} $HermesInstallGit == "1" + ; Git for Windows always installs per-machine and triggers UAC. We use + ; ExecShellWait (NSIS's wrapper around Windows ShellExecute) instead of + ; nsExec::ExecToLog because ShellExecute preserves the foreground focus + ; chain across non-elevated → elevated process spawns. With nsExec the + ; intermediate hidden winget.exe breaks that chain and UAC ends up + ; behind the installer window. + ; + ; Trade-off: ExecShellWait doesn't capture output, so winget runs in + ; its own console window. The console flashes briefly while winget + ; downloads, then UAC fires for the elevated Git installer with + ; correct foreground promotion. + DetailPrint "Installing Git for Windows via winget (UAC prompt will appear)..." + ExecShellWait "open" "winget" "install -e --id Git.Git --silent --disable-interactivity --accept-package-agreements --accept-source-agreements" SW_SHOWNORMAL + + ; ExecShellWait returns no exit code, so verify by checking the file + ; system directly. Don't use `where git` — that reads OUR process's + ; PATH, which was captured at NSIS startup before Git's installer ran + ; and modified the system PATH. Until we restart, the new PATH isn't + ; visible to us. Probe Git's standard install locations instead. + StrCpy $0 "0" ; "git found" flag + ${If} ${FileExists} "$PROGRAMFILES64\Git\bin\bash.exe" + StrCpy $0 "1" + ${ElseIf} ${FileExists} "$PROGRAMFILES\Git\bin\bash.exe" + StrCpy $0 "1" + ${ElseIf} ${FileExists} "$PROGRAMFILES32\Git\bin\bash.exe" + StrCpy $0 "1" + ${ElseIf} ${FileExists} "$LOCALAPPDATA\Programs\Git\bin\bash.exe" + StrCpy $0 "1" + ${EndIf} + + ${If} $0 == "1" + DetailPrint "Git for Windows installed successfully." + ${Else} + DetailPrint "Git for Windows install did not complete (bash.exe not found at standard install locations)." + MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "Git for Windows install via winget did not complete successfully.$\r$\n$\r$\nYou can install Git for Windows manually from https://git-scm.com/download/win after Hermes setup finishes. Hermes' terminal tool will not work until Git Bash is available." + ${EndIf} + ${EndIf} + + hermes_prereq_install_done: +!macroend diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 66ff9d03a13..cfd3630dd10 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -186,7 +186,9 @@ "allowToChangeInstallationDirectory": true, "perMachine": false, "shortcutName": "Hermes", - "uninstallDisplayName": "Hermes" + "uninstallDisplayName": "Hermes", + "include": "installer/prereq-check.nsh", + "warningsAsErrors": false } } }