diff --git a/bin/omarchy-install-openclaw-cli b/bin/omarchy-install-openclaw-cli index 3a13c6c7..e4469181 100755 --- a/bin/omarchy-install-openclaw-cli +++ b/bin/omarchy-install-openclaw-cli @@ -20,6 +20,12 @@ 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" @@ -48,22 +54,47 @@ on_path() { [[ $(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() { - runtime_runs && on_path + 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 restarts it. A service running any other OpenClaw is -# the user's arrangement and stays. +# 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 ]] && grep -qF "/usr/lib/node_modules/openclaw/" "$unit"; then + if [[ -f $unit ]] && unit_runs "$unit" "/usr/lib/node_modules/openclaw/"; then echo "Moving the OpenClaw $role service to $prefix..." - if ! "$runtime_command" "$role" install --force; then + 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 @@ -82,8 +113,8 @@ case "$mode" in ;; esac -if (( EUID == 0 )); then - echo "Run this command as your desktop user, without sudo." >&2 +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 @@ -94,12 +125,28 @@ 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..." @@ -110,18 +157,13 @@ if ! runtime_runs; then fi fi -if command_ours; then - mkdir -p "${command_path%/*}" - ln -sfn "$runtime_command" "$command_path" -else - 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 +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 "Remove it or reorder PATH, then run omarchy-install-openclaw-cli --now again." >&2 + echo "Put ~/.local/bin on PATH, then run omarchy-install-openclaw-cli --now again." >&2 exit 1 fi diff --git a/bin/omarchy-remove-ai-openclaw b/bin/omarchy-remove-ai-openclaw index f4502cd8..7d793bdc 100755 --- a/bin/omarchy-remove-ai-openclaw +++ b/bin/omarchy-remove-ai-openclaw @@ -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. diff --git a/test/shell.d/openclaw-cli-test.sh b/test/shell.d/openclaw-cli-test.sh index 29654d76..74ac24b6 100755 --- a/test/shell.d/openclaw-cli-test.sh +++ b/test/shell.d/openclaw-cli-test.sh @@ -21,6 +21,10 @@ cat >"$mock_bin/omarchy-pkg-add" <<'SH' printf 'pkg-add %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" touch "$OMARCHY_TEST_ROOT/package-installed" SH +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" +SH chmod +x "$mock_bin/"* # Stands in for upstream's install-cli.sh: it writes the command the way the @@ -34,7 +38,7 @@ cat >"$prefix/bin/openclaw" <>"$OMARCHY_TEST_ROOT/events" +printf 'runtime %s%s\n' "\$*" "\${OPENCLAW_PROFILE:+ profile=\$OPENCLAW_PROFILE}" >>"$OMARCHY_TEST_ROOT/events" exec true "$prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/entry.js" "\$@" EOF chmod 755 "$prefix/bin/openclaw" @@ -122,10 +126,33 @@ printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw" chmod +x "$test_tmp/usr-bin/openclaw" run omarchy-install-openclaw-cli --now && fail "an openclaw earlier on PATH fails the install" grep -q "on PATH is $test_tmp/usr-bin/openclaw" "$test_tmp/output" || fail "an openclaw earlier on PATH is named" "$(cat "$test_tmp/output")" +[[ ! -e $test_home/.openclaw && ! -e $command ]] || fail "an openclaw earlier on PATH is refused before anything is touched" +rm "$test_tmp/usr-bin/openclaw" +run omarchy-install-openclaw-cli --now || fail "--now follows once nothing shadows the runtime" "$(cat "$test_tmp/output")" +printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw" +chmod +x "$test_tmp/usr-bin/openclaw" run omarchy-install-openclaw-cli --check && fail "--check calls a shadowed runtime installed" rm "$test_tmp/usr-bin/openclaw" run omarchy-install-openclaw-cli --check || fail "--check follows once nothing shadows the runtime" -pass "the runtime has to be the openclaw PATH finds" +pass "the runtime has to be the openclaw PATH finds, and anything else is refused before it is set up" + +# A runtime that answers without the package still leaves --now a pacman step, +# which the default agent must not run outside a terminal. +rm "$test_tmp/package-installed" +run omarchy-install-openclaw-cli --check && fail "--check calls a runtime without its package installed" +pass "--check needs the package too, so --now never has a password to ask for unseen" + +# Upstream's installer rewrites a loaded gateway service to the copy it has +# just made, so a gateway running another OpenClaw stops the seeding first. +new_home foreign-gateway +mkdir -p "$test_home/.config/systemd/user" +printf 'Environment=OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json\nExecStart=/opt/node %s/openclaw/dist/index.js gateway\n' "$test_home" "$test_home" \ + >"$test_home/.config/systemd/user/openclaw-gateway.service" +run omarchy-install-openclaw-cli --now && fail "a gateway running another OpenClaw stops the install" +grep -q "runs another OpenClaw" "$test_tmp/output" || fail "a gateway running another OpenClaw is named" "$(cat "$test_tmp/output")" +! grep -q '^install-cli' "$events" && [[ ! -e $test_home/.openclaw ]] || + fail "a gateway running another OpenClaw is refused before anything is set up" "$(cat "$events")" +pass "a gateway running another OpenClaw is refused before upstream's installer can take it over" # A gateway the old package installed runs from /usr/lib/node_modules, which # the seed package no longer ships; one running any other OpenClaw stays. @@ -134,9 +161,11 @@ units="$test_home/.config/systemd/user" mkdir -p "$units" printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$units/openclaw-gateway.service" printf 'ExecStart=/opt/node /home/someone/openclaw/dist/index.js node run\n' >"$units/openclaw-node.service" -run omarchy-install-openclaw-cli --now || fail "--now moves the old package's services" "$(cat "$test_tmp/output")" -grep -Fxq "runtime gateway install --force" "$events" || fail "--now moves a gateway the old package installed" "$(cat "$events")" -! grep -q "runtime node install" "$events" || fail "--now leaves a service running another OpenClaw alone" "$(cat "$events")" +OPENCLAW_PROFILE=work run omarchy-install-openclaw-cli --now || fail "--now moves the old package's services" "$(cat "$test_tmp/output")" +grep -A1 -Fx "systemctl --user stop openclaw-gateway.service" "$events" | grep -Fxq "runtime gateway install --force" || + fail "--now stops a gateway the old package installed, then moves it" "$(cat "$events")" +! grep -q "runtime node install\|openclaw-node" "$events" || fail "--now leaves a service running another OpenClaw alone" "$(cat "$events")" +! grep -q "install --force profile=work" "$events" || fail "--now moves the default unit whatever profile the shell selects" "$(cat "$events")" pass "a service the old package installed moves to the runtime, and only that one" # The migration moves only machines that have the package, and waits for the diff --git a/test/shell.d/remove-ai-test.sh b/test/shell.d/remove-ai-test.sh index 5778f373..693b929d 100644 --- a/test/shell.d/remove-ai-test.sh +++ b/test/shell.d/remove-ai-test.sh @@ -245,6 +245,8 @@ fresh_openclaw_home() { "$HOME/.local/share/applications" "$HOME/.local/share/icons/hicolor/256x256/apps" touch "$HOME/.config/systemd/user/openclaw-gateway.service" \ "$HOME/.config/systemd/user/openclaw-gateway.service.bak" \ + "$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak" \ + "$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak" \ "$HOME/.config/systemd/user/openclaw-node.service" \ "$HOME/.openclaw/openclaw.json" \ "$HOME/.local/share/applications/OpenClaw.desktop" \ @@ -269,6 +271,8 @@ fresh_openclaw_home for gone in .config/systemd/user/openclaw-gateway.service \ .config/systemd/user/openclaw-gateway.service.bak \ + .config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak \ + .config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak \ .config/systemd/user/default.target.wants/openclaw-gateway.service \ .config/systemd/user/openclaw-node.service \ .config/systemd/user/default.target.wants/openclaw-node.service \