From 256511618d9fb0a6d11eb8e163a0f778d58c4d2e Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Thu, 2 Jul 2026 05:30:19 +0200 Subject: [PATCH] fix(menubar): refuse menubar updates from CLIs older than 0.9.9 CLIs before 0.9.9 resolve /releases/latest for menubar --force, which can point at a CLI-only release with no menubar asset, so the update they run installs nothing. The update dialog and performUpdate now detect an installed CLI older than the installer fix (909efcf, first shipped in 0.9.9) and direct the user to upgrade the CLI first, with the exact command, instead of suggesting a command that cannot work. Unknown CLI versions are not flagged. --- mac/Sources/CodeBurnMenubar/CodeBurnApp.swift | 7 +++++- .../CodeBurnMenubar/Data/UpdateChecker.swift | 24 +++++++++++++++++++ .../UpdateCheckerTests.swift | 21 ++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) 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: "")) + } }