import Foundation import ServiceManagement @MainActor protocol AutostartManaging: AnyObject { var isEnabled: Bool { get set } } 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 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) } ) { let libraryDir = libraryDirectory ?? FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! let launchAgentsDir = libraryDir.appendingPathComponent("LaunchAgents") // Ensure directory exists try? FileManager.default.createDirectory(at: launchAgentsDir, withIntermediateDirectories: true) self.launchAgentURL = launchAgentsDir.appendingPathComponent("\(bundleIdentifier).plist") self.executablePath = executablePath self.writePlist = writePlist self.removePlist = removePlist } var isEnabled: Bool { get { return FileManager.default.fileExists(atPath: 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) } catch { print("Failed to enable autostart: \(error)") } } private func disable() { guard isEnabled else { return } do { try removePlist(launchAgentURL) } catch { print("Failed to disable autostart: \(error)") } } }