Spaces:
Configuration error
Configuration error
| 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 type { CostUsageSummary } from "../../infra/session-cost-usage.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 { formatTokenCount, formatUsd } from "../../utils/usage-format.js"; | |
| import { withProgress } from "../progress.js"; | |
| import { runCommandWithRuntime } from "../cli-utils.js"; | |
| import { | |
| runDaemonInstall, | |
| runDaemonRestart, | |
| runDaemonStart, | |
| runDaemonStatus, | |
| runDaemonStop, | |
| runDaemonUninstall, | |
| } from "../daemon-cli.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"; | |
| function styleHealthChannelLine(line: string, rich: boolean): string { | |
| if (!rich) return line; | |
| const colon = line.indexOf(":"); | |
| if (colon === -1) return line; | |
| const label = line.slice(0, colon + 1); | |
| const detail = line.slice(colon + 1).trimStart(); | |
| const normalized = detail.toLowerCase(); | |
| const applyPrefix = (prefix: string, color: (value: string) => string) => | |
| `${label} ${color(detail.slice(0, prefix.length))}${detail.slice(prefix.length)}`; | |
| if (normalized.startsWith("failed")) return applyPrefix("failed", theme.error); | |
| if (normalized.startsWith("ok")) return applyPrefix("ok", theme.success); | |
| if (normalized.startsWith("linked")) return applyPrefix("linked", theme.success); | |
| if (normalized.startsWith("configured")) return applyPrefix("configured", theme.success); | |
| if (normalized.startsWith("not linked")) return applyPrefix("not linked", theme.warn); | |
| if (normalized.startsWith("not configured")) return applyPrefix("not configured", theme.muted); | |
| if (normalized.startsWith("unknown")) return applyPrefix("unknown", theme.warn); | |
| return line; | |
| } | |
| function runGatewayCommand(action: () => Promise<void>, label?: string) { | |
| return runCommandWithRuntime(defaultRuntime, action, (err) => { | |
| const message = String(err); | |
| defaultRuntime.error(label ? `${label}: ${message}` : message); | |
| defaultRuntime.exit(1); | |
| }); | |
| } | |
| function parseDaysOption(raw: unknown, fallback = 30): number { | |
| if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(1, Math.floor(raw)); | |
| if (typeof raw === "string" && raw.trim() !== "") { | |
| const parsed = Number(raw); | |
| if (Number.isFinite(parsed)) return Math.max(1, Math.floor(parsed)); | |
| } | |
| return fallback; | |
| } | |
| function renderCostUsageSummary(summary: CostUsageSummary, days: number, rich: boolean): string[] { | |
| const totalCost = formatUsd(summary.totals.totalCost) ?? "$0.00"; | |
| const totalTokens = formatTokenCount(summary.totals.totalTokens) ?? "0"; | |
| const lines = [ | |
| colorize(rich, theme.heading, `Usage cost (${days} days)`), | |
| `${colorize(rich, theme.muted, "Total:")} ${totalCost} · ${totalTokens} tokens`, | |
| ]; | |
| if (summary.totals.missingCostEntries > 0) { | |
| lines.push( | |
| `${colorize(rich, theme.muted, "Missing entries:")} ${summary.totals.missingCostEntries}`, | |
| ); | |
| } | |
| const latest = summary.daily.at(-1); | |
| if (latest) { | |
| const latestCost = formatUsd(latest.totalCost) ?? "$0.00"; | |
| const latestTokens = formatTokenCount(latest.totalTokens) ?? "0"; | |
| lines.push( | |
| `${colorize(rich, theme.muted, "Latest day:")} ${latest.date} · ${latestCost} · ${latestTokens} tokens`, | |
| ); | |
| } | |
| return lines; | |
| } | |
| 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.molt.bot/cli/gateway")}\n`, | |
| ), | |
| ); | |
| addGatewayRunCommand( | |
| gateway.command("run").description("Run the WebSocket Gateway (foreground)"), | |
| ); | |
| gateway | |
| .command("status") | |
| .description("Show gateway service status + probe the Gateway") | |
| .option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)") | |
| .option("--token <token>", "Gateway token (if required)") | |
| .option("--password <password>", "Gateway password (password auth)") | |
| .option("--timeout <ms>", "Timeout in ms", "10000") | |
| .option("--no-probe", "Skip RPC probe") | |
| .option("--deep", "Scan system-level services", false) | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonStatus({ | |
| rpc: opts, | |
| probe: Boolean(opts.probe), | |
| deep: Boolean(opts.deep), | |
| json: Boolean(opts.json), | |
| }); | |
| }); | |
| gateway | |
| .command("install") | |
| .description("Install the Gateway service (launchd/systemd/schtasks)") | |
| .option("--port <port>", "Gateway port") | |
| .option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node") | |
| .option("--token <token>", "Gateway token (token auth)") | |
| .option("--force", "Reinstall/overwrite if already installed", false) | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonInstall(opts); | |
| }); | |
| gateway | |
| .command("uninstall") | |
| .description("Uninstall the Gateway service (launchd/systemd/schtasks)") | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonUninstall(opts); | |
| }); | |
| gateway | |
| .command("start") | |
| .description("Start the Gateway service (launchd/systemd/schtasks)") | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonStart(opts); | |
| }); | |
| gateway | |
| .command("stop") | |
| .description("Stop the Gateway service (launchd/systemd/schtasks)") | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonStop(opts); | |
| }); | |
| gateway | |
| .command("restart") | |
| .description("Restart the Gateway service (launchd/systemd/schtasks)") | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| await runDaemonRestart(opts); | |
| }); | |
| 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) => { | |
| await runGatewayCommand(async () => { | |
| 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)); | |
| }, "Gateway call failed"); | |
| }), | |
| ); | |
| gatewayCallOpts( | |
| gateway | |
| .command("usage-cost") | |
| .description("Fetch usage cost summary from session logs") | |
| .option("--days <days>", "Number of days to include", "30") | |
| .action(async (opts) => { | |
| await runGatewayCommand(async () => { | |
| const days = parseDaysOption(opts.days); | |
| const result = await callGatewayCli("usage.cost", opts, { days }); | |
| if (opts.json) { | |
| defaultRuntime.log(JSON.stringify(result, null, 2)); | |
| return; | |
| } | |
| const rich = isRich(); | |
| const summary = result as CostUsageSummary; | |
| for (const line of renderCostUsageSummary(summary, days, rich)) { | |
| defaultRuntime.log(line); | |
| } | |
| }, "Gateway usage cost failed"); | |
| }), | |
| ); | |
| gatewayCallOpts( | |
| gateway | |
| .command("health") | |
| .description("Fetch Gateway health") | |
| .action(async (opts) => { | |
| await runGatewayCommand(async () => { | |
| 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(styleHealthChannelLine(line, rich)); | |
| } | |
| } | |
| }); | |
| }), | |
| ); | |
| gateway | |
| .command("probe") | |
| .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) => { | |
| await runGatewayCommand(async () => { | |
| await gatewayStatusCommand(opts, defaultRuntime); | |
| }); | |
| }); | |
| 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) => { | |
| await runGatewayCommand(async () => { | |
| 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); | |
| } | |
| } | |
| }, "gateway discover failed"); | |
| }); | |
| } | |