import Foundation import ServiceManagement @MainActor protocol AutostartManaging: AnyObject { var isEnabled: Bool { get set } /// Latest failure observed by the manager. `nil` when the launch-agent /// plist is in its expected state (or has never been touched). Callers /// can surface this through UI/alerts without having to inspect logs. /// The flag is reset to `nil` whenever the next operation succeeds. var lastError: String? { get } } class AutostartManager: AutostartManaging { static let shared = AutostartManager() private let launchAgentURL: URL private let bundleIdentifier = "com.june.Haven" private let executablePath: () -> String? private let writePlist: (Data, URL) throws -> Void private let removePlist: (URL) throws -> Void private let ensureLaunchAgentsDirectory: (URL) throws -> Void private let fileExists: (String) -> Bool private(set) var lastError: String? init( libraryDirectory: URL? = nil, executablePath: @escaping () -> String? = { Bundle.main.executablePath }, writePlist: @escaping (Data, URL) throws -> Void = { data, url in try data.write(to: url) }, removePlist: @escaping (URL) throws -> Void = { try FileManager.default.removeItem(at: $0) }, ensureLaunchAgentsDirectory: @escaping (URL) throws -> Void = { url in try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) }, fileExists: @escaping (String) -> Bool = { FileManager.default.fileExists(atPath: $0) } ) { let libraryDir = libraryDirectory ?? FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! let launchAgentsDir = libraryDir.appendingPathComponent("LaunchAgents") // Capture directory-creation failures instead of dropping them with // `try?`. The most common cause in practice is a parent path that is a // regular file (the test suite exercises this) or an unwritable parent; // either way the manager must continue to function (it's still safe to // query `isEnabled`) but the cause needs to be reachable for callers. do { try ensureLaunchAgentsDirectory(launchAgentsDir) } catch { lastError = "Failed to prepare LaunchAgents directory: \(error.localizedDescription)" } self.launchAgentURL = launchAgentsDir.appendingPathComponent("\(bundleIdentifier).plist") self.executablePath = executablePath self.writePlist = writePlist self.removePlist = removePlist self.ensureLaunchAgentsDirectory = ensureLaunchAgentsDirectory self.fileExists = fileExists } var isEnabled: Bool { get { return fileExists(launchAgentURL.path) } set { if newValue { enable() } else { disable() } } } private func enable() { guard !isEnabled else { return } // Point to the actual executable inside the bundle guard let appPath = executablePath() else { return } let plistContent: [String: Any] = [ "Label": bundleIdentifier, "ProgramArguments": [appPath], "RunAtLoad": true, "ProcessType": "Interactive", ] do { let data = try PropertyListSerialization.data( fromPropertyList: plistContent, format: .xml, options: 0) try writePlist(data, launchAgentURL) lastError = nil } catch { lastError = "Failed to enable autostart: \(error.localizedDescription)" } } private func disable() { guard isEnabled else { return } do { try removePlist(launchAgentURL) lastError = nil } catch { lastError = "Failed to disable autostart: \(error.localizedDescription)" } } }