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>
535 lines
22 KiB
Bash
Executable File
535 lines
22 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Install Hermes Desktop for the default agent; its runtime provides the hermes command
|
|
# omarchy:args=<--check|--now|--retire-mise>
|
|
# omarchy:examples=omarchy install hermes cli --now | omarchy install hermes cli --check
|
|
# omarchy:requires-sudo=true
|
|
|
|
# There is one Hermes on a machine, and it is the desktop app's. hermes-desktop
|
|
# ships upstream's installer and the release it was built from, and that
|
|
# installer makes the only Hermes that can update itself: a checkout under
|
|
# ~/.hermes with its own venv, which `hermes update` fast-forwards. The mise
|
|
# build this replaced had no checkout, so nothing an update could move.
|
|
# Choosing Hermes as the default agent therefore installs the app, the way
|
|
# Install > AI does, short of opening its window; see omarchy-install-ai-hermes.
|
|
# Hermes is only ever installed through the app: --check asks whether the
|
|
# app's Hermes is in and its command is what omarchy-agent would run, and
|
|
# --now installs the app whatever else answered to hermes before, saving
|
|
# whatever held the command's name aside before upstream's installer takes it.
|
|
#
|
|
# The wrapper the retired installer wrote, and the mise environment it built,
|
|
# are known here and nowhere else: --retire-mise is what the migration runs,
|
|
# and --now runs the same removal once it has replaced the wrapper.
|
|
#
|
|
# Each mode is named outright. The app's launcher used to call this with no
|
|
# arguments to reconcile a mise copy, and a default of --now would turn every
|
|
# launch of the app into an install.
|
|
|
|
set -euo pipefail
|
|
|
|
mode=${1:-}
|
|
|
|
# Keep the runtime at the root even when invoked from a Hermes profile.
|
|
HERMES_HOME=$(realpath -ms -- "${HERMES_HOME:-$HOME/.hermes}")
|
|
home_parent=$(dirname -- "$HERMES_HOME")
|
|
if [[ ${home_parent##*/} == [Pp][Rr][Oo][Ff][Ii][Ll][Ee][Ss] ]]; then
|
|
HERMES_HOME=$(dirname -- "$home_parent")
|
|
fi
|
|
export HERMES_HOME
|
|
|
|
runtime="$HERMES_HOME/hermes-agent"
|
|
native_app="$runtime/apps/desktop/release/linux-unpacked"
|
|
command_path="$HOME/.local/bin/hermes"
|
|
|
|
# The wrapper the retired mise-backed installer wrote, matched whole so a
|
|
# wrapper that merely mentions it is not mistaken for it, and the tool it
|
|
# built. Running the wrapper built Hermes through mise, so it is never run to
|
|
# find out whether Hermes is there.
|
|
legacy_marker='# Written by omarchy-install-hermes-cli.'
|
|
legacy_tool='pipx:hermes-agent[extras=all]'
|
|
|
|
legacy_stub() {
|
|
[[ -f $command_path ]] && grep -qxF "$legacy_marker" "$command_path"
|
|
}
|
|
|
|
# Only the wrapper Omarchy wrote proves the environment is Omarchy's to remove:
|
|
# the regular file at its path, or the copy a run of this installer saved aside
|
|
# before upstream's installer took the name, since a user who chose Hermes
|
|
# before the migration ran has it there and the environment still requested.
|
|
# A link is someone else's arrangement, even when it lands on the wrapper, and
|
|
# so is a linked backup directory: the installer makes real ones.
|
|
saved_wrapper() {
|
|
[[ ! -L ${1%/hermes} && -f $1 && ! -L $1 ]] && grep -qxF "$legacy_marker" "$1"
|
|
}
|
|
|
|
legacy_owned() {
|
|
local saved
|
|
if legacy_stub && [[ ! -L $command_path ]]; then
|
|
return 0
|
|
fi
|
|
for saved in "$HOME/.local/bin"/.hermes-before-desktop.*/hermes; do
|
|
if saved_wrapper "$saved"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Gone means neither installed nor still asked for in the global config, where
|
|
# `mise up` would build it again. `mise rm -g` exits 0 whether or not it removed
|
|
# anything, so the listing is read instead, and a listing that cannot be read
|
|
# -- mise broken, or not there to read it -- is not an answer. The key carries
|
|
# the backend and name, with or without the options.
|
|
mise_retired() {
|
|
local requested
|
|
requested=$(mise ls -g --json 2>/dev/null) || return 1
|
|
! mise where "$legacy_tool" >/dev/null 2>&1 && ! grep -qE '"pipx:hermes-agent(\[|")' <<<"$requested"
|
|
}
|
|
|
|
# Removes the environment the retired wrapper built, judged by what is left
|
|
# rather than by what the commands claimed. Only for a caller that has proved
|
|
# it Omarchy's.
|
|
retire_mise() {
|
|
if mise_retired; then
|
|
return 0
|
|
fi
|
|
mise rm -g "$legacy_tool" >/dev/null 2>&1 || true
|
|
mise uninstall --all "$legacy_tool" >/dev/null 2>&1 || true
|
|
if mise_retired; then
|
|
return 0
|
|
fi
|
|
echo "Could not remove the Hermes that mise built. Finish by hand:" >&2
|
|
if omarchy-cmd-missing mise; then
|
|
echo " omarchy pkg add mise" >&2
|
|
fi
|
|
echo " mise rm -g '$legacy_tool'" >&2
|
|
echo " mise uninstall --all '$legacy_tool'" >&2
|
|
return 1
|
|
}
|
|
|
|
# Once the environment is gone the proof has done its work. Left behind it
|
|
# would keep --check answering no for an install that is finished, and claim
|
|
# an environment the user builds later as Omarchy's. The copies are Omarchy's
|
|
# own wrapper, so removing them takes nothing of the user's; a backup directory
|
|
# that held nothing else goes with them.
|
|
forget_legacy() {
|
|
local saved
|
|
if legacy_stub && [[ ! -L $command_path ]]; then
|
|
rm -f "$command_path"
|
|
fi
|
|
for saved in "$HOME/.local/bin"/.hermes-before-desktop.*/hermes; do
|
|
if saved_wrapper "$saved"; then
|
|
rm -f "$saved"
|
|
rmdir "${saved%/hermes}" 2>/dev/null || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
# A hermes at that path is usable when it is a command that runs: a regular
|
|
# executable whose --version answers. The executable bit alone proves little -- a
|
|
# directory passes -x on search permission, and a wrapper whose interpreter or
|
|
# target is gone passes it too. The desktop app applies the same probe with the
|
|
# same 15 second budget, so what passes here is what it will use.
|
|
hermes_runs() {
|
|
[[ -f $command_path && -x $command_path ]] &&
|
|
timeout 15 "$command_path" --version >/dev/null 2>&1
|
|
}
|
|
|
|
# A flag counts only when the help defines it, not whenever it is mentioned:
|
|
# closing its short form, followed by its metavar, or padded to a description
|
|
# column. Matched against the help text rather than by parsing an actual
|
|
# invocation on purpose: a release that ignores unknown arguments would turn
|
|
# the probe into a live session.
|
|
help_defines_flag() {
|
|
grep -qE -- "$1(]|[[:space:]][[:upper:]]|[[:space:]]{2}|$)" <<<"$2"
|
|
}
|
|
|
|
# Probed for the flags omarchy-agent actually passes -- --query to seed the
|
|
# session and --tui to keep it interactive -- rather than a marker standing in
|
|
# for them.
|
|
hermes_prompt_ready() {
|
|
local help
|
|
hermes_runs &&
|
|
help=$(timeout 15 "$command_path" chat --help 2>/dev/null) &&
|
|
help_defines_flag '--tui' "$help" &&
|
|
help_defines_flag '--query' "$help"
|
|
}
|
|
|
|
# The command upstream's installer writes execs into the runtime. Anything else
|
|
# at that path -- an official install elsewhere, a hand-rolled wrapper, even a
|
|
# dangling link -- is the user's. Matched as a plain string with its trailing
|
|
# slash, because the path carries a dot and a bare prefix would also claim a
|
|
# wrapper pointing at ~/.hermes-old.
|
|
ours() {
|
|
[[ -f $command_path && ! -L $command_path ]] && grep -qF "$HERMES_HOME/" "$command_path"
|
|
}
|
|
|
|
# What omarchy-agent runs is whichever hermes is first on PATH, and Omarchy puts
|
|
# mise's shims ahead of ~/.local/bin. So the command the probe vets has to be
|
|
# the one PATH finds, or the agent is on a different Hermes than the one set
|
|
# up. A PATH that finds none is not in the way: the theme unit asks from a
|
|
# service whose PATH has no ~/.local/bin, and runs the command by its path.
|
|
on_path() {
|
|
local found
|
|
found=$(type -P hermes) || return 0
|
|
[[ $(realpath -m -- "$found") == "$(realpath -m -- "$command_path")" ]]
|
|
}
|
|
|
|
shadowing_hermes() {
|
|
type -P hermes || true
|
|
}
|
|
|
|
# The installer writes the marker last, so it is the one thing that says a
|
|
# runtime finished installing rather than merely started; the venv appears
|
|
# several stages earlier. And a marker left behind by an install whose venv has
|
|
# since gone answers for nothing, so the command has to run.
|
|
runtime_ready() {
|
|
[[ -f $runtime/.hermes-bootstrap-complete && -f $runtime/venv/bin/hermes && -x $runtime/venv/bin/hermes && -f $runtime/venv/bin/python && -x $runtime/venv/bin/python ]] &&
|
|
timeout 15 "$runtime/venv/bin/hermes" --version >/dev/null 2>&1
|
|
}
|
|
|
|
native_app_complete() {
|
|
[[ -f $native_app/Hermes && -x $native_app/Hermes && -f $native_app/resources/app.asar && -f $native_app/resources/install-stamp.json ]]
|
|
}
|
|
|
|
# What --check answers, and what --now has nothing left to do about: the app
|
|
# is installed and the Hermes on the machine is its own -- the command
|
|
# upstream's installer wrote, the runtime's completion marker and the seeded
|
|
# packaged app, because the app only runs against a runtime it prepared and
|
|
# the terminal has to be on the same one -- and that command is the one PATH
|
|
# finds, with nothing of the retired wrapper's left to remove. The two agree
|
|
# on purpose: choosing Hermes from the menu asks first and opens a terminal
|
|
# only on a no, so a yes here has to mean --now would take no minutes-long
|
|
# step where nobody can watch it.
|
|
installed() {
|
|
on_path || return 1
|
|
if legacy_owned; then
|
|
return 1
|
|
fi
|
|
omarchy-pkg-present hermes-desktop && ours && [[ -f $runtime/.hermes-bootstrap-complete ]] && native_app_complete && hermes_prompt_ready
|
|
}
|
|
|
|
# Once a Hermes answers at the command's path, the environment the retired
|
|
# wrapper built goes with whatever proof is left of it, and then PATH has to
|
|
# agree with the probe: a mise shim, or anything else ahead of ~/.local/bin, is
|
|
# what the default agent would run instead.
|
|
settle_path() {
|
|
if legacy_owned; then
|
|
echo "Removing the Hermes that mise built; Hermes now updates itself..."
|
|
retire_mise || return 1
|
|
forget_legacy
|
|
fi
|
|
if ! on_path; then
|
|
echo "$command_path is ready, but 'hermes' on PATH is $(shadowing_hermes), which is what the default agent runs." >&2
|
|
echo "Remove or reorder it, then run omarchy-install-hermes-cli --now again." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# The updater switches to main before checking for changes, so main has to
|
|
# start at the packaged release. Whatever main pointed at first is kept under
|
|
# another name rather than judged: upstream rewrites its history often enough
|
|
# that a clone's main sits off origin/main with no local commit on it, and a
|
|
# user's own commits are exactly what must not be lost either way.
|
|
keep_main() {
|
|
local main_commit kept
|
|
main_commit=$(git -C "$runtime" rev-parse --verify refs/heads/main 2>/dev/null || true)
|
|
if [[ -n $main_commit && $main_commit != "$release_commit" && $main_commit != "$(git -C "$runtime" rev-parse --verify refs/remotes/origin/main 2>/dev/null)" ]]; then
|
|
# A run that stopped after this point keeps it again on the next try; one
|
|
# branch at that commit is enough.
|
|
if [[ -z $(git -C "$runtime" for-each-ref --points-at "$main_commit" 'refs/heads/main-before-omarchy-*') ]]; then
|
|
kept="main-before-omarchy-$(date +%s)"
|
|
git -C "$runtime" branch "$kept" "$main_commit"
|
|
echo "Kept what Hermes main pointed at as the branch $kept; main now starts at the packaged release."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# The repository a process's GIT_DIR names, canonical: relative to its working
|
|
# directory when relative, and with no trailing slash or dot segments either
|
|
# way. Read NUL-delimited as the kernel writes it, never through a pipe: a
|
|
# path may carry a newline, and grep stopping at a match would fail a pipe
|
|
# under pipefail and call a busy git idle.
|
|
git_dir_of() {
|
|
local pid=$1 entry dir="" cwd
|
|
while IFS= read -r -d '' entry; do
|
|
if [[ $entry == GIT_DIR=* ]]; then
|
|
dir=${entry#GIT_DIR=}
|
|
break
|
|
fi
|
|
done <"/proc/$pid/environ" 2>/dev/null || return 1
|
|
[[ -n $dir ]] || return 1
|
|
if [[ $dir != /* ]]; then
|
|
cwd=$(readlink "/proc/$pid/cwd" 2>/dev/null) || return 1
|
|
dir="$cwd/$dir"
|
|
fi
|
|
realpath -m -- "$dir"
|
|
}
|
|
|
|
# A git of this user working in the runtime: a fetch Hermes started to work
|
|
# out its --version, or its updater. Found by working directory, or by a
|
|
# GIT_DIR in its environment, rather than by command line, since the remote
|
|
# helpers git spawns carry no repository path on theirs. Compared canonical,
|
|
# because /proc resolves every link and the runtime path keeps them.
|
|
git_busy_in_runtime() {
|
|
local pid cwd dir real
|
|
real=$(realpath -e -- "$runtime" 2>/dev/null) || return 1
|
|
for pid in $(pgrep -u "$(id -u)" -f '^([^ ]*/)?git(-remote-[a-z]+)?( |$)' 2>/dev/null); do
|
|
cwd=$(readlink "/proc/$pid/cwd" 2>/dev/null) || continue
|
|
if [[ $cwd == "$real" || $cwd == "$real/"* ]]; then
|
|
return 0
|
|
fi
|
|
if dir=$(git_dir_of "$pid") && [[ $dir == "$real/.git" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Hermes works out its --version against origin, and the probe's 15 second
|
|
# budget can kill it mid-fetch, which leaves git's shallow.lock behind; a fetch
|
|
# of ours that a network blip interrupted leaves the same. Git refuses to clear
|
|
# a lock it did not take, and a file's age alone cannot say whether anything
|
|
# still holds it, so a live git in the runtime is waited for first, and only a
|
|
# lock nobody holds and nothing has touched for a minute is cleared. The fetch
|
|
# itself gets three tries.
|
|
unshallow_runtime() {
|
|
local attempt waited lock="$runtime/.git/shallow.lock"
|
|
for attempt in 1 2 3; do
|
|
waited=0
|
|
while git_busy_in_runtime && (( waited < 60 )); do
|
|
if (( waited == 0 )); then
|
|
echo "Waiting for Hermes to finish working in $runtime..."
|
|
fi
|
|
sleep 2
|
|
waited=$(( waited + 2 ))
|
|
done
|
|
if git_busy_in_runtime; then
|
|
echo "Hermes is still working in $runtime. Let it finish, then run omarchy-install-hermes-cli --now again." >&2
|
|
return 1
|
|
fi
|
|
if [[ -e $lock && -n $(find "$lock" -mmin +1 2>/dev/null) ]]; then
|
|
rm -f "$lock"
|
|
fi
|
|
if git -C "$runtime" fetch --unshallow origin main; then
|
|
return 0
|
|
fi
|
|
if (( attempt < 3 )); then
|
|
sleep 2
|
|
fi
|
|
done
|
|
echo "Could not fetch the Hermes history from origin. Check the network, then run omarchy-install-hermes-cli --now again." >&2
|
|
return 1
|
|
}
|
|
|
|
case "$mode" in
|
|
--check)
|
|
if installed; then exit 0; else exit 1; fi
|
|
;;
|
|
--retire-mise)
|
|
# The migration's whole job. The environment goes before the wrapper, since
|
|
# once the wrapper is gone nothing marks the environment as Omarchy's and a
|
|
# rerun could not finish what a failed removal left behind.
|
|
if legacy_owned; then
|
|
retire_mise || exit 1
|
|
forget_legacy
|
|
fi
|
|
exit 0
|
|
;;
|
|
--now) ;;
|
|
*)
|
|
echo "Usage: omarchy-install-hermes-cli <--check|--now|--retire-mise>" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if (( EUID == 0 )); then
|
|
echo "Run this command as your desktop user, without sudo." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if installed; then
|
|
exit 0
|
|
fi
|
|
|
|
if ! omarchy-pkg-present hermes-desktop; then
|
|
echo "Installing Hermes Desktop..."
|
|
omarchy-pkg-add hermes-desktop
|
|
fi
|
|
|
|
if [[ ! -r /usr/share/hermes-desktop/install.sh || ! -r /usr/share/hermes-desktop/runtime.patch ]] ||
|
|
! release_commit=$(jq -er 'select(.branch == "main") | .commit | select(test("^[0-9a-f]{40}$"))' /opt/hermes-desktop/resources/install-stamp.json 2>/dev/null); then
|
|
echo "The installed Hermes package cannot prepare in-app updates. Run 'omarchy update', then try again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
runtime_present=false
|
|
if runtime_ready; then
|
|
runtime_present=true
|
|
fi
|
|
|
|
# The packaged app can be seeded only beside a runtime at the release it was
|
|
# built from, with its desktop sources untouched, and a seeded app has to be
|
|
# whole. Asked before a command is replaced, so that a refusal leaves the
|
|
# user's Hermes exactly as it was, and again before seeding.
|
|
# Asked through ||, so nothing in here stops on its own: every git answer is
|
|
# checked, and one that cannot be read refuses rather than reading as clean.
|
|
refuse_unless_seedable() {
|
|
local runtime_commit changes
|
|
if [[ -e $native_app || -L $native_app ]]; then
|
|
if ! native_app_complete; then
|
|
echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
fi
|
|
if ! runtime_commit=$(git -C "$runtime" rev-parse HEAD 2>/dev/null) || [[ -z $runtime_commit ]]; then
|
|
echo "Could not read the Hermes runtime's git state at $runtime. Repair that installation before trying again; existing files have been kept." >&2
|
|
return 1
|
|
fi
|
|
if [[ $runtime_commit != "$release_commit" ]]; then
|
|
echo "The Hermes runtime has moved beyond the packaged desktop release. Run 'hermes desktop --build-only', then try again." >&2
|
|
return 1
|
|
fi
|
|
if ! changes=$(git -C "$runtime" status --porcelain --untracked-files=all -- apps/desktop package.json package-lock.json 2>/dev/null); then
|
|
echo "Could not read the Hermes runtime's git state at $runtime. Repair that installation before trying again; existing files have been kept." >&2
|
|
return 1
|
|
fi
|
|
if [[ -n $changes ]]; then
|
|
echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
if [[ $runtime_present == "true" ]]; then
|
|
refuse_unless_seedable || exit 1
|
|
fi
|
|
|
|
# Whether this run gave Hermes something new to show the theme to: a runtime
|
|
# it set up, or the app it seeded. Putting a command back is neither.
|
|
set_up=false
|
|
|
|
if [[ $runtime_present == "false" && ( -e $runtime || -L $runtime ) ]]; then
|
|
# The upstream installer can reset an existing checkout. Do not pin a newer
|
|
# or modified runtime back to the package release while repairing setup.
|
|
if [[ $(git -C "$runtime" rev-parse HEAD 2>/dev/null) != "$release_commit" ]] ||
|
|
[[ -n $(git -C "$runtime" status --porcelain --untracked-files=all) ]]; then
|
|
echo "Hermes setup is incomplete at $runtime. Repair that installation before trying again; existing files have been kept." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# The command is the runtime's own and runs; anything else at the path gets
|
|
# replaced below. The retired wrapper is never run to find out.
|
|
command_ready=false
|
|
if ! legacy_stub && ours && hermes_runs; then
|
|
command_ready=true
|
|
fi
|
|
|
|
# Upstream replaces these commands, including foreign files and symlinks,
|
|
# whether it sets the runtime up or only writes the commands again. Keep their
|
|
# original bytes/links before handing the names to the runtime.
|
|
if [[ $runtime_present == "false" || $command_ready == "false" ]]; then
|
|
command_backup=""
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
existing="$HOME/.local/bin/$command"
|
|
if [[ -e $existing || -L $existing ]]; then
|
|
if [[ ! -f $existing && ! -L $existing ]]; then
|
|
echo "Cannot replace $existing: move it aside before installing Hermes." >&2
|
|
exit 1
|
|
fi
|
|
if [[ -z $command_backup ]]; then
|
|
command_backup=$(mktemp -d "$HOME/.local/bin/.hermes-before-desktop.XXXXXX")
|
|
echo "Saving existing Hermes commands in $command_backup"
|
|
fi
|
|
cp -a -- "$existing" "$command_backup/"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ $runtime_present == "false" ]]; then
|
|
echo "Setting up the Hermes runtime..."
|
|
# npx stops to ask before fetching Playwright for the browser tools, and in
|
|
# the terminal the menu opens for this nobody is there to say yes.
|
|
npm_config_yes=true bash /usr/share/hermes-desktop/install.sh --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$HERMES_HOME"
|
|
if ! runtime_ready; then
|
|
echo "Hermes runtime setup did not complete. Re-run this command after resolving the installer error." >&2
|
|
exit 1
|
|
fi
|
|
set_up=true
|
|
elif [[ $command_ready == "false" ]]; then
|
|
# The runtime is in place but the command is not its own, or does not run:
|
|
# gone, the retired wrapper, one the user put there, or the runtime's own
|
|
# with its mode bits stripped. Upstream's path stage writes its commands
|
|
# without touching the checkout.
|
|
echo "Restoring the Hermes commands..."
|
|
bash /usr/share/hermes-desktop/install.sh --stage path --dir "$runtime" --hermes-home "$HERMES_HOME"
|
|
fi
|
|
|
|
runtime_commit=$(git -C "$runtime" rev-parse HEAD)
|
|
if [[ $runtime_commit == "$release_commit" ]]; then
|
|
# Start main at the packaged release, with enough history for its first
|
|
# fast-forward.
|
|
keep_main
|
|
if [[ $(git -C "$runtime" rev-parse --is-shallow-repository) == "true" ]]; then
|
|
unshallow_runtime
|
|
fi
|
|
git -C "$runtime" switch -C main "$release_commit"
|
|
|
|
if git -C "$runtime" apply --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
|
git -C "$runtime" apply /usr/share/hermes-desktop/runtime.patch
|
|
elif ! git -C "$runtime" apply --reverse --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
|
echo "The Hermes Linux runtime patch conflicts with local changes. Existing files have been kept." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
refuse_unless_seedable || exit 1
|
|
if [[ ! -e $native_app && ! -L $native_app ]]; then
|
|
mkdir -p -- "${native_app%/*}"
|
|
staging=$(mktemp -d "${native_app%/*}/.linux-unpacked.XXXXXX")
|
|
trap 'rm -rf -- "$staging"' EXIT
|
|
cp -a /opt/hermes-desktop/. "$staging/"
|
|
chmod 0755 "$staging/chrome-sandbox"
|
|
mv -T --no-clobber -- "$staging" "$native_app"
|
|
if [[ -e $staging ]]; then
|
|
echo "A Hermes desktop app appeared during setup. It has been kept; please try again." >&2
|
|
exit 1
|
|
fi
|
|
trap - EXIT
|
|
|
|
# Record this matching prebuilt app using the CLI's own content hash, so
|
|
# subsequent menu launches do not rebuild an app that is already current.
|
|
env -u PYTHONPATH -u PYTHONHOME "$runtime/venv/bin/python" - "$runtime" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, sys.argv[1])
|
|
if Path(sys.argv[1], "hermes_cli/main_desktop.py").is_file():
|
|
from hermes_cli.main_desktop import _write_desktop_build_stamp
|
|
else:
|
|
from hermes_cli.main import _write_desktop_build_stamp
|
|
|
|
_write_desktop_build_stamp(Path(sys.argv[1]), source_mode=False)
|
|
PY
|
|
set_up=true
|
|
fi
|
|
|
|
# What omarchy-agent runs is the command, not the venv, so the command is what
|
|
# has to answer for the seeded sessions.
|
|
if ! hermes_prompt_ready; then
|
|
echo "Hermes is installed at $runtime, but $command_path does not run the interactive seeded sessions Omarchy needs." >&2
|
|
echo "Update Hermes with 'hermes update', then run omarchy-install-hermes-cli --now again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
settle_path || exit 1
|
|
|
|
# Only a running Hermes can be told which skin to show; a unit outlives this
|
|
# terminal and reports to the journal.
|
|
if [[ $set_up == "true" ]]; then
|
|
echo "Matching Hermes to the current theme once it is set up..."
|
|
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
|
|
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme omarchy-theme-set-hermes --wait
|
|
fi
|