mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
macOS desktop: install + in-app self-update (#35607)
* fix(installer): align macOS HERMES_HOME with the rest of the stack
paths.rs computed the macOS Hermes home as ~/Library/Application Support/
hermes, but nothing else does: hermes_constants.get_hermes_home() (Python),
scripts/install.sh, and the Electron desktop's resolveHermesHome() all use
~/.hermes on macOS. The drift meant the Tauri installer wrote the install to
one directory and the desktop looked for it in another, so a fresh GUI
install never found its backend (the file's own comment warned this exact
drift would break things). Use ~/.hermes on macOS to match.
* fix(install.sh): always emit a stage result frame on failure
Stage helpers (clone_repo, install_deps, check_python, …) were written for
the monolithic flow and call `exit 1` on failure. Under `--stage`, that
terminated the process before the JSON result frame was printed, so the
installer's parse_stage_result saw "no frame" instead of a clean
{ok:false,...} contract response. Run the stage body in a subshell so an
`exit` only unwinds the subshell and the parent still emits the frame.
* feat(install.sh): auto-provision git on macOS/Linux (parity with install.ps1)
install.ps1 downloads PortableGit on Windows, but install.sh just printed a
"please install git" hint and exited — so a fresh Mac with no developer tools
(no Xcode CLT → no git) couldn't get past the clone step. check_git now tries
to install git before bailing:
- macOS: Homebrew if present (headless), else `xcode-select --install`
(the CLT prompt also provides the compiler some wheels need), polling for
git to appear.
- Linux: apt/dnf/pacman via sudo when available.
Falls back to the manual instructions only if auto-provision fails.
* feat(desktop): in-app GUI+backend self-update on macOS/Linux
On Windows the staged Hermes-Setup binary drives updates (quit → hermes
update → hermes desktop --build-only → relaunch). The mac drag-install has no
such binary, so "Update now" previously just printed `hermes update`.
Since there's no venv-shim file lock on POSIX, the desktop can drive the whole
update itself. applyUpdates now, when no staged updater exists on mac/linux:
1. runs `hermes update --yes [--branch <current>]` (backend git pull + deps),
2. runs `hermes desktop --build-only` (OS-aware GUI rebuild) with the
Hermes-managed Node + venv on PATH,
3. spawns a detached swapper that waits for this process to exit, dittos the
freshly built Hermes.app over the running bundle, clears quarantine, and
relaunches.
Degrades to "backend updated — restart to load the new GUI" if the rebuild
fails or there's no .app bundle to swap (dev run, Linux AppImage).
* chore: uptick
This commit is contained in:
parent
dfc2fd887e
commit
5f9e0545ca
4 changed files with 340 additions and 14 deletions
|
|
@ -233,6 +233,23 @@ json_escape() {
|
|||
-e 's/"/\\"/g'
|
||||
}
|
||||
|
||||
# npm rewrites tracked package-lock.json files non-deterministically during
|
||||
# `npm install` / `npm run pack`. On a managed install those diffs are never
|
||||
# intentional, but they leave the checkout dirty — which forces `hermes update`
|
||||
# to autostash on every run and makes branch switches fragile. Restore them so
|
||||
# a fresh install ends with a clean tree. Best-effort; only touches lockfiles.
|
||||
restore_dirty_lockfiles() {
|
||||
local repo="${1:-$INSTALL_DIR}"
|
||||
[ -n "$repo" ] && [ -d "$repo/.git" ] || return 0
|
||||
command -v git >/dev/null 2>&1 || return 0
|
||||
local dirty
|
||||
dirty=$(git -C "$repo" diff --name-only 2>/dev/null | grep 'package-lock\.json$' || true)
|
||||
[ -z "$dirty" ] && return 0
|
||||
echo "$dirty" | while IFS= read -r f; do
|
||||
[ -n "$f" ] && git -C "$repo" checkout -- "$f" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
emit_manifest() {
|
||||
# Stage-Desktop is included only with --include-desktop, mirroring
|
||||
# install.ps1: the signed bootstrap installer (Hermes-Setup) passes it so
|
||||
|
|
@ -617,6 +634,73 @@ ensure_fts5() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Best-effort automatic git provisioning, mirroring install.ps1's Install-Git
|
||||
# (which downloads PortableGit on Windows). git is required to clone the repo,
|
||||
# and a fresh "normie" machine with no developer tools won't have it. Returns 0
|
||||
# if git is available afterwards, non-zero otherwise (caller prints manual
|
||||
# instructions and aborts).
|
||||
attempt_install_git() {
|
||||
case "$OS" in
|
||||
macos)
|
||||
# Prefer Homebrew — fully headless when present.
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
log_info "Installing Git via Homebrew..."
|
||||
brew install git >/dev/null 2>&1 || true
|
||||
command -v git >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
# Fall back to Apple Command Line Tools, which provide git AND the
|
||||
# compiler some Python wheels need. `xcode-select --install` pops a
|
||||
# system dialog (Apple gates CLT behind it — it cannot be fully
|
||||
# silent without MDM), so we trigger it and poll for git to appear.
|
||||
if command -v xcode-select >/dev/null 2>&1; then
|
||||
log_info "Requesting Apple Command Line Tools (provides git + compiler)..."
|
||||
log_info "If a macOS dialog appears, click \"Install\" and accept the license."
|
||||
xcode-select --install >/dev/null 2>&1 || true
|
||||
local waited=0
|
||||
local timeout=900
|
||||
while [ "$waited" -lt "$timeout" ]; do
|
||||
if command -v git >/dev/null 2>&1 && git --version >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
if [ $((waited % 60)) -eq 0 ]; then
|
||||
log_info "Still waiting for Command Line Tools install ($((waited / 60))m)..."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
return 1
|
||||
;;
|
||||
linux)
|
||||
local sudo_cmd=""
|
||||
if [ "$(id -u 2>/dev/null || echo 1000)" -ne 0 ]; then
|
||||
command -v sudo >/dev/null 2>&1 && sudo_cmd="sudo"
|
||||
fi
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian)
|
||||
log_info "Installing Git via apt..."
|
||||
$sudo_cmd env DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true
|
||||
$sudo_cmd env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git >/dev/null 2>&1 || true
|
||||
;;
|
||||
fedora)
|
||||
log_info "Installing Git via dnf..."
|
||||
$sudo_cmd dnf install -y git >/dev/null 2>&1 || true
|
||||
;;
|
||||
arch)
|
||||
log_info "Installing Git via pacman..."
|
||||
$sudo_cmd pacman -S --noconfirm git >/dev/null 2>&1 || true
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
command -v git >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
check_git() {
|
||||
log_info "Checking Git..."
|
||||
|
||||
|
|
@ -638,7 +722,15 @@ check_git() {
|
|||
fi
|
||||
fi
|
||||
|
||||
log_info "Please install Git:"
|
||||
# Try to install it automatically before giving up (parity with install.ps1).
|
||||
log_info "Attempting to install Git automatically..."
|
||||
if attempt_install_git; then
|
||||
GIT_VERSION=$(git --version | awk '{print $3}')
|
||||
log_success "Git $GIT_VERSION installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Could not install Git automatically. Please install it manually:"
|
||||
|
||||
case "$OS" in
|
||||
linux)
|
||||
|
|
@ -1833,7 +1925,8 @@ install_node_deps() {
|
|||
log_success "TUI dependencies installed"
|
||||
fi
|
||||
|
||||
|
||||
# Keep the checkout clean so `hermes update` doesn't autostash every run.
|
||||
restore_dirty_lockfiles "$INSTALL_DIR"
|
||||
}
|
||||
|
||||
run_setup_wizard() {
|
||||
|
|
@ -2232,6 +2325,10 @@ install_desktop() {
|
|||
return 1
|
||||
fi
|
||||
log_success "Desktop app built: $app"
|
||||
|
||||
# `npm install` + `npm run pack` rewrite lockfiles; restore them so the
|
||||
# checkout stays clean for the next `hermes update`.
|
||||
restore_dirty_lockfiles "$INSTALL_DIR"
|
||||
}
|
||||
|
||||
# Each --stage runs in its own process, so (unlike the monolithic main() where
|
||||
|
|
@ -2354,8 +2451,14 @@ run_stage_protocol() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
# Run the stage body in a subshell so a stage helper that calls `exit 1`
|
||||
# on failure (clone_repo, install_deps, etc. were written for the monolithic
|
||||
# flow) only exits the subshell — the parent still reaches the JSON result
|
||||
# frame below. Without this, a failed --stage would terminate the process
|
||||
# before emitting the frame and the Rust/Electron parser would see "no
|
||||
# result frame" instead of a clean {ok:false} contract response.
|
||||
set +e
|
||||
run_stage_body "$stage"
|
||||
( run_stage_body "$stage" )
|
||||
local code=$?
|
||||
set -e
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue