From 873d9ad331e762babde7945f39ff1390d125ab22 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:04:06 -0400 Subject: [PATCH 01/11] Update dependencies. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 2 +- Brewfile | 4 ++-- Sources/mas/Commands/Config.swift | 2 +- Sources/mas/Models/InstalledApp.swift | 2 +- Sources/mas/Models/MASError.swift | 2 +- Sources/mas/Utilities/Processes/Sudo.swift | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.swiftformat b/.swiftformat index 42f609e61..32062d41a 100644 --- a/.swiftformat +++ b/.swiftformat @@ -2,7 +2,7 @@ # .swiftformat # mas # -# SwiftFormat 0.61.0 +# SwiftFormat 0.61.1 # # Disabled rules (enabled by default) diff --git a/Brewfile b/Brewfile index 3b93bcc5a..1f28ebc01 100644 --- a/Brewfile +++ b/Brewfile @@ -1,11 +1,11 @@ brew "actionlint" # 1.7.12 brew "editorconfig-checker" # 3.6.1 -brew "gh" # 2.91.0 +brew "gh" # 2.92.0 brew "git" # 2.54.0 brew "ipsw" # 3.1.672 brew "markdownlint-cli2" # 0.22.1 brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.7.4 brew "shellcheck" # 0.11.0 -brew "swiftformat" # 0.61.0 +brew "swiftformat" # 0.61.1 brew "swiftlint" # 0.63.2 brew "yamllint" # 1.38.0 diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/Config.swift index 9c632745b..4ccae875c 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/Config.swift @@ -102,7 +102,7 @@ private func configStringValue(_ name: String) -> String { return unknown } - return unsafe .init(cString: &buffer) // swiftformat:disable:this spaceAroundOperators + return unsafe .init(cString: &buffer) } private let unknown = "unknown" diff --git a/Sources/mas/Models/InstalledApp.swift b/Sources/mas/Models/InstalledApp.swift index 7a8ab2a21..f3973cb2d 100644 --- a/Sources/mas/Models/InstalledApp.swift +++ b/Sources/mas/Models/InstalledApp.swift @@ -127,7 +127,7 @@ private extension JSON.Node { case let array as [Any?]: .array(.init(array.map { .init(for: $0) })) default: - value.map { .string(.init(describing: $0)) } ?? .null // swiftformat:disable:this indent + value.map { .string(.init(describing: $0)) } ?? .null } } } diff --git a/Sources/mas/Models/MASError.swift b/Sources/mas/Models/MASError.swift index 624e5e8f1..1daf23ee4 100644 --- a/Sources/mas/Models/MASError.swift +++ b/Sources/mas/Models/MASError.swift @@ -20,7 +20,7 @@ enum MASError: Error { ) -> Self { .error( message, - cause: cause.map { Self.error($0) }, // swiftformat:disable:this redundantStaticSelf + cause: cause.map { Self.error($0) }, separatorWhenCause: separatorWhenCause, separatorWhenNoCause: separatorWhenNoCause, ) diff --git a/Sources/mas/Utilities/Processes/Sudo.swift b/Sources/mas/Utilities/Processes/Sudo.swift index 2d4e87a0d..83c008a00 100644 --- a/Sources/mas/Utilities/Processes/Sudo.swift +++ b/Sources/mas/Utilities/Processes/Sudo.swift @@ -30,7 +30,7 @@ private func sudo(_ args: some Sequence) throws { guard spawnStatus == 0 else { throw MASError.error( "Failed to spawn installer process", - cause: unsafe .init(cString: strerror(spawnStatus)), // swiftformat:disable:this spaceAroundOperators + cause: unsafe .init(cString: strerror(spawnStatus)), separatorWhenCause: ": ", ) } From 5f8024e1fbe29e075e4ebfba13e3b2ee14bd862a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:02:53 -0400 Subject: [PATCH 02/11] Rename `numberCommas` as `number_commas` in `Scripts/mas`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Scripts/mas | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/mas b/Scripts/mas index 4a414a619..aef8c31e3 100755 --- a/Scripts/mas +++ b/Scripts/mas @@ -57,7 +57,7 @@ try ( lookup|info) { { exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -sr ' -def numberCommas($n): +def number_commas($n): ($n | abs | round | tostring) as $s | if $n < 0 then "-" else "" end + ([$s | while(length > 0; .[:-3]) | .[-3:]] | reverse | join(",")) ; @@ -81,7 +81,7 @@ try ( "\($key_map[$k]) \("▁" * ($max_key_length - ($key_map[$k] | length) + 1)) \( $in[$k] | if $k == "fileSizeBytes" then - numberCommas(tonumber? / 1e6 // 0) + " MB" + number_commas(tonumber? / 1e6 // 0) + " MB" elif $k == "currentVersionReleaseDate" then (fromdateiso8601? | strflocaltime("%Y-%m-%d")) // . else From 9579fb652e18f5b221b50f6a87b91e81e7437da4 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:08:18 -0400 Subject: [PATCH 03/11] Cleanup `CatalogApp`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Models/CatalogApp.swift | 163 +++++++++--------- .../Utilities/Resources/Dependencies.swift | 4 + Sources/mas/Utilities/Swift/Required.swift | 17 ++ 3 files changed, 107 insertions(+), 77 deletions(-) create mode 100644 Sources/mas/Utilities/Swift/Required.swift diff --git a/Sources/mas/Models/CatalogApp.swift b/Sources/mas/Models/CatalogApp.swift index af56ce7b7..be490e508 100644 --- a/Sources/mas/Models/CatalogApp.swift +++ b/Sources/mas/Models/CatalogApp.swift @@ -22,51 +22,6 @@ struct CatalogApp { private let json: Lazy - fileprivate init?(macDesktopAppObject object: JSON.Object) async throws { - guard try object["supportedDevices"]?.decode(to: [String]?.self)?.contains("MacDesktop-MacDesktop") == true else { - return nil - } - - let appStorePageURLString: String = try object["trackViewUrl"]?.decode() ?? "" - guard - let minimumOSVersion = try? await URL(string: appStorePageURLString) - .flatMap({ url in - try SwiftSoup.parse(try await Dependencies.current.dataFrom(url).0, appStorePageURLString) - .getElementById("serialized-server-data")? // swiftformat:disable:this acronyms - .data() - .query( - string: """ - $.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text - """, - )? - .firstMatch(of: minimumOSVersionRegex) - .map { String($0.version) } - }) - else { - return nil - } - - var jsonObject = object - let jsonMinimumOSVersion = try jsonObject[minimumOSVersionKey]?.decode() ?? "" - if jsonMinimumOSVersion != minimumOSVersion { - if let index = jsonObject.fields.firstIndex(where: { $0.key == minimumOSVersionKey }) { - jsonObject.fields[index] = (minimumOSVersionKey, .string(minimumOSVersion)) - } else { - jsonObject.fields.append((minimumOSVersionKey, .string(minimumOSVersion))) - } - } - - self.init( - adamID: try jsonObject["trackId"]?.decode() ?? 0, - appStorePageURLString: appStorePageURLString, - minimumOSVersion: minimumOSVersion, - name: try jsonObject["trackName"]?.decode() ?? "", - sellerURLString: try jsonObject["sellerUrl"]?.decode(), - version: try jsonObject["version"]?.decode() ?? "", - jsonObject: jsonObject, - ) - } - private init( adamID: ADAMID, appStorePageURLString: String, @@ -112,6 +67,50 @@ extension CatalogApp: JSONDecodable { jsonObject: object, ) } + + fileprivate init?(macDesktopAppObject object: JSON.Object) async throws { + guard + try object["supportedDevices"]?.decode(to: [String]?.self)?.contains("MacDesktop-MacDesktop") == true, + let appStorePageURLString = try object["trackViewUrl"]?.decode(to: String?.self), + let minimumOSVersion = try? await URL(string: appStorePageURLString) + .flatMap( + { url in + try SwiftSoup.parse(try await Dependencies.current.dataFrom(url).0, appStorePageURLString) + .getElementById("serialized-server-data")? // swiftformat:disable:this acronyms + .data() + .query( + string: """ + $.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text + """, + )? + .firstMatch(of: minimumOSVersionRegex) + .map { String($0.version) } + }, + ) + else { + return nil + } + + var object = object + let jsonMinimumOSVersion = try object[minimumOSVersionKey]?.decode() ?? "" + if jsonMinimumOSVersion != minimumOSVersion { + if let index = object.fields.firstIndex(where: { $0.key == minimumOSVersionKey }) { + object.fields[index] = (minimumOSVersionKey, .string(minimumOSVersion)) + } else { + object.fields.append((minimumOSVersionKey, .string(minimumOSVersion))) + } + } + + self.init( + adamID: try object["trackId"]?.decode() ?? 0, + appStorePageURLString: appStorePageURLString, + minimumOSVersion: minimumOSVersion, + name: try object["trackName"]?.decode() ?? "", + sellerURLString: try object["sellerUrl"]?.decode(), + version: try object["version"]?.decode() ?? "", + jsonObject: object, + ) + } } private extension JSON.Node { @@ -297,14 +296,26 @@ private func lookup(appID: AppID, inRegion region: Region = appStoreRegion) asyn case let .bundleID(bundleID): URLQueryItem(name: "bundleId", value: bundleID) } - return if let catalogAppJSONObject = try await catalogAppJSONObjects("lookup", queryItem, inRegion: region).first { + return if // swiftformat:disable:this wrap wrapArguments + let catalogAppJSONObject = try await catalogAppJSONObjects( + from: Dependencies.current.lookupURL, + queryItem, + inRegion: region, + ) + .first // swiftformat:disable:this indent + { try .init(object: catalogAppJSONObject) } else { - try await catalogAppJSONObjects("lookup", queryItem, inRegion: region, additionalQueryItems: .init()) - .first + try await catalogAppJSONObjects( + from: Dependencies.current.lookupURL, + queryItem, + inRegion: region, + additionalQueryItems: .init(), + ) + .first // swiftformat:disable indent .flatMap { try await CatalogApp(macDesktopAppObject: $0) } ?? { throw MASError.unknownAppID(appID) }() - } + } // swiftformat:enable indent } func search(for searchTerm: String) async throws -> [CatalogApp] { @@ -314,44 +325,42 @@ func search(for searchTerm: String) async throws -> [CatalogApp] { private func search(for searchTerm: String, inRegion region: Region = appStoreRegion) async throws -> [CatalogApp] { let queryItem = URLQueryItem(name: "term", value: searchTerm) let catalogApps = - try await catalogAppJSONObjects("search", queryItem, inRegion: region).map { try CatalogApp(object: $0) } + try await catalogAppJSONObjects(from: Dependencies.current.searchURL, queryItem, inRegion: region) + .map { try CatalogApp(object: $0) } let adamIDSet = Set(catalogApps.map(\.adamID)) return catalogApps.priorityMerge( - try await catalogAppJSONObjects("search", queryItem, inRegion: region, additionalQueryItems: .init()) - .concurrentCompactMap { catalogAppJSONObject in - guard - try catalogAppJSONObject["trackId"]?.decode(to: ADAMID?.self).map({ adamIDSet.contains($0) }) == false - else { - return nil - } - - return try await .init(macDesktopAppObject: catalogAppJSONObject) + try await catalogAppJSONObjects( + from: Dependencies.current.searchURL, + queryItem, + inRegion: region, + additionalQueryItems: .init(), + ) + .concurrentCompactMap { catalogAppJSONObject in // swiftformat:disable indent + try catalogAppJSONObject["trackId"]?.decode(to: ADAMID?.self).map { adamIDSet.contains($0) } == false + ? try await .init(macDesktopAppObject: catalogAppJSONObject) + : nil }, - ) { $0.name.similarity(to: searchTerm) } + ) { $0.name.similarity(to: searchTerm) } // swiftformat:enable indent } private func catalogAppJSONObjects( - _ action: String, + from url: URL, _ queryItem: URLQueryItem, inRegion region: Region, additionalQueryItems: [URLQueryItem] = [.init(name: "entity", value: "desktopSoftware")], ) async throws -> [JSON.Object] { - let urlString = "https://itunes.apple.com/\(action)" - return try await URL(string: urlString).map { url in - try await unsafe Dependencies.current - .dataFrom( - url.appending( - queryItems: [.init(name: "media", value: "software")] - + additionalQueryItems - + [.init(name: "country", value: region), queryItem], - ), - ) - .0 - .withUnsafeBytes { bufferPointer in - try CatalogAppResults(json: .init(parsing: unsafe RawSpan(_unsafeBytes: unsafe bufferPointer))).resultObjects - } - } - ?? { throw MASError.invalidURL(urlString) }() // swiftformat:disable:this indent + try await unsafe Dependencies.current + .dataFrom( + url.appending( + queryItems: [.init(name: "media", value: "software")] + + additionalQueryItems + + [.init(name: "country", value: region), queryItem], + ), + ) + .0 + .withUnsafeBytes { bufferPointer in + try CatalogAppResults(json: .init(parsing: unsafe RawSpan(_unsafeBytes: unsafe bufferPointer))).resultObjects + } } private let minimumOSVersionKey = JSON.Key("minimumOsVersion") diff --git a/Sources/mas/Utilities/Resources/Dependencies.swift b/Sources/mas/Utilities/Resources/Dependencies.swift index aa72e11f8..24624a4e1 100644 --- a/Sources/mas/Utilities/Resources/Dependencies.swift +++ b/Sources/mas/Utilities/Resources/Dependencies.swift @@ -11,6 +11,10 @@ struct Dependencies { @TaskLocal static var current = Self() + @Required(URL(string: "https://itunes.apple.com/lookup")) + var lookupURL: URL + @Required(URL(string: "https://itunes.apple.com/search")) + var searchURL: URL let dataFrom: @Sendable (URL) async throws -> (Data, URLResponse) let lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp let searchForAppsMatchingSearchTerm: @Sendable (String) async throws -> [CatalogApp] diff --git a/Sources/mas/Utilities/Swift/Required.swift b/Sources/mas/Utilities/Swift/Required.swift new file mode 100644 index 000000000..93ec87733 --- /dev/null +++ b/Sources/mas/Utilities/Swift/Required.swift @@ -0,0 +1,17 @@ +// +// Required.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +@propertyWrapper +struct Required { + let wrappedValue: Value + + init(_ value: @autoclosure () -> Value?, file: StaticString = #file, line: UInt = #line) { + wrappedValue = value() ?? { preconditionFailure("Required value was nil", file: file, line: line) }() + } +} + +extension Required: Sendable where Value: Sendable {} From 43108c456a8f5a7e85989a59eede15f4a14f15fe Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:46:17 -0400 Subject: [PATCH 04/11] Unindent lines starting with a `+` binary operator. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/AppStoreAction+download.swift | 2 +- Sources/mas/Commands/Reset.swift | 2 +- Sources/mas/Models/CatalogApp.swift | 4 ++-- Sources/mas/Utilities/Output/Printer.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 9340dc242..224b59fe9 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -287,7 +287,7 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver { MAS.printer.notice( [action.performed.uppercasingFirst, snapshot.appNameAndVersion] - + (appFolderURL.map { ["in", $0.filePath] } ?? .init()), + + (appFolderURL.map { ["in", $0.filePath] } ?? .init()), // swiftformat:disable:this indent ) if let appFolderURL { diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 76e671087..9124e3965 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -43,7 +43,7 @@ extension MAS { "/System/Library/PrivateFrameworks/AppStoreComponents.framework/Support/appstorecomponentsd", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoreagent", "/System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/XPCServices" - + "/SetStoreUpdateService.xpc/Contents/MacOS/SetStoreUpdateService", + + "/SetStoreUpdateService.xpc/Contents/MacOS/SetStoreUpdateService", // swiftformat:disable:this indent "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeaccountd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeassetd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storedownloadd", diff --git a/Sources/mas/Models/CatalogApp.swift b/Sources/mas/Models/CatalogApp.swift index be490e508..4b8257ab1 100644 --- a/Sources/mas/Models/CatalogApp.swift +++ b/Sources/mas/Models/CatalogApp.swift @@ -353,8 +353,8 @@ private func catalogAppJSONObjects( .dataFrom( url.appending( queryItems: [.init(name: "media", value: "software")] - + additionalQueryItems - + [.init(name: "country", value: region), queryItem], + + additionalQueryItems // swiftformat:disable:this indent + + [.init(name: "country", value: region), queryItem], // swiftformat:disable:this indent ), ) .0 diff --git a/Sources/mas/Utilities/Output/Printer.swift b/Sources/mas/Utilities/Output/Printer.swift index f161faf68..ebd6b0dc8 100644 --- a/Sources/mas/Utilities/Output/Printer.swift +++ b/Sources/mas/Utilities/Output/Printer.swift @@ -145,7 +145,7 @@ struct Printer { print( items.first.map { item in ["\(formattedPrefix) \(mas.indent(item, with: indent))"] - + items.dropFirst().map { mas.indent($0, with: indent) } + + items.dropFirst().map { mas.indent($0, with: indent) } // swiftformat:disable:this indent } ?? [formattedPrefix], // swiftformat:disable:this indent separator: mas.indent(separator, with: indent), From bba3fc1a849ae6a1bb6d835a6d1c32b5f6667f49 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:05:15 -0400 Subject: [PATCH 05/11] Ensure `PhaseType.description` always starts with an uppercase character. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/AppStoreAction+download.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 224b59fe9..29942fd1b 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -513,7 +513,7 @@ extension PhaseType: CustomStringConvertible { case .downloaded: "Downloaded" case let .performing(action): - action.performing + action.performing.uppercasingFirst } } } From c213f4970cff66fe598ae0fafbe29a13dcddb63b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:48:12 -0400 Subject: [PATCH 06/11] Improve lint disable comments. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Config.swift | 4 ++-- Sources/mas/Utilities/Versions/Version+SemVer.swift | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/Config.swift index 4ccae875c..af39450b6 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/Config.swift @@ -23,8 +23,8 @@ extension MAS { func run() { outputFormatOptionGroup.info( JSON.Object( // swiftformat:disable:this wrap wrapArguments - dictionaryLiteral: // swiftformat:disable indent - ("mas", .string(version)), // swiftlint:disable vertical_parameter_alignment_on_call + dictionaryLiteral: // swiftlint:disable vertical_parameter_alignment_on_call + ("mas", .string(version)), // swiftformat:disable indent ("slice", .string(runningSliceArchitecture)), ("slices", .string(supportedSliceArchitectures.joined(separator: " "))), ("dist", .string(distribution)), diff --git a/Sources/mas/Utilities/Versions/Version+SemVer.swift b/Sources/mas/Utilities/Versions/Version+SemVer.swift index eb63acd26..aa0562d06 100644 --- a/Sources/mas/Utilities/Versions/Version+SemVer.swift +++ b/Sources/mas/Utilities/Versions/Version+SemVer.swift @@ -14,9 +14,8 @@ internal import Foundation protocol Version: RawRepresentable { var coreElements: [String] { get } var prereleaseElements: [String] { get } - var buildElements: [String] { get } + var buildElements: [String] { get } // swiftlint:disable unused_declaration - // swiftlint:disable unused_declaration var core: String { get } // periphery:ignore var prerelease: String? { get } // periphery:ignore var build: String? { get } // periphery:ignore From bef598fd665013623894f4a2bf0206e269ed53f6 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:29:55 -0400 Subject: [PATCH 07/11] Decompose `Version+SemVer.swift`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Versions/CoreIntegerVersion.swift | 22 ++ .../Utilities/Versions/MajorMinorPatch.swift | 12 + .../Versions/MajorMinorPatchInteger.swift | 28 +++ .../mas/Utilities/Versions/SemVerSyntax.swift | 34 +++ .../Versions/SemVerSyntaxInteger.swift | 8 + .../Utilities/Versions/UniversalSemVer.swift | 84 +++++++ .../Versions/UniversalSemVerInt.swift | 73 ++++++ .../Utilities/Versions/Version+SemVer.swift | 216 ------------------ 8 files changed, 261 insertions(+), 216 deletions(-) create mode 100644 Sources/mas/Utilities/Versions/CoreIntegerVersion.swift create mode 100644 Sources/mas/Utilities/Versions/MajorMinorPatch.swift create mode 100644 Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift create mode 100644 Sources/mas/Utilities/Versions/SemVerSyntax.swift create mode 100644 Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift create mode 100644 Sources/mas/Utilities/Versions/UniversalSemVer.swift create mode 100644 Sources/mas/Utilities/Versions/UniversalSemVerInt.swift diff --git a/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift b/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift new file mode 100644 index 000000000..3497b38fa --- /dev/null +++ b/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift @@ -0,0 +1,22 @@ +// +// CoreIntegerVersion.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol CoreIntegerVersion: Version { + associatedtype Integer: BinaryInteger + + var coreIntegers: [Integer] { get } +} + +extension CoreIntegerVersion where Integer: FixedWidthInteger { + static func coreElements(from coreIntegers: [Integer]) -> [String] { + coreIntegers.map { .init($0) } + } + + var coreElements: [String] { + Self.coreElements(from: coreIntegers) + } +} diff --git a/Sources/mas/Utilities/Versions/MajorMinorPatch.swift b/Sources/mas/Utilities/Versions/MajorMinorPatch.swift new file mode 100644 index 000000000..088952a7d --- /dev/null +++ b/Sources/mas/Utilities/Versions/MajorMinorPatch.swift @@ -0,0 +1,12 @@ +// +// MajorMinorPatch.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol MajorMinorPatch { // swiftlint:disable unused_declaration + var major: String { get } // periphery:ignore + var minor: String { get } // periphery:ignore + var patch: String { get } // periphery:ignore +} // swiftlint:enable unused_declaration diff --git a/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift b/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift new file mode 100644 index 000000000..419227334 --- /dev/null +++ b/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift @@ -0,0 +1,28 @@ +// +// MajorMinorPatchInteger.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol MajorMinorPatchInteger: MajorMinorPatch { + associatedtype Integer: BinaryInteger + + var majorInteger: Integer { get } + var minorInteger: Integer { get } + var patchInteger: Integer { get } +} + +extension MajorMinorPatchInteger { + var major: String { + .init(majorInteger) + } + + var minor: String { + .init(minorInteger) + } + + var patch: String { + .init(patchInteger) + } +} diff --git a/Sources/mas/Utilities/Versions/SemVerSyntax.swift b/Sources/mas/Utilities/Versions/SemVerSyntax.swift new file mode 100644 index 000000000..f2c469945 --- /dev/null +++ b/Sources/mas/Utilities/Versions/SemVerSyntax.swift @@ -0,0 +1,34 @@ +// +// SemVerSyntax.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol SemVerSyntax: Version {} + +extension SemVerSyntax { + static func core(from coreElements: [String]) -> String { + coreElements.joined(separator: ".") + } + + static func prerelease(from prereleaseElements: [String]) -> String? { + prereleaseElements.isEmpty ? nil : prereleaseElements.joined(separator: ".") + } + + static func build(from buildElements: [String]) -> String? { + buildElements.isEmpty ? nil : buildElements.joined(separator: ".") + } + + var core: String { + Self.core(from: coreElements) + } + + var prerelease: String? { + Self.prerelease(from: prereleaseElements) + } + + var build: String? { + Self.build(from: buildElements) + } +} diff --git a/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift b/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift new file mode 100644 index 000000000..eb9ce03ab --- /dev/null +++ b/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift @@ -0,0 +1,8 @@ +// +// SemVerSyntaxInteger.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol SemVerSyntaxInteger: CoreIntegerVersion, SemVerSyntax, MajorMinorPatchInteger {} diff --git a/Sources/mas/Utilities/Versions/UniversalSemVer.swift b/Sources/mas/Utilities/Versions/UniversalSemVer.swift new file mode 100644 index 000000000..e7befd07b --- /dev/null +++ b/Sources/mas/Utilities/Versions/UniversalSemVer.swift @@ -0,0 +1,84 @@ +// +// UniversalSemVer.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +private import BigInt +internal import Foundation + +struct UniversalSemVer: SemVerSyntax { + let coreElements: [String] + let prereleaseElements: [String] + let buildElements: [String] + let rawValue: String + + init(rawValue: String) { + guard let match = rawValue.wholeMatch(of: universalSemVerRegex) else { + preconditionFailure("Failed to match regex \(universalSemVerRegex)") + } + + coreElements = match.1.elements + prereleaseElements = match.2.elements + buildElements = match.3.elements + self.rawValue = rawValue + } +} + +private extension BigUInt { + func compare(to that: Self) -> ComparisonResult { + self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending + } +} + +private extension FixedWidthInteger { + func compare(to that: Self) -> ComparisonResult { + self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending + } +} + +private extension String { + func compareSemVerElement( + to that: Self, + options mask: CompareOptions = .init(), + range: Range? = nil, + locale: Locale? = nil, + ) -> ComparisonResult { + let thatInteger = BigUInt(that) + return BigUInt(self).map { thatInteger.map($0.compare(to:)) ?? .orderedAscending } + ?? thatInteger.map { _ in .orderedDescending } // swiftformat:disable:this indent + ?? compare(that, options: mask, range: range, locale: locale) // swiftformat:disable:this indent + } +} + +private extension [String] { + func compareSemVerElements(to that: Self) -> ComparisonResult { + zip(self, that).first { $0 != $1 }.map { $0.compareSemVerElement(to: $1) } + ?? dropLast { $0 == "0" }.count.compare(to: that.dropLast { $0 == "0" }.count) // swiftformat:disable:this indent + } +} + +extension Substring? { + var elements: [String] { + map { $0.split(separator: ".") }?.map(String.init(_:)) ?? .init() + } +} + +extension Version { + func compareSemVer(to that: Self) -> ComparisonResult { + let coreComparison = coreElements.compareSemVerElements(to: that.coreElements) + return coreComparison == .orderedSame + ? prereleaseElements.compareSemVerElements(to: that.prereleaseElements) // swiftformat:disable:this indent + : coreComparison + } + + func compareSemVerAndBuild(to that: Self) -> ComparisonResult { + let semVerComparison = compareSemVer(to: that) + return semVerComparison == .orderedSame + ? buildElements.compareSemVerElements(to: that.buildElements) // swiftformat:disable:this indent + : semVerComparison + } +} + +let universalSemVerRegex = /([^-+]*+)?+(?:-([^+]*+))?+(?:\+(.*+))?+/ diff --git a/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift b/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift new file mode 100644 index 000000000..f5debfb2d --- /dev/null +++ b/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift @@ -0,0 +1,73 @@ +// +// UniversalSemVerInt.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +struct UniversalSemVerInt: SemVerSyntaxInteger { + let coreIntegers: [Int] + let prereleaseElements: [String] + let buildElements: [String] + let rawValue: String + + var majorInteger: Int { + coreIntegers[0] + } + + var minorInteger: Int { + coreIntegers[1] + } + + var patchInteger: Int { + coreIntegers[2] + } + + init( + coreIntegers: [Int], + prereleaseElements: [String] = .init(), + buildElements: [String] = .init(), + ) { // periphery:ignore + self.init( + coreIntegers: coreIntegers, + prereleaseElements: prereleaseElements, + buildElements: buildElements, + rawValue: """ + \(Self.core(from: Self.coreElements(from: coreIntegers)))\ + \(Self.prerelease(from: prereleaseElements).map { "-\($0)" } ?? "")\ + \(Self.build(from: buildElements).map { "+\($0)" } ?? "") + """, + ) + } + + init?(rawValue: String) { + do { + guard let match = rawValue.wholeMatch(of: universalSemVerRegex) else { + preconditionFailure("Failed to match regex \(universalSemVerRegex)") + } + + self = .init( + coreIntegers: try match.1.elements.map { coreElement in + try .init(coreElement) ?? { throw MASError.error(coreElement) }() + }, + prereleaseElements: match.2.elements, + buildElements: match.3.elements, + rawValue: rawValue, + ) + } catch { + return nil + } + } + + private init( + coreIntegers: [Int], + prereleaseElements: [String], + buildElements: [String], + rawValue: String, + ) { + self.coreIntegers = coreIntegers.padding(toCount: 3, with: 0) + self.prereleaseElements = prereleaseElements + self.buildElements = buildElements + self.rawValue = rawValue + } +} diff --git a/Sources/mas/Utilities/Versions/Version+SemVer.swift b/Sources/mas/Utilities/Versions/Version+SemVer.swift index aa0562d06..d47637752 100644 --- a/Sources/mas/Utilities/Versions/Version+SemVer.swift +++ b/Sources/mas/Utilities/Versions/Version+SemVer.swift @@ -5,12 +5,6 @@ // Copyright © 2025 mas-cli. All rights reserved. // -private import BigInt -internal import Foundation - -// swiftlint:disable:next blanket_disable_command -// swiftlint:disable file_types_order one_declaration_per_file - protocol Version: RawRepresentable { var coreElements: [String] { get } var prereleaseElements: [String] { get } @@ -20,213 +14,3 @@ protocol Version: RawRepresentable { var prerelease: String? { get } // periphery:ignore var build: String? { get } // periphery:ignore } // swiftlint:enable unused_declaration - -protocol CoreIntegerVersion: Version { - associatedtype Integer: BinaryInteger - - var coreIntegers: [Integer] { get } -} - -extension CoreIntegerVersion where Integer: FixedWidthInteger { - fileprivate static func coreElements(from coreIntegers: [Integer]) -> [String] { - coreIntegers.map { .init($0) } - } - - var coreElements: [String] { - Self.coreElements(from: coreIntegers) - } -} - -protocol MajorMinorPatch { // swiftlint:disable unused_declaration - var major: String { get } // periphery:ignore - var minor: String { get } // periphery:ignore - var patch: String { get } // periphery:ignore -} // swiftlint:enable unused_declaration - -protocol MajorMinorPatchInteger: MajorMinorPatch { - associatedtype Integer: BinaryInteger - - var majorInteger: Integer { get } - var minorInteger: Integer { get } - var patchInteger: Integer { get } -} - -extension MajorMinorPatchInteger { - var major: String { - .init(majorInteger) - } - - var minor: String { - .init(minorInteger) - } - - var patch: String { - .init(patchInteger) - } -} - -protocol SemVerSyntax: Version {} - -extension SemVerSyntax { - fileprivate static func core(from coreElements: [String]) -> String { - coreElements.joined(separator: ".") - } - - fileprivate static func prerelease(from prereleaseElements: [String]) -> String? { - prereleaseElements.isEmpty ? nil : prereleaseElements.joined(separator: ".") - } - - fileprivate static func build(from buildElements: [String]) -> String? { - buildElements.isEmpty ? nil : buildElements.joined(separator: ".") - } - - var core: String { - Self.core(from: coreElements) - } - - var prerelease: String? { - Self.prerelease(from: prereleaseElements) - } - - var build: String? { - Self.build(from: buildElements) - } -} - -protocol SemVerSyntaxInteger: CoreIntegerVersion, SemVerSyntax, MajorMinorPatchInteger {} - -struct UniversalSemVerInt: SemVerSyntaxInteger { - let coreIntegers: [Int] - let prereleaseElements: [String] - let buildElements: [String] - let rawValue: String - - var majorInteger: Int { - coreIntegers[0] - } - - var minorInteger: Int { - coreIntegers[1] - } - - var patchInteger: Int { - coreIntegers[2] - } - - init( - coreIntegers: [Int], - prereleaseElements: [String] = .init(), - buildElements: [String] = .init(), - ) { // periphery:ignore - self.init( - coreIntegers: coreIntegers, - prereleaseElements: prereleaseElements, - buildElements: buildElements, - rawValue: """ - \(Self.core(from: Self.coreElements(from: coreIntegers)))\ - \(Self.prerelease(from: prereleaseElements).map { "-\($0)" } ?? "")\ - \(Self.build(from: buildElements).map { "+\($0)" } ?? "") - """, - ) - } - - init?(rawValue: String) { - do { - let match = rawValue.wholeMatch(of: universalSemVerRegex)! // swiftlint:disable:this force_unwrapping - self = .init( - coreIntegers: try match.1.elements.map { coreElement in - try .init(coreElement) ?? { throw MASError.error(coreElement) }() - }, - prereleaseElements: match.2.elements, - buildElements: match.3.elements, - rawValue: rawValue, - ) - } catch { - return nil - } - } - - private init( - coreIntegers: [Int], - prereleaseElements: [String], - buildElements: [String], - rawValue: String, - ) { - self.coreIntegers = coreIntegers.padding(toCount: 3, with: 0) - self.prereleaseElements = prereleaseElements - self.buildElements = buildElements - self.rawValue = rawValue - } -} - -struct UniversalSemVer: SemVerSyntax { - let coreElements: [String] - let prereleaseElements: [String] - let buildElements: [String] - let rawValue: String - - init(rawValue: String) { - let match = rawValue.wholeMatch(of: universalSemVerRegex)! // swiftlint:disable:this force_unwrapping - coreElements = match.1.elements - prereleaseElements = match.2.elements - buildElements = match.3.elements - self.rawValue = rawValue - } -} - -private extension BigUInt { - func compare(to that: Self) -> ComparisonResult { - self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending - } -} - -private extension FixedWidthInteger { - func compare(to that: Self) -> ComparisonResult { - self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending - } -} - -private extension String { - func compareSemVerElement( - to that: Self, - options mask: CompareOptions = .init(), - range: Range? = nil, - locale: Locale? = nil, - ) -> ComparisonResult { - let thatInteger = BigUInt(that) - return BigUInt(self).map { thatInteger.map($0.compare(to:)) ?? .orderedAscending } - ?? thatInteger.map { _ in .orderedDescending } // swiftformat:disable:this indent - ?? compare(that, options: mask, range: range, locale: locale) // swiftformat:disable:this indent - } -} - -private extension [String] { - func compareSemVerElements(to that: Self) -> ComparisonResult { - zip(self, that).first { $0 != $1 }.map { $0.compareSemVerElement(to: $1) } - ?? dropLast { $0 == "0" }.count.compare(to: that.dropLast { $0 == "0" }.count) // swiftformat:disable:this indent - } -} - -private extension Substring? { - var elements: [String] { - map { $0.split(separator: ".") }?.map(String.init(_:)) ?? .init() - } -} - -extension Version { - func compareSemVer(to that: Self) -> ComparisonResult { - let coreComparison = coreElements.compareSemVerElements(to: that.coreElements) - return coreComparison == .orderedSame - ? prereleaseElements.compareSemVerElements(to: that.prereleaseElements) // swiftformat:disable:this indent - : coreComparison - } - - func compareSemVerAndBuild(to that: Self) -> ComparisonResult { - let semVerComparison = compareSemVer(to: that) - return semVerComparison == .orderedSame - ? buildElements.compareSemVerElements(to: that.buildElements) // swiftformat:disable:this indent - : semVerComparison - } -} - -private let universalSemVerRegex = /([^-+]*+)?+(?:-([^+]*+))?+(?:\+(.*+))?+/ From 0d7d029393e8a33f35cf29a8cd8632a6a86c1e53 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:14:09 -0400 Subject: [PATCH 08/11] Fix `UniversalSemVer` comparisons. Conform `UniversalSemVer` to `ExpressibleByStringLiteral`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../mas/Utilities/Versions/UniversalSemVer.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/mas/Utilities/Versions/UniversalSemVer.swift b/Sources/mas/Utilities/Versions/UniversalSemVer.swift index e7befd07b..e535be5f1 100644 --- a/Sources/mas/Utilities/Versions/UniversalSemVer.swift +++ b/Sources/mas/Utilities/Versions/UniversalSemVer.swift @@ -8,7 +8,7 @@ private import BigInt internal import Foundation -struct UniversalSemVer: SemVerSyntax { +struct UniversalSemVer: SemVerSyntax, ExpressibleByStringLiteral { let coreElements: [String] let prereleaseElements: [String] let buildElements: [String] @@ -24,6 +24,10 @@ struct UniversalSemVer: SemVerSyntax { buildElements = match.3.elements self.rawValue = rawValue } + + init(stringLiteral value: String) { + self.init(rawValue: value) + } } private extension BigUInt { @@ -68,9 +72,11 @@ extension Substring? { extension Version { func compareSemVer(to that: Self) -> ComparisonResult { let coreComparison = coreElements.compareSemVerElements(to: that.coreElements) - return coreComparison == .orderedSame - ? prereleaseElements.compareSemVerElements(to: that.prereleaseElements) // swiftformat:disable:this indent - : coreComparison + return coreComparison != .orderedSame + ? coreComparison // swiftformat:disable:this indent + : prereleaseElements.isEmpty != that.prereleaseElements.isEmpty // swiftformat:disable:next wrap wrapArguments + ? prereleaseElements.isEmpty ? .orderedDescending : .orderedAscending + : prereleaseElements.compareSemVerElements(to: that.prereleaseElements) } func compareSemVerAndBuild(to that: Self) -> ComparisonResult { From e3058eb95e39e05e5cdf301c08818c41a75edf02 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:05:30 -0400 Subject: [PATCH 09/11] Rename command Swift files. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/{Config.swift => MAS.Config.swift} | 2 +- Sources/mas/Commands/{Get.swift => MAS.Get.swift} | 2 +- Sources/mas/Commands/{Home.swift => MAS.Home.swift} | 2 +- Sources/mas/Commands/{Install.swift => MAS.Install.swift} | 2 +- Sources/mas/Commands/{List.swift => MAS.List.swift} | 2 +- Sources/mas/Commands/{Lookup.swift => MAS.Lookup.swift} | 2 +- Sources/mas/Commands/{Lucky.swift => MAS.Lucky.swift} | 2 +- Sources/mas/Commands/{Open.swift => MAS.Open.swift} | 2 +- Sources/mas/Commands/{Outdated.swift => MAS.Outdated.swift} | 2 +- Sources/mas/Commands/{Reset.swift => MAS.Reset.swift} | 2 +- Sources/mas/Commands/{Search.swift => MAS.Search.swift} | 2 +- Sources/mas/Commands/{Seller.swift => MAS.Seller.swift} | 2 +- Sources/mas/Commands/{SignOut.swift => MAS.SignOut.swift} | 2 +- Sources/mas/Commands/{Uninstall.swift => MAS.Uninstall.swift} | 2 +- Sources/mas/Commands/{Update.swift => MAS.Update.swift} | 2 +- Sources/mas/Commands/{Version.swift => MAS.Version.swift} | 2 +- .../Utilities/Versions/{Version+SemVer.swift => Version.swift} | 2 +- .../Commands/{MASTests+Home.swift => MASTests+MAS.Home.swift} | 2 +- .../Commands/{MASTests+List.swift => MASTests+MAS.List.swift} | 2 +- .../{MASTests+Lookup.swift => MASTests+MAS.Lookup.swift} | 2 +- .../{MASTests+Search.swift => MASTests+MAS.Search.swift} | 2 +- .../{MASTests+Seller.swift => MASTests+MAS.Seller.swift} | 2 +- .../{MASTests+Version.swift => MASTests+MAS.Version.swift} | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) rename Sources/mas/Commands/{Config.swift => MAS.Config.swift} (99%) rename Sources/mas/Commands/{Get.swift => MAS.Get.swift} (97%) rename Sources/mas/Commands/{Home.swift => MAS.Home.swift} (98%) rename Sources/mas/Commands/{Install.swift => MAS.Install.swift} (97%) rename Sources/mas/Commands/{List.swift => MAS.List.swift} (98%) rename Sources/mas/Commands/{Lookup.swift => MAS.Lookup.swift} (97%) rename Sources/mas/Commands/{Lucky.swift => MAS.Lucky.swift} (98%) rename Sources/mas/Commands/{Open.swift => MAS.Open.swift} (99%) rename Sources/mas/Commands/{Outdated.swift => MAS.Outdated.swift} (98%) rename Sources/mas/Commands/{Reset.swift => MAS.Reset.swift} (99%) rename Sources/mas/Commands/{Search.swift => MAS.Search.swift} (98%) rename Sources/mas/Commands/{Seller.swift => MAS.Seller.swift} (98%) rename Sources/mas/Commands/{SignOut.swift => MAS.SignOut.swift} (95%) rename Sources/mas/Commands/{Uninstall.swift => MAS.Uninstall.swift} (99%) rename Sources/mas/Commands/{Update.swift => MAS.Update.swift} (98%) rename Sources/mas/Commands/{Version.swift => MAS.Version.swift} (93%) rename Sources/mas/Utilities/Versions/{Version+SemVer.swift => Version.swift} (95%) rename Tests/MASTests/Commands/{MASTests+Home.swift => MASTests+MAS.Home.swift} (94%) rename Tests/MASTests/Commands/{MASTests+List.swift => MASTests+MAS.List.swift} (97%) rename Tests/MASTests/Commands/{MASTests+Lookup.swift => MASTests+MAS.Lookup.swift} (99%) rename Tests/MASTests/Commands/{MASTests+Search.swift => MASTests+MAS.Search.swift} (99%) rename Tests/MASTests/Commands/{MASTests+Seller.swift => MASTests+MAS.Seller.swift} (93%) rename Tests/MASTests/Commands/{MASTests+Version.swift => MASTests+MAS.Version.swift} (92%) diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/MAS.Config.swift similarity index 99% rename from Sources/mas/Commands/Config.swift rename to Sources/mas/Commands/MAS.Config.swift index af39450b6..3f07c6078 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/MAS.Config.swift @@ -1,5 +1,5 @@ // -// Config.swift +// MAS.Config.swift // mas // // Copyright © 2025 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Get.swift b/Sources/mas/Commands/MAS.Get.swift similarity index 97% rename from Sources/mas/Commands/Get.swift rename to Sources/mas/Commands/MAS.Get.swift index 58d34f203..b9997e1fd 100644 --- a/Sources/mas/Commands/Get.swift +++ b/Sources/mas/Commands/MAS.Get.swift @@ -1,5 +1,5 @@ // -// Get.swift +// MAS.Get.swift // mas // // Copyright © 2026 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/MAS.Home.swift similarity index 98% rename from Sources/mas/Commands/Home.swift rename to Sources/mas/Commands/MAS.Home.swift index a941d5a7c..7064a8db9 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/MAS.Home.swift @@ -1,5 +1,5 @@ // -// Home.swift +// MAS.Home.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/MAS.Install.swift similarity index 97% rename from Sources/mas/Commands/Install.swift rename to Sources/mas/Commands/MAS.Install.swift index 2e43fe216..f36c07f0a 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/MAS.Install.swift @@ -1,5 +1,5 @@ // -// Install.swift +// MAS.Install.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/MAS.List.swift similarity index 98% rename from Sources/mas/Commands/List.swift rename to Sources/mas/Commands/MAS.List.swift index ba5b9a16e..98a7eff9e 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/MAS.List.swift @@ -1,5 +1,5 @@ // -// List.swift +// MAS.List.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Lookup.swift b/Sources/mas/Commands/MAS.Lookup.swift similarity index 97% rename from Sources/mas/Commands/Lookup.swift rename to Sources/mas/Commands/MAS.Lookup.swift index 68b238e15..ba91541d7 100644 --- a/Sources/mas/Commands/Lookup.swift +++ b/Sources/mas/Commands/MAS.Lookup.swift @@ -1,5 +1,5 @@ // -// Lookup.swift +// MAS.Lookup.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/MAS.Lucky.swift similarity index 98% rename from Sources/mas/Commands/Lucky.swift rename to Sources/mas/Commands/MAS.Lucky.swift index 2fe80f32e..f3cbb306d 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/MAS.Lucky.swift @@ -1,5 +1,5 @@ // -// Lucky.swift +// MAS.Lucky.swift // mas // // Copyright © 2017 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/MAS.Open.swift similarity index 99% rename from Sources/mas/Commands/Open.swift rename to Sources/mas/Commands/MAS.Open.swift index 725cac919..eb3f0c58b 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/MAS.Open.swift @@ -1,5 +1,5 @@ // -// Open.swift +// MAS.Open.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/MAS.Outdated.swift similarity index 98% rename from Sources/mas/Commands/Outdated.swift rename to Sources/mas/Commands/MAS.Outdated.swift index 915b33792..3e25f77b5 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/MAS.Outdated.swift @@ -1,5 +1,5 @@ // -// Outdated.swift +// MAS.Outdated.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/MAS.Reset.swift similarity index 99% rename from Sources/mas/Commands/Reset.swift rename to Sources/mas/Commands/MAS.Reset.swift index 9124e3965..c494897f5 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/MAS.Reset.swift @@ -1,5 +1,5 @@ // -// Reset.swift +// MAS.Reset.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/MAS.Search.swift similarity index 98% rename from Sources/mas/Commands/Search.swift rename to Sources/mas/Commands/MAS.Search.swift index 313745cd6..acdd788d4 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/MAS.Search.swift @@ -1,5 +1,5 @@ // -// Search.swift +// MAS.Search.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Seller.swift b/Sources/mas/Commands/MAS.Seller.swift similarity index 98% rename from Sources/mas/Commands/Seller.swift rename to Sources/mas/Commands/MAS.Seller.swift index 61b60f3c0..8a58663ed 100644 --- a/Sources/mas/Commands/Seller.swift +++ b/Sources/mas/Commands/MAS.Seller.swift @@ -1,5 +1,5 @@ // -// Seller.swift +// MAS.Seller.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/MAS.SignOut.swift similarity index 95% rename from Sources/mas/Commands/SignOut.swift rename to Sources/mas/Commands/MAS.SignOut.swift index cd993aa22..fec5983e5 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/MAS.SignOut.swift @@ -1,5 +1,5 @@ // -// SignOut.swift +// MAS.SignOut.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/MAS.Uninstall.swift similarity index 99% rename from Sources/mas/Commands/Uninstall.swift rename to Sources/mas/Commands/MAS.Uninstall.swift index 20cb75f7e..64dd8a126 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/MAS.Uninstall.swift @@ -1,5 +1,5 @@ // -// Uninstall.swift +// MAS.Uninstall.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Update.swift b/Sources/mas/Commands/MAS.Update.swift similarity index 98% rename from Sources/mas/Commands/Update.swift rename to Sources/mas/Commands/MAS.Update.swift index 215c6fdb3..e33304391 100644 --- a/Sources/mas/Commands/Update.swift +++ b/Sources/mas/Commands/MAS.Update.swift @@ -1,5 +1,5 @@ // -// Update.swift +// MAS.Update.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/MAS.Version.swift similarity index 93% rename from Sources/mas/Commands/Version.swift rename to Sources/mas/Commands/MAS.Version.swift index 8f09e47bf..ad0539183 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/MAS.Version.swift @@ -1,5 +1,5 @@ // -// Version.swift +// MAS.Version.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Utilities/Versions/Version+SemVer.swift b/Sources/mas/Utilities/Versions/Version.swift similarity index 95% rename from Sources/mas/Utilities/Versions/Version+SemVer.swift rename to Sources/mas/Utilities/Versions/Version.swift index d47637752..26355867e 100644 --- a/Sources/mas/Utilities/Versions/Version+SemVer.swift +++ b/Sources/mas/Utilities/Versions/Version.swift @@ -1,5 +1,5 @@ // -// Version+SemVer.swift +// Version.swift // mas // // Copyright © 2025 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Home.swift b/Tests/MASTests/Commands/MASTests+MAS.Home.swift similarity index 94% rename from Tests/MASTests/Commands/MASTests+Home.swift rename to Tests/MASTests/Commands/MASTests+MAS.Home.swift index 63e8c4da2..28647060e 100644 --- a/Tests/MASTests/Commands/MASTests+Home.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Home.swift @@ -1,5 +1,5 @@ // -// MASTests+Home.swift +// MASTests+MAS.Home.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+List.swift b/Tests/MASTests/Commands/MASTests+MAS.List.swift similarity index 97% rename from Tests/MASTests/Commands/MASTests+List.swift rename to Tests/MASTests/Commands/MASTests+MAS.List.swift index 52a8a1e21..7408294f6 100644 --- a/Tests/MASTests/Commands/MASTests+List.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.List.swift @@ -1,5 +1,5 @@ // -// MASTests+List.swift +// MASTests+MAS.List.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Lookup.swift b/Tests/MASTests/Commands/MASTests+MAS.Lookup.swift similarity index 99% rename from Tests/MASTests/Commands/MASTests+Lookup.swift rename to Tests/MASTests/Commands/MASTests+MAS.Lookup.swift index fc6f806ae..2a29b06d3 100644 --- a/Tests/MASTests/Commands/MASTests+Lookup.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Lookup.swift @@ -1,5 +1,5 @@ // -// MASTests+Lookup.swift +// MASTests+MAS.Lookup.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Search.swift b/Tests/MASTests/Commands/MASTests+MAS.Search.swift similarity index 99% rename from Tests/MASTests/Commands/MASTests+Search.swift rename to Tests/MASTests/Commands/MASTests+MAS.Search.swift index 04cfca448..b226630a7 100644 --- a/Tests/MASTests/Commands/MASTests+Search.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Search.swift @@ -1,5 +1,5 @@ // -// MASTests+Search.swift +// MASTests+MAS.Search.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Seller.swift b/Tests/MASTests/Commands/MASTests+MAS.Seller.swift similarity index 93% rename from Tests/MASTests/Commands/MASTests+Seller.swift rename to Tests/MASTests/Commands/MASTests+MAS.Seller.swift index 802b26906..1e450191e 100644 --- a/Tests/MASTests/Commands/MASTests+Seller.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Seller.swift @@ -1,5 +1,5 @@ // -// MASTests+Seller.swift +// MASTests+MAS.Seller.swift // mas // // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Version.swift b/Tests/MASTests/Commands/MASTests+MAS.Version.swift similarity index 92% rename from Tests/MASTests/Commands/MASTests+Version.swift rename to Tests/MASTests/Commands/MASTests+MAS.Version.swift index 8dc37a65a..6d873dbfb 100644 --- a/Tests/MASTests/Commands/MASTests+Version.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Version.swift @@ -1,5 +1,5 @@ // -// MASTests+Version.swift +// MASTests+MAS.Version.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. From adee8d88c9bd24ed68a5ad39bc8e13963404750b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:08:45 -0400 Subject: [PATCH 10/11] Move test utilities. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/MASTests/{Utilities => Testing}/Consequences.swift | 0 Tests/MASTests/{Utilities => Testing}/Data.swift | 0 Tests/MASTests/{Utilities => Testing}/Resources.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Tests/MASTests/{Utilities => Testing}/Consequences.swift (100%) rename Tests/MASTests/{Utilities => Testing}/Data.swift (100%) rename Tests/MASTests/{Utilities => Testing}/Resources.swift (100%) diff --git a/Tests/MASTests/Utilities/Consequences.swift b/Tests/MASTests/Testing/Consequences.swift similarity index 100% rename from Tests/MASTests/Utilities/Consequences.swift rename to Tests/MASTests/Testing/Consequences.swift diff --git a/Tests/MASTests/Utilities/Data.swift b/Tests/MASTests/Testing/Data.swift similarity index 100% rename from Tests/MASTests/Utilities/Data.swift rename to Tests/MASTests/Testing/Data.swift diff --git a/Tests/MASTests/Utilities/Resources.swift b/Tests/MASTests/Testing/Resources.swift similarity index 100% rename from Tests/MASTests/Utilities/Resources.swift rename to Tests/MASTests/Testing/Resources.swift From 3413fa3bb7e1d4e01841ad2122992d250f302a7e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:59:38 -0400 Subject: [PATCH 11/11] Update `AGENTS.md`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f89731dab..eb6fd3135 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -298,8 +298,9 @@ Within this section & all subsections, `X` is a placeholder for any type name. 6. `switch` 7. Multiple `guard` 8. `if` / `else if`… / `else` -9. Forced unwrapping (`!` suffix) -10. `fatalError(_:file:line:)` +9. `preconditionFailure(_:file:line:)` +10. Forced unwrapping (`!` suffix) +11. `fatalError(_:file:line:)` #### Throwing