import ArgumentParser import Foundation import RunnerCore import RunnerHost /// `gitea-macos-runner doctor` — verify the host before anything else. /// /// Every check corresponds to a failure that would otherwise show up as an /// opaque error deep inside a VM boot: wrong architecture, unsigned binary, /// locked keychain, non-admin Gitea token, dead download URL. Run this first, /// and again after `service install`. struct DoctorCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "doctor", abstract: "Check that this host can build and run macOS guests." ) @OptionGroup var options: GlobalOptions /// Emit machine-readable JSON instead of aligned text. @Flag(name: .long, help: "Emit results as JSON.") var json: Bool = false /// By default `doctor` exits non-zero when any check fails, so it can gate a /// setup script. This makes it always exit zero. @Flag(name: .customLong("no-fail"), help: "Exit zero even when checks fail.") var noFail: Bool = false func run() async throws { // Deliberately does not use options.loadConfig(): a broken or missing // config is exactly the state doctor exists to diagnose, so it is // reported as a check rather than thrown as an error. let checks = await Doctor.runChecks(configPath: options.configPath) if json { let payload: [[String: Any]] = checks.map { check in var entry: [String: Any] = [ "name": check.name, "result": check.result.label, "detail": check.detail, "blocking": check.isBlocking, ] if let remediation = check.remediation { entry["remediation"] = remediation } return entry } let data = try JSONSerialization.data( withJSONObject: payload, options: [.prettyPrinted, .sortedKeys] ) print(String(data: data, encoding: .utf8) ?? "[]") } else { print(Doctor.format(checks)) } if !noFail, checks.contains(where: \.isBlocking) { throw ExitCode(1) } } }