import Foundation enum LaunchAgentManager { private static let legacyLaunchdLabel = "com.steipete.clawdbot" private static var plistURL: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist") } private static var legacyPlistURL: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist") } static func status() async -> Bool { guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false } let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"]) return result == 0 } static func set(enabled: Bool, bundlePath: String) async { if enabled { _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"]) try? FileManager.default.removeItem(at: self.legacyPlistURL) self.writePlist(bundlePath: bundlePath) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) _ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path]) _ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"]) } else { // Disable autostart going forward but leave the current app running. // bootout would terminate the launchd job immediately (and crash the app if launched via agent). try? FileManager.default.removeItem(at: self.plistURL) } } private static func writePlist(bundlePath: String) { let plist = """ Label com.clawdbot.mac ProgramArguments \(bundlePath)/Contents/MacOS/Clawdbot WorkingDirectory \(FileManager.default.homeDirectoryForCurrentUser.path) RunAtLoad KeepAlive EnvironmentVariables PATH \(CommandResolver.preferredPaths().joined(separator: ":")) StandardOutPath \(LogLocator.launchdLogPath) StandardErrorPath \(LogLocator.launchdLogPath) """ try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8) } @discardableResult private static func runLaunchctl(_ args: [String]) async -> Int32 { await Task.detached(priority: .utility) { () -> Int32 in let process = Process() process.launchPath = "/bin/launchctl" process.arguments = args process.standardOutput = Pipe() process.standardError = Pipe() do { try process.run() process.waitUntilExit() return process.terminationStatus } catch { return -1 } }.value } }