import { execFile } from 'child_process'; export interface TailscaleServeConfig { /** The gateway's local port (e.g. 18800). */ localPort: number; /** Tailscale Serve HTTPS port (default 443). */ servePort?: number; /** Custom hostname (default: machine hostname). */ hostname?: string; } /** Execute a command with a timeout, returning stdout. */ function exec(cmd: string, args: string[], timeoutMs = 5000): Promise { return new Promise((resolve, reject) => { execFile(cmd, args, { timeout: timeoutMs }, (error, stdout, stderr) => { if (error) { reject(new Error(`${cmd} ${args.join(' ')} failed: ${stderr || error.message}`)); } else { resolve(stdout.trim()); } }); }); } /** * Check if Tailscale CLI is available and the daemon is running. */ export async function isTailscaleAvailable(): Promise<{ available: boolean; version?: string; error?: string }> { try { const version = await exec('tailscale', ['version', '--short']); // Check if daemon is running by getting status await exec('tailscale', ['status', '--json']); return { available: true, version }; } catch (err) { return { available: false, error: err instanceof Error ? err.message : String(err) }; } } /** * Get the MagicDNS hostname from Tailscale status. */ async function getTailscaleHostname(): Promise { try { const statusJson = await exec('tailscale', ['status', '--json']); const status = JSON.parse(statusJson) as { Self?: { DNSName?: string } }; const dnsName = status.Self?.DNSName; // DNSName ends with a trailing dot — strip it return dnsName?.replace(/\.$/, ''); } catch { return undefined; } } /** * Start Tailscale Serve to expose the local gateway port on the tailnet. * * Runs: `tailscale serve --bg --https= http://127.0.0.1:` */ export async function startTailscaleServe(config: TailscaleServeConfig): Promise { const servePort = config.servePort ?? 443; const target = `http://127.0.0.1:${config.localPort}`; try { await exec('tailscale', ['serve', '--bg', `--https=${servePort}`, target]); const hostname = config.hostname ?? await getTailscaleHostname(); if (hostname) { const url = servePort === 443 ? `https://${hostname}` : `https://${hostname}:${servePort}`; console.log(`Tailscale Serve: Flynn available at ${url}`); return url; } console.log('Tailscale Serve: started (could not determine hostname)'); return undefined; } catch (err) { console.error('Failed to start Tailscale Serve:', err instanceof Error ? err.message : err); throw err; } } /** * Stop Tailscale Serve for the configured port. * * Runs: `tailscale serve --https= off` */ export async function stopTailscaleServe(config: TailscaleServeConfig): Promise { const servePort = config.servePort ?? 443; try { await exec('tailscale', ['serve', `--https=${servePort}`, 'off']); console.log('Tailscale Serve: stopped'); } catch (err) { // Best effort — don't fail shutdown console.error('Failed to stop Tailscale Serve:', err instanceof Error ? err.message : err); } }