diff --git a/bin/omarchy-install-openclaw-cli b/bin/omarchy-install-openclaw-cli index 5b01eecf..825de500 100755 --- a/bin/omarchy-install-openclaw-cli +++ b/bin/omarchy-install-openclaw-cli @@ -79,22 +79,18 @@ unit_runs() { # 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" + local unit + unit=$(unit_path gateway) [[ -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, 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. -legacy_units() { - 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 "$role" - fi - done +# 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 @@ -102,7 +98,7 @@ legacy_units() { # migrate, which upstream's installer does the moment it finds one loaded. stop_legacy_services() { local role - for role in $(legacy_units); do + 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 @@ -110,19 +106,36 @@ stop_legacy_services() { done } -# Installed again from the runtime, upstream rewrites the unit and starts it, -# unless its installer already did. +# Not moved yet: still on the old copy, or running before and not now. +unsettled() { + unit_runs "$(unit_path "$1")" "/usr/lib/node_modules/openclaw/" || + { [[ " ${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 and then has to have settled. rehome_services() { local role - for role in $(legacy_units); do - echo "Moving the OpenClaw $role service to $prefix..." - if ! "$runtime_command" "$role" install --force; then - echo "Could not move the OpenClaw $role service. Finish with: openclaw $role install --force" >&2 + for role in "${moving[@]}"; do + if unsettled "$role"; then + echo "Moving the OpenClaw $role service to $prefix..." + "$runtime_command" "$role" install --force || 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 done } +# Said whenever a failure leaves a service stopped by this run. +stopped_note() { + if (( ${#running[@]} )); then + echo "The OpenClaw ${running[*]} service was stopped for this and is not running now." >&2 + fi +} + case "$mode" in --check) if installed; then exit 0; else exit 1; fi @@ -167,15 +180,29 @@ if [[ $seeding == "true" && ( ! -r $seed/install-cli.sh || ! -r $seed/openclaw.t exit 1 fi +# What the old package installed and what of it was running, read before +# anything changes. +moving=() +running=() +for role in gateway node; do + if [[ -f $(unit_path "$role") ]] && unit_runs "$(unit_path "$role")" "/usr/lib/node_modules/openclaw/"; then + moving+=("$role") + if systemctl --user is-active --quiet "openclaw-$role.service"; then + running+=("$role") + fi + fi +done + 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 + 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 + stopped_note exit 1 fi fi @@ -183,7 +210,10 @@ fi mkdir -p "${command_path%/*}" ln -sfn "$runtime_command" "$command_path" -rehome_services +if ! rehome_services; then + stopped_note + exit 1 +fi 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 diff --git a/test/shell.d/openclaw-cli-test.sh b/test/shell.d/openclaw-cli-test.sh index 21bdb17a..1ffa279e 100755 --- a/test/shell.d/openclaw-cli-test.sh +++ b/test/shell.d/openclaw-cli-test.sh @@ -21,16 +21,27 @@ cat >"$mock_bin/omarchy-pkg-add" <<'SH' printf 'pkg-add %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" touch "$OMARCHY_TEST_ROOT/package-installed" SH +# The user manager, as far as these tests need one: a service is active while +# a marker says so. Stopping clears it; OMARCHY_TEST_STOP_FAIL refuses. cat >"$mock_bin/systemctl" <<'SH' #!/bin/bash -printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" -[[ -z ${OMARCHY_TEST_STOP_FAIL:-} ]] +case "$2" in + stop) + printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" + [[ -z ${OMARCHY_TEST_STOP_FAIL:-} ]] || exit 1 + rm -f "$HOME/active-$3" + ;; + is-active) [[ -e $HOME/active-$4 ]] ;; +esac SH chmod +x "$mock_bin/"* # Stands in for upstream's install-cli.sh: it writes the command the way the # real one does, execing into the prefix's tools, and that command logs what # it is asked. OMARCHY_TEST_INSTALL_BROKEN leaves a command that cannot run. +# Like upstream, ` install --force` rewrites the unit onto the runtime +# and starts it, OMARCHY_TEST_START_FAIL making the start fail, and the +# installer does that itself for a gateway it finds loaded. cat >"$seed/install-cli.sh" <<'SH' printf 'install-cli %s%s\n' "$*" "${OPENCLAW_PROFILE:+ profile=$OPENCLAW_PROFILE}" >>"$OMARCHY_TEST_ROOT/events" prefix=$HOME/.openclaw @@ -38,11 +49,19 @@ mkdir -p "$prefix/bin" "$prefix/tools/node-v24.19.0" cat >"$prefix/bin/openclaw" <>"$OMARCHY_TEST_ROOT/events" +if [[ \${2:-} == "install" ]]; then + printf 'ExecStart=$prefix/tools/node-v24.19.0/bin/node $prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/index.js %s\n' "\$1" >"\$HOME/.config/systemd/user/openclaw-\$1.service" + [[ -z "\${OMARCHY_TEST_START_FAIL:-}" ]] || exit 1 + touch "\$HOME/active-openclaw-\$1.service" +fi exec true "$prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/entry.js" "\$@" EOF chmod 755 "$prefix/bin/openclaw" +if [[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]]; then + "$prefix/bin/openclaw" gateway install --force || true +fi SH touch "$seed/openclaw.tgz" @@ -180,6 +199,29 @@ grep -q "Could not stop the OpenClaw gateway service" "$test_tmp/output" || fail fail "a gateway that will not stop leaves the runtime unseeded" "$(cat "$events")" pass "a gateway that will not stop stops the install before anything is seeded" +# Upstream's installer rewrites the gateway itself and only warns when it will +# not start again, so a gateway that was running has to be running afterwards. +new_home start-fails +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +touch "$test_home/active-openclaw-gateway.service" +OMARCHY_TEST_START_FAIL=1 run omarchy-install-openclaw-cli --now && fail "a moved gateway that does not start fails the install" +grep -q "Could not move the OpenClaw gateway service" "$test_tmp/output" && grep -q "is not running now" "$test_tmp/output" || + fail "a moved gateway that does not start is named, and so is its being stopped" "$(cat "$test_tmp/output")" +pass "a gateway that was running is running again from the runtime, or the install fails saying it is stopped" + +# With the runtime already in place nothing is seeded, so the move is Omarchy's. +new_home runtime-first +run omarchy-install-openclaw-cli --now || fail "--now sets OpenClaw up" "$(cat "$test_tmp/output")" +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +touch "$test_home/active-openclaw-gateway.service" +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now moves a gateway beside a runtime that already runs" "$(cat "$test_tmp/output")" +! grep -q '^install-cli' "$events" && grep -Fxq "runtime gateway install --force" "$events" && [[ -e $test_home/active-openclaw-gateway.service ]] || + fail "--now moves a gateway beside a runtime that already runs" "$(cat "$events")" +pass "a gateway beside a runtime that already runs is moved without seeding" + # The migration moves only machines that have the package, and waits for the # package that seeds. new_home migration-none