@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
132 lines (131 loc) • 4.46 kB
JavaScript
import { execFile, execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
const TAILSCALE_PATHS = [
"tailscale",
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
];
export async function isVpnConnected() {
try {
const stdout = await execTailscaleCmd(["status", "--json"]);
const status = JSON.parse(stdout);
return status.BackendState === "Running" && status.Self?.Online === true;
}
catch {
return false;
}
}
const TAILSCALE_SOCKET_PATHS = ["/var/run/tailscale/tailscaled.sock", "/tmp/tailscale.sock"];
export function headscaleDnsConfig(tenant = "mesh", env = "dev", overrides) {
return {
namespace: `${tenant}-${env}-headscale`,
pod: "headscale-0",
container: "dns-writer",
filePath: "/var/lib/headscale/dns/extra-records.json",
...overrides,
};
}
function execTailscaleCmd(args) {
const socketPath = TAILSCALE_SOCKET_PATHS.find(existsSync);
const attempts = [];
for (const binary of TAILSCALE_PATHS) {
if (socketPath) {
attempts.push({ binary, args: ["--socket", socketPath, ...args] });
}
attempts.push({ binary, args });
}
return new Promise((resolve, reject) => {
let index = 0;
function tryNext() {
if (index >= attempts.length) {
reject(new Error("All tailscale binary attempts failed"));
return;
}
const attempt = attempts[index++];
execFile(attempt.binary, attempt.args, (error, stdout) => {
if (error) {
tryNext();
}
else {
resolve(stdout);
}
});
}
tryNext();
});
}
export async function getTailscaleInfo() {
try {
const stdout = await execTailscaleCmd(["status", "--json"]);
const status = JSON.parse(stdout);
if (status.BackendState === "Running" &&
status.Self?.Online === true &&
status.Self.HostName &&
status.Self.TailscaleIPs?.length) {
return {
hostname: status.Self.HostName,
ip: status.Self.TailscaleIPs[0],
};
}
return null;
}
catch {
return null;
}
}
function dnsWriterExec(cfg, cmd, interactive = false) {
return [
"exec", ...(interactive ? ["-i"] : []),
"-n", cfg.namespace, cfg.pod, "-c", cfg.container, "--", ...cmd,
];
}
export function readDnsRecords(cfg) {
try {
const raw = execFileSync("kubectl", dnsWriterExec(cfg, ["cat", cfg.filePath]), {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
});
const trimmed = raw.trim();
if (!trimmed || trimmed === "[]")
return [];
try {
return JSON.parse(trimmed);
}
catch {
console.warn(`[vpn] DNS records file contains invalid JSON, treating as empty`);
return [];
}
}
catch (err) {
const msg = err.message ?? String(err);
if (msg.includes("not found") || msg.includes("Unable to connect")) {
throw new Error(`Cannot reach Headscale DNS writer (namespace=${cfg.namespace}, pod=${cfg.pod}).\n` +
`Verify kubectl context and that the Headscale pod is running:\n` +
` kubectl get pods -n ${cfg.namespace}`);
}
if (msg.includes("No such file")) {
return [];
}
throw err;
}
}
export function registerDnsRecords(cfg, records) {
const json = JSON.stringify(records);
try {
execFileSync("kubectl", dnsWriterExec(cfg, ["sh", "-c", `cat > '${cfg.filePath}'`], true), { input: json, stdio: ["pipe", "pipe", "pipe"] });
}
catch (err) {
const msg = err.message ?? String(err);
throw new Error(`Failed to write DNS records to Headscale (namespace=${cfg.namespace}, pod=${cfg.pod}).\n` +
`${msg.includes("not found") ? "Pod not found. " : ""}` +
`Verify kubectl context: kubectl get pods -n ${cfg.namespace}`);
}
}
export function unregisterDnsRecords(cfg, suffix) {
try {
const records = readDnsRecords(cfg);
const filtered = records.filter((r) => !r.name.endsWith(suffix));
registerDnsRecords(cfg, filtered);
}
catch {
}
}