-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance changelog summarization with security analysis #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
86c9329
821f2e1
83d9c22
ff153b3
7f7aec5
5fe6f84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import Foundation | ||
|
|
||
| enum Brew { | ||
| static func caskIsOutdated(_ cask: String) -> Bool { | ||
| let command = "brew outdated --cask --greedy --quiet | grep -x \(shellQuote(cask))" | ||
| return run(command).exitCode == 0 | ||
| } | ||
|
|
||
| static func guessCask(from appName: String) -> String { | ||
| return appName.lowercased() | ||
| .replacingOccurrences(of: " ", with: "-") | ||
| .replacingOccurrences(of: ".", with: "") | ||
| } | ||
|
|
||
| static func getCaskInfo(_ cask: String) -> CaskInfo? { | ||
| let result = run("brew info --cask \(shellQuote(cask)) --json") | ||
| guard result.exitCode == 0, | ||
| let data = result.output.data(using: .utf8) else { | ||
| return nil | ||
| } | ||
|
|
||
| do { | ||
| let casks = try JSONDecoder().decode([CaskInfo].self, from: data) | ||
| return casks.first | ||
| } catch { | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| static func updateCask(_ cask: String) -> Bool { | ||
| let result = run("brew upgrade --cask \(shellQuote(cask))") | ||
| return result.exitCode == 0 | ||
| } | ||
|
|
||
| static func isBrewInstalled() -> Bool { | ||
| return run("command -v brew").exitCode == 0 | ||
| } | ||
|
|
||
| struct CaskInfo: Decodable { | ||
| let token: String | ||
| let full_name: String | ||
| let tap: String | ||
| let version: String | ||
| let installed: String? | ||
| let outdated: Bool | ||
| let homepage: String? | ||
| let url: String | ||
| let name: [String] | ||
| let desc: String? | ||
| } | ||
|
Comment on lines
+39
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The properties in struct CaskInfo: Decodable {
let token: String
let fullName: String
let tap: String
let version: String
let installed: String?
let outdated: Bool
let homepage: String?
let url: String
let name: [String]
let desc: String?
enum CodingKeys: String, CodingKey {
case token, tap, version, installed, outdated, homepage, url, name, desc
case fullName = "full_name"
}
} |
||
|
|
||
| private static func run(_ command: String) -> (exitCode: Int32, output: String) { | ||
| let task = Process() | ||
| task.launchPath = "/bin/zsh" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| task.arguments = ["-lc", command] | ||
|
|
||
| let pipe = Pipe() | ||
| task.standardOutput = pipe | ||
| task.standardError = pipe | ||
|
|
||
| do { | ||
| try task.run() | ||
| task.waitUntilExit() | ||
|
|
||
| let data = pipe.fileHandleForReading.readDataToEndOfFile() | ||
| let output = String(data: data, encoding: .utf8) ?? "" | ||
|
|
||
| return (task.terminationStatus, output) | ||
| } catch { | ||
| return (-1, "") | ||
| } | ||
| } | ||
|
|
||
| private static func shellQuote(_ string: String) -> String { | ||
| return "'\(string.replacingOccurrences(of: "'", with: "'\\''"))'" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct GitHub { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct Release: Decodable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let tag_name: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let name: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let body: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let draft: Bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let prerelease: Bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let published_at: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let assets: [Asset] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct Asset: Decodable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let name: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let browser_download_url: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let content_type: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let size: Int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The properties in
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static func latest(owner: String, repo: String, token: String? = nil) async throws -> Release { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var request = URLRequest(url: URL(string: "https://api.github.com/repos/\(owner)/\(repo)/releases/latest")!) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Force-unwrapping the URL with
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("AutoUp/1.0", forHTTPHeaderField: "User-Agent") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let token = token { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let (data, response) = try await URLSession.shared.data(for: request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let httpResponse = response as? HTTPURLResponse else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw URLError(.badServerResponse) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if httpResponse.statusCode == 403 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw GitHubError.rateLimited | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard httpResponse.statusCode == 200 else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw GitHubError.apiError(httpResponse.statusCode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return try JSONDecoder().decode(Release.self, from: data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When decoding the JSON, you can set the let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(Release.self, from: data) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static func releases(owner: String, repo: String, count: Int = 10, token: String? = nil) async throws -> [Release] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var request = URLRequest(url: URL(string: "https://api.github.com/repos/\(owner)/\(repo)/releases?per_page=\(count)")!) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Force-unwrapping the URL with guard let url = URL(string: "https://api.github.com/repos/\(owner)/\(repo)/releases?per_page=\(count)") else {
throw GitHubError.invalidRepo
}
var request = URLRequest(url: url) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("AutoUp/1.0", forHTTPHeaderField: "User-Agent") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let token = token { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let (data, response) = try await URLSession.shared.data(for: request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let httpResponse = response as? HTTPURLResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpResponse.statusCode == 200 else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw URLError(.badServerResponse) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error handling here is less specific than in the guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if httpResponse.statusCode == 403 {
throw GitHubError.rateLimited
}
guard httpResponse.statusCode == 200 else {
throw GitHubError.apiError(httpResponse.statusCode)
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return try JSONDecoder().decode([Release].self, from: data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enum GitHubError: LocalizedError { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case rateLimited | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case apiError(Int) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case invalidRepo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var errorDescription: String? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch self { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .rateLimited: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "GitHub API rate limit exceeded" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .apiError(let code): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "GitHub API error: \(code)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .invalidRepo: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Invalid GitHub repository" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import Foundation | ||
|
|
||
| enum InstallerError: LocalizedError { | ||
| case noAppFound | ||
| case dmgAttachFailed(Int32) | ||
| case pkgInstallFailed(Int32) | ||
| case codesignFailed | ||
| case backupFailed | ||
|
|
||
| var errorDescription: String? { | ||
| switch self { | ||
| case .noAppFound: | ||
| return "Couldn't find the app in the download" | ||
| case .dmgAttachFailed(let code): | ||
| return "DMG mount failed with code \(code)" | ||
| case .pkgInstallFailed(let code): | ||
| return "PKG install failed with code \(code)" | ||
| case .codesignFailed: | ||
| return "App signature verification failed" | ||
| case .backupFailed: | ||
| return "Couldn't backup current version" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum Installer { | ||
| static func installZIP(from zipURL: URL, toApplications name: String, bundleID: String, currentVersion: String) throws { | ||
| // Create backup first | ||
| let currentAppPath = "/Applications/\(name).app" | ||
| if FileManager.default.fileExists(atPath: currentAppPath) { | ||
| _ = try? SecurityChecks.backup(appPath: currentAppPath, bundleID: bundleID, version: currentVersion) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignoring a failed backup with do {
_ = try SecurityChecks.backup(appPath: currentAppPath, bundleID: bundleID, version: currentVersion)
} catch {
throw InstallerError.backupFailed
} |
||
| } | ||
|
|
||
| let tmp = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) | ||
| try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) | ||
| defer { try? FileManager.default.removeItem(at: tmp) } | ||
|
|
||
| _ = try run("/usr/bin/unzip", ["-qq", zipURL.path, "-d", tmp.path]) | ||
| let app = try findApp(in: tmp) | ||
|
|
||
| // Verify codesign before installing | ||
| guard SecurityChecks.verifyCodeSign(app.path) else { | ||
| throw InstallerError.codesignFailed | ||
| } | ||
|
|
||
| try moveToApplications(app) | ||
| } | ||
|
|
||
| static func installDMG(from dmgURL: URL, bundleID: String, currentVersion: String) throws { | ||
| // Create backup first | ||
| let apps = try? FileManager.default.contentsOfDirectory(atPath: "/Applications") | ||
| let currentAppPath = apps?.first { $0.hasSuffix(".app") && Bundle(path: "/Applications/\($0)")?.bundleIdentifier == bundleID } | ||
|
|
||
| if let appPath = currentAppPath { | ||
| let fullPath = "/Applications/\(appPath)" | ||
| _ = try? SecurityChecks.backup(appPath: fullPath, bundleID: bundleID, version: currentVersion) | ||
| } | ||
|
|
||
| let (code, out) = try run("/usr/bin/hdiutil", ["attach", "-nobrowse", "-quiet", dmgURL.path]) | ||
| guard code == 0 else { | ||
| throw InstallerError.dmgAttachFailed(code) | ||
| } | ||
|
|
||
| guard let mount = out.split(separator: "\t").last.map(String.init) else { | ||
| throw InstallerError.dmgAttachFailed(-1) | ||
| } | ||
|
Comment on lines
+64
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsing the output of |
||
|
|
||
| defer { _ = try? run("/usr/bin/hdiutil", ["detach", "-quiet", mount]) } | ||
|
|
||
| let app = try findApp(in: URL(fileURLWithPath: mount)) | ||
|
|
||
| // Verify codesign before installing | ||
| guard SecurityChecks.verifyCodeSign(app.path) else { | ||
| throw InstallerError.codesignFailed | ||
| } | ||
|
|
||
| try moveToApplications(app) | ||
| } | ||
|
|
||
| static func installPKG(from pkgURL: URL) throws { | ||
| let (code, _) = try run("/usr/sbin/installer", ["-pkg", pkgURL.path, "-target", "/"]) | ||
| guard code == 0 else { | ||
| throw InstallerError.pkgInstallFailed(code) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Private Helpers | ||
|
|
||
| private static func findApp(in dir: URL) throws -> URL { | ||
| let items = try FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) | ||
| if let app = items.first(where: { $0.pathExtension == "app" }) { | ||
| return app | ||
| } | ||
|
|
||
| // Recursive search in case of subfolders | ||
| for url in items where url.hasDirectoryPath { | ||
| if let app = try? findApp(in: url) { | ||
| return app | ||
| } | ||
| } | ||
|
|
||
| throw InstallerError.noAppFound | ||
| } | ||
|
|
||
| private static func moveToApplications(_ src: URL) throws { | ||
| let dst = URL(fileURLWithPath: "/Applications").appendingPathComponent(src.lastPathComponent) | ||
|
|
||
| if FileManager.default.fileExists(atPath: dst.path) { | ||
| try FileManager.default.removeItem(at: dst) | ||
| } | ||
|
|
||
| try FileManager.default.copyItem(at: src, to: dst) | ||
|
|
||
| // Remove quarantine if present | ||
| _ = SecurityChecks.removeQuarantine(dst.path) | ||
| } | ||
|
|
||
| @discardableResult | ||
| private static func run(_ bin: String, _ args: [String]) throws -> (Int32, String) { | ||
| let process = Process() | ||
| process.executableURL = URL(fileURLWithPath: bin) | ||
| process.arguments = args | ||
|
|
||
| let pipe = Pipe() | ||
| process.standardOutput = pipe | ||
| process.standardError = pipe | ||
|
|
||
| try process.run() | ||
| process.waitUntilExit() | ||
|
|
||
| let data = pipe.fileHandleForReading.readDataToEndOfFile() | ||
| let output = String(data: data, encoding: .utf8) ?? "" | ||
|
|
||
| return (process.terminationStatus, output) | ||
| } | ||
|
Comment on lines
+119
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import Foundation | ||
|
|
||
| // Curated mapping of bundle IDs to GitHub repositories | ||
| let RepoHints: [String: (owner: String, repo: String)] = [ | ||
| // Developer Tools | ||
| "com.microsoft.VSCode": ("microsoft", "vscode"), | ||
| "com.github.GitHubDesktop": ("desktop", "desktop"), | ||
| "com.figma.Desktop": ("figma", "figma-linux"), | ||
| "com.postmanlabs.mac": ("postmanlabs", "postman-app-support"), | ||
|
|
||
| // Productivity | ||
| "com.raycast.macos": ("raycast", "raycast"), | ||
| "com.electron.reeder.5": ("reederapp", "reeder5"), | ||
| "com.culturedcode.ThingsMac": ("culturedcode", "things-mac"), | ||
| "com.flexibits.fantastical2.mac": ("flexibits", "fantastical-mac"), | ||
|
|
||
| // Media & Design | ||
| "org.blender": ("blender", "blender"), | ||
| "com.spotify.client": ("spotify", "spotify-desktop"), | ||
| "com.getdavinci.DaVinciResolve": ("blackmagicdesign", "davinci-resolve"), | ||
|
|
||
| // Communication | ||
| "com.tinyspeck.slackmacgap": ("slack", "slack-desktop"), | ||
| "com.microsoft.teams2": ("microsoft", "teams-desktop"), | ||
| "ru.keepcoder.Telegram": ("telegramdesktop", "tdesktop"), | ||
|
|
||
| // Utilities | ||
| "com.1password.1password": ("1password", "1password-desktop"), | ||
| "com.objective-see.lulu.app": ("objective-see", "lulu"), | ||
| "com.posthog.desktop": ("posthog", "posthog-desktop"), | ||
| "com.sindresorhus.CleanMyMac": ("sindresorhus", "cleanmymac"), | ||
|
|
||
| // Browsers | ||
| "com.google.Chrome": ("google", "chrome"), | ||
| "com.microsoft.edgemac": ("microsoft", "edge"), | ||
| "com.brave.Browser": ("brave", "brave-browser"), | ||
|
|
||
| // Open Source | ||
| "org.videolan.vlc": ("videolan", "vlc"), | ||
| "org.mozilla.firefox": ("mozilla", "firefox"), | ||
| "com.openemu.OpenEmu": ("openemu", "openemu"), | ||
|
Comment on lines
+6
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of the repository mappings in
It would be beneficial to verify this list to ensure the repository discovery feature works as expected. |
||
| ] | ||
|
|
||
| enum RepoDiscovery { | ||
| static func guessRepository(for bundleID: String, appName: String) -> (owner: String, repo: String)? { | ||
| // Check our curated list first | ||
| if let repo = RepoHints[bundleID] { | ||
| return repo | ||
| } | ||
|
|
||
| // Try common patterns | ||
| let cleanName = appName.lowercased() | ||
| .replacingOccurrences(of: " ", with: "-") | ||
| .replacingOccurrences(of: ".", with: "") | ||
|
|
||
| // Common organization patterns | ||
| let commonOwners = [ | ||
| cleanName, | ||
| "\(cleanName)-team", | ||
| "\(cleanName)app", | ||
| "electron-apps" | ||
| ] | ||
|
|
||
| // Return first guess (caller should validate) | ||
| return (owner: commonOwners.first ?? cleanName, repo: cleanName) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for guessing a repository owner is flawed. |
||
| } | ||
|
|
||
| static func validateRepository(owner: String, repo: String) async -> Bool { | ||
| do { | ||
| _ = try await GitHub.latest(owner: owner, repo: repo) | ||
| return true | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
catchblock currently swallows the error, which can make debugging difficult if JSON decoding fails for an unexpected reason. Consider logging the error to aid in future troubleshooting.