Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions bin/omarchy-cmd-hermes-home
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

# omarchy:summary=Resolve the shared Hermes home from a root or profile session
# omarchy:hidden=true

set -euo pipefail

hermes_home=${HERMES_HOME:-$HOME/.hermes}
if [[ $hermes_home != /* ]]; then
echo "HERMES_HOME must name an absolute Hermes data directory." >&2
exit 1
fi

# Setup, removal and themes share the root above a Hermes profiles/<name> home.
# Normalize spelling without resolving symlinks, which ownership checks retain.
hermes_home=$(realpath -ms -- "$hermes_home")
hermes_parent=${hermes_home%/*}
if [[ ${hermes_parent##*/} == "profiles" ]]; then
hermes_home=${hermes_parent%/*}
fi

if [[ $hermes_home != /* || $hermes_home == "/" ]]; then
echo "HERMES_HOME must name an absolute Hermes data directory other than /." >&2
exit 1
fi

printf '%s\n' "$hermes_home"
25 changes: 11 additions & 14 deletions bin/omarchy-install-ai-hermes
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,25 @@

set -e

# No CLI is installed here on purpose. Hermes Desktop only runs against a
# runtime built from its own commit, so it provisions one itself under
# ~/.hermes on first launch, which takes a few minutes and shows its own
# progress. Handing it the mise CLI instead fails: PyPI trails the tags, and
# the version gap fails the app's readiness probe with a 401.
HERMES_HOME=$(omarchy-cmd-hermes-home)
export HERMES_HOME

# Install the desktop here; its first launch sets up the shared, writable
# Hermes runtime inside the app.
echo "Installing Hermes Desktop..."
omarchy-pkg-add hermes-desktop

# If Hermes was already installed for the terminal, the app supersedes it: one
# machine, one Hermes. This drops that copy so the terminal, the default agent
# and the app all end up on the app's installation.
omarchy-install-hermes-cli || true
omarchy-install-hermes-cli --check-package

echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &

# Only a running Hermes can be told which skin to show, and the first launch
# takes minutes; a unit outlives this terminal and reports to the journal.
# A unit waits for the gateway to start before handing over the current skin.
echo "Matching Hermes to the current theme once it is set up..."
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme omarchy-theme-set-hermes --wait
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme \
--setenv="HERMES_HOME=$HERMES_HOME" omarchy-theme-set-hermes --wait

echo ""
echo "Hermes Desktop has been installed."
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
echo "Its first launch installs Hermes inside the app, which takes a few minutes."
echo "Update it from Hermes Desktop or with: hermes update"
141 changes: 89 additions & 52 deletions bin/omarchy-install-hermes-cli
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

# omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin
# omarchy:args=[--check|--now|--owns|--remove]
# omarchy:summary=Set up the Hermes CLI, sharing the native desktop installation when present
# omarchy:args=[--check|--check-package|--now|--owns|--remove]
# omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now

# Hermes pins every one of its dependencies exactly and declares
Expand All @@ -16,15 +16,15 @@
# dependency -- and omarchy-mise-install writes a fixed stub with nowhere to
# say otherwise.
#
# There is only ever one Hermes on a machine. hermes-desktop cannot run against
# this one -- it needs a runtime built from its own commit, and the version gap
# fails its readiness probe -- so it installs its own under ~/.hermes and puts
# that on PATH. When the package is present it therefore owns Hermes outright:
# this installer stands aside and removes its own copy, so the terminal, the
# default agent and the app are all the same installation.
# With hermes-desktop installed, the package provisions a native installation
# whose CLI and desktop can update together. Keep any old Omarchy mise CLI
# until that replacement is ready, and leave foreign environments alone.

set -euo pipefail

HERMES_HOME=$(omarchy-cmd-hermes-home)
export HERMES_HOME

mode=${1:-}

tool='pipx:hermes-agent[extras=all]'
Expand All @@ -33,29 +33,25 @@ python='3.13'
# The line that identifies the stub as this installer's; matched whole, so a
# wrapper that merely mentions the command is not mistaken for ours.
marker='# Written by omarchy-install-hermes-cli.'
native_root="$HERMES_HOME/hermes-agent"
predecessor_receipt="$native_root/.git/omarchy-mise-predecessor"

# The package, not the runtime directory: it is installed before the app has
# ever run, and that is exactly when we must not start building a second copy.
desktop_owns_hermes() {
omarchy-pkg-present hermes-desktop
}

# The venv appears at the python-deps stage, several stages before the one that
# installs the command, so its presence says nothing about being usable. The
# marker is written last, and the command is what the agent actually runs.
# Older packages forward unknown flags to Electron and call this helper on
# launch. Calling their --check would recurse. Inspect package metadata first.
desktop_supports_native_install() {
pacman -Qlq hermes-desktop 2>/dev/null | grep -xF '/usr/share/hermes-desktop/install.sh' >/dev/null
}

# The package knows whether the native checkout, wrapper and desktop build
# agree. Then probe the actual command for the flags Omarchy's agent needs.
desktop_hermes_ready() {
[[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]] || return 1

# An executable of that name proves nothing about whose it is; the app's own
# points into ~/.hermes, and anything else is not the install we are asking
# about. Matched as a plain string, because the path carries a dot and an
# unanchored pattern would also claim a wrapper pointing at ~/xhermes.
[[ -f $HOME/.local/bin/hermes ]] || return 1
grep -qF "$HOME/.hermes" "$HOME/.local/bin/hermes" || return 1

# And a marker left behind by an install whose venv has since gone answers
# for nothing, so the command has to run, exactly as a foreign one must.
hermes_prompt_ready
desktop_supports_native_install && hermes-desktop --check && hermes_prompt_ready
}

# Whether Hermes is really installed, not merely whether the stub exists. A
Expand All @@ -74,6 +70,29 @@ ours() {
grep -qxF "$marker" "$HOME/.local/bin/hermes"
}

# The package records the marked predecessor before replacing its wrapper.
# Keep that proof across interrupted setup and failed cleanup, without claiming
# a same-named environment from a symlink or a merely similar receipt.
owns_predecessor() {
[[ -d $native_root/.git && ! -L $native_root/.git && -f $predecessor_receipt && ! -L $predecessor_receipt ]] &&
cmp -s <(printf '%s\n' "$tool") "$predecessor_receipt"
}

remove_predecessor() {
echo "Hermes Desktop is ready; removing the superseded Omarchy CLI..." >&2
mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
if mise where "$tool" >/dev/null 2>&1; then
echo "Could not remove the superseded Hermes mise environment. Finish by hand:" >&2
echo " mise rm -g '$tool'" >&2
echo " mise uninstall --all '$tool'" >&2
return 1
fi
if owns_predecessor; then
rm -f "$predecessor_receipt"
fi
}

foreign_hermes() {
[[ -e $HOME/.local/bin/hermes || -L $HOME/.local/bin/hermes ]] && ! ours
}
Expand Down Expand Up @@ -112,6 +131,20 @@ hermes_prompt_ready() {
help_defines_flag '--query' "$help"
}

# The menu needs a compatible package before launching, but a cold runtime is
# expected: the desktop will install it. Inspect metadata without running it.
if [[ $mode == "--check-package" ]]; then
# Consume the whole file list: grep -q can SIGPIPE pacman on the large seed
# package, making a matching capability fail under pipefail.
if desktop_owns_hermes &&
pacman -Qlq hermes-desktop 2>/dev/null | grep -xF '/usr/share/hermes-desktop/seed/.git/config' >/dev/null; then
exit 0
else
echo "Install or update the hermes-desktop package before setting up native Hermes updates." >&2
exit 1
fi
fi

# --owns answers whether the wrapper on PATH is the one this command wrote, so
# the migration and Remove Preinstalls do not each carry their own copy of the
# marker and drift from it.
Expand All @@ -122,34 +155,31 @@ fi
# --remove tears down a Hermes CLI this installer put in place -- the mise tool
# it installed and the stub it marked -- so Remove Hermes clears a CLI the app
# never superseded (an interrupted install, or the terminal CLI from before the
# app existed) rather than leaving it stranded on PATH. The whole teardown
# turns on the marked stub, exactly as replacement does further down: without
# it nothing proves the mise environment is Omarchy's rather than one the user
# built against the same spec, and a user's stays theirs. The desktop takeover
# removes the environment without asking, but that is its own bargain -- a
# second Hermes has to go whoever built it, and the app still provides the
# command afterwards; here nothing would. Idempotent: nothing owned, nothing
# to do.
# app existed) rather than leaving it stranded on PATH. A marked stub or the
# package's exact predecessor receipt proves the mise environment is ours.
# Explicit removal needs no ready replacement. Idempotent: nothing owned,
# nothing to do.
if [[ $mode == "--remove" ]]; then
if ours; then
if ours || owns_predecessor; then
mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
rm -f "$HOME/.local/bin/hermes" 2>/dev/null || true
if ours; then rm -f "$HOME/.local/bin/hermes" 2>/dev/null || true; fi

# Every step is attempted before any is judged, and judged by what is left
# rather than by what the commands claimed: the marked stub still answering
# hermes, or mise still resolving the tool, is a CLI still installed no
# matter how the removal exited.
# Not "run --remove again": once the stub is gone nothing marks the mise
# environment as ours, so a rerun would find nothing it owns and succeed
# without touching what was left. Only the full commands finish the job.
# A legacy CLI without a receipt loses ownership when the stub goes, so
# include the complete recovery commands instead of assuming a retry can
# still identify it. Receipt-backed removals retain their proof on failure.
if ours || mise where "$tool" >/dev/null 2>&1; then
echo "Could not remove the Hermes CLI Omarchy installed. Finish by hand:" >&2
echo " rm -f ~/.local/bin/hermes" >&2
echo " mise rm -g '$tool'" >&2
echo " mise uninstall --all '$tool'" >&2
exit 1
fi
if owns_predecessor; then rm -f "$predecessor_receipt"; fi
fi

exit 0
Expand All @@ -169,31 +199,38 @@ if [[ $mode == "--check" ]]; then
if installed && hermes_prompt_ready; then exit 0; else exit 1; fi
fi

# Hand Hermes over to the app rather than keeping a second copy beside it.
# Capture ownership before the native installer replaces the marked wrapper.
# No cleanup runs on a failed install. A completed package-only setup leaves a
# receipt so a later invocation can finish removing the proven predecessor.
if desktop_owns_hermes; then
# Not gated on that copy being healthy: `mise up` can rebuild it against the
# wrong interpreter and a half-finished install answers to neither test, and
# either way it is still a second Hermes. Removing nothing is harmless.
if mise where "$tool" >/dev/null 2>&1; then
echo "Hermes Desktop provides Hermes; removing the separate CLI install..." >&2
fi
if [[ $mode == "--now" ]]; then
if ! desktop_supports_native_install; then
echo "Update the hermes-desktop package before setting up native Hermes updates." >&2
exit 1
fi
owned_cli=false
if ours || owns_predecessor; then owned_cli=true; fi

mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
hermes-desktop --install
if ! desktop_hermes_ready; then
echo "Hermes Desktop did not finish setting up a CLI with interactive queries." >&2
exit 1
fi

# Our own stub has to go with it. Left in place it still answers `hermes`
# until the app's bootstrap overwrites it, and answering means building the
# second Hermes this whole arrangement exists to avoid.
if ours; then
rm -f "$HOME/.local/bin/hermes"
if [[ $owned_cli == "true" ]]; then
remove_predecessor
fi
exit 0
fi

if desktop_hermes_ready; then
if owns_predecessor; then remove_predecessor; fi
exit 0
fi

echo "Hermes Desktop is installed but has not set Hermes up yet." >&2
echo "Launch Hermes Desktop once to finish installing it." >&2
echo "Hermes Desktop is installed but has not finished setting up Hermes." >&2
echo "Launch Hermes Desktop to finish installing it inside the app." >&2
echo "For terminal setup, run omarchy-install-hermes-cli --now." >&2
exit 1
fi

Expand Down
Loading