diff --git a/supacode/Clients/Github/GithubCLIClient.swift b/supacode/Clients/Github/GithubCLIClient.swift index ebae8d6d..c8446573 100644 --- a/supacode/Clients/Github/GithubCLIClient.swift +++ b/supacode/Clients/Github/GithubCLIClient.swift @@ -249,6 +249,17 @@ nonisolated private func escapeGraphQLString(_ value: String) -> String { .replacing("\t", with: "\\t") } +nonisolated private func isOutdatedGitHubCLI(_ error: ShellClientError) -> Bool { + let combined = "\(error.stdout)\n\(error.stderr)".lowercased() + if combined.contains("unknown flag: --json") { + return true + } + if combined.contains("unknown shorthand flag") && combined.contains("json") { + return true + } + return false +} + nonisolated private func runGh( shell: ShellClient, arguments: [String], @@ -261,6 +272,9 @@ nonisolated private func runGh( return try await shell.runLogin(env, ["gh"] + arguments, repoRoot, log: shouldLog).stdout } catch { if let shellError = error as? ShellClientError { + if isOutdatedGitHubCLI(shellError) { + throw GithubCLIError.outdated + } let message = shellError.errorDescription ?? "Command failed: \(command)" throw GithubCLIError.commandFailed(message) } diff --git a/supacode/Clients/Github/GithubCLIError.swift b/supacode/Clients/Github/GithubCLIError.swift index 5a5e7964..ab0e6f4f 100644 --- a/supacode/Clients/Github/GithubCLIError.swift +++ b/supacode/Clients/Github/GithubCLIError.swift @@ -2,12 +2,15 @@ import Foundation nonisolated enum GithubCLIError: LocalizedError, Equatable { case unavailable + case outdated case commandFailed(String) var errorDescription: String? { switch self { case .unavailable: return "GitHub CLI is unavailable" + case .outdated: + return "GitHub CLI is outdated. Update to the latest version." case .commandFailed(let message): return message } diff --git a/supacode/Features/Settings/Views/GithubSettingsView.swift b/supacode/Features/Settings/Views/GithubSettingsView.swift index 0f94129a..08a5beb7 100644 --- a/supacode/Features/Settings/Views/GithubSettingsView.swift +++ b/supacode/Features/Settings/Views/GithubSettingsView.swift @@ -6,6 +6,7 @@ final class GithubSettingsViewModel { enum State: Equatable { case loading case unavailable + case outdated case notAuthenticated case authenticated(username: String, host: String) case error(String) @@ -33,6 +34,15 @@ final class GithubSettingsViewModel { } else { state = .notAuthenticated } + } catch let error as GithubCLIError { + switch error { + case .outdated: + state = .outdated + case .unavailable: + state = .unavailable + case .commandFailed(let message): + state = .error(message) + } } catch { state = .error(error.localizedDescription) } @@ -81,6 +91,15 @@ struct GithubSettingsView: View { .font(.callout) } + case .outdated: + VStack(alignment: .leading, spacing: 8) { + Label("GitHub CLI outdated", systemImage: "exclamationmark.triangle") + .foregroundStyle(.orange) + Text("Update GitHub CLI to the latest version to use GitHub integration.") + .foregroundStyle(.secondary) + .font(.callout) + } + case .authenticated(let username, let host): LabeledContent("Signed in as") { Text(username) @@ -104,7 +123,8 @@ struct GithubSettingsView: View { } .formStyle(.grouped) - if case .unavailable = viewModel.state { + switch viewModel.state { + case .unavailable: HStack { Button("Get GitHub CLI") { NSWorkspace.shared.open(URL(string: "https://cli.github.com")!) @@ -113,6 +133,17 @@ struct GithubSettingsView: View { Spacer() } .padding(.top) + case .outdated: + HStack { + Button("Update GitHub CLI") { + NSWorkspace.shared.open(URL(string: "https://cli.github.com")!) + } + .help("Open GitHub CLI website") + Spacer() + } + .padding(.top) + default: + EmptyView() } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)