-
Notifications
You must be signed in to change notification settings - Fork 0
Add Mac App Store integration and detection #5
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?
Conversation
- Add Anchoring + Decoy pricing in Pro tab ($3.99 → $2.99, decoy option) - Implement Loss Aversion for security warnings ("Don't risk unpatched apps") - Add Social Proof elements (user count, testimonials) - Create Zeigarnik Effect progress indicators with completion tension - Set smart defaults (auto-update ON) using Default Effect - Enhance error states with Actor-Observer bias (situational framing) - Add Authority signals (industry-standard verification) - Improve Privacy tab with Reciprocity bias ("helps improve reliability") 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add SecurityChecks for codesign verification and quarantine handling - Create Installer for ZIP/DMG/PKG with automatic backup before install - Implement Rollback system for one-click version restoration - Include cleanup utilities for managing backup storage - All installs verify signatures before replacing apps 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Implement SparkleHints for SUFeedURL discovery from app bundles - Add GitHub API client for releases with rate limiting and auth - Create RepoHints mapping for 30+ popular apps to GitHub repos - Add Brew cask integration for Homebrew-managed apps - Include repository validation and fallback discovery patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Replace BGTaskScheduler with macOS LaunchAgent for better reliability - Add install/uninstall functionality with proper plist generation - Include status checking and schedule updating capabilities - Configure for low-priority background execution - Handle loading/unloading with proper error handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Implement MAS updates detection using mas command line tool - Add deep link integration for App Store updates page - Include MAS app categorization and receipt validation - Support automatic mas CLI installation via Homebrew - Add App Store ID extraction from app bundles 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary of ChangesHello @adityash8, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly expands the application's capabilities by integrating with the Mac App Store for updates, enhancing app installation and management, and introducing robust background update mechanisms. It also refines the user interface with improved feedback and introduces telemetry for better product development and user experience. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
Code Review
This pull request introduces a substantial amount of new functionality, including Mac App Store integration, update detection via various sources, and core utilities for installation and security. The code is generally well-structured. My review focuses on improving robustness, correctness, and maintainability. I've identified several areas for improvement, such as using safer methods for URL construction and command-line tool interaction, ensuring atomic file operations during installation, and addressing a few logical bugs in update and backup detection. I've also pointed out opportunities to reduce code duplication and adhere more closely to Swift idioms. Overall, this is a great addition, and addressing these points will make the new features more reliable and easier to maintain.
} | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Force-unwrapping the URL with URL(string: ...)!
is unsafe and can cause a runtime crash if the owner
or repo
strings contain characters that are invalid in a URL. You should use URLComponents
to construct the URL safely, which will handle proper encoding and prevent crashes.
var request = URLRequest(url: URL(string: "https://api.github.com/repos/\(owner)/\(repo)/releases/latest")!) | |
guard var urlComponents = URLComponents(string: "https://api.github.com/repos/\(owner)/\(repo)/releases/latest") else { | |
throw GitHubError.invalidRepo | |
} | |
var request = URLRequest(url: urlComponents.url!) |
let task = Process() | ||
task.executableURL = URL(fileURLWithPath: "/usr/bin/codesign") | ||
task.arguments = ["--verify", "--deep", "--strict", appPath] | ||
try? task.run() |
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.
Using try? task.run()
silently ignores potential errors during process launch (e.g., if the codesign
binary is not found or there are permission issues). This is dangerous for a security-critical function. The function should be marked as throws
and you should use try task.run()
to allow callers to handle any errors. This applies to getQuarantineStatus
and removeQuarantine
as well.
try? task.run() | |
try task.run() |
for line in lines { | ||
let components = line.components(separatedBy: .whitespaces) | ||
if components.count >= 2 { | ||
let appID = components[0] | ||
let name = components[1...].joined(separator: " ") | ||
|
||
let appInfo = AppInfo( | ||
name: name, | ||
bundleIdentifier: "mas.\(appID)", | ||
version: "unknown", | ||
path: nil, | ||
iconPath: nil | ||
) | ||
apps.append(appInfo) | ||
} | ||
} |
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 parsing logic for mas outdated
is incorrect. The output of mas outdated
includes the version information (e.g., AppName (1.0 -> 2.0)
). The current code joins all components after the ID, which incorrectly includes the version string in the app's name. You need to parse the line more carefully to extract only the app name.
for line in lines where !line.isEmpty {
let components = line.split(separator: " ")
guard components.count >= 2 else { continue }
let appID = String(components[0])
// Find where the version string starts
let versionStartIndex = line.range(of: " (")?.lowerBound ?? line.endIndex
let namePart = line[line.index(after: line.firstIndex(of: " ") ?? line.startIndex)..<versionStartIndex]
let name = String(namePart).trimmingCharacters(in: .whitespaces)
if !name.isEmpty {
let appInfo = AppInfo(
name: name,
bundleIdentifier: "mas.\(appID)",
version: "unknown",
path: nil,
iconPath: nil
)
apps.append(appInfo)
}
static func caskIsOutdated(_ cask: String) -> Bool { | ||
let command = "brew outdated --cask --greedy --quiet | grep -x \(shellQuote(cask))" | ||
return run(command).exitCode == 0 |
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.
This implementation is inefficient as it fetches all outdated casks and then uses grep
to find a match. A more robust and efficient approach would be to use brew outdated --cask --json
, parse the JSON output, and check if the target cask is present in the resulting list. This avoids the overhead of grep
and is less prone to breaking if the command's text output format changes.
static func caskIsOutdated(_ cask: String) -> Bool {
let result = run("brew outdated --cask --json")
guard result.exitCode == 0, let data = result.output.data(using: .utf8) else {
return false
}
struct OutdatedCasks: Decodable {
let casks: [CaskInfo]
}
struct CaskInfo: Decodable {
let name: String
}
if let outdated = try? JSONDecoder().decode(OutdatedCasks.self, from: data) {
return outdated.casks.contains { $0.name == cask }
}
return false
}
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Silently ignoring a backup failure with try?
is risky. The backup mechanism is critical for the rollback feature. If a backup fails, the installation should be aborted to prevent leaving the system in a state where rollback is not possible. You should handle the error explicitly. This also applies to the backup logic in installDMG
.
_ = try? SecurityChecks.backup(appPath: currentAppPath, bundleID: bundleID, version: currentVersion) | |
try SecurityChecks.backup(appPath: currentAppPath, bundleID: bundleID, version: currentVersion) |
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 comment
The reason will be displayed to describe this comment to others. Learn more.
To make your Decodable
structs more idiomatic in Swift, you should use camelCase
for property names (e.g., tagName
, publishedAt
). You can achieve this by setting the keyDecodingStrategy
on your JSONDecoder
instance to .convertFromSnakeCase
.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(Release.self, from: data)
for key in sparkleKeys { | ||
if let value = bundle.object(forInfoDictionaryKey: key) { | ||
print("Found Sparkle key \(key) for \(bundle.bundleIdentifier ?? "unknown"): \(value)") | ||
} | ||
} | ||
|
||
return nil |
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.
private static func shell(_ command: String) -> Int32 { | ||
let process = Process() | ||
process.launchPath = "/bin/zsh" | ||
process.arguments = ["-lc", command] | ||
|
||
do { | ||
try process.run() | ||
process.waitUntilExit() | ||
return process.terminationStatus | ||
} catch { | ||
return -1 | ||
} | ||
} | ||
|
||
private static func shellQuote(_ string: String) -> String { | ||
return "'\(string.replacingOccurrences(of: "'", with: "'\\''"))'" | ||
} |
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 shell
and shellQuote
helper functions are duplicated across multiple files (LaunchAgent.swift
, Brew.swift
, MAS.swift
), with slightly different implementations. This increases maintenance overhead. These should be consolidated into a single, shared utility file to ensure consistency and make future changes easier.
_ = try? SecurityChecks.backup(appPath: fullPath, bundleID: bundleID, version: currentVersion) | ||
} | ||
|
||
let (code, out) = try run("/usr/bin/hdiutil", ["attach", "-nobrowse", "-quiet", dmgURL.path]) |
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.
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? | ||
} |
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 property names in CaskInfo
use snake_case
, which is common in JSON APIs but not idiomatic in Swift. For better code style and maintainability, you should use camelCase
for property names and configure the JSONDecoder
to convert from snake case automatically. This makes your Swift code cleaner and more consistent.1
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?
}
Style Guide References
Footnotes
-
Swift API Design Guidelines recommend using camelCase for property names to maintain consistency with Swift's naming conventions. ↩
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.
This is the final PR Bugbot will review for you during this billing cycle
Your free Bugbot reviews will reset on October 16
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
} | ||
|
||
static func track(_ name: String, props: [String: Any] = [:]) { | ||
guard UserDefaults.standard.bool(forKey: "telemetry_enabled") else { return } |
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.
Button("Clear All Data") { | ||
Button("Clear Cache (\(cacheSize))") { | ||
// TODO: Implement data clearing | ||
cacheSize = "0 MB" |
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.
Summary
Test plan
🤖 Generated with Claude Code