import { i as __exportAll } from "./tool-display-B7qTeoCS.js";
import { A as isVerbose, L as warn, M as logVerbose, j as isYes } from "./utils-BnlaT0eO.js";
import { t as createSubsystemLogger } from "./subsystem-ocBR131w.js";
import { n as defaultRuntime } from "./runtime-CRvfzclG.js";
import { n as runExec } from "./exec-CZr_EJHt.js";
import { stdin, stdout } from "node:process";
import { existsSync } from "node:fs";
import * as http from "node:http";
import readline from "node:readline/promises";

//#region src/cli/prompt.ts
async function promptYesNo(question, defaultYes = false) {
	if (isVerbose() && isYes()) return true;
	if (isYes()) return true;
	const rl = readline.createInterface({
		input: stdin,
		output: stdout
	});
	const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
	const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
	rl.close();
	if (!answer) return defaultYes;
	return answer.startsWith("y");
}

//#endregion
//#region src/infra/binaries.ts
async function ensureBinary(name, exec = runExec, runtime = defaultRuntime) {
	await exec("which", [name]).catch(() => {
		runtime.error(`Missing required binary: ${name}. Please install it.`);
		runtime.exit(1);
	});
}

//#endregion
//#region src/infra/tailscale.ts
var tailscale_exports = /* @__PURE__ */ __exportAll({
	addTailscaleServePath: () => addTailscaleServePath,
	disableTailscaleFunnel: () => disableTailscaleFunnel,
	disableTailscaleServe: () => disableTailscaleServe,
	enableTailscaleFunnel: () => enableTailscaleFunnel,
	enableTailscaleServe: () => enableTailscaleServe,
	enableTailscaleServeFunnel: () => enableTailscaleServeFunnel,
	findTailscaleBinary: () => findTailscaleBinary,
	getTailnetHostname: () => getTailnetHostname,
	getTailscaleBinary: () => getTailscaleBinary,
	readTailscaleStatusJson: () => readTailscaleStatusJson,
	readTailscaleWhoisIdentity: () => readTailscaleWhoisIdentity
});
const log = createSubsystemLogger("infra/tailscale");
function parsePossiblyNoisyJsonObject(stdout) {
	const trimmed = stdout.trim();
	const start = trimmed.indexOf("{");
	const end = trimmed.lastIndexOf("}");
	if (start >= 0 && end > start) return JSON.parse(trimmed.slice(start, end + 1));
	return JSON.parse(trimmed);
}
/**
* Locate Tailscale binary using multiple strategies:
* 1. PATH lookup (via which command)
* 2. Known macOS app path
* 3. find /Applications for Tailscale.app
* 4. locate database (if available)
*
* @returns Path to Tailscale binary or null if not found
*/
async function findTailscaleBinary() {
	const checkBinary = async (path) => {
		if (!path || !existsSync(path)) return false;
		try {
			await Promise.race([runExec(path, ["--version"], { timeoutMs: 3e3 }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), 3e3))]);
			return true;
		} catch {
			return false;
		}
	};
	try {
		const { stdout } = await runExec("which", ["tailscale"]);
		const fromPath = stdout.trim();
		if (fromPath && await checkBinary(fromPath)) return fromPath;
	} catch {}
	const macAppPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
	if (await checkBinary(macAppPath)) return macAppPath;
	try {
		const { stdout } = await runExec("find", [
			"/Applications",
			"-maxdepth",
			"3",
			"-name",
			"Tailscale",
			"-path",
			"*/Tailscale.app/Contents/MacOS/Tailscale"
		], { timeoutMs: 5e3 });
		const found = stdout.trim().split("\n")[0];
		if (found && await checkBinary(found)) return found;
	} catch {}
	try {
		const { stdout } = await runExec("locate", ["Tailscale.app"]);
		const candidates = stdout.trim().split("\n").filter((line) => line.includes("/Tailscale.app/Contents/MacOS/Tailscale"));
		for (const candidate of candidates) if (await checkBinary(candidate)) return candidate;
	} catch {}
	return null;
}
async function getTailnetHostname(exec = runExec, detectedBinary) {
	const candidates = detectedBinary ? [detectedBinary] : ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"];
	let lastError;
	for (const candidate of candidates) {
		if (candidate.startsWith("/") && !existsSync(candidate)) continue;
		try {
			const { stdout } = await exec(candidate, ["status", "--json"], {
				timeoutMs: 5e3,
				maxBuffer: 4e5
			});
			const parsed = stdout ? parsePossiblyNoisyJsonObject(stdout) : {};
			const self = typeof parsed.Self === "object" && parsed.Self !== null ? parsed.Self : void 0;
			const dns = typeof self?.DNSName === "string" ? self.DNSName : void 0;
			const ips = Array.isArray(self?.TailscaleIPs) ? parsed.Self.TailscaleIPs ?? [] : [];
			if (dns && dns.length > 0) return dns.replace(/\.$/, "");
			if (ips.length > 0) return ips[0];
			throw new Error("Could not determine Tailscale DNS or IP");
		} catch (err) {
			lastError = err;
		}
	}
	throw lastError ?? /* @__PURE__ */ new Error("Could not determine Tailscale DNS or IP");
}
/**
* Get the Tailscale binary command to use.
* Returns a cached detected binary or the default "tailscale" command.
*/
let cachedTailscaleBinary = null;
async function getTailscaleBinary() {
	const forcedBinary = process.env.MINION_TEST_TAILSCALE_BINARY?.trim();
	if (forcedBinary) {
		cachedTailscaleBinary = forcedBinary;
		return forcedBinary;
	}
	if (cachedTailscaleBinary) return cachedTailscaleBinary;
	cachedTailscaleBinary = await findTailscaleBinary();
	return cachedTailscaleBinary ?? "tailscale";
}
async function readTailscaleStatusJson(exec = runExec, opts) {
	const { stdout } = await exec(await getTailscaleBinary(), ["status", "--json"], {
		timeoutMs: opts?.timeoutMs ?? 5e3,
		maxBuffer: 4e5
	});
	return stdout ? parsePossiblyNoisyJsonObject(stdout) : {};
}
const whoisCache = /* @__PURE__ */ new Map();
function extractExecErrorText(err) {
	const errOutput = err;
	return {
		stdout: typeof errOutput.stdout === "string" ? errOutput.stdout : "",
		stderr: typeof errOutput.stderr === "string" ? errOutput.stderr : "",
		message: typeof errOutput.message === "string" ? errOutput.message : "",
		code: typeof errOutput.code === "string" ? errOutput.code : ""
	};
}
function isPermissionDeniedError(err) {
	const { stdout, stderr, message, code } = extractExecErrorText(err);
	if (code.toUpperCase() === "EACCES") return true;
	const combined = `${stdout}\n${stderr}\n${message}`.toLowerCase();
	return combined.includes("permission denied") || combined.includes("access denied") || combined.includes("operation not permitted") || combined.includes("not permitted") || combined.includes("requires root") || combined.includes("must be run as root") || combined.includes("must be run with sudo") || combined.includes("requires sudo") || combined.includes("need sudo");
}
async function execWithSudoFallback(exec, bin, args, opts) {
	try {
		return await exec(bin, args, opts);
	} catch (err) {
		if (!isPermissionDeniedError(err)) throw err;
		logVerbose(`Command failed, retrying with sudo: ${bin} ${args.join(" ")}`);
		try {
			return await exec("sudo", [
				"-n",
				bin,
				...args
			], opts);
		} catch (sudoErr) {
			const { stderr, message } = extractExecErrorText(sudoErr);
			const detail = (stderr || message).trim();
			if (detail) logVerbose(`Sudo retry failed: ${detail}`);
			throw err;
		}
	}
}
async function enableTailscaleServe(port, exec = runExec) {
	await execWithSudoFallback(exec, await getTailscaleBinary(), [
		"serve",
		"--bg",
		"--yes",
		`${port}`
	], {
		maxBuffer: 2e5,
		timeoutMs: 15e3
	});
}
/**
* Add a path-specific route to Tailscale serve (e.g. /oauth-callback → localhost:51234).
* The route is additive — it does not reset the existing root serve config.
*/
async function addTailscaleServePath(path, port, exec = runExec) {
	await execWithSudoFallback(exec, await getTailscaleBinary(), [
		"serve",
		"--bg",
		"--yes",
		`--set-path=${path}`,
		`http://127.0.0.1:${port}`
	], {
		maxBuffer: 2e5,
		timeoutMs: 15e3
	});
}
async function disableTailscaleServe(exec = runExec) {
	await execWithSudoFallback(exec, await getTailscaleBinary(), ["serve", "reset"], {
		maxBuffer: 2e5,
		timeoutMs: 15e3
	});
}
async function enableTailscaleFunnel(port, exec = runExec) {
	await execWithSudoFallback(exec, await getTailscaleBinary(), [
		"funnel",
		"--bg",
		"--yes",
		`${port}`
	], {
		maxBuffer: 2e5,
		timeoutMs: 15e3
	});
}
async function disableTailscaleFunnel(exec = runExec) {
	await execWithSudoFallback(exec, await getTailscaleBinary(), ["funnel", "reset"], {
		maxBuffer: 2e5,
		timeoutMs: 15e3
	});
}
const TAILSCALE_LOCAL_SOCKET = "/run/tailscale/tailscaled.sock";
/**
* Read the current Tailscale serve config via the local API.
* Returns null if the socket is unavailable or the config is empty.
*/
async function readTailscaleServeConfig() {
	return new Promise((resolve) => {
		const req = http.request({
			socketPath: TAILSCALE_LOCAL_SOCKET,
			hostname: "local-tailscaled.sock",
			path: "/localapi/v0/serve-config",
			method: "GET",
			timeout: 5e3
		}, (res) => {
			let body = "";
			res.on("data", (chunk) => {
				body += chunk.toString();
			});
			res.on("end", () => {
				try {
					const parsed = JSON.parse(body);
					resolve(Object.keys(parsed).length > 0 ? parsed : null);
				} catch {
					resolve(null);
				}
			});
		});
		req.on("error", () => resolve(null));
		req.on("timeout", () => {
			req.destroy();
			resolve(null);
		});
		req.end();
	});
}
/**
* Write an updated Tailscale serve config via the local API.
*/
async function writeTailscaleServeConfig(cfg) {
	return new Promise((resolve) => {
		const body = JSON.stringify(cfg);
		const req = http.request({
			socketPath: TAILSCALE_LOCAL_SOCKET,
			hostname: "local-tailscaled.sock",
			path: "/localapi/v0/serve-config",
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"Content-Length": Buffer.byteLength(body)
			},
			timeout: 1e4
		}, (res) => {
			res.resume();
			res.on("end", () => resolve(res.statusCode !== void 0 && res.statusCode < 300));
		});
		req.on("error", () => resolve(false));
		req.on("timeout", () => {
			req.destroy();
			resolve(false);
		});
		req.write(body);
		req.end();
	});
}
/**
* Enable Tailscale Funnel for ALL serve virtual hosts by setting
* AllowFunnel=true via the local API. Called after `enableTailscaleServe`
* to make the serve endpoints publicly accessible (not just tailnet-only).
*
* `tailscale serve --bg --yes {port}` strips AllowFunnel on every run, so
* this must be called after each `enableTailscaleServe` call.
*/
async function enableTailscaleServeFunnel() {
	const cfg = await readTailscaleServeConfig();
	if (!cfg) {
		warn("tailscale serve config unavailable; skipping AllowFunnel setup");
		return;
	}
	const web = cfg["Web"];
	if (!web || Object.keys(web).length === 0) {
		warn("tailscale serve: no Web virtual host found; skipping AllowFunnel setup");
		return;
	}
	const allowFunnel = cfg["AllowFunnel"] ?? {};
	let changed = false;
	for (const hostKey of Object.keys(web)) if (allowFunnel[hostKey] !== true) {
		allowFunnel[hostKey] = true;
		changed = true;
	}
	if (!changed) return;
	cfg["AllowFunnel"] = allowFunnel;
	if (!await writeTailscaleServeConfig(cfg)) warn("tailscale serve: failed to set AllowFunnel via local API");
}
function getString(value) {
	return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function readRecord(value) {
	return value && typeof value === "object" ? value : null;
}
function parseWhoisIdentity(payload) {
	const userProfile = readRecord(payload.UserProfile) ?? readRecord(payload.userProfile) ?? readRecord(payload.User);
	const login = getString(userProfile?.LoginName) ?? getString(userProfile?.Login) ?? getString(userProfile?.login) ?? getString(payload.LoginName) ?? getString(payload.login);
	if (!login) return null;
	return {
		login,
		name: getString(userProfile?.DisplayName) ?? getString(userProfile?.Name) ?? getString(userProfile?.displayName) ?? getString(payload.DisplayName) ?? getString(payload.name)
	};
}
function readCachedWhois(ip, now) {
	const cached = whoisCache.get(ip);
	if (!cached) return;
	if (cached.expiresAt <= now) {
		whoisCache.delete(ip);
		return;
	}
	return cached.value;
}
function writeCachedWhois(ip, value, ttlMs) {
	whoisCache.set(ip, {
		value,
		expiresAt: Date.now() + ttlMs
	});
}
async function readTailscaleWhoisIdentity(ip, exec = runExec, opts) {
	const normalized = ip.trim();
	if (!normalized) return null;
	const cached = readCachedWhois(normalized, Date.now());
	if (cached !== void 0) return cached;
	const cacheTtlMs = opts?.cacheTtlMs ?? 6e4;
	const errorTtlMs = opts?.errorTtlMs ?? 5e3;
	try {
		const { stdout } = await exec(await getTailscaleBinary(), [
			"whois",
			"--json",
			normalized
		], {
			timeoutMs: opts?.timeoutMs ?? 5e3,
			maxBuffer: 2e5
		});
		const identity = parseWhoisIdentity(stdout ? parsePossiblyNoisyJsonObject(stdout) : {});
		writeCachedWhois(normalized, identity, cacheTtlMs);
		return identity;
	} catch {
		writeCachedWhois(normalized, null, errorTtlMs);
		return null;
	}
}

//#endregion
export { enableTailscaleServeFunnel as a, readTailscaleStatusJson as c, ensureBinary as d, promptYesNo as f, enableTailscaleServe as i, readTailscaleWhoisIdentity as l, disableTailscaleServe as n, findTailscaleBinary as o, enableTailscaleFunnel as r, getTailnetHostname as s, disableTailscaleFunnel as t, tailscale_exports as u };