diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index dd31cb9..d6c1cd0 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -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" diff --git a/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift b/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift index e85dc08..0d87a34 100644 --- a/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift +++ b/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift @@ -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 diff --git a/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift b/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift index 44f52b5..63084d9 100644 --- a/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift +++ b/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift @@ -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: "")) + } }