$npx -y skills add fayazara/macos-app-skills --skill auto-updateAdd Sparkle auto-update support to a native macOS app. Use this skill whenever the user wants to add auto-updates, implement Sparkle, set up an appcast feed, add "Check for Updates" to their app, or integrate automatic update checking. Also trigger when the user mentions Sparkle,
| 1 | # Sparkle Auto-Update for macOS Apps |
| 2 | |
| 3 | This skill adds [Sparkle](https://sparkle-project.org/) auto-update support to a native macOS app. Sparkle is the standard open-source framework for macOS app updates outside the Mac App Store. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | The implementation has 4 parts: |
| 8 | |
| 9 | 1. **SPM dependency** -- add the Sparkle package to the Xcode project |
| 10 | 2. **UpdaterManager.swift** -- a singleton that wraps `SPUStandardUpdaterController` |
| 11 | 3. **Info.plist keys** -- `SUFeedURL` and `SUPublicEDKey` |
| 12 | 4. **UI integration** -- "Check for Updates" button in settings/menu bar |
| 13 | |
| 14 | ## Step 1: Add Sparkle via SPM |
| 15 | |
| 16 | In Xcode: File > Add Package Dependencies > enter: |
| 17 | |
| 18 | ``` |
| 19 | https://github.com/sparkle-project/Sparkle |
| 20 | ``` |
| 21 | |
| 22 | Use the "Up to Next Major Version" rule with `2.0.0`. Add the `Sparkle` framework to your app target. |
| 23 | |
| 24 | Or add it to `Package.swift` if your project uses one: |
| 25 | |
| 26 | ```swift |
| 27 | .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0") |
| 28 | ``` |
| 29 | |
| 30 | ## Step 2: Create UpdaterManager.swift |
| 31 | |
| 32 | Copy `references/UpdaterManager.swift` into your project. This is a singleton that: |
| 33 | |
| 34 | - Creates `SPUStandardUpdaterController` early (before `applicationDidFinishLaunching` returns) |
| 35 | - Publishes `canCheckForUpdates` for UI binding |
| 36 | - Exposes `automaticallyChecksForUpdates` toggle |
| 37 | - Skips all update logic in DEBUG builds (so you don't get update prompts during development) |
| 38 | - For menu-bar-only apps: temporarily switches to `.regular` activation policy before showing the update window |
| 39 | |
| 40 | The key design decisions in this file: |
| 41 | |
| 42 | - **`startingUpdater: false`** in the initializer, then calling `start()` explicitly in `applicationDidFinishLaunching`. This gives you control over timing. |
| 43 | - **DEBUG guards** on `start()` and `checkForUpdates()`. Sparkle should never run in debug builds -- it would try to update your debug app with a release build. |
| 44 | - **`ObservableObject` with `@Published`** (not `@Observable`) because we need the Combine `publisher(for:)` bridge from Sparkle's KVO. |
| 45 | |
| 46 | ## Step 3: Configure Info.plist |
| 47 | |
| 48 | Add these keys to your app's `Info.plist`: |
| 49 | |
| 50 | ```xml |
| 51 | <key>SUFeedURL</key> |
| 52 | <string>https://raw.githubusercontent.com/OWNER/REPO/main/appcast.xml</string> |
| 53 | |
| 54 | <key>SUPublicEDKey</key> |
| 55 | <string>YOUR_PUBLIC_EDDSA_KEY_HERE</string> |
| 56 | |
| 57 | <key>SUEnableInstallerLauncherService</key> |
| 58 | <true/> |
| 59 | ``` |
| 60 | |
| 61 | ### Generating EdDSA Keys |
| 62 | |
| 63 | Sparkle uses EdDSA (Ed25519) signing. Generate a keypair: |
| 64 | |
| 65 | ```bash |
| 66 | # Find generate_keys in your DerivedData after building the project with Sparkle |
| 67 | find ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1 |
| 68 | ``` |
| 69 | |
| 70 | Run it: |
| 71 | |
| 72 | ```bash |
| 73 | /path/to/generate_keys |
| 74 | ``` |
| 75 | |
| 76 | This prints the public key and stores the private key in your Keychain. Put the public key in `SUPublicEDKey` in Info.plist. The private key stays in Keychain and is used by `sign_update` during release. |
| 77 | |
| 78 | ## Step 4: Wire Into App |
| 79 | |
| 80 | ### App Delegate |
| 81 | |
| 82 | ```swift |
| 83 | final class AppDelegate: NSObject, NSApplicationDelegate { |
| 84 | private let updaterManager = UpdaterManager.shared |
| 85 | |
| 86 | func applicationDidFinishLaunching(_ notification: Notification) { |
| 87 | updaterManager.start() |
| 88 | } |
| 89 | } |
| 90 | ``` |
| 91 | |
| 92 | The `UpdaterManager.shared` property must be accessed early so the `SPUStandardUpdaterController` is created before the app finishes launching. Referencing it in the `AppDelegate` property ensures this. |
| 93 | |
| 94 | ### Settings UI (About Pane) |
| 95 | |
| 96 | ```swift |
| 97 | struct AboutSettingsPane: View { |
| 98 | @ObservedObject private var updaterManager = UpdaterManager.shared |
| 99 | |
| 100 | var body: some View { |
| 101 | Form { |
| 102 | Section("Updates") { |
| 103 | Toggle(isOn: Binding( |
| 104 | get: { updaterManager.automaticallyChecksForUpdates }, |
| 105 | set: { updaterManager.automaticallyChecksForUpdates = $0 } |
| 106 | )) { |
| 107 | Text("Automatically check for updates") |
| 108 | } |
| 109 | |
| 110 | Button("Check for Updates...") { |
| 111 | updaterManager.checkForUpdates() |
| 112 | } |
| 113 | .disabled(!updaterManager.canCheckForUpdates) |
| 114 | } |
| 115 | } |
| 116 | .formStyle(.grouped) |
| 117 | .scrollContentBackground(.hidden) |
| 118 | } |
| 119 | } |
| 120 | ``` |
| 121 | |
| 122 | ### Menu Bar (optional) |
| 123 | |
| 124 | ```swift |
| 125 | Button { |
| 126 | updaterManager.checkForUpdates() |
| 127 | } label: { |
| 128 | Label("Check for Updates...", systemImage: "arrow.down.circle") |
| 129 | } |
| 130 | .disabled(!updaterManager.canCheckForUpdates) |
| 131 | ``` |
| 132 | |
| 133 | ## Step 5: Create Initial Appcast |
| 134 | |
| 135 | Create an `appcast.xml` at the root of your repo. It |