Merge pull request #593 from getagentseal/fix/menubar-min-cli-gate

fix(menubar): refuse menubar updates from CLIs older than 0.9.9
This commit is contained in:
Resham Joshi 2026-07-02 05:31:31 +02:00 committed by GitHub
commit 1e859ffabc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 1 deletions

View file

@ -971,7 +971,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
alert.alertStyle = .warning
} else if updateChecker.updateAvailable, let latest = updateChecker.latestVersion {
alert.messageText = "Update Available"
alert.informativeText = "\(AppVersion.display(latest)) is available (you have \(AppVersion.display(updateChecker.currentVersion))). Run:\n\ncodeburn menubar --force"
let header = "\(AppVersion.display(latest)) is available (you have \(AppVersion.display(updateChecker.currentVersion)))."
if updateChecker.cliTooOldForUpdate {
alert.informativeText = "\(header) Your codeburn CLI is too old to install it. First run:\n\n\(updateChecker.cliUpdateCommand)\n\nthen:\n\ncodeburn menubar --force"
} else {
alert.informativeText = "\(header) Run:\n\ncodeburn menubar --force"
}
alert.alertStyle = .informational
} else {
alert.messageText = "Up to Date"

View file

@ -8,6 +8,11 @@ private let cachedVersionKey = "UpdateChecker.latestVersion"
private let cachedCliVersionKey = "UpdateChecker.latestCliVersion"
private let updateTimeoutSeconds: UInt64 = 120
private let maxUpdateStderrBytes = 64 * 1024
// The installer that scans `mac-v*` releases for the menubar zip (instead of
// `/releases/latest`, which can resolve to a CLI release that carries no menubar
// asset) landed in CLI 0.9.9 (commit 909efcf). Older CLIs cannot perform a correct
// `menubar --force`, so we refuse to run them and ask the user to upgrade the CLI first.
private let minCliVersionForUpdate = "0.9.9"
private final class LockedDataBuffer: @unchecked Sendable {
private let lock = NSLock()
@ -51,6 +56,13 @@ final class UpdateChecker {
return normalizedLatest.compare(normalizedInstalled, options: .numeric) == .orderedDescending
}
/// True when the installed CLI predates the `menubar --force` fix and would fail to
/// install the new app. Distinct from `cliUpdateAvailable`: a CLI can be behind the
/// latest release yet still new enough (>= 0.9.9) to update the menubar correctly.
var cliTooOldForUpdate: Bool {
Self.isCliTooOld(installed: installedCliVersion)
}
var cliUpdateCommand: String {
let argv = CodeburnCLI.baseArgv()
let path = argv.first ?? ""
@ -146,7 +158,19 @@ final class UpdateChecker {
return nil
}
nonisolated static func isCliTooOld(installed: String?) -> Bool {
guard let installed else { return false }
let normalizedInstalled = AppVersion.normalize(installed)
guard !normalizedInstalled.isEmpty else { return false }
return AppVersion.normalize(minCliVersionForUpdate).compare(normalizedInstalled, options: .numeric) == .orderedDescending
}
func performUpdate() {
installedCliVersion = Self.queryInstalledCliVersion()
if cliTooOldForUpdate {
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

View file

@ -36,4 +36,25 @@ struct UpdateCheckerTests {
#expect(UpdateChecker.resolveLatestMenubarRelease(in: releases) == nil)
}
@Test("flags CLI older than the menubar-update fix as too old")
func flagsCliBelowMinimumAsTooOld() {
#expect(UpdateChecker.isCliTooOld(installed: "0.9.8"))
#expect(UpdateChecker.isCliTooOld(installed: "v0.9.8"))
#expect(UpdateChecker.isCliTooOld(installed: "0.8.12"))
}
@Test("accepts CLI at or above the menubar-update fix version")
func acceptsCliAtOrAboveMinimum() {
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.9"))
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.10"))
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.14"))
#expect(!UpdateChecker.isCliTooOld(installed: "1.0.0"))
}
@Test("does not flag when the CLI version is unknown")
func ignoresUnknownCliVersion() {
#expect(!UpdateChecker.isCliTooOld(installed: nil))
#expect(!UpdateChecker.isCliTooOld(installed: ""))
}
}