mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-27 09:55:13 +00:00
fix(menubar): clarify update failure status
This commit is contained in:
parent
2f8e5bddcd
commit
4e9c3a1dfe
3 changed files with 97 additions and 4 deletions
|
|
@ -14,6 +14,35 @@ private let maxUpdateStderrBytes = 64 * 1024
|
|||
// `menubar --force`, so we refuse to run them and ask the user to upgrade the CLI first.
|
||||
private let minCliVersionForUpdate = "0.9.9"
|
||||
|
||||
enum UpdateFailureStage: Equatable {
|
||||
case check
|
||||
case cliUpdate
|
||||
case menubarUpdate
|
||||
|
||||
var badgeLabel: String {
|
||||
switch self {
|
||||
case .check: "Update Check Failed"
|
||||
case .cliUpdate: "CLI Update Failed"
|
||||
case .menubarUpdate: "Menubar Update Failed"
|
||||
}
|
||||
}
|
||||
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .check: "CodeBurn could not check GitHub for updates."
|
||||
case .cliUpdate: "CodeBurn could not update the CLI."
|
||||
case .menubarUpdate: "CodeBurn could not update the menubar app."
|
||||
}
|
||||
}
|
||||
|
||||
var retryHelp: String {
|
||||
switch self {
|
||||
case .check: "Click to retry the update check."
|
||||
case .cliUpdate, .menubarUpdate: "Click to retry the update."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LockedDataBuffer: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var data = Data()
|
||||
|
|
@ -38,6 +67,19 @@ final class UpdateChecker {
|
|||
var installedCliVersion: String?
|
||||
var isUpdating = false
|
||||
var updateError: String?
|
||||
var updateFailureStage: UpdateFailureStage?
|
||||
|
||||
var updateBadgeLabel: String {
|
||||
if isUpdating { return "Updating..." }
|
||||
return updateFailureStage?.badgeLabel ?? "Update"
|
||||
}
|
||||
|
||||
var updateHelpText: String {
|
||||
guard let error = updateError, let stage = updateFailureStage else {
|
||||
return "Update the CLI and menubar to the latest release"
|
||||
}
|
||||
return "\(stage.summary)\n\n\(error)\n\n\(stage.retryHelp)"
|
||||
}
|
||||
|
||||
var updateAvailable: Bool {
|
||||
guard let latest = latestVersion else { return false }
|
||||
|
|
@ -88,6 +130,7 @@ final class UpdateChecker {
|
|||
|
||||
func check() async {
|
||||
updateError = nil
|
||||
updateFailureStage = nil
|
||||
installedCliVersion = Self.queryInstalledCliVersion()
|
||||
guard let url = URL(string: releasesAPI) else { return }
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -118,7 +161,8 @@ final class UpdateChecker {
|
|||
UserDefaults.standard.set(version, forKey: cachedVersionKey)
|
||||
if let cliVersion { UserDefaults.standard.set(cliVersion, forKey: cachedCliVersionKey) }
|
||||
} catch {
|
||||
updateError = "Update check failed: \(error.localizedDescription)"
|
||||
updateFailureStage = .check
|
||||
updateError = error.localizedDescription
|
||||
NSLog("CodeBurn: update check failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
|
@ -195,9 +239,11 @@ final class UpdateChecker {
|
|||
if cliUpdateAvailable || cliTooOldForUpdate {
|
||||
isUpdating = true
|
||||
updateError = nil
|
||||
updateFailureStage = nil
|
||||
let cliPath = CodeburnCLI.baseArgv().first ?? ""
|
||||
guard let argv = Self.cliUpdateInvocation(cliPath: cliPath), let bin = argv.first else {
|
||||
isUpdating = false
|
||||
updateFailureStage = .cliUpdate
|
||||
updateError = "Could not find the package manager for \(cliPath.isEmpty ? "the CLI" : cliPath). Run \u{201C}\(cliUpdateCommand)\u{201D} manually, then try again."
|
||||
return
|
||||
}
|
||||
|
|
@ -209,6 +255,7 @@ final class UpdateChecker {
|
|||
guard let self else { return }
|
||||
if status != 0 {
|
||||
self.isUpdating = false
|
||||
self.updateFailureStage = .cliUpdate
|
||||
self.updateError = stderr.isEmpty ? "CLI update failed (exit \(status))" : stderr
|
||||
NSLog("CodeBurn: CLI update failed (exit \(status)): \(stderr)")
|
||||
return
|
||||
|
|
@ -263,11 +310,13 @@ final class UpdateChecker {
|
|||
func performUpdate() {
|
||||
installedCliVersion = Self.queryInstalledCliVersion()
|
||||
if cliTooOldForUpdate {
|
||||
updateFailureStage = .menubarUpdate
|
||||
updateError = "Your codeburn CLI (\(AppVersion.display(installedCliVersion ?? ""))) is too old to update the menubar. Run “\(cliUpdateCommand)” first, then try again."
|
||||
return
|
||||
}
|
||||
isUpdating = true
|
||||
updateError = nil
|
||||
updateFailureStage = nil
|
||||
|
||||
let process = CodeburnCLI.makeProcess(subcommand: ["menubar", "--force"])
|
||||
let errPipe = Pipe()
|
||||
|
|
@ -297,6 +346,7 @@ final class UpdateChecker {
|
|||
guard let self else { return }
|
||||
self.isUpdating = false
|
||||
if proc.terminationStatus != 0 {
|
||||
self.updateFailureStage = .menubarUpdate
|
||||
self.updateError = stderr.isEmpty ? "Update failed (exit \(proc.terminationStatus))" : stderr
|
||||
NSLog("CodeBurn: update failed (exit \(proc.terminationStatus)): \(stderr)")
|
||||
} else {
|
||||
|
|
@ -309,6 +359,7 @@ final class UpdateChecker {
|
|||
try process.run()
|
||||
} catch {
|
||||
isUpdating = false
|
||||
updateFailureStage = .menubarUpdate
|
||||
updateError = error.localizedDescription
|
||||
NSLog("CodeBurn: update spawn failed: \(error)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,7 +520,9 @@ private struct UpdateBadge: View {
|
|||
|
||||
var body: some View {
|
||||
Button {
|
||||
if updateChecker.updateAvailable || updateChecker.cliUpdateAvailable {
|
||||
if updateChecker.updateFailureStage == .check {
|
||||
Task { await updateChecker.check() }
|
||||
} else if updateChecker.updateAvailable || updateChecker.cliUpdateAvailable {
|
||||
updateChecker.performFullUpdate()
|
||||
} else {
|
||||
Task { await updateChecker.check() }
|
||||
|
|
@ -538,7 +540,7 @@ private struct UpdateBadge: View {
|
|||
Image(systemName: "arrow.down.circle.fill")
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
Text(updateChecker.isUpdating ? "Updating..." : (updateChecker.updateError == nil ? "Update" : "Failed"))
|
||||
Text(updateChecker.updateBadgeLabel)
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
|
@ -548,7 +550,9 @@ private struct UpdateBadge: View {
|
|||
.tint(Theme.brandAccent)
|
||||
.controlSize(.mini)
|
||||
.disabled(updateChecker.isUpdating)
|
||||
.help(updateChecker.updateError ?? "Update the CLI and menubar to the latest release")
|
||||
.help(updateChecker.updateHelpText)
|
||||
.accessibilityLabel(updateChecker.updateBadgeLabel)
|
||||
.accessibilityHint(updateChecker.updateHelpText)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,44 @@ struct UpdateCheckerTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Suite("Update failure presentation")
|
||||
@MainActor
|
||||
struct UpdateFailurePresentationTests {
|
||||
@Test("identifies an automatic update check failure")
|
||||
func updateCheckFailure() {
|
||||
let checker = UpdateChecker()
|
||||
checker.updateFailureStage = .check
|
||||
checker.updateError = "GitHub returned HTTP 403."
|
||||
|
||||
#expect(checker.updateBadgeLabel == "Update Check Failed")
|
||||
#expect(checker.updateHelpText.contains("could not check GitHub for updates"))
|
||||
#expect(checker.updateHelpText.contains("GitHub returned HTTP 403."))
|
||||
#expect(checker.updateHelpText.contains("retry the update check"))
|
||||
}
|
||||
|
||||
@Test("distinguishes CLI update failures")
|
||||
func cliUpdateFailure() {
|
||||
let checker = UpdateChecker()
|
||||
checker.updateFailureStage = .cliUpdate
|
||||
checker.updateError = "npm exited with status 1."
|
||||
|
||||
#expect(checker.updateBadgeLabel == "CLI Update Failed")
|
||||
#expect(checker.updateHelpText.contains("could not update the CLI"))
|
||||
#expect(checker.updateHelpText.contains("npm exited with status 1."))
|
||||
}
|
||||
|
||||
@Test("distinguishes menubar update failures")
|
||||
func menubarUpdateFailure() {
|
||||
let checker = UpdateChecker()
|
||||
checker.updateFailureStage = .menubarUpdate
|
||||
checker.updateError = "Checksum mismatch."
|
||||
|
||||
#expect(checker.updateBadgeLabel == "Menubar Update Failed")
|
||||
#expect(checker.updateHelpText.contains("could not update the menubar app"))
|
||||
#expect(checker.updateHelpText.contains("Checksum mismatch."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - one-click full update: package-manager resolution
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue