diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index ec61df01..bb7f63cb 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -4,7 +4,7 @@ # .markdownlint-cli2.yaml # mas # -# markdownlint-cli2 0.21.0 / markdownlint 0.40.0 +# markdownlint-cli2 0.22.0 / markdownlint 0.40.0 # --- gitignore: true diff --git a/.swiftformat b/.swiftformat index bdffb087..c7b9f557 100644 --- a/.swiftformat +++ b/.swiftformat @@ -14,6 +14,7 @@ #--enable markTypes #--enable preferExplicitFalse #--enable testSuiteAccessControl +#--enable wrapMultilineConditionalAssignment # Enabled rules (disabled by default) --enable acronyms @@ -34,7 +35,6 @@ --enable validateTestCases --enable wrapConditionalBodies --enable wrapEnumCases ---enable wrapMultilineConditionalAssignment --enable wrapMultilineFunctionChains --enable wrapSwitchCases diff --git a/.swiftlint.yml b/.swiftlint.yml index e267929f..98ad6339 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -34,7 +34,7 @@ disabled_rules: - strict_fileprivate - type_body_length attributes: - always_on_line_above: ['@Flag', '@MainActor', '@OptionGroup'] + always_on_line_above: ['@Flag', '@MainActor', '@OptionGroup', '@TaskLocal'] deployment_target: macOS_deployment_target: 13 macOSApplicationExtension_deployment_target: 13 diff --git a/Brewfile b/Brewfile index 46829977..e854e3e1 100644 --- a/Brewfile +++ b/Brewfile @@ -1,8 +1,8 @@ brew "actionlint" # 1.7.11 -brew "gh" # 2.87.3 +brew "gh" # 2.88.1 brew "git" # 2.53.0 -brew "ipsw" # 3.1.660 -brew "markdownlint-cli2" # 0.21.0 +brew "ipsw" # 3.1.665 +brew "markdownlint-cli2" # 0.22.0 brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.6.0 brew "shellcheck" # 0.11.0 brew "swiftformat" # 0.60.1 diff --git a/Documentation/Sample.swift b/Documentation/Sample.swift index 4a58ec15..12148435 100644 --- a/Documentation/Sample.swift +++ b/Documentation/Sample.swift @@ -29,7 +29,7 @@ final class Sample { } /// Use `()` for void arguments & `Void` for void return types. -let closure: () -> Void = { +func x(_: () -> Void) { // Do nothing } diff --git a/Package.resolved b/Package.resolved index f327c43b..ed3a1efb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", - "version" : "1.7.0" + "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", + "version" : "1.7.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "8d9834a6189db730f6264db7556a7ffb751e99ee", - "version" : "1.4.0" + "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", + "version" : "1.4.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "dba183c96b2da4e4b80bb31b1e2e59cb9542b8fc", - "version" : "2.13.0" + "revision" : "fd541c4b3fa7dcec2e7cfe049505b6e4382cd66b", + "version" : "2.13.2" } } ], diff --git a/Package.swift b/Package.swift index ea1e1909..5d0d17b8 100644 --- a/Package.swift +++ b/Package.swift @@ -19,11 +19,11 @@ _ = Package( products: [.executable(name: "mas", targets: ["mas"])], dependencies: [ .package(url: "https://github.com/KittyMac/Sextant.git", from: "0.4.38"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.7.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.7.1"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.3.0"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.4.1"), .package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0"), - .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.13.0"), + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.13.2"), ], targets: [ .plugin(name: "MASBuildToolPlugin", capability: .buildTool()), diff --git a/Scripts/lint b/Scripts/lint index d7250172..4cfe7519 100755 --- a/Scripts/lint +++ b/Scripts/lint @@ -78,7 +78,7 @@ shellcheck -s bash -o all -e SC1009,SC1088,SC2296,SC2298,SC2299,SC2300,SC2301,SC printf -- $'--> 🚷 Non-Executables\n' readonly -a non_executables=(Scripts/***/*(N.^f+111)) if (("${#non_executables[@]}")); then - printf $'%s\n' "${non_executables[@]}" + printf $'\e[1;91m%s\e[0m\n' "${non_executables[@]}" ((exit_status |= 1)) fi diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h b/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h index 71a70357..1e03e4dc 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h b/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h index c841f396..18c20329 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h b/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h index 6dd33135..5759d2bb 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h b/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h index 69154150..2e17b654 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h index a90e9dff..248fa850 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h index 8b668795..7c0530f3 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h index 1c0d624b..b0ac191b 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h index e79d2b31..c713bcc1 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h index 4df93670..6e9a9081 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h index f9d3d20c..2064c718 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h index e87e8c18..76e6fb44 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h index ec534a56..47237628 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h index a168cfff..79f26920 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h b/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h index e998e3a3..1970e2f7 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.665, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3) // - LC_SOURCE_VERSION: 716.2.2.0.0 diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 350e9ac7..bd31b39d 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -146,7 +146,7 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver { return } guard let continuation = unsafe continuation else { - MAS.printer.error("Failed to obtain download continuation for ADAM ID \(adamID)") + MAS.printer.error("Failed to get download continuation for ADAM ID \(adamID)") return } @@ -204,7 +204,8 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver { case .downloading where prevPhaseType == .processing, .downloaded where prevPhaseType == .downloading, - .performing: + .performing + : // swiftformat:disable:this indent MAS.printer.clearCurrentLine(of: .standardOutput) MAS.printer.notice(snapshot.activePhaseType, snapshot.appNameAndVersion) default: @@ -469,17 +470,16 @@ private enum PhaseType: Equatable { // swiftlint:disable:this one_declaration_pe } init(_ action: AppStoreAction, rawValue: Int64?) { - self = - switch rawValue { - case 0: - .downloading - case 1: - .performing(action) - case 5: - .downloaded - default: - .processing - } + self = switch rawValue { + case 0: + .downloading + case 1: + .performing(action) + case 5: + .downloaded + default: + .processing + } } } diff --git a/Sources/mas/AppStore/AppStoreAction.swift b/Sources/mas/AppStore/AppStoreAction.swift index f0f4ee08..24c55258 100644 --- a/Sources/mas/AppStore/AppStoreAction.swift +++ b/Sources/mas/AppStore/AppStoreAction.swift @@ -37,17 +37,8 @@ enum AppStoreAction { } } - func apps( - withAppIDs appIDs: [AppID], - force: Bool, - installedApps: [InstalledApp], - lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp, - ) async throws { - try await apps( - withADAMIDs: await appIDs.lookupCatalogApps(using: lookupAppFromAppID).map(\.adamID), - force: force, - installedApps: installedApps, - ) + func apps(withAppIDs appIDs: [AppID], force: Bool, installedApps: [InstalledApp]) async throws { + try await apps(withADAMIDs: await appIDs.catalogApps.map(\.adamID), force: force, installedApps: installedApps) } func apps(withADAMIDs adamIDs: [ADAMID], force: Bool, installedApps: [InstalledApp]) async throws { diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/Config.swift index a98db704..1292cf78 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/Config.swift @@ -42,7 +42,7 @@ extension MAS { private var runningSliceArchitecture: String { var info = utsname() return unsafe uname(&info) == 0 - ? withUnsafePointer(to: &info.machine) { pointer in // swiftformat:disable indent + ? unsafe withUnsafePointer(to: &info.machine) { pointer in // swiftformat:disable indent unsafe pointer.withMemoryRebound( to: CChar.self, capacity: unsafe MemoryLayout.size(ofValue: unsafe pointer), diff --git a/Sources/mas/Commands/Get.swift b/Sources/mas/Commands/Get.swift index 2089e449..6f2ac5b5 100644 --- a/Sources/mas/Commands/Get.swift +++ b/Sources/mas/Commands/Get.swift @@ -26,7 +26,6 @@ extension MAS { withAppIDs: catalogAppIDsOptionGroup.appIDs, force: forceOptionGroup.force, installedApps: try await installedApps, - lookupAppFromAppID: lookup(appID:), ) } } diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index d99032c0..4746d392 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -23,18 +23,14 @@ extension MAS { private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup func run() async { - await run(lookupAppFromAppID: lookup(appID:)) + await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.catalogApps) } - private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async { - await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID)) - } - - func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations + func run(catalogApps: [CatalogApp]) async { await run(appStorePageURLStrings: catalogApps.map(\.appStorePageURLString)) } - private func run(appStorePageURLStrings: [String]) async { // swiftformat:disable:this organizeDeclarations + private func run(appStorePageURLStrings: [String]) async { await appStorePageURLStrings.forEach(attemptTo: "open") { appStorePageURLString in guard let url = URL(string: appStorePageURLString) else { throw MASError.unparsableURL(appStorePageURLString) diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 8e7332a7..17c36137 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -25,7 +25,6 @@ extension MAS { withAppIDs: catalogAppIDsOptionGroup.appIDs, force: forceOptionGroup.force, installedApps: try await installedApps, - lookupAppFromAppID: lookup(appID:), ) } } diff --git a/Sources/mas/Commands/Lookup.swift b/Sources/mas/Commands/Lookup.swift index 2ecb089d..b946cfac 100644 --- a/Sources/mas/Commands/Lookup.swift +++ b/Sources/mas/Commands/Lookup.swift @@ -24,14 +24,10 @@ extension MAS { private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup func run() async { - await run(lookupAppFromAppID: lookup(appID:)) + run(catalogApps: await catalogAppIDsOptionGroup.appIDs.catalogApps) } - private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async { - run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID)) - } - - func run(catalogApps: [CatalogApp]) { // swiftformat:disable:this organizeDeclarations + func run(catalogApps: [CatalogApp]) { printer.info( catalogApps.map { catalogApp in """ diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index a81c10d9..e6469643 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -27,15 +27,12 @@ extension MAS { private var searchTermOptionGroup: SearchTermOptionGroup func run() async throws { - try await run(installedApps: try await installedApps, searchForAppsMatchingSearchTerm: search(for:)) + try await run(installedApps: try await installedApps) } - private func run( - installedApps: [InstalledApp], - searchForAppsMatchingSearchTerm: (String) async throws -> [CatalogApp], - ) async throws { + private func run(installedApps: [InstalledApp]) async throws { let searchTerm = searchTermOptionGroup.searchTerm - guard let adamID = try await searchForAppsMatchingSearchTerm(searchTerm).first?.adamID else { + guard let adamID = try await Dependencies.current.searchForAppsMatchingSearchTerm(searchTerm).first?.adamID else { throw MASError.noCatalogAppsFound(for: searchTerm) } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 949d552c..a7c1f451 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -27,11 +27,13 @@ extension MAS { private var appIDString: String? func run() async throws { - try await run(lookupAppFromAppID: lookup(appID:)) - } - - private func run(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async throws { - try await run(appStorePageURLString: appStorePageURLString(lookupAppFromAppID: lookupAppFromAppID)) + try await run( + appStorePageURLString: appIDString.map { appIDString in + try await Dependencies.current // swiftformat:disable:next indent + .lookupAppFromAppID(AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID)) + .appStorePageURLString // swiftformat:disable:this indent + }, + ) } private func run(appStorePageURLString: String?) async throws { @@ -43,17 +45,6 @@ extension MAS { try await openMacAppStorePage(forAppStorePageURLString: appStorePageURLString) } - - private func appStorePageURLString(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async throws -> String? { - guard let appIDString else { - return nil - } - - return try await lookupAppFromAppID( - AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID), - ) - .appStorePageURLString - } } } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index bfea0ce1..31c890aa 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -24,17 +24,13 @@ extension MAS { private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup func run() async throws { - await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:)) + await run(installedApps: try await installedApps.filter(!\.isTestFlight)) } - private func run( - installedApps: [InstalledApp], - lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp, - ) async { + private func run(installedApps: [InstalledApp]) async { run( outdatedApps: await installedApps.outdatedApps( filterFor: installedAppIDsOptionGroup.appIDs, - lookupAppFromAppID: lookupAppFromAppID, accuracy: outdatedAppOptionGroup.accuracy, shouldCheckMinimumOSVersion: outdatedAppOptionGroup.shouldCheckMinimumOSVersion, shouldWarnIfUnknownApp: verboseOptionGroup.verbose, diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 4f011a03..fe42886d 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -25,14 +25,12 @@ extension MAS { private var searchTermOptionGroup: SearchTermOptionGroup func run() async throws { - try await run(searchForAppsMatchingSearchTerm: search(for:)) - } - - private func run(searchForAppsMatchingSearchTerm: (String) async throws -> [CatalogApp]) async throws { - try run(catalogApps: try await searchForAppsMatchingSearchTerm(searchTermOptionGroup.searchTerm)) + try run( + catalogApps: try await Dependencies.current.searchForAppsMatchingSearchTerm(searchTermOptionGroup.searchTerm), + ) } - func run(catalogApps: [CatalogApp]) throws { // swiftformat:disable:this organizeDeclarations + func run(catalogApps: [CatalogApp]) throws { guard let maxADAMIDLength = catalogApps.map({ String(describing: $0.adamID).count }).max(), let maxNameLength = catalogApps.map(\.name.count).max() diff --git a/Sources/mas/Commands/Seller.swift b/Sources/mas/Commands/Seller.swift index 9596a94b..f43dcfc9 100644 --- a/Sources/mas/Commands/Seller.swift +++ b/Sources/mas/Commands/Seller.swift @@ -24,14 +24,10 @@ extension MAS { private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup func run() async { - await run(lookupAppFromAppID: lookup(appID:)) + await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.catalogApps) } - private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async { - await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID)) - } - - func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations + func run(catalogApps: [CatalogApp]) async { await run( sellerURLStrings: catalogApps.compactMap { catalogApp in guard let sellerURLString = catalogApp.sellerURLString else { @@ -44,7 +40,7 @@ extension MAS { ) } - private func run(sellerURLStrings: [String]) async { // swiftformat:disable:this organizeDeclarations + private func run(sellerURLStrings: [String]) async { await sellerURLStrings.forEach(attemptTo: "open") { sellerURLString in guard let url = URL(string: sellerURLString) else { throw MASError.unparsableURL(sellerURLString) diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 968c31e6..89e00929 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -122,7 +122,7 @@ extension MAS { printer.error( """ Failed to revert ownership of uninstalled \(appPath.quoted) back to uid \(appUID) & gid \(appGID):\ - failed to obtain uninstalled app URL + failed to get uninstalled app URL """, ) continue diff --git a/Sources/mas/Commands/Update.swift b/Sources/mas/Commands/Update.swift index 401ed804..b71cc118 100644 --- a/Sources/mas/Commands/Update.swift +++ b/Sources/mas/Commands/Update.swift @@ -27,19 +27,15 @@ extension MAS { private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup func run() async throws { - try await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:)) + try await run(installedApps: try await installedApps.filter(!\.isTestFlight)) } - private func run( - installedApps: [InstalledApp], - lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp, - ) async throws { + private func run(installedApps: [InstalledApp]) async throws { try await run( outdatedApps: forceOptionGroup.force // swiftformat:disable:next indent ? installedApps.filter(for: installedAppIDsOptionGroup.appIDs).map { ($0, "") } : await installedApps.outdatedApps( filterFor: installedAppIDsOptionGroup.appIDs, - lookupAppFromAppID: lookupAppFromAppID, accuracy: outdatedAppOptionGroup.accuracy, shouldCheckMinimumOSVersion: outdatedAppOptionGroup.shouldCheckMinimumOSVersion, shouldWarnIfUnknownApp: verboseOptionGroup.verbose, diff --git a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift index c776ad25..7cf2f9ae 100644 --- a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift +++ b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift @@ -24,58 +24,51 @@ func lookup(appID: AppID) async throws -> CatalogApp { /// - Returns: A `CatalogApp` for the given `appID` if `appID` is valid. /// - Throws: A `MASError.unknownAppID(appID)` if `appID` is invalid. /// Some other `Error` if any other problem occurs. -func lookup( - appID: AppID, - inRegion region: Region = appStoreRegion, - dataFrom dataSource: (URL) async throws -> (Data, URLResponse) = urlSession.data(from:), -) async throws -> CatalogApp { - let queryItem = - switch appID { - case let .adamID(adamID): - URLQueryItem(name: "id", value: .init(adamID)) - case let .bundleID(bundleID): - URLQueryItem(name: "bundleId", value: bundleID) - } - guard // swiftformat:disable:this wrap wrapArguments +func lookup(appID: AppID, inRegion region: Region = appStoreRegion) async throws -> CatalogApp { + let queryItem = switch appID { + case let .adamID(adamID): + URLQueryItem(name: "id", value: .init(adamID)) + case let .bundleID(bundleID): + URLQueryItem(name: "bundleId", value: bundleID) + } + return if // swiftformat:disable:this wrap wrapArguments let catalogApp = // swiftformat:disable:next indent - try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region), dataFrom: dataSource).first - else { - guard - let catalogApp = try await getCatalogApps( - from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: []), - dataFrom: dataSource, - ) - .first, - catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") ?? false - else { - throw MASError.unknownAppID(appID) + try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region)).first + { + catalogApp + } else { + try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: [])) + .first // swiftformat:disable indent + .flatMap { catalogApp in + catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") == true + ? catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersionFromAppStorePage) + : nil } - - return catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersion(dataFrom: dataSource)) - } - - return catalogApp + ?? { throw MASError.unknownAppID(appID) }() + } // swiftformat:enable indent } private extension CatalogApp { - func minimumOSVersion(dataFrom: (URL) async throws -> (Data, URLResponse) = urlSession.data(from:)) async -> String { - do { - return try await URL(string: appStorePageURLString) - .flatMap { url in // swiftformat:disable indent - try unsafe SwiftSoup.parse(try await dataFrom(url).0, appStorePageURLString) - .select("#serialized-server-data") - .first()? - .data() - .query( - string: - "$.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text", - )? - .firstMatch(of: minimumOSVersionRegex)? - .version + var minimumOSVersionFromAppStorePage: String { + get async { + do { + return try await URL(string: appStorePageURLString) + .flatMap { url in // swiftformat:disable indent + try unsafe SwiftSoup.parse(try await Dependencies.current.dataFrom(url).0, appStorePageURLString) + .select("#serialized-server-data") + .first()? + .data() + .query( + string: + "$.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text", + )? + .firstMatch(of: minimumOSVersionRegex)? + .version + } + .map(String.init(_:)) ?? minimumOSVersion // swiftformat:enable indent + } catch { + return minimumOSVersion } - .map(String.init(_:)) ?? minimumOSVersion // swiftformat:enable indent - } catch { - return minimumOSVersion } } } @@ -94,22 +87,15 @@ func search(for searchTerm: String) async throws -> [CatalogApp] { /// search for apps. /// - Returns: A `[CatalogApp]` matching `searchTerm`. /// - Throws: An `Error` if any problem occurs. -func search( - for searchTerm: String, - inRegion region: Region = appStoreRegion, - dataFrom dataSource: @escaping @Sendable (URL) async throws -> (Data, URLResponse) = urlSession.data(from:), -) async throws -> [CatalogApp] { +func search(for searchTerm: String, inRegion region: Region = appStoreRegion) async throws -> [CatalogApp] { let queryItem = URLQueryItem(name: "term", value: searchTerm) - let catalogApps = try await getCatalogApps(from: try url("search", queryItem, inRegion: region), dataFrom: dataSource) + let catalogApps = try await getCatalogApps(from: try url("search", queryItem, inRegion: region)) let adamIDSet = Set(catalogApps.map(\.adamID)) - return catalogApps.priorityMerge( - try await getCatalogApps( - from: try url("search", queryItem, inRegion: region, additionalQueryItems: []), - dataFrom: dataSource, - ) - .filter { ($0.supportedDevices?.contains("MacDesktop-MacDesktop") ?? false) && !adamIDSet.contains($0.adamID) } - .concurrentMap { $0.with(minimumOSVersion: await $0.minimumOSVersion(dataFrom: dataSource)) }, - ) { $0.name.similarity(to: searchTerm) } + return catalogApps.priorityMerge( // swiftformat:disable indent + try await getCatalogApps(from: try url("search", queryItem, inRegion: region, additionalQueryItems: [])) + .filter { ($0.supportedDevices?.contains("MacDesktop-MacDesktop") == true) && !adamIDSet.contains($0.adamID) } + .concurrentMap { $0.with(minimumOSVersion: await $0.minimumOSVersionFromAppStorePage) }, + ) { $0.name.similarity(to: searchTerm) } // swiftformat:enable indent } private func url( @@ -133,9 +119,8 @@ private func url( ) // swiftformat:enable indent } -private func getCatalogApps(from url: URL, dataFrom: (URL) async throws -> (Data, URLResponse)) -async throws -> [CatalogApp] { // swiftformat:disable:this indent - let (data, _) = try await dataFrom(url) +private func getCatalogApps(from url: URL) async throws -> [CatalogApp] { + let (data, _) = try await Dependencies.current.dataFrom(url) do { return try JSONDecoder().decode(CatalogAppResults.self, from: data).results } catch { @@ -143,5 +128,4 @@ async throws -> [CatalogApp] { // swiftformat:disable:this indent } } -private let urlSession = URLSession(configuration: .ephemeral) private nonisolated(unsafe) let minimumOSVersionRegex = /macOS\s*(?\S+)/ diff --git a/Sources/mas/Models/AppID.swift b/Sources/mas/Models/AppID.swift index 0abe27f0..4d804ef8 100644 --- a/Sources/mas/Models/AppID.swift +++ b/Sources/mas/Models/AppID.swift @@ -33,9 +33,10 @@ enum AppID: CustomStringConvertible { } extension [AppID] { // swiftlint:disable:this file_types_order - func lookupCatalogApps(using lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) - async -> [CatalogApp] { // swiftformat:disable:this indent - await concurrentCompactMap(attemptingTo: "lookup app for", lookupAppFromAppID) + var catalogApps: [CatalogApp] { + get async { + await concurrentCompactMap(attemptingTo: "lookup app for", Dependencies.current.lookupAppFromAppID) + } } } diff --git a/Sources/mas/Models/OutdatedApp.swift b/Sources/mas/Models/OutdatedApp.swift index 230d0d6c..c1ba330f 100644 --- a/Sources/mas/Models/OutdatedApp.swift +++ b/Sources/mas/Models/OutdatedApp.swift @@ -17,8 +17,7 @@ typealias OutdatedApp = ( extension [InstalledApp] { func outdatedApps( - filterFor appIDs: [AppID], // swiftlint:disable:next unneeded_escaping - lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp, + filterFor appIDs: [AppID], accuracy: OutdatedAccuracy, shouldCheckMinimumOSVersion: Bool, shouldWarnIfUnknownApp: Bool, @@ -26,7 +25,7 @@ extension [InstalledApp] { @Sendable func installableCatalogApp(from installedApp: InstalledApp) async -> CatalogApp? { do { - let catalogApp = try await lookupAppFromAppID(.bundleID(installedApp.bundleID)) + let catalogApp = try await Dependencies.current.lookupAppFromAppID(.bundleID(installedApp.bundleID)) return shouldCheckMinimumOSVersion // swiftformat:disable indent && UniversalSemVerInt(from: catalogApp.minimumOSVersion).flatMap { minimumOSVersion in ProcessInfo.processInfo.isOperatingSystemAtLeast( @@ -39,7 +38,7 @@ extension [InstalledApp] { } == false ? nil : catalogApp } catch { // swiftformat:enable indent - if let error = error as? MASError, case MASError.unknownAppID = error { + if case MASError.unknownAppID = error { if shouldWarnIfUnknownApp { MAS.printer.warning(error, "; was expected to identify: ", installedApp.name, separator: "") } @@ -54,28 +53,29 @@ extension [InstalledApp] { accuracy == .accurate ? { @Sendable installedApp in // swiftformat:disable indent if shouldCheckMinimumOSVersion, await installableCatalogApp(from: installedApp) == nil { - return nil - } - return await withCheckedContinuation { continuation in - Task { - let alreadyResumed = ManagedAtomic(false) - do { - try await AppStore.install.app(withADAMID: installedApp.adamID) { appStoreVersion, shouldOutput in - if - shouldOutput, - let appStoreVersion, - installedApp.version != appStoreVersion, - !alreadyResumed.exchange(true, ordering: .acquiringAndReleasing) - { - continuation.resume(returning: OutdatedApp(installedApp, appStoreVersion)) + nil + } else { + await withCheckedContinuation { continuation in + Task { + let alreadyResumed = ManagedAtomic(false) + do { + try await AppStore.install.app(withADAMID: installedApp.adamID) { appStoreVersion, shouldOutput in + if + shouldOutput, + let appStoreVersion, + installedApp.version != appStoreVersion, + !alreadyResumed.exchange(true, ordering: .acquiringAndReleasing) + { + continuation.resume(returning: OutdatedApp(installedApp, appStoreVersion)) + } + return true } - return true + } catch { + MAS.printer.error(error: error) + } + if !alreadyResumed.load(ordering: .acquiring) { + continuation.resume(returning: nil) } - } catch { - MAS.printer.error(error: error) - } - if !alreadyResumed.load(ordering: .acquiring) { - continuation.resume(returning: nil) } } } diff --git a/Sources/mas/Utilities/Dependencies.swift b/Sources/mas/Utilities/Dependencies.swift new file mode 100644 index 00000000..aa72e11f --- /dev/null +++ b/Sources/mas/Utilities/Dependencies.swift @@ -0,0 +1,28 @@ +// +// Dependencies.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +internal import Foundation + +struct Dependencies { + @TaskLocal + static var current = Self() + + let dataFrom: @Sendable (URL) async throws -> (Data, URLResponse) + let lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp + let searchForAppsMatchingSearchTerm: @Sendable (String) async throws -> [CatalogApp] + + init( + dataFrom: @escaping @Sendable (URL) async throws -> (Data, URLResponse) + = URLSession(configuration: .ephemeral).data(from:), // swiftformat:disable:this indent + lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp = lookup(appID:), + searchForAppsMatchingSearchTerm: @escaping @Sendable (String) async throws -> [CatalogApp] = search(for:), + ) { + self.dataFrom = dataFrom + self.lookupAppFromAppID = lookupAppFromAppID + self.searchForAppsMatchingSearchTerm = searchForAppsMatchingSearchTerm + } +} diff --git a/Sources/mas/Utilities/Sequence.swift b/Sources/mas/Utilities/Sequence.swift index 63ea0576..a882edaf 100644 --- a/Sources/mas/Utilities/Sequence.swift +++ b/Sources/mas/Utilities/Sequence.swift @@ -37,8 +37,8 @@ extension Sequence { var primaryIterator = makeIterator() var secondaryIterator = secondary.makeIterator() - var primaryItemAndScore: (item: Element, score: Double)? = primaryIterator.next().map { ($0, score($0)) } - var secondaryItemAndScore: (item: Element, score: Double)? = secondaryIterator.next().map { ($0, score($0)) } + var primaryItemAndScore = primaryIterator.next().map { (item: $0, score: score($0)) } + var secondaryItemAndScore = secondaryIterator.next().map { (item: $0, score: score($0)) } while let primaryInfo = primaryItemAndScore, let secondaryInfo = secondaryItemAndScore { if primaryInfo.score >= secondaryInfo.score { diff --git a/Tests/MASTests/Controllers/MASTests+CatalogApp+ITunesSearch.swift b/Tests/MASTests/Controllers/MASTests+CatalogApp+ITunesSearch.swift index 502d6a25..c9310cf5 100644 --- a/Tests/MASTests/Controllers/MASTests+CatalogApp+ITunesSearch.swift +++ b/Tests/MASTests/Controllers/MASTests+CatalogApp+ITunesSearch.swift @@ -13,7 +13,9 @@ private extension MASTests { @Test func `iTunes searches for slack`() async { let actual = await consequencesOf( - try await search(for: "slack") { _ in try (Data(fromResource: "slack"), URLResponse()) }.count, + try await Dependencies.$current.withValue(.init { _ in (try Data(fromResource: "slack"), URLResponse()) }) { + try await search(for: "slack").count + }, ) let expected = Consequences(39) #expect(actual == expected) @@ -23,7 +25,11 @@ private extension MASTests { func `looks up slack`() async { let adamID = 803_453_959 as ADAMID let actual = await consequencesOf( - try await lookup(appID: .adamID(adamID)) { _ in try (Data(fromResource: "slack-lookup"), URLResponse()) }, + try await Dependencies.$current.withValue( + .init { _ in (try Data(fromResource: "slack-lookup"), URLResponse()) }, + ) { + try await lookup(appID: .adamID(adamID)) + }, ) #expect(actual.error == nil && actual.stdout.isEmpty && actual.stderr.isEmpty) guard let catalogApp = actual.value else { @@ -34,9 +40,15 @@ private extension MASTests { #expect( catalogApp.adamID == adamID // swiftformat:disable indent && catalogApp.appStorePageURLString == "https://apps.apple.com/us/app/slack-for-desktop/id803453959?mt=12" + && catalogApp.bundleID == "com.tinyspeck.slackmacgap" + && catalogApp.fileSizeBytes == "74398324" + && catalogApp.formattedPrice == "Free" + && catalogApp.minimumOSVersion == "10.9" && catalogApp.name == "Slack" + && catalogApp.releaseDate == "2018-10-02T23:28:05Z" && catalogApp.sellerName == "Slack Technologies, Inc." && catalogApp.sellerURLString == "https://slack.com" + && catalogApp.supportedDevices == nil && catalogApp.version == "3.3.3", ) // swiftformat:enable indent } diff --git a/Tests/MASTests/Utilities/Consequences.swift b/Tests/MASTests/Utilities/Consequences.swift index fb40d826..987c2e57 100644 --- a/Tests/MASTests/Utilities/Consequences.swift +++ b/Tests/MASTests/Utilities/Consequences.swift @@ -28,19 +28,11 @@ struct Consequences { extension Consequences: Equatable where Value: Equatable { // swiftlint:disable:this file_types_order static func == (lhs: Self, rhs: Self) -> Bool { - guard lhs.value == rhs.value, lhs.stdout == rhs.stdout, lhs.stderr == rhs.stderr else { - return false - } - - return switch (lhs.error, rhs.error) { - case (nil, nil): - true - case let (lhsError?, rhsError?): - (lhsError as NSError) == (rhsError as NSError) - default: - false - } - } + lhs.value == rhs.value + && lhs.stdout == rhs.stdout // swiftformat:disable indent + && lhs.stderr == rhs.stderr + && lhs.error as NSError? == rhs.error as NSError? + } // swiftformat:enable indent } private struct StandardStreamCapture { // swiftlint:disable:this one_declaration_per_file