import Foundation /// Bridges the SwiftUI app to the Python engine via the `admin/models_api.py` /// JSON bridge. The app launches the script as a subprocess and reads /// newline-delimited JSON on stdout (JSON-lines), so long-running commands can /// stream progress. enum PythonBridge { /// Resolve the Python interpreter + script path. /// Prefers `uv run` so extras are available; falls back to `python3`. static var scriptURL: URL { // Search order: cwd + ancestors, the app's own bundle root, home, and // some common mount points (the repo often lives under /Volumes/...). let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) var searchRoots: [URL] = [] var dir = cwd for _ in 0..<8 { searchRoots.append(dir); dir.deleteLastPathComponent() } if let bundle = Bundle.main.resourceURL { searchRoots.append(bundle) } searchRoots.append(URL(fileURLWithPath: NSHomeDirectory())) searchRoots.append(URL(fileURLWithPath: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/G-ART")) for root in searchRoots { let candidate = root.appendingPathComponent("admin/models_api.py") if FileManager.default.fileExists(atPath: candidate.path) { return candidate } } // Fall back to cwd. return cwd.appendingPathComponent("admin/models_api.py") } /// Project root containing `admin/`. static var projectRoot: URL { scriptURL.deletingLastPathComponent().deletingLastPathComponent() } static var pythonCommand: [String] { // Prefer `uv run python` (has extras); fall back to `python3`. if let uv = findUV() { return [uv, "run", "python", scriptURL.path] } return ["python3", scriptURL.path] } static var workingDirectory: URL { projectRoot } /// Locate `uv` on PATH or in common install locations. private static func findUV() -> String? { if let onPath = which("uv") { return onPath } let candidates = [ "/usr/local/bin/uv", "/opt/homebrew/bin/uv", "/opt/local/bin/uv", "\(NSHomeDirectory())/.local/bin/uv", "\(NSHomeDirectory())/.cargo/bin/uv", "/Library/Frameworks/Python.framework/Versions/3.14/bin/uv", ] for c in candidates where FileManager.default.isExecutableFile(atPath: c) { return c } return nil } private static func which(_ name: String) -> String? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = ["which", name] let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() do { try process.run() } catch { return nil } process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) return (path?.isEmpty ?? true) ? nil : path } // MARK: - Streaming JSON-lines /// Runs a command and collects every parsed JSON document on stdout. /// `onProgress` is called for each document (used for download progress). static func run(command: String, arguments: [String: Any] = [:], onLine: @escaping ([String: Any]) -> Void, completion: @escaping (Int, String?) -> Void) { var cmd = pythonCommand cmd.append(command) if !arguments.isEmpty, let data = try? JSONSerialization.data(withJSONObject: arguments), let str = String(data: data, encoding: .utf8) { cmd.append(str) } let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = cmd process.currentDirectoryURL = workingDirectory let pipe = Pipe() process.standardOutput = pipe let errPipe = Pipe() process.standardError = errPipe var stderrBuffer = "" var stdoutBuffer = "" let outputLock = NSLock() errPipe.fileHandleForReading.readabilityHandler = { handle in let data = handle.availableData if let s = String(data: data, encoding: .utf8) { stderrBuffer += s } } pipe.fileHandleForReading.readabilityHandler = { handle in let data = handle.availableData guard !data.isEmpty, let s = String(data: data, encoding: .utf8) else { return } outputLock.lock() stdoutBuffer += s let parts = stdoutBuffer.split(separator: "\n", omittingEmptySubsequences: false) stdoutBuffer = String(parts.last ?? "") let lines = parts.dropLast() outputLock.unlock() for line in lines { if let obj = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] { onLine(obj) } } } process.terminationHandler = { p in pipe.fileHandleForReading.readabilityHandler = nil errPipe.fileHandleForReading.readabilityHandler = nil outputLock.lock() let finalLine = stdoutBuffer.trimmingCharacters(in: .whitespacesAndNewlines) stdoutBuffer = "" outputLock.unlock() if !finalLine.isEmpty, let obj = try? JSONSerialization.jsonObject(with: Data(finalLine.utf8)) as? [String: Any] { onLine(obj) } let error = p.terminationStatus == 0 ? nil : (stderrBuffer.isEmpty ? "O comando \(command) falhou (código \(p.terminationStatus))." : stderrBuffer) completion(Int(p.terminationStatus), error) } do { try process.run() } catch { completion(1, error.localizedDescription) } } // MARK: - Convenience: single JSON result /// Runs a command and delivers the first parsed JSON document as the result. static func call(command: String, arguments: [String: Any] = [:], completion: @escaping ([String: Any]?, String?) -> Void) { var result: [String: Any]? run(command: command, arguments: arguments, onLine: { obj in if result == nil { result = obj } }, completion: { code, err in completion(result, err) }) } }