Install Hermes Desktop whenever Hermes is chosen as the default agent

Hermes is only ever installed through the app. Choosing it as the default agent used to stand aside for a hermes that worked but came from somewhere else, and to refuse one that predated seeded sessions; both left the machine on a Hermes that was not the app's, which is the one Omarchy prepares for in-app updates and hands the theme to. Now --check answers only for the app's own Hermes, and --now installs the app whatever answered to hermes before, saving the previous command aside as it always did for the app path. The desktop installer no longer adds the package itself, since the shared install does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Spencer BullandClaude Fable 5.1 committed 2026-09-22 23:11:16 -05:00
1 parent 44b0f2a70f
commit 73a2b91cf5
6 files changed
+176 -141

No files matched your search

+32 -64
View File
@@ -126,47 +126,15 @@ run_installer --check && fail "--check reports a link to the retired wrapper as
rm -f "$hermes"
pass "--check never runs the retired mise wrapper"
# Hermes is only ever installed through the app: a hermes that works but did
# not come from it is not the app's, so --check says no and --now installs the
# app over it, the command saved aside by the runtime setup further on.
write_hermes
run_installer --check || fail "--check follows a hermes that runs and takes seeded sessions"
pass "--check is true for a working hermes"
# The flags have to be defined by the help, not merely mentioned in it, and
# both of them: omarchy-agent passes --query to seed the session and --tui to
# keep it interactive.
OMARCHY_TEST_HERMES_HELP='Run with --tui for a terminal session; see --query in the docs.' run_installer --check &&
fail "--check accepts flags that are only mentioned"
OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query"
OMARCHY_TEST_HERMES_HELP='[-q QUERY, --query QUERY]' run_installer --check && fail "--check accepts a hermes without --tui"
pass "--check needs both flags defined, not mentioned"
chmod -x "$hermes"
run_installer --check && fail "--check accepts a hermes that is not executable"
rm -f "$hermes"
mkdir "$hermes"
run_installer --check && fail "--check accepts a directory at the command's path"
rmdir "$hermes"
ln -s "$test_tmp/nowhere" "$hermes"
run_installer --check && fail "--check accepts a dangling link"
rm -f "$hermes"
pass "--check rejects what is not a command that runs"
# A hermes the user set up themselves is what the default agent will run, so
# --now installs nothing beside it.
write_hermes
before=$(cat "$hermes")
run_installer --check && fail "--check calls a hermes without the app installed"
: >"$pkg_log"
run_installer --now || fail "--now succeeds over a working hermes of the user's own" "$(cat "$test_tmp/output")"
[[ $(cat "$hermes") == "$before" ]] || fail "--now leaves the user's hermes as it was"
[[ ! -s $pkg_log ]] || fail "--now installs a package beside a working hermes"
pass "--now leaves a working hermes of the user's own alone"
# One that runs but predates seeded sessions is theirs to update, not ours to
# replace with the app.
: >"$pkg_log"
OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --now && fail "--now replaces a hermes that predates seeded sessions"
grep -q 'Update it' "$test_tmp/output" || fail "an old hermes gets update guidance" "$(cat "$test_tmp/output")"
[[ ! -s $pkg_log ]] || fail "an old hermes has a package installed over it"
pass "--now tells the user to update a hermes that predates seeded sessions"
run_installer --now && fail "--now carries on past the mocked package failure"
grep -qx 'hermes-desktop' "$pkg_log" || fail "--now installs the app over a hermes from elsewhere" "$(cat "$test_tmp/output")"
pass "a hermes that did not come from the app is not the app's; choosing Hermes installs the app"
rm -f "$hermes"
: >"$pkg_log"
@@ -210,6 +178,27 @@ write_hermes ours
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the runtime's own command"
pass "--check needs the app's own command once the app is installed"
# The flags have to be defined by the help, not merely mentioned in it, and
# both of them: omarchy-agent passes --query to seed the session and --tui to
# keep it interactive.
OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='Run with --tui for a terminal session; see --query in the docs.' run_installer --check &&
fail "--check accepts flags that are only mentioned"
OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query"
OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='[-q QUERY, --query QUERY]' run_installer --check && fail "--check accepts a hermes without --tui"
pass "--check needs both flags defined, not mentioned"
chmod -x "$hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a hermes that is not executable"
rm -f "$hermes"
mkdir "$hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a directory at the command's path"
rmdir "$hermes"
ln -s "$test_tmp/nowhere" "$hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a dangling link"
rm -f "$hermes"
write_hermes ours
pass "--check rejects what is not a command that runs"
# Installed and finished: nothing to do, and quickly, because choosing the
# agent from the menu runs this.
: >"$pkg_log"
@@ -238,15 +227,9 @@ pass "--check does not need ~/.local/bin on the caller's PATH"
# one the agent gets.
cp "$hermes" "$mock_bin/hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls a shadowed command installed"
rm -rf "$test_home/.hermes" "$test_home/.local/bin/.hermes-before-desktop."*
run_installer --check && fail "--check calls a shadowed command of the user's own installed"
: >"$pkg_log"
run_installer --now && fail "--now reports a shadowed command as ready"
grep -qF "$mock_bin/hermes" "$test_tmp/output" || fail "--now names the command in the way" "$(cat "$test_tmp/output")"
[[ ! -s $pkg_log ]] || fail "a shadowed command has the app installed over it"
rm -f "$mock_bin/hermes"
run_installer --check || fail "--check follows the command once nothing shadows it"
pass "a hermes ahead of ~/.local/bin on PATH is reported, not installed over"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the command once nothing shadows it"
pass "a hermes ahead of ~/.local/bin on PATH is not the one the agent gets"
# The retired wrapper's environment goes with the wrapper's proof, wherever
# that is. --retire-mise is the migration's whole job; --now does the same once
@@ -272,20 +255,5 @@ OMARCHY_TEST_MISE_SIBLING=1 run_installer --retire-mise || fail "--retire-mise s
! grep -q '^mise rm' "$mise_log" || fail "a similarly named tool is taken for the retired one" "$(cat "$mise_log")"
pass "a mise tool named like the retired one does not keep the migration pending"
# The shim mise put ahead of ~/.local/bin is what the agent would run until
# the environment goes; it goes with the uninstall, and so does the saved
# wrapper, since once the environment is gone the proof would only keep
# --check saying no and claim an environment the user builds later.
write_hermes
saved="$test_home/.local/bin/.hermes-before-desktop.abc123"
mkdir -p "$saved"
write_legacy_stub "$saved/hermes"
cp "$hermes" "$mock_bin/hermes"
: >"$mise_log"; rm -f "$mise_log.removed" "$mise_log.unrequested"
run_installer --check && fail "--check calls a handover with the shim still ahead finished"
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$mock_bin/hermes" run_installer --now || fail "--now finishes the handover over a saved wrapper" "$(cat "$test_tmp/output")"
grep -qF "mise uninstall --all pipx:hermes-agent[extras=all]" "$mise_log" || fail "--now retires the environment the saved wrapper proves Omarchy's" "$(cat "$mise_log")"
[[ ! -e $mock_bin/hermes ]] || fail "the shim is gone with the environment"
[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its empty directory go once the environment is gone"
run_installer --check || fail "--check follows a finished handover" "$(cat "$test_tmp/output")"
pass "--now retires the mise Hermes once the runtime installer has saved the wrapper aside"
# --now finishing a handover, mise environment and shim included, needs the
# package fixtures and lives in hermes-desktop-install-test.sh.
+72 -8
View File
@@ -175,6 +175,26 @@ cat >"$test_tmp/bin/systemd-run" <<'MOCK'
#!/bin/bash
printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events"
MOCK
# The only thing the installer asks mise is to remove what the retired wrapper
# built. `where` finds that environment and `ls -g` lists it while a test says
# it was built and it has not been uninstalled and unrequested; uninstalling
# also takes the shim stand-in named in OMARCHY_TEST_SHIM, as mise's does.
cat >"$test_tmp/bin/mise" <<'MOCK'
#!/bin/bash
printf 'mise %s\n' "$*" >>"$OMARCHY_TEST_ROOT/mise-log"
case "$1" in
where) [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-removed ]] ;;
ls)
if [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-unrequested ]]; then
echo '{"pipx:hermes-agent[extras=all]": [{"version": "latest"}]}'
else
echo '{}'
fi
;;
rm) touch "$OMARCHY_TEST_ROOT/mise-unrequested" ;;
uninstall) touch "$OMARCHY_TEST_ROOT/mise-removed"; rm -f "${OMARCHY_TEST_SHIM:-}" ;;
esac
MOCK
chmod +x "$test_tmp/bin/"*
# Substitute only system package paths in scratch copies of the actual scripts.
@@ -279,6 +299,28 @@ run_installer && fail "a newer runtime cannot receive an older native app"
[[ ! -e $native ]] || fail "no mismatched native app was copied"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
assert_stopped "a missing updated app prevents launch and theme setup"
# The refusal comes before anything is touched: a launcher of the user's own
# beside that newer runtime is neither saved aside nor replaced by a run that
# is going to stop anyway.
own_launcher="#!/bin/bash
exec \"$test_home/tools/hermes\" \"\$@\""
printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes"
: >"$test_tmp/events"
run_cli --now && fail "a newer runtime with a launcher of its own still stops"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "the stop still carries the build-only guidance" "$(cat "$test_tmp/output")"
[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "a run that stops leaves the user's launcher as it was"
[[ -z $(find "$test_home/.local/bin" -maxdepth 1 -name '.hermes-before-desktop.*' -print) ]] || fail "a run that stops saves nothing aside"
[[ ! -s $test_tmp/events ]] || fail "a run that stops writes no commands" "$(cat "$test_tmp/events")"
# A git state that cannot be read is a refusal too, not a clean tree: back at
# the release with the index unreadable, the launcher is still left alone.
git -C "$runtime" checkout -q --detach "$release_commit"
chmod 000 "$runtime/.git/index"
: >"$test_tmp/events"
run_cli --now && { chmod 644 "$runtime/.git/index"; fail "an unreadable git state is not a clean tree"; }
chmod 644 "$runtime/.git/index"
grep -q 'Could not read' "$test_tmp/output" || fail "an unreadable git state is named" "$(cat "$test_tmp/output")"
[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "an unreadable git state leaves the user's launcher as it was"
[[ ! -s $test_tmp/events ]] || fail "an unreadable git state writes no commands" "$(cat "$test_tmp/events")"
pass "updated runtimes are preserved and never seeded with the old packaged app"
new_home dirty-desktop
@@ -350,7 +392,7 @@ printf 'local edit\n' >"$runtime/runtime.txt"
: >"$test_tmp/events"
run_installer || fail "a finished install with local edits is accepted" "$(cat "$test_tmp/output")"
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "local edits to a finished runtime are preserved"
[[ $(cat "$test_tmp/events") == $'package hermes-desktop\nlaunch' ]] || fail "a finished install is only opened" "$(cat "$test_tmp/events")"
[[ $(cat "$test_tmp/events") == launch ]] || fail "a finished install is only opened" "$(cat "$test_tmp/events")"
pass "a finished install is left as the user has it"
new_home full-history-retry
@@ -516,14 +558,14 @@ fi
SH
chmod +x "$test_home/.local/bin/hermes"
own_hermes=$(cat "$test_home/.local/bin/hermes")
run_cli --now || fail "the default agent path accepts the user's own Hermes" "$(cat "$test_tmp/output")"
[[ ! -s $test_tmp/events ]] || fail "a working Hermes of the user's own has the app installed beside it" "$(cat "$test_tmp/events")"
[[ $(cat "$test_home/.local/bin/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is left alone"
run_installer || fail "the app installs over the user's own Hermes" "$(cat "$test_tmp/output")"
grep -qx bootstrap "$test_tmp/events" || fail "the app sets up its own runtime"
run_cli --check && fail "--check calls a Hermes from elsewhere the app's"
run_cli --now || fail "choosing Hermes installs the app over a Hermes from elsewhere" "$(cat "$test_tmp/output")"
[[ $(head -2 "$test_tmp/events") == $'package hermes-desktop\nbootstrap' ]] || fail "the app and its runtime are installed over a Hermes from elsewhere" "$(cat "$test_tmp/events")"
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is saved aside"
pass "the default agent path stands aside for the user's own Hermes; the app supersedes it"
[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the previous hermes command is saved aside"
grep -qF "$hermes_home/" "$test_home/.local/bin/hermes" || fail "the command is now the app's"
run_cli --check || fail "--check follows the app's Hermes"
pass "Hermes is only installed through the app: a Hermes from elsewhere is superseded, its command saved aside"
# A finished runtime whose command is gone, or not its own, gets its command
# back from upstream's path stage alone: no bootstrap, the runtime untouched,
@@ -567,6 +609,28 @@ rm -f "$test_tmp/bin/hermes"
run_cli --check || fail "--check follows the install once nothing shadows it"
pass "a hermes ahead of ~/.local/bin on PATH is reported, not set up over"
# A machine that chose Hermes before its migration ran: the wrapper's copy the
# runtime setup saved aside proves the mise environment Omarchy's, and mise's
# shim for it sits ahead of ~/.local/bin. Choosing Hermes again finishes the
# handover: the environment goes, the shim and the saved copy with it, and
# only then is the command the one PATH finds.
new_home handover
run_cli --now || fail "handover fixture sets up" "$(cat "$test_tmp/output")"
saved="$test_home/.local/bin/.hermes-before-desktop.mise01"
mkdir -p "$saved"
printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$saved/hermes"
cp "$test_home/.local/bin/hermes" "$test_tmp/bin/hermes"
: >"$test_tmp/mise-log"; rm -f "$test_tmp/mise-removed" "$test_tmp/mise-unrequested"
OMARCHY_TEST_MISE_BUILT=1 run_cli --check && fail "--check calls a handover with the mise environment still there finished"
: >"$test_tmp/events"
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$test_tmp/bin/hermes" run_cli --now || fail "--now finishes the handover" "$(cat "$test_tmp/output")"
grep -q 'mise uninstall --all' "$test_tmp/mise-log" || fail "the environment the saved wrapper proves Omarchy's is removed" "$(cat "$test_tmp/mise-log")"
[[ ! -e $test_tmp/bin/hermes ]] || fail "the shim is gone with the environment"
[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its directory go once the environment is gone"
[[ ! -s $test_tmp/events ]] || fail "the handover sets nothing up again" "$(cat "$test_tmp/events")"
run_cli --check || fail "--check follows the finished handover"
pass "choosing Hermes again before the migration finishes the handover: the mise Hermes and its shim go"
new_home custom-profile
hermes_home="$test_home/custom home"
runtime="$hermes_home/hermes-agent"
+13 -5
View File
@@ -227,18 +227,26 @@ pass "a linked backup directory is neither proof nor touched"
write_stub
OMARCHY_TEST_DEFAULT_AGENT=hermes OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds for a Hermes default agent"
grep -q 'omarchy default agent hermes' "$test_tmp/output" || fail "a default agent that just went is told how to come back" "$(cat "$test_tmp/output")"
cat >"$hermes" <<'SH'
# A working Hermes is the app's: the package, its runtime finished and seeded,
# and the command upstream's installer wrote for it.
app_runtime="$test_home/.hermes/hermes-agent"
mkdir -p "$app_runtime/apps/desktop/release/linux-unpacked/resources"
touch "$app_runtime/.hermes-bootstrap-complete" "$app_runtime/apps/desktop/release/linux-unpacked/resources/app.asar" "$app_runtime/apps/desktop/release/linux-unpacked/resources/install-stamp.json"
printf '#!/bin/bash\nexit 0\n' >"$app_runtime/apps/desktop/release/linux-unpacked/Hermes"
chmod +x "$app_runtime/apps/desktop/release/linux-unpacked/Hermes"
cat >"$hermes" <<SH
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
# stands in for: exec "$app_runtime/venv/bin/python" "$app_runtime/hermes" "\$@"
if [[ \${1:-} == "chat" && \${2:-} == "--help" ]]; then
echo "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$hermes"
OMARCHY_TEST_DEFAULT_AGENT=hermes run_migration || fail "the migration succeeds with a working Hermes"
! grep -q 'default agent' "$test_tmp/output" || fail "a working Hermes gets reinstall guidance"
rm -f "$hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_DEFAULT_AGENT=hermes run_migration || fail "the migration succeeds with the app's Hermes in place"
! grep -q 'default agent' "$test_tmp/output" || fail "the app's own Hermes gets reinstall guidance" "$(cat "$test_tmp/output")"
rm -rf "$hermes" "$test_home/.hermes"
OMARCHY_TEST_DEFAULT_AGENT=codex run_migration || fail "the migration succeeds for another default agent"
! grep -q 'default agent' "$test_tmp/output" || fail "another default agent gets Hermes guidance"
pass "the migration says how to reinstall a Hermes that was the default agent"