From c45b933259fb52e79846fd0f614771b4776b114c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:24:28 -0400 Subject: [PATCH 01/15] Update dependencies. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .markdownlint-cli2.yaml | 2 +- Brewfile | 6 +++--- Package.resolved | 12 ++++++------ Package.swift | 6 +++--- .../include/CommerceKit/CKDownloadQueue.h | 2 +- .../include/CommerceKit/CKPurchaseController.h | 2 +- .../include/CommerceKit/CKServiceInterface.h | 2 +- .../include/CommerceKit/CommerceKit.h | 2 +- .../StoreFoundation/ISAccountService-Protocol.h | 2 +- .../include/StoreFoundation/ISServiceProxy.h | 2 +- .../include/StoreFoundation/ISStoreAccount.h | 2 +- .../include/StoreFoundation/SSDownload.h | 2 +- .../include/StoreFoundation/SSDownloadMetadata.h | 2 +- .../include/StoreFoundation/SSDownloadPhase.h | 2 +- .../include/StoreFoundation/SSDownloadStatus.h | 2 +- .../include/StoreFoundation/SSPurchase.h | 2 +- .../include/StoreFoundation/SSPurchaseResponse.h | 2 +- .../include/StoreFoundation/StoreFoundation.h | 2 +- 18 files changed, 27 insertions(+), 27 deletions(-) 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/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/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/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 From e372176c2ed019bf8baab31793f191c3eb83cae7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:57:15 -0400 Subject: [PATCH 02/15] Color non-executable scripts red when linting. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Scripts/lint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a2b0d0261ab9df978ef7bfc08843d3de1c9cb0b7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 03/15] =?UTF-8?q?Simplify=20`[InstalledApp].outdatedApps(?= =?UTF-8?q?=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Models/OutdatedApp.swift | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Sources/mas/Models/OutdatedApp.swift b/Sources/mas/Models/OutdatedApp.swift index 230d0d6c..74cb597e 100644 --- a/Sources/mas/Models/OutdatedApp.swift +++ b/Sources/mas/Models/OutdatedApp.swift @@ -54,28 +54,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) } } } From b61e2e6250aeba8a584a96b7be3956e03bd71335 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 04/15] Disable `wrapMultilineConditionalAssignment` SwiftFormat rule. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 2 +- .../AppStore/AppStoreAction+download.swift | 21 +++++++++---------- .../Controllers/CatalogApp+ITunesSearch.swift | 13 ++++++------ 3 files changed, 17 insertions(+), 19 deletions(-) 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/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 350e9ac7..45975db4 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -469,17 +469,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/Controllers/CatalogApp+ITunesSearch.swift b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift index c776ad25..02f93aad 100644 --- a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift +++ b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift @@ -29,13 +29,12 @@ func lookup( 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) - } + 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 let catalogApp = // swiftformat:disable:next indent try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region), dataFrom: dataSource).first From e4bb45eb4eae8accad111ec97fc20b56e1bd53f0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 05/15] =?UTF-8?q?Simplify=20`lookup(=E2=80=A6)`=20&=20`sea?= =?UTF-8?q?rch(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Controllers/CatalogApp+ITunesSearch.swift | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift index 02f93aad..712998bd 100644 --- a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift +++ b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift @@ -35,25 +35,24 @@ func lookup( case let .bundleID(bundleID): URLQueryItem(name: "bundleId", value: bundleID) } - guard // swiftformat:disable:this wrap wrapArguments + 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) + { + catalogApp + } else { + try await getCatalogApps( + from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: []), + dataFrom: dataSource, + ) + .first + .flatMap { catalogApp in + catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") == true // swiftformat:disable:next indent + ? catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersion(dataFrom: dataSource)) + : nil } - - return catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersion(dataFrom: dataSource)) + ?? { throw MASError.unknownAppID(appID) }() } - - return catalogApp } private extension CatalogApp { @@ -106,7 +105,7 @@ func search( from: try url("search", queryItem, inRegion: region, additionalQueryItems: []), dataFrom: dataSource, ) - .filter { ($0.supportedDevices?.contains("MacDesktop-MacDesktop") ?? false) && !adamIDSet.contains($0.adamID) } + .filter { ($0.supportedDevices?.contains("MacDesktop-MacDesktop") == true) && !adamIDSet.contains($0.adamID) } .concurrentMap { $0.with(minimumOSVersion: await $0.minimumOSVersion(dataFrom: dataSource)) }, ) { $0.name.similarity(to: searchTerm) } } From 9b3d59493beb4eb61af782d8d37c48b5394b836c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 06/15] Elide `Open.appStorePageURLString(lookupAppFromAppID:)`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Open.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 949d552c..63b06ce0 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -31,7 +31,12 @@ extension MAS { } 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 lookupAppFromAppID(AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID)) + .appStorePageURLString // swiftformat:disable:this indent + }, + ) } private func run(appStorePageURLString: String?) async throws { @@ -43,17 +48,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 - } } } From 8bdf9e49d97562458eb0fbdc0a225ea6dd907e27 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 07/15] Improve switch multi-case formatting. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/AppStoreAction+download.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 45975db4..d62723b1 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -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: From a5380ffab983a9e312c71a5be06d04d680640064 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 08/15] Use inferred instead of explicit types. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Documentation/Sample.swift | 2 +- Sources/mas/Utilities/Sequence.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 { From 8663ee405d5e6b06b5a7aebae455fe3819426dce Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:26:44 -0400 Subject: [PATCH 09/15] Do not unnecessarily cast error. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Models/OutdatedApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mas/Models/OutdatedApp.swift b/Sources/mas/Models/OutdatedApp.swift index 74cb597e..2646a630 100644 --- a/Sources/mas/Models/OutdatedApp.swift +++ b/Sources/mas/Models/OutdatedApp.swift @@ -39,7 +39,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: "") } From 9df0fcdad3432cefe85d7b1de807f02bf980faa1 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:21:49 -0400 Subject: [PATCH 10/15] Insert missing `unsafe`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Config.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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), From d80b26e685b9aae497f64ec0804e17102e57239a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:18:35 -0400 Subject: [PATCH 11/15] Reword "obtain" as "get". Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/AppStoreAction+download.swift | 2 +- Sources/mas/Commands/Uninstall.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index d62723b1..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 } 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 From f01cbce4e8c03778dd6b9b69f4349a7ccdf31d0e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 15 Mar 2026 05:57:05 -0400 Subject: [PATCH 12/15] Simplify `Consequences.==`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/MASTests/Utilities/Consequences.swift | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) 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 From ee9403d3b2c697141b9a45ab2f34ad374e132370 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 13/15] Inject `lookupAppFromAppID` dependency. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 2 +- Sources/mas/AppStore/AppStoreAction.swift | 13 ++----------- Sources/mas/Commands/Get.swift | 1 - Sources/mas/Commands/Home.swift | 10 +++------- Sources/mas/Commands/Install.swift | 1 - Sources/mas/Commands/Lookup.swift | 8 ++------ Sources/mas/Commands/Open.swift | 7 ++----- Sources/mas/Commands/Outdated.swift | 8 ++------ Sources/mas/Commands/Seller.swift | 10 +++------- Sources/mas/Commands/Update.swift | 8 ++------ Sources/mas/Models/AppID.swift | 7 ++++--- Sources/mas/Models/OutdatedApp.swift | 5 ++--- Sources/mas/Utilities/Dependencies.swift | 17 +++++++++++++++++ 13 files changed, 40 insertions(+), 57 deletions(-) create mode 100644 Sources/mas/Utilities/Dependencies.swift 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/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/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/Open.swift b/Sources/mas/Commands/Open.swift index 63b06ce0..a7c1f451 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -27,13 +27,10 @@ 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: appIDString.map { appIDString in - try await lookupAppFromAppID(AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID)) + try await Dependencies.current // swiftformat:disable:next indent + .lookupAppFromAppID(AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID)) .appStorePageURLString // swiftformat:disable:this indent }, ) 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/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/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/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 2646a630..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( diff --git a/Sources/mas/Utilities/Dependencies.swift b/Sources/mas/Utilities/Dependencies.swift new file mode 100644 index 00000000..42941f18 --- /dev/null +++ b/Sources/mas/Utilities/Dependencies.swift @@ -0,0 +1,17 @@ +// +// Dependencies.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +struct Dependencies { + @TaskLocal + static var current = Self() + + let lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp + + init(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp = lookup(appID:)) { + self.lookupAppFromAppID = lookupAppFromAppID + } +} From c82a9bb0cbd8adc58a4bec8a37186e6ed9e1ea59 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:15 -0400 Subject: [PATCH 14/15] Inject `dataFrom` dependency. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Controllers/CatalogApp+ITunesSearch.swift | 82 ++++++++----------- Sources/mas/Utilities/Dependencies.swift | 10 ++- .../MASTests+CatalogApp+ITunesSearch.swift | 16 +++- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift index 712998bd..7cf2f9ae 100644 --- a/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift +++ b/Sources/mas/Controllers/CatalogApp+ITunesSearch.swift @@ -24,11 +24,7 @@ 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 { +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)) @@ -37,43 +33,42 @@ func lookup( } 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 + try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region)).first { catalogApp } else { - try await getCatalogApps( - from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: []), - dataFrom: dataSource, - ) - .first + try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: [])) + .first // swiftformat:disable indent .flatMap { catalogApp in - catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") == true // swiftformat:disable:next indent - ? catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersion(dataFrom: dataSource)) + catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") == true + ? catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersionFromAppStorePage) : nil } ?? { 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 } } } @@ -92,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, - ) + 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.minimumOSVersion(dataFrom: dataSource)) }, - ) { $0.name.similarity(to: searchTerm) } + .concurrentMap { $0.with(minimumOSVersion: await $0.minimumOSVersionFromAppStorePage) }, + ) { $0.name.similarity(to: searchTerm) } // swiftformat:enable indent } private func url( @@ -131,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 { @@ -141,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/Utilities/Dependencies.swift b/Sources/mas/Utilities/Dependencies.swift index 42941f18..3e7a585b 100644 --- a/Sources/mas/Utilities/Dependencies.swift +++ b/Sources/mas/Utilities/Dependencies.swift @@ -5,13 +5,21 @@ // 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 - init(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp = lookup(appID:)) { + 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:), + ) { + self.dataFrom = dataFrom self.lookupAppFromAppID = lookupAppFromAppID } } 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 } From 681c587a777b1ed24dec925de18a77b1861126a6 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:00:16 -0400 Subject: [PATCH 15/15] Inject `searchForAppsMatchingSearchTerm` dependency. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 9 +++------ Sources/mas/Commands/Search.swift | 10 ++++------ Sources/mas/Utilities/Dependencies.swift | 3 +++ 3 files changed, 10 insertions(+), 12 deletions(-) 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/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/Utilities/Dependencies.swift b/Sources/mas/Utilities/Dependencies.swift index 3e7a585b..aa72e11f 100644 --- a/Sources/mas/Utilities/Dependencies.swift +++ b/Sources/mas/Utilities/Dependencies.swift @@ -13,13 +13,16 @@ struct Dependencies { 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 } }