186 lines
6.6 KiB
TypeScript
186 lines
6.6 KiB
TypeScript
import type { Command } from "commander";
|
|
import { gatewayStatusCommand } from "../../commands/gateway-status.js";
|
|
import { formatHealthChannelLines, type HealthSummary } from "../../commands/health.js";
|
|
import { discoverGatewayBeacons } from "../../infra/bonjour-discovery.js";
|
|
import { WIDE_AREA_DISCOVERY_DOMAIN } from "../../infra/widearea-dns.js";
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
import { formatDocsLink } from "../../terminal/links.js";
|
|
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
|
import { withProgress } from "../progress.js";
|
|
import { callGatewayCli, gatewayCallOpts } from "./call.js";
|
|
import type { GatewayDiscoverOpts } from "./discover.js";
|
|
import {
|
|
dedupeBeacons,
|
|
parseDiscoverTimeoutMs,
|
|
pickBeaconHost,
|
|
pickGatewayPort,
|
|
renderBeaconLines,
|
|
} from "./discover.js";
|
|
import { addGatewayRunCommand } from "./run.js";
|
|
|
|
export function registerGatewayCli(program: Command) {
|
|
const gateway = addGatewayRunCommand(
|
|
program
|
|
.command("gateway")
|
|
.description("Run the WebSocket Gateway")
|
|
.addHelpText(
|
|
"after",
|
|
() =>
|
|
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/gateway", "docs.clawd.bot/cli/gateway")}\n`,
|
|
),
|
|
);
|
|
|
|
// Back-compat: legacy launchd plists used gateway-daemon; keep hidden alias.
|
|
addGatewayRunCommand(
|
|
program
|
|
.command("gateway-daemon", { hidden: true })
|
|
.description("Run the WebSocket Gateway as a long-lived daemon"),
|
|
);
|
|
|
|
gatewayCallOpts(
|
|
gateway
|
|
.command("call")
|
|
.description("Call a Gateway method")
|
|
.argument("<method>", "Method name (health/status/system-presence/cron.*)")
|
|
.option("--params <json>", "JSON object string for params", "{}")
|
|
.action(async (method, opts) => {
|
|
try {
|
|
const params = JSON.parse(String(opts.params ?? "{}"));
|
|
const result = await callGatewayCli(method, opts, params);
|
|
if (opts.json) {
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
const rich = isRich();
|
|
defaultRuntime.log(
|
|
`${colorize(rich, theme.heading, "Gateway call")}: ${colorize(rich, theme.muted, String(method))}`,
|
|
);
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
} catch (err) {
|
|
defaultRuntime.error(`Gateway call failed: ${String(err)}`);
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
|
|
gatewayCallOpts(
|
|
gateway
|
|
.command("health")
|
|
.description("Fetch Gateway health")
|
|
.action(async (opts) => {
|
|
try {
|
|
const result = await callGatewayCli("health", opts);
|
|
if (opts.json) {
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
const rich = isRich();
|
|
const obj =
|
|
result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
|
const durationMs = typeof obj.durationMs === "number" ? obj.durationMs : null;
|
|
defaultRuntime.log(colorize(rich, theme.heading, "Gateway Health"));
|
|
defaultRuntime.log(
|
|
`${colorize(rich, theme.success, "OK")}${durationMs != null ? ` (${durationMs}ms)` : ""}`,
|
|
);
|
|
if (obj.channels && typeof obj.channels === "object") {
|
|
for (const line of formatHealthChannelLines(obj as HealthSummary)) {
|
|
defaultRuntime.log(line);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
defaultRuntime.error(String(err));
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
|
|
gateway
|
|
.command("status")
|
|
.description("Show gateway reachability + discovery + health + status summary (local + remote)")
|
|
.option("--url <url>", "Explicit Gateway WebSocket URL (still probes localhost)")
|
|
.option("--ssh <target>", "SSH target for remote gateway tunnel (user@host or user@host:port)")
|
|
.option("--ssh-identity <path>", "SSH identity file path")
|
|
.option("--ssh-auto", "Try to derive an SSH target from Bonjour discovery", false)
|
|
.option("--token <token>", "Gateway token (applies to all probes)")
|
|
.option("--password <password>", "Gateway password (applies to all probes)")
|
|
.option("--timeout <ms>", "Overall probe budget in ms", "3000")
|
|
.option("--json", "Output JSON", false)
|
|
.action(async (opts) => {
|
|
try {
|
|
await gatewayStatusCommand(opts, defaultRuntime);
|
|
} catch (err) {
|
|
defaultRuntime.error(String(err));
|
|
defaultRuntime.exit(1);
|
|
}
|
|
});
|
|
|
|
gateway
|
|
.command("discover")
|
|
.description(
|
|
`Discover gateways via Bonjour (multicast local. + unicast ${WIDE_AREA_DISCOVERY_DOMAIN})`,
|
|
)
|
|
.option("--timeout <ms>", "Per-command timeout in ms", "2000")
|
|
.option("--json", "Output JSON", false)
|
|
.action(async (opts: GatewayDiscoverOpts) => {
|
|
try {
|
|
const timeoutMs = parseDiscoverTimeoutMs(opts.timeout, 2000);
|
|
const beacons = await withProgress(
|
|
{
|
|
label: "Scanning for gateways…",
|
|
indeterminate: true,
|
|
enabled: opts.json !== true,
|
|
delayMs: 0,
|
|
},
|
|
async () => await discoverGatewayBeacons({ timeoutMs }),
|
|
);
|
|
|
|
const deduped = dedupeBeacons(beacons).sort((a, b) =>
|
|
String(a.displayName || a.instanceName).localeCompare(
|
|
String(b.displayName || b.instanceName),
|
|
),
|
|
);
|
|
|
|
if (opts.json) {
|
|
const enriched = deduped.map((b) => {
|
|
const host = pickBeaconHost(b);
|
|
const port = pickGatewayPort(b);
|
|
return { ...b, wsUrl: host ? `ws://${host}:${port}` : null };
|
|
});
|
|
defaultRuntime.log(
|
|
JSON.stringify(
|
|
{
|
|
timeoutMs,
|
|
domains: ["local.", WIDE_AREA_DISCOVERY_DOMAIN],
|
|
count: enriched.length,
|
|
beacons: enriched,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const rich = isRich();
|
|
defaultRuntime.log(colorize(rich, theme.heading, "Gateway Discovery"));
|
|
defaultRuntime.log(
|
|
colorize(
|
|
rich,
|
|
theme.muted,
|
|
`Found ${deduped.length} gateway(s) · domains: local., ${WIDE_AREA_DISCOVERY_DOMAIN}`,
|
|
),
|
|
);
|
|
if (deduped.length === 0) return;
|
|
|
|
for (const beacon of deduped) {
|
|
for (const line of renderBeaconLines(beacon, rich)) {
|
|
defaultRuntime.log(line);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
defaultRuntime.error(`gateway discover failed: ${String(err)}`);
|
|
defaultRuntime.exit(1);
|
|
}
|
|
});
|
|
}
|