Add OpenClaw as a desktop app and a coding agent
(cherry picked from commit eb56446c42)
(cherry picked from commit bde2584b5596412142d0a2039d44dd6a25949517)
131 lines
5.5 KiB
Bash
Executable File
131 lines
5.5 KiB
Bash
Executable File
#!/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"
|