import { z } from "zod"; import { safeParseJsonWithSchema } from "../utils/zod-parse.js"; export type TailscaleStatusCommandResult = { code: number | null; stdout: string; }; export type TailscaleStatusCommandRunner = ( argv: string[], opts: { timeoutMs: number }, ) => Promise; const TAILSCALE_STATUS_COMMAND_CANDIDATES = [ "tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale", ]; const TailscaleStatusSchema = z.object({ Self: z .object({ DNSName: z.string().optional(), TailscaleIPs: z.array(z.string()).optional(), }) .optional(), }); function parsePossiblyNoisyStatus(raw: string): z.infer | null { const start = raw.indexOf("{"); const end = raw.lastIndexOf("}"); if (start === -1 || end <= start) { return null; } return safeParseJsonWithSchema(TailscaleStatusSchema, raw.slice(start, end + 1)); } function extractTailnetHostFromStatusJson(raw: string): string | null { const parsed = parsePossiblyNoisyStatus(raw); const dns = parsed?.Self?.DNSName; if (dns && dns.length > 0) { return dns.replace(/\.$/, ""); } const ips = parsed?.Self?.TailscaleIPs ?? []; return ips.length > 0 ? (ips[0] ?? null) : null; } export async function resolveTailnetHostWithRunner( runCommandWithTimeout?: TailscaleStatusCommandRunner, ): Promise { if (!runCommandWithTimeout) { return null; } for (const candidate of TAILSCALE_STATUS_COMMAND_CANDIDATES) { try { const result = await runCommandWithTimeout([candidate, "status", "--json"], { timeoutMs: 5000, }); if (result.code !== 0) { continue; } const raw = result.stdout.trim(); if (!raw) { continue; } const host = extractTailnetHostFromStatusJson(raw); if (host) { return host; } } catch { continue; } } return null; }