// // Cask.swift // Brewer // // Created by Cédric MAS on 08/07/2024. // import Foundation enum ShellError: Error { case emptyOutput } @discardableResult func shell(_ command: String) throws -> String { let task = Process() task.executableURL = URL(filePath: "/bin/bash") task.arguments = ["-c", command] let pipe = Pipe() task.standardError = pipe task.standardOutput = pipe try task.run() if let data = try pipe.fileHandleForReading.readToEnd(), let output = String(data: data, encoding: .utf8) { return output } throw ShellError.emptyOutput } struct Cask: Codable { let token: String let fullToken: String let tap: String let name: [String] let desc: String let homepage: String let url: String let version: String let installed: String? let outdated: Bool enum CodingKeys: String, CodingKey { case token, tap, name, desc, homepage, url, version, installed, outdated case fullToken = "full_token" } } struct Formulae: Codable { struct Version: Codable { let stable: String let head: String? let bottle: Bool enum CodingKeys: String, CodingKey { case stable case head, bottle } } let name: String let fullName: String let tap: String let oldNames: [String] let aliases: [String] let versionedFormulae: [String] let desc: String let license: String? let homepage: String let versions: Version let outdated: Bool enum CodingKeys: String, CodingKey { case name case tap case aliases case desc case license case homepage case versions case outdated case fullName = "full_name" case oldNames = "oldnames" case versionedFormulae = "versioned_formulae" } } struct InfoResponse: Codable { let formulae: [Formulae] let casks: [Cask] enum CodingKeys: String, CodingKey { case casks case formulae } } @Observable class Homebrew { var data: InfoResponse? var isLoading = false var errorMessage: String? func getInfo(on query: String) { self.isLoading = true self.errorMessage = nil self.data = nil Task { [weak self] in do { let res = try shell( "/opt/homebrew/bin/brew info --json=v2 \(query)") if let data = res.data(using: .utf8) { let output = try JSONDecoder().decode( InfoResponse.self, from: data) self?.data = output } } catch { self?.errorMessage = error.localizedDescription } self?.isLoading = false } } func getInstalled() { self.isLoading = true self.data = nil self.errorMessage = nil Task { [weak self] in do { let res = try shell( "/opt/homebrew/bin/brew info --json=v2 --installed") if let data = res.data(using: .utf8) { let output = try JSONDecoder().decode( InfoResponse.self, from: data) self?.data = output } } catch { self?.errorMessage = error.localizedDescription } self?.isLoading = false } } func isDownloaded(_ name: String) async -> Bool { self.isLoading = true self.errorMessage = nil self.data = nil let task = Task { [weak self] in do { let res = try shell( "/opt/homebrew/bin/brew list -1 \(name) >/dev/null 2>&1; echo $?" ) .trimmingCharacters(in: .whitespacesAndNewlines) self?.isLoading = false return res == "0" } catch { self?.errorMessage = error.localizedDescription } return false } switch await task.result { case .success(let success): return success case .failure(let fail): return false } } func install(_ fullToken: String, isCask: Bool = false) async -> Bool { self.isLoading = true self.data = nil let task = Task { [weak self] in do { try shell( "/opt/homebrew/bin/brew install \(isCask ? "--cask" : "") \(fullToken)" ) } catch { self?.errorMessage = error.localizedDescription } self?.isLoading = false return await self?.isDownloaded(fullToken) ?? false } return switch await task.result { case .success(let success): success case .failure(let fail): false } } func uninstall(_ fullToken: String) async -> Bool { self.isLoading = true self.data = nil let task = Task { [weak self] in do { let res = try shell( "/opt/homebrew/bin/brew uninstall \(fullToken); echo $?" ) .trimmingCharacters(in: .whitespacesAndNewlines) } catch { self?.errorMessage = error.localizedDescription } self?.isLoading = false return await !(self?.isDownloaded(fullToken) ?? true) } return switch await task.result { case .success(let success): success case .failure(let failure): false } } }