import { exec, execSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
const execAsync = promisify(exec);
const ANSI_RESET = "\x1b[0m";
const ANSI_BOLD = "\x1b[1m";
const ANSI_GREEN = "\x1b[32m";
const ANSI_YELLOW = "\x1b[33m";
const ANSI_RED = "\x1b[31m";
const ANSI_CYAN = "\x1b[36m";
function color(text, code) {
    return `${code}${text}${ANSI_RESET}`;
}
function okText(text) {
    return color(text, ANSI_GREEN);
}
function warnText(text) {
    return color(text, ANSI_YELLOW);
}
function errorText(text) {
    return color(text, ANSI_RED);
}
function infoText(text) {
    return color(text, ANSI_CYAN);
}
function boldText(text) {
    return color(text, ANSI_BOLD);
}
// ─── Terminal Constants ─────────────────────────────────────────────────────
const TERMINAL_BUNDLE_ID = "com.apple.Terminal";
const TERMINAL_NAME = "Terminal";
// ─── Node.js Binary Resolution ──────────────────────────────────────────────
/**
 * Resolve the absolute real path to the Node.js binary.
 * Follows symlinks (e.g. Homebrew shims) to find the actual executable,
 * which is what macOS TCC uses for permission checks — TCC resolves
 * symlinks internally, so we must store the real Cellar path.
 *
 * To prevent Homebrew upgrades from changing this path, `prepare-mac`
 * pins the node formula (`brew pin node`). If the path does change,
 * `repairTccPathsIfChanged()` detects it at startup and re-grants.
 */
function resolveNodeBinaryPath() {
    try {
        const whichPath = execSync("which node", { encoding: "utf-8" }).trim();
        if (!whichPath)
            return null;
        return realpathSync(whichPath);
    }
    catch {
        return null;
    }
}
let cachedNodePath;
function getNodeBinaryPath() {
    if (cachedNodePath === undefined) {
        cachedNodePath = resolveNodeBinaryPath();
    }
    return cachedNodePath;
}
/**
 * Resolve the absolute real path to the cliclick binary.
 * Same rationale as Node — TCC resolves symlinks internally.
 */
function resolveCliclickBinaryPath() {
    try {
        const whichPath = execSync("which cliclick", { encoding: "utf-8" }).trim();
        if (!whichPath)
            return null;
        return realpathSync(whichPath);
    }
    catch {
        return null;
    }
}
let cachedCliclickPath;
function getCliclickBinaryPath() {
    if (cachedCliclickPath === undefined) {
        cachedCliclickPath = resolveCliclickBinaryPath();
    }
    return cachedCliclickPath;
}
// ─── TCC Permission Management ──────────────────────────────────────────────
const TCC_SERVICES = {
    screenRecording: "kTCCServiceScreenCapture",
    accessibility: "kTCCServiceAccessibility",
    appleEvents: "kTCCServiceAppleEvents",
    fullDiskAccess: "kTCCServiceSystemPolicyAllFiles",
    camera: "kTCCServiceCamera",
    microphone: "kTCCServiceMicrophone",
    contacts: "kTCCServiceAddressBook",
    calendar: "kTCCServiceCalendar",
    reminders: "kTCCServiceReminders",
    photos: "kTCCServicePhotos",
    speechRecognition: "kTCCServiceSpeechRecognition",
    inputMonitoring: "kTCCServiceListenEvent",
    desktopFolder: "kTCCServiceSystemPolicyDesktopFolder",
    documentsFolder: "kTCCServiceSystemPolicyDocumentsFolder",
    downloadsFolder: "kTCCServiceSystemPolicyDownloadsFolder",
};
/**
 * System-level TCC services that macOS manages in the *system* TCC.db
 * (`/Library/Application Support/com.apple.TCC/TCC.db`), not the user-level
 * one. Writing these to the user DB appears to succeed but `tccd` ignores the
 * rows at runtime — the permission is never actually granted. These must be
 * granted manually in System Settings > Privacy & Security, or via MDM /
 * configuration profiles.
 */
const SYSTEM_TCC_SERVICES = new Set([
    "kTCCServiceScreenCapture", // Screen Recording
    "kTCCServiceAccessibility", // Accessibility
    "kTCCServiceListenEvent", // Input Monitoring
    "kTCCServiceSystemPolicyAllFiles", // Full Disk Access
]);
/**
 * Attempt to grant a TCC permission to a specific client via the user TCC.db.
 *
 * On macOS Sonoma+ the user-level TCC.db can be modified with sqlite3 when
 * SIP's TCC protection allows it (common on manufacturer-provisioned Macs or
 * when using MDM). Falls back to opening System Settings if direct write fails.
 *
 * **Important:** System-level services (Accessibility, Screen Recording, Input
 * Monitoring, Full Disk Access) are managed by the system TCC.db and cannot be
 * granted by writing to the user DB. This function returns `false` for those
 * services — they require manual approval in System Settings.
 *
 * @param clientId - bundle ID (e.g. "com.apple.Terminal") or absolute path to executable
 * @param clientType - 0 for bundle ID, 1 for absolute path
 *
 * Returns true if the permission was successfully inserted/verified.
 */
async function grantTCCPermissionForClient(service, clientId, clientType) {
    if (process.platform !== "darwin")
        return true;
    const tccService = TCC_SERVICES[service];
    // System-level services live in the system TCC.db which requires root /
    // MDM to modify. Writing to the user DB silently does nothing — tccd
    // ignores the row and the permission is never actually granted at runtime.
    if (SYSTEM_TCC_SERVICES.has(tccService)) {
        return false;
    }
    const dbPath = `${process.env.HOME}/Library/Application Support/com.apple.TCC/TCC.db`;
    try {
        const escaped = clientId.replace(/'/g, "''");
        const { stdout: existing } = await execAsync(`sqlite3 "${dbPath}" "SELECT auth_value FROM access WHERE service='${tccService}' AND client='${escaped}';" 2>/dev/null`);
        if (existing.trim() === "2")
            return true;
        await execAsync(`sqlite3 "${dbPath}" "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, flags) VALUES ('${tccService}', '${escaped}', ${clientType}, 2, 3, 1, 0);" 2>/dev/null`);
        const { stdout: verify } = await execAsync(`sqlite3 "${dbPath}" "SELECT auth_value FROM access WHERE service='${tccService}' AND client='${escaped}';" 2>/dev/null`);
        return verify.trim() === "2";
    }
    catch {
        return false;
    }
}
/**
 * Read TCC auth_value for a service + client (bundle id or absolute path).
 * Checks the user-level TCC.db by default.
 */
async function readTccAuthValue(service, clientId) {
    if (process.platform !== "darwin")
        return null;
    const tccService = TCC_SERVICES[service];
    const dbPath = `${process.env.HOME}/Library/Application Support/com.apple.TCC/TCC.db`;
    const escaped = clientId.replace(/'/g, "''");
    try {
        const { stdout } = await execAsync(`sqlite3 "${dbPath}" "SELECT auth_value FROM access WHERE service='${tccService}' AND client='${escaped}';" 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
        const t = stdout.trim();
        if (t === "")
            return null;
        const n = parseInt(t, 10);
        return Number.isNaN(n) ? null : n;
    }
    catch {
        return null;
    }
}
/**
 * Read TCC auth_value from the *system* TCC.db.
 * System-level services (Screen Recording, Accessibility, Input Monitoring,
 * Full Disk Access) are managed in `/Library/Application Support/com.apple.TCC/TCC.db`.
 * Reading this DB may require root on some macOS versions, so failures are expected.
 */
async function readSystemTccAuthValue(service, clientId) {
    if (process.platform !== "darwin")
        return null;
    const tccService = TCC_SERVICES[service];
    const dbPath = "/Library/Application Support/com.apple.TCC/TCC.db";
    const escaped = clientId.replace(/'/g, "''");
    try {
        const { stdout } = await execAsync(`sqlite3 "${dbPath}" "SELECT auth_value FROM access WHERE service='${tccService}' AND client='${escaped}';" 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
        const t = stdout.trim();
        if (t === "")
            return null;
        const n = parseInt(t, 10);
        return Number.isNaN(n) ? null : n;
    }
    catch {
        return null;
    }
}
/**
 * Grant a TCC permission to Apple Terminal (bundle ID-based).
 */
export async function grantTCCPermission(service) {
    return grantTCCPermissionForClient(service, TERMINAL_BUNDLE_ID, 0);
}
/**
 * Grant a TCC permission to the Node.js binary (path-based).
 * TCC tracks executable permissions by absolute path for non-bundled binaries.
 */
export async function grantTCCPermissionForNode(service) {
    const nodePath = getNodeBinaryPath();
    if (!nodePath)
        return false;
    return grantTCCPermissionForClient(service, nodePath, 1);
}
/**
 * Grant a TCC permission to the cliclick binary (path-based).
 * cliclick directly calls Accessibility APIs (CGEventPost) and needs
 * its own TCC entry — granting to Terminal or Node is not sufficient
 * when the agent runs via pm2/launchd.
 */
async function grantTCCPermissionForCliclick(service) {
    const cliclickPath = getCliclickBinaryPath();
    if (!cliclickPath)
        return false;
    return grantTCCPermissionForClient(service, cliclickPath, 1);
}
/**
 * Open the System Settings panel for a given TCC permission category.
 */
export function openSystemSettingsForPermission(permission) {
    const urls = {
        screenRecording: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
        accessibility: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
        appleEvents: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
        fullDiskAccess: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
        camera: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
        microphone: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
        contacts: "x-apple.systempreferences:com.apple.preference.security?Privacy_Contacts",
        calendar: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars",
        reminders: "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders",
        photos: "x-apple.systempreferences:com.apple.preference.security?Privacy_Photos",
        speechRecognition: "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition",
        inputMonitoring: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent",
        desktopFolder: "x-apple.systempreferences:com.apple.preference.security?Privacy_DesktopFolder",
        documentsFolder: "x-apple.systempreferences:com.apple.preference.security?Privacy_DocumentsFolder",
        downloadsFolder: "x-apple.systempreferences:com.apple.preference.security?Privacy_DownloadsFolder",
    };
    try {
        execSync(`open "${urls[permission]}"`, { stdio: "ignore" });
    }
    catch { /* non-fatal */ }
}
/**
 * Attempt to grant all required TCC permissions for Apple Terminal and Node.js.
 * Also grants Accessibility specifically to cliclick, which calls the API directly.
 * Tries direct TCC.db writes; returns which permissions were successfully granted
 * for Terminal (Node/cliclick results are best-effort alongside Terminal).
 */
export async function grantAllTerminalPermissions() {
    const results = {};
    for (const key of Object.keys(TCC_SERVICES)) {
        results[key] = await grantTCCPermission(key);
        await grantTCCPermissionForNode(key);
        if (key === "accessibility") {
            await grantTCCPermissionForCliclick(key);
        }
    }
    return results;
}
/**
 * Attempt to grant all required TCC permissions specifically for the Node.js binary.
 * Also grants Accessibility to cliclick which is the actual API caller.
 * Returns which permissions were successfully granted.
 */
export async function grantAllNodePermissions() {
    const results = {};
    for (const key of Object.keys(TCC_SERVICES)) {
        results[key] = await grantTCCPermissionForNode(key);
        if (key === "accessibility") {
            await grantTCCPermissionForCliclick(key);
        }
    }
    return results;
}
/**
 * Get the resolved Node.js binary path (for display purposes).
 */
export { getNodeBinaryPath };
/**
 * Get the resolved cliclick binary path (for display purposes).
 */
export { getCliclickBinaryPath };
/**
 * Check if Screen Recording permission is granted on macOS.
 *
 * We do this by attempting a silent screenshot. If it succeeds and
 * produces a non-trivial file, we have permission. If the file is
 * tiny or missing, the permission was denied.
 */
export async function checkScreenRecordingPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        const tmpFile = `/tmp/visionclaw-permission-check-${Date.now()}.png`;
        await execAsync(`screencapture -x -C "${tmpFile}"`);
        // Check if the file exists and has meaningful content
        const { stdout } = await execAsync(`stat -f%z "${tmpFile}" 2>/dev/null || echo 0`);
        const size = parseInt(stdout.trim(), 10);
        // Clean up
        await execAsync(`rm -f "${tmpFile}"`).catch(() => { });
        // A real screenshot is at least a few KB; a blank/denied one is tiny or zero
        return size > 1000;
    }
    catch {
        return false;
    }
}
/**
 * Check if cliclick is installed (required for computer_use tools).
 */
export async function checkCliclickInstalled() {
    if (process.platform !== "darwin")
        return true;
    try {
        const { stdout } = await execAsync("which cliclick 2>/dev/null");
        return stdout.trim().length > 0;
    }
    catch {
        return false;
    }
}
/**
 * Check if ffmpeg is installed (required for screenshot compression and audio transcription).
 */
export async function checkFfmpegInstalled() {
    try {
        const { stdout } = await execAsync("which ffmpeg 2>/dev/null");
        if (stdout.trim().length > 0)
            return true;
    }
    catch { /* fall through */ }
    const candidates = [
        "/opt/homebrew/bin/ffmpeg",
        "/usr/local/bin/ffmpeg",
        "/usr/bin/ffmpeg",
    ];
    for (const c of candidates) {
        try {
            await execAsync(`test -x "${c}"`);
            return true;
        }
        catch { /* try next */ }
    }
    return false;
}
/**
 * Check if whisper-cli is installed (required for audio transcription).
 */
export async function checkWhisperCppInstalled() {
    try {
        const { stdout } = await execAsync("which whisper-cli 2>/dev/null");
        if (stdout.trim().length > 0)
            return true;
    }
    catch { /* fall through */ }
    const candidates = [
        "/opt/homebrew/bin/whisper-cli",
        "/usr/local/bin/whisper-cli",
    ];
    for (const c of candidates) {
        try {
            await execAsync(`test -x "${c}"`);
            return true;
        }
        catch { /* try next */ }
    }
    return false;
}
export const WHISPER_MODEL_DIR = `${process.env.HOME}/.local/share/whisper-cpp`;
const WHISPER_MODEL_FILENAME = "ggml-large-v3-turbo.bin";
export const WHISPER_MODEL_PATH = `${WHISPER_MODEL_DIR}/${WHISPER_MODEL_FILENAME}`;
export const WHISPER_MODEL_URL = "https://screenshots.tos-accelerate.volces.com/uploads/ggml-large-v3-turbo.bin";
/**
 * Fetch the expected file size from the remote URL via a HEAD request.
 * Result is cached for the lifetime of the process.
 */
let _cachedRemoteSize;
function getWhisperModelExpectedSize() {
    if (_cachedRemoteSize !== undefined)
        return _cachedRemoteSize;
    try {
        const output = execSync(`curl -sI -L "${WHISPER_MODEL_URL}" 2>/dev/null | grep -i content-length | tail -1`, { encoding: "utf-8", timeout: 15_000 });
        const match = /content-length:\s*(\d+)/i.exec(output);
        _cachedRemoteSize = match ? parseInt(match[1], 10) : null;
    }
    catch {
        _cachedRemoteSize = null;
    }
    return _cachedRemoteSize;
}
/**
 * Check if the whisper-cpp model file exists AND has the expected size.
 * Fetches expected size from the remote URL via HEAD request (cached).
 * Returns "valid" if present and correct size, "incomplete" if present but wrong
 * size (e.g. interrupted download), or "missing" if not found.
 * If the remote size cannot be determined (no network), a present file is
 * assumed valid.
 */
export function checkWhisperModelIntegrity() {
    try {
        const stat = statSync(WHISPER_MODEL_PATH);
        const expectedSize = getWhisperModelExpectedSize();
        if (expectedSize === null) {
            // Network unavailable — file exists, assume valid
            return stat.size > 0 ? "valid" : "incomplete";
        }
        return stat.size === expectedSize ? "valid" : "incomplete";
    }
    catch {
        return "missing";
    }
}
/**
 * Check if the whisper-cpp model file exists (legacy boolean wrapper).
 */
export function checkWhisperModelExists() {
    return checkWhisperModelIntegrity() === "valid";
}
/**
 * Check if pm2 is installed globally (used as the process manager in production).
 */
export async function checkPm2Installed() {
    try {
        const { stdout } = await execAsync("which pm2 2>/dev/null");
        return stdout.trim().length > 0;
    }
    catch {
        return false;
    }
}
/**
 * Check if the GitHub CLI (`gh`) is installed.
 */
export async function checkGhInstalled() {
    try {
        const { stdout } = await execAsync("which gh 2>/dev/null");
        return stdout.trim().length > 0;
    }
    catch {
        return false;
    }
}
/**
 * Check if the GitHub CLI is authenticated (logged in).
 */
export async function checkGhAuthenticated() {
    try {
        const result = await execAsync("gh auth status 2>&1");
        return !result.stdout.includes("not logged");
    }
    catch {
        return false;
    }
}
/**
 * Check if Google Chrome or Chromium is installed (required for Playwright browser tools).
 */
export async function checkChromeInstalled() {
    // Check common macOS app locations
    const appPaths = [
        "/Applications/Google Chrome.app",
        "/Applications/Chromium.app",
        `${process.env.HOME}/Applications/Google Chrome.app`,
        `${process.env.HOME}/Applications/Chromium.app`,
    ];
    for (const p of appPaths) {
        try {
            await execAsync(`test -d "${p}"`);
            return true;
        }
        catch { /* try next */ }
    }
    // Check PATH-based executables
    try {
        const { stdout } = await execAsync("which google-chrome chromium chromium-browser 2>/dev/null | head -1");
        if (stdout.trim().length > 0)
            return true;
    }
    catch { /* fall through */ }
    return false;
}
/**
 * Check if Accessibility permission is granted on macOS.
 *
 * Uses `cliclick p` (print cursor position) which requires the Accessibility
 * API (CGEventPost / AXUIElement). The previous osascript-based check tested
 * Automation permission to System Events, not the actual Accessibility API
 * that cliclick needs at runtime.
 */
export async function checkAccessibilityPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        const { stdout } = await execAsync("cliclick p 2>/dev/null");
        // cliclick p outputs "x,y" coordinates on success
        return /^\d+,\d+/.test(stdout.trim());
    }
    catch {
        return false;
    }
}
/**
 * Check if Automation (AppleScript) permission is granted on macOS.
 *
 * Tests whether osascript can send Apple Events to System Events, which is
 * the primary Automation target at runtime (pressKey uses `key code` via
 * System Events). macOS tracks Automation permission per-target app, so
 * we must test the actual target the agent depends on.
 */
export async function checkAutomationPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        const { stdout } = await execAsync(`osascript -e 'tell application "System Events" to return name of first process' 2>&1`);
        return stdout.trim().length > 0 && !stdout.includes("not allowed");
    }
    catch {
        return false;
    }
}
/**
 * Check if Files & Folders / Full Disk Access is sufficient.
 *
 * Tests whether we can read a protected location (~/Library/Mail).
 * Full Disk Access protects ~/Library/Mail, ~/Library/Messages, etc.
 * If we can list it, Terminal has FDA.
 */
export async function checkFullDiskAccessPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        await execAsync(`ls "${process.env.HOME}/Library/Mail" >/dev/null 2>&1`);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if Camera permission is granted.
 * Uses AVFoundation's authorizationStatus API via a compiled Swift helper.
 */
export async function checkCameraPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("camera", [
        "import AVFoundation",
        "exit(AVCaptureDevice.authorizationStatus(for: .video) == .authorized ? 0 : 1)",
    ].join("\n"));
}
/**
 * Check if Microphone permission is granted.
 */
export async function checkMicrophonePermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("mic", [
        "import AVFoundation",
        "exit(AVCaptureDevice.authorizationStatus(for: .audio) == .authorized ? 0 : 1)",
    ].join("\n"));
}
/**
 * Check if Contacts permission is granted.
 */
export async function checkContactsPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("contacts", [
        "import Contacts",
        "let s = CNContactStore.authorizationStatus(for: .contacts)",
        "exit(s == .authorized ? 0 : 1)",
    ].join("\n"));
}
/**
 * Check if Calendar permission is granted.
 */
export async function checkCalendarPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("calendar", [
        "import EventKit",
        "let s = EKEventStore.authorizationStatus(for: .event)",
        "if #available(macOS 14.0, *) {",
        "  exit(s == .fullAccess ? 0 : 1)",
        "} else {",
        "  exit(s == .authorized ? 0 : 1)",
        "}",
    ].join("\n"));
}
/**
 * Check if Reminders permission is granted.
 */
export async function checkRemindersPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("reminders", [
        "import EventKit",
        "let s = EKEventStore.authorizationStatus(for: .reminder)",
        "if #available(macOS 14.0, *) {",
        "  exit(s == .fullAccess ? 0 : 1)",
        "} else {",
        "  exit(s == .authorized ? 0 : 1)",
        "}",
    ].join("\n"));
}
/**
 * Check if Photos permission is granted.
 */
export async function checkPhotosPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("photos", [
        "import Photos",
        "let s = PHPhotoLibrary.authorizationStatus(for: .readWrite)",
        "exit(s == .authorized || s == .limited ? 0 : 1)",
    ].join("\n"));
}
/**
 * Check if Speech Recognition permission is granted.
 */
export async function checkSpeechRecognitionPermission() {
    if (process.platform !== "darwin")
        return true;
    return runSwiftCheck("speech", [
        "import Speech",
        "exit(SFSpeechRecognizer.authorizationStatus() == .authorized ? 0 : 1)",
    ].join("\n"));
}
/**
 * Check if Input Monitoring permission is granted for Terminal, Node, and (in Electron) the app bundle.
 *
 * Input Monitoring is a system-level TCC service — entries may live in either the
 * user-level or system-level TCC.db depending on how the permission was granted
 * (manual toggle vs MDM profile). We check both databases to avoid false negatives,
 * which was observed on Mac Mini where entries only existed in the system DB.
 */
export async function checkInputMonitoringPermission() {
    if (process.platform !== "darwin")
        return true;
    const terminalOk = (await readTccAuthValue("inputMonitoring", TERMINAL_BUNDLE_ID)) === 2 ||
        (await readSystemTccAuthValue("inputMonitoring", TERMINAL_BUNDLE_ID)) === 2;
    const nodePath = getNodeBinaryPath();
    const nodeOk = !nodePath ||
        (await readTccAuthValue("inputMonitoring", nodePath)) === 2 ||
        (await readSystemTccAuthValue("inputMonitoring", nodePath)) === 2;
    if (process.versions.electron) {
        const electronBundleId = process.execPath.includes("VisionClaw.app/")
            ? "com.babelcloud.visionclaw"
            : "com.github.Electron";
        const electronOk = (await readTccAuthValue("inputMonitoring", electronBundleId)) === 2 ||
            (await readSystemTccAuthValue("inputMonitoring", electronBundleId)) === 2;
        return electronOk && terminalOk && nodeOk;
    }
    return terminalOk && nodeOk;
}
/**
 * Check if Desktop folder access is granted.
 */
export async function checkDesktopFolderPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        await execAsync(`ls "${process.env.HOME}/Desktop" >/dev/null 2>&1`);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if Documents folder access is granted.
 */
export async function checkDocumentsFolderPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        await execAsync(`ls "${process.env.HOME}/Documents" >/dev/null 2>&1`);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if Downloads folder access is granted.
 */
export async function checkDownloadsFolderPermission() {
    if (process.platform !== "darwin")
        return true;
    try {
        await execAsync(`ls "${process.env.HOME}/Downloads" >/dev/null 2>&1`);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Compile and run a Swift snippet, returning true if exit code is 0.
 *
 * Used to check macOS permission status via native framework APIs
 * (AVFoundation, EventKit, CoreLocation, etc.) which cannot be queried
 * from the shell or TCC database reliably.
 */
async function runSwiftCheck(label, code) {
    const base = join(tmpdir(), `visionclaw-perm-check-${label}`);
    const src = `${base}.swift`;
    const bin = base;
    try {
        writeFileSync(src, code, "utf-8");
        await execAsync(`swiftc -O -o "${bin}" "${src}" 2>/dev/null`, { timeout: 30_000 });
        try {
            unlinkSync(src);
        }
        catch { /* cleanup */ }
        await execAsync(`"${bin}"`, { timeout: 10_000 });
        return true;
    }
    catch {
        try {
            unlinkSync(src);
        }
        catch { /* cleanup */ }
        return false;
    }
}
/**
 * Print instructions for granting a macOS permission to Apple Terminal.
 */
export function printPermissionInstructions(permission) {
    const settingsPath = {
        "screen-recording": "Screen Recording",
        "accessibility": "Accessibility",
        "automation": "Automation",
        "full-disk-access": "Full Disk Access",
        "camera": "Camera",
        "microphone": "Microphone",
        "contacts": "Contacts",
        "calendar": "Calendars",
        "reminders": "Reminders",
        "photos": "Photos",
        "speech-recognition": "Speech Recognition",
        "input-monitoring": "Input Monitoring",
        "desktop-folder": "Files and Folders",
        "documents-folder": "Files and Folders",
        "downloads-folder": "Files and Folders",
    };
    const panel = settingsPath[permission];
    console.log(`\n  To grant ${panel} permission:`);
    console.log(`  1. Open System Settings > Privacy & Security > ${panel}`);
    console.log(`  2. Click '+' and add Terminal (or toggle it on)`);
    if (permission === "accessibility") {
        const cliclickPath = getCliclickBinaryPath();
        const nodePath = getNodeBinaryPath();
        console.log(`  3. Also add cliclick${cliclickPath ? ` (${cliclickPath})` : ""} — it calls the Accessibility API directly`);
        console.log(`  4. Also add Node${nodePath ? ` (${nodePath})` : ""}`);
        console.log(`  5. Restart Terminal after granting permission`);
    }
    else {
        console.log(`  3. Restart Terminal after granting permission`);
    }
}
/**
 * Run all macOS permission checks and report results.
 * Returns true if all required permissions are granted.
 */
export async function checkMacOSPermissions() {
    if (process.platform !== "darwin") {
        return true;
    }
    console.log(`${boldText("--- macOS Permissions Check ---")}\n`);
    console.log(`  ${infoText("Terminal:")} ${TERMINAL_NAME} (${TERMINAL_BUNDLE_ID})\n`);
    let allGranted = true;
    const permChecks = [
        { label: "Screen Recording", check: checkScreenRecordingPermission, permType: "screen-recording" },
        { label: "Accessibility", check: checkAccessibilityPermission, permType: "accessibility" },
        { label: "Automation", check: checkAutomationPermission, permType: "automation" },
        { label: "Full Disk Access", check: checkFullDiskAccessPermission, permType: "full-disk-access" },
        { label: "Camera", check: checkCameraPermission, permType: "camera" },
        { label: "Microphone", check: checkMicrophonePermission, permType: "microphone" },
        { label: "Contacts", check: checkContactsPermission, permType: "contacts" },
        { label: "Calendar", check: checkCalendarPermission, permType: "calendar" },
        { label: "Reminders", check: checkRemindersPermission, permType: "reminders" },
        { label: "Photos", check: checkPhotosPermission, permType: "photos" },
        { label: "Speech Recognition", check: checkSpeechRecognitionPermission, permType: "speech-recognition" },
        { label: "Input Monitoring", check: checkInputMonitoringPermission, permType: "input-monitoring" },
        { label: "Desktop Folder", check: checkDesktopFolderPermission, permType: "desktop-folder" },
        { label: "Documents Folder", check: checkDocumentsFolderPermission, permType: "documents-folder" },
        { label: "Downloads Folder", check: checkDownloadsFolderPermission, permType: "downloads-folder" },
    ];
    for (const { label, check, permType } of permChecks) {
        const padded = `${label}:`.padEnd(22);
        process.stdout.write(`  ${padded} `);
        const granted = await check();
        if (granted) {
            console.log(okText("Granted"));
        }
        else {
            console.log(errorText("NOT GRANTED"));
            printPermissionInstructions(permType);
            allGranted = false;
        }
    }
    // cliclick (for computer_use tools)
    process.stdout.write("  cliclick:              ");
    const cliclickOk = await checkCliclickInstalled();
    if (cliclickOk) {
        console.log(okText("Installed"));
    }
    else {
        console.log(errorText("NOT INSTALLED"));
        console.log("\n  To install cliclick: brew install cliclick");
        allGranted = false;
    }
    // ffmpeg (for screenshot compression and audio transcription)
    process.stdout.write("  ffmpeg:                ");
    const ffmpegOk = await checkFfmpegInstalled();
    if (ffmpegOk) {
        console.log(okText("Installed"));
    }
    else {
        console.log(errorText("NOT INSTALLED"));
        console.log("\n  To install ffmpeg: brew install ffmpeg");
        allGranted = false;
    }
    // Chrome / Chromium (for Playwright browser tools)
    process.stdout.write("  Chrome:                ");
    const chromeOk = await checkChromeInstalled();
    if (chromeOk) {
        console.log(okText("Found"));
    }
    else {
        console.log(errorText("NOT FOUND"));
        console.log("\n  Install Google Chrome from https://www.google.com/chrome/");
        allGranted = false;
    }
    // GitHub CLI (for code operations)
    process.stdout.write("  gh (GitHub CLI):       ");
    const ghOk = await checkGhInstalled();
    if (ghOk) {
        const ghAuth = await checkGhAuthenticated();
        if (ghAuth) {
            console.log(okText("Installed & Authenticated"));
        }
        else {
            console.log(warnText("Installed; not authenticated yet"));
            console.log("  Setup can authenticate gh using a server-delivered GitHub PAT when GitHub is enabled.");
        }
    }
    else {
        console.log(errorText("NOT INSTALLED"));
        console.log("\n  To install: brew install gh");
        allGranted = false;
    }
    // whisper-cpp (for audio transcription)
    process.stdout.write("  whisper-cli:           ");
    const whisperOk = await checkWhisperCppInstalled();
    if (whisperOk) {
        console.log(okText("Installed"));
    }
    else {
        console.log(errorText("NOT INSTALLED"));
        console.log("\n  To install: brew install whisper-cpp (binary is whisper-cli)");
        allGranted = false;
    }
    // whisper model (large-v3-turbo for transcription)
    process.stdout.write("  Whisper model:         ");
    const modelStatus = checkWhisperModelIntegrity();
    if (modelStatus === "valid") {
        console.log(okText("Found"));
    }
    else {
        console.log(errorText(modelStatus === "incomplete" ? "INCOMPLETE (bad download)" : "NOT FOUND"));
        console.log(`\n  Download the model (1.5GB, one-time):`);
        console.log(`  mkdir -p ${WHISPER_MODEL_DIR} && curl -L -o ${WHISPER_MODEL_PATH} '${WHISPER_MODEL_URL}'`);
        allGranted = false;
    }
    // pm2 (process manager for production)
    process.stdout.write("  pm2:                   ");
    const pm2Ok = await checkPm2Installed();
    if (pm2Ok) {
        console.log(okText("Installed"));
    }
    else {
        console.log(errorText("NOT INSTALLED"));
        console.log("\n  To install: npm install -g pm2");
        allGranted = false;
    }
    console.log("");
    if (!allGranted) {
        console.log(warnText("  Some permissions or prerequisites are missing. VisionClaw needs these to control the desktop."));
        console.log(warnText("  You can continue setup now and grant them later, but the agent will"));
        console.log(`${warnText("  not be fully functional until all required items are ready.")}\n`);
    }
    else {
        console.log(`${okText("  All permissions granted!")}\n`);
    }
    return allGranted;
}
// ─── TCC Path Change Detection & Repair ─────────────────────────────────────
const TCC_PATHS_FILE = join(process.env.HOME ?? "/tmp", ".visionclaw", "tcc-binary-paths.json");
function loadTccPaths() {
    try {
        if (!existsSync(TCC_PATHS_FILE))
            return null;
        return JSON.parse(readFileSync(TCC_PATHS_FILE, "utf-8"));
    }
    catch {
        return null;
    }
}
/**
 * Save the current binary paths so we can detect changes on next startup.
 */
export function saveTccPaths() {
    const record = {
        nodePath: getNodeBinaryPath(),
        cliclickPath: getCliclickBinaryPath(),
    };
    try {
        const dir = join(process.env.HOME ?? "/tmp", ".visionclaw");
        mkdirSync(dir, { recursive: true });
        writeFileSync(TCC_PATHS_FILE, JSON.stringify(record, null, 2), "utf-8");
    }
    catch { /* non-fatal */ }
}
/**
 * Detect if Homebrew-managed binary paths have changed since the last
 * `saveTccPaths()` call. If so, re-grant all non-system TCC permissions
 * for the new paths automatically and return a list of system-level
 * services that need manual re-granting in System Settings.
 *
 * Call this on every agent startup (in index.ts) to self-heal after
 * accidental `brew upgrade`.
 *
 * Returns null if no path change was detected, or a summary object.
 */
export async function repairTccPathsIfChanged() {
    if (process.platform !== "darwin")
        return null;
    const saved = loadTccPaths();
    if (!saved) {
        // First run — just save current paths, no repair needed
        saveTccPaths();
        return null;
    }
    const currentNode = getNodeBinaryPath();
    const currentCliclick = getCliclickBinaryPath();
    const nodeChanged = saved.nodePath !== currentNode;
    const cliclickChanged = saved.cliclickPath !== currentCliclick;
    if (!nodeChanged && !cliclickChanged)
        return null;
    // Paths changed — re-grant non-system TCC permissions automatically
    const autoRepaired = [];
    const manualRequired = [];
    for (const key of Object.keys(TCC_SERVICES)) {
        const tccService = TCC_SERVICES[key];
        const isSystem = SYSTEM_TCC_SERVICES.has(tccService);
        if (nodeChanged && currentNode) {
            if (isSystem) {
                manualRequired.push(`${key} (node: ${currentNode})`);
            }
            else {
                const ok = await grantTCCPermissionForClient(key, currentNode, 1);
                if (ok)
                    autoRepaired.push(`${key}/node`);
            }
        }
        if (cliclickChanged && currentCliclick && key === "accessibility") {
            if (isSystem) {
                manualRequired.push(`${key} (cliclick: ${currentCliclick})`);
            }
            else {
                const ok = await grantTCCPermissionForClient(key, currentCliclick, 1);
                if (ok)
                    autoRepaired.push(`${key}/cliclick`);
            }
        }
    }
    // Save the new paths
    saveTccPaths();
    return { nodeChanged, cliclickChanged, autoRepaired, manualRequired };
}
/**
 * Pin Homebrew formulae that VisionClaw depends on for TCC permissions.
 * Prevents `brew upgrade` from changing their Cellar paths, which would
 * invalidate macOS permissions granted to the old versioned binary.
 *
 * Safe to call multiple times — `brew pin` on an already-pinned formula
 * is a no-op (exits 0).
 */
export function pinHomebrewBinaries() {
    const formulae = ["node", "cliclick"];
    for (const formula of formulae) {
        try {
            execSync(`brew pin ${formula} 2>/dev/null`, { stdio: "ignore" });
        }
        catch { /* formula may not be installed via Homebrew */ }
    }
}
//# sourceMappingURL=macos-permissions.js.map