diff --git a/AGENTS.md b/AGENTS.md index 6142300..8a6f779 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ When asked to deploy, do exactly this, from the worktree root: 1. `go generate ./web`, if anything under `web/src/` or `api/openapi.yaml` changed. 2. `go install ./cmd/byre && launchctl kickstart -k gui/$(id -u)/run.fc.byre`, always as that one command, never `go install` on its own, and always the last action of the turn. 3. When you resume, confirm it came up: read the tail of `~/Library/Logs/byre.log`, and `GET /api/admin/status` through the `tailscale serve` URL (`tailscale serve status` shows it). Report what you deployed and what you checked. -4. Once the service is confirmed healthy, rebuild and relaunch the macOS app: run `mac/bundle.sh`, then `pkill -x Byre` to quit every running byre app, whichever worktree it was built from (it exits 1 when none is running, which is fine), then `open mac/.build/byre.app`. The app reads its server address from the shared `run.fc.byre.mac` defaults, so the new build reconnects to prod with no setup. Report that the app was rebuilt and relaunched. +4. Once the service is confirmed healthy, rebuild and relaunch the macOS app: run `mac/bundle.sh`, then `pkill -x Byre` to quit every running byre app, whichever worktree it was built from (it exits 1 when none is running, which is fine), then `open mac/.build/byre.app`. The app reads its server address from its iCloud-synced SwiftData store, shared by every build, so the new build reconnects to prod with no setup. Report that the app was rebuilt and relaunched. What to expect: diff --git a/TODO.md b/TODO.md index 87a1c3d..949bdf3 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,5 @@ # M8 — macOS client -- macOS client signing and SwiftData with iCloud sync - Sign `byre.app` with the paid developer team (provisioning profile, iCloud and CloudKit entitlements) in `mac/bundle.sh`, and move the app's persisted state (the `byre.server` origin, drafts, last visits) from `UserDefaults` to SwiftData with CloudKit sync, so the server address follows the user to every Mac. - macOS client parity review Side by side with the web client on the same server, window size and appearance, using disposable tasks: every tab, switcher, Inbox choice types, merge/discard dialogs, light/dark, narrow windows. Document build, install and storage in `docs/operations.md`. diff --git a/docs/macos.md b/docs/macos.md index fa7371e..43d1701 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -18,9 +18,11 @@ The first version copies the web client: the same two equal panes, tabs, labels, - `UserNotifications` for notifications. - Third-party code: Apple's `swift-openapi-generator` (with `swift-openapi-runtime` and `swift-openapi-urlsession`) for the API types and client, and SwiftTerm for the terminal. -The project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`, cached as `mac/.build/byre.icns` until the SVG changes) and signs it ad hoc; notification permission needs the bundle identifier. +The project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`, cached as `mac/.build/byre.icns` until the SVG changes) and signs it; notification permission needs the bundle identifier. -Only the release configuration is built. A debug build stops at an `#error` in `ByreApp.swift`, and even a failed one leaves the next release build recompiling every target, since the two configurations share `mac/.build`. The generated API client is its own target, `ByreAPI`, so an edit to the app recompiles only the `Byre` target. SwiftPM builds release targets whole-module, so that is one compile of the whole `Byre` target, and a one-file change still costs that. The app keeps its server origin in the `byre.server` default (`defaults read run.fc.byre.mac byre.server` prints it). +The app is signed with the team's Apple Development certificate (`codesign --sign "Apple Development"`), with the entitlements in `mac/byre.entitlements` (the team-prefixed application identifier, the iCloud container with CloudKit, and development push for sync), and `mac/byre.provisionprofile` embedded as `Contents/embedded.provisionprofile`. The profile is a development profile Xcode manages: it allows the team's Apple Development certificates and the Macs registered to the team, so it runs only on those Macs, and its CloudKit environment is Development. `security cms -D -i mac/byre.provisionprofile` prints it, `ProvisionedDevices` and `DeveloperCertificates` included, and `security find-identity -v -p codesigning` lists the certificates on this Mac. Building on a Mac not yet in the profile, or one without an Apple Development certificate, means regenerating it with Xcode's automatic signing: an Xcode app project with bundle identifier `run.fc.byre.mac`, the team, and an entitlements file naming the iCloud container with CloudKit, built with `xcodebuild -allowProvisioningUpdates -allowProvisioningDeviceRegistration` while the team's Apple ID is signed in to Xcode, creates the certificate, registers the Mac and writes the new profile to `~/Library/Developer/Xcode/UserData/Provisioning Profiles/`; copy it over `mac/byre.provisionprofile` and commit it. + +Only the release configuration is built. A debug build stops at an `#error` in `ByreApp.swift`, and even a failed one leaves the next release build recompiling every target, since the two configurations share `mac/.build`. The generated API client is its own target, `ByreAPI`, so an edit to the app recompiles only the `Byre` target. SwiftPM builds release targets whole-module, so that is one compile of the whole `Byre` target, and a one-file change still costs that. The app keeps its server origin, drafts and visits in SwiftData (see "Persistence"). SwiftUI's own macros don't build here: the SDK declares `@State`, `@Entry`, `@Animatable` and the rest as macros implemented in the `SwiftUIMacros` plugin, which ships with Xcode and not with the Command Line Tools (`ls /Library/Developer/CommandLineTools/usr/lib/swift/host/plugins` lists the plugins there). `@State` is reached through its property-wrapper type instead, as `typealias ViewState = State` and `@ViewState var …`; other SwiftUI macros aren't used. `@Observable` and `@Bindable` (Observation's plugin, which the Command Line Tools have), `@Environment` and `@FocusState` work as usual. @@ -147,7 +149,7 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima ## Drafts -`Drafts.swift` ports `drafts.ts` with `UserDefaults` in place of `localStorage`. `localStorage` is per origin, so the key adds the server origin, normalized to lower-case scheme and host with the port always written, in front of the web client's key: `byre.drafts..`, for example `byre.drafts.https://byre.example.ts.net:443.you@example.com`. Change Server… leaves the old server's drafts in place, and switching back finds them. The structure is the web client's: composer text by `message:` or `message:lead`, in-flight sends folded back in, checked choices and Other text by inbox item, pruned when a task closes or an item resolves, saved 400 ms after a change and at once on blur, send and quit. Pasted images aren't kept, as in the browser; after a relaunch the composer says how many need re-attaching. +`Drafts.swift` ports `drafts.ts` with a SwiftData `DraftRecord` in place of `localStorage` (see "Persistence"). `localStorage` is per origin, so the record's `scope` is the server origin, normalized to lower-case scheme and host with the port always written, then the login: `.`, for example `https://byre.example.ts.net:443.you@example.com`. Its `json` holds the web client's JSON object. A flush writes only after a local change, so a Mac that loaded its drafts before another Mac's arrived doesn't overwrite them by quitting. Each finished CloudKit import reloads the composer texts and answers from the record when there is no unflushed local change and no send in flight; a composer already on screen keeps the text it showed, and picks up the new text the next time it appears. Two Macs editing drafts for the same scope within one sync of each other keep whichever flushed last. Change Server… leaves the old server's drafts in place, and switching back finds them. The structure is the web client's: composer text by `message:` or `message:lead`, in-flight sends folded back in, checked choices and Other text by inbox item, pruned when a task closes or an item resolves, saved 400 ms after a change and at once on blur, send and quit. Pasted images aren't kept, as in the browser; after a relaunch the composer says how many need re-attaching. ## Keyboard and menus @@ -155,7 +157,11 @@ The app menu has About, Change Server…, Hide, Hide Others and Quit; Edit has t ## Connection -On first launch the window shows one field, Server URL (an `https` origin with no path), and Connect. Connect calls `GET /api/admin/status` and explains a failure: an untrusted certificate, an unreachable server ("Is Tailscale connected?"), 401 (no Tailscale identity: use the `ts.net` HTTPS address), 403 (the request didn't come through `tailscale serve`); anything else shows the error's own text. Certificate validation is never relaxed. On success the origin, normalized to a lower-case host with any port, is kept in the `byre.server` default and startup runs; later launches start against it directly. Change Server… (in the app menu) flushes drafts, reports pane focus false, drops the store and streams, and shows the connection screen again, its field holding the current origin. Quitting does the same flush and report, waiting up to 2 s for the report before exiting (`applicationShouldTerminate` in `ByreApp.swift`). +On first launch the window shows one field, Server URL (an `https` origin with no path), and Connect. Connect calls `GET /api/admin/status` and explains a failure: an untrusted certificate, an unreachable server ("Is Tailscale connected?"), 401 (no Tailscale identity: use the `ts.net` HTTPS address), 403 (the request didn't come through `tailscale serve`); anything else shows the error's own text. Certificate validation is never relaxed. On success the origin, normalized to a lower-case host with any port, is kept as the app's one `ServerRecord` (see "Persistence") and startup runs; later launches start against it directly. While the connection screen shows with an empty field, each finished CloudKit import (`NSPersistentCloudKitContainer.eventChangedNotification`) looks for a `ServerRecord` again and starts against it, so on a new Mac the address another Mac saved connects without typing. + +## Persistence + +`Persistence.swift` holds the app's state in SwiftData, synced through CloudKit to the private database of container `iCloud.run.fc.byre.mac`, so it follows the iCloud account to every Mac: `ServerRecord` (the server origin), `DraftRecord` (drafts, see "Drafts") and `VisitRecord` (one per task: when it was last selected, read by the task switcher). CloudKit has no unique constraints, so two Macs can each create a record for the same thing; readers take the newest `ServerRecord` and `DraftRecord` and the latest `VisitRecord` per task, and the next write keeps one and deletes the rest. The store opens at launch and the app exits if it can't, which is what an unsigned build without the iCloud entitlement does. Change Server… (in the app menu) flushes drafts, reports pane focus false, drops the store and streams, and shows the connection screen again, its field holding the current origin. Quitting does the same flush and report, waiting up to 2 s for the report before exiting (`applicationShouldTerminate` in `ByreApp.swift`). ## Gaps in SwiftUI diff --git a/docs/operations.md b/docs/operations.md index e13f66a..e76c1c8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -13,7 +13,7 @@ - `internal/tailscale`: `tailscale serve` for task previews (see "Previews"). - `internal/workflow`: validating workflow definitions and reading and writing their YAML files (see "Workflows"). - `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `favicon.svg` (a lead dot in the accent colour with spokes to five task-coloured dots; its colours are saturated versions of the `web/style.css` accent and task hues, not copies of the palette, since the palette's pastels blur together at 16px; they switch with the light/dark scheme), `favicon.png` (the same icon in its dark colours on an opaque dark `--surface` square at 180×180, rendered from `web/favicon-tile.svg`, which is not embedded; opaque because Orion puts a transparent icon on a white tile; linked as a second icon and as the `apple-touch-icon`, which Orion prefers over the SVG for its tab icon; Orion's icon store (`~/Library/Application Support/Orion/Defaults/website_icons`, kept apart from its cache) keys icons by hostname without the port, so another server on the same hostname can supply byre's tab icon; `favicon-tile.svg` repeats the shapes and dark colours of `favicon.svg`, so change both together, then regenerate the PNG with `rsvg-convert --width 180 --height 180 --output web/favicon.png web/favicon-tile.svg`), `dist/`, `src/` (for source maps), `fonts/` and `vendor/` (Mermaid, see "Inbox"; xterm.js, see "Terminals"). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`. -- `mac/` is the macOS client in progress; `docs/macos.md` is its design and says what exists. `mac/bundle.sh` builds `mac/.build/byre.app`, which connects to the byre `https` origin typed into its connection screen (the same `tailscale serve` URL a browser uses). Its API client is generated from `api/openapi.yaml` at build time, and `go generate ./mac` regenerates the committed `mac/Sources/Byre/Palette.swift` from the custom properties in `web/style.css`, so run it after changing the palette. The app keeps its state in its own defaults domain (`defaults read run.fc.byre.mac` prints it): the server origin in `byre.server`, last task visits in `byre.visits`, and composer and answer drafts as JSON in `byre.drafts..`, one key per server and login. Pasted images stay in memory with the connection and are dropped on Change Server… and quit. It reports `PUT /api/me/pane-focus` as the web client does, and sends `focused: false` on Change Server…, on closing the window and on quit. +- `mac/` is the macOS client in progress; `docs/macos.md` is its design and says what exists. `mac/bundle.sh` builds `mac/.build/byre.app`, which connects to the byre `https` origin typed into its connection screen (the same `tailscale serve` URL a browser uses). Its API client is generated from `api/openapi.yaml` at build time, and `go generate ./mac` regenerates the committed `mac/Sources/Byre/Palette.swift` from the custom properties in `web/style.css`, so run it after changing the palette. The app keeps its state in SwiftData synced through the iCloud private database of container `iCloud.run.fc.byre.mac` (the CloudKit console shows its records): the server origin, last task visits, and composer and answer drafts as JSON, one record per server and login (`docs/macos.md`, "Persistence"). Pasted images stay in memory with the connection and are dropped on Change Server… and quit. It reports `PUT /api/me/pane-focus` as the web client does, and sends `focused: false` on Change Server…, on closing the window and on quit. ## Data diff --git a/mac/Sources/Byre/Drafts.swift b/mac/Sources/Byre/Drafts.swift index f90341d..3b53348 100644 --- a/mac/Sources/Byre/Drafts.swift +++ b/mac/Sources/Byre/Drafts.swift @@ -1,5 +1,6 @@ import Foundation import Observation +import SwiftData private struct Saved: Codable { struct Composer: Codable { @@ -39,30 +40,43 @@ final class Drafts { private var lostImages: [String: Int] = [:] private var checked: [String: [String]] = [:] private var texts: [String: String] = [:] - @ObservationIgnored private let storeKey: String + @ObservationIgnored private let scope: String @ObservationIgnored private var timer: Task? + @ObservationIgnored private var dirty = false init(origin: URL, login: String) { - storeKey = "byre.drafts.\(draftOrigin(origin)).\(login)" - let saved = Self.read(storeKey) - for (key, d) in saved.composer { - if !d.text.isEmpty { - composerTexts[key] = d.text - } - if d.images > 0 { - lostImages[key] = d.images + scope = "\(draftOrigin(origin)).\(login)" + load(Self.read(scope)) + } + + private func load(_ saved: Saved) { + composerTexts = saved.composer.filter { !$0.value.text.isEmpty }.mapValues(\.text) + lostImages = saved.composer.reduce(into: [:]) { lost, d in + let n = d.value.images - (composerImages[d.key] ?? 0) + if n > 0 { + lost[d.key] = n } } checked = saved.checked texts = saved.texts } - private static func read(_ key: String) -> Saved { - guard let raw = UserDefaults.standard.string(forKey: key) else { + func reload() { + if !dirty && sending.isEmpty { + load(Self.read(scope)) + } + } + + private static func records(_ scope: String) -> [DraftRecord] { + fetchRecords(FetchDescriptor(predicate: #Predicate { $0.scope == scope }, sortBy: [SortDescriptor(\.saved, order: .reverse)])) + } + + private static func read(_ scope: String) -> Saved { + guard let record = records(scope).first else { return Saved() } do { - return try JSONDecoder().decode(Saved.self, from: Data(raw.utf8)) + return try JSONDecoder().decode(Saved.self, from: record.json) } catch { log.error("read drafts: \(String(describing: error), privacy: .public)") return Saved() @@ -91,6 +105,7 @@ final class Drafts { } private func save() { + dirty = true timer?.cancel() timer = Task { [weak self] in try? await Task.sleep(for: .milliseconds(400)) @@ -103,6 +118,10 @@ final class Drafts { func flush() { timer?.cancel() timer = nil + guard dirty else { + return + } + dirty = false var out = Saved(checked: checked, texts: texts) let keys = Set(composerTexts.keys).union(composerImages.keys).union(sending.map(\.key)).union(lostImages.keys) for key in keys { @@ -114,7 +133,18 @@ final class Drafts { } } do { - UserDefaults.standard.set(String(decoding: try JSONEncoder().encode(out), as: UTF8.self), forKey: storeKey) + let json = try JSONEncoder().encode(out) + let records = Self.records(scope) + if records.isEmpty { + persistence.insert(DraftRecord(scope: scope, json: json)) + } else { + records[0].json = json + records[0].saved = Date() + for record in records.dropFirst() { + persistence.delete(record) + } + } + saveRecords() } catch { log.error("save drafts: \(String(describing: error), privacy: .public)") } @@ -153,6 +183,7 @@ final class Drafts { let send = DraftSend(key: key, text: text, images: images) sending.append(send) composerTexts[key] = nil + dirty = true return send } @@ -166,6 +197,7 @@ final class Drafts { composerTexts[send.key] = text } } + dirty = true flush() } @@ -199,6 +231,7 @@ final class Drafts { func clearAnswer(_ id: String) { checked[id] = nil texts[id] = nil + dirty = true flush() } } diff --git a/mac/Sources/Byre/Persistence.swift b/mac/Sources/Byre/Persistence.swift new file mode 100644 index 0000000..648ecca --- /dev/null +++ b/mac/Sources/Byre/Persistence.swift @@ -0,0 +1,70 @@ +import Foundation +import SwiftData + +@Model +final class ServerRecord { + var origin = "" + var saved = Date.distantPast + + init(origin: String) { + self.origin = origin + saved = Date() + } +} + +@Model +final class DraftRecord { + var scope = "" + var json = Data() + var saved = Date.distantPast + + init(scope: String, json: Data) { + self.scope = scope + self.json = json + saved = Date() + } +} + +@Model +final class VisitRecord { + var taskId = "" + var at = 0.0 + + init(taskId: String, at: Double) { + self.taskId = taskId + self.at = at + } +} + +private let container: ModelContainer = { + let schema = Schema([ServerRecord.self, DraftRecord.self, VisitRecord.self]) + do { + return try ModelContainer(for: schema, configurations: ModelConfiguration(schema: schema, cloudKitDatabase: .private("iCloud.run.fc.byre.mac"))) + } catch { + fatalError("open persistent store: \(error)") + } +}() + +@MainActor +var persistence: ModelContext { + container.mainContext +} + +@MainActor +func fetchRecords(_ descriptor: FetchDescriptor) -> [T] { + do { + return try persistence.fetch(descriptor) + } catch { + log.error("fetch \(String(describing: T.self), privacy: .public): \(String(describing: error), privacy: .public)") + return [] + } +} + +@MainActor +func saveRecords() { + do { + try persistence.save() + } catch { + log.error("save records: \(String(describing: error), privacy: .public)") + } +} diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index f5fcfbc..4ef09ce 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -1,9 +1,9 @@ +import CoreData import Foundation import Observation +import SwiftData import SwiftUI -private let originKey = "byre.server" - @MainActor @Observable final class Connection { @@ -21,16 +21,33 @@ final class Connection { @ObservationIgnored private var generation = 0 init() { - let saved = UserDefaults.standard.string(forKey: originKey) - serverText = saved ?? "" - if let saved, let origin = parseOrigin(saved) { - phase = .starting - Task { - await start(origin) + serverText = "" + resume() + Task { + for await note in NotificationCenter.default.notifications(named: NSPersistentCloudKitContainer.eventChangedNotification) { + if let event = note.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event, event.type == .import, event.endDate != nil { + store?.drafts?.reload() + resume() + } } } } + private func resume() { + guard case .connect = phase, serverText.isEmpty, !connecting, let saved = Self.servers().first?.origin, let origin = parseOrigin(saved) else { + return + } + serverText = saved + phase = .starting + Task { + await start(origin) + } + } + + private static func servers() -> [ServerRecord] { + fetchRecords(FetchDescriptor(sortBy: [SortDescriptor(\.saved, order: .reverse)])) + } + var store: Store? { if case .running(let store) = phase { return store @@ -59,7 +76,11 @@ final class Connection { return } connecting = false - UserDefaults.standard.set(origin.absoluteString, forKey: originKey) + for server in Self.servers() { + persistence.delete(server) + } + persistence.insert(ServerRecord(origin: origin.absoluteString)) + saveRecords() serverText = origin.absoluteString phase = .starting await start(origin) diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index f662ff1..72df1ae 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -1,11 +1,11 @@ import Foundation import Observation +import SwiftData let taskTabs: [TaskTab] = [.transcript, .changes, .files, .terminal, .inbox, .reviews, .workflow, .project] private let historyPage = 1000 private let leadPage = 50 -private let visitsKey = "byre.visits" struct TaskState { enum Kind { @@ -360,16 +360,26 @@ final class Store { } func lastVisits() -> [String: Double] { - UserDefaults.standard.dictionary(forKey: visitsKey) as? [String: Double] ?? [:] + fetchRecords(FetchDescriptor()).reduce(into: [:]) { visits, v in + visits[v.taskId] = max(visits[v.taskId] ?? 0, v.at) + } } private func markVisited(_ taskId: String?) { guard let taskId else { return } - var visits = lastVisits() - visits[taskId] = Date().timeIntervalSince1970 * 1000 - UserDefaults.standard.set(visits, forKey: visitsKey) + let at = Date().timeIntervalSince1970 * 1000 + let records = fetchRecords(FetchDescriptor(predicate: #Predicate { $0.taskId == taskId })) + if records.isEmpty { + persistence.insert(VisitRecord(taskId: taskId, at: at)) + } else { + records[0].at = at + for record in records.dropFirst() { + persistence.delete(record) + } + } + saveRecords() } func loadSummaryReads() async throws { diff --git a/mac/bundle.sh b/mac/bundle.sh index 6640b3e..dc65341 100755 --- a/mac/bundle.sh +++ b/mac/bundle.sh @@ -23,5 +23,6 @@ cp "$build/release/Byre" "$app/Contents/MacOS/Byre" cp "$mac/Info.plist" "$app/Contents/Info.plist" cp "$root"/web/fonts/*.woff2 "$app/Contents/Resources/fonts/" cp "$icns" "$app/Contents/Resources/byre.icns" -codesign --force --sign - "$app" +cp "$mac/byre.provisionprofile" "$app/Contents/embedded.provisionprofile" +codesign --force --sign "Apple Development" --entitlements "$mac/byre.entitlements" "$app" echo "$app" diff --git a/mac/byre.entitlements b/mac/byre.entitlements new file mode 100644 index 0000000..cd280f9 --- /dev/null +++ b/mac/byre.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.application-identifier + XDZ54KDM4W.run.fc.byre.mac + com.apple.developer.team-identifier + XDZ54KDM4W + com.apple.developer.icloud-container-identifiers + + iCloud.run.fc.byre.mac + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.aps-environment + development + + diff --git a/mac/byre.provisionprofile b/mac/byre.provisionprofile new file mode 100644 index 0000000..f44f51f Binary files /dev/null and b/mac/byre.provisionprofile differ