Choosing Hermes as the default agent built it through mise: a pipx environment with no checkout, so `hermes update` had nothing to move, and the only Hermes that could update itself was the one Hermes Desktop set up. Both paths now run the same setup. omarchy-install-hermes-cli installs the hermes-desktop package and runs upstream's installer from it, pinned to the packaged release and started on main, exactly as Install > AI did; omarchy-install-ai-hermes is that plus opening the app. The terminal, the default agent and the app share one runtime, and it updates itself. --check answers whether --now has anything left to do, not merely whether a hermes runs: choosing Hermes from the menu asks first and opens a terminal only on a no, so a yes has to mean no minutes-long step would run where nobody can see it. With the app installed that means the runtime's own command, its completion marker and the seeded packaged app; a finished runtime whose command is gone, somebody else's, or its own but unable to run gets it back from upstream's path stage without bootstrapping again. Either way the command has to be the one PATH finds, because omarchy-agent runs bare `hermes` and Omarchy puts mise's shims ahead of ~/.local/bin; a command in the way is named rather than installed over. The modes are named outright because the app's launcher used to call this command with no arguments to reconcile a mise copy; a default of --now would turn every launch into an install. --check still refuses to run the retired wrapper, since running it built Hermes through mise, and a machine whose migration is pending can still have it on PATH. Provisioning no longer writes the wrapper, Remove Preinstalls no longer looks for it, and the wrapper, the environment it built and what proves them Omarchy's are known to the installer alone: --retire-mise is the migration's whole job, and --now runs the same removal once the runtime installer has saved the wrapper aside, so a user who chose Hermes before their migration ran is not left with mise's shim answering `hermes`. Only the wrapper proves the environment is Omarchy's, at its path or in that saved copy, so the environment goes first and the wrapper last, judged by mise neither having it installed nor still requesting it; a removal that leaves either behind, or a listing that cannot be read, mise missing included, stops with the commands to finish by hand and leaves the migration pending. The migration that once installed the wrapper is kept as a no-op for late updaters, and one whose default agent was Hermes is told to choose it again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com>
132 lines
5.4 KiB
Bash
Executable File
132 lines
5.4 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed.
|
|
# omarchy:requires-sudo=true
|
|
|
|
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
|
set -euo pipefail
|
|
|
|
ensure_hermes_stopped() {
|
|
python3 - "$HOME" <<'PY'
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
home = Path(sys.argv[1])
|
|
roots = [str((home / relative).resolve()) for relative in ('.hermes', '.config/Hermes')]
|
|
roots.append('/opt/hermes-desktop')
|
|
|
|
def belongs_to_hermes(target):
|
|
target = target.removesuffix(' (deleted)')
|
|
return any(target == root or target.startswith(root + '/') for root in roots)
|
|
|
|
holders = []
|
|
for process in Path('/proc').iterdir():
|
|
if not process.name.isdigit() or int(process.name) == os.getpid():
|
|
continue
|
|
try:
|
|
if process.stat().st_uid != os.getuid():
|
|
continue
|
|
targets = [os.fsdecode(arg) for arg in (process / 'cmdline').read_bytes().split(b'\0')]
|
|
entries = [process / 'exe', process / 'cwd']
|
|
try:
|
|
entries.extend((process / 'fd').iterdir())
|
|
except PermissionError:
|
|
pass
|
|
for entry in entries:
|
|
try:
|
|
targets.append(os.readlink(entry))
|
|
except OSError:
|
|
pass
|
|
if any(belongs_to_hermes(target) for target in targets):
|
|
holders.append(process.name)
|
|
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
|
continue
|
|
|
|
if holders:
|
|
print('Close Hermes and processes using its files before removing it (PIDs: '
|
|
+ ', '.join(holders) + '). Then try again.', file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
}
|
|
|
|
# Removing an open SQLite WAL leaves a live writer on a deleted generation.
|
|
ensure_hermes_stopped
|
|
omarchy-pkg-drop hermes-desktop
|
|
|
|
# The installer leaves a unit waiting to hand the app the Omarchy theme.
|
|
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
|
|
|
|
# Upstream's installer writes this when the runtime under ~/.hermes has landed,
|
|
# whether Omarchy ran it for the app or the app's own first launch did, and it
|
|
# is the only thing that tells that runtime apart from one the user installed
|
|
# themselves -- the paths are the same either way. Without it the install never
|
|
# got that far: a machine where the package landed but nothing set Hermes up
|
|
# still has whatever was there before, and none of it is ours to delete unasked.
|
|
if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then
|
|
# The checkout and venv, its own uv, its own node. None of it is any use once
|
|
# the app is gone, so it goes without asking; what the user made with the app
|
|
# is a different question, answered below.
|
|
rm -rf \
|
|
"$HOME/.hermes/hermes-agent" \
|
|
"$HOME/.hermes/bootstrap-cache" \
|
|
"$HOME/.hermes/bin" \
|
|
"$HOME/.hermes/node"
|
|
|
|
# Only the wrappers pointing into ~/.hermes, matched as a plain string: the
|
|
# path carries a dot, so an unanchored pattern would also claim a wrapper
|
|
# pointing at a sibling like ~/xhermes.
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
wrapper="$HOME/.local/bin/$command"
|
|
|
|
if [[ -f $wrapper && ! -L $wrapper ]] && grep -qF "$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
|
|
|
|
fi
|
|
|
|
# What survives to here is the user's: the chats, memories and skills in
|
|
# ~/.hermes, the connections and their encrypted tokens in ~/.config/Hermes.
|
|
# Keeping them stays the default -- they are small, and finding them intact
|
|
# after a reinstall is the better surprise -- but a removal meant to be
|
|
# complete should not leave credentials behind either, so the choice is put in
|
|
# front of the user with the size, default no. Asked whenever the directories
|
|
# exist, marker or no marker: on a machine where the marker never appeared the
|
|
# data came from the terminal CLI or an install the app never finished, and it
|
|
# is still what removal is asked to clean up. Naming the paths keeps the
|
|
# question honest there too -- ~/.hermes may still carry a runtime the app
|
|
# never owned, a yes takes that with it, and saying so is the prompt's job.
|
|
# Without a terminal to ask in, keeping everything is the answer.
|
|
data_removed=false
|
|
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && omarchy-cmd-present gum; then
|
|
# du answers non-zero when either directory is missing, and pipefail would
|
|
# turn that into an aborted removal; the size is worth no such thing.
|
|
size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true)
|
|
if gum confirm --default=false "Also delete ~/.hermes and ~/.config/Hermes ($size: chats, memories, skills, connections and tokens)?"; then
|
|
ensure_hermes_stopped
|
|
rm -rf "$HOME/.hermes" "$HOME/.config/Hermes"
|
|
data_removed=true
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "Hermes Desktop has been removed."
|
|
if [[ $data_removed == true ]]; then
|
|
echo "Its chats, memories, and settings in ~/.hermes and ~/.config/Hermes are gone too."
|
|
elif [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]]; then
|
|
echo "Your chats, memories, and skills are still in ~/.hermes,"
|
|
echo "and your connections and settings in ~/.config/Hermes."
|
|
fi
|