#!/bin/sh
# Sets up this computer for Vux, the iPhone voice app, and pairs it with the phone:
#
#     curl -fsSL https://vux.dev/install.sh | sh -s https://vux.dev/pair/<code>
#
# With Hermes Agent on this computer, it sets up Hermes for Vux. Otherwise (or with --vux-agent after the URL) it
# installs Vux's own agent, vux-agent, as a background service in ~/.vux-agent. Either way the phone reaches
# the agent over Tailscale HTTPS, and Vux gets its URL and keys through the pairing URL. Safe to run again: it
# picks up where it stopped, and updates what it installed. Never prints a key.
#
# Exit codes: 0 done, 1 failed (the message says why), 3 waiting for the user (the message says what they need
# to do; run the same command again afterwards).
set -eu

NODE_VERSION=24.21.0
MRL_VERSION=5.5.0
PI_PORT=8650
BASE=${VUX_AGENT_BASE:-https://vux.dev}

say() { printf 'vux: %s\n' "$*"; }
fail() { printf 'vux: %s\n' "$*" >&2; exit 1; }
wait_for_user() { printf 'vux: %s\n' "$@"; exit 3; }

# Flags go anywhere, usually after the URL: `sh -s --vux-agent URL` would make sh read the flag as its own.
AGENT='' PAIR=''
for arg in "$@"; do
	case "$arg" in
		--vux-agent) AGENT=vux ;;
		--hermes) AGENT=hermes ;;
		-*) fail "unknown option $arg (the options are --vux-agent and --hermes)" ;;
		*) [ -z "$PAIR" ] || fail "more than one pairing URL"; PAIR=$arg ;;
	esac
done
if [ -n "$PAIR" ] && ! printf '%s' "$PAIR" | grep -Eq '^https://vux\.dev/pair/[0-9a-hjkmnp-tv-z]{10}$'; then
	fail "the pairing URL looks wrong: copy the command again from Vux (it ends in https://vux.dev/pair/ + 10 characters)"
fi
if [ -z "$AGENT" ]; then
	if command -v hermes >/dev/null; then AGENT=hermes; else AGENT=vux; fi
fi
if [ "$AGENT" = hermes ]; then
	command -v hermes >/dev/null || fail "--hermes: there's no hermes command here"
	command -v python3 >/dev/null || fail "python3 is missing: Hermes setup needs it"
	say "setting up Hermes for Vux (to install Vux's own agent instead, run this again with --vux-agent after the URL)"
fi

case "$(uname -s)" in
	Linux) OS=linux ;;
	Darwin) OS=darwin ;;
	*) fail "Vux's setup runs on Linux or macOS, not $(uname -s)" ;;
esac
case "$(uname -m)" in
	x86_64 | amd64) ARCH=x64; MRL_ARCH=amd64 ;;
	aarch64 | arm64) ARCH=arm64; MRL_ARCH=arm64 ;;
	*) fail "unsupported processor: $(uname -m)" ;;
esac
for tool in curl tar; do
	command -v "$tool" >/dev/null || fail "$tool is missing: install it and run this again"
done
if [ "$AGENT" = vux ] && [ "$OS" = linux ]; then
	command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1 ||
		fail "systemd's user services aren't available here (systemctl --user): Vux's agent needs them to keep running"
fi

sha256() {
	if command -v sha256sum >/dev/null; then sha256sum "$1"; else shasum -a 256 "$1"; fi | cut -d' ' -f1
}

# download URL SHA256 FILE
download() {
	curl -fsSL "$1" -o "$3" || fail "couldn't download $1"
	[ -z "$2" ] || [ "$(sha256 "$3")" = "$2" ] || fail "$1 doesn't match its checksum"
}

# curl config for a bearer key, so no key is ever on a command line: bearer FILE KEY
bearer() { printf 'header = "Authorization: Bearer %s"\n' "$2" >"$1"; }
status_of() { curl -s -K "$1" -o /dev/null -w '%{http_code}' "$2"; }

umask 077
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

if [ "$AGENT" = hermes ]; then
	HERMES_ENV=$(hermes config env-path)
	HERMES_HOME=$(dirname "$(hermes config path)")
	STATE="$HOME/.hermes/vux"
else
	STATE="$HOME/.vux-agent"
fi
mkdir -p "$STATE"

# Tailscale: the phone reaches the agent at this computer's Tailscale HTTPS name.
TAILSCALE=$(command -v tailscale || true)
[ -n "$TAILSCALE" ] || [ ! -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ] ||
	TAILSCALE=/Applications/Tailscale.app/Contents/MacOS/Tailscale
[ -n "$TAILSCALE" ] && "$TAILSCALE" status >/dev/null 2>&1 ||
	wait_for_user "Tailscale isn't installed or signed in on this computer. Install Tailscale here and on the iPhone" \
		"(https://tailscale.com/download), sign in to the same account on both, then run this again."

# ModelRelay key: speech for Vux, and vux-agent's models unless set otherwise. Hermes keeps its own model settings.
relay_key() {
	key_file="$STATE/relay-key"
	mrl=$(command -v mrl || echo "$STATE/bin/mrl")
	login_link() { sed -n 's/.*"verification_uri_complete":"\([^"]*\)".*/\1/p' "$STATE/login.json" 2>/dev/null | head -1; }
	login_running() { [ -f "$STATE/login.pid" ] && kill -0 "$(cat "$STATE/login.pid")" 2>/dev/null; }
	new_key() { "$mrl" keys create --name vux --print >"$TMP/key" 2>/dev/null && mv "$TMP/key" "$key_file"; }

	if [ ! -s "$key_file" ]; then
		existing=${MODELRELAY_API_KEY:-}
		[ -n "$existing" ] || [ "$AGENT" != hermes ] ||
			existing=$(sed -n 's/^MODELRELAY_API_KEY=["'\'']*\([^"'\'']*\).*/\1/p' "$HERMES_ENV" | head -1)
		if [ -n "$existing" ]; then
			printf '%s' "$existing" | grep -Eq '^mr_sk_[A-Za-z0-9_-]{20,200}$' || fail "MODELRELAY_API_KEY isn't a ModelRelay key (mr_sk_…)"
			printf '%s\n' "$existing" >"$key_file"
		fi
	fi
	if [ ! -s "$key_file" ]; then
		if [ ! -x "$mrl" ]; then
			case "$OS-$MRL_ARCH" in
				darwin-arm64) sum=57fef1ab98318087d3bf2b6213a11b6984d8b2da68eca25a506b5a5e4e29f79a ;;
				darwin-amd64) sum=e52e3d51dd5f03920a8968aef9bcb3ea09850e253cb8dee564b5ae5b9a103d38 ;;
				linux-arm64) sum=2d66e1cf2b47fc5d62b80a6c3f0ca3c98676ab11c60c652932589d1065ead270 ;;
				linux-amd64) sum=db2b9ad5010f22bdfa1c16d39f56d9c706dc967a79b4a60143858afd1e3c0660 ;;
			esac
			download "https://releases.modelrelay.ai/mrl/$MRL_VERSION/mrl-$MRL_VERSION-$OS-$MRL_ARCH.tar.gz" "$sum" "$TMP/mrl.tgz"
			mkdir -p "$STATE/bin" && tar -xzf "$TMP/mrl.tgz" -C "$STATE/bin" mrl
		fi
		if ! login_running && ! new_key; then
			# Not signed in to ModelRelay: sign in by approving a link on the phone.
			rm -f "$STATE/login.json"
			nohup "$mrl" auth login --device --json >"$STATE/login.json" 2>&1 </dev/null &
			echo $! >"$STATE/login.pid"
			for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do [ -n "$(login_link)" ] && break; sleep 1; done
			[ -n "$(login_link)" ] || fail "ModelRelay sign-in didn't start: $(head -c 300 "$STATE/login.json")"
		fi
		if login_running; then
			# A terminal waits here. An agent's shell may not show the link until this exits, and may stop a
			# command after 2 minutes, so it gets the link back soon.
			if [ -t 1 ]; then tries=600; else tries=45; fi
			say "ModelRelay sign-in: open this link on the iPhone, sign in or sign up, and tap Approve:"
			say "$(login_link)"
			while login_running && [ "$tries" -gt 0 ]; do sleep 1; tries=$((tries - 1)); done
			login_running && wait_for_user "Still waiting for the ModelRelay approval: open the link above on the iPhone," \
				"then run this again."
			rm -f "$STATE/login.pid"
			new_key || fail "ModelRelay sign-in didn't finish ($(tail -c 200 "$STATE/login.json")): run this again for a new link"
			rm -f "$STATE/login.json"
		fi
	fi
	bearer "$TMP/relay.curl" "$(cat "$key_file")"
	status=$(curl -s -K "$TMP/relay.curl" -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
		-d '{"model":"gemini-3.8-flash-tts","input":"ok","voice":"Kore","response_format":"wav"}' \
		https://api.modelrelay.ai/v1/audio/speech)
	case "$status" in
		200) ;;
		401) rm -f "$key_file"; fail "ModelRelay refused the key: run this again to sign in and make a new one" ;;
		402) fail "the ModelRelay account has no credit: add some at https://modelrelay.ai, then run this again" ;;
		*) fail "ModelRelay answered $status: try again in a minute" ;;
	esac
}

# Serves http://127.0.0.1:PORT over Tailscale HTTPS, on the first of Tailscale's HTTPS ports that isn't serving
# something else, and sets URL.
serve_https() {
	local_port=$1
	"$TAILSCALE" status --json --peers=false >"$TMP/tailscale.json"
	"$TAILSCALE" serve status --json >"$TMP/serve.json" 2>/dev/null || echo '{}' >"$TMP/serve.json"
	if [ "$AGENT" = hermes ]; then
		cp "$TMP/tailscale.json" "$STATE/tailscale.json"
		python3 -c '
import json, sys
host = json.load(open(sys.argv[1]))["Self"]["DNSName"].rstrip(".")
web = (json.loads(open(sys.argv[2]).read() or "{}") or {}).get("Web") or {}
def ours(port):
    handlers = (web.get(f"{host}:{port}") or {}).get("Handlers") or {}
    return all(h.get("Proxy") == f"http://127.0.0.1:{sys.argv[3]}" for h in handlers.values())
print(host, next((p for p in (443, 8443, 10000) if ours(p)), ""))
' "$TMP/tailscale.json" "$TMP/serve.json" "$local_port" >"$TMP/https"
	else
		"$NODE" -e '
const fs = require("node:fs");
const host = JSON.parse(fs.readFileSync(process.argv[1])).Self.DNSName.replace(/\.$/, "");
const web = JSON.parse(fs.readFileSync(process.argv[2], "utf8") || "{}")?.Web ?? {};
const ours = (port) => Object.values(web[`${host}:${port}`]?.Handlers ?? {})
	.every((h) => h.Proxy === `http://127.0.0.1:${process.argv[3]}`);
console.log(host, [443, 8443, 10000].find(ours) ?? "");
' "$TMP/tailscale.json" "$TMP/serve.json" "$local_port" >"$TMP/https"
	fi
	read -r host https_port <"$TMP/https" || true
	[ -n "$https_port" ] || fail "Tailscale's HTTPS ports 443, 8443 and 10000 all serve something else on this computer"
	if ! "$TAILSCALE" serve --bg --https="$https_port" "http://127.0.0.1:$local_port" >"$TMP/serve.out" 2>&1; then
		if grep -qi 'denied' "$TMP/serve.out"; then
			wait_for_user "Tailscale needs permission for $(id -un) to serve HTTPS. Run this on this computer:" \
				"    sudo tailscale set --operator=$(id -un)" "then run this again."
		fi
		link=$(grep -Eo 'https://login\.tailscale\.com/[^ ]+' "$TMP/serve.out" | head -1)
		[ -z "$link" ] || wait_for_user "Tailscale HTTPS isn't on for this tailnet. Open this link to turn it on:" "$link" \
			"then run this again."
		fail "tailscale serve failed: $(head -c 300 "$TMP/serve.out")"
	fi
	URL="https://$host"
	[ "$https_port" = 443 ] || URL="$URL:$https_port"
}

# Waits for URL to answer with the curl config's key (a new HTTPS name can take a minute for its certificate).
check_https() {
	tries=60
	until [ "$(status_of "$1" "$URL/v1/capabilities")" = 200 ]; do
		tries=$((tries - 1))
		[ "$tries" -gt 0 ] || fail "the agent runs, but $URL doesn't answer: check tailscale serve status"
		sleep 2
	done
	say "the agent answers at $URL"
}

# Hands URL and keys to Vux: pair KEY_FILE
pair() {
	if [ "$AGENT" = hermes ]; then
		python3 -c 'import json, sys; print(json.dumps({"url": sys.argv[1], "key": open(sys.argv[2]).read().strip(),
			"relay": open(sys.argv[3]).read().strip()}))' "$URL" "$1" "$STATE/relay-key" >"$TMP/pair.json"
	else
		"$NODE" -e 'const fs = require("node:fs"); process.stdout.write(JSON.stringify({ url: process.argv[1],
			key: fs.readFileSync(process.argv[2], "utf8").trim(), relay: fs.readFileSync(process.argv[3], "utf8").trim() }))' \
			"$URL" "$1" "$STATE/relay-key" >"$TMP/pair.json"
	fi
	status=$(curl -s -X PUT -H 'Content-Type: application/json' --data-binary "@$TMP/pair.json" -o "$TMP/pair.out" \
		-w '%{http_code}' "$PAIR")
	case "$status" in
		204) say "Done. Go back to Vux." ;;
		404) fail "Vux's pairing code is wrong or expired (codes last 15 minutes): copy the command again from Vux and run it" ;;
		409) fail "this pairing code was already used: copy the command again from Vux and run it" ;;
		*) fail "vux.dev answered $status: $(head -c 300 "$TMP/pair.out")" ;;
	esac
}

# ---- Hermes -------------------------------------------------------------------------------------------------
if [ "$AGENT" = hermes ]; then
	PORT=$(sed -n 's/^API_SERVER_PORT=["'\'']*\([0-9]*\).*/\1/p' "$HERMES_ENV" | head -1)
	PORT=${PORT:-8642}

	# The API server Vux talks to.
	if ! grep -q '^API_SERVER_KEY=' "$HERMES_ENV"; then
		say "turning on Hermes's API server"
		hermes config set API_SERVER_ENABLED true >/dev/null
		hermes config set API_SERVER_KEY "$(python3 -c 'import secrets; print(secrets.token_hex(32))')" >/dev/null
	fi

	# vux-auth: a token per phone that reaches only what Vux uses, and push notifications.
	plugin="$HERMES_HOME/plugins/vux-auth"
	mkdir -p "$plugin"
	changed=
	for file in __init__.py plugin.yaml desk_question.json; do
		download "$BASE/hermes/vux-auth/$file" "" "$TMP/$file"
		cmp -s "$TMP/$file" "$plugin/$file" || { cp "$TMP/$file" "$plugin/$file"; changed=1; }
	done
	hermes plugins enable vux-auth >/dev/null 2>&1 || fail "hermes couldn't enable the vux-auth plugin: run hermes plugins enable vux-auth to see why"

	# The setup helper reads the gateway key and mints this phone's token (into $STATE/token-<code>).
	download "$BASE/vux-setup.py" "" "$STATE/setup.py"
	if [ -n "$PAIR" ]; then python3 "$STATE/setup.py" "$HERMES_ENV" "$PAIR" >/dev/null
	else python3 "$STATE/setup.py" "$HERMES_ENV" >/dev/null; fi
	# Checks use this phone's token (proving vux-auth is loaded), or the gateway key without a pairing URL.
	probe="$TMP/hermes.curl"
	if [ -n "$PAIR" ]; then bearer "$probe" "$(cat "$STATE/token-${PAIR##*/}")"
	else bearer "$probe" "$(sed -n 's/^API_SERVER_KEY=["'\'']*\([^"'\'']*\).*/\1/p' "$HERMES_ENV" | head -1)"; fi

	# Restart the gateway when vux-auth changed, or the API server (with this phone's token) doesn't answer yet.
	if [ -n "$changed" ] || [ "$(status_of "$probe" "http://127.0.0.1:$PORT/v1/capabilities")" != 200 ]; then
		# `hermes gateway restart` stops a gateway started by hand (`hermes gateway run`) without starting it
		# again, so only a service is restarted from here.
		hermes gateway status >"$TMP/status.out" 2>&1 || true
		if grep -qi 'running manually' "$TMP/status.out"; then
			wait_for_user "The Hermes gateway has to restart to load Vux's settings. It was started by hand" \
				"(hermes gateway run): stop it and start it again, or send /restart in a Hermes chat, then run this again."
		elif grep -qi 'not running' "$TMP/status.out"; then
			wait_for_user "The Hermes gateway isn't running. Start it (hermes gateway start, or hermes gateway run)," \
				"then run this again."
		fi
		say "restarting the Hermes gateway"
		if ! hermes gateway restart >"$TMP/restart.out" 2>&1; then
			wait_for_user "The Hermes gateway has to restart, and it can't from here ($(head -c 200 "$TMP/restart.out"))." \
				"Send /restart in your Hermes chat, wait a few seconds, then run this again."
		fi
		tries=60
		until [ "$(status_of "$probe" "http://127.0.0.1:$PORT/v1/capabilities")" = 200 ]; do
			tries=$((tries - 1))
			[ "$tries" -gt 0 ] || fail "Hermes's API server doesn't answer on port $PORT after the restart: see hermes gateway status"
			sleep 1
		done
	fi
	say "Hermes's API server is running"

	relay_key
	serve_https "$PORT"
	check_https "$probe"

	# Voice skills: Hermes skills that answer in a few spoken sentences.
	download "$BASE/voice-skills.json" "" "$TMP/skills.json"
	python3 -c '
import json, sys
for s in json.load(open(sys.argv[1]))["skills"]:
    if s.get("reviewed"): print(s["name"], s["install"], s.get("category") or "-")
' "$TMP/skills.json" | while read -r name install category; do
		if [ -n "$(find "$HERMES_HOME/skills" -maxdepth 3 -type d -name "$name" 2>/dev/null)" ]; then
			hermes skills update "$name" >/dev/null 2>&1 || say "note: couldn't update the voice skill $name"
		elif [ "$category" = - ]; then
			hermes skills install "$install" --yes >/dev/null 2>&1 || say "note: couldn't install the voice skill $name"
		else
			hermes skills install "$install" --category "$category" --yes >/dev/null 2>&1 ||
				say "note: couldn't install the voice skill $name"
		fi
	done

	# Photos: Hermes on ModelRelay without the modelrelay provider plugin describes photos instead of showing them.
	provider=$(hermes config get model.provider 2>/dev/null || true)
	base_url=$(hermes config get model.base_url 2>/dev/null || true)
	if [ "$provider" != modelrelay ] && { [ "$provider" = custom:modelrelay ] || [ "$base_url" = https://api.modelrelay.ai/v1 ]; }; then
		say "note: for Hermes to see photos from Vux, ask it to follow https://vux.dev/hermes-photos.md"
	fi

	if [ -z "$PAIR" ]; then
		say "done. To connect a phone, run this again with the command from Vux."
		exit 0
	fi
	code=${PAIR##*/}
	pair "$STATE/token-$code"
	rm -f "$STATE/token-$code" "$STATE/pair.json" "$STATE/pair.curl"
	exit 0
fi

# ---- vux-agent ---------------------------------------------------------------------------------------------
DIR=$STATE
PORT=$PI_PORT
LABEL=dev.vux.agent

# Node, private to the agent.
NODE="$DIR/node/bin/node"
if [ "$("$NODE" --version 2>/dev/null || true)" != "v$NODE_VERSION" ]; then
	say "installing Node $NODE_VERSION"
	case "$OS-$ARCH" in
		darwin-arm64) sum=bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057 ;;
		darwin-x64) sum=1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097 ;;
		linux-arm64) sum=724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5 ;;
		linux-x64) sum=6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff ;;
	esac
	download "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-$OS-$ARCH.tar.gz" "$sum" "$TMP/node.tgz"
	rm -rf "$DIR/node" && mkdir -p "$DIR/node"
	tar -xzf "$TMP/node.tgz" -C "$DIR/node" --strip-components=1
fi
PATH="$DIR/node/bin:$PATH"

# The agent.
say "installing Vux's agent"
download "$BASE/agent/vux-agent.tgz" "" "$TMP/agent.tgz"
rm -rf "$DIR/app.new" && mkdir -p "$DIR/app.new"
tar -xzf "$TMP/agent.tgz" -C "$DIR/app.new"
(cd "$DIR/app.new" && npm ci --omit=dev --no-audit --no-fund --no-update-notifier --loglevel=error >/dev/null) ||
	fail "npm couldn't install the agent's packages"
# On macOS, launchd runs the agent from Vux Agent.app, signed with Vux's Developer ID: macOS's privacy prompts and
# Privacy & Security name the app a file access comes from, which would otherwise be node. Grants stay across updates.
PROMPTS=''
if [ "$OS" = darwin ]; then
	download "$BASE/agent/VuxAgent.app.tgz" "" "$TMP/launcher.tgz"
	rm -rf "$DIR/launcher.new" && mkdir -p "$DIR/launcher.new"
	tar -xzf "$TMP/launcher.tgz" -C "$DIR/launcher.new"
	[ -d "$DIR/Vux Agent.app" ] || PROMPTS=1
fi

relay_key

# Configuration and service.
[ -s "$DIR/agent-key" ] || "$NODE" -e 'console.log(require("node:crypto").randomBytes(32).toString("hex"))' >"$DIR/agent-key"
quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
# Models other than ModelRelay's default, set by hand (vux-agent/README.md): kept across updates.
models=$(grep -E '^(MODEL_API|MODEL_BASE_URL|MODEL_API_KEY|MODELS|ANTHROPIC_API_KEY)=' "$DIR/env" 2>/dev/null || true)
{
	echo "VUX_AGENT_KEY=$(cat "$DIR/agent-key")"
	echo "MODELRELAY_API_KEY=$(cat "$DIR/relay-key")"
	echo "WORKSPACE=$(quote "${VUX_AGENT_WORKSPACE:-$HOME}")"
	echo "DATABASE=$(quote "$DIR/agent.sqlite")"
	echo "APPROVALS=$(quote "$DIR/approvals.json")"
	echo "SKILLS=$(quote "$DIR/skills")"
	echo "PHONES=$(quote "$DIR/phones.json")"
	echo "MEMORY=$(quote "$DIR/memory")"
	echo "PORT=$PORT"
	# The agent's shell commands see the same tools as this shell.
	echo "PATH=$(quote "$PATH")"
	[ -z "$models" ] || printf '%s\n' "$models"
} >"$DIR/env"
cat >"$DIR/run" <<'EOF'
#!/bin/sh
umask 077
set -a
. "$HOME/.vux-agent/env"
set +a
cd "$HOME/.vux-agent/app" &&
	exec "$HOME/.vux-agent/node/bin/node" --experimental-strip-types --disable-warning=ExperimentalWarning src/main.ts
EOF
chmod 700 "$DIR/run"
# The paired phones: ~/.vux-agent/phones list, ~/.vux-agent/phones revoke NAME.
cat >"$DIR/phones" <<'EOF'
#!/bin/sh
umask 077
set -a
. "$HOME/.vux-agent/env"
set +a
cd "$HOME/.vux-agent/app" &&
	exec "$HOME/.vux-agent/node/bin/node" --experimental-strip-types --disable-warning=ExperimentalWarning src/phones-cli.ts "$@"
EOF
chmod 700 "$DIR/phones"

if [ "$OS" = linux ]; then systemctl --user stop vux-agent 2>/dev/null || true
else launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true; fi
# Memory kept inside the app folder (an agent run without MEMORY) moves out before the folder is replaced.
if [ -d "$DIR/app/memory" ] && [ ! -e "$DIR/memory" ]; then mv "$DIR/app/memory" "$DIR/memory"; fi
rm -rf "$DIR/app" && mv "$DIR/app.new" "$DIR/app"
if [ "$OS" = darwin ]; then
	rm -rf "$DIR/Vux Agent.app" && mv "$DIR/launcher.new/Vux Agent.app" "$DIR/Vux Agent.app" && rmdir "$DIR/launcher.new"
fi
if [ "$OS" = linux ]; then
	mkdir -p "$HOME/.config/systemd/user"
	cat >"$HOME/.config/systemd/user/vux-agent.service" <<EOF
[Unit]
Description=Vux's agent (https://vux.dev/setup.md)
After=network-online.target

[Service]
ExecStart=$DIR/run
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
EOF
	systemctl --user daemon-reload
	systemctl --user enable --now vux-agent >/dev/null 2>&1 || fail "systemd couldn't start vux-agent: see journalctl --user -u vux-agent"
	# Keep it running while the user is logged out.
	if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null)" != yes ] &&
		! loginctl enable-linger "$(id -un)" 2>/dev/null; then
		say "note: the agent stops when $(id -un) logs out. To keep it running: sudo loginctl enable-linger $(id -un)"
	fi
else
	mkdir -p "$HOME/Library/LaunchAgents"
	PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
	cat >"$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key><string>$LABEL</string>
	<key>ProgramArguments</key><array><string>$DIR/Vux Agent.app/Contents/MacOS/vux-agent</string></array>
	<key>RunAtLoad</key><true/>
	<key>KeepAlive</key><true/>
	<key>StandardOutPath</key><string>$DIR/agent.log</string>
	<key>StandardErrorPath</key><string>$DIR/agent.log</string>
</dict>
</plist>
EOF
	# bootout returns before the old one is gone, and bootstrap fails until it is.
	tries=10
	until launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null; do
		tries=$((tries - 1))
		[ "$tries" -gt 0 ] || fail "launchd couldn't start $LABEL: see $DIR/agent.log"
		sleep 1
	done
fi
bearer "$TMP/agent.curl" "$(cat "$DIR/agent-key")"
tries=30
until [ "$(status_of "$TMP/agent.curl" "http://127.0.0.1:$PORT/v1/capabilities")" = 200 ]; do
	tries=$((tries - 1))
	[ "$tries" -gt 0 ] || fail "the agent didn't start: see $([ "$OS" = linux ] && echo 'journalctl --user -u vux-agent' || echo "$DIR/agent.log")"
	sleep 1
done
say "Vux's agent is running"
[ -z "$PROMPTS" ] ||
	say "note: the first time the agent opens Desktop, Documents, Downloads or Photos, macOS asks on this screen to allow Vux Agent"

# Voice skills: the catalog's reviewed ones, installed (or updated) through the agent.
download "$BASE/voice-skills.json" "" "$TMP/skills.json"
"$NODE" -e '
const fs = require("node:fs");
const [catalog, keyFile, port] = process.argv.slice(1);
const key = fs.readFileSync(keyFile, "utf8").trim();
(async () => {
	for (const skill of JSON.parse(fs.readFileSync(catalog, "utf8")).skills.filter((s) => s.reviewed)) {
		const response = await fetch(`http://127.0.0.1:${port}/v1/skills`, { method: "POST",
			headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
			body: JSON.stringify({ install: skill.install, category: skill.category }) });
		const { answer } = await response.json();
		if (answer !== "installed") console.log(`vux: note: voice skill ${skill.name}: ${answer}`);
	}
})();
' "$TMP/skills.json" "$DIR/agent-key" "$PORT"

serve_https "$PORT"
check_https "$TMP/agent.curl"
if [ -z "$PAIR" ]; then
	say "done. To connect a phone, run this again with the command from Vux."
	exit 0
fi
# This phone's own token (the phone never gets agent-key): minted once per pairing code, kept until it's sent.
code=${PAIR##*/}
token="$DIR/token-$code"
if [ ! -s "$token" ]; then
	"$DIR/phones" list | grep -q "^iPhone $code	" &&
		fail "this pairing code was already used: copy the command again from Vux and run it"
	"$DIR/phones" add "iPhone $code" >"$token.new" || fail "couldn't make a token for this phone"
	mv "$token.new" "$token"
fi
bearer "$TMP/phone.curl" "$(cat "$token")"
[ "$(status_of "$TMP/phone.curl" "$URL/v1/capabilities")" = 200 ] || fail "the agent doesn't take this phone's token"
pair "$token"
rm -f "$token"
