The wizard writes its config while prompts remain, so the watcher starts probing a gateway that is not up yet; each probe hangs until its 10 second timeout, and OpenClaw's CLI, killed by it, takes the terminal it inherited on stdin out of raw mode on the way out. The wizard does not set raw mode again, so from then on its prompts echo keys instead of reading them: the first letter typed at the channel search landed and the next one was printed below the prompt. The probes now read from /dev/null. This is the change reverted earlier on a test that only ever saw probes that exited on their own, never one the timeout killed.
135 lines
5.8 KiB
Bash
Executable File
135 lines
5.8 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}
|
|
|
|
# Probes never get the terminal. The wizard writes its config while prompts
|
|
# remain, a probe of a gateway not yet up then hangs until the timeout, and
|
|
# OpenClaw's CLI takes a terminal on stdin out of raw mode as it is killed,
|
|
# which leaves the wizard's next prompt echoing keys instead of reading them.
|
|
gateway_answers() {
|
|
timeout 10 openclaw dashboard --json </dev/null 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
|
|
}
|
|
|
|
# A gateway already answering means OpenClaw is set up, and this run must not
|
|
# 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 </dev/null 2>/dev/null) || return 1
|
|
jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1
|
|
port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null)
|
|
[[ -n $port ]] || return 1
|
|
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"
|