@@ -8,6 +8,16 @@ enum SpritesCLIAuth {
88 case unknown, checking, authenticated, notAuthenticated
99}
1010
11+ enum ClaudeCodeVersionStatus : Equatable {
12+ case unknown
13+ case checking
14+ case upToDate( version: String )
15+ case updateAvailable( current: String , latest: String )
16+ case updating
17+ case updateFailed( error: String )
18+ case failed
19+ }
20+
1121@Observable
1222@MainActor
1323final class SpriteOverviewViewModel {
@@ -19,6 +29,7 @@ final class SpriteOverviewViewModel {
1929 var isAuthenticatingGitHub = false
2030 var spritesCLIAuthStatus : SpritesCLIAuth = . unknown
2131 var isAuthenticatingSprites = false
32+ var claudeCodeVersionStatus : ClaudeCodeVersionStatus = . unknown
2233 var errorMessage : String ?
2334 var isUploading = false
2435 var uploadResult : SpritesAPIClient . FileUploadResponse ?
@@ -195,6 +206,74 @@ final class SpriteOverviewViewModel {
195206 }
196207 }
197208
209+ func checkClaudeCodeVersion( apiClient: SpritesAPIClient ) async {
210+ claudeCodeVersionStatus = . checking
211+
212+ async let execResult = apiClient. runExec (
213+ spriteName: sprite. name,
214+ command: " claude --version 2>/dev/null || echo CLAUDE_NOT_FOUND "
215+ )
216+ async let latestVersion = fetchNpmLatestVersion ( )
217+
218+ let ( output, success) = await execResult
219+ let npmVersion = await latestVersion
220+
221+ if !success || output. contains ( " CLAUDE_NOT_FOUND " ) || output. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty {
222+ claudeCodeVersionStatus = . failed
223+ return
224+ }
225+
226+ let installed = output. trimmingCharacters ( in: . whitespacesAndNewlines)
227+
228+ if let npm = npmVersion, let installedSemver = extractSemver ( from: installed) {
229+ if installedSemver == npm {
230+ claudeCodeVersionStatus = . upToDate( version: installed)
231+ } else {
232+ claudeCodeVersionStatus = . updateAvailable( current: installed, latest: npm)
233+ }
234+ } else {
235+ claudeCodeVersionStatus = . upToDate( version: installed)
236+ }
237+ }
238+
239+ func updateClaudeCode( apiClient: SpritesAPIClient ) async {
240+ claudeCodeVersionStatus = . updating
241+ let ( output, success) = await apiClient. runExec (
242+ spriteName: sprite. name,
243+ command: " claude update 2>&1 && claude --version " ,
244+ timeout: 120
245+ )
246+ if success {
247+ let lines = output. trimmingCharacters ( in: . whitespacesAndNewlines) . split ( separator: " \n " )
248+ if let lastLine = lines. last {
249+ let newInstalled = String ( lastLine) . trimmingCharacters ( in: . whitespacesAndNewlines)
250+ let npmVersion = await fetchNpmLatestVersion ( )
251+ if let npm = npmVersion, let semver = extractSemver ( from: newInstalled) , semver == npm {
252+ claudeCodeVersionStatus = . upToDate( version: newInstalled)
253+ } else {
254+ claudeCodeVersionStatus = . upToDate( version: newInstalled)
255+ }
256+ } else {
257+ claudeCodeVersionStatus = . updateFailed( error: " Update succeeded but could not read version " )
258+ }
259+ } else {
260+ claudeCodeVersionStatus = . updateFailed( error: " Update failed " )
261+ }
262+ }
263+
264+ private func fetchNpmLatestVersion( ) async -> String ? {
265+ guard let url = URL ( string: " https://registry.npmjs.org/@anthropic-ai/claude-code/latest " ) else { return nil }
266+ guard let ( data, _) = try ? await URLSession . shared. data ( from: url) else { return nil }
267+ struct NpmResponse : Decodable { let version : String }
268+ return try ? JSONDecoder ( ) . decode ( NpmResponse . self, from: data) . version
269+ }
270+
271+ private func extractSemver( from string: String ) -> String ? {
272+ let pattern = #"\d+\.\d+\.\d+"#
273+ return string. range ( of: pattern, options: . regularExpression)
274+ . map { String ( string [ $0] ) }
275+ }
276+
198277 func authenticateSprites( apiClient: SpritesAPIClient ) async {
199278 guard let token = apiClient. spritesToken else { return }
200279 isAuthenticatingSprites = true
0 commit comments