Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit af99516

Browse files
committed
feat(menubar): show CLI update banner when a newer version is available
1 parent a555c74 commit af99516

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/re
55
private let checkIntervalSeconds: TimeInterval = 2 * 24 * 60 * 60
66
private let lastCheckKey = "UpdateChecker.lastCheckDate"
77
private let cachedVersionKey = "UpdateChecker.latestVersion"
8+
private let cachedCliVersionKey = "UpdateChecker.latestCliVersion"
89
private let updateTimeoutSeconds: UInt64 = 120
910
private let maxUpdateStderrBytes = 64 * 1024
1011

@@ -28,6 +29,8 @@ private final class LockedDataBuffer: @unchecked Sendable {
2829
@Observable
2930
final class UpdateChecker {
3031
var latestVersion: String?
32+
var latestCliVersion: String?
33+
var installedCliVersion: String?
3134
var isUpdating = false
3235
var updateError: String?
3336

@@ -40,22 +43,44 @@ final class UpdateChecker {
4043
return normalizedLatest.compare(normalizedCurrent, options: .numeric) == .orderedDescending
4144
}
4245

46+
var cliUpdateAvailable: Bool {
47+
guard let latest = latestCliVersion, let installed = installedCliVersion else { return false }
48+
let normalizedLatest = AppVersion.normalize(latest)
49+
let normalizedInstalled = AppVersion.normalize(installed)
50+
guard !normalizedInstalled.isEmpty else { return false }
51+
return normalizedLatest.compare(normalizedInstalled, options: .numeric) == .orderedDescending
52+
}
53+
54+
var cliUpdateCommand: String {
55+
let argv = CodeburnCLI.baseArgv()
56+
let path = argv.first ?? ""
57+
if path.contains("/homebrew/") { return "brew upgrade codeburn" }
58+
return "npm update -g codeburn"
59+
}
60+
4361
var currentVersion: String {
4462
AppVersion.normalizedBundleShortVersion
4563
}
4664

4765
func checkIfNeeded() async {
66+
if installedCliVersion == nil {
67+
installedCliVersion = Self.queryInstalledCliVersion()
68+
}
4869
let lastCheck = UserDefaults.standard.double(forKey: lastCheckKey)
4970
let now = Date().timeIntervalSince1970
5071
if now - lastCheck < checkIntervalSeconds {
5172
latestVersion = UserDefaults.standard.string(forKey: cachedVersionKey)
73+
latestCliVersion = UserDefaults.standard.string(forKey: cachedCliVersionKey)
5274
return
5375
}
5476
await check()
5577
}
5678

5779
func check() async {
5880
updateError = nil
81+
if installedCliVersion == nil {
82+
installedCliVersion = Self.queryInstalledCliVersion()
83+
}
5984
guard let url = URL(string: releasesAPI) else { return }
6085
var request = URLRequest(url: url)
6186
request.timeoutInterval = 30
@@ -77,15 +102,43 @@ final class UpdateChecker {
77102
.replacingOccurrences(of: "CodeBurnMenubar-", with: "")
78103
.replacingOccurrences(of: ".zip", with: "")
79104

105+
let cliVersion = Self.resolveLatestCliVersion(in: releases)
106+
80107
latestVersion = version
108+
latestCliVersion = cliVersion
81109
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: lastCheckKey)
82110
UserDefaults.standard.set(version, forKey: cachedVersionKey)
111+
if let cliVersion { UserDefaults.standard.set(cliVersion, forKey: cachedCliVersionKey) }
83112
} catch {
84113
updateError = "Update check failed: \(error.localizedDescription)"
85114
NSLog("CodeBurn: update check failed: \(error)")
86115
}
87116
}
88117

118+
nonisolated static func resolveLatestCliVersion(in releases: [GitHubRelease]) -> String? {
119+
for release in releases where release.tag_name.hasPrefix("v") && !release.tag_name.hasPrefix("mac-v") {
120+
return AppVersion.normalize(release.tag_name)
121+
}
122+
return nil
123+
}
124+
125+
nonisolated static func queryInstalledCliVersion() -> String? {
126+
let process = CodeburnCLI.makeProcess(subcommand: ["--version"])
127+
let pipe = Pipe()
128+
process.standardOutput = pipe
129+
process.standardError = FileHandle.nullDevice
130+
do {
131+
try process.run()
132+
process.waitUntilExit()
133+
guard process.terminationStatus == 0 else { return nil }
134+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
135+
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
136+
return output.isEmpty ? nil : output
137+
} catch {
138+
return nil
139+
}
140+
}
141+
89142
nonisolated static func resolveLatestMenubarRelease(in releases: [GitHubRelease]) -> (release: GitHubRelease, asset: GitHubAsset)? {
90143
for release in releases where release.tag_name.hasPrefix("mac-v") {
91144
guard let asset = release.assets.first(where: {

mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ struct MenuBarContent: View {
8282

8383
FooterBar()
8484

85+
CLIUpdateBanner()
86+
8587
StarBanner()
8688
}
8789
}
@@ -459,6 +461,49 @@ struct FlameMark: View {
459461
}
460462
}
461463

464+
struct CLIUpdateBanner: View {
465+
@Environment(UpdateChecker.self) private var updateChecker
466+
467+
var body: some View {
468+
if updateChecker.cliUpdateAvailable {
469+
HStack(spacing: 6) {
470+
Image(systemName: "arrow.up.circle.fill")
471+
.font(.system(size: 10, weight: .semibold))
472+
.foregroundStyle(.blue)
473+
474+
Text("CLI \(updateChecker.latestCliVersion ?? "") available")
475+
.font(.system(size: 10.5, weight: .medium))
476+
.foregroundStyle(.primary)
477+
478+
Button {
479+
NSPasteboard.general.clearContents()
480+
NSPasteboard.general.setString(updateChecker.cliUpdateCommand, forType: .string)
481+
} label: {
482+
HStack(spacing: 3) {
483+
Text(updateChecker.cliUpdateCommand)
484+
.font(.system(size: 10, weight: .medium, design: .monospaced))
485+
Image(systemName: "doc.on.doc")
486+
.font(.system(size: 8))
487+
}
488+
.foregroundStyle(.blue)
489+
}
490+
.buttonStyle(.plain)
491+
.help("Copy update command to clipboard")
492+
493+
Spacer(minLength: 0)
494+
}
495+
.padding(.horizontal, 12)
496+
.padding(.vertical, 6)
497+
.background(Color.blue.opacity(0.06))
498+
.overlay(alignment: .top) {
499+
Rectangle()
500+
.fill(Color.secondary.opacity(0.18))
501+
.frame(height: 0.5)
502+
}
503+
}
504+
}
505+
}
506+
462507
private let starBannerGitHubURL = URL(string: "https://github.com/getagentseal/codeburn")!
463508

464509
/// Shown at the very bottom on first launch. A small terracotta strip nudges users to star the

0 commit comments

Comments
 (0)