Keep build-time registration private, verify the published launcher, and then let the native command register the final desktop entry. Keep that launcher first on the desktop environment PATH for subsequent registrations. Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
261 lines
10 KiB
Bash
261 lines
10 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
export HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
|
HERMES_HOME=$(realpath -ms "$HERMES_HOME")
|
|
root="$HERMES_HOME/hermes-agent"
|
|
marker="$root/.omarchy-hermes-desktop"
|
|
installer=/usr/share/hermes-desktop/install.sh
|
|
cli=("$root/venv/bin/python" "$root/hermes")
|
|
|
|
die() {
|
|
echo "Hermes: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
desktop_executable() {
|
|
local executable
|
|
for executable in "$root/apps/desktop/release/linux-unpacked/"{Hermes,hermes}; do
|
|
if [[ -f $executable && -x $executable ]]; then
|
|
printf '%s\n' "$executable"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
native_wrapper() {
|
|
local wrapper=$1 entry=$2 suffix=${3:-}
|
|
[[ -f $wrapper && ! -L $wrapper ]] && cmp -s "$wrapper" <(
|
|
printf '#!/usr/bin/env bash\nunset PYTHONPATH\nunset PYTHONHOME\nexec "%s/venv/bin/python" "%s/%s"%s "$@"\n' "$root" "$root" "$entry" "$suffix"
|
|
)
|
|
}
|
|
|
|
runtime_ready() {
|
|
[[ -f $root/.hermes-bootstrap-complete && -x ${cli[0]} ]] &&
|
|
[[ -x $HOME/.local/bin/hermes ]] &&
|
|
native_wrapper "$HOME/.local/bin/hermes" hermes &&
|
|
desktop_executable >/dev/null &&
|
|
timeout 15 env -u PYTHONPATH -u PYTHONHOME "${cli[@]}" --version >/dev/null 2>&1
|
|
}
|
|
|
|
ready() {
|
|
if [[ -f $marker ]]; then
|
|
grep -qxF ready "$marker" || return 1
|
|
fi
|
|
runtime_ready
|
|
}
|
|
|
|
restore_launchers() {
|
|
local command wrapper entry
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
[[ -f $install_backup/$command ]] || continue
|
|
wrapper="$HOME/.local/bin/$command"
|
|
entry=hermes
|
|
[[ $command == "hermes-agent" ]] && entry=run_agent.py
|
|
if [[ ! -e $wrapper && ! -L $wrapper ]] || native_wrapper "$wrapper" "$entry" ||
|
|
{ [[ -f $wrapper && ! -L $wrapper ]] && grep -qxF '# Written by omarchy-install-hermes-cli.' "$wrapper"; } ||
|
|
{ [[ $command == "hermes-acp" ]] && native_wrapper "$wrapper" hermes ' acp'; }; then
|
|
cp -p "$install_backup/$command" "$wrapper"
|
|
else
|
|
echo "Keeping changed $wrapper; its original is in $install_backup." >&2
|
|
return
|
|
fi
|
|
done
|
|
rm -rf "$install_backup"
|
|
}
|
|
|
|
stage() {
|
|
bash "$installer" --dir "${2:-$root}" --branch main --non-interactive --stage "$1"
|
|
}
|
|
|
|
install_desktop() {
|
|
(( EUID != 0 )) || die "Run this installer as your desktop user, without sudo."
|
|
mkdir -p "$HERMES_HOME"
|
|
exec 9>"$HERMES_HOME/.omarchy-hermes-desktop.lock"
|
|
flock -n 9 || die "Another Hermes installation is already running."
|
|
if ready; then
|
|
exec 9>&-
|
|
return
|
|
fi
|
|
|
|
# Upstream replaces these launchers. Admit its exact shims or Omarchy's
|
|
# marked predecessor before starting work, never a user's custom command.
|
|
local command wrapper entry
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
wrapper="$HOME/.local/bin/$command"
|
|
[[ -e $wrapper || -L $wrapper ]] || continue
|
|
entry=hermes
|
|
[[ $command == "hermes-agent" ]] && entry=run_agent.py
|
|
if native_wrapper "$wrapper" "$entry"; then continue; fi
|
|
if [[ $command == "hermes-acp" ]] && native_wrapper "$wrapper" hermes ' acp'; then continue; fi
|
|
if [[ $command == "hermes" && -f $wrapper && ! -L $wrapper ]] &&
|
|
grep -qxF '# Written by omarchy-install-hermes-cli.' "$wrapper"; then continue; fi
|
|
die "Keeping the existing $wrapper. Move or update that installation before installing Hermes Desktop."
|
|
done
|
|
|
|
# The native prerequisite stage may install Node's three user symlinks.
|
|
for command in node npm npx; do
|
|
wrapper="$HOME/.local/bin/$command"
|
|
if [[ -e $wrapper || -L $wrapper ]] &&
|
|
[[ ! -L $wrapper || $(readlink "$wrapper") != "$HERMES_HOME/node/bin/$command" ]]; then
|
|
die "Keeping the existing $wrapper. Move it before installing Hermes Desktop."
|
|
fi
|
|
done
|
|
|
|
local legacy=false
|
|
if [[ -e $root || -L $root ]]; then
|
|
[[ -d $root/.git && ! -L $root && ! -L $root/.git ]] || die "Keeping $root; it is not a managed checkout."
|
|
if [[ -f $marker ]] && grep -qxF updating "$marker"; then
|
|
legacy=true
|
|
elif [[ ! -f $marker ]]; then
|
|
python -I - "$root/.hermes-bootstrap-complete" <<'PY' || die "Keeping the existing checkout; install its desktop with 'hermes desktop'."
|
|
import json, sys
|
|
try:
|
|
stamp = json.load(open(sys.argv[1]))
|
|
assert stamp.get('desktopVersion') and stamp.get('pinnedCommit') in {
|
|
'e624e9fde561e1add9388384012b295fde669ade',
|
|
'29112bef099274229cadff79cdff7bf7b99c4b77',
|
|
}
|
|
except (OSError, ValueError, AssertionError, AttributeError):
|
|
sys.exit(1)
|
|
PY
|
|
legacy=true
|
|
fi
|
|
[[ -z $(git -C "$root" status --porcelain) ]] || die "Keeping local changes in $root. Save them before installing the desktop."
|
|
case "$(git -C "$root" remote get-url origin)" in
|
|
https://github.com/NousResearch/hermes-agent.git | git@github.com:NousResearch/hermes-agent.git) ;;
|
|
*) die "Keeping the checkout's custom origin. Install its desktop with 'hermes desktop'." ;;
|
|
esac
|
|
if [[ $legacy == true ]]; then
|
|
case "$(git -C "$root" rev-parse HEAD)" in
|
|
e624e9fde561e1add9388384012b295fde669ade | 29112bef099274229cadff79cdff7bf7b99c4b77) ;;
|
|
*)
|
|
if [[ $(git -C "$root" symbolic-ref --short -q HEAD) != "main" ]] ||
|
|
! git -C "$root" merge-base --is-ancestor HEAD refs/remotes/origin/main; then
|
|
die "The legacy checkout has moved. Run 'hermes update', then 'hermes desktop'."
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
install_backup=$(mktemp -d "$HERMES_HOME/.omarchy-hermes-launchers.XXXXXX")
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
wrapper="$HOME/.local/bin/$command"
|
|
if [[ -f $wrapper ]]; then cp -p "$wrapper" "$install_backup/$command"; fi
|
|
done
|
|
trap restore_launchers EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
local mise_predecessor=false
|
|
if [[ -f $install_backup/hermes ]] && grep -qxF '# Written by omarchy-install-hermes-cli.' "$install_backup/hermes"; then
|
|
mise_predecessor=true
|
|
fi
|
|
|
|
if [[ $legacy == true ]]; then
|
|
if [[ $mise_predecessor == true ]]; then
|
|
printf '%s\n' 'pipx:hermes-agent[extras=all]' >"$root/.git/omarchy-mise-predecessor"
|
|
fi
|
|
if ! grep -qxF '/.omarchy-hermes-desktop' "$root/.git/info/exclude"; then
|
|
printf '/.omarchy-hermes-desktop\n' >>"$root/.git/info/exclude"
|
|
fi
|
|
printf 'updating\n' >"$marker"
|
|
local refspec='+refs/heads/main:refs/remotes/origin/main'
|
|
if ! git -C "$root" config --get-all remote.origin.fetch | grep -qxF "$refspec"; then
|
|
git -C "$root" config --add remote.origin.fetch "$refspec"
|
|
fi
|
|
git -C "$root" fetch origin "$refspec"
|
|
env -u PYTHONPATH -u PYTHONHOME "${cli[@]}" update --yes --branch main
|
|
elif [[ ! -e $root ]]; then
|
|
stage prerequisites
|
|
# Publish only a complete clone with its ownership marker. An interrupted
|
|
# clone stays aside for inspection and cannot block the next installation.
|
|
local repository_dir
|
|
repository_dir=$(mktemp -d "$HERMES_HOME/.omarchy-hermes-repository.XXXXXX")
|
|
echo "Preparing the Hermes checkout in $repository_dir"
|
|
stage repository "$repository_dir/hermes-agent"
|
|
printf '/.omarchy-hermes-desktop\n' >>"$repository_dir/hermes-agent/.git/info/exclude"
|
|
printf 'pending\n' >"$repository_dir/hermes-agent/.omarchy-hermes-desktop"
|
|
mv -T --no-clobber "$repository_dir/hermes-agent" "$root"
|
|
[[ ! -e $repository_dir/hermes-agent ]] || die "Keeping the checkout that appeared at $root during installation."
|
|
rmdir "$repository_dir"
|
|
fi
|
|
|
|
if [[ $mise_predecessor == true ]]; then
|
|
printf '%s\n' 'pipx:hermes-agent[extras=all]' >"$root/.git/omarchy-mise-predecessor"
|
|
fi
|
|
|
|
if [[ $legacy == false ]]; then
|
|
for command in venv python-deps node-deps config; do stage "$command"; done
|
|
fi
|
|
# The native builder also handles updates and supports the namespace
|
|
# sandbox. The shell installer's desktop stage still requires a sudo chown.
|
|
# The build registers a desktop entry, too. Keep it private until the new
|
|
# CLI is published so a different hermes on PATH cannot become its target.
|
|
env -u PYTHONPATH -u PYTHONHOME XDG_DATA_HOME="$install_backup/desktop" "${cli[@]}" desktop --build-only
|
|
|
|
# Build first: a failed download leaves the old terminal CLI usable.
|
|
stage complete
|
|
stage path
|
|
runtime_ready || die "The CLI or desktop is not ready. See the installer output above."
|
|
env -u PYTHONPATH -u PYTHONHOME PATH="$HOME/.local/bin:$PATH" "${cli[@]}" desktop --skip-build --build-only
|
|
if ! grep -qxF '/.omarchy-hermes-desktop' "$root/.git/info/exclude"; then
|
|
printf '/.omarchy-hermes-desktop\n' >>"$root/.git/info/exclude"
|
|
fi
|
|
printf 'ready\n' >"$marker"
|
|
trap - EXIT INT TERM
|
|
rm -rf "$install_backup"
|
|
exec 9>&-
|
|
if [[ -f $root/.git/omarchy-mise-predecessor ]] && command -v omarchy-install-hermes-cli >/dev/null 2>&1; then
|
|
omarchy-install-hermes-cli || echo 'Run omarchy-install-hermes-cli again to finish the old CLI cleanup.' >&2
|
|
fi
|
|
echo "Hermes is ready. Update it in the app or with 'hermes update'."
|
|
}
|
|
|
|
case "${1:-}" in
|
|
--check) ready; exit ;;
|
|
--install) install_desktop; exit ;;
|
|
--setup)
|
|
shift
|
|
if /usr/bin/hermes-desktop --install; then
|
|
setsid --fork /usr/bin/hermes-desktop "$@" >/dev/null 2>&1
|
|
exit
|
|
else
|
|
status=$?
|
|
echo "Hermes installation failed. Retry with: hermes-desktop --install" >&2
|
|
if [[ -t 0 ]]; then read -r -p 'Press Enter to close.'; fi
|
|
exit "$status"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if ! ready; then
|
|
if [[ -t 0 && -t 1 ]]; then
|
|
install_desktop
|
|
else
|
|
exec xdg-terminal-exec /usr/bin/hermes-desktop --setup "$@"
|
|
fi
|
|
fi
|
|
|
|
export HERMES_DESKTOP_HERMES_ROOT="$root"
|
|
export HERMES_DESKTOP_PASSWORD_STORE="${HERMES_DESKTOP_PASSWORD_STORE:-gnome-libsecret}"
|
|
# Upstream desktop registration resolves its launcher through PATH.
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
unset ELECTRON_RUN_AS_NODE PYTHONPATH PYTHONHOME
|
|
|
|
flags=()
|
|
if [[ -n ${WAYLAND_DISPLAY:-} || ${XDG_SESSION_TYPE:-} == "wayland" ]]; then
|
|
flags=(--ozone-platform=wayland)
|
|
for flag in "$@"; do
|
|
case "$flag" in --ozone-platform=* | --ozone-platform-hint=*) flags=() ;; esac
|
|
done
|
|
fi
|
|
|
|
# Disable only the unusable setuid helper; Chromium keeps its namespace sandbox.
|
|
if unshare --user --map-root-user true 2>/dev/null; then
|
|
flags+=(--disable-setuid-sandbox)
|
|
fi
|
|
exec "$(desktop_executable)" "${flags[@]}" "$@"
|