import Foundation public struct PythonRuntime: Equatable { public let workingDirectory: URL public let python: URL } public func parsePythonRuntimeConfig(_ content: String) -> [String: String] { var values: [String: String] = [:] for rawLine in content.components(separatedBy: .newlines) { let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines) guard !line.isEmpty, !line.hasPrefix("#") else { continue } let parts = line.split(separator: "=", maxSplits: 1).map(String.init) guard parts.count == 2 else { continue } let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) var value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines) if value.count >= 2, ((value.hasPrefix("\"") && value.hasSuffix("\"")) || (value.hasPrefix("'") && value.hasSuffix("'"))) { value.removeFirst() value.removeLast() } guard key == "REYNA_CLI_DIR" || key == "REYNA_CLI_PYTHON" else { continue } values[key] = value } return values } public func pythonRuntimeConfig(homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> [String: String] { let paths = [ homeDirectory.appendingPathComponent(".reyna-cli.env"), homeDirectory.appendingPathComponent(".config/reyna-cli/env"), ] var result: [String: String] = [:] for path in paths { guard let content = try? String(contentsOf: path, encoding: .utf8) else { continue } result.merge(parsePythonRuntimeConfig(content)) { _, newest in newest } } return result } public func resolvePythonRuntime( homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, environment: [String: String] = ProcessInfo.processInfo.environment, config: [String: String]? = nil, isExecutable: (String) -> Bool = { FileManager.default.isExecutableFile(atPath: $0) } ) -> PythonRuntime { let loadedConfig = config ?? pythonRuntimeConfig(homeDirectory: homeDirectory) let defaultWorkDirectory = homeDirectory.appendingPathComponent("Projects/reyna-cli").path let workDirectoryPath = environment["REYNA_CLI_DIR"] ?? loadedConfig["REYNA_CLI_DIR"] ?? defaultWorkDirectory let selectedPython = environment["REYNA_CLI_PYTHON"] ?? loadedConfig["REYNA_CLI_PYTHON"] ?? URL(fileURLWithPath: workDirectoryPath).appendingPathComponent(".venv/bin/python").path let pythonPath = isExecutable(selectedPython) ? selectedPython : "/usr/bin/python3" return PythonRuntime( workingDirectory: URL(fileURLWithPath: workDirectoryPath), python: URL(fileURLWithPath: pythonPath) ) } public func pythonCommand(runtime: PythonRuntime, forwardedArguments: [String]) -> [String] { [runtime.python.path, "-m", "reyna_cli.cli"] + forwardedArguments } public func pythonLaunchEnvironment(from environment: [String: String] = ProcessInfo.processInfo.environment) -> [String: String] { var sanitized = environment for key in ["PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV"] { sanitized.removeValue(forKey: key) } sanitized["PYTHONNOUSERSITE"] = "1" return sanitized } public func runPythonCLI(forwardedArguments: [String]) -> Int32 { let runtime = resolvePythonRuntime() let command = pythonCommand(runtime: runtime, forwardedArguments: forwardedArguments) let process = Process() process.executableURL = runtime.python process.arguments = Array(command.dropFirst()) process.currentDirectoryURL = runtime.workingDirectory process.environment = pythonLaunchEnvironment() do { try process.run() process.waitUntilExit() return process.terminationStatus } catch { fputs("error: failed to run Reyna CLI Python: \(error)\n", stderr) return 127 } }