feat: migrate Mac mini services into Reyna CLI

This commit is contained in:
Adolfo Reyna
2026-08-16 08:24:45 -04:00
parent 9fd04b0ce4
commit 032bc3a580
26 changed files with 4216 additions and 315 deletions
@@ -22,6 +22,7 @@
C0000000000000000000000D /* RemindersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000011 /* RemindersProvider.swift */; };
C0000000000000000000000E /* RemindersAuthorizationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000012 /* RemindersAuthorizationProvider.swift */; };
C00000000000000000000011 /* SystemInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000015 /* SystemInfoProvider.swift */; };
C00000000000000000000012 /* PythonLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000016 /* PythonLauncher.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -43,6 +44,7 @@
B00000000000000000000011 /* RemindersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersProvider.swift; sourceTree = "<group>"; };
B00000000000000000000012 /* RemindersAuthorizationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersAuthorizationProvider.swift; sourceTree = "<group>"; };
B00000000000000000000015 /* SystemInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfoProvider.swift; sourceTree = "<group>"; };
B00000000000000000000016 /* PythonLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PythonLauncher.swift; sourceTree = "<group>"; };
B0000000000000000000000D /* Reyna CLI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Reyna CLI.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -117,6 +119,7 @@
B00000000000000000000011 /* RemindersProvider.swift */,
B00000000000000000000012 /* RemindersAuthorizationProvider.swift */,
B00000000000000000000015 /* SystemInfoProvider.swift */,
B00000000000000000000016 /* PythonLauncher.swift */,
B00000000000000000000008 /* SocketPathValidation.swift */,
B00000000000000000000009 /* SocketServer.swift */,
);
@@ -209,6 +212,7 @@
C0000000000000000000000D /* RemindersProvider.swift in Sources */,
C0000000000000000000000E /* RemindersAuthorizationProvider.swift in Sources */,
C00000000000000000000011 /* SystemInfoProvider.swift in Sources */,
C00000000000000000000012 /* PythonLauncher.swift in Sources */,
C00000000000000000000006 /* SocketPathValidation.swift in Sources */,
C00000000000000000000007 /* SocketServer.swift in Sources */,
C00000000000000000000008 /* CSignalSupport.c in Sources */,
@@ -5,6 +5,19 @@ import Foundation
// Headless design: socket-server or stdin JSON-lines mode only.
public func runReynaCLIHost(arguments: [String] = CommandLine.arguments) -> Never {
let hasSocket = arguments.contains("--socket")
let hasPython = arguments.contains("--python")
if hasSocket && hasPython {
fputs("error: --python cannot be combined with --socket\n", stderr)
Darwin.exit(2)
}
if let idx = arguments.firstIndex(of: "--python") {
let forwardedArguments = Array(arguments.dropFirst(idx + 1))
Darwin.exit(runPythonCLI(forwardedArguments: forwardedArguments))
}
if let idx = arguments.firstIndex(of: "--socket") {
let nextIdx = idx + 1
guard nextIdx < arguments.count else {
@@ -0,0 +1,90 @@
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
}
}
@@ -0,0 +1,74 @@
import XCTest
@testable import ReynaCLIHostCore
final class PythonLauncherTests: XCTestCase {
func testConfiguredRuntimeUsesEnvironmentBeforeConfigAndDefaults() throws {
let home = URL(fileURLWithPath: "/tmp/reyna-home")
let config = [
"REYNA_CLI_DIR": "/config/repo",
"REYNA_CLI_PYTHON": "/config/python",
]
let environment = [
"REYNA_CLI_DIR": "/environment/repo",
"REYNA_CLI_PYTHON": "/environment/python",
]
let runtime = resolvePythonRuntime(
homeDirectory: home,
environment: environment,
config: config,
isExecutable: { path in path == "/environment/python" }
)
XCTAssertEqual(runtime.workingDirectory.path, "/environment/repo")
XCTAssertEqual(runtime.python.path, "/environment/python")
}
func testConfiguredRuntimeFallsBackToSystemPythonWhenSelectedPythonMissing() throws {
let runtime = resolvePythonRuntime(
homeDirectory: URL(fileURLWithPath: "/tmp/reyna-home"),
environment: [:],
config: ["REYNA_CLI_DIR": "/config/repo", "REYNA_CLI_PYTHON": "/missing/python"],
isExecutable: { _ in false }
)
XCTAssertEqual(runtime.workingDirectory.path, "/config/repo")
XCTAssertEqual(runtime.python.path, "/usr/bin/python3")
}
func testPythonCommandRunsFixedCliModuleAndForwardsArguments() {
let runtime = PythonRuntime(
workingDirectory: URL(fileURLWithPath: "/repo"),
python: URL(fileURLWithPath: "/repo/.venv/bin/python")
)
XCTAssertEqual(
pythonCommand(runtime: runtime, forwardedArguments: ["local-services", "speech", "generate", "hello"]),
["/repo/.venv/bin/python", "-m", "reyna_cli.cli", "local-services", "speech", "generate", "hello"]
)
}
func testPythonEnvironmentRemovesParentInterpreterOverrides() {
let environment = pythonLaunchEnvironment(from: [
"PATH": "/bin",
"PYTHONHOME": "/wrong",
"PYTHONPATH": "/wrong/site-packages",
"VIRTUAL_ENV": "/wrong/.venv",
"KEEP": "yes",
])
XCTAssertNil(environment["PYTHONHOME"])
XCTAssertNil(environment["PYTHONPATH"])
XCTAssertNil(environment["VIRTUAL_ENV"])
XCTAssertEqual(environment["PYTHONNOUSERSITE"], "1")
XCTAssertEqual(environment["KEEP"], "yes")
}
func testConfigParserIgnoresCommentsBlankLinesAndQuotes() {
let config = parsePythonRuntimeConfig("# comment\nREYNA_CLI_DIR = '/repo path'\n\nREYNA_CLI_PYTHON=\"/python\"\ninvalid\n")
XCTAssertEqual(config["REYNA_CLI_DIR"], "/repo path")
XCTAssertEqual(config["REYNA_CLI_PYTHON"], "/python")
XCTAssertNil(config["invalid"])
}
}