Merge pull request #10246 from omacom/add-openclaw-ai-app
Add OpenClaw as a desktop app and a coding agent
This commit is contained in:
17 files changed
+1088
-6
No files matched your search
@@ -65,6 +65,7 @@ GROUP_DESCRIPTIONS[monitor]="Monitor status helpers"
|
||||
GROUP_DESCRIPTIONS[network]="Network status helpers"
|
||||
GROUP_DESCRIPTIONS[notification]="Notification helpers"
|
||||
GROUP_DESCRIPTIONS[mise]="Mise tool wrappers"
|
||||
GROUP_DESCRIPTIONS[openclaw]="OpenClaw agent platform setup"
|
||||
GROUP_DESCRIPTIONS[osd]="On-screen display status helpers"
|
||||
GROUP_DESCRIPTIONS[pkg]="Package management helpers"
|
||||
GROUP_DESCRIPTIONS[plugin]="Omarchy shell plugin and bar widget management"
|
||||
|
||||
@@ -82,6 +82,15 @@ grok)
|
||||
command=(grok --permission-mode bypassPermissions)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
openclaw)
|
||||
# The launcher owns onboarding and the gateway dance: OpenClaw's terminal UI
|
||||
# must attach to the running gateway (the embedded `openclaw chat` refuses to
|
||||
# start while the gateway owns the state directory). It has no permission
|
||||
# prompts to skip, and --message seeds the session while keeping it
|
||||
# interactive.
|
||||
command=(omarchy-launch-openclaw --tui)
|
||||
[[ -n ${prompt:-} ]] && command+=(--message "$prompt")
|
||||
;;
|
||||
codex)
|
||||
command=(codex --approve-for-me)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set and launch the default coding agent
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush]
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush]
|
||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
||||
|
||||
installing=false
|
||||
@@ -32,11 +32,12 @@ claude | claude-code) agent="claude"; name="Claude Code" ;;
|
||||
codex) agent="codex"; name="Codex" ;;
|
||||
crush) agent="crush"; name="Crush" ;;
|
||||
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
|
||||
openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-cli" ;;
|
||||
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
|
||||
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
|
||||
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush>"
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -45,7 +46,8 @@ agent_package=${agent_package:-$agent}
|
||||
|
||||
# Hermes reaches mise through its own installer rather than straight from
|
||||
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
|
||||
# say so. See omarchy-install-hermes-cli.
|
||||
# say so. See omarchy-install-hermes-cli. OpenClaw comes from its pacman
|
||||
# package the same way; see omarchy-install-openclaw-cli.
|
||||
if [[ -n ${agent_installer:-} ]]; then
|
||||
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
|
||||
# nothing about whether Hermes is installed behind it. Treating a cold stub
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the OpenClaw agent platform and its Control UI web app
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
|
||||
# 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
|
||||
# onboards or starts that gateway before opening the window. The icon ships
|
||||
# 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 \
|
||||
omarchy-launch-openclaw
|
||||
|
||||
# A first install runs onboarding right here: launching the app instead would
|
||||
# open a second floating terminal for the wizard, identical to this one.
|
||||
# omarchy-openclaw-onboard runs the wizard the way this flow needs (terminal
|
||||
# prompts, gateway as a user service, and it actually returns). A machine
|
||||
# that is already onboarded goes straight to the app.
|
||||
if [[ -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
echo "Opening OpenClaw..."
|
||||
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
|
||||
elif omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
echo "Opening OpenClaw..."
|
||||
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "OpenClaw has been installed."
|
||||
echo "If you skipped onboarding, launching OpenClaw from the app grid resumes it."
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Ensure the OpenClaw CLI is installed for the default agent
|
||||
# omarchy:args=[--check|--now]
|
||||
# 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.
|
||||
|
||||
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
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-install-openclaw-cli [--check|--now]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open the OpenClaw Control UI (or its terminal UI with --tui), onboarding or starting the gateway first when needed.
|
||||
# omarchy:args=[--tui [--message <text>]]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
tui=false
|
||||
message=()
|
||||
if [[ ${1:-} == "--tui" ]]; then
|
||||
tui=true
|
||||
shift
|
||||
if [[ ${1:-} == "--message" ]]; then
|
||||
message=(--message "${2:?--message needs a value}")
|
||||
shift 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Onboarding is OpenClaw's own interactive wizard, run through
|
||||
# omarchy-openclaw-onboard so it stays in the terminal, installs the gateway
|
||||
# as a user service, and returns (see that script for why bare `openclaw
|
||||
# onboard` does none of those as of 2026.9.1). The config check gates what
|
||||
# follows because the wizard's "Skip for now" also exits 0: only inference
|
||||
# that passed writes the config, and skipping must not loop back into the
|
||||
# wizard.
|
||||
if [[ ! -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
if [[ $tui == "true" ]]; then
|
||||
# Already in a terminal, so the wizard runs right here.
|
||||
omarchy-openclaw-onboard
|
||||
[[ -f $HOME/.openclaw/openclaw.json ]] || exit 1
|
||||
else
|
||||
# The wizard needs a real terminal, and chaining the relaunch means
|
||||
# finishing it lands the user in the app. Single quotes so $HOME expands
|
||||
# in the spawned terminal.
|
||||
# shellcheck disable=SC2016
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
'omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]] && omarchy-launch-openclaw'
|
||||
fi
|
||||
fi
|
||||
|
||||
dashboard_url() {
|
||||
# browserUrl carries a single-use browser handoff; url is the shared-auth
|
||||
# fallback for gateways predating the handoff flow. The timeout keeps a
|
||||
# wedged CLI from hanging an app-grid launch that has no terminal to ^C.
|
||||
timeout 10 openclaw dashboard --json 2>/dev/null | jq -re '.browserUrl // .url // empty'
|
||||
}
|
||||
|
||||
# --json never starts the gateway, so an empty answer means it is not running.
|
||||
# Recovery goes through the gateway's own service commands. `dashboard --yes`
|
||||
# used to be the start/install-without-prompting path, but as of 2026.9.1 it
|
||||
# defers to "the owning supervisor" for both a missing and a stopped unit,
|
||||
# and once the gateway is up it copies a one-time pairing URL into the
|
||||
# clipboard, which nothing here needs. Enablement, not the unit file, decides:
|
||||
# a unit that was written but never enabled (an install that died halfway)
|
||||
# would otherwise be "started" once and stay off at every following login,
|
||||
# where --force rewrites and enables it.
|
||||
if ! url=$(dashboard_url); then
|
||||
if systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then
|
||||
timeout 60 openclaw gateway start >&2 || true
|
||||
else
|
||||
timeout 120 openclaw gateway install --force >&2 || true
|
||||
fi
|
||||
|
||||
# A first-ever service install (unit write, daemon-reload, first boot)
|
||||
# takes materially longer than starting an installed unit, so the budget
|
||||
# is sized for the slow case.
|
||||
for _ in {1..30}; do
|
||||
url=$(dashboard_url) && break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z ${url:-} ]]; then
|
||||
echo "OpenClaw's gateway did not come up. Check it with: openclaw gateway status" >&2
|
||||
echo "If onboarding never finished, rerun it with: omarchy-openclaw-onboard" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $tui == "true" ]]; then
|
||||
# Attach to the gateway rather than `openclaw chat`: chat is the embedded
|
||||
# local runtime, which refuses to start while the gateway owns ~/.openclaw's
|
||||
# state directory -- and on any machine set up through Install > AI, the
|
||||
# gateway service is running whenever the desktop session is.
|
||||
exec openclaw tui "${message[@]}"
|
||||
fi
|
||||
|
||||
# The handoff URL rides in the browser's argv, which uwsm's app daemon echoes
|
||||
# into the user journal. Accepted: the token is single-use with a ten-minute
|
||||
# expiry against a loopback-only gateway, and journal access already implies
|
||||
# access to ~/.openclaw itself.
|
||||
exec omarchy-launch-webapp "$url"
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Run OpenClaw's setup wizard the way Omarchy needs it: in the terminal, installing the gateway as a user service, and returning when it is done.
|
||||
|
||||
# Not bare `openclaw onboard`: as of 2026.9.1 that is the guided flow, which
|
||||
# ends by running a foreground gateway and handing off to a browser tab, and
|
||||
# never returns. --install-daemon keeps it to the classic wizard (minimal
|
||||
# prompts with --flow quickstart) that installs the gateway service, and
|
||||
# --skip-ui drops its closing Control UI/TUI prompt since whoever called this
|
||||
# opens one next.
|
||||
#
|
||||
# That classic wizard has one wrinkle: with --skip-ui it prints "Onboarding
|
||||
# complete" and then never exits. Upstream only calls exit(0) when it launched
|
||||
# the TUI, and the model sign-in leaves an open socket that keeps the process
|
||||
# alive otherwise. So this script watches for the gateway to answer and then
|
||||
# stops the wizard. That is safe because every prompt in the quickstart flow
|
||||
# runs before the gateway service is installed and reachable: by the time the
|
||||
# dashboard answers, only the closing notes are left.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
wizard=(openclaw onboard --flow quickstart --install-daemon --skip-ui)
|
||||
config=$HOME/.openclaw/openclaw.json
|
||||
settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3}
|
||||
# Counted from the moment the config exists, i.e. once the wizard has applied
|
||||
# setup; the prompts before that take as long as the user takes.
|
||||
gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180}
|
||||
|
||||
gateway_answers() {
|
||||
timeout 10 openclaw dashboard --json 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
|
||||
# read its own success off state that predates it: the wizard's repair pass
|
||||
# would be stopped mid-prompt the moment the watcher looked. Nothing to do.
|
||||
if [[ -f $config ]] && gateway_answers; then
|
||||
echo "OpenClaw is already set up and its gateway is running." >&2
|
||||
echo "To change providers or settings, run: openclaw onboard --classic" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Marks when this run began, so a config left behind by an earlier, incomplete
|
||||
# setup is not mistaken for this run having applied its own: the gateway
|
||||
# deadline below must not start ticking while the user is still at prompts.
|
||||
started=$(mktemp)
|
||||
trap 'rm -f "$started"' EXIT
|
||||
|
||||
# Backgrounded so this script can watch it, but with the terminal kept as its
|
||||
# stdin (bash would otherwise hand a background job /dev/null). Job control is
|
||||
# off in a script, so it stays in the terminal's foreground process group and
|
||||
# reads from it freely. Ctrl-C does not reach it directly, though: bash starts
|
||||
# async children with SIGINT ignored when job control is off, so the INT trap
|
||||
# below is what turns Ctrl-C into the wizard's exit.
|
||||
"${wizard[@]}" <&0 &
|
||||
wizard_pid=$!
|
||||
|
||||
stop_wizard() {
|
||||
kill -TERM "$wizard_pid" 2>/dev/null || true
|
||||
}
|
||||
# Any signal at this script, whether Ctrl-C from the terminal or a kill aimed
|
||||
# at its pid alone, takes the wizard down with it rather than leaving it
|
||||
# running unwatched.
|
||||
trap 'stop_wizard' INT TERM HUP
|
||||
|
||||
# Whether this run has applied setup: the config exists and is not older than
|
||||
# the run itself.
|
||||
config_applied() {
|
||||
[[ -f $config && ! $started -nt $config ]]
|
||||
}
|
||||
|
||||
# Whether this run's gateway is up: setup applied, and the process answering
|
||||
# on the gateway's port is the main process of the service the wizard
|
||||
# installs. Not the dashboard alone: with no config on disk `openclaw
|
||||
# dashboard --json` still probes the default loopback port, so a gateway left
|
||||
# behind by something else (an earlier guided onboarding's foreground
|
||||
# gateway, say) would read as this run's success while the user is still at
|
||||
# the first prompt. And not the unit being active either: its Type=simple
|
||||
# counts it active from the fork, before it has found the port taken by such
|
||||
# an orphan, and the app would then open on the orphan rather than the
|
||||
# service this run installed.
|
||||
gateway_ready() {
|
||||
config_applied || return 1
|
||||
local json port listener main_pid
|
||||
json=$(timeout 10 openclaw dashboard --json 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
|
||||
listener=$(ss -ltnpH "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
||||
main_pid=$(systemctl --user show -p MainPID --value openclaw-gateway.service 2>/dev/null)
|
||||
[[ -n $listener && -n $main_pid && $main_pid != 0 && $listener == "$main_pid" ]]
|
||||
}
|
||||
|
||||
stopped=false
|
||||
timed_out=false
|
||||
config_seen_at=
|
||||
while kill -0 "$wizard_pid" 2>/dev/null; do
|
||||
sleep 2
|
||||
if gateway_ready; then
|
||||
# Let the outro finish printing, then end the process the wizard leaves
|
||||
# running.
|
||||
sleep "$settle_seconds"
|
||||
stop_wizard
|
||||
stopped=true
|
||||
break
|
||||
fi
|
||||
config_applied || continue
|
||||
: "${config_seen_at:=$SECONDS}"
|
||||
if (( SECONDS - config_seen_at >= gateway_timeout )); then
|
||||
# Setup was applied but the service never came up (port taken, unit
|
||||
# failing, ...): the wizard would sit in its never-exiting state forever,
|
||||
# and so would whoever is waiting on this script.
|
||||
stop_wizard
|
||||
timed_out=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
wait "$wizard_pid"
|
||||
rc=$?
|
||||
|
||||
if [[ $timed_out == true ]]; then
|
||||
echo "OpenClaw's gateway did not come up within ${gateway_timeout}s of setup finishing." >&2
|
||||
echo "Check it with: openclaw gateway status" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A wizard stopped here after the gateway came up did its job. One that
|
||||
# exited on its own, including a user who chose "Skip for now" (no config,
|
||||
# non-zero), keeps its own exit code.
|
||||
[[ $stopped == true ]] && rc=0
|
||||
exit "$rc"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the OpenClaw agent platform along with its gateway service and web app.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
unit_dir="$HOME/.config/systemd/user"
|
||||
|
||||
# Only systemd's own word counts as "stopped": a non-zero exit from is-active
|
||||
# also covers an unreachable user manager, which says nothing about whether
|
||||
# the process is alive.
|
||||
unit_stopped() {
|
||||
local state
|
||||
state=$(systemctl --user is-active "$1" 2>/dev/null) || true
|
||||
[[ $state == inactive || $state == failed ]]
|
||||
}
|
||||
|
||||
# The user services OpenClaw installs for itself: the gateway through
|
||||
# onboarding, and the node host if the user ever paired this machine to
|
||||
# another gateway. OpenClaw writes them to $HOME/.config regardless of
|
||||
# XDG_CONFIG_HOME, so look exactly there; no unit means nothing registered.
|
||||
# Upstream's own teardown knows every piece its install wrote (unit file, the
|
||||
# default.target.wants enablement symlink, the reload), so prefer it while the
|
||||
# binary is still installed and fall back to doing the same by hand. A service
|
||||
# that will not stop aborts the removal: dropping the package would strand the
|
||||
# live process on deleted code with no way to restart it cleanly.
|
||||
for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node.service; do
|
||||
[[ -f $unit_file ]] || continue
|
||||
unit=${unit_file##*/}
|
||||
role=${unit#openclaw-}
|
||||
role=${role%.service}
|
||||
if openclaw "$role" uninstall >/dev/null 2>&1 ||
|
||||
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"
|
||||
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.
|
||||
systemctl --user reset-failed "$unit" 2>/dev/null || true
|
||||
else
|
||||
echo "Could not stop $unit; OpenClaw was not removed." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
omarchy-pkg-drop openclaw
|
||||
|
||||
# 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"
|
||||
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
|
||||
|
||||
# ~/.openclaw stays unless asked: the chats, memories, and credentials in
|
||||
# there are the user's agent, and finding them intact after a reinstall is the
|
||||
# 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.
|
||||
state_removed=false
|
||||
if [[ -d $HOME/.openclaw && -t 0 ]] && command -v gum >/dev/null; 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"
|
||||
state_removed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "OpenClaw has been removed."
|
||||
if [[ $state_removed == true ]]; then
|
||||
echo "Its chats, memories, and settings in ~/.openclaw are gone too."
|
||||
elif [[ -d $HOME/.openclaw ]]; then
|
||||
echo "Your agent's chats, memories, and settings are still in ~/.openclaw."
|
||||
fi
|
||||
@@ -13,6 +13,7 @@ The private-use glyphs in `omarchy.ttf` are:
|
||||
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
|
||||
- `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one
|
||||
- `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from <https://fontawesome.com/icons/staff-snake>, the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size
|
||||
- `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG; `E90B` is left for the Perplexity mark already in flight on another branch
|
||||
|
||||
The agent marks are monochrome so the menu can render them using the active
|
||||
theme's foreground and selection colors.
|
||||
Binary file not shown.
@@ -143,6 +143,7 @@
|
||||
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
|
||||
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
|
||||
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
||||
"setup.default.agent.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","checked":"[[ \"$(omarchy-default-agent)\" == \"openclaw\" ]]","action":"omarchy-default-agent openclaw"},
|
||||
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
|
||||
"setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"},
|
||||
"setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
|
||||
@@ -243,6 +244,7 @@
|
||||
"install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"},
|
||||
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
|
||||
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
|
||||
"install.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","disabled":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-openclaw"},
|
||||
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"},
|
||||
"install.gaming.steam": {"icon":"","label":"Steam","disabled":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
|
||||
"install.gaming.retroarch": {"icon":"","label":"RetroArch","disabled":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
|
||||
@@ -308,6 +310,7 @@
|
||||
"remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"},
|
||||
"remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"},
|
||||
"remove.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"omarchy-pkg-present ollama","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-ollama"},
|
||||
"remove.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","when":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-openclaw"},
|
||||
"remove.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","when":"omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-t3-code"},
|
||||
"remove.gaming.steam": {"icon":"","label":"Steam","when":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-steam"},
|
||||
"remove.gaming.retroarch": {"icon":"","label":"RetroArch","when":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-retroarch"},
|
||||
|
||||
+3
-1
@@ -44,10 +44,12 @@ Crashes can also be silenced one program at a time, which is what the diagnosis
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, and Hermes Desktop.
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, Hermes Desktop, and OpenClaw.
|
||||
|
||||
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself.
|
||||
|
||||
OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it.
|
||||
|
||||
### Local LLMs
|
||||
|
||||
Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. LM Studio provides a GUI interface for finding open-weight models, installing them, and running them. It's a great way to get going easily. Ollama offers a CLI for doing so similarly. But if you're new to local models, I'd start with LM Studio. You can install either under _Install > AI_ in the Omarchy Menu.
|
||||
|
||||
@@ -542,3 +542,68 @@ fi
|
||||
grep -F "missing is not installed" "$test_tmp/missing-output" >/dev/null ||
|
||||
fail "agent launcher explains when the default command is missing"
|
||||
pass "agent launcher reports a missing default command"
|
||||
|
||||
# OpenClaw comes from its pacman package, not mise: choosing it must route
|
||||
# through omarchy-install-openclaw-cli and never touch a mise environment.
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == openclaw && ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]]
|
||||
SH
|
||||
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "pkg-add $*" >>"$OMARCHY_TEST_STUB_LOG"
|
||||
SH
|
||||
cat >"$mock_bin/omarchy-launch-openclaw" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' omarchy-launch-openclaw "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG"
|
||||
SH
|
||||
cat >"$mock_bin/openclaw" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-pkg-present" "$mock_bin/omarchy-pkg-add" \
|
||||
"$mock_bin/omarchy-launch-openclaw" "$mock_bin/openclaw"
|
||||
|
||||
: >"$launch_log"
|
||||
: >"$terminal_log"
|
||||
: >"$mise_history"
|
||||
OMARCHY_TEST_OPENCLAW_INSTALLED=true omarchy-default-agent openclaw
|
||||
read -r chosen <"$agent_file"
|
||||
[[ $chosen == openclaw ]] || fail "choosing OpenClaw records it as the default agent"
|
||||
mapfile -d '' -t launch_args <"$launch_log"
|
||||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent omarchy-launch-openclaw --tui" ]] ||
|
||||
fail "choosing OpenClaw launches its terminal UI"
|
||||
[[ ! -s $terminal_log ]] || fail "an installed OpenClaw needs no install terminal"
|
||||
! grep -q 'use -g openclaw' "$mise_history" || fail "OpenClaw never installs through mise"
|
||||
pass "choosing OpenClaw uses the package and launches its terminal UI"
|
||||
|
||||
: >"$terminal_log"
|
||||
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent openclaw
|
||||
mapfile -d '' -t terminal_args <"$terminal_log"
|
||||
[[ ${terminal_args[*]} == "omarchy-default-agent --install openclaw" ]] ||
|
||||
fail "a missing OpenClaw routes through the install terminal"
|
||||
pass "a missing OpenClaw routes through the install terminal"
|
||||
|
||||
: >"$stub_log"
|
||||
: >"$inline_log"
|
||||
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent --install openclaw >/dev/null
|
||||
grep -Fx "pkg-add openclaw" "$stub_log" >/dev/null ||
|
||||
fail "installing OpenClaw as default agent adds its package"
|
||||
mapfile -d '' -t inline_args <"$inline_log"
|
||||
[[ ${inline_args[*]} == "omarchy-launch-openclaw --tui" ]] ||
|
||||
fail "installing OpenClaw as default agent hands over to its terminal UI"
|
||||
pass "installing OpenClaw as default agent adds its package"
|
||||
|
||||
: >"$launch_log"
|
||||
omarchy agent prompt "Review this project"
|
||||
mapfile -d '' -t launch_args <"$launch_log"
|
||||
# Element-wise: the prompt must travel as one argv entry, which a space-joined
|
||||
# comparison could not tell apart from a prompt split into words.
|
||||
[[ ${#launch_args[@]} == 5 &&
|
||||
${launch_args[0]} == "--app-id=org.omarchy.agent" &&
|
||||
${launch_args[1]} == "omarchy-launch-openclaw" &&
|
||||
${launch_args[2]} == "--tui" &&
|
||||
${launch_args[3]} == "--message" &&
|
||||
${launch_args[4]} == "Review this project" ]] ||
|
||||
fail "OpenClaw receives prompts through --message" "argv: ${launch_args[*]}"
|
||||
pass "OpenClaw receives prompts through --message"
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
|
||||
export TEST_LOG="$tmp_dir/log"
|
||||
export PATH="$tmp_dir/bin:$PATH"
|
||||
export HOME="$tmp_dir/home"
|
||||
|
||||
for stub in omarchy-launch-webapp omarchy-launch-floating-terminal-with-presentation omarchy-openclaw-onboard; do
|
||||
cat >"$tmp_dir/bin/$stub" <<SCRIPT
|
||||
#!/bin/bash
|
||||
printf '$stub:%s\n' "\$*" >>"\$TEST_LOG"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/$stub"
|
||||
done
|
||||
|
||||
# The retry loop sleeps between polls; a no-op keeps the suite fast.
|
||||
printf '#!/bin/bash\n' >"$tmp_dir/bin/sleep"
|
||||
chmod +x "$tmp_dir/bin/sleep"
|
||||
|
||||
# The launcher asks systemd whether the gateway unit is enabled; a flag file
|
||||
# stands in for the user manager.
|
||||
cat >"$tmp_dir/bin/systemctl" <<SCRIPT
|
||||
#!/bin/bash
|
||||
printf 'systemctl:%s\\n' "\$*" >>"\$TEST_LOG"
|
||||
[[ \$* == *is-enabled* ]] && [[ -f $tmp_dir/gateway-enabled ]]
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/systemctl"
|
||||
|
||||
# A machine that never onboarded gets the wizard, not a dashboard probe.
|
||||
"$ROOT/bin/omarchy-launch-openclaw"
|
||||
|
||||
# Through omarchy-openclaw-onboard, never bare `openclaw onboard`, which as of
|
||||
# 2026.9.1 ends in a browser tab with a foreground gateway and never returns.
|
||||
grep -q '^omarchy-launch-floating-terminal-with-presentation:omarchy-openclaw-onboard && \[\[ -f \$HOME/.openclaw/openclaw.json \]\] && omarchy-launch-openclaw$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "$(grep floating "$TEST_LOG")"
|
||||
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "webapp opened anyway"
|
||||
pass "OpenClaw launch hands a never-onboarded machine to the wizard"
|
||||
|
||||
mkdir -p "$HOME/.openclaw"
|
||||
touch "$HOME/.openclaw/openclaw.json"
|
||||
|
||||
# A running gateway answers the first probe; its handoff URL opens as the app.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $* == *--json* ]] &&
|
||||
echo '{"ok":true,"url":"http://127.0.0.1:18789/?token=shared","browserUrl":"http://127.0.0.1:18789/#handoff"}'
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
"$ROOT/bin/omarchy-launch-openclaw"
|
||||
|
||||
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#handoff$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch opens the running gateway's handoff URL"
|
||||
pass "OpenClaw launch opens the running gateway's handoff URL"
|
||||
|
||||
# A machine whose gateway unit was never enabled (never installed, or an
|
||||
# install that died after writing the unit) gets `gateway install --force`,
|
||||
# then is polled until the dashboard answers. Never `dashboard --yes`: since
|
||||
# 2026.9.1 that neither installs nor starts anything and, once the gateway is
|
||||
# up, pushes a one-time pairing URL into the clipboard.
|
||||
cat >"$tmp_dir/bin/openclaw" <<SCRIPT
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\\n' "\$*" >>"\$TEST_LOG"
|
||||
if [[ \$* == *--json* ]]; then
|
||||
[[ -f $tmp_dir/gateway-up ]] || { echo '{"ok":false,"reason":"Gateway is not running."}'; exit 1; }
|
||||
echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#cold-start"}'
|
||||
elif [[ \$1 == gateway ]]; then
|
||||
touch "$tmp_dir/gateway-up"
|
||||
fi
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rm -f "$tmp_dir/gateway-up" "$tmp_dir/gateway-enabled"
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
touch "$HOME/.config/systemd/user/openclaw-gateway.service"
|
||||
: >"$TEST_LOG"
|
||||
"$ROOT/bin/omarchy-launch-openclaw"
|
||||
|
||||
grep -q '^openclaw:gateway install --force$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch installs a never-enabled gateway before opening the app"
|
||||
! grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "started the half-installed unit instead"
|
||||
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "webapp never opened"
|
||||
pass "OpenClaw launch installs a never-enabled gateway before opening the app"
|
||||
|
||||
# An enabled but stopped unit is started, not reinstalled.
|
||||
touch "$tmp_dir/gateway-enabled"
|
||||
rm -f "$tmp_dir/gateway-up"
|
||||
: >"$TEST_LOG"
|
||||
"$ROOT/bin/omarchy-launch-openclaw"
|
||||
|
||||
grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch starts a stopped gateway before opening the app"
|
||||
! grep -q '^openclaw:gateway install' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch starts a stopped gateway before opening the app" "reinstalled the unit"
|
||||
! grep -q -- '--yes' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch starts a stopped gateway before opening the app" "fell back to dashboard --yes"
|
||||
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
|
||||
fail "OpenClaw launch starts a stopped gateway before opening the app" "webapp never opened"
|
||||
pass "OpenClaw launch starts a stopped gateway before opening the app"
|
||||
rm -f "$tmp_dir/gateway-enabled"
|
||||
|
||||
# A dashboard probe that hangs is cut off, so the launch still fails cleanly
|
||||
# instead of sitting forever behind an app-grid icon with no terminal.
|
||||
cat >"$tmp_dir/bin/timeout" <<SCRIPT
|
||||
#!/bin/bash
|
||||
printf 'timeout:%s\\n' "\$1" >>"\$TEST_LOG"
|
||||
exit 124
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/timeout"
|
||||
: >"$TEST_LOG"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc != 0 ]] || fail "OpenClaw launch bounds a hanging dashboard probe"
|
||||
grep -q '^timeout:10$' "$TEST_LOG" || fail "OpenClaw launch bounds a hanging dashboard probe" "probe ran without a timeout"
|
||||
pass "OpenClaw launch bounds a hanging dashboard probe"
|
||||
rm -f "$tmp_dir/bin/timeout"
|
||||
|
||||
# A gateway that never answers fails the launch instead of opening a dead page.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $* == *--json* ]] && exit 1
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
webapp_calls_before=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
|
||||
webapp_calls_after=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
|
||||
|
||||
[[ $rc != 0 ]] || fail "OpenClaw launch fails cleanly when the gateway never comes up"
|
||||
[[ $webapp_calls_before == "$webapp_calls_after" ]] ||
|
||||
fail "OpenClaw launch fails cleanly when the gateway never comes up" "webapp opened anyway"
|
||||
pass "OpenClaw launch fails cleanly when the gateway never comes up"
|
||||
|
||||
# --tui runs onboarding in the terminal it is already in, then attaches to the
|
||||
# gateway with `openclaw tui` -- never the embedded chat, which the running
|
||||
# gateway's state-directory lock would refuse.
|
||||
rm -f "$HOME/.openclaw/openclaw.json"
|
||||
cat >"$tmp_dir/bin/omarchy-openclaw-onboard" <<SCRIPT
|
||||
#!/bin/bash
|
||||
printf 'omarchy-openclaw-onboard:%s\n' "\$*" >>"\$TEST_LOG"
|
||||
mkdir -p "\$HOME/.openclaw" && touch "\$HOME/.openclaw/openclaw.json"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/omarchy-openclaw-onboard"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
floating_calls_before=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
|
||||
"$ROOT/bin/omarchy-launch-openclaw" --tui
|
||||
|
||||
grep -q '^omarchy-openclaw-onboard:$' "$TEST_LOG" ||
|
||||
fail "--tui onboards in the current terminal" "$(grep 'onboard' "$TEST_LOG")"
|
||||
floating_calls_after=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
|
||||
[[ $floating_calls_before == "$floating_calls_after" ]] ||
|
||||
fail "--tui onboards in the current terminal" "spawned a floating terminal"
|
||||
grep -q '^openclaw:tui$' "$TEST_LOG" || fail "--tui attaches to the gateway"
|
||||
pass "--tui onboards in place and attaches to the gateway"
|
||||
|
||||
# The stub also logs argv one entry per line, so a prompt split into words
|
||||
# would show up as extra lines rather than pass a space-joined comparison.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $1 == tui ]] && printf 'argv:[%s]\n' "$@" >>"$TEST_LOG"
|
||||
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
: >"$TEST_LOG"
|
||||
"$ROOT/bin/omarchy-launch-openclaw" --tui --message "Review this project"
|
||||
[[ $(grep -c '^argv:' "$TEST_LOG") == 3 ]] && grep -q '^argv:\[Review this project\]$' "$TEST_LOG" ||
|
||||
fail "--tui seeds the session through --message" "$(grep '^argv:' "$TEST_LOG" | tr '\n' ' ')"
|
||||
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
|
||||
fail "--tui seeds the session through --message" "webapp opened instead"
|
||||
pass "--tui seeds the session through --message"
|
||||
@@ -225,6 +225,7 @@ const expectedAgents = {
|
||||
codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' },
|
||||
grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' },
|
||||
hermes: { icon: '\ue90a', iconFont: 'omarchy', label: 'Hermes' },
|
||||
openclaw: { icon: '\ue90c', iconFont: 'omarchy', label: 'OpenClaw' },
|
||||
copilot: { icon: '', label: 'Copilot' },
|
||||
crush: { icon: '', label: 'Crush' },
|
||||
}
|
||||
@@ -245,7 +246,7 @@ assertDeepEqual(
|
||||
defaultItems
|
||||
.filter(item => item.parent === 'setup.default.agent')
|
||||
.map(item => item.label),
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenCode', 'Ori', 'Pi'],
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenClaw', 'OpenCode', 'Ori', 'Pi'],
|
||||
'menu sorts coding agents alphabetically'
|
||||
)
|
||||
const expectedDefaults = {
|
||||
@@ -637,5 +638,5 @@ assert(
|
||||
JS
|
||||
|
||||
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
|
||||
[[ $font_charset == *"e900-e90a"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
[[ $font_charset == *"e900-e90a"* && $font_charset == *"e90c"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
pass "Omarchy icon font includes the official agent marks"
|
||||
Executable
+288
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
|
||||
export TEST_LOG="$tmp_dir/log"
|
||||
export PATH="$tmp_dir/bin:$PATH"
|
||||
export HOME="$tmp_dir/home"
|
||||
export OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS=0
|
||||
unit="$HOME/.config/systemd/user/openclaw-gateway.service"
|
||||
export unit
|
||||
|
||||
# systemd and ss stand in for themselves. Once the wizard has written the unit
|
||||
# it is running as pid 4242, and that same process holds the gateway port
|
||||
# unless a test plants a different listener (an orphan) in $listener_file.
|
||||
listener_file="$tmp_dir/listener"
|
||||
export listener_file
|
||||
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
|
||||
if [[ $* == *MainPID* ]]; then
|
||||
[[ -f $unit ]] && echo 4242 || echo 0
|
||||
exit 0
|
||||
fi
|
||||
[[ $* == *is-active* ]] && [[ -f $unit ]]
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/systemctl"
|
||||
cat >"$tmp_dir/bin/ss" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
pid=
|
||||
[[ -f $listener_file ]] && pid=$(cat "$listener_file")
|
||||
[[ -z $pid && -f $unit ]] && pid=4242
|
||||
[[ -n $pid ]] && echo "LISTEN 0 511 127.0.0.1:18789 0.0.0.0:* users:((\"node\",pid=$pid,fd=3))"
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/ss"
|
||||
export OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT=1
|
||||
|
||||
# A wizard that configures OpenClaw, prints its outro, and then lingers forever
|
||||
# on an open handle: the 2026.9.1 --skip-ui behaviour. It "installs the
|
||||
# gateway" by writing the config a moment in, so the gateway only starts
|
||||
# answering partway through, never at once.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard)
|
||||
( sleep 1; mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit" ) &
|
||||
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
|
||||
while :; do sleep 0.2; done
|
||||
;;
|
||||
dashboard)
|
||||
[[ -f $HOME/.openclaw/openclaw.json ]] && echo '{"ok":true,"port":18789}' || { echo '{"ok":false}'; exit 1; }
|
||||
;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
|
||||
start=$SECONDS
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
|
||||
grep -q '^openclaw:onboard --flow quickstart --install-daemon --skip-ui$' "$TEST_LOG" ||
|
||||
fail "onboarding runs the classic quickstart wizard with the service install" "$(grep '^openclaw:onboard' "$TEST_LOG" || true)"
|
||||
pass "onboarding runs the classic quickstart wizard with the service install"
|
||||
|
||||
[[ $rc == 0 ]] || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "rc=$rc"
|
||||
grep -q '^terminated$' "$TEST_LOG" ||
|
||||
fail "a wizard that lingers after the gateway is up is stopped and counts as success" "wizard was never signalled"
|
||||
(( elapsed < 30 )) || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "took ${elapsed}s"
|
||||
pass "a wizard that lingers after the gateway is up is stopped and counts as success"
|
||||
|
||||
# The wizard only starts being stopped once the gateway actually answers: a
|
||||
# stub that never writes the config is left alone and must be ended by its own
|
||||
# exit, not the watcher.
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $1 == onboard ]] && { echo "Skipped for now."; exit 3; }
|
||||
echo '{"ok":false}'; exit 1
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 3 ]] || fail "an abandoned wizard passes its exit code through" "rc=$rc"
|
||||
[[ ! -f $HOME/.openclaw/openclaw.json ]] || fail "an abandoned wizard passes its exit code through" "config appeared"
|
||||
! grep -q '^terminated$' "$TEST_LOG" || fail "an abandoned wizard passes its exit code through" "watcher signalled it anyway"
|
||||
pass "an abandoned wizard passes its exit code through"
|
||||
|
||||
# A wizard that finishes and exits on its own is simply waited for.
|
||||
: >"$TEST_LOG"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $1 == onboard ]] && { mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"; exit 0; }
|
||||
echo '{"ok":true,"port":18789}'
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 0 ]] || fail "a wizard that exits cleanly is waited for" "rc=$rc"
|
||||
pass "a wizard that exits cleanly is waited for"
|
||||
|
||||
# Input reaches the wizard: it runs in the foreground, not backgrounded (which
|
||||
# would stop it on SIGTTIN the moment it read stdin).
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard)
|
||||
read -r answer
|
||||
printf 'answer:%s\n' "$answer" >>"$TEST_LOG"
|
||||
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
|
||||
exit 0
|
||||
;;
|
||||
dashboard) echo '{"ok":true,"port":18789}' ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
printf 'gpt-5\n' | "$ROOT/bin/omarchy-openclaw-onboard" >/dev/null 2>&1
|
||||
grep -q '^answer:gpt-5$' "$TEST_LOG" ||
|
||||
fail "the wizard reads terminal input" "$(grep '^answer' "$TEST_LOG" || echo 'no input reached it')"
|
||||
pass "the wizard reads terminal input"
|
||||
|
||||
# An already-configured machine whose gateway answers is not onboarded again:
|
||||
# the wizard's repair pass would otherwise be stopped mid-prompt the moment
|
||||
# the watcher saw the pre-existing gateway.
|
||||
: >"$TEST_LOG"
|
||||
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $1 == onboard ]] && { echo wizard-ran >>"$TEST_LOG"; exit 0; }
|
||||
echo '{"ok":true,"port":18789}'
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 0 ]] || fail "an already-running OpenClaw is left alone" "rc=$rc"
|
||||
! grep -q '^wizard-ran$' "$TEST_LOG" || fail "an already-running OpenClaw is left alone" "wizard ran anyway"
|
||||
pass "an already-running OpenClaw is left alone"
|
||||
|
||||
# Setup applied but the gateway never answers: the wizard would linger
|
||||
# forever, so it is stopped after the deadline and that is reported as failure.
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard)
|
||||
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
|
||||
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
|
||||
while :; do sleep 0.2; done
|
||||
;;
|
||||
dashboard) echo '{"ok":false}'; exit 1 ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
start=$SECONDS
|
||||
rc=0
|
||||
err=$("$ROOT/bin/omarchy-openclaw-onboard" </dev/null 2>&1 >/dev/null) || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
[[ $rc == 1 ]] || fail "a gateway that never comes up ends the wait with a failure" "rc=$rc"
|
||||
grep -q '^terminated$' "$TEST_LOG" || fail "a gateway that never comes up ends the wait with a failure" "wizard left running"
|
||||
[[ $err == *"gateway status"* ]] || fail "a gateway that never comes up ends the wait with a failure" "no diagnostic: $err"
|
||||
(( elapsed < 30 )) || fail "a gateway that never comes up ends the wait with a failure" "took ${elapsed}s"
|
||||
pass "a gateway that never comes up ends the wait with a failure"
|
||||
|
||||
# A signal at this script alone takes the wizard down with it.
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
|
||||
dashboard) echo '{"ok":false}'; exit 1 ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 &
|
||||
wrapper=$!
|
||||
sleep 1
|
||||
kill -TERM "$wrapper"
|
||||
wait "$wrapper" 2>/dev/null || true
|
||||
sleep 1
|
||||
grep -q '^terminated$' "$TEST_LOG" || fail "stopping the wrapper stops the wizard" "wizard survived"
|
||||
! pgrep -f "$tmp_dir/bin/openclaw onboard" >/dev/null || fail "stopping the wrapper stops the wizard" "wizard process still alive"
|
||||
pass "stopping the wrapper stops the wizard"
|
||||
|
||||
# A config left by an earlier, incomplete setup does not start the gateway
|
||||
# deadline: only this run applying setup does. The wizard here takes longer
|
||||
# than the deadline at its prompts, never touches the old config, and must be
|
||||
# left to finish on its own.
|
||||
: >"$TEST_LOG"
|
||||
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
|
||||
sleep 1
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
|
||||
dashboard) echo '{"ok":false}'; exit 1 ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 0 ]] || fail "a stale config does not start the gateway deadline" "rc=$rc"
|
||||
grep -q '^finished$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was cut short"
|
||||
! grep -q '^terminated$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was signalled"
|
||||
pass "a stale config does not start the gateway deadline"
|
||||
|
||||
# ...while the same stale config being rewritten by this run does arm it.
|
||||
: >"$TEST_LOG"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard) touch "$HOME/.openclaw/openclaw.json"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
|
||||
dashboard) echo '{"ok":false}'; exit 1 ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 1 ]] || fail "a config rewritten by this run arms the gateway deadline" "rc=$rc"
|
||||
grep -q '^terminated$' "$TEST_LOG" || fail "a config rewritten by this run arms the gateway deadline" "wizard left running"
|
||||
pass "a config rewritten by this run arms the gateway deadline"
|
||||
|
||||
# A gateway that answers while no config exists is not this run's: with no
|
||||
# config `openclaw dashboard --json` still probes the default port, so a
|
||||
# process left behind by something else (an earlier guided onboarding's
|
||||
# foreground gateway) would otherwise end a wizard still at its first prompt.
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
|
||||
dashboard) echo '{"ok":true,"port":18789}' ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 0 ]] || fail "an orphaned gateway answering with no config does not end the wizard" "rc=$rc"
|
||||
grep -q '^finished$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was cut short"
|
||||
! grep -q '^terminated$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was signalled"
|
||||
pass "an orphaned gateway answering with no config does not end the wizard"
|
||||
|
||||
# And after setup, an orphan still bound to the port is not success even
|
||||
# while the unit reports active: Type=simple counts it active from the fork,
|
||||
# before it has discovered the port is taken. The port's owner has to be the
|
||||
# unit's own main process. Here it never is, so the deadline ends the wait as
|
||||
# a failure rather than opening the app on the orphan.
|
||||
: >"$TEST_LOG"
|
||||
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
|
||||
echo 999 >"$listener_file"
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
case $1 in
|
||||
onboard) mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
|
||||
dashboard) echo '{"ok":true,"port":18789}' ;;
|
||||
esac
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
[[ $rc == 1 ]] || fail "an orphan holding the port is not mistaken for the active unit" "rc=$rc"
|
||||
grep -q '^terminated$' "$TEST_LOG" || fail "an orphan holding the port is not mistaken for the active unit" "wizard left running"
|
||||
grep -q '^systemctl:--user show -p MainPID --value openclaw-gateway.service$' "$TEST_LOG" ||
|
||||
fail "an orphan holding the port is not mistaken for the active unit" "unit ownership never checked"
|
||||
pass "an orphan holding the port is not mistaken for the active unit"
|
||||
rm -f "$listener_file"
|
||||
@@ -93,3 +93,161 @@ ollama_row=$(grep '^ "remove.ai.ollama":' "$ROOT/default/omarchy/omarchy-menu.j
|
||||
[[ $ollama_row == *'"when":"omarchy-pkg-present ollama"'* ]] ||
|
||||
fail "Ollama removal is offered only where the package is installed" "$ollama_row"
|
||||
pass "Ollama removal is offered only where the package is installed"
|
||||
|
||||
# OpenClaw's gateway unit and web app launcher are the app's own; the agent
|
||||
# state in ~/.openclaw is the user's. systemctl and openclaw are stubbed so the
|
||||
# sandbox never reaches the real user manager or a real gateway.
|
||||
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/systemctl"
|
||||
|
||||
# Default openclaw stub: the packaged CLI predates `gateway uninstall`, so the
|
||||
# remover has to fall back to its manual systemd path.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
exit 1
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
|
||||
fresh_openclaw_home() {
|
||||
fresh_home
|
||||
mkdir -p "$HOME/.config/systemd/user/default.target.wants" "$HOME/.openclaw" \
|
||||
"$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-node.service" \
|
||||
"$HOME/.openclaw/openclaw.json" \
|
||||
"$HOME/.local/share/applications/OpenClaw.desktop" \
|
||||
"$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
|
||||
ln -s ../openclaw-gateway.service \
|
||||
"$HOME/.config/systemd/user/default.target.wants/openclaw-gateway.service"
|
||||
ln -s ../openclaw-node.service \
|
||||
"$HOME/.config/systemd/user/default.target.wants/openclaw-node.service"
|
||||
}
|
||||
|
||||
# gum would ask about ~/.openclaw; the tests never run on a terminal, so it
|
||||
# must not even be reached. Logging it proves that.
|
||||
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'gum:%s\n' "$*" >>"$TEST_LOG"
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/gum"
|
||||
|
||||
fresh_openclaw_home
|
||||
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
|
||||
|
||||
for gone in .config/systemd/user/openclaw-gateway.service \
|
||||
.config/systemd/user/openclaw-gateway.service.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 \
|
||||
.local/share/applications/OpenClaw.desktop \
|
||||
.local/share/icons/hicolor/256x256/apps/openclaw.png; do
|
||||
[[ ! -e $HOME/$gone && ! -L $HOME/$gone ]] || fail "OpenClaw removal deletes the service and launcher it installed" "$gone"
|
||||
done
|
||||
pass "OpenClaw removal deletes the service and launcher it installed"
|
||||
|
||||
grep -q '^systemctl:--user disable --now openclaw-gateway.service$' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal stops the gateway service"
|
||||
grep -q '^systemctl:--user disable --now openclaw-node.service$' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal stops the gateway service" "node host service left running"
|
||||
for unit in openclaw-gateway.service openclaw-node.service; do
|
||||
grep -q "^systemctl:--user reset-failed $unit\$" "$TEST_LOG" ||
|
||||
fail "OpenClaw removal stops the gateway service" "$unit left in systemd's failed list"
|
||||
done
|
||||
pass "OpenClaw removal stops the gateway service"
|
||||
|
||||
# Without a terminal there is nobody to ask, so the state stays and gum is
|
||||
# never invoked (a gum that answered "yes" on its own would be a data loss).
|
||||
[[ -f $HOME/.openclaw/openclaw.json ]] || fail "OpenClaw removal keeps the user's agent state"
|
||||
! grep -q '^gum:' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal keeps the user's agent state" "asked about ~/.openclaw without a terminal"
|
||||
pass "OpenClaw removal keeps the user's agent state"
|
||||
|
||||
# A CLI that knows `gateway uninstall` owns the teardown; the manual systemd
|
||||
# fallback must not run.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
rm -f "$HOME/.config/systemd/user/openclaw-$1.service" \
|
||||
"$HOME/.config/systemd/user/default.target.wants/openclaw-$1.service"
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
|
||||
: >"$TEST_LOG"
|
||||
fresh_openclaw_home
|
||||
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
|
||||
|
||||
grep -q '^openclaw:gateway uninstall$' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal prefers upstream's own gateway teardown"
|
||||
grep -q '^openclaw:node uninstall$' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal prefers upstream's own gateway teardown" "node host not handed to upstream"
|
||||
! grep -q '^systemctl:--user disable' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal prefers upstream's own gateway teardown" "manual disable ran too"
|
||||
pass "OpenClaw removal prefers upstream's own gateway teardown"
|
||||
|
||||
# Without the unit file there is nothing of ours registered, so systemd stays untouched.
|
||||
systemctl_calls_before=$(grep -c '^systemctl:' "$TEST_LOG" || true)
|
||||
fresh_home
|
||||
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
|
||||
systemctl_calls_after=$(grep -c '^systemctl:' "$TEST_LOG" || true)
|
||||
|
||||
[[ $systemctl_calls_before == "$systemctl_calls_after" ]] ||
|
||||
fail "OpenClaw removal leaves systemd alone when onboarding never ran"
|
||||
pass "OpenClaw removal leaves systemd alone when onboarding never ran"
|
||||
|
||||
# A gateway that will not stop aborts the removal before the package drop:
|
||||
# pacman would otherwise strand the live process on deleted code.
|
||||
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
|
||||
exit 1
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/openclaw"
|
||||
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
|
||||
[[ $2 == disable ]] && exit 1
|
||||
[[ $2 == is-active ]] && exit 0
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/systemctl"
|
||||
|
||||
drop_calls_before=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
|
||||
fresh_openclaw_home
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
|
||||
drop_calls_after=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
|
||||
|
||||
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when the gateway cannot be stopped"
|
||||
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
|
||||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "unit file deleted"
|
||||
[[ $drop_calls_before == "$drop_calls_after" ]] ||
|
||||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "package dropped anyway"
|
||||
pass "OpenClaw removal aborts when the gateway cannot be stopped"
|
||||
|
||||
# An unreachable user manager is not a stopped gateway: every probe failing
|
||||
# with no answer must still abort, not read as "confirmed inactive".
|
||||
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
|
||||
#!/bin/bash
|
||||
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
|
||||
echo "Failed to connect to user scope bus" >&2
|
||||
exit 1
|
||||
SCRIPT
|
||||
chmod +x "$tmp_dir/bin/systemctl"
|
||||
|
||||
: >"$TEST_LOG"
|
||||
fresh_openclaw_home
|
||||
rc=0
|
||||
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
|
||||
|
||||
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when systemd cannot be reached"
|
||||
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
|
||||
fail "OpenClaw removal aborts when systemd cannot be reached" "unit file deleted"
|
||||
! grep -q '^drop:openclaw$' "$TEST_LOG" ||
|
||||
fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway"
|
||||
pass "OpenClaw removal aborts when systemd cannot be reached"
|
||||
Reference in new issue
Block a user