mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-30 03:32:36 +00:00
GitHub asset name includes a v prefix (v0.8.0) while CFBundleShortVersionString does not (0.8.0). Strip the prefix before comparing. Also capture stderr on update failure so the button doesn't hang on "Updating..." forever.
103 lines
3.7 KiB
Swift
103 lines
3.7 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/releases/latest"
|
|
private let checkIntervalSeconds: TimeInterval = 2 * 24 * 60 * 60
|
|
private let lastCheckKey = "UpdateChecker.lastCheckDate"
|
|
private let cachedVersionKey = "UpdateChecker.latestVersion"
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class UpdateChecker {
|
|
var latestVersion: String?
|
|
var isUpdating = false
|
|
var updateError: String?
|
|
|
|
var updateAvailable: Bool {
|
|
guard let latest = latestVersion else { return false }
|
|
let current = currentVersion
|
|
let normalizedLatest = latest.hasPrefix("v") ? String(latest.dropFirst()) : latest
|
|
let normalizedCurrent = current.hasPrefix("v") ? String(current.dropFirst()) : current
|
|
return !normalizedCurrent.isEmpty && normalizedCurrent != "dev" && normalizedLatest != normalizedCurrent
|
|
}
|
|
|
|
var currentVersion: String {
|
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
|
}
|
|
|
|
func checkIfNeeded() async {
|
|
let lastCheck = UserDefaults.standard.double(forKey: lastCheckKey)
|
|
let now = Date().timeIntervalSince1970
|
|
if now - lastCheck < checkIntervalSeconds {
|
|
latestVersion = UserDefaults.standard.string(forKey: cachedVersionKey)
|
|
return
|
|
}
|
|
await check()
|
|
}
|
|
|
|
func check() async {
|
|
guard let url = URL(string: releasesAPI) else { return }
|
|
var request = URLRequest(url: url)
|
|
request.setValue("codeburn-menubar-updater", forHTTPHeaderField: "User-Agent")
|
|
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
|
|
|
|
do {
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
|
let release = try JSONDecoder().decode(GitHubRelease.self, from: data)
|
|
guard let asset = release.assets.first(where: {
|
|
$0.name.hasPrefix("CodeBurnMenubar-") && $0.name.hasSuffix(".zip")
|
|
}) else { return }
|
|
|
|
let version = asset.name
|
|
.replacingOccurrences(of: "CodeBurnMenubar-", with: "")
|
|
.replacingOccurrences(of: ".zip", with: "")
|
|
|
|
latestVersion = version
|
|
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: lastCheckKey)
|
|
UserDefaults.standard.set(version, forKey: cachedVersionKey)
|
|
} catch {
|
|
NSLog("CodeBurn: update check failed: \(error)")
|
|
}
|
|
}
|
|
|
|
func performUpdate() {
|
|
isUpdating = true
|
|
updateError = nil
|
|
|
|
let process = CodeburnCLI.makeProcess(subcommand: ["menubar", "--force"])
|
|
let errPipe = Pipe()
|
|
process.standardOutput = FileHandle.nullDevice
|
|
process.standardError = errPipe
|
|
|
|
process.terminationHandler = { [weak self] proc in
|
|
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
let stderr = String(data: errData, encoding: .utf8) ?? ""
|
|
Task { @MainActor in
|
|
guard let self else { return }
|
|
if proc.terminationStatus != 0 {
|
|
self.isUpdating = false
|
|
self.updateError = stderr.isEmpty ? "Update failed (exit \(proc.terminationStatus))" : stderr
|
|
NSLog("CodeBurn: update failed (exit \(proc.terminationStatus)): \(stderr)")
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
try process.run()
|
|
} catch {
|
|
isUpdating = false
|
|
updateError = error.localizedDescription
|
|
NSLog("CodeBurn: update spawn failed: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct GitHubRelease: Decodable {
|
|
let tag_name: String
|
|
let assets: [GitHubAsset]
|
|
}
|
|
|
|
private struct GitHubAsset: Decodable {
|
|
let name: String
|
|
let browser_download_url: String
|
|
}
|