mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-26 17:35:10 +00:00
feat(menubar): show CLI update banner when a newer version is available
This commit is contained in:
parent
a555c747d6
commit
af99516633
2 changed files with 98 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/re
|
|||
private let checkIntervalSeconds: TimeInterval = 2 * 24 * 60 * 60
|
||||
private let lastCheckKey = "UpdateChecker.lastCheckDate"
|
||||
private let cachedVersionKey = "UpdateChecker.latestVersion"
|
||||
private let cachedCliVersionKey = "UpdateChecker.latestCliVersion"
|
||||
private let updateTimeoutSeconds: UInt64 = 120
|
||||
private let maxUpdateStderrBytes = 64 * 1024
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ private final class LockedDataBuffer: @unchecked Sendable {
|
|||
@Observable
|
||||
final class UpdateChecker {
|
||||
var latestVersion: String?
|
||||
var latestCliVersion: String?
|
||||
var installedCliVersion: String?
|
||||
var isUpdating = false
|
||||
var updateError: String?
|
||||
|
||||
|
|
@ -40,15 +43,34 @@ final class UpdateChecker {
|
|||
return normalizedLatest.compare(normalizedCurrent, options: .numeric) == .orderedDescending
|
||||
}
|
||||
|
||||
var cliUpdateAvailable: Bool {
|
||||
guard let latest = latestCliVersion, let installed = installedCliVersion else { return false }
|
||||
let normalizedLatest = AppVersion.normalize(latest)
|
||||
let normalizedInstalled = AppVersion.normalize(installed)
|
||||
guard !normalizedInstalled.isEmpty else { return false }
|
||||
return normalizedLatest.compare(normalizedInstalled, options: .numeric) == .orderedDescending
|
||||
}
|
||||
|
||||
var cliUpdateCommand: String {
|
||||
let argv = CodeburnCLI.baseArgv()
|
||||
let path = argv.first ?? ""
|
||||
if path.contains("/homebrew/") { return "brew upgrade codeburn" }
|
||||
return "npm update -g codeburn"
|
||||
}
|
||||
|
||||
var currentVersion: String {
|
||||
AppVersion.normalizedBundleShortVersion
|
||||
}
|
||||
|
||||
func checkIfNeeded() async {
|
||||
if installedCliVersion == nil {
|
||||
installedCliVersion = Self.queryInstalledCliVersion()
|
||||
}
|
||||
let lastCheck = UserDefaults.standard.double(forKey: lastCheckKey)
|
||||
let now = Date().timeIntervalSince1970
|
||||
if now - lastCheck < checkIntervalSeconds {
|
||||
latestVersion = UserDefaults.standard.string(forKey: cachedVersionKey)
|
||||
latestCliVersion = UserDefaults.standard.string(forKey: cachedCliVersionKey)
|
||||
return
|
||||
}
|
||||
await check()
|
||||
|
|
@ -56,6 +78,9 @@ final class UpdateChecker {
|
|||
|
||||
func check() async {
|
||||
updateError = nil
|
||||
if installedCliVersion == nil {
|
||||
installedCliVersion = Self.queryInstalledCliVersion()
|
||||
}
|
||||
guard let url = URL(string: releasesAPI) else { return }
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = 30
|
||||
|
|
@ -77,15 +102,43 @@ final class UpdateChecker {
|
|||
.replacingOccurrences(of: "CodeBurnMenubar-", with: "")
|
||||
.replacingOccurrences(of: ".zip", with: "")
|
||||
|
||||
let cliVersion = Self.resolveLatestCliVersion(in: releases)
|
||||
|
||||
latestVersion = version
|
||||
latestCliVersion = cliVersion
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: lastCheckKey)
|
||||
UserDefaults.standard.set(version, forKey: cachedVersionKey)
|
||||
if let cliVersion { UserDefaults.standard.set(cliVersion, forKey: cachedCliVersionKey) }
|
||||
} catch {
|
||||
updateError = "Update check failed: \(error.localizedDescription)"
|
||||
NSLog("CodeBurn: update check failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func resolveLatestCliVersion(in releases: [GitHubRelease]) -> String? {
|
||||
for release in releases where release.tag_name.hasPrefix("v") && !release.tag_name.hasPrefix("mac-v") {
|
||||
return AppVersion.normalize(release.tag_name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
nonisolated static func queryInstalledCliVersion() -> String? {
|
||||
let process = CodeburnCLI.makeProcess(subcommand: ["--version"])
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = FileHandle.nullDevice
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
guard process.terminationStatus == 0 else { return nil }
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return output.isEmpty ? nil : output
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func resolveLatestMenubarRelease(in releases: [GitHubRelease]) -> (release: GitHubRelease, asset: GitHubAsset)? {
|
||||
for release in releases where release.tag_name.hasPrefix("mac-v") {
|
||||
guard let asset = release.assets.first(where: {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ struct MenuBarContent: View {
|
|||
|
||||
FooterBar()
|
||||
|
||||
CLIUpdateBanner()
|
||||
|
||||
StarBanner()
|
||||
}
|
||||
}
|
||||
|
|
@ -459,6 +461,49 @@ struct FlameMark: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct CLIUpdateBanner: View {
|
||||
@Environment(UpdateChecker.self) private var updateChecker
|
||||
|
||||
var body: some View {
|
||||
if updateChecker.cliUpdateAvailable {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
Text("CLI \(updateChecker.latestCliVersion ?? "") available")
|
||||
.font(.system(size: 10.5, weight: .medium))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Button {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(updateChecker.cliUpdateCommand, forType: .string)
|
||||
} label: {
|
||||
HStack(spacing: 3) {
|
||||
Text(updateChecker.cliUpdateCommand)
|
||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
||||
Image(systemName: "doc.on.doc")
|
||||
.font(.system(size: 8))
|
||||
}
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Copy update command to clipboard")
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.blue.opacity(0.06))
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color.secondary.opacity(0.18))
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let starBannerGitHubURL = URL(string: "https://github.com/getagentseal/codeburn")!
|
||||
|
||||
/// Shown at the very bottom on first launch. A small terracotta strip nudges users to star the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue