import Foundation // Lightweight helper to manage the user LaunchAgent that runs kubectl port-forwards. // This replaces the in-app PortForwardManager supervision. final class LaunchAgentManager { // Filenames/paths private let label = "org.prole.prole-db.kpf-dev" private var plistURL: URL { let home = FileManager.default.homeDirectoryForCurrentUser return home.appendingPathComponent("Library/LaunchAgents/\(label).plist") } // Deprecated: helper script is no longer bundled. All shell commands must go via $PROLE_HOME/env.sh. // Leaving the placeholder to avoid breaking callers; it is unused now. private var helperScriptURL: URL { let home = FileManager.default.homeDirectoryForCurrentUser return home.appendingPathComponent("Library/Application Support/Prole/bin/prole-kpf.sh") } // Key that stores the list of commands inside the plist (array of strings) private let commandsKey = "ProleCommands" // Read commands from the LaunchAgent plist. Returns empty if missing. func readCommands() -> [String] { guard let data = try? Data(contentsOf: plistURL) else { return [] } var format = PropertyListSerialization.PropertyListFormat.xml guard let obj = try? PropertyListSerialization.propertyList(from: data, options: [], format: &format), let dict = obj as? [String: Any], let cmds = dict[commandsKey] as? [String] else { return [] } return cmds } // Write commands back to the plist, preserving other keys if present. func writeCommands(_ commands: [String]) { var dict: [String: Any] = [:] if let data = try? Data(contentsOf: plistURL), let obj = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil), let existing = obj as? [String: Any] { dict = existing } dict[commandsKey] = commands if let out = try? PropertyListSerialization.data(fromPropertyList: dict, format: .xml, options: 0) { // Ensure parent directory exists try? FileManager.default.createDirectory(at: plistURL.deletingLastPathComponent(), withIntermediateDirectories: true) try? out.write(to: plistURL) } } // Reload the LaunchAgent: try kickstart; if not present, bootstrap then kickstart. func resetAgent() { let uid = getuid() let domainTarget = "gui/\(uid)/\(label)" // Try kickstart first (reload) let ks = run("/bin/launchctl", ["kickstart", "-k", domainTarget]) if ks.exitCode == 0 { return } // If kickstart failed, try bootout then bootstrap, then kickstart again _ = run("/bin/launchctl", ["bootout", "gui/\(uid)", domainTarget]) // bootstrap requires path to plist _ = run("/bin/launchctl", ["bootstrap", "gui/\(uid)", plistURL.path]) _ = run("/bin/launchctl", ["kickstart", "-k", domainTarget]) } // Deprecated: No-op. We no longer bundle or copy any init-port-forwards.sh; app uses $PROLE_HOME/env.sh. func ensureHelperScript() { /* no-op */ } static let defaultHelperScript = "" // MARK: - Helpers @discardableResult private func run(_ path: String, _ args: [String]) -> (exitCode: Int32, out: String, err: String) { let p = Process() p.executableURL = URL(fileURLWithPath: path) p.arguments = args let outPipe = Pipe(); let errPipe = Pipe() p.standardOutput = outPipe p.standardError = errPipe do { try p.run() } catch { return (exitCode: -1, out: "", err: String(describing: error)) } p.waitUntilExit() let outData = outPipe.fileHandleForReading.readDataToEndOfFile() let errData = errPipe.fileHandleForReading.readDataToEndOfFile() let outStr = String(data: outData, encoding: .utf8) ?? "" let errStr = String(data: errData, encoding: .utf8) ?? "" return (p.terminationStatus, outStr, errStr) } }