import Foundation enum GatewayLaunchAgentManager { private static var plistURL: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist") } private static func gatewayExecutablePath(bundlePath: String) -> String { "\(bundlePath)/Contents/Resources/Relay/clawdis-gateway" } static func status() async -> Bool { guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false } let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) return result.status == 0 } static func set(enabled: Bool, bundlePath: String, port: Int) async -> String? { if enabled { let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) guard FileManager.default.isExecutableFile(atPath: gatewayBin) else { return "Embedded gateway missing in bundle; rebuild via scripts/package-mac-app.sh" } self.writePlist(bundlePath: bundlePath, port: port) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) let bootstrap = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path]) if bootstrap.status != 0 { return bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Failed to bootstrap gateway launchd job" : bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines) } _ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) return nil } _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) try? FileManager.default.removeItem(at: self.plistURL) return nil } static func kickstart() async { _ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) } private static func writePlist(bundlePath: String, port: Int) { let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) let plist = """ Label \(gatewayLaunchdLabel) ProgramArguments \(gatewayBin) --port \(port) --bind loopback WorkingDirectory \(FileManager.default.homeDirectoryForCurrentUser.path) RunAtLoad KeepAlive EnvironmentVariables PATH \(CommandResolver.preferredPaths().joined(separator: ":")) CLAWDIS_SKIP_BROWSER_CONTROL_SERVER 1 CLAWDIS_IMAGE_BACKEND sips StandardOutPath \(LogLocator.launchdGatewayLogPath) StandardErrorPath \(LogLocator.launchdGatewayLogPath) """ try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8) } private struct LaunchctlResult { let status: Int32 let output: String } @discardableResult private static func runLaunchctl(_ args: [String]) async -> LaunchctlResult { await Task.detached(priority: .utility) { () -> LaunchctlResult in let process = Process() process.launchPath = "/bin/launchctl" process.arguments = args let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe do { try process.run() process.waitUntilExit() let data = pipe.fileHandleForReading.readToEndSafely() let output = String(data: data, encoding: .utf8) ?? "" return LaunchctlResult(status: process.terminationStatus, output: output) } catch { return LaunchctlResult(status: -1, output: error.localizedDescription) } }.value } }