An open terminal can retain deleted SQLite WAL files across removal and reinstall, causing the updated desktop to refuse session writes. Check user processes before package/runtime removal and again after data confirmation, without killing sessions. Cover the failure with real SQLite writers in isolated fixtures. Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
148 lines
6.2 KiB
Bash
Executable File
148 lines
6.2 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
|
|
|
|
# The mise CLI is the app's predecessor, not the app itself: Hermes Desktop takes
|
|
# it over on install and runs its own runtime instead, so a copy still here is one
|
|
# the app never superseded -- an interrupted install, or the terminal CLI from
|
|
# before the app existed. Remove Hermes clears that too, scoped by the installer
|
|
# to what Omarchy owns so a hermes the user set up themselves is left alone.
|
|
# Tolerated here rather than fatal, so the ~/.hermes handling below still runs;
|
|
# the failure is answered for at the end instead of being swallowed.
|
|
cli_removed=true
|
|
ensure_hermes_stopped
|
|
omarchy-install-hermes-cli --remove || cli_removed=false
|
|
|
|
# The app writes this when the runtime it provisions under ~/.hermes has landed,
|
|
# 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 app
|
|
# never got that far: a machine where it was installed but never launched 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
|
|
|
|
# The messages above still hold -- the app and its runtime are gone -- but a CLI
|
|
# teardown that failed already said so on stderr, and that stands.
|
|
if [[ $cli_removed == "false" ]]; then
|
|
exit 1
|
|
fi
|