136 lines
5.6 KiB
Swift
136 lines
5.6 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
enum PreflightStatus {
|
|
case pass
|
|
case warning(String)
|
|
case fail(String)
|
|
}
|
|
|
|
struct PreflightCheck: Identifiable {
|
|
let id = UUID()
|
|
let name: String
|
|
let detail: String
|
|
let status: PreflightStatus
|
|
|
|
var icon: String {
|
|
switch status {
|
|
case .pass: return "checkmark.circle.fill"
|
|
case .warning: return "exclamationmark.triangle.fill"
|
|
case .fail: return "xmark.circle.fill"
|
|
}
|
|
}
|
|
var color: Color {
|
|
switch status {
|
|
case .pass: return .green
|
|
case .warning: return .orange
|
|
case .fail: return .red
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PreflightReport {
|
|
let checks: [PreflightCheck]
|
|
var canProceed: Bool {
|
|
!checks.contains { if case .fail = $0.status { return true }; return false }
|
|
}
|
|
var hasWarnings: Bool {
|
|
checks.contains { if case .warning = $0.status { return true }; return false }
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class BuildPreflightChecker {
|
|
private let config: DaemonConfiguration
|
|
private let buildService: BuildService
|
|
|
|
init(config: DaemonConfiguration, buildService: BuildService) {
|
|
self.config = config; self.buildService = buildService
|
|
}
|
|
|
|
func run(project: SavedProject?, device: ConnectedDevice?, action: String) async -> PreflightReport {
|
|
var checks: [PreflightCheck] = []
|
|
checks.append(await checkDaemon())
|
|
checks.append(checkProject(project))
|
|
checks.append(checkTeamID())
|
|
checks.append(checkDevice(device, action: action))
|
|
if let p = project { checks.append(await checkPath(p)) }
|
|
checks.append(checkNoBuildRunning())
|
|
return PreflightReport(checks: checks)
|
|
}
|
|
|
|
private func checkDaemon() async -> PreflightCheck {
|
|
guard let url = URL(string: "\(config.baseURL)/health") else {
|
|
return PreflightCheck(name: "Daemon", detail: "Invalid URL",
|
|
status: .fail("Daemon URL malformed. Check Settings."))
|
|
}
|
|
var req = URLRequest(url: url); req.timeoutInterval = 3
|
|
do {
|
|
let (_, resp) = try await URLSession.shared.data(for: req)
|
|
guard (resp as? HTTPURLResponse)?.statusCode == 200 else {
|
|
return PreflightCheck(name: "Daemon", detail: "Bad response",
|
|
status: .fail("Daemon not responding correctly."))
|
|
}
|
|
return PreflightCheck(name: "Daemon", detail: "Connected to \(config.baseURL)", status: .pass)
|
|
} catch {
|
|
return PreflightCheck(name: "Daemon", detail: error.localizedDescription,
|
|
status: .fail("Cannot reach \(config.baseURL). Check Tailscale."))
|
|
}
|
|
}
|
|
|
|
private func checkProject(_ project: SavedProject?) -> PreflightCheck {
|
|
guard let p = project else {
|
|
return PreflightCheck(name: "Project", detail: "None selected",
|
|
status: .fail("Select a project before building."))
|
|
}
|
|
return PreflightCheck(name: "Project", detail: "\(p.name) — \(p.scheme)", status: .pass)
|
|
}
|
|
|
|
private func checkTeamID() -> PreflightCheck {
|
|
let t = config.developmentTeam.trimmingCharacters(in: .whitespaces)
|
|
if t.isEmpty {
|
|
return PreflightCheck(name: "Team ID", detail: "Not set",
|
|
status: .fail("Enter your Team ID in Settings → Build."))
|
|
}
|
|
let valid = t.count == 10 && t.allSatisfy({ $0.isLetter || $0.isNumber }) && t == t.uppercased()
|
|
return PreflightCheck(name: "Team ID", detail: t,
|
|
status: valid ? .pass : .warning("Format looks wrong — should be 10 uppercase alphanumerics."))
|
|
}
|
|
|
|
private func checkDevice(_ device: ConnectedDevice?, action: String) -> PreflightCheck {
|
|
if action == "build" {
|
|
return PreflightCheck(name: "Device", detail: "Build only", status: .pass)
|
|
}
|
|
guard let d = device else {
|
|
return PreflightCheck(name: "Device", detail: "None selected",
|
|
status: .fail("Select a paired device to deploy."))
|
|
}
|
|
return PreflightCheck(name: "Device",
|
|
detail: "\(d.name) · iOS \(d.osVersion)",
|
|
status: d.isNetworkConnected ? .pass :
|
|
.warning("Device paired but may not be on network."))
|
|
}
|
|
|
|
private func checkPath(_ project: SavedProject) async -> PreflightCheck {
|
|
let encoded = project.rootPath.urlEncoded
|
|
guard let url = URL(string: "\(config.baseURL)/files/tree?path=\(encoded)") else {
|
|
return PreflightCheck(name: "Project Path", detail: project.rootPath, status: .pass)
|
|
}
|
|
var req = URLRequest(url: url); req.timeoutInterval = 5
|
|
do {
|
|
let (_, resp) = try await URLSession.shared.data(for: req)
|
|
let code = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
|
return PreflightCheck(name: "Project Path", detail: project.rootPath,
|
|
status: code == 200 ? .pass : .fail("Path not found on Mac."))
|
|
} catch {
|
|
return PreflightCheck(name: "Project Path", detail: "Check timed out", status: .warning("Could not verify path."))
|
|
}
|
|
}
|
|
|
|
private func checkNoBuildRunning() -> PreflightCheck {
|
|
buildService.isBuilding
|
|
? PreflightCheck(name: "Build State", detail: "Already running",
|
|
status: .fail("Wait for current build to finish."))
|
|
: PreflightCheck(name: "Build State", detail: "Ready", status: .pass)
|
|
}
|
|
}
|