@@ -5,6 +5,7 @@ private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/re
55private let checkIntervalSeconds : TimeInterval = 2 * 24 * 60 * 60
66private let lastCheckKey = " UpdateChecker.lastCheckDate "
77private let cachedVersionKey = " UpdateChecker.latestVersion "
8+ private let cachedCliVersionKey = " UpdateChecker.latestCliVersion "
89private let updateTimeoutSeconds : UInt64 = 120
910private let maxUpdateStderrBytes = 64 * 1024
1011
@@ -28,6 +29,8 @@ private final class LockedDataBuffer: @unchecked Sendable {
2829@Observable
2930final 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: {
0 commit comments