Add Hermes as a desktop app and a coding agent

Hermes joins Install > AI as a desktop app, sits beside it under Remove > AI,
and becomes a choice in the default-agent list. The CLI installs through
omarchy-install-hermes-cli rather than a bare `mise use`, so its interpreter
is pinned before mise builds it.

Rebased onto quattro. Ori claimed U+E909 in #7709 while this branch was open,
so the Hermes mark moves to U+E90A in the icon font, the menu entries, the
font README, and the charset the menu test pins. The glyph outline itself is
unchanged; it is spliced in beside Ori rather than over it.

Co-Authored-By: witcheer <witcheer.eth@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SySdB3RtCA8BNv6Am246BP
This commit is contained in:
Omabot
2026-08-26 18:04:41 +02:00
committed by David Heinemeier Hansson
co-authored by witcheer Claude Opus 5
parent 0ae1694830
commit a12a21c02f
13 changed files with 434 additions and 6 deletions
+9
View File
@@ -86,6 +86,15 @@ codex)
command=(codex --approve-for-me) command=(codex --approve-for-me)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt") [[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;; ;;
hermes)
# Hermes has no "start interactive, seeded with this prompt" mode. --oneshot
# answers the prompt and exits, which is the closest it offers.
if [[ -n ${prompt:-} ]]; then
command=(hermes --yolo --oneshot "$prompt")
else
command=(hermes --yolo)
fi
;;
omp) omp)
command=(omp --auto-approve) command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt") [[ -n ${prompt:-} ]] && command+=(-- "$prompt")
+20 -4
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Set and launch the default coding agent # omarchy:summary=Set and launch the default coding agent
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush] # omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush]
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude # omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
installing=false installing=false
@@ -33,20 +33,36 @@ codex) agent="codex"; name="Codex" ;;
crush) agent="crush"; name="Crush" ;; crush) agent="crush"; name="Crush" ;;
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;; grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;; agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;; copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
*) *)
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush>" echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush>"
exit 1 exit 1
;; ;;
esac esac
agent_package=${agent_package:-$agent} agent_package=${agent_package:-$agent}
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then # Hermes reaches mise through its own installer rather than straight from
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
# say so. See omarchy-install-hermes-cli.
if [[ -n ${agent_installer:-} ]]; then
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
# nothing about whether Hermes is installed behind it. Treating a cold stub
# as installed skips the floating terminal and runs the minute-long install
# inside the menu action instead.
agent_present() { "$agent_installer" --check; }
agent_install() { "$agent_installer" --now; }
else
agent_present() { mise where "$agent_package" &>/dev/null; }
agent_install() { mise use -g "$agent_package"; }
fi
if [[ $installing == "false" ]] && ! agent_present; then
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent" exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
fi fi
if ! mise use -g "$agent_package"; then if ! agent_install; then
if [[ $installing == "true" ]]; then if [[ $installing == "true" ]]; then
echo "Could not install $name with mise" >&2 echo "Could not install $name with mise" >&2
else else
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# omarchy:summary=Install the Hermes desktop app
# omarchy:requires-sudo=true
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.
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
echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
echo ""
echo "Hermes Desktop has been installed."
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin
# omarchy:args=[--now]
# omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now
# Hermes pins every one of its dependencies exactly and declares
# Requires-Python >=3.11,<3.14, so it can neither be built against Arch's
# Python nor share the python-* packages. mise builds it a private environment
# instead.
#
# It gets its own installer rather than a line in omarchy-mise-install because
# of the interpreter pin. Given no compatible interpreter to hand, uv builds
# the venv against the system Python in violation of Hermes' own bound,
# reports success, and leaves the breakage to surface later inside a
# 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.
set -euo pipefail
mode=${1:-}
tool='pipx:hermes-agent[extras=all]'
python='3.13'
# 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.
desktop_hermes_ready() {
[[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]] || return 1
[[ -x $HOME/.local/bin/hermes ]] || 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.
grep -q "$HOME/.hermes" "$HOME/.local/bin/hermes"
}
# Whether Hermes is really installed, not merely whether the stub exists. A
# stub on its own is cold: running it installs Hermes, which takes minutes.
installed() {
[[ -d "$(mise where "$tool" 2>/dev/null)/hermes-agent/lib/python$python" ]]
}
# --check lets callers tell a cold stub from a working one before they commit
# to a path that assumes Hermes is ready.
if [[ $mode == "--check" ]]; then
if desktop_owns_hermes; then
if desktop_hermes_ready; then exit 0; else exit 1; fi
fi
if installed; then exit 0; else exit 1; fi
fi
# Hand Hermes over to the app rather than keeping a second copy beside it.
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
mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
# 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 [[ -f $HOME/.local/bin/hermes ]] && grep -q omarchy-install-hermes-cli "$HOME/.local/bin/hermes"; then
rm -f "$HOME/.local/bin/hermes"
fi
if desktop_hermes_ready; then
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
exit 1
fi
mkdir -p "$HOME/.local/bin"
rm -f "$HOME/.local/bin/hermes"
cat >"$HOME/.local/bin/hermes" <<EOF
#!/bin/bash
# Written by omarchy-install-hermes-cli.
export UV_PYTHON="$python"
# Exported rather than set on the install line alone, so the version resolved
# to run agrees with the one just installed. Hermes ships several times a week
# and mise's cooldown would otherwise hold a new release back for days.
export MISE_MINIMUM_RELEASE_AGE=0
# mise up -- which omarchy update runs -- reinstalls without that pin, so this
# asks which interpreter is actually there rather than whether anything is.
if ! [[ -d "\$(mise where '$tool' 2>/dev/null)/hermes-agent/lib/python$python" ]]; then
echo "Installing Hermes on Python $python (this takes a minute)..." >&2
# mise's pipx backend shells out to uv, which a stock Omarchy does not have.
# It is fetched here rather than when this stub was written, so setting up a
# machine that never runs Hermes costs nothing.
if omarchy-cmd-missing uv && ! mise where uv >/dev/null 2>&1; then
mise use -g --quiet uv@latest || exit 1
fi
mise use -g --quiet --force '$tool' || exit 1
fi
exec mise x '$tool' -- hermes "\$@"
EOF
chmod +x "$HOME/.local/bin/hermes"
# The desktop app resolves a hermes on PATH by running `hermes --version` with
# a 15 second budget, then falls back to cloning its own copy when that times
# out. A first-run mise install does not fit in 15 seconds, so anything that
# hands Hermes to the GUI has to install it here rather than leave it stubbed.
if [[ $mode == "--now" ]]; then
"$HOME/.local/bin/hermes" --version
fi
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed.
# omarchy:requires-sudo=true
set -e
omarchy-pkg-drop hermes-desktop
# The app installs a Hermes of its own under ~/.hermes -- the checkout and venv,
# its own uv, its own node -- and puts its commands on PATH. None of it is any
# use once the app is gone. Not ~/.config/Hermes, which holds the gateway
# connections and their encrypted tokens, the active profile and the update
# settings. Not the rest of ~/.hermes either:
# the chats, memories and the skills Hermes wrote for itself are the user's,
# they are small, and finding them still there after a reinstall is the better
# surprise.
rm -rf \
"$HOME/.hermes/hermes-agent" \
"$HOME/.hermes/bootstrap-cache" \
"$HOME/.hermes/bin" \
"$HOME/.hermes/node"
# Only the wrappers pointing into ~/.hermes. The app writes these at its own
# path stage, so a machine where it was installed but never launched still has
# whatever was there before, and that is not ours to delete.
for command in hermes hermes-agent hermes-acp; do
wrapper="$HOME/.local/bin/$command"
if [[ -f $wrapper ]] && grep -q "$HOME/.hermes" "$wrapper"; then
rm -f "$wrapper"
fi
done
# When Hermes brought its own Node it symlinked these next to its own commands,
# and they point at what we just deleted. Only the links into ~/.hermes: a
# system Node, or someone else's, lives somewhere else entirely.
for command in node npm npx; do
link="$HOME/.local/bin/$command"
if [[ -L $link && $(readlink "$link") == "$HOME/.hermes"/* ]]; then
rm -f "$link"
fi
done
echo ""
echo "Hermes Desktop has been removed."
echo "Your chats, memories, and skills are still in ~/.hermes,"
echo "and your connections and settings in ~/.config/Hermes."
+4
View File
@@ -17,6 +17,10 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \ ~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \
~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk ~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
# Hermes Desktop owns this path once installed, and its own CLI is not a
# preinstall to sweep away.
omarchy-pkg-present hermes-desktop || rm -f ~/.local/bin/hermes
omarchy-pkg-drop \ omarchy-pkg-drop \
aether \ aether \
cliamp \ cliamp \
+1
View File
@@ -12,6 +12,7 @@ The private-use glyphs in `omarchy.ttf` are:
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg> - `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG - `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
- `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one - `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one
- `U+E90A` — Hermes, traced from the Hermes app icon (<https://hermes-agent.nousresearch.com/>), the same art the `hermes-desktop` package ships as its icon
The agent marks are monochrome so the menu can render them using the active The agent marks are monochrome so the menu can render them using the active
theme's foreground and selection colors. theme's foreground and selection colors.
Binary file not shown.
+3
View File
@@ -141,6 +141,7 @@
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"}, "setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
"setup.default.agent.crush": {"icon":"󰋑","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"}, "setup.default.agent.crush": {"icon":"󰋑","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"}, "setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"}, "setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"}, "setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
"setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"}, "setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"},
@@ -239,6 +240,7 @@
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"}, "install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
"install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"}, "install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"}, "install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
"install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"},
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"}, "install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""}, "install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"}, "install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"},
@@ -292,6 +294,7 @@
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"}, "remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
"remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"}, "remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
"remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"}, "remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
"remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"}, "remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"},
"remove.browser.edge": {"icon":"󰇩","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"}, "remove.browser.edge": {"icon":"󰇩","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"},
"remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"}, "remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"},
+1
View File
@@ -13,3 +13,4 @@ omarchy-mise-install npm:@kitlangton/ghui ghui
omarchy-mise-install aqua:modem-dev/hunk hunk omarchy-mise-install aqua:modem-dev/hunk hunk
omarchy-mise-install github:basecamp/hey-cli hey omarchy-mise-install github:basecamp/hey-cli hey
omarchy-mise-install github:OpenRouterLabs/ori-releases ori omarchy-mise-install github:OpenRouterLabs/ori-releases ori
omarchy-install-hermes-cli
+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mise_log="$test_tmp/mise-log"
mkdir -p "$mock_bin" "$test_home/.local/bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
#!/bin/bash
! command -v "$1" >/dev/null 2>&1
SH
# `mise where` must fail so the installer sees no Hermes behind the stub.
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
[[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]] && exit 0
[[ $1 != "where" ]]
SH
chmod +x "$mock_bin"/*
run_installer() {
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$test_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 2>&1
}
stub_marker="omarchy-install-hermes-cli"
app_stub_body='#!/bin/bash
exec /home/x/.hermes/hermes-agent/venv/bin/hermes "$@"'
# Writing the stub must not provision anything: user setup calls this on every
# machine, including the ones that never run Hermes.
: >"$mise_log"
rm -f "$test_home/.local/bin/hermes"
run_installer 0 || fail "installer failed with no desktop installed"
[[ -x $test_home/.local/bin/hermes ]] || fail "installer writes a hermes stub when the desktop is absent"
grep -q "$stub_marker" "$test_home/.local/bin/hermes" || fail "the stub records which command wrote it"
tr '\0' ' ' <"$mise_log" | grep -q "use -g --quiet uv" &&
fail "writing the stub does not install uv"
pass "writing the Hermes stub provisions nothing"
# The desktop app owns Hermes, so our own stub must go rather than sit there
# answering `hermes` until the app's bootstrap replaces it.
printf '%s\n' "#!/bin/bash" "# $stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ ! -e $test_home/.local/bin/hermes ]] ||
fail "the desktop taking over removes the stub this command wrote"
pass "installing the desktop app removes the CLI stub"
# ...but the app's own hermes is not ours to delete.
printf '%s\n' "$app_stub_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ -x $test_home/.local/bin/hermes ]] ||
fail "the desktop app's own hermes command survives"
pass "the app's own hermes command is left alone"
# A copy mise cannot vouch for is still a second Hermes.
printf '%s\n' "#!/bin/bash" "# $stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 1 || true
tr '\0' '\n' <"$mise_log" | grep -q "uninstall" ||
fail "takeover removes a mise copy even when it is not healthy"
pass "takeover removes an unhealthy mise copy"
# --check answers about Hermes being usable, not about the venv appearing. The
# venv exists from the python-deps stage, several stages before the command.
rm -rf "$test_home/.hermes"
rm -f "$test_home/.local/bin/hermes"
run_installer 1 --check && fail "--check reports Hermes missing before the app installs it"
mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
printf '%s\n' "#!/bin/bash" >"$test_home/.hermes/hermes-agent/venv/bin/hermes"
chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes"
run_installer 1 --check && fail "--check waits for the install to finish, not just the venv"
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 --check || fail "--check reports Hermes present once the app has finished"
pass "--check follows the app's completed install"
# An executable called hermes that belongs to something else is not this
# install being ready.
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/somebody-elses-hermes \"\$@\"" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 --check && fail "--check rejects a hermes command belonging to something else"
pass "--check rejects a foreign hermes command"
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
SH
chmod +x "$mock_bin"/*
seed_install() {
rm -rf "$test_home"
mkdir -p "$test_home/.hermes/hermes-agent" "$test_home/.hermes/bootstrap-cache" \
"$test_home/.hermes/bin" "$test_home/.hermes/node/bin" \
"$test_home/.hermes/memories" "$test_home/.hermes/sessions" \
"$test_home/.config/Hermes" "$test_home/.local/bin"
printf 'chat\n' >"$test_home/.hermes/sessions/one.json"
printf 'memory\n' >"$test_home/.hermes/memories/one.md"
printf 'soul\n' >"$test_home/.hermes/SOUL.md"
printf 'uv\n' >"$test_home/.hermes/bin/uv"
ln -sf "$test_home/.hermes/node/bin/node" "$test_home/.local/bin/node"
ln -sf "$test_home/.hermes/node/bin/npm" "$test_home/.local/bin/npm"
ln -sf /usr/bin/npx "$test_home/.local/bin/npx"
printf 'node\n' >"$test_home/.hermes/node/bin/node"
}
remove() {
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" HOME="$test_home" PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1
}
# The app brings its own uv and its own node; both are runtime, not data.
seed_install
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds"
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime checkout is removed"
[[ ! -d $test_home/.hermes/bin ]] || fail "the uv the app installed is removed"
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
pass "removal takes the whole runtime the app installed"
[[ -d $test_home/.config/Hermes ]] ||
fail "gateway connections, tokens and settings survive removal"
pass "removal keeps the app's connections and settings"
# -L, not -e: a dangling symlink fails -e while very much still being there.
[[ ! -L $test_home/.local/bin/node ]] || fail "a node symlink into ~/.hermes is removed"
[[ ! -L $test_home/.local/bin/npm ]] || fail "an npm symlink into ~/.hermes is removed"
[[ -L $test_home/.local/bin/npx ]] || fail "an npx symlink pointing elsewhere survives"
pass "removal clears only the managed Node links it stranded"
[[ -f $test_home/.hermes/sessions/one.json ]] || fail "chats survive removal"
[[ -f $test_home/.hermes/memories/one.md ]] || fail "memories survive removal"
[[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal"
pass "removal keeps what belongs to the user"
[[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed"
pass "removal takes the command the app installed"
# Installed but never launched: the app never wrote these, so they are somebody
# else's and must survive.
seed_install
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/my-own-hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds with a foreign hermes present"
[[ -f $test_home/.local/bin/hermes ]] ||
fail "a hermes command the app did not write survives removal"
pass "removal leaves a hermes it does not own"
+3 -2
View File
@@ -224,6 +224,7 @@ const expectedAgents = {
claude: { icon: '󰛄', label: 'Claude' }, claude: { icon: '󰛄', label: 'Claude' },
codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' }, codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' },
grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' }, grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' },
hermes: { icon: '\ue90a', iconFont: 'omarchy', label: 'Hermes' },
copilot: { icon: '', label: 'Copilot' }, copilot: { icon: '', label: 'Copilot' },
crush: { icon: '󰋑', label: 'Crush' }, crush: { icon: '󰋑', label: 'Crush' },
} }
@@ -244,7 +245,7 @@ assertDeepEqual(
defaultItems defaultItems
.filter(item => item.parent === 'setup.default.agent') .filter(item => item.parent === 'setup.default.agent')
.map(item => item.label), .map(item => item.label),
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'omp', 'OpenCode', 'Ori', 'Pi'], ['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenCode', 'Ori', 'Pi'],
'menu sorts coding agents alphabetically' 'menu sorts coding agents alphabetically'
) )
const expectedDefaults = { const expectedDefaults = {
@@ -636,5 +637,5 @@ assert(
JS JS
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf") font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
[[ $font_charset == *"e900-e909"* ]] || fail "Omarchy icon font includes every custom menu glyph" [[ $font_charset == *"e900-e90a"* ]] || fail "Omarchy icon font includes every custom menu glyph"
pass "Omarchy icon font includes the official agent marks" pass "Omarchy icon font includes the official agent marks"