Files
clawdbot/src/commands/onboard-remote.ts
2026-01-07 01:19:31 +01:00

156 lines
4.5 KiB
TypeScript

import type { ClawdbotConfig } from "../config/config.js";
import type { GatewayBonjourBeacon } from "../infra/bonjour-discovery.js";
import { discoverGatewayBeacons } from "../infra/bonjour-discovery.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { detectBinary } from "./onboard-helpers.js";
const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
function pickHost(beacon: GatewayBonjourBeacon): string | undefined {
return beacon.tailnetDns || beacon.lanHost || beacon.host;
}
function buildLabel(beacon: GatewayBonjourBeacon): string {
const host = pickHost(beacon);
const port = beacon.gatewayPort ?? beacon.port ?? 18789;
const title = beacon.displayName ?? beacon.instanceName;
const hint = host ? `${host}:${port}` : "host unknown";
return `${title} (${hint})`;
}
function ensureWsUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) return DEFAULT_GATEWAY_URL;
return trimmed;
}
export async function promptRemoteGatewayConfig(
cfg: ClawdbotConfig,
prompter: WizardPrompter,
): Promise<ClawdbotConfig> {
let selectedBeacon: GatewayBonjourBeacon | null = null;
let suggestedUrl = cfg.gateway?.remote?.url ?? DEFAULT_GATEWAY_URL;
const hasBonjourTool =
(await detectBinary("dns-sd")) || (await detectBinary("avahi-browse"));
const wantsDiscover = hasBonjourTool
? await prompter.confirm({
message: "Discover gateway on LAN (Bonjour)?",
initialValue: true,
})
: false;
if (!hasBonjourTool) {
await prompter.note(
[
"Bonjour discovery requires dns-sd (macOS) or avahi-browse (Linux).",
"Docs: https://docs.clawd.bot/gateway/discovery",
].join("\n"),
"Discovery",
);
}
if (wantsDiscover) {
const spin = prompter.progress("Searching for gateways…");
const beacons = await discoverGatewayBeacons({ timeoutMs: 2000 });
spin.stop(
beacons.length > 0
? `Found ${beacons.length} gateway(s)`
: "No gateways found",
);
if (beacons.length > 0) {
const selection = await prompter.select({
message: "Select gateway",
options: [
...beacons.map((beacon, index) => ({
value: String(index),
label: buildLabel(beacon),
})),
{ value: "manual", label: "Enter URL manually" },
],
});
if (selection !== "manual") {
const idx = Number.parseInt(String(selection), 10);
selectedBeacon = Number.isFinite(idx) ? (beacons[idx] ?? null) : null;
}
}
}
if (selectedBeacon) {
const host = pickHost(selectedBeacon);
const port = selectedBeacon.gatewayPort ?? 18789;
if (host) {
const mode = await prompter.select({
message: "Connection method",
options: [
{
value: "direct",
label: `Direct gateway WS (${host}:${port})`,
},
{ value: "ssh", label: "SSH tunnel (loopback)" },
],
});
if (mode === "direct") {
suggestedUrl = `ws://${host}:${port}`;
} else {
suggestedUrl = DEFAULT_GATEWAY_URL;
await prompter.note(
[
"Start a tunnel before using the CLI:",
`ssh -N -L 18789:127.0.0.1:18789 <user>@${host}${
selectedBeacon.sshPort ? ` -p ${selectedBeacon.sshPort}` : ""
}`,
"Docs: https://docs.clawd.bot/gateway/remote",
].join("\n"),
"SSH tunnel",
);
}
}
}
const urlInput = await prompter.text({
message: "Gateway WebSocket URL",
initialValue: suggestedUrl,
validate: (value) =>
String(value).trim().startsWith("ws://") ||
String(value).trim().startsWith("wss://")
? undefined
: "URL must start with ws:// or wss://",
});
const url = ensureWsUrl(String(urlInput));
const authChoice = (await prompter.select({
message: "Gateway auth",
options: [
{ value: "token", label: "Token (recommended)" },
{ value: "off", label: "No auth" },
],
})) as "token" | "off";
let token = cfg.gateway?.remote?.token ?? "";
if (authChoice === "token") {
token = String(
await prompter.text({
message: "Gateway token",
initialValue: token,
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
} else {
token = "";
}
return {
...cfg,
gateway: {
...cfg.gateway,
mode: "remote",
remote: {
url,
token: token || undefined,
},
},
};
}