openclaw/src/shared/tailscale-status.ts

75 lines
1.9 KiB
TypeScript

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<TailscaleStatusCommandResult>;
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<typeof TailscaleStatusSchema> | 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<string | null> {
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;
}