PadXcode-Daemon/Daemon/DaemonStartupValidator.swift
2026-04-12 00:42:51 -07:00

135 lines
4.6 KiB
Swift

import Foundation
// MARK: - Result type (Codable so /system/validate can return JSON)
struct StartupCheckResult: Codable {
let name: String
let passed: Bool
let message: String
}
// MARK: - Validator
enum DaemonStartupValidator {
static func run() -> [StartupCheckResult] {
[
checkXcode(),
checkXcodebuild(),
checkDevicectl(),
checkSourcekitLSP(),
checkTeamID(),
checkSigningIdentity(),
checkXcodeGen(),
]
}
// MARK: - Checks
private static func checkXcode() -> StartupCheckResult {
let exists = FileManager.default.fileExists(atPath: "/Applications/Xcode.app")
return StartupCheckResult(
name: "Xcode Installation",
passed: exists,
message: exists
? "Xcode found at /Applications/Xcode.app"
: "Xcode not found — install from developer.apple.com or the App Store"
)
}
private static func checkXcodebuild() -> StartupCheckResult {
let result = shell("xcodebuild -version 2>/dev/null | head -1")
let passed = result.contains("Xcode")
return StartupCheckResult(
name: "xcodebuild",
passed: passed,
message: passed
? result.trimmingCharacters(in: .whitespacesAndNewlines)
: "xcodebuild not functional — run: xcode-select --install"
)
}
private static func checkDevicectl() -> StartupCheckResult {
let result = shell("xcrun devicectl --version 2>/dev/null")
let passed = !result.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
return StartupCheckResult(
name: "devicectl",
passed: passed,
message: passed ? "devicectl available" : "devicectl not found — requires Xcode 15+"
)
}
private static func checkSourcekitLSP() -> StartupCheckResult {
let path = "/Applications/Xcode.app/Contents/Developer/Toolchains/" +
"XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp"
let exists = FileManager.default.fileExists(atPath: path)
return StartupCheckResult(
name: "sourcekit-lsp",
passed: exists,
message: exists ? "sourcekit-lsp found" : "sourcekit-lsp not found — completions unavailable"
)
}
private static func checkTeamID() -> StartupCheckResult {
let teamID = loadTeamID()
let valid = teamID.count == 10
&& teamID != "YOUR_10_CHAR_TEAM_ID"
&& teamID.allSatisfy({ $0.isLetter || $0.isNumber })
return StartupCheckResult(
name: "Team ID",
passed: valid,
message: valid
? "Team ID configured: \(teamID)"
: "Team ID not set — edit ~/.padxcode/config.json"
)
}
private static func checkSigningIdentity() -> StartupCheckResult {
let result = shell(
"security find-identity -v -p codesigning 2>/dev/null | grep 'Apple Development'"
)
let passed = !result.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let certName = result
.components(separatedBy: "\"")
.dropFirst()
.first ?? "certificate present"
return StartupCheckResult(
name: "Apple Development Certificate",
passed: passed,
message: passed
? "Found: \(certName)"
: "No Apple Development certificate — Xcode → Settings → Accounts → Manage Certificates"
)
}
private static func checkXcodeGen() -> StartupCheckResult {
let result = shell("which xcodegen 2>/dev/null")
.trimmingCharacters(in: .whitespacesAndNewlines)
let passed = !result.isEmpty
return StartupCheckResult(
name: "XcodeGen",
passed: passed,
message: passed
? "XcodeGen at \(result)"
: "XcodeGen not found — brew install xcodegen"
)
}
// MARK: - Shell helper
@discardableResult
private static func shell(_ command: String) -> String {
let p = Process()
p.executableURL = URL(fileURLWithPath: "/bin/zsh")
p.arguments = ["-lc", command]
let pipe = Pipe()
p.standardOutput = pipe
p.standardError = Pipe()
try? p.run()
p.waitUntilExit()
return String(
data: pipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
) ?? ""
}
}