The first E2E run of the migration found that the new runtime cannot install its gateway service while the old package's gateway, still running from deleted files, holds the state directory, so that service is stopped first. Removal also takes the unit backups `openclaw update` leaves behind. Codex's review found the rest: upstream's installer takes over any loaded gateway service, so a gateway running another OpenClaw, a foreign command on PATH or an openclaw shadowing it are refused before anything is set up; root is refused before --check runs the user's wrapper; --check needs the package, since --now would otherwise run pacman where no terminal can ask for a password; and moving a service clears the selectors that would aim the install at another unit. Co-Authored-By: Codex XHigh <noreply@openai.com>
170 lines
6.5 KiB
Bash
Executable File
170 lines
6.5 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Install OpenClaw for the default agent as the self-updating copy under ~/.openclaw
|
|
# omarchy:args=<--check|--now>
|
|
# omarchy:examples=omarchy install openclaw cli --now | omarchy install openclaw cli --check
|
|
# omarchy:requires-sudo=true
|
|
|
|
# There is one OpenClaw on a machine, and it is the one under ~/.openclaw that
|
|
# upstream's install-cli.sh makes: a private Node and the package in a prefix
|
|
# the user owns, which `openclaw update` and the Control UI's Update button
|
|
# replace in place. A copy in /usr belongs to pacman, and upstream's updater
|
|
# refuses to touch it. The openclaw package is therefore the seed rather than
|
|
# the runtime: the release Omarchy ships and the installer that came with it.
|
|
# The CLI, the gateway service and the Install > AI web app all run this copy.
|
|
#
|
|
# Each mode is named outright, like omarchy-install-hermes-cli: the default
|
|
# agent asks --check first and opens a terminal for --now only on a no.
|
|
|
|
set -euo pipefail
|
|
|
|
mode=${1:-}
|
|
|
|
# Everything here runs the user's own files, --check included.
|
|
if (( EUID == 0 )); then
|
|
echo "Run this command as your desktop user, without sudo." >&2
|
|
exit 1
|
|
fi
|
|
|
|
prefix="$HOME/.openclaw"
|
|
runtime_command="$prefix/bin/openclaw"
|
|
command_path="$HOME/.local/bin/openclaw"
|
|
seed=/usr/share/openclaw
|
|
|
|
# Usable means the command answers, not that the file is there: upstream's
|
|
# wrapper execs the prefix's own Node, so a prefix missing either fails here.
|
|
runtime_runs() {
|
|
[[ -f $runtime_command && -x $runtime_command ]] &&
|
|
timeout 30 "$runtime_command" --version >/dev/null 2>&1
|
|
}
|
|
|
|
# The name on PATH is a link to the runtime's command. A link already aimed
|
|
# there, even one left dangling by a removed runtime, is Omarchy's to rewrite;
|
|
# anything else at the path is the user's.
|
|
command_ours() {
|
|
[[ ! -e $command_path && ! -L $command_path ]] || [[ $(readlink -- "$command_path") == "$runtime_command" ]]
|
|
}
|
|
|
|
# What omarchy-agent, the web app and a terminal run is whichever openclaw is
|
|
# first on PATH, and Omarchy puts /usr/bin and the mise shims ahead of
|
|
# ~/.local/bin, so the command has to resolve to the runtime from there.
|
|
on_path() {
|
|
local found
|
|
found=$(type -P openclaw) || return 1
|
|
[[ $(realpath -m -- "$found") == "$(realpath -m -- "$runtime_command")" ]]
|
|
}
|
|
|
|
# Nothing yet, or the runtime's command.
|
|
path_clear() {
|
|
! type -P openclaw >/dev/null || on_path
|
|
}
|
|
|
|
# The package counts too: without it --now has a pacman step to take, and
|
|
# answering yes here would run that where no terminal can ask for a password.
|
|
installed() {
|
|
omarchy-pkg-present openclaw && runtime_runs && on_path
|
|
}
|
|
|
|
# The service a unit runs, read from its ExecStart alone: other lines can name
|
|
# ~/.openclaw paths whichever OpenClaw the unit starts.
|
|
unit_runs() {
|
|
grep -E '^ExecStart=' "$1" | grep -qF -- "$2"
|
|
}
|
|
|
|
# A gateway service already running some other OpenClaw. Upstream's installer
|
|
# rewrites a loaded gateway service to the copy it has just installed, so
|
|
# seeding beside one would take it over.
|
|
foreign_gateway() {
|
|
local unit="$HOME/.config/systemd/user/openclaw-gateway.service"
|
|
[[ -f $unit ]] && ! unit_runs "$unit" "$prefix/" && ! unit_runs "$unit" "/usr/lib/node_modules/openclaw/"
|
|
}
|
|
|
|
# A service the openclaw package's own copy installed runs from
|
|
# /usr/lib/node_modules/openclaw, which that package no longer ships. Installed
|
|
# again from the runtime, it runs code `openclaw update` can replace; upstream
|
|
# rewrites the unit and starts it. It is stopped first: still running the old
|
|
# code from deleted files, it holds the state directory the newer runtime has
|
|
# to migrate, and could not restart from those files anyway. A service running
|
|
# any other OpenClaw is the user's arrangement and stays.
|
|
rehome_services() {
|
|
local role unit
|
|
for role in gateway node; do
|
|
unit="$HOME/.config/systemd/user/openclaw-$role.service"
|
|
if [[ -f $unit ]] && unit_runs "$unit" "/usr/lib/node_modules/openclaw/"; then
|
|
echo "Moving the OpenClaw $role service to $prefix..."
|
|
systemctl --user stop "openclaw-$role.service" 2>/dev/null || true
|
|
# The selectors would point the install at another unit and command.
|
|
if ! env -u OPENCLAW_PROFILE -u OPENCLAW_SYSTEMD_UNIT -u OPENCLAW_WRAPPER "$runtime_command" "$role" install --force; then
|
|
echo "Could not move the OpenClaw $role service. Finish with: openclaw $role install --force" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
case "$mode" in
|
|
--check)
|
|
if installed; then exit 0; else exit 1; fi
|
|
;;
|
|
--now) ;;
|
|
*)
|
|
echo "Usage: omarchy-install-openclaw-cli <--check|--now>" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if ! command_ours; then
|
|
echo "$command_path is not the OpenClaw Omarchy set up. Move it aside, then run omarchy-install-openclaw-cli --now again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! omarchy-pkg-present openclaw; then
|
|
echo "Installing OpenClaw..."
|
|
omarchy-pkg-add openclaw
|
|
fi
|
|
|
|
# A runtime that already answers is never reinstalled: it may be past the
|
|
# packaged release by its own updates, and seeding would take it back.
|
|
seeding=false
|
|
if ! runtime_runs; then
|
|
seeding=true
|
|
if [[ ! -r $seed/install-cli.sh || ! -r $seed/openclaw.tgz ]]; then
|
|
echo "The installed OpenClaw package cannot set up a self-updating OpenClaw. Run 'omarchy update', then try again." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Refused before anything in the home is touched.
|
|
if ! path_clear; then
|
|
echo "'openclaw' on PATH is $(type -P openclaw), which is what Omarchy would run instead of $runtime_command." >&2
|
|
echo "Remove it or reorder PATH, then run omarchy-install-openclaw-cli --now again." >&2
|
|
exit 1
|
|
fi
|
|
if [[ $seeding == "true" ]] && foreign_gateway; then
|
|
echo "The OpenClaw gateway service runs another OpenClaw, which setting one up in $prefix would take over." >&2
|
|
echo "Remove that gateway with 'openclaw gateway uninstall', then run omarchy-install-openclaw-cli --now again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $seeding == "true" ]]; then
|
|
# Every choice the installer reads from the environment is named, so an
|
|
# OPENCLAW_INSTALL_METHOD or OPENCLAW_PREFIX left in a shell cannot move it.
|
|
echo "Setting up OpenClaw in $prefix..."
|
|
bash "$seed/install-cli.sh" --install-method npm --prefix "$prefix" --version "$seed/openclaw.tgz" --no-onboard
|
|
if ! runtime_runs; then
|
|
echo "OpenClaw setup did not complete. Re-run this command after resolving the installer error." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "${command_path%/*}"
|
|
ln -sfn "$runtime_command" "$command_path"
|
|
|
|
rehome_services
|
|
|
|
if ! on_path; then
|
|
echo "$runtime_command is ready, but 'openclaw' on PATH is $(type -P openclaw || echo missing), which is what Omarchy runs." >&2
|
|
echo "Put ~/.local/bin on PATH, then run omarchy-install-openclaw-cli --now again." >&2
|
|
exit 1
|
|
fi
|