Merge pull request #13296 from omacom/openclaw-self-updating
Install OpenClaw as a self-updating copy under ~/.openclaw
This commit is contained in:
11 files changed
+681
-44
No files matched your search
@@ -50,10 +50,11 @@ esac
|
||||
|
||||
agent_package=${agent_package:-$agent}
|
||||
|
||||
# Hermes and OpenClaw are not mise tools: Hermes is the desktop app's
|
||||
# self-updating runtime and OpenClaw comes from its pacman package. Each has
|
||||
# its own installer with the same --check/--now contract mise-backed agents
|
||||
# get from mise; see omarchy-install-hermes-cli and omarchy-install-openclaw-cli.
|
||||
# Hermes and OpenClaw are not mise tools: each is a self-updating runtime in
|
||||
# the user's home, Hermes the desktop app's and OpenClaw the one its Control
|
||||
# UI updates. Each has its own installer with the same --check/--now contract
|
||||
# mise-backed agents get from mise; see omarchy-install-hermes-cli and
|
||||
# omarchy-install-openclaw-cli.
|
||||
if [[ -n ${agent_installer:-} ]]; then
|
||||
# Not omarchy-cmd-present: a command on PATH says nothing about whether the
|
||||
# install behind it finished or runs the sessions omarchy-agent starts.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
# The same self-updating install the default agent gets.
|
||||
omarchy-install-openclaw-cli --now
|
||||
|
||||
# The desktop app is OpenClaw's Control UI: a web app served by its own
|
||||
# gateway. The launcher entry goes through omarchy-launch-openclaw, which
|
||||
@@ -14,7 +14,7 @@ omarchy-pkg-add openclaw
|
||||
# inside the package, so nothing is fetched here.
|
||||
echo "Installing the OpenClaw web app..."
|
||||
omarchy-webapp-install OpenClaw "http://127.0.0.1:18789" \
|
||||
/usr/lib/node_modules/openclaw/dist/control-ui/apple-touch-icon.png \
|
||||
/usr/share/openclaw/openclaw.png \
|
||||
omarchy-launch-openclaw
|
||||
|
||||
# A first install runs onboarding right here: launching the app instead would
|
||||
|
||||
@@ -1,29 +1,271 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Ensure the OpenClaw CLI is installed for the default agent
|
||||
# omarchy:args=[--check|--now]
|
||||
# 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
|
||||
|
||||
# OpenClaw is not a mise tool: the openclaw pacman package is the one OpenClaw
|
||||
# installation on the machine — the CLI, the gateway, and the Install > AI web
|
||||
# app all share it, and the fast ring keeps it current. The default-agent flow
|
||||
# talks to that package through the same --check/--now contract mise-backed
|
||||
# agents get from mise itself.
|
||||
# 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
|
||||
|
||||
case "${1:---now}" in
|
||||
--check)
|
||||
omarchy-pkg-present openclaw
|
||||
;;
|
||||
--now)
|
||||
if ! omarchy-pkg-present openclaw; then
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
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
|
||||
|
||||
# Omarchy's OpenClaw is the default one, in ~/.openclaw with the default unit,
|
||||
# whatever another profile, unit or state directory a shell selects; a service
|
||||
# installed from here would otherwise carry that selection.
|
||||
unset OPENCLAW_PROFILE OPENCLAW_SYSTEMD_UNIT OPENCLAW_WRAPPER OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# A unit's ExecStart, as the program and the script it is handed: the first
|
||||
# argument that is not an option. Later arguments and other lines can name
|
||||
# ~/.openclaw whichever OpenClaw the unit starts.
|
||||
unit_program() {
|
||||
local words
|
||||
read -ra words <<<"$(sed -n 's/^ExecStart=//p' "$1" | head -1)"
|
||||
echo "${words[0]:-}"
|
||||
}
|
||||
|
||||
unit_script() {
|
||||
local words word
|
||||
read -ra words <<<"$(sed -n 's/^ExecStart=//p' "$1" | head -1)"
|
||||
for word in "${words[@]:1}"; do
|
||||
if [[ $word != -* ]]; then
|
||||
echo "$word"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# The runtime's own OpenClaw, as upstream's installer lays it out.
|
||||
runs_runtime() {
|
||||
[[ $(unit_script "$1") == "$prefix"/tools/*/lib/node_modules/openclaw/* ]]
|
||||
}
|
||||
|
||||
# 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
|
||||
unit=$(unit_path gateway)
|
||||
[[ -f $unit ]] && ! runs_runtime "$unit" && ! on_old_package "$unit"
|
||||
}
|
||||
|
||||
# A service the openclaw package's own copy installed runs from
|
||||
# /usr/lib/node_modules/openclaw, which that package no longer ships, and is
|
||||
# moved to the runtime, where `openclaw update` can replace its code. A service
|
||||
# running any other OpenClaw is the user's arrangement and stays. The --now
|
||||
# steps below list these, and which were running, before anything changes.
|
||||
unit_path() {
|
||||
echo "$HOME/.config/systemd/user/openclaw-$1.service"
|
||||
}
|
||||
|
||||
# Stopped before anything is installed: still running the old code from deleted
|
||||
# files, such a service holds the state directory the newer runtime has to
|
||||
# migrate, which upstream's installer does the moment it finds one loaded.
|
||||
stop_legacy_services() {
|
||||
local role
|
||||
for role in "${moving[@]}"; do
|
||||
if ! systemctl --user stop "openclaw-$role.service"; then
|
||||
echo "Could not stop the OpenClaw $role service, so OpenClaw was not moved to $prefix." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ " ${running[*]} " == *" $role "* ]]; then
|
||||
stopped+=("$role")
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Still the old package's: its script, or the runtime's script on the old
|
||||
# package's Node. Upstream's installer keeps the Node a service already ran, so
|
||||
# a moved unit can end up running the runtime's OpenClaw on /usr/bin/node, from
|
||||
# the nodejs package the seed no longer depends on. Any other Node, Bun or
|
||||
# script is the user's.
|
||||
on_old_package() {
|
||||
[[ $(unit_script "$1") == /usr/lib/node_modules/openclaw/* ]] ||
|
||||
{ [[ $(unit_program "$1") == "/usr/bin/node" ]] && runs_runtime "$1"; }
|
||||
}
|
||||
|
||||
# Not moved yet: still on the old package, or running before and not now.
|
||||
unsettled() {
|
||||
on_old_package "$(unit_path "$1")" ||
|
||||
{ [[ " ${running[*]} " == *" $1 "* ]] && ! systemctl --user is-active --quiet "openclaw-$1.service"; }
|
||||
}
|
||||
|
||||
# Upstream's installer rewrites a loaded gateway itself, but only warns when
|
||||
# the restart fails, so whatever is still unsettled is installed again from the
|
||||
# runtime, on the runtime's own Node, and then has to have settled.
|
||||
rehome_services() {
|
||||
local role node pin=()
|
||||
if node=$(realpath -e -- "$prefix/tools/node/bin/node" 2>/dev/null); then
|
||||
pin=(--runtime-path "$node")
|
||||
fi
|
||||
for role in "${moving[@]}"; do
|
||||
if unsettled "$role"; then
|
||||
echo "Moving the OpenClaw $role service to $prefix..."
|
||||
"$runtime_command" "$role" install --force "${pin[@]}" || true
|
||||
fi
|
||||
if unsettled "$role"; then
|
||||
echo "Could not move the OpenClaw $role service to $prefix. Finish with: openclaw $role install --force" >&2
|
||||
return 1
|
||||
fi
|
||||
# Installing a service enables and starts it; one the user had left
|
||||
# stopped or disabled goes back to that.
|
||||
if [[ " ${running[*]} " != *" $role "* ]]; then
|
||||
systemctl --user stop "openclaw-$role.service" || true
|
||||
fi
|
||||
if [[ " ${enabled[*]} " != *" $role "* ]]; then
|
||||
systemctl --user disable "openclaw-$role.service" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Said on any failure that leaves a service this run stopped not running.
|
||||
stopped_note() {
|
||||
local role
|
||||
for role in "${stopped[@]}"; do
|
||||
if ! systemctl --user is-active --quiet "openclaw-$role.service"; then
|
||||
echo "The OpenClaw $role service was stopped for this and is not running now." >&2
|
||||
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
|
||||
echo "Usage: omarchy-install-openclaw-cli <--check|--now>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Refused before anything is installed or touched: the command's name, what
|
||||
# PATH finds, and a gateway running another OpenClaw all have to be clear.
|
||||
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 ! 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
|
||||
|
||||
# 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
|
||||
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 ! omarchy-pkg-present openclaw; then
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
fi
|
||||
if [[ $seeding == "true" && ( ! -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
|
||||
|
||||
# What the old package installed, and what of it was running and enabled, read
|
||||
# before anything changes.
|
||||
moving=()
|
||||
running=()
|
||||
enabled=()
|
||||
stopped=()
|
||||
for role in gateway node; do
|
||||
if [[ -f $(unit_path "$role") ]] && on_old_package "$(unit_path "$role")"; then
|
||||
moving+=("$role")
|
||||
if systemctl --user is-active --quiet "openclaw-$role.service"; then
|
||||
running+=("$role")
|
||||
fi
|
||||
if systemctl --user is-enabled --quiet "openclaw-$role.service"; then
|
||||
enabled+=("$role")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
trap 'status=$?; (( status == 0 )) || stopped_note' EXIT
|
||||
stop_legacy_services
|
||||
|
||||
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 || true
|
||||
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
|
||||
@@ -26,8 +26,12 @@ settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3}
|
||||
# setup; the prompts before that take as long as the user takes.
|
||||
gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180}
|
||||
|
||||
# Probes never get the terminal. The wizard writes its config while prompts
|
||||
# remain, a probe of a gateway not yet up then hangs until the timeout, and
|
||||
# OpenClaw's CLI takes a terminal on stdin out of raw mode as it is killed,
|
||||
# which leaves the wizard's next prompt echoing keys instead of reading them.
|
||||
gateway_answers() {
|
||||
timeout 10 openclaw dashboard --json 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
|
||||
timeout 10 openclaw dashboard --json </dev/null 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A gateway already answering means OpenClaw is set up, and this run must not
|
||||
@@ -81,7 +85,7 @@ config_applied() {
|
||||
gateway_ready() {
|
||||
config_applied || return 1
|
||||
local json port listener main_pid
|
||||
json=$(timeout 10 openclaw dashboard --json 2>/dev/null) || return 1
|
||||
json=$(timeout 10 openclaw dashboard --json </dev/null 2>/dev/null) || return 1
|
||||
jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1
|
||||
port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null)
|
||||
[[ -n $port ]] || return 1
|
||||
|
||||
@@ -35,8 +35,9 @@ for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node.
|
||||
systemctl --user disable --now "$unit" 2>/dev/null ||
|
||||
unit_stopped "$unit"; then
|
||||
# .bak is what `gateway install --force` leaves behind when it rewrites a
|
||||
# unit, so it goes with the unit.
|
||||
rm -f "$unit_file" "$unit_file.bak" "$unit_dir/default.target.wants/$unit"
|
||||
# unit, and .reconcile-*.bak what `openclaw update` leaves when it
|
||||
# refreshes one, so they go with the unit.
|
||||
rm -f "$unit_file" "$unit_file.bak" "$unit_file".reconcile-*.bak "$unit_dir/default.target.wants/$unit"
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
# A unit that had been failing stays listed as "not-found failed" after
|
||||
# its file is gone until its failed state is reset.
|
||||
@@ -49,6 +50,14 @@ done
|
||||
|
||||
omarchy-pkg-drop openclaw
|
||||
|
||||
# The command on PATH goes, so nothing answers to openclaw. The OpenClaw it
|
||||
# pointed at stays in ~/.openclaw with the state below: its own updates may
|
||||
# have migrated that state past the packaged release, which could not open it
|
||||
# again, and a reinstall picks the kept copy back up instead of seeding one.
|
||||
if [[ -L $HOME/.local/bin/openclaw && $(readlink -- "$HOME/.local/bin/openclaw") == "$HOME/.openclaw/bin/openclaw" ]]; then
|
||||
rm -f "$HOME/.local/bin/openclaw"
|
||||
fi
|
||||
|
||||
# The web app launcher and icon omarchy-install-ai-openclaw created.
|
||||
rm -f "$HOME/.local/share/applications/OpenClaw.desktop"
|
||||
rm -f "$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
|
||||
@@ -59,12 +68,13 @@ gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
|
||||
# better surprise. But it also holds the plugin runtimes and caches OpenClaw
|
||||
# downloads for itself, easily hundreds of megabytes, so the choice is put in
|
||||
# front of the user with the size rather than left silent. Without a terminal
|
||||
# to ask in, keeping it is the answer.
|
||||
# to ask in, keeping it is the answer. The snapshots `openclaw update` takes
|
||||
# of that state sit beside it and go with it.
|
||||
state_removed=false
|
||||
if [[ -d $HOME/.openclaw && -t 0 ]]; then
|
||||
size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1)
|
||||
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then
|
||||
rm -rf "$HOME/.openclaw"
|
||||
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, downloaded plugins, and OpenClaw itself)?"; then
|
||||
rm -rf "$HOME/.openclaw" "$HOME/.openclaw.update-captures"
|
||||
state_removed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in new issue
Block a user