Files
workouts/Scripts/asc.swift
T
rzen 85d0eaddbb Workouts 2.0: re-base persistence on iCloud Drive documents
Replace Core Data + NSPersistentCloudKitContainer + App-Group store +
WatchConnectivity dictionary sync with the QuickRabbit iCloud-documents
architecture:

- iCloud Drive JSON documents are the sole source of truth (one file per
  aggregate: Splits/<ULID>.json, Workouts/YYYY/MM/<ULID>.json), with a
  rebuildable SwiftData cache populated only by an NSMetadataQuery observer
  and a connect-time reconcile. Soft-delete tombstones; hard iCloud gate.
- Shared model layer (ULID, Codable *Documents + stateless mappers, @Model
  cache entities, SwiftData container) compiled into both targets.
- New iPhone<->Watch bridge over WatchConnectivity keyed by ULIDs; the phone
  is the sole writer of iCloud Drive, the watch round-trips documents.
- AppServices DI + iCloud-required root gate; Swift 6 strict concurrency.
- Starter splits generated on demand from the bundled YAML catalogs.
- Migrate to XcodeGen (project.yml), iOS 26 / watchOS 26; CloudDocuments
  entitlement (drop CloudKit/App Group/aps-environment).
- Duration stored as Int seconds (was a Date epoch hack); fix workout
  end-on-create, undismissable delete dialog, toolbar-hiding nav stacks,
  and the Settings placeholder.
- Add README/CHANGELOG/LICENSE, .gitignore, refreshed REQUIREMENTS, and the
  Scripts/ TestFlight pipeline (release.sh + ASC API scripts).

MARKETING_VERSION 2.0.
2026-06-19 14:25:27 -04:00

60 lines
2.5 KiB
Swift
Executable File

#!/usr/bin/env swift
// Generic ASC API helper: asc.swift <get|post|patch|delete> <path-after-/v1/> [json-body]
import Foundation
import CryptoKit
func die(_ msg: String) -> Never {
FileHandle.standardError.write((msg + "\n").data(using: .utf8)!)
exit(1)
}
func b64url(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
let env = ProcessInfo.processInfo.environment
guard let keyID = env["ASC_KEY_ID"], let issuer = env["ASC_ISSUER_ID"], let keyPath = env["ASC_KEY_PATH"] else {
die("missing ASC env")
}
guard CommandLine.arguments.count >= 3 else { die("usage: asc.swift <method> <path> [body]") }
let method = CommandLine.arguments[1].uppercased()
let path = CommandLine.arguments[2]
let body = CommandLine.arguments.count > 3 ? CommandLine.arguments[3].data(using: .utf8) : nil
guard let pem = try? String(contentsOfFile: keyPath, encoding: .utf8),
let key = try? P256.Signing.PrivateKey(pemRepresentation: pem) else { die("key error") }
let now = Int(Date().timeIntervalSince1970)
let header = "{\"alg\":\"ES256\",\"kid\":\"\(keyID)\",\"typ\":\"JWT\"}"
let payload = "{\"iss\":\"\(issuer)\",\"iat\":\(now),\"exp\":\(now + 1200),\"aud\":\"appstoreconnect-v1\"}"
let signingInput = b64url(Data(header.utf8)) + "." + b64url(Data(payload.utf8))
guard let sig = try? key.signature(for: Data(signingInput.utf8)) else { die("signing failed") }
let jwt = signingInput + "." + b64url(sig.rawRepresentation)
let base = path.hasPrefix("v2/") ? "https://api.appstoreconnect.apple.com/" : "https://api.appstoreconnect.apple.com/v1/"
var req = URLRequest(url: URL(string: base + path)!)
req.httpMethod = method
req.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
if let body {
req.httpBody = body
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
let sem = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: req) { d, resp, err in
if let err { die("network: \(err)") }
let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
print("HTTP \(status)")
if let d, !d.isEmpty {
if let obj = try? JSONSerialization.jsonObject(with: d),
let pretty = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys]) {
print(String(data: pretty, encoding: .utf8)!)
} else {
print(String(data: d, encoding: .utf8) ?? "")
}
}
sem.signal()
}.resume()
sem.wait()