import Foundation // Bridge to invoke Prole Tools environment wrapper for status/restart. // Use the environment defined in env.sh and execute scripts via $PROLE_SERVICE // e.g. "$PROLE_SERVICE/init_port_forwards.sh " so we don't rely on PATH. enum PFScriptBridge { // Keep the last command description actually used, for display in the UI. private static var lastInvocation: String = "$PROLE_SERVICE/init_port_forwards.sh" /// Public accessor for display purposes. Shows the last invocation we used. static func invocationDescription() -> String { lastInvocation } @discardableResult static func restart() -> (code: Int32, out: String, err: String) { runScript(arg: "restart") } static func status() -> (code: Int32, out: String, err: String) { runScript(arg: "status") } static func dbStatus() -> (code: Int32, out: String, err: String) { runRawCommand(command: "kubectl cnpg status prole-db", description: "kubectl cnpg status prole-db") } static func cnpgStatus() -> (code: Int32, out: String, err: String) { runRawCommand(command: "kubectl cnpg status prole-db | head -10", description: "kubectl cnpg status prole-db | head -10") } static func openbaoStatus() -> (code: Int32, out: String, err: String) { // Check if the openbao pod is Ready in the default namespace (or wherever it's deployed) // We look for any pod with label app=openbao and check its ready status let cmd = "kubectl get pods -l app=openbao -o jsonpath='{.items[*].status.containerStatuses[*].ready}'" return runRawCommand(command: cmd, description: "kubectl check openbao ready") } private static func runRawCommand(command: String, description: String) -> (code: Int32, out: String, err: String) { lastInvocation = description let p = Process() p.executableURL = URL(fileURLWithPath: "/bin/bash") p.arguments = ["-lc", command] // Use home as default CWD let env = ProcessInfo.processInfo.environment let home = env["HOME"] ?? NSHomeDirectory() let proleHomeEnv = env["PROLE_HOME"] p.currentDirectoryURL = URL(fileURLWithPath: proleHomeEnv ?? home) Logger.shared.info("invoking raw: \(description)") let outPipe = Pipe(); let errPipe = Pipe() p.standardOutput = outPipe p.standardError = errPipe do { try p.run() } catch { let errStr = String(describing: error) Logger.shared.error("spawn error: \(errStr)") return (-1, "", errStr) } p.waitUntilExit() let out = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" let err = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" let code = p.terminationStatus return (code, out, err) } private static func runScript(arg: String) -> (code: Int32, out: String, err: String) { let fm = FileManager.default let env = ProcessInfo.processInfo.environment let home = env["HOME"] ?? NSHomeDirectory() let proleHomeEnv = env["PROLE_HOME"] // Candidate locations for env.sh let envCandidates: [String] = [ (proleHomeEnv != nil ? (proleHomeEnv! + "/env.sh") : nil), home + "/dev/prole/env.sh", ].compactMap { $0 } // Candidate locations for the script if we can't use env.sh let scriptCandidates: [String] = [ (proleHomeEnv != nil ? (proleHomeEnv! + "/etc/init_port_forwards.sh") : nil), home + "/dev/prole/etc/init_port_forwards.sh", ].compactMap { $0 } // Build the command using the best available method. var command: String var cwd: String = proleHomeEnv ?? home if let envPath = envCandidates.first(where: { fm.fileExists(atPath: $0) }) { // Use env.sh to populate PROLE_* and invoke via $PROLE_SERVICE command = "export PROLE_HOME=\"${PROLE_HOME:-$HOME}\"; . \"\(envPath)\"; \"${PROLE_SERVICE}/init_port_forwards.sh\" \(arg)" lastInvocation = ". \(envPath) && $PROLE_SERVICE/init_port_forwards.sh \(arg)" } else if let scriptPath = scriptCandidates.first(where: { fm.fileExists(atPath: $0) }) { // Fallback: directly execute the repo script command = "\"\(scriptPath)\" \(arg)" lastInvocation = scriptPath + " \(arg)" } else { // Nothing found; construct a failing but explicit command for diagnostics command = "echo '[prole-tools] env.sh and init_port_forwards.sh not found' 1>&2; exit 127" lastInvocation = " init_port_forwards.sh \(arg)" } let p = Process() p.executableURL = URL(fileURLWithPath: "/bin/bash") p.arguments = ["-lc", command] p.currentDirectoryURL = URL(fileURLWithPath: cwd) Logger.shared.info("invoking: \(lastInvocation)") let outPipe = Pipe(); let errPipe = Pipe() p.standardOutput = outPipe p.standardError = errPipe do { try p.run() } catch { let errStr = String(describing: error) Logger.shared.error("spawn error: \(errStr)") return (-1, "", errStr) } p.waitUntilExit() let out = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" let err = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" let code = p.terminationStatus // Combine to match UI exactly var combined = out if !err.isEmpty { combined += (combined.isEmpty ? "" : "\n") + err } let trimmedCombined = combined.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedCombined.isEmpty { Logger.shared.info("exit code: \(code); (no output)") } else { Logger.shared.info("output (exit=\(code)):\n\(combined)") } return (code, out, err) } }