From fa7bdea02950b2d3ded4d5d4d2264f9c21283c0e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:08:42 -0400 Subject: [PATCH 01/81] Fix breaking lint issue in MasStoreSearch.swift. Resolve #546 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Controllers/MasStoreSearch.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MasKit/Controllers/MasStoreSearch.swift b/Sources/MasKit/Controllers/MasStoreSearch.swift index f5a32e4..43b96c6 100644 --- a/Sources/MasKit/Controllers/MasStoreSearch.swift +++ b/Sources/MasKit/Controllers/MasStoreSearch.swift @@ -122,8 +122,8 @@ class MasStoreSearch: StoreSearch { firstly { networkManager.loadData(from: pageUrl) }.map { data in - let html = String(decoding: data, as: UTF8.self) - guard let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], + guard let html = String(data: data, encoding: .utf8), + let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) else { return nil From 5c11f9afb7d06ddc5862e30ea4ce59bcf62d17a6 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:45:20 -0400 Subject: [PATCH 02/81] Git ignore JetBrains IntelliJ IDEA config directory. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 18eb50c..1f92665 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ .build/ .envrc .fseventsd +.idea/ .rubygems/ .swiftpm/ Carthage/ From 7132e7eed909e33a1de268e61e9e84ac8876084e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 06:43:40 -0400 Subject: [PATCH 03/81] Fix typos in documentation Partial #538 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- docs/sample.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 249795c..16760d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We love pull requests from everyone. By participating in this project, you agree - [Open an issue](https://github.com/mas-cli/mas/issues/new) to simply ask a question or request a new feature. - Search for similar issues with the [ERROR MESSAGE](https://github.com/mas-cli/mas/issues?utf8=%E2%9C%93&q=is%3Aopen+ERROR+MESSAGE) -you are exeriencing. +you are experiencing. - If one doesn't exist, [open a new issue](https://github.com/mas-cli/mas/issues/new) - Clearly describe the issue including steps to reproduce when it is a bug. - Include the earliest version of `mas` that you know has the issue. diff --git a/docs/sample.swift b/docs/sample.swift index 793d947..fd7091e 100644 --- a/docs/sample.swift +++ b/docs/sample.swift @@ -74,7 +74,7 @@ case let .success(data): print("The response returned successfully \(data)") case let .failure(error): - print("An error occured: \(error)") + print("An error occurred: \(error)") } // MARK: Organization From 47f4b5c0da42f8e8d670e9ef294ca78b0e318d94 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 06:52:16 -0400 Subject: [PATCH 04/81] Fix typos in comments Resolves #538 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Commands/Lucky.swift | 2 +- Sources/MasKit/Controllers/SoftwareMap.swift | 2 +- Sources/MasKit/Formatters/AppListFormatter.swift | 4 ++-- Sources/MasKit/Formatters/SearchResultFormatter.swift | 2 +- Tests/MasKitTests/Controllers/AppLibraryMock.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/MasKit/Commands/Lucky.swift b/Sources/MasKit/Commands/Lucky.swift index f8288ab..9452eba 100644 --- a/Sources/MasKit/Commands/Lucky.swift +++ b/Sources/MasKit/Commands/Lucky.swift @@ -69,7 +69,7 @@ public struct LuckyCommand: CommandProtocol { /// /// - Parameters: /// - appId: App identifier - /// - options: command opetions. + /// - options: command options. /// - Returns: Result of the operation. fileprivate func install(_ appId: UInt64, options: Options) -> Result { // Try to download applications with given identifiers and collect results diff --git a/Sources/MasKit/Controllers/SoftwareMap.swift b/Sources/MasKit/Controllers/SoftwareMap.swift index f30301f..0294abe 100644 --- a/Sources/MasKit/Controllers/SoftwareMap.swift +++ b/Sources/MasKit/Controllers/SoftwareMap.swift @@ -6,7 +6,7 @@ // Copyright © 2020 mas-cli. All rights reserved. // -/// Somewhat analygous to CKSoftwareMap +/// Somewhat analogous to CKSoftwareMap protocol SoftwareMap { func allSoftwareProducts() -> [SoftwareProduct] func product(for bundleIdentifier: String) -> SoftwareProduct? diff --git a/Sources/MasKit/Formatters/AppListFormatter.swift b/Sources/MasKit/Formatters/AppListFormatter.swift index e147595..21c915c 100644 --- a/Sources/MasKit/Formatters/AppListFormatter.swift +++ b/Sources/MasKit/Formatters/AppListFormatter.swift @@ -15,8 +15,8 @@ enum AppListFormatter { /// Formats text output with list results. /// - /// - Parameter products: List of sortware products app data. - /// - Returns: Multiliune text outoutp. + /// - Parameter products: List of software products app data. + /// - Returns: Multiline text output. static func format(products: [SoftwareProduct]) -> String { // find longest appName for formatting, default 50 let maxLength = products.map(\.appNameOrBundleIdentifier.count).max() ?? nameColumnMinWidth diff --git a/Sources/MasKit/Formatters/SearchResultFormatter.swift b/Sources/MasKit/Formatters/SearchResultFormatter.swift index 03160cf..cd0061e 100644 --- a/Sources/MasKit/Formatters/SearchResultFormatter.swift +++ b/Sources/MasKit/Formatters/SearchResultFormatter.swift @@ -13,7 +13,7 @@ enum SearchResultFormatter { /// Formats text output with search results. /// /// - Parameter results: Search results with app data - /// - Returns: Multiliune text outoutp. + /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { // find longest appName for formatting, default 50 let maxLength = results.map(\.trackName.count).max() ?? 50 diff --git a/Tests/MasKitTests/Controllers/AppLibraryMock.swift b/Tests/MasKitTests/Controllers/AppLibraryMock.swift index 4ca7256..2a1f4b1 100644 --- a/Tests/MasKitTests/Controllers/AppLibraryMock.swift +++ b/Tests/MasKitTests/Controllers/AppLibraryMock.swift @@ -27,7 +27,7 @@ class AppLibraryMock: AppLibrary { } } -/// Members not part of the AppLibrary protocol that are only for test state managment. +/// Members not part of the AppLibrary protocol that are only for test state management. extension AppLibraryMock { /// Clears out the list of installed apps. func reset() { From d9780b876813c9924f95115ccd31caa1cde71daa Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 07:31:19 -0400 Subject: [PATCH 05/81] Fix incorrect function comment Resolves #538 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Controllers/AppLibrary.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MasKit/Controllers/AppLibrary.swift b/Sources/MasKit/Controllers/AppLibrary.swift index ac5ade4..2edbc89 100644 --- a/Sources/MasKit/Controllers/AppLibrary.swift +++ b/Sources/MasKit/Controllers/AppLibrary.swift @@ -28,9 +28,9 @@ protocol AppLibrary { /// Common logic extension AppLibrary { - /// Finds an app by name. + /// Finds an app by ID. /// - /// - Parameter id: MAS ID for app. + /// - Parameter forId: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. func installedApp(forId identifier: UInt64) -> SoftwareProduct? { let appId = NSNumber(value: identifier) From 7e9e3ba16806e024662b0026d7713d336a3cfd88 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 01:12:23 -0400 Subject: [PATCH 06/81] Output to stderr responses from Apple endpoints that are unparsable as JSON Resolves #536 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Controllers/MasStoreSearch.swift | 2 +- Sources/MasKit/Errors/MASError.swift | 14 +++++++++++--- Tests/MasKitTests/Errors/MASErrorTestCase.swift | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/MasKit/Controllers/MasStoreSearch.swift b/Sources/MasKit/Controllers/MasStoreSearch.swift index 43b96c6..0db3268 100644 --- a/Sources/MasKit/Controllers/MasStoreSearch.swift +++ b/Sources/MasKit/Controllers/MasStoreSearch.swift @@ -108,7 +108,7 @@ class MasStoreSearch: StoreSearch { do { return try JSONDecoder().decode(SearchResultList.self, from: data).results } catch { - throw MASError.jsonParsing(error: error as NSError) + throw MASError.jsonParsing(data: data) } } } diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/MasKit/Errors/MASError.swift index 716f4ea..984b73d 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/MasKit/Errors/MASError.swift @@ -28,7 +28,7 @@ public enum MASError: Error, Equatable { case uninstallFailed case noData - case jsonParsing(error: NSError?) + case jsonParsing(data: Data?) } // MARK: - CustomStringConvertible @@ -93,8 +93,16 @@ extension MASError: CustomStringConvertible { case .noData: return "Service did not return data" - case .jsonParsing: - return "Unable to parse response JSON" + case .jsonParsing(let data): + if let data { + if let unparsable = String(data: data, encoding: .utf8) { + return "Unable to parse response as JSON: \n\(unparsable)" + } else { + return "Received defective response" + } + } else { + return "Received empty response" + } } } } diff --git a/Tests/MasKitTests/Errors/MASErrorTestCase.swift b/Tests/MasKitTests/Errors/MASErrorTestCase.swift index cdefffd..e78722b 100644 --- a/Tests/MasKitTests/Errors/MASErrorTestCase.swift +++ b/Tests/MasKitTests/Errors/MASErrorTestCase.swift @@ -122,7 +122,7 @@ class MASErrorTestCase: XCTestCase { } func testJsonParsing() { - error = .jsonParsing(error: nil) - XCTAssertEqual(error.description, "Unable to parse response JSON") + error = .jsonParsing(data: nil) + XCTAssertEqual(error.description, "Received empty response") } } From b400422f4ae18325cbf84662b9b93e199de6939a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:16:28 -0400 Subject: [PATCH 07/81] `outdated` checks if new app version requires newer macOS for all kinds of apps, not just mac-software. Resolve #540 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Models/SearchResult.swift | 3 --- Sources/MasKit/Models/SoftwareProduct.swift | 6 ++---- Tests/MasKitTests/Models/SoftwareProductSpec.swift | 14 +++++++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Sources/MasKit/Models/SearchResult.swift b/Sources/MasKit/Models/SearchResult.swift index fe928b6..b2ccd6b 100644 --- a/Sources/MasKit/Models/SearchResult.swift +++ b/Sources/MasKit/Models/SearchResult.swift @@ -10,7 +10,6 @@ struct SearchResult: Decodable { var bundleId: String var currentVersionReleaseDate: String var fileSizeBytes: String? - var kind: String var minimumOsVersion: String var price: Double? var sellerName: String @@ -24,7 +23,6 @@ struct SearchResult: Decodable { bundleId: String = "", currentVersionReleaseDate: String = "", fileSizeBytes: String = "0", - kind: String = "", minimumOsVersion: String = "", price: Double = 0.0, sellerName: String = "", @@ -37,7 +35,6 @@ struct SearchResult: Decodable { self.bundleId = bundleId self.currentVersionReleaseDate = currentVersionReleaseDate self.fileSizeBytes = fileSizeBytes - self.kind = kind self.minimumOsVersion = minimumOsVersion self.price = price self.sellerName = sellerName diff --git a/Sources/MasKit/Models/SoftwareProduct.swift b/Sources/MasKit/Models/SoftwareProduct.swift index 280266a..6ce7180 100644 --- a/Sources/MasKit/Models/SoftwareProduct.swift +++ b/Sources/MasKit/Models/SoftwareProduct.swift @@ -28,15 +28,13 @@ extension SoftwareProduct { /// - Parameter storeApp: App from search result. /// - Returns: true if the app is outdated; false otherwise. func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool { - // Only look at min OS version if we have one, also only consider macOS apps - // Replace string literal with MasStoreSearch.Entity once `search` branch is merged. - if let osVersion = Version(tolerant: storeApp.minimumOsVersion), storeApp.kind == "mac-software" { + // If storeApp requires a version of macOS newer than the running version, do not consider self outdated. + if let osVersion = Version(tolerant: storeApp.minimumOsVersion) { let requiredVersion = OperatingSystemVersion( majorVersion: osVersion.major, minorVersion: osVersion.minor, patchVersion: osVersion.patch ) - // Don't consider an app outdated if the version in the app store requires a higher OS version. guard ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion) else { return false } diff --git a/Tests/MasKitTests/Models/SoftwareProductSpec.swift b/Tests/MasKitTests/Models/SoftwareProductSpec.swift index ba1262f..73806a4 100644 --- a/Tests/MasKitTests/Models/SoftwareProductSpec.swift +++ b/Tests/MasKitTests/Models/SoftwareProductSpec.swift @@ -26,10 +26,10 @@ public class SoftwareProductSpec: QuickSpec { itemIdentifier: 111 ) - let currentApp = SearchResult(kind: "mac-software", version: "1.0.0") - let appUpdate = SearchResult(kind: "mac-software", version: "2.0.0") - let higherOs = SearchResult(kind: "mac-software", minimumOsVersion: "99.0.0", version: "3.0.0") - let updateIos = SearchResult(kind: "software", minimumOsVersion: "99.0.0", version: "3.0.0") + let currentApp = SearchResult(version: "1.0.0") + let appUpdate = SearchResult(version: "2.0.0") + let higherOs = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0") + let updateIos = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0") it("is not outdated when there is no new version available") { expect(app.isOutdatedWhenComparedTo(currentApp)) == false @@ -37,11 +37,11 @@ public class SoftwareProductSpec: QuickSpec { it("is outdated when there is a new version available") { expect(app.isOutdatedWhenComparedTo(appUpdate)) == true } - it("is not outdated when the new version requires a higher OS version") { + it("is not outdated when the new version of mac-software requires a higher OS version") { expect(app.isOutdatedWhenComparedTo(higherOs)) == false } - it("ignores minimum iOS version") { - expect(app.isOutdatedWhenComparedTo(updateIos)) == true + it("is not outdated when the new version of software requires a higher OS version") { + expect(app.isOutdatedWhenComparedTo(updateIos)) == false } } } From 466ea671941e3062511cdb0fe4fdcf03a8528c35 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:28:25 -0400 Subject: [PATCH 08/81] Improve scripts: bootstrap, build, format, lint, test & version. Allow them to be run from any directory. Call version from lint & test to create Package.swift with version info. Fail when accessing unset variables. Improve variable names. Fix lint issues. Improve lint & format scripts. Don't require user input to continue linting. Much cleaner lint output. Reorder lint output. Get swift-format from Brewfile instead of from Package.swift: - Speeds up linting. - Properly models dependency (not a code dependency). - swift-format depends on an old version of swift-argument-parser. Will refactor to use SAP soon. Include some improvements from 1.8.7 PR. Other scripts need improvement, too. Resolve #545 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 7 ++--- Brewfile | 1 + Package.resolved | 27 ----------------- Package.swift | 27 ----------------- script/bootstrap | 11 +++++-- script/build | 15 +++++++--- script/format | 44 ++++++++++++++++------------ script/lint | 75 ++++++++++++++++++++++++++++++++++-------------- script/test | 12 ++++++-- script/version | 13 ++++++--- 10 files changed, 122 insertions(+), 110 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index abb72ba..1c9c9e3 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,13 +4,12 @@ # # https://github.com/realm/SwiftLint#configuration # - +--- disabled_rules: - non_optional_string_data_conversion - trailing_comma - excluded: - docs - opening_brace: - allow_multiline_func: true + ignore_multiline_function_signatures: true + ignore_multiline_statement_conditions: true diff --git a/Brewfile b/Brewfile index f849bab..9362a2f 100644 --- a/Brewfile +++ b/Brewfile @@ -1,6 +1,7 @@ brew "markdownlint-cli" brew "shellcheck" brew "shfmt" +brew "swift-format" brew "swiftformat" # Already installed on GitHub Actions runner. diff --git a/Package.resolved b/Package.resolved index ba46a29..6952cad 100644 --- a/Package.resolved +++ b/Package.resolved @@ -64,33 +64,6 @@ "version": "2.1.1" } }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version": "1.3.0" - } - }, - { - "package": "swift-format", - "repositoryURL": "https://github.com/apple/swift-format", - "state": { - "branch": "release/5.9", - "revision": "1323e87eced56bdcfed1bb78af1f16f39274d032", - "version": null - } - }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": "release/5.9", - "revision": "9a101b70eee2a9dec04f92d2d47b22ebe57a1aae", - "version": null - } - }, { "package": "Version", "repositoryURL": "https://github.com/mxcl/Version.git", diff --git a/Package.swift b/Package.swift index 15da902..2df3ba1 100644 --- a/Package.swift +++ b/Package.swift @@ -75,30 +75,3 @@ let package = Package( ], swiftLanguageVersions: [.v5] ) - -// https://github.com/apple/swift-format#matching-swift-format-to-your-swift-version-swift-57-and-earlier -#if compiler(>=5.8) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.9")) - ] -#elseif compiler(>=5.7) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.7")) - ] -#elseif compiler(>=5.6) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.6")) - ] -#elseif compiler(>=5.5) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.5-branch")) - ] -#elseif compiler(>=5.4) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.4-branch")) - ] -#elseif compiler(>=5.3) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.3-branch")) - ] -#endif diff --git a/script/bootstrap b/script/bootstrap index 9177fc7..fcfc38d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/bootstrap # mas @@ -6,10 +6,17 @@ # Installs development dependencies and builds project dependencies. # +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + main() { script/clean - echo "==> 👢 Bootstrapping" + printf $'==> 👢 Bootstrapping\n' # Install Homebrew tools rm -f Brewfile.lock.json diff --git a/script/build b/script/build index 8f0d1ae..44f3db0 100755 --- a/script/build +++ b/script/build @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/build # mas @@ -6,9 +6,16 @@ # Builds the Swift Package. # +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + # Build for the host architecture by default. ARCH=() -if [[ "$1" == '--universal' ]]; then +if [[ "${#}" -gt 1 && "${1}" == '--universal' ]]; then ARCH=(--arch arm64 --arch x86_64) fi @@ -21,6 +28,6 @@ fi echo "==> 🏗️ Building mas ($(script/version))" swift build \ --configuration release \ - "${ARCH[@]}" \ + "${ARCH[@]+"${ARCH[@]}"}" \ --disable-sandbox \ - "${CACHE[@]}" + "${CACHE[@]+"${CACHE[@]}"}" diff --git a/script/format b/script/format index dc1287c..9091140 100755 --- a/script/format +++ b/script/format @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/format # mas @@ -6,34 +6,42 @@ # Linting checks for development and CI. # # Automatically formats and fixes style violations using various tools. -# Additionally runs `lint` to report any remaining style violations. # # Please keep in sync with script/lint. # -echo "==> 🚨 Formatting mas" +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" -for LINTER in markdownlint shfmt swiftformat swiftlint; do +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> 🚨 Formatting mas\n' + +for LINTER in markdownlint shfmt swift-format swiftformat swiftlint; do if [[ ! -x "$(command -v ${LINTER})" ]]; then - echo "error: ${LINTER} is not installed. Run 'script/bootstrap' or 'brew install ${LINTER}'." + printf $'error: %s is not installed. Run \'script/bootstrap\' or \'brew install %s\'.\n' "${LINTER}" "${LINTER}" exit 1 fi done -echo -echo "--> 🖊 Markdown" -markdownlint --config .markdownlint.json --fix .github . - -echo -echo "--> 🕊️ Swift" for SOURCE in Package.swift Sources Tests; do - swiftformat ${SOURCE} - swift run swift-format format --in-place --recursive ${SOURCE} - swiftlint lint --fix --strict ${SOURCE} + printf -- $'--> 🕊 %s swift-format\n' "${SOURCE}" + swift-format format --in-place --recursive "${SOURCE}" + printf -- $'--> 🕊 %s swiftformat\n' "${SOURCE}" + swiftformat "${SOURCE}" + printf -- $'--> 🕊 %s swiftlint\n' "${SOURCE}" + swiftlint --fix --strict "${SOURCE}" done -echo -echo "--> 📜 Bash" -shfmt -i 2 -l -w contrib/ script/ +printf -- $'--> 📜 Bash shfmt\n' +shfmt \ + --write \ + --list \ + --indent 2 \ + --case-indent \ + contrib/ script/ -script/lint +printf -- $'--> 〽️ Markdown\n' +markdownlint --config .markdownlint.json --fix .github . diff --git a/script/lint b/script/lint index e3c3668..c362dbb 100755 --- a/script/lint +++ b/script/lint @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -u # # script/lint # mas @@ -10,32 +10,63 @@ # Please keep in sync with script/format. # -echo "==> 🚨 Linting mas" +set -o pipefail -for LINTER in git markdownlint periphery shfmt swiftformat swiftlint; do - if [[ ! -x "$(command -v ${LINTER})" ]]; then - echo "error: ${LINTER} is not installed. Run 'script/bootstrap' or 'brew install ${LINTER}'." +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> 🚨 Linting mas (%s)\n' "$(script/version)" + +for linter in git markdownlint periphery shellcheck shfmt swift-format swiftformat swiftlint; do + if [[ ! -x "$(command -v ${linter})" ]]; then + printf $'error: %s is not installed. Run \'script/bootstrap\' or \'brew install %s\'.\n' "${linter}" "${linter}" exit 1 fi done -echo "--> 🌳 Git" -git diff --check - -echo -echo "--> 🖊 Markdown" -markdownlint --config .markdownlint.json .github . - -echo -echo "--> 🕊️ Swift" -for SOURCE in Package.swift Sources Tests; do - swiftformat --lint ${SOURCE} - swift run swift-format lint --recursive ${SOURCE} - swiftlint lint --strict ${SOURCE} +exit_code=0 +for source in Package.swift Sources Tests; do + printf -- $'--> 🕊 %s swift-format\n' "${source}" + swift-format lint --strict --recursive "${source}" + ((exit_code |= "${?}")) + printf -- $'--> 🕊 %s swiftformat\n' "${source}" + script -q /dev/null swiftformat --lint --strict "${source}" | + (grep -vxE $'Running SwiftFormat\\.\\.\\.\r|\\(lint mode - no files will be changed\\.\\)\r|Reading (?:config|swift-version) file at .*|\033\[32mSwiftFormat completed in \\d+\\.\\d+s\\.\033\\[0m\r|0/\\d+ files require formatting\\.\r' || true) + ((exit_code |= "${?}")) + printf -- $'--> 🕊 %s swiftlint\n' "${source}" + swiftlint --strict --quiet "${source}" 2> \ + >((grep -vxF $'warning: Configuration option \'allow_multiline_func\' in \'opening_brace\' rule is deprecated. Use the option \'ignore_multiline_function_signatures\' instead.' || true) >&2) + ((exit_code |= "${?}")) done -periphery scan -echo -echo "--> 📜 Bash" +printf -- $'--> 🐚 Bash shellcheck\n' shellcheck --shell=bash script/* -shfmt -d -i 2 -l contrib/ script/ +((exit_code |= "${?}")) + +printf -- $'--> 📜 Bash shfmt\n' +shfmt \ + --diff \ + --list \ + --indent 2 \ + --case-indent \ + contrib/ script/ +((exit_code |= "${?}")) + +printf -- $'--> 〽️ Markdown\n' +markdownlint --config .markdownlint.json .github . +((exit_code |= "${?}")) + +printf -- $'--> 🌳 Git\n' +PAGER='cat' git diff --check +((exit_code |= "${?}")) + +printf -- $'--> 🌀 Periphery\n' +script -q /dev/null periphery scan --strict --quiet --disable-update-check | + (grep -vxF $'\033[0;1;32m* \033[0;0m\033[0;1mNo unused code detected.\033[0;0m\r\n' || true) +((exit_code |= "${?}")) + +exit "${exit_code}" diff --git a/script/test b/script/test index 01188ef..e927db7 100755 --- a/script/test +++ b/script/test @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/test # mas @@ -6,5 +6,13 @@ # Runs mas tests. # -echo "==> ✅ Testing" +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> ✅ Testing mas (%s)\n' "$(script/version)" + swift test diff --git a/script/version b/script/version index 9bce7a8..bbc2219 100755 --- a/script/version +++ b/script/version @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/version # mas @@ -9,12 +9,17 @@ # This no longer works with MARKETING_VERSION build setting in Info.plist # agvtool what-marketing-version -terse1 +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + VERSION=$(git describe --abbrev=0 --tags) VERSION=${VERSION#v} -SCRIPT_PATH=$(dirname "$(which "$0")") - -cat <"${SCRIPT_PATH}/../Sources/MasKit/Package.swift" +cat <"Sources/MasKit/Package.swift" // Generated by: script/version enum Package { static let version = "${VERSION}" From 5676dcb7972b364f6a4aed08f8e7a07ea9a50f71 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:27:14 -0400 Subject: [PATCH 09/81] Re-enable `purchase`. `purchase` seems to work on macOS 12+. Resolve #289 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/Commands/Purchase.swift | 6 ------ Tests/MasKitTests/Commands/PurchaseCommandSpec.swift | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/MasKit/Commands/Purchase.swift b/Sources/MasKit/Commands/Purchase.swift index c8dcd2a..9138027 100644 --- a/Sources/MasKit/Commands/Purchase.swift +++ b/Sources/MasKit/Commands/Purchase.swift @@ -29,12 +29,6 @@ public struct PurchaseCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { - if #available(macOS 10.15, *) { - // Purchases are no longer possible as of Catalina. - // https://github.com/mas-cli/mas/issues/289 - return .failure(.notSupported) - } - // Try to download applications with given identifiers and collect results let appIds = options.appIds.filter { appId in if let product = appLibrary.installedApp(forId: appId) { diff --git a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift b/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift index ce85977..8ceaa4a 100644 --- a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift +++ b/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift @@ -20,11 +20,7 @@ public class PurchaseCommandSpec: QuickSpec { it("purchases apps") { let cmd = PurchaseCommand() let result = cmd.run(PurchaseCommand.Options(appIds: [])) - expect(result) - .to( - beFailure { error in - expect(error) == .notSupported - }) + expect(result).to(beSuccess()) } } } From d6087ce2883e5e191bd2d97e966b322d3dcf02a2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:32:30 -0400 Subject: [PATCH 10/81] Update README.md. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 71258b0..8843713 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,6 @@ $ mas lucky twitter > Please note that this command will not allow you to install (or even purchase) an app for the first time: use the `purchase` command in that case. -> ⛔ The `purchase` command is not supported as of macOS 10.15 Catalina. Please see [Known Issues](#%EF%B8%8F-known-issues). ```bash $ mas purchase 768053424 @@ -193,10 +192,9 @@ docs for more details. ## ⚠️ Known Issues Over time, Apple has changed the APIs used by `mas` to manage App Store apps, limiting its capabilities. Please sign in -or purchase apps using the App Store app instead. Subsequent redownloads can be performed with `mas install`. +using the App Store app instead. Subsequent redownloads can be performed with `mas install`. - ⛔️ The `signin` command is not supported as of macOS 10.13 High Sierra. [#164](https://github.com/mas-cli/mas/issues/164) -- ⛔️ The `purchase` command is not supported as of macOS 10.15 Catalina. [#289](https://github.com/mas-cli/mas/issues/289) - ⛔️ The `account` command is not supported as of macOS 12 Monterey. [#417](https://github.com/mas-cli/mas/issues/417) The versions `mas` sees from the app bundles on your Mac don't always match the versions reported by the App Store for From 8e671f26242d4695d181c5af29fb04da635415c3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:47:59 -0400 Subject: [PATCH 11/81] Improve `Account`, `SignIn`, `SignOut` & `ISStoreAccount` extension & associated code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent `ISAccountService.signIn(with:)` deprecation warning. Improve macOS version handling encapsulation. Output error if `StoreAccount.signin(…)` provides a `nil` `ISStoreAccount`. Improve `signin` `"Already Signed In" error output. Remove unnecessary output. Simplify code. Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- README.md | 7 +- Sources/MasKit/AppStore/ISStoreAccount.swift | 94 ++++++++++--------- Sources/MasKit/Commands/Account.swift | 3 +- Sources/MasKit/Commands/SignIn.swift | 32 ++----- Sources/MasKit/Commands/SignOut.swift | 3 +- Sources/MasKit/Errors/MASError.swift | 6 +- .../MasKitTests/Errors/MASErrorTestCase.swift | 4 +- 7 files changed, 68 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 71258b0..e9282d1 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,6 @@ To sign into the Mac App Store for the first time run `mas signin`. ```bash $ mas signin mas@example.com -==> Signing in to Apple ID: mas@example.com Password: ``` @@ -171,15 +170,13 @@ If you experience issues signing in this way, you can ask to sign in using a gra (provided by Mac App Store application): ```bash -$ mas signin --dialog mas@example.com -==> Signing in to Apple ID: mas@example.com +mas signin --dialog mas@example.com ``` You can also embed your password in the command. ```bash -$ mas signin mas@example.com 'ZdkM4f$gzF;gX3ABXNLf8KcCt.x.np' -==> Signing in to Apple ID: mas@example.com +mas signin mas@example.com 'ZdkM4f$gzF;gX3ABXNLf8KcCt.x.np' ``` Use `mas signout` to sign out from the Mac App Store. diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/MasKit/AppStore/ISStoreAccount.swift index 5865332..c61866d 100644 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ b/Sources/MasKit/AppStore/ISStoreAccount.swift @@ -17,65 +17,73 @@ extension ISStoreAccount: StoreAccount { let group = DispatchGroup() group.enter() - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - accountService.primaryAccount { (storeAccount: ISStoreAccount) in + ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in account = storeAccount group.leave() } _ = group.wait(timeout: .now() + 30) } else { - // macOS 10.9-10.12 - let accountStore = CKAccountStore.shared() - account = accountStore.primaryAccount + account = CKAccountStore.shared().primaryAccount } return account } static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> StoreAccount { - var storeAccount: ISStoreAccount? - var maserror: MASError? - - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - let client = ISStoreClient(storeClientType: 0) - accountService.setStoreClient(client) - - let context = ISAuthenticationContext(accountID: 0) - context.appleIDOverride = username - - if systemDialog { - context.appleIDOverride = username + if #available(macOS 10.13, *) { + // Signing in is no longer possible as of High Sierra. + // https://github.com/mas-cli/mas/issues/164 + throw MASError.notSupported } else { - context.demoMode = true - context.demoAccountName = username - context.demoAccountPassword = password - context.demoAutologinMode = true - } - - let group = DispatchGroup() - group.enter() - - // Only works on macOS Sierra and below - accountService.signIn(with: context) { success, account, error in - if success { - storeAccount = account - } else { - maserror = .signInFailed(error: error as NSError?) + let primaryAccount = primaryAccount + if primaryAccount != nil { + throw MASError.alreadySignedIn(asAccountId: primaryAccount!.identifier) } - group.leave() - } - if systemDialog { - group.wait() - } else { - _ = group.wait(timeout: .now() + 30) - } + let password = + password.isEmpty && !systemDialog + ? String(validatingUTF8: getpass("Password: "))! + : password - if let account = storeAccount { - return account - } + let accountService = ISServiceProxy.genericShared().accountService + accountService.setStoreClient(ISStoreClient(storeClientType: 0)) - throw maserror ?? MASError.signInFailed(error: nil) + let context = ISAuthenticationContext(accountID: 0) + context.appleIDOverride = username + if !systemDialog { + context.demoMode = true + context.demoAccountName = username + context.demoAccountPassword = password + context.demoAutologinMode = true + } + + let group = DispatchGroup() + group.enter() + + var storeAccount: ISStoreAccount? + var maserror: MASError? + // Only works on macOS Sierra and below + accountService.signIn(with: context) { success, account, error in + if success, let account { + storeAccount = account + } else { + maserror = .signInFailed(error: error as NSError?) + } + group.leave() + } + + if systemDialog { + group.wait() + } else { + _ = group.wait(timeout: .now() + 30) + } + + if let storeAccount { + return storeAccount + } + + throw maserror ?? MASError.signInFailed(error: nil) + } } } diff --git a/Sources/MasKit/Commands/Account.swift b/Sources/MasKit/Commands/Account.swift index 45249f7..9701054 100644 --- a/Sources/MasKit/Commands/Account.swift +++ b/Sources/MasKit/Commands/Account.swift @@ -25,9 +25,8 @@ public struct AccountCommand: CommandProtocol { } if let account = ISStoreAccount.primaryAccount { - print(String(describing: account.identifier)) + print(account.identifier) } else { - printError("Not signed in") return .failure(.notSignedIn) } return .success(()) diff --git a/Sources/MasKit/Commands/SignIn.swift b/Sources/MasKit/Commands/SignIn.swift index f1b7cf0..1ac241f 100644 --- a/Sources/MasKit/Commands/SignIn.swift +++ b/Sources/MasKit/Commands/SignIn.swift @@ -19,32 +19,16 @@ public struct SignInCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { - if #available(macOS 10.13, *) { - // Signing in is no longer possible as of High Sierra. - // https://github.com/mas-cli/mas/issues/164 - return .failure(.notSupported) - } - - guard ISStoreAccount.primaryAccount == nil else { - return .failure(.alreadySignedIn) - } - do { - printInfo("Signing in to Apple ID: \(options.username)") - - let password: String = { - if options.password.isEmpty, !options.dialog { - return String(validatingUTF8: getpass("Password: "))! - } - return options.password - }() - - _ = try ISStoreAccount.signIn(username: options.username, password: password, systemDialog: options.dialog) - } catch let error as NSError { - return .failure(.signInFailed(error: error)) + _ = try ISStoreAccount.signIn( + username: options.username, + password: options.password, + systemDialog: options.dialog + ) + return .success(()) + } catch { + return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) } - - return .success(()) } } diff --git a/Sources/MasKit/Commands/SignOut.swift b/Sources/MasKit/Commands/SignOut.swift index efdf467..b222b8c 100644 --- a/Sources/MasKit/Commands/SignOut.swift +++ b/Sources/MasKit/Commands/SignOut.swift @@ -19,8 +19,7 @@ public struct SignOutCommand: CommandProtocol { /// Runs the command. public func run(_: Options) -> Result { if #available(macOS 10.13, *) { - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - accountService.signOut() + ISServiceProxy.genericShared().accountService.signOut() } else { // Using CKAccountStore to sign out does nothing on High Sierra // https://github.com/mas-cli/mas/issues/129 diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/MasKit/Errors/MASError.swift index 716f4ea..aaa768f 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/MasKit/Errors/MASError.swift @@ -13,7 +13,7 @@ public enum MASError: Error, Equatable { case notSignedIn case signInFailed(error: NSError?) - case alreadySignedIn + case alreadySignedIn(asAccountId: String) case purchaseFailed(error: NSError?) case downloadFailed(error: NSError?) @@ -52,8 +52,8 @@ extension MASError: CustomStringConvertible { return "Sign in failed" } - case .alreadySignedIn: - return "Already signed in" + case .alreadySignedIn(let accountId): + return "Already signed in as \(accountId)" case .purchaseFailed(let error): if let error { diff --git a/Tests/MasKitTests/Errors/MASErrorTestCase.swift b/Tests/MasKitTests/Errors/MASErrorTestCase.swift index cdefffd..38e4ae1 100644 --- a/Tests/MasKitTests/Errors/MASErrorTestCase.swift +++ b/Tests/MasKitTests/Errors/MASErrorTestCase.swift @@ -57,8 +57,8 @@ class MASErrorTestCase: XCTestCase { } func testAlreadySignedIn() { - error = .alreadySignedIn - XCTAssertEqual(error.description, "Already signed in") + error = .alreadySignedIn(asAccountId: "person@example.com") + XCTAssertEqual(error.description, "Already signed in as person@example.com") } func testPurchaseFailed() { From d2f2d3edff77d362b5831251ec13e855d69c49e4 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:12:13 -0400 Subject: [PATCH 12/81] `ISStoreAccount` static functions return `ISStoreAccount` instead of `StoreAccount`. Add `var dsID` to `StoreAccount` so functions can use `StoreAccount` without casting to `ISStoreAccount`. Simplify code. Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/Downloader.swift | 10 +++------- Sources/MasKit/AppStore/ISStoreAccount.swift | 15 +++++++-------- Sources/MasKit/AppStore/SSPurchase.swift | 2 +- Sources/MasKit/AppStore/StoreAccount.swift | 3 +++ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift index ca6530c..7bf98ab 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/MasKit/AppStore/Downloader.swift @@ -63,18 +63,14 @@ private func downloadWithRetries( /// Only works for free apps. Defaults to false. /// - Returns: A promise the completes when the download is complete. private func download(_ appID: UInt64, purchase: Bool = false) -> Promise { - var storeAccount: ISStoreAccount? + var storeAccount: StoreAccount? if #unavailable(macOS 12) { // Monterey obscured the user's account information, but still allows // redownloads without passing it to SSPurchase. // https://github.com/mas-cli/mas/issues/417 - guard let account = ISStoreAccount.primaryAccount else { - return Promise(error: MASError.notSignedIn) - } - - storeAccount = account as? ISStoreAccount + storeAccount = ISStoreAccount.primaryAccount guard storeAccount != nil else { - fatalError("Unable to cast StoreAccount to ISStoreAccount") + return Promise(error: MASError.notSignedIn) } } diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/MasKit/AppStore/ISStoreAccount.swift index c61866d..d78cd23 100644 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ b/Sources/MasKit/AppStore/ISStoreAccount.swift @@ -10,27 +10,26 @@ import CommerceKit import StoreFoundation extension ISStoreAccount: StoreAccount { - static var primaryAccount: StoreAccount? { - var account: ISStoreAccount? - + static var primaryAccount: ISStoreAccount? { if #available(macOS 10.13, *) { let group = DispatchGroup() group.enter() + var account: ISStoreAccount? ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in account = storeAccount group.leave() } _ = group.wait(timeout: .now() + 30) - } else { - account = CKAccountStore.shared().primaryAccount - } - return account + return account + } else { + return CKAccountStore.shared().primaryAccount + } } - static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> StoreAccount { + static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> ISStoreAccount { if #available(macOS 10.13, *) { // Signing in is no longer possible as of High Sierra. // https://github.com/mas-cli/mas/issues/164 diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift index c3d3dcb..6cc858d 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/MasKit/AppStore/SSPurchase.swift @@ -13,7 +13,7 @@ typealias SSPurchaseCompletion = (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void extension SSPurchase { - convenience init(adamId: UInt64, account: ISStoreAccount?, purchase: Bool = false) { + convenience init(adamId: UInt64, account: StoreAccount?, purchase: Bool = false) { self.init() var parameters: [String: Any] = [ diff --git a/Sources/MasKit/AppStore/StoreAccount.swift b/Sources/MasKit/AppStore/StoreAccount.swift index 6de28e4..a26cd07 100644 --- a/Sources/MasKit/AppStore/StoreAccount.swift +++ b/Sources/MasKit/AppStore/StoreAccount.swift @@ -6,6 +6,9 @@ // Copyright © 2018 Andrew Naylor. All rights reserved. // +import Foundation + protocol StoreAccount { var identifier: String { get set } + var dsID: NSNumber { get set } } From a5c8957fc9b2f495cc6e2571a02da6a89ee43808 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:31:35 -0400 Subject: [PATCH 13/81] Refactor `Downloader` & `SSPurchase` to no longer need `StoreAccount`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move towards better purchase encapsulation: all purchase logic will eventually be in the `perform(…)` function, not in convenience constructor, to ensure the correct values are used when `perform(…)` is called, instead of values that were correct sometime before the call. Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/Downloader.swift | 15 ++++----------- Sources/MasKit/AppStore/SSPurchase.swift | 13 +++++++++---- Sources/MasKit/AppStore/StoreAccount.swift | 1 + 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift index 7bf98ab..9e33085 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/MasKit/AppStore/Downloader.swift @@ -63,19 +63,12 @@ private func downloadWithRetries( /// Only works for free apps. Defaults to false. /// - Returns: A promise the completes when the download is complete. private func download(_ appID: UInt64, purchase: Bool = false) -> Promise { - var storeAccount: StoreAccount? - if #unavailable(macOS 12) { - // Monterey obscured the user's account information, but still allows - // redownloads without passing it to SSPurchase. - // https://github.com/mas-cli/mas/issues/417 - storeAccount = ISStoreAccount.primaryAccount - guard storeAccount != nil else { - return Promise(error: MASError.notSignedIn) + Promise { seal in + guard let purchase = SSPurchase(adamId: appID, purchase: purchase) else { + seal.reject(MASError.notSignedIn) + return } - } - return Promise { seal in - let purchase = SSPurchase(adamId: appID, account: storeAccount, purchase: purchase) purchase.perform { purchase, _, error, response in if let error { seal.reject(MASError.purchaseFailed(error: error as NSError?)) diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift index 6cc858d..5fd0eab 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/MasKit/AppStore/SSPurchase.swift @@ -13,7 +13,7 @@ typealias SSPurchaseCompletion = (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void extension SSPurchase { - convenience init(adamId: UInt64, account: StoreAccount?, purchase: Bool = false) { + convenience init?(adamId: UInt64, purchase: Bool = false) { self.init() var parameters: [String: Any] = [ @@ -41,9 +41,14 @@ extension SSPurchase { itemIdentifier = adamId - if let account { - accountIdentifier = account.dsID - appleID = account.identifier + if #unavailable(macOS 12) { + // Monterey obscures the user's App Store account information, but allows + // redownloads without passing the account to SSPurchase. + // https://github.com/mas-cli/mas/issues/417 + if let storeAccount = ISStoreAccount.primaryAccount { + accountIdentifier = storeAccount.dsID + appleID = storeAccount.identifier + } } // Not sure if this is needed, but lets use it here. diff --git a/Sources/MasKit/AppStore/StoreAccount.swift b/Sources/MasKit/AppStore/StoreAccount.swift index a26cd07..366cd6e 100644 --- a/Sources/MasKit/AppStore/StoreAccount.swift +++ b/Sources/MasKit/AppStore/StoreAccount.swift @@ -8,6 +8,7 @@ import Foundation +// periphery:ignore - save for future use in testing protocol StoreAccount { var identifier: String { get set } var dsID: NSNumber { get set } From 943638fef4fdd6020a53126957c4cb51ed251120 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:27:53 -0400 Subject: [PATCH 14/81] =?UTF-8?q?Improve=20`ISStoreAccount.signIn(?= =?UTF-8?q?=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Output error for non-`nil` `ISStoreAccount.primaryAccount` only if `ISStoreAccount.primaryAccount.isSignedIn` is `true`. Output error when no password provided to password prompt for `mas signin`. Remove unnecessary blank lines in `MASError.swift`. Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/ISStoreAccount.swift | 9 ++++++--- Sources/MasKit/Errors/MASError.swift | 17 +++-------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/MasKit/AppStore/ISStoreAccount.swift index d78cd23..29be002 100644 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ b/Sources/MasKit/AppStore/ISStoreAccount.swift @@ -35,9 +35,8 @@ extension ISStoreAccount: StoreAccount { // https://github.com/mas-cli/mas/issues/164 throw MASError.notSupported } else { - let primaryAccount = primaryAccount - if primaryAccount != nil { - throw MASError.alreadySignedIn(asAccountId: primaryAccount!.identifier) + if let account = primaryAccount, account.isSignedIn { + throw MASError.alreadySignedIn(asAccountId: account.identifier) } let password = @@ -45,6 +44,10 @@ extension ISStoreAccount: StoreAccount { ? String(validatingUTF8: getpass("Password: "))! : password + guard !password.isEmpty || systemDialog else { + throw MASError.noPasswordProvided + } + let accountService = ISServiceProxy.genericShared().accountService accountService.setStoreClient(ISStoreClient(storeClientType: 0)) diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/MasKit/Errors/MASError.swift index aaa768f..57e40af 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/MasKit/Errors/MASError.swift @@ -12,6 +12,7 @@ public enum MASError: Error, Equatable { case notSupported case notSignedIn + case noPasswordProvided case signInFailed(error: NSError?) case alreadySignedIn(asAccountId: String) @@ -37,62 +38,50 @@ extension MASError: CustomStringConvertible { switch self { case .notSignedIn: return "Not signed in" - + case .noPasswordProvided: + return "No password provided" case .notSupported: return """ This command is not supported on this macOS version due to changes in macOS. \ For more information see: \ https://github.com/mas-cli/mas#%EF%B8%8F-known-issues """ - case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" } else { return "Sign in failed" } - case .alreadySignedIn(let accountId): return "Already signed in as \(accountId)" - case .purchaseFailed(let error): if let error { return "Download request failed: \(error.localizedDescription)" } else { return "Download request failed" } - case .downloadFailed(let error): if let error { return "Download failed: \(error.localizedDescription)" } else { return "Download failed" } - case .noDownloads: return "No downloads began" - case .cancelled: return "Download cancelled" - case .searchFailed: return "Search failed" - case .noSearchResultsFound: return "No results found" - case .noVendorWebsite: return "App does not have a vendor website" - case .notInstalled: return "Not installed" - case .uninstallFailed: return "Uninstall failed" - case .noData: return "Service did not return data" - case .jsonParsing: return "Unable to parse response JSON" } From aceb4c3ddb62efaf07e030842074b0a7921188b0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:32:19 -0400 Subject: [PATCH 15/81] =?UTF-8?q?Remove=20unnecessary=20output=20when=20no?= =?UTF-8?q?thing=20downloaded=20by=20`download(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/Downloader.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift index 9e33085..fae5aa8 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/MasKit/AppStore/Downloader.swift @@ -76,7 +76,6 @@ private func download(_ appID: UInt64, purchase: Bool = false) -> Promise } guard response?.downloads.isEmpty == false, let purchase else { - print("No downloads") seal.reject(MASError.noDownloads) return } From c01f7c541e0c6fdab427dd0c2d4f8cfe34237338 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:15:44 -0400 Subject: [PATCH 16/81] Migrate `ISStoreAccount` from Grand Central Dispatch to PromiseKit. Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 1 + Sources/MasKit/AppStore/ISStoreAccount.swift | 116 +++++++++---------- Sources/MasKit/AppStore/SSPurchase.swift | 2 +- Sources/MasKit/Commands/Account.swift | 10 +- Sources/MasKit/Commands/SignIn.swift | 1 + Sources/MasKit/Errors/MASError.swift | 8 ++ 6 files changed, 72 insertions(+), 66 deletions(-) diff --git a/.swiftformat b/.swiftformat index ce62d1c..f727bd2 100644 --- a/.swiftformat +++ b/.swiftformat @@ -11,6 +11,7 @@ --disable blankLinesAroundMark --disable consecutiveSpaces --disable hoistPatternLet +--disable hoistTry --disable indent --disable trailingCommas diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/MasKit/AppStore/ISStoreAccount.swift index 29be002..ae62f10 100644 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ b/Sources/MasKit/AppStore/ISStoreAccount.swift @@ -7,85 +7,81 @@ // import CommerceKit +import PromiseKit import StoreFoundation extension ISStoreAccount: StoreAccount { - static var primaryAccount: ISStoreAccount? { + static var primaryAccount: Promise { if #available(macOS 10.13, *) { - let group = DispatchGroup() - group.enter() - - var account: ISStoreAccount? - ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in - account = storeAccount - group.leave() - } - - _ = group.wait(timeout: .now() + 30) - - return account + return race( + Promise { seal in + ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in + seal.fulfill(storeAccount) + } + }, + after(seconds: 30).then { + Promise(error: MASError.notSignedIn) + } + ) } else { - return CKAccountStore.shared().primaryAccount + return .value(CKAccountStore.shared().primaryAccount) } } - static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> ISStoreAccount { + static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { if #available(macOS 10.13, *) { // Signing in is no longer possible as of High Sierra. // https://github.com/mas-cli/mas/issues/164 - throw MASError.notSupported + return Promise(error: MASError.notSupported) } else { - if let account = primaryAccount, account.isSignedIn { - throw MASError.alreadySignedIn(asAccountId: account.identifier) - } + return + primaryAccount + .then { account -> Promise in + if account.isSignedIn { + return Promise(error: MASError.alreadySignedIn(asAccountId: account.identifier)) + } - let password = - password.isEmpty && !systemDialog - ? String(validatingUTF8: getpass("Password: "))! - : password + let password = + password.isEmpty && !systemDialog + ? String(validatingUTF8: getpass("Password: "))! + : password - guard !password.isEmpty || systemDialog else { - throw MASError.noPasswordProvided - } + guard !password.isEmpty || systemDialog else { + return Promise(error: MASError.noPasswordProvided) + } - let accountService = ISServiceProxy.genericShared().accountService - accountService.setStoreClient(ISStoreClient(storeClientType: 0)) + let context = ISAuthenticationContext(accountID: 0) + context.appleIDOverride = username - let context = ISAuthenticationContext(accountID: 0) - context.appleIDOverride = username - if !systemDialog { - context.demoMode = true - context.demoAccountName = username - context.demoAccountPassword = password - context.demoAutologinMode = true - } + let signInPromise = + Promise { seal in + let accountService = ISServiceProxy.genericShared().accountService + accountService.setStoreClient(ISStoreClient(storeClientType: 0)) + accountService.signIn(with: context) { success, storeAccount, error in + if success, let storeAccount { + seal.fulfill(storeAccount) + } else { + seal.reject(MASError.signInFailed(error: error as NSError?)) + } + } + } - let group = DispatchGroup() - group.enter() + if systemDialog { + return signInPromise + } else { + context.demoMode = true + context.demoAccountName = username + context.demoAccountPassword = password + context.demoAutologinMode = true - var storeAccount: ISStoreAccount? - var maserror: MASError? - // Only works on macOS Sierra and below - accountService.signIn(with: context) { success, account, error in - if success, let account { - storeAccount = account - } else { - maserror = .signInFailed(error: error as NSError?) + return race( + signInPromise, + after(seconds: 30).then { + Promise(error: MASError.signInFailed(error: nil)) + } + ) + } } - group.leave() - } - - if systemDialog { - group.wait() - } else { - _ = group.wait(timeout: .now() + 30) - } - - if let storeAccount { - return storeAccount - } - - throw maserror ?? MASError.signInFailed(error: nil) } } } diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift index 5fd0eab..2bab990 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/MasKit/AppStore/SSPurchase.swift @@ -45,7 +45,7 @@ extension SSPurchase { // Monterey obscures the user's App Store account information, but allows // redownloads without passing the account to SSPurchase. // https://github.com/mas-cli/mas/issues/417 - if let storeAccount = ISStoreAccount.primaryAccount { + if let storeAccount = try? ISStoreAccount.primaryAccount.wait() { accountIdentifier = storeAccount.dsID appleID = storeAccount.identifier } diff --git a/Sources/MasKit/Commands/Account.swift b/Sources/MasKit/Commands/Account.swift index 9701054..333e2d2 100644 --- a/Sources/MasKit/Commands/Account.swift +++ b/Sources/MasKit/Commands/Account.swift @@ -24,11 +24,11 @@ public struct AccountCommand: CommandProtocol { return .failure(.notSupported) } - if let account = ISStoreAccount.primaryAccount { - print(account.identifier) - } else { - return .failure(.notSignedIn) + do { + print(try ISStoreAccount.primaryAccount.wait().identifier) + return .success(()) + } catch { + return .failure(error as? MASError ?? .failed(error: error as NSError)) } - return .success(()) } } diff --git a/Sources/MasKit/Commands/SignIn.swift b/Sources/MasKit/Commands/SignIn.swift index 1ac241f..e04769e 100644 --- a/Sources/MasKit/Commands/SignIn.swift +++ b/Sources/MasKit/Commands/SignIn.swift @@ -25,6 +25,7 @@ public struct SignInCommand: CommandProtocol { password: options.password, systemDialog: options.dialog ) + .wait() return .success(()) } catch { return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/MasKit/Errors/MASError.swift index 57e40af..d5c4683 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/MasKit/Errors/MASError.swift @@ -11,6 +11,8 @@ import Foundation public enum MASError: Error, Equatable { case notSupported + case failed(error: NSError?) + case notSignedIn case noPasswordProvided case signInFailed(error: NSError?) @@ -46,6 +48,12 @@ extension MASError: CustomStringConvertible { For more information see: \ https://github.com/mas-cli/mas#%EF%B8%8F-known-issues """ + case .failed(let error): + if let error { + return "Failed: \(error.localizedDescription)" + } else { + return "Failed" + } case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" From a45308f2f26d124237c2d4f982c7cab975c311e9 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:38:24 -0400 Subject: [PATCH 17/81] =?UTF-8?q?Configure=20`SSPurchase`=20in=20``perform?= =?UTF-8?q?(=E2=80=A6)`=20instead=20of=20in=20a=20`convenience=20init`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/Downloader.swift | 16 ++++------------ Sources/MasKit/AppStore/SSPurchase.swift | 10 ++-------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift index fae5aa8..4af1a2f 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/MasKit/AppStore/Downloader.swift @@ -34,9 +34,7 @@ func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise { } } -private func downloadWithRetries( - _ appID: UInt64, purchase: Bool = false, attempts: Int = 3 -) -> Promise { +private func downloadWithRetries(_ appID: UInt64, purchase: Bool = false, attempts: Int = 3) -> Promise { download(appID, purchase: purchase).recover { error -> Promise in guard attempts > 1 else { throw error @@ -59,17 +57,11 @@ private func downloadWithRetries( /// Downloads an app, printing progress to the console. /// /// - Parameter appID: The ID of the app to be downloaded -/// - Parameter purchase: Flag indicating whether the app needs to be purchased. -/// Only works for free apps. Defaults to false. +/// - Parameter purchase: Flag indicating whether the app needs to be purchased. Only works for free apps. /// - Returns: A promise the completes when the download is complete. -private func download(_ appID: UInt64, purchase: Bool = false) -> Promise { +private func download(_ appID: UInt64, purchase: Bool) -> Promise { Promise { seal in - guard let purchase = SSPurchase(adamId: appID, purchase: purchase) else { - seal.reject(MASError.notSignedIn) - return - } - - purchase.perform { purchase, _, error, response in + SSPurchase().perform(adamId: appID, purchase: purchase) { purchase, _, error, response in if let error { seal.reject(MASError.purchaseFailed(error: error as NSError?)) return diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift index 2bab990..09c5bc8 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/MasKit/AppStore/SSPurchase.swift @@ -13,9 +13,7 @@ typealias SSPurchaseCompletion = (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void extension SSPurchase { - convenience init?(adamId: UInt64, purchase: Bool = false) { - self.init() - + func perform(adamId: UInt64, purchase: Bool, _ completion: @escaping SSPurchaseCompletion) { var parameters: [String: Any] = [ "productType": "C", "price": 0, @@ -56,14 +54,10 @@ extension SSPurchase { isRedownload = false } - let downloadMetadata = SSDownloadMetadata() + downloadMetadata = SSDownloadMetadata() downloadMetadata.kind = "software" downloadMetadata.itemIdentifier = adamId - self.downloadMetadata = downloadMetadata - } - - func perform(_ completion: @escaping SSPurchaseCompletion) { CKPurchaseController.shared().perform(self, withOptions: 0, completionHandler: completion) } } From fe749fc2eb77fcc3f35fb5116dd5d94bde2a12dc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:17:36 -0400 Subject: [PATCH 18/81] =?UTF-8?q?Handle=20`CKPurchaseController.shared().p?= =?UTF-8?q?erform(=E2=80=A6)`=20callback=20within=20`SSPurchase.perform(ad?= =?UTF-8?q?amId:purchase:)`=20to=20encapsulate=20`CKPurchaseController`=20?= =?UTF-8?q?&=20its=20callback.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chain `Promise`s instead of calling `Promise.wait()`. Improve comments. Resolve #562 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/MasKit/AppStore/Downloader.swift | 60 +++++----------------- Sources/MasKit/AppStore/SSPurchase.swift | 65 +++++++++++++++++------- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift index 4af1a2f..2208869 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/MasKit/AppStore/Downloader.swift @@ -35,56 +35,22 @@ func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise { } private func downloadWithRetries(_ appID: UInt64, purchase: Bool = false, attempts: Int = 3) -> Promise { - download(appID, purchase: purchase).recover { error -> Promise in - guard attempts > 1 else { - throw error - } - - // If the download failed due to network issues, try again. Otherwise, fail immediately. - guard case MASError.downloadFailed(let downloadError) = error, - case NSURLErrorDomain = downloadError?.domain - else { - throw error - } - - let attempts = attempts - 1 - printWarning((downloadError ?? error).localizedDescription) - printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").") - return downloadWithRetries(appID, purchase: purchase, attempts: attempts) - } -} - -/// Downloads an app, printing progress to the console. -/// -/// - Parameter appID: The ID of the app to be downloaded -/// - Parameter purchase: Flag indicating whether the app needs to be purchased. Only works for free apps. -/// - Returns: A promise the completes when the download is complete. -private func download(_ appID: UInt64, purchase: Bool) -> Promise { - Promise { seal in - SSPurchase().perform(adamId: appID, purchase: purchase) { purchase, _, error, response in - if let error { - seal.reject(MASError.purchaseFailed(error: error as NSError?)) - return + SSPurchase().perform(adamId: appID, purchase: purchase) + .recover { error -> Promise in + guard attempts > 1 else { + throw error } - guard response?.downloads.isEmpty == false, let purchase else { - seal.reject(MASError.noDownloads) - return + // If the download failed due to network issues, try again. Otherwise, fail immediately. + guard case MASError.downloadFailed(let downloadError) = error, + case NSURLErrorDomain = downloadError?.domain + else { + throw error } - seal.fulfill(purchase) + let attempts = attempts - 1 + printWarning((downloadError ?? error).localizedDescription) + printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").") + return downloadWithRetries(appID, purchase: purchase, attempts: attempts) } - }.then { purchase -> Promise in - let observer = PurchaseDownloadObserver(purchase: purchase) - let download = Promise { seal in - observer.errorHandler = seal.reject - observer.completionHandler = seal.fulfill_ - } - - let downloadQueue = CKDownloadQueue.shared() - let observerID = downloadQueue.add(observer) - return download.ensure { - downloadQueue.remove(observerID) - } - } } diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift index 09c5bc8..c29a16f 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/MasKit/AppStore/SSPurchase.swift @@ -7,13 +7,11 @@ // import CommerceKit +import PromiseKit import StoreFoundation -typealias SSPurchaseCompletion = - (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void - extension SSPurchase { - func perform(adamId: UInt64, purchase: Bool, _ completion: @escaping SSPurchaseCompletion) { + func perform(adamId: UInt64, purchase: Bool) -> Promise { var parameters: [String: Any] = [ "productType": "C", "price": 0, @@ -27,7 +25,6 @@ extension SSPurchase { parameters["pricingParameters"] = "STDQ" } else { - // is redownload, use existing functionality parameters["pricingParameters"] = "STDRDL" } @@ -39,17 +36,7 @@ extension SSPurchase { itemIdentifier = adamId - if #unavailable(macOS 12) { - // Monterey obscures the user's App Store account information, but allows - // redownloads without passing the account to SSPurchase. - // https://github.com/mas-cli/mas/issues/417 - if let storeAccount = try? ISStoreAccount.primaryAccount.wait() { - accountIdentifier = storeAccount.dsID - appleID = storeAccount.identifier - } - } - - // Not sure if this is needed, but lets use it here. + // Not sure if this is needed… if purchase { isRedownload = false } @@ -58,6 +45,50 @@ extension SSPurchase { downloadMetadata.kind = "software" downloadMetadata.itemIdentifier = adamId - CKPurchaseController.shared().perform(self, withOptions: 0, completionHandler: completion) + // Monterey obscures the user's App Store account, but allows + // redownloads without passing any account IDs to SSPurchase. + // https://github.com/mas-cli/mas/issues/417 + if #available(macOS 12, *) { + return perform() + } + + return + ISStoreAccount.primaryAccount + .then { storeAccount in + self.accountIdentifier = storeAccount.dsID + self.appleID = storeAccount.identifier + return self.perform() + } + } + + private func perform() -> Promise { + Promise { seal in + CKPurchaseController.shared().perform(self, withOptions: 0) { purchase, _, error, response in + if let error { + seal.reject(MASError.purchaseFailed(error: error as NSError?)) + return + } + + guard response?.downloads.isEmpty == false, let purchase else { + seal.reject(MASError.noDownloads) + return + } + + seal.fulfill(purchase) + } + } + .then { purchase in + let observer = PurchaseDownloadObserver(purchase: purchase) + let downloadQueue = CKDownloadQueue.shared() + let observerID = downloadQueue.add(observer) + + return Promise { seal in + observer.errorHandler = seal.reject + observer.completionHandler = seal.fulfill_ + } + .ensure { + downloadQueue.remove(observerID) + } + } } } From d413d8cfa1568259a84509cfc56c49ea60eafd70 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:05:41 -0400 Subject: [PATCH 19/81] Move MasKit module to mas. Move MasKitTests module to masTests. Rename MasKit enum as Mas. Upgrade swift-tools-version from 5.3 to 5.6.1. swift-tools-version 5.5+ is necessary to allow test code to import executable target code, to allow MasKit library code to be moved into the mas executable. Upgrade to swift-tools-version to 5.6.1 instead of to 5.5 because they support all the same macOS versions. Standardize comments. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .gitignore | 1 + Package.resolved | 150 +++++++++--------- Package.swift | 24 +-- README.md | 2 +- .../AppStore/CKSoftwareMap+SoftwareMap.swift | 2 +- .../CKSoftwareProduct+SoftwareProduct.swift | 2 +- .../{MasKit => mas}/AppStore/Downloader.swift | 2 +- .../AppStore/ISStoreAccount.swift | 2 +- .../AppStore/PurchaseDownloadObserver.swift | 2 +- .../{MasKit => mas}/AppStore/SSPurchase.swift | 2 +- .../AppStore/StoreAccount.swift | 2 +- .../{MasKit => mas}/Commands/Account.swift | 2 +- Sources/{MasKit => mas}/Commands/Home.swift | 2 +- Sources/{MasKit => mas}/Commands/Info.swift | 2 +- .../{MasKit => mas}/Commands/Install.swift | 2 +- Sources/{MasKit => mas}/Commands/List.swift | 2 +- Sources/{MasKit => mas}/Commands/Lucky.swift | 2 +- Sources/{MasKit => mas}/Commands/Open.swift | 2 +- .../{MasKit => mas}/Commands/Outdated.swift | 2 +- .../{MasKit => mas}/Commands/Purchase.swift | 2 +- Sources/{MasKit => mas}/Commands/Reset.swift | 2 +- Sources/{MasKit => mas}/Commands/Search.swift | 2 +- Sources/{MasKit => mas}/Commands/SignIn.swift | 2 +- .../{MasKit => mas}/Commands/SignOut.swift | 2 +- .../{MasKit => mas}/Commands/Uninstall.swift | 2 +- .../{MasKit => mas}/Commands/Upgrade.swift | 2 +- Sources/{MasKit => mas}/Commands/Vendor.swift | 2 +- .../{MasKit => mas}/Commands/Version.swift | 2 +- .../Controllers/AppLibrary.swift | 2 +- .../Controllers/MasAppLibrary.swift | 2 +- .../Controllers/MasStoreSearch.swift | 2 +- .../Controllers/SoftwareMap.swift | 2 +- .../Controllers/StoreSearch.swift | 2 +- Sources/{MasKit => mas}/Errors/MASError.swift | 2 +- .../ExternalCommands/ExternalCommand.swift | 2 +- .../ExternalCommands/OpenSystemCommand.swift | 2 +- .../SysCtlSystemCommand.swift | 2 +- .../Formatters/AppInfoFormatter.swift | 2 +- .../Formatters/AppListFormatter.swift | 2 +- .../Formatters/SearchResultFormatter.swift | 2 +- .../Formatters/Utilities.swift | 4 +- .../{MasKit/MasKit.swift => mas/Mas.swift} | 6 +- .../{MasKit => mas}/Models/SearchResult.swift | 2 +- .../Models/SearchResultList.swift | 2 +- .../Models/SoftwareProduct.swift | 4 +- .../Network/NetworkManager.swift | 2 +- .../Network/NetworkSession.swift | 2 +- .../Network/URLSession+NetworkSession.swift | 2 +- Sources/mas/main.swift | 5 +- .../{MasKitTests => masTests}/.swiftlint.yml | 2 +- .../Commands/AccountCommandSpec.swift | 6 +- .../Commands/HomeCommandSpec.swift | 6 +- .../Commands/InfoCommandSpec.swift | 6 +- .../Commands/InstallCommandSpec.swift | 6 +- .../Commands/ListCommandSpec.swift | 6 +- .../Commands/LuckyCommandSpec.swift | 6 +- .../Commands/OpenCommandSpec.swift | 6 +- .../Commands/OutdatedCommandSpec.swift | 6 +- .../Commands/PurchaseCommandSpec.swift | 6 +- .../Commands/ResetCommandSpec.swift | 6 +- .../Commands/SearchCommandSpec.swift | 6 +- .../Commands/SignInCommandSpec.swift | 6 +- .../Commands/SignOutCommandSpec.swift | 6 +- .../Commands/UninstallCommandSpec.swift | 6 +- .../Commands/UpgradeCommandSpec.swift | 6 +- .../Commands/VendorCommandSpec.swift | 6 +- .../Commands/VersionCommandSpec.swift | 6 +- .../Controllers/AppLibraryMock.swift | 4 +- .../Controllers/MasAppLibrarySpec.swift | 6 +- .../Controllers/MasStoreSearchSpec.swift | 6 +- .../Controllers/StoreSearchMock.swift | 4 +- .../Errors/MASErrorTestCase.swift | 6 +- .../Extensions/Bundle+JSON.swift | 4 +- .../Extensions/String+FileExtension.swift | 2 +- .../OpenSystemCommandMock.swift | 4 +- .../OpenSystemCommandSpec.swift | 6 +- .../Formatters/AppListFormatterSpec.swift | 6 +- .../SearchResultFormatterSpec.swift | 6 +- .../JSON/lookup/fantastical.json | 0 .../JSON/lookup/notability.json | 0 .../JSON/lookup/slack.json | 0 .../JSON/lookup/things.json | 0 .../JSON/search/bbedit.json | 0 .../JSON/search/bear.json | 0 .../JSON/search/deliveries.json | 0 .../JSON/search/fantastical.json | 0 .../JSON/search/mojave.json | 0 .../JSON/search/nonexistent.json | 0 .../JSON/search/notability.json | 0 .../JSON/search/slack.json | 0 .../JSON/search/things-3.json | 0 .../JSON/search/things-that-go-bump.json | 0 .../JSON/search/things.json | 0 .../JSON/search/tweetbot.json | 0 .../Models/SearchResultListSpec.swift | 6 +- .../Models/SearchResultSpec.swift | 6 +- .../Models/SoftwareProductMock.swift | 4 +- .../Models/SoftwareProductSpec.swift | 6 +- .../Network/NetworkManagerTests.swift | 6 +- .../Network/NetworkSessionMock.swift | 4 +- .../Network/NetworkSessionMockFromFile.swift | 2 +- .../Nimble/ResultPredicates.swift | 4 +- .../OutputListener.swift | 4 +- .../OutputListenerSpec.swift | 6 +- .../{MasKitTests => masTests}/Strongify.swift | 2 +- script/version | 2 +- 106 files changed, 236 insertions(+), 252 deletions(-) rename Sources/{MasKit => mas}/AppStore/CKSoftwareMap+SoftwareMap.swift (97%) rename Sources/{MasKit => mas}/AppStore/CKSoftwareProduct+SoftwareProduct.swift (95%) rename Sources/{MasKit => mas}/AppStore/Downloader.swift (99%) rename Sources/{MasKit => mas}/AppStore/ISStoreAccount.swift (99%) rename Sources/{MasKit => mas}/AppStore/PurchaseDownloadObserver.swift (99%) rename Sources/{MasKit => mas}/AppStore/SSPurchase.swift (99%) rename Sources/{MasKit => mas}/AppStore/StoreAccount.swift (96%) rename Sources/{MasKit => mas}/Commands/Account.swift (98%) rename Sources/{MasKit => mas}/Commands/Home.swift (99%) rename Sources/{MasKit => mas}/Commands/Info.swift (99%) rename Sources/{MasKit => mas}/Commands/Install.swift (99%) rename Sources/{MasKit => mas}/Commands/List.swift (98%) rename Sources/{MasKit => mas}/Commands/Lucky.swift (99%) rename Sources/{MasKit => mas}/Commands/Open.swift (99%) rename Sources/{MasKit => mas}/Commands/Outdated.swift (99%) rename Sources/{MasKit => mas}/Commands/Purchase.swift (99%) rename Sources/{MasKit => mas}/Commands/Reset.swift (99%) rename Sources/{MasKit => mas}/Commands/Search.swift (99%) rename Sources/{MasKit => mas}/Commands/SignIn.swift (99%) rename Sources/{MasKit => mas}/Commands/SignOut.swift (98%) rename Sources/{MasKit => mas}/Commands/Uninstall.swift (99%) rename Sources/{MasKit => mas}/Commands/Upgrade.swift (99%) rename Sources/{MasKit => mas}/Commands/Vendor.swift (99%) rename Sources/{MasKit => mas}/Commands/Version.swift (97%) rename Sources/{MasKit => mas}/Controllers/AppLibrary.swift (99%) rename Sources/{MasKit => mas}/Controllers/MasAppLibrary.swift (99%) rename Sources/{MasKit => mas}/Controllers/MasStoreSearch.swift (99%) rename Sources/{MasKit => mas}/Controllers/SoftwareMap.swift (96%) rename Sources/{MasKit => mas}/Controllers/StoreSearch.swift (99%) rename Sources/{MasKit => mas}/Errors/MASError.swift (99%) rename Sources/{MasKit => mas}/ExternalCommands/ExternalCommand.swift (99%) rename Sources/{MasKit => mas}/ExternalCommands/OpenSystemCommand.swift (97%) rename Sources/{MasKit => mas}/ExternalCommands/SysCtlSystemCommand.swift (98%) rename Sources/{MasKit => mas}/Formatters/AppInfoFormatter.swift (99%) rename Sources/{MasKit => mas}/Formatters/AppListFormatter.swift (99%) rename Sources/{MasKit => mas}/Formatters/SearchResultFormatter.swift (99%) rename Sources/{MasKit => mas}/Formatters/Utilities.swift (98%) rename Sources/{MasKit/MasKit.swift => mas/Mas.swift} (93%) rename Sources/{MasKit => mas}/Models/SearchResult.swift (99%) rename Sources/{MasKit => mas}/Models/SearchResultList.swift (95%) rename Sources/{MasKit => mas}/Models/SoftwareProduct.swift (98%) rename Sources/{MasKit => mas}/Network/NetworkManager.swift (98%) rename Sources/{MasKit => mas}/Network/NetworkSession.swift (95%) rename Sources/{MasKit => mas}/Network/URLSession+NetworkSession.swift (98%) rename Tests/{MasKitTests => masTests}/.swiftlint.yml (91%) rename Tests/{MasKitTests => masTests}/Commands/AccountCommandSpec.swift (90%) rename Tests/{MasKitTests => masTests}/Commands/HomeCommandSpec.swift (96%) rename Tests/{MasKitTests => masTests}/Commands/InfoCommandSpec.swift (96%) rename Tests/{MasKitTests => masTests}/Commands/InstallCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Commands/ListCommandSpec.swift (87%) rename Tests/{MasKitTests => masTests}/Commands/LuckyCommandSpec.swift (91%) rename Tests/{MasKitTests => masTests}/Commands/OpenCommandSpec.swift (96%) rename Tests/{MasKitTests => masTests}/Commands/OutdatedCommandSpec.swift (89%) rename Tests/{MasKitTests => masTests}/Commands/PurchaseCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Commands/ResetCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Commands/SearchCommandSpec.swift (95%) rename Tests/{MasKitTests => masTests}/Commands/SignInCommandSpec.swift (90%) rename Tests/{MasKitTests => masTests}/Commands/SignOutCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Commands/UninstallCommandSpec.swift (97%) rename Tests/{MasKitTests => masTests}/Commands/UpgradeCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Commands/VendorCommandSpec.swift (96%) rename Tests/{MasKitTests => masTests}/Commands/VersionCommandSpec.swift (88%) rename Tests/{MasKitTests => masTests}/Controllers/AppLibraryMock.swift (95%) rename Tests/{MasKitTests => masTests}/Controllers/MasAppLibrarySpec.swift (95%) rename Tests/{MasKitTests => masTests}/Controllers/MasStoreSearchSpec.swift (97%) rename Tests/{MasKitTests => masTests}/Controllers/StoreSearchMock.swift (95%) rename Tests/{MasKitTests => masTests}/Errors/MASErrorTestCase.swift (98%) rename Tests/{MasKitTests => masTests}/Extensions/Bundle+JSON.swift (95%) rename Tests/{MasKitTests => masTests}/Extensions/String+FileExtension.swift (96%) rename Tests/{MasKitTests => masTests}/ExternalCommands/OpenSystemCommandMock.swift (92%) rename Tests/{MasKitTests => masTests}/ExternalCommands/OpenSystemCommandSpec.swift (91%) rename Tests/{MasKitTests => masTests}/Formatters/AppListFormatterSpec.swift (96%) rename Tests/{MasKitTests => masTests}/Formatters/SearchResultFormatterSpec.swift (97%) rename Tests/{MasKitTests => masTests}/JSON/lookup/fantastical.json (100%) rename Tests/{MasKitTests => masTests}/JSON/lookup/notability.json (100%) rename Tests/{MasKitTests => masTests}/JSON/lookup/slack.json (100%) rename Tests/{MasKitTests => masTests}/JSON/lookup/things.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/bbedit.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/bear.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/deliveries.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/fantastical.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/mojave.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/nonexistent.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/notability.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/slack.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/things-3.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/things-that-go-bump.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/things.json (100%) rename Tests/{MasKitTests => masTests}/JSON/search/tweetbot.json (100%) rename Tests/{MasKitTests => masTests}/Models/SearchResultListSpec.swift (92%) rename Tests/{MasKitTests => masTests}/Models/SearchResultSpec.swift (90%) rename Tests/{MasKitTests => masTests}/Models/SoftwareProductMock.swift (89%) rename Tests/{MasKitTests => masTests}/Models/SoftwareProductSpec.swift (95%) rename Tests/{MasKitTests => masTests}/Network/NetworkManagerTests.swift (97%) rename Tests/{MasKitTests => masTests}/Network/NetworkSessionMock.swift (95%) rename Tests/{MasKitTests => masTests}/Network/NetworkSessionMockFromFile.swift (98%) rename Tests/{MasKitTests => masTests}/Nimble/ResultPredicates.swift (96%) rename Tests/{MasKitTests => masTests}/OutputListener.swift (93%) rename Tests/{MasKitTests => masTests}/OutputListenerSpec.swift (92%) rename Tests/{MasKitTests => masTests}/Strongify.swift (95%) diff --git a/.gitignore b/.gitignore index 1f92665..70f7959 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ build/ default.profraw releases/ xcuserdata +Sources/mas/Package.swift Sources/MasKit/Package.swift diff --git a/Package.resolved b/Package.resolved index 6952cad..a4d32c7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,79 +1,77 @@ { - "object": { - "pins": [ - { - "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", - "state": { - "branch": null, - "revision": "a1671cf728db837cf5ec1980a80d276bbba748f6", - "version": "0.18.0" - } - }, - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", - "version": "2.1.1" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", - "version": "2.1.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc", - "version": "10.0.0" - } - }, - { - "package": "PromiseKit", - "repositoryURL": "https://github.com/mxcl/PromiseKit.git", - "state": { - "branch": null, - "revision": "43772616c46a44a9977e41924ae01d0e55f2f9ca", - "version": "6.18.1" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "f9d519828bb03dfc8125467d8f7b93131951124c", - "version": "5.0.1" - } - }, - { - "package": "Regex", - "repositoryURL": "https://github.com/sharplet/Regex.git", - "state": { - "branch": null, - "revision": "76c2b73d4281d77fc3118391877efd1bf972f515", - "version": "2.1.1" - } - }, - { - "package": "Version", - "repositoryURL": "https://github.com/mxcl/Version.git", - "state": { - "branch": null, - "revision": "1fe824b80d89201652e7eca7c9252269a1d85e25", - "version": "2.0.1" - } + "pins" : [ + { + "identity" : "commandant", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Carthage/Commandant.git", + "state" : { + "revision" : "a1671cf728db837cf5ec1980a80d276bbba748f6", + "version" : "0.18.0" } - ] - }, - "version": 1 + }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", + "version" : "2.1.1" + } + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", + "version" : "2.1.0" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc", + "version" : "10.0.0" + } + }, + { + "identity" : "promisekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/PromiseKit.git", + "state" : { + "revision" : "43772616c46a44a9977e41924ae01d0e55f2f9ca", + "version" : "6.18.1" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick.git", + "state" : { + "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", + "version" : "5.0.1" + } + }, + { + "identity" : "regex", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sharplet/Regex.git", + "state" : { + "revision" : "76c2b73d4281d77fc3118391877efd1bf972f515", + "version" : "2.1.1" + } + }, + { + "identity" : "version", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/Version.git", + "state" : { + "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", + "version" : "2.0.1" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 2df3ba1..402c576 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -13,11 +13,7 @@ let package = Package( .executable( name: "mas", targets: ["mas"] - ), - .library( - name: "MasKit", - targets: ["MasKit"] - ), + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -31,18 +27,8 @@ let package = Package( targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( + .executableTarget( name: "mas", - dependencies: ["MasKit"], - swiftSettings: [ - .unsafeFlags([ - "-I", "Sources/PrivateFrameworks/CommerceKit", - "-I", "Sources/PrivateFrameworks/StoreFoundation", - ]) - ] - ), - .target( - name: "MasKit", dependencies: [ "Commandant", "PromiseKit", @@ -62,8 +48,8 @@ let package = Package( ] ), .testTarget( - name: "MasKitTests", - dependencies: ["MasKit", "Nimble", "Quick"], + name: "masTests", + dependencies: ["mas", "Nimble", "Quick"], resources: [.copy("JSON")], swiftSettings: [ .unsafeFlags([ diff --git a/README.md b/README.md index 6bf0ebe..8b76753 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ Build output can be found in the `.build/` directory within the project. The tests in this project are a recent work-in-progress. Since Xcode does not officially support tests for command-line tool targets, -all logic is part of the MasKit target with tests in MasKitTests. +all logic is part of the mas target with tests in masTests. Tests are written using [Quick]. ```bash diff --git a/Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift b/Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift similarity index 97% rename from Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift rename to Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift index 5993e4d..dde4535 100644 --- a/Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift +++ b/Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift @@ -1,6 +1,6 @@ // // CKSoftwareMap+SoftwareMap.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift b/Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift similarity index 95% rename from Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift rename to Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift index 8bcafcf..9516bac 100644 --- a/Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift +++ b/Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift @@ -1,6 +1,6 @@ // // CKSoftwareProduct+SoftwareProduct.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift similarity index 99% rename from Sources/MasKit/AppStore/Downloader.swift rename to Sources/mas/AppStore/Downloader.swift index 2208869..b832335 100644 --- a/Sources/MasKit/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -1,6 +1,6 @@ // // Downloader.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift similarity index 99% rename from Sources/MasKit/AppStore/ISStoreAccount.swift rename to Sources/mas/AppStore/ISStoreAccount.swift index ae62f10..7dcfd30 100644 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -1,6 +1,6 @@ // // ISStoreAccount.swift -// mas-cli +// mas // // Created by Andrew Naylor on 22/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift similarity index 99% rename from Sources/MasKit/AppStore/PurchaseDownloadObserver.swift rename to Sources/mas/AppStore/PurchaseDownloadObserver.swift index 355a91a..343b49c 100644 --- a/Sources/MasKit/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -1,6 +1,6 @@ // // PurchaseDownloadObserver.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift similarity index 99% rename from Sources/MasKit/AppStore/SSPurchase.swift rename to Sources/mas/AppStore/SSPurchase.swift index c29a16f..3fd5703 100644 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -1,6 +1,6 @@ // // SSPurchase.swift -// mas-cli +// mas // // Created by Andrew Naylor on 25/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/AppStore/StoreAccount.swift b/Sources/mas/AppStore/StoreAccount.swift similarity index 96% rename from Sources/MasKit/AppStore/StoreAccount.swift rename to Sources/mas/AppStore/StoreAccount.swift index 366cd6e..f434166 100644 --- a/Sources/MasKit/AppStore/StoreAccount.swift +++ b/Sources/mas/AppStore/StoreAccount.swift @@ -1,6 +1,6 @@ // // StoreAccount.swift -// mas-cli +// mas // // Created by Ben Chatelain on 4/3/18. // Copyright © 2018 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Account.swift b/Sources/mas/Commands/Account.swift similarity index 98% rename from Sources/MasKit/Commands/Account.swift rename to Sources/mas/Commands/Account.swift index 333e2d2..6e58cb2 100644 --- a/Sources/MasKit/Commands/Account.swift +++ b/Sources/mas/Commands/Account.swift @@ -1,6 +1,6 @@ // // Account.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Home.swift b/Sources/mas/Commands/Home.swift similarity index 99% rename from Sources/MasKit/Commands/Home.swift rename to Sources/mas/Commands/Home.swift index f3f6bbb..896ae30 100644 --- a/Sources/MasKit/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -1,6 +1,6 @@ // // Home.swift -// mas-cli +// mas // // Created by Ben Chatelain on 2018-12-29. // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Commands/Info.swift b/Sources/mas/Commands/Info.swift similarity index 99% rename from Sources/MasKit/Commands/Info.swift rename to Sources/mas/Commands/Info.swift index 82974dc..5530c4d 100644 --- a/Sources/MasKit/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -1,6 +1,6 @@ // // Info.swift -// mas-cli +// mas // // Created by Denis Lebedev on 21/10/2016. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Install.swift b/Sources/mas/Commands/Install.swift similarity index 99% rename from Sources/MasKit/Commands/Install.swift rename to Sources/mas/Commands/Install.swift index f941cbf..12b5378 100644 --- a/Sources/MasKit/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -1,6 +1,6 @@ // // Install.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/List.swift b/Sources/mas/Commands/List.swift similarity index 98% rename from Sources/MasKit/Commands/List.swift rename to Sources/mas/Commands/List.swift index 23752e7..e280a4b 100644 --- a/Sources/MasKit/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -1,6 +1,6 @@ // // List.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift similarity index 99% rename from Sources/MasKit/Commands/Lucky.swift rename to Sources/mas/Commands/Lucky.swift index 9452eba..f1c7805 100644 --- a/Sources/MasKit/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -1,6 +1,6 @@ // // Lucky.swift -// mas-cli +// mas // // Created by Pablo Varela on 05/11/17. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Open.swift b/Sources/mas/Commands/Open.swift similarity index 99% rename from Sources/MasKit/Commands/Open.swift rename to Sources/mas/Commands/Open.swift index 85e04db..1a0817e 100644 --- a/Sources/MasKit/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -1,6 +1,6 @@ // // Open.swift -// mas-cli +// mas // // Created by Ben Chatelain on 2018-12-29. // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift similarity index 99% rename from Sources/MasKit/Commands/Outdated.swift rename to Sources/mas/Commands/Outdated.swift index ed3545f..893b4b7 100644 --- a/Sources/MasKit/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -1,6 +1,6 @@ // // Outdated.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift similarity index 99% rename from Sources/MasKit/Commands/Purchase.swift rename to Sources/mas/Commands/Purchase.swift index 9138027..77df061 100644 --- a/Sources/MasKit/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -1,6 +1,6 @@ // // Purchase.swift -// mas-cli +// mas // // Created by Jakob Rieck on 24/10/2017. // Copyright (c) 2017 Jakob Rieck. All rights reserved. diff --git a/Sources/MasKit/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift similarity index 99% rename from Sources/MasKit/Commands/Reset.swift rename to Sources/mas/Commands/Reset.swift index 37d6c2d..cefcaf2 100644 --- a/Sources/MasKit/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -1,6 +1,6 @@ // // Reset.swift -// mas-cli +// mas // // Created by Andrew Naylor on 14/09/2016. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Search.swift b/Sources/mas/Commands/Search.swift similarity index 99% rename from Sources/MasKit/Commands/Search.swift rename to Sources/mas/Commands/Search.swift index cc0f2c7..3d2af78 100644 --- a/Sources/MasKit/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -1,6 +1,6 @@ // // Search.swift -// mas-cli +// mas // // Created by Michael Schneider on 4/14/16. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift similarity index 99% rename from Sources/MasKit/Commands/SignIn.swift rename to Sources/mas/Commands/SignIn.swift index e04769e..8ed6d1c 100644 --- a/Sources/MasKit/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -1,6 +1,6 @@ // // SignIn.swift -// mas-cli +// mas // // Created by Andrew Naylor on 14/02/2016. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift similarity index 98% rename from Sources/MasKit/Commands/SignOut.swift rename to Sources/mas/Commands/SignOut.swift index b222b8c..65e213a 100644 --- a/Sources/MasKit/Commands/SignOut.swift +++ b/Sources/mas/Commands/SignOut.swift @@ -1,6 +1,6 @@ // // SignOut.swift -// mas-cli +// mas // // Created by Andrew Naylor on 14/02/2016. // Copyright © 2016 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift similarity index 99% rename from Sources/MasKit/Commands/Uninstall.swift rename to Sources/mas/Commands/Uninstall.swift index 97d3767..0f3570e 100644 --- a/Sources/MasKit/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -1,6 +1,6 @@ // // Uninstall.swift -// mas-cli +// mas // // Created by Ben Chatelain on 2018-12-27. // Copyright © 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift similarity index 99% rename from Sources/MasKit/Commands/Upgrade.swift rename to Sources/mas/Commands/Upgrade.swift index 1acf0f0..a6ba781 100644 --- a/Sources/MasKit/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -1,6 +1,6 @@ // // Upgrade.swift -// mas-cli +// mas // // Created by Andrew Naylor on 30/12/2015. // Copyright © 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift similarity index 99% rename from Sources/MasKit/Commands/Vendor.swift rename to Sources/mas/Commands/Vendor.swift index 0fe8401..f0f45f5 100644 --- a/Sources/MasKit/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -1,6 +1,6 @@ // // Vendor.swift -// mas-cli +// mas // // Created by Ben Chatelain on 2018-12-29. // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Commands/Version.swift b/Sources/mas/Commands/Version.swift similarity index 97% rename from Sources/MasKit/Commands/Version.swift rename to Sources/mas/Commands/Version.swift index d166170..1d2d580 100644 --- a/Sources/MasKit/Commands/Version.swift +++ b/Sources/mas/Commands/Version.swift @@ -1,6 +1,6 @@ // // Version.swift -// mas-cli +// mas // // Created by Andrew Naylor on 20/09/2015. // Copyright © 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift similarity index 99% rename from Sources/MasKit/Controllers/AppLibrary.swift rename to Sources/mas/Controllers/AppLibrary.swift index 2edbc89..d481a52 100644 --- a/Sources/MasKit/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -1,6 +1,6 @@ // // AppLibrary.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift similarity index 99% rename from Sources/MasKit/Controllers/MasAppLibrary.swift rename to Sources/mas/Controllers/MasAppLibrary.swift index 7cc86c5..d842194 100644 --- a/Sources/MasKit/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -1,6 +1,6 @@ // // MasAppLibrary.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift similarity index 99% rename from Sources/MasKit/Controllers/MasStoreSearch.swift rename to Sources/mas/Controllers/MasStoreSearch.swift index 0db3268..dda28f8 100644 --- a/Sources/MasKit/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -1,6 +1,6 @@ // // MasStoreSearch.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Controllers/SoftwareMap.swift b/Sources/mas/Controllers/SoftwareMap.swift similarity index 96% rename from Sources/MasKit/Controllers/SoftwareMap.swift rename to Sources/mas/Controllers/SoftwareMap.swift index 0294abe..46abea8 100644 --- a/Sources/MasKit/Controllers/SoftwareMap.swift +++ b/Sources/mas/Controllers/SoftwareMap.swift @@ -1,6 +1,6 @@ // // SoftwareMap.swift -// MasKit +// mas // // Created by Ben Chatelain on 3/1/20. // Copyright © 2020 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift similarity index 99% rename from Sources/MasKit/Controllers/StoreSearch.swift rename to Sources/mas/Controllers/StoreSearch.swift index f4866bb..c326699 100644 --- a/Sources/MasKit/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -1,6 +1,6 @@ // // StoreSearch.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift similarity index 99% rename from Sources/MasKit/Errors/MASError.swift rename to Sources/mas/Errors/MASError.swift index 7fb0fb2..9008c78 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -1,6 +1,6 @@ // // MASError.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. diff --git a/Sources/MasKit/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift similarity index 99% rename from Sources/MasKit/ExternalCommands/ExternalCommand.swift rename to Sources/mas/ExternalCommands/ExternalCommand.swift index 785bc84..40946be 100644 --- a/Sources/MasKit/ExternalCommands/ExternalCommand.swift +++ b/Sources/mas/ExternalCommands/ExternalCommand.swift @@ -1,6 +1,6 @@ // // ExternalCommand.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/1/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/ExternalCommands/OpenSystemCommand.swift b/Sources/mas/ExternalCommands/OpenSystemCommand.swift similarity index 97% rename from Sources/MasKit/ExternalCommands/OpenSystemCommand.swift rename to Sources/mas/ExternalCommands/OpenSystemCommand.swift index eddb2d6..ebce90d 100644 --- a/Sources/MasKit/ExternalCommands/OpenSystemCommand.swift +++ b/Sources/mas/ExternalCommands/OpenSystemCommand.swift @@ -1,6 +1,6 @@ // // OpenSystemCommand.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/2/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift similarity index 98% rename from Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift rename to Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index 3c2a401..41d3dcb 100644 --- a/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -1,6 +1,6 @@ // // SysCtlSystemCommand.swift -// MasKit +// mas // // Created by Chris Araman on 6/3/21. // Copyright © 2021 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift similarity index 99% rename from Sources/MasKit/Formatters/AppInfoFormatter.swift rename to Sources/mas/Formatters/AppInfoFormatter.swift index 6810cfd..3d5f114 100644 --- a/Sources/MasKit/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -1,6 +1,6 @@ // // AppInfoFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/7/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Formatters/AppListFormatter.swift b/Sources/mas/Formatters/AppListFormatter.swift similarity index 99% rename from Sources/MasKit/Formatters/AppListFormatter.swift rename to Sources/mas/Formatters/AppListFormatter.swift index 21c915c..5edd055 100644 --- a/Sources/MasKit/Formatters/AppListFormatter.swift +++ b/Sources/mas/Formatters/AppListFormatter.swift @@ -1,6 +1,6 @@ // // AppListFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 6/7/20. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift similarity index 99% rename from Sources/MasKit/Formatters/SearchResultFormatter.swift rename to Sources/mas/Formatters/SearchResultFormatter.swift index cd0061e..15fa4aa 100644 --- a/Sources/MasKit/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -1,6 +1,6 @@ // // SearchResultFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/11/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift similarity index 98% rename from Sources/MasKit/Formatters/Utilities.swift rename to Sources/mas/Formatters/Utilities.swift index 33f3b67..7092d57 100644 --- a/Sources/MasKit/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -1,6 +1,6 @@ // // Utilities.swift -// mas-cli +// mas // // Created by Andrew Naylor on 14/09/2016. // Copyright © 2016 Andrew Naylor. All rights reserved. @@ -18,7 +18,7 @@ let csi = "\u{001B}[" var printObserver: ((String) -> Void)? // Override global print for testability. - // See MasKitTests/OutputListener.swift. + // See masTests/OutputListener.swift. func print( _ items: Any..., separator: String = " ", diff --git a/Sources/MasKit/MasKit.swift b/Sources/mas/Mas.swift similarity index 93% rename from Sources/MasKit/MasKit.swift rename to Sources/mas/Mas.swift index 20708db..5000efe 100644 --- a/Sources/MasKit/MasKit.swift +++ b/Sources/mas/Mas.swift @@ -1,6 +1,6 @@ // -// MasKit.swift -// MasKit +// Mas.swift +// mas // // Created by Chris Araman on 4/22/21. // Copyright © 2021 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import PromiseKit -public enum MasKit { +public enum Mas { public static func initialize() { PromiseKit.conf.Q.map = .global() PromiseKit.conf.Q.return = .global() diff --git a/Sources/MasKit/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift similarity index 99% rename from Sources/MasKit/Models/SearchResult.swift rename to Sources/mas/Models/SearchResult.swift index b2ccd6b..7ac757b 100644 --- a/Sources/MasKit/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -1,6 +1,6 @@ // // SearchResult.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Models/SearchResultList.swift b/Sources/mas/Models/SearchResultList.swift similarity index 95% rename from Sources/MasKit/Models/SearchResultList.swift rename to Sources/mas/Models/SearchResultList.swift index cd29756..68f40d1 100644 --- a/Sources/MasKit/Models/SearchResultList.swift +++ b/Sources/mas/Models/SearchResultList.swift @@ -1,6 +1,6 @@ // // SearchResultList.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Models/SoftwareProduct.swift b/Sources/mas/Models/SoftwareProduct.swift similarity index 98% rename from Sources/MasKit/Models/SoftwareProduct.swift rename to Sources/mas/Models/SoftwareProduct.swift index 6ce7180..aa49d81 100644 --- a/Sources/MasKit/Models/SoftwareProduct.swift +++ b/Sources/mas/Models/SoftwareProduct.swift @@ -1,6 +1,6 @@ // // SoftwareProduct.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Foundation import Version -/// Protocol describing the members of CKSoftwareProduct used throughout MasKit. +/// Protocol describing the members of CKSoftwareProduct used throughout mas. protocol SoftwareProduct { var appName: String { get } var bundleIdentifier: String { get set } diff --git a/Sources/MasKit/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift similarity index 98% rename from Sources/MasKit/Network/NetworkManager.swift rename to Sources/mas/Network/NetworkManager.swift index 7333612..45fa7b8 100644 --- a/Sources/MasKit/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -1,6 +1,6 @@ // // NetworkManager.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Network/NetworkSession.swift b/Sources/mas/Network/NetworkSession.swift similarity index 95% rename from Sources/MasKit/Network/NetworkSession.swift rename to Sources/mas/Network/NetworkSession.swift index 28cd649..af590ce 100644 --- a/Sources/MasKit/Network/NetworkSession.swift +++ b/Sources/mas/Network/NetworkSession.swift @@ -1,6 +1,6 @@ // // NetworkSession.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Network/URLSession+NetworkSession.swift b/Sources/mas/Network/URLSession+NetworkSession.swift similarity index 98% rename from Sources/MasKit/Network/URLSession+NetworkSession.swift rename to Sources/mas/Network/URLSession+NetworkSession.swift index 7c15df0..04db152 100644 --- a/Sources/MasKit/Network/URLSession+NetworkSession.swift +++ b/Sources/mas/Network/URLSession+NetworkSession.swift @@ -1,6 +1,6 @@ // // URLSession+NetworkSession.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/mas/main.swift b/Sources/mas/main.swift index e872960..3c0776e 100644 --- a/Sources/mas/main.swift +++ b/Sources/mas/main.swift @@ -1,15 +1,14 @@ // // main.swift -// mas-cli +// mas // // Created by Andrew Naylor on 11/07/2015. // Copyright © 2015 Andrew Naylor. All rights reserved. // import Commandant -import MasKit -MasKit.initialize() +Mas.initialize() let registry = CommandRegistry() let helpCommand = HelpCommand(registry: registry) diff --git a/Tests/MasKitTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml similarity index 91% rename from Tests/MasKitTests/.swiftlint.yml rename to Tests/masTests/.swiftlint.yml index daa6dd0..d234b91 100644 --- a/Tests/MasKitTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -1,6 +1,6 @@ # # .swiftlint.yml -# MasKitTests +# masTests # # https://github.com/realm/SwiftLint#configuration # diff --git a/Tests/MasKitTests/Commands/AccountCommandSpec.swift b/Tests/masTests/Commands/AccountCommandSpec.swift similarity index 90% rename from Tests/MasKitTests/Commands/AccountCommandSpec.swift rename to Tests/masTests/Commands/AccountCommandSpec.swift index b78f16e..73a187f 100644 --- a/Tests/MasKitTests/Commands/AccountCommandSpec.swift +++ b/Tests/masTests/Commands/AccountCommandSpec.swift @@ -1,6 +1,6 @@ // // AccountCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,13 +9,13 @@ import Nimble import Quick -@testable import MasKit +@testable import mas // Deprecated test public class AccountCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues xdescribe("Account command") { diff --git a/Tests/MasKitTests/Commands/HomeCommandSpec.swift b/Tests/masTests/Commands/HomeCommandSpec.swift similarity index 96% rename from Tests/MasKitTests/Commands/HomeCommandSpec.swift rename to Tests/masTests/Commands/HomeCommandSpec.swift index ece1252..bfbe5c4 100644 --- a/Tests/MasKitTests/Commands/HomeCommandSpec.swift +++ b/Tests/masTests/Commands/HomeCommandSpec.swift @@ -1,6 +1,6 @@ // // HomeCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-29. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class HomeCommandSpec: QuickSpec { override public func spec() { @@ -23,7 +23,7 @@ public class HomeCommandSpec: QuickSpec { let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("home command") { beforeEach { diff --git a/Tests/MasKitTests/Commands/InfoCommandSpec.swift b/Tests/masTests/Commands/InfoCommandSpec.swift similarity index 96% rename from Tests/MasKitTests/Commands/InfoCommandSpec.swift rename to Tests/masTests/Commands/InfoCommandSpec.swift index b2bdc9c..2cd8982 100644 --- a/Tests/MasKitTests/Commands/InfoCommandSpec.swift +++ b/Tests/masTests/Commands/InfoCommandSpec.swift @@ -1,6 +1,6 @@ // // InfoCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class InfoCommandSpec: QuickSpec { override public func spec() { @@ -37,7 +37,7 @@ public class InfoCommandSpec: QuickSpec { """ beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("Info command") { beforeEach { diff --git a/Tests/MasKitTests/Commands/InstallCommandSpec.swift b/Tests/masTests/Commands/InstallCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/InstallCommandSpec.swift rename to Tests/masTests/Commands/InstallCommandSpec.swift index a870649..a3a009e 100644 --- a/Tests/MasKitTests/Commands/InstallCommandSpec.swift +++ b/Tests/masTests/Commands/InstallCommandSpec.swift @@ -1,6 +1,6 @@ // // InstallCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class InstallCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("install command") { it("installs apps") { diff --git a/Tests/MasKitTests/Commands/ListCommandSpec.swift b/Tests/masTests/Commands/ListCommandSpec.swift similarity index 87% rename from Tests/MasKitTests/Commands/ListCommandSpec.swift rename to Tests/masTests/Commands/ListCommandSpec.swift index 2901753..300322e 100644 --- a/Tests/MasKitTests/Commands/ListCommandSpec.swift +++ b/Tests/masTests/Commands/ListCommandSpec.swift @@ -1,6 +1,6 @@ // // ListCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-27. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class ListCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("list command") { it("lists apps") { diff --git a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift b/Tests/masTests/Commands/LuckyCommandSpec.swift similarity index 91% rename from Tests/MasKitTests/Commands/LuckyCommandSpec.swift rename to Tests/masTests/Commands/LuckyCommandSpec.swift index 034d15c..8fbda3c 100644 --- a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift +++ b/Tests/masTests/Commands/LuckyCommandSpec.swift @@ -1,6 +1,6 @@ // // LuckyCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class LuckyCommandSpec: QuickSpec { override public func spec() { @@ -17,7 +17,7 @@ public class LuckyCommandSpec: QuickSpec { let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("lucky command") { xit("installs the first app matching a search") { diff --git a/Tests/MasKitTests/Commands/OpenCommandSpec.swift b/Tests/masTests/Commands/OpenCommandSpec.swift similarity index 96% rename from Tests/MasKitTests/Commands/OpenCommandSpec.swift rename to Tests/masTests/Commands/OpenCommandSpec.swift index c01dcd4..7a8562e 100644 --- a/Tests/MasKitTests/Commands/OpenCommandSpec.swift +++ b/Tests/masTests/Commands/OpenCommandSpec.swift @@ -1,6 +1,6 @@ // // OpenCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2019-01-03. // Copyright © 2019 mas-cli. All rights reserved. @@ -10,7 +10,7 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class OpenCommandSpec: QuickSpec { override public func spec() { @@ -24,7 +24,7 @@ public class OpenCommandSpec: QuickSpec { let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("open command") { beforeEach { diff --git a/Tests/MasKitTests/Commands/OutdatedCommandSpec.swift b/Tests/masTests/Commands/OutdatedCommandSpec.swift similarity index 89% rename from Tests/MasKitTests/Commands/OutdatedCommandSpec.swift rename to Tests/masTests/Commands/OutdatedCommandSpec.swift index 6b7c72d..22a6947 100644 --- a/Tests/MasKitTests/Commands/OutdatedCommandSpec.swift +++ b/Tests/masTests/Commands/OutdatedCommandSpec.swift @@ -1,6 +1,6 @@ // // OutdatedCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class OutdatedCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("outdated command") { it("displays apps with pending updates") { diff --git a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift b/Tests/masTests/Commands/PurchaseCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/PurchaseCommandSpec.swift rename to Tests/masTests/Commands/PurchaseCommandSpec.swift index 8ceaa4a..4db873f 100644 --- a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift +++ b/Tests/masTests/Commands/PurchaseCommandSpec.swift @@ -1,6 +1,6 @@ // // PurchaseCommandSpec.swift -// MasKitTests +// masTests // // Created by Maximilian Blochberger on 2020-03-21. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class PurchaseCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("purchase command") { it("purchases apps") { diff --git a/Tests/MasKitTests/Commands/ResetCommandSpec.swift b/Tests/masTests/Commands/ResetCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/ResetCommandSpec.swift rename to Tests/masTests/Commands/ResetCommandSpec.swift index 6150274..6405025 100644 --- a/Tests/MasKitTests/Commands/ResetCommandSpec.swift +++ b/Tests/masTests/Commands/ResetCommandSpec.swift @@ -1,6 +1,6 @@ // // ResetCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class ResetCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("reset command") { it("resets the App Store state") { diff --git a/Tests/MasKitTests/Commands/SearchCommandSpec.swift b/Tests/masTests/Commands/SearchCommandSpec.swift similarity index 95% rename from Tests/MasKitTests/Commands/SearchCommandSpec.swift rename to Tests/masTests/Commands/SearchCommandSpec.swift index 7e2e98d..f15f812 100644 --- a/Tests/MasKitTests/Commands/SearchCommandSpec.swift +++ b/Tests/masTests/Commands/SearchCommandSpec.swift @@ -1,6 +1,6 @@ // // SearchCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class SearchCommandSpec: QuickSpec { override public func spec() { @@ -22,7 +22,7 @@ public class SearchCommandSpec: QuickSpec { let storeSearch = StoreSearchMock() beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("search command") { beforeEach { diff --git a/Tests/MasKitTests/Commands/SignInCommandSpec.swift b/Tests/masTests/Commands/SignInCommandSpec.swift similarity index 90% rename from Tests/MasKitTests/Commands/SignInCommandSpec.swift rename to Tests/masTests/Commands/SignInCommandSpec.swift index 5f44888..183b2d5 100644 --- a/Tests/MasKitTests/Commands/SignInCommandSpec.swift +++ b/Tests/masTests/Commands/SignInCommandSpec.swift @@ -1,6 +1,6 @@ // // SignInCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,13 +9,13 @@ import Nimble import Quick -@testable import MasKit +@testable import mas // Deprecated test public class SignInCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues xdescribe("signin command") { diff --git a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift b/Tests/masTests/Commands/SignOutCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/SignOutCommandSpec.swift rename to Tests/masTests/Commands/SignOutCommandSpec.swift index 6a6bd61..c746deb 100644 --- a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift +++ b/Tests/masTests/Commands/SignOutCommandSpec.swift @@ -1,6 +1,6 @@ // // SignOutCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class SignOutCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("signout command") { it("signs out") { diff --git a/Tests/MasKitTests/Commands/UninstallCommandSpec.swift b/Tests/masTests/Commands/UninstallCommandSpec.swift similarity index 97% rename from Tests/MasKitTests/Commands/UninstallCommandSpec.swift rename to Tests/masTests/Commands/UninstallCommandSpec.swift index 5aafeac..53a7410 100644 --- a/Tests/MasKitTests/Commands/UninstallCommandSpec.swift +++ b/Tests/masTests/Commands/UninstallCommandSpec.swift @@ -1,6 +1,6 @@ // // UninstallCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-27. // Copyright © 2018 mas-cli. All rights reserved. @@ -10,12 +10,12 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class UninstallCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("uninstall command") { let appId = 12345 diff --git a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift b/Tests/masTests/Commands/UpgradeCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/UpgradeCommandSpec.swift rename to Tests/masTests/Commands/UpgradeCommandSpec.swift index 8a544d3..2b9b173 100644 --- a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift +++ b/Tests/masTests/Commands/UpgradeCommandSpec.swift @@ -1,6 +1,6 @@ // // UpgradeCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class UpgradeCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("upgrade command") { it("upgrades stuff") { diff --git a/Tests/MasKitTests/Commands/VendorCommandSpec.swift b/Tests/masTests/Commands/VendorCommandSpec.swift similarity index 96% rename from Tests/MasKitTests/Commands/VendorCommandSpec.swift rename to Tests/masTests/Commands/VendorCommandSpec.swift index 5e342cb..2c5fe94 100644 --- a/Tests/MasKitTests/Commands/VendorCommandSpec.swift +++ b/Tests/masTests/Commands/VendorCommandSpec.swift @@ -1,6 +1,6 @@ // // VendorCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2019-01-03. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class VendorCommandSpec: QuickSpec { override public func spec() { @@ -23,7 +23,7 @@ public class VendorCommandSpec: QuickSpec { let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("vendor command") { beforeEach { diff --git a/Tests/MasKitTests/Commands/VersionCommandSpec.swift b/Tests/masTests/Commands/VersionCommandSpec.swift similarity index 88% rename from Tests/MasKitTests/Commands/VersionCommandSpec.swift rename to Tests/masTests/Commands/VersionCommandSpec.swift index ba10acc..13ea5d4 100644 --- a/Tests/MasKitTests/Commands/VersionCommandSpec.swift +++ b/Tests/masTests/Commands/VersionCommandSpec.swift @@ -1,6 +1,6 @@ // // VersionCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class VersionCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("version command") { it("displays the current version") { diff --git a/Tests/MasKitTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift similarity index 95% rename from Tests/MasKitTests/Controllers/AppLibraryMock.swift rename to Tests/masTests/Controllers/AppLibraryMock.swift index 2a1f4b1..4a87784 100644 --- a/Tests/MasKitTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -1,12 +1,12 @@ // // AppLibraryMock.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. // -@testable import MasKit +@testable import mas class AppLibraryMock: AppLibrary { var installedApps = [SoftwareProduct]() diff --git a/Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift similarity index 95% rename from Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift rename to Tests/masTests/Controllers/MasAppLibrarySpec.swift index 9409879..9d823a4 100644 --- a/Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -1,6 +1,6 @@ // // MasAppLibrarySpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 3/1/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,14 +9,14 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class MasAppLibrarySpec: QuickSpec { override public func spec() { let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("mas app library") { it("contains all installed apps") { diff --git a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift similarity index 97% rename from Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift rename to Tests/masTests/Controllers/MasStoreSearchSpec.swift index 5fed9e5..99e1bdb 100644 --- a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -1,6 +1,6 @@ // // MasStoreSearchSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/4/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class MasStoreSearchSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("url string") { it("contains the app name") { diff --git a/Tests/MasKitTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift similarity index 95% rename from Tests/MasKitTests/Controllers/StoreSearchMock.swift rename to Tests/masTests/Controllers/StoreSearchMock.swift index 69f518d..5037b92 100644 --- a/Tests/MasKitTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -1,6 +1,6 @@ // // StoreSearchMock.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/4/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import PromiseKit -@testable import MasKit +@testable import mas class StoreSearchMock: StoreSearch { var apps: [Int: SearchResult] = [:] diff --git a/Tests/MasKitTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift similarity index 98% rename from Tests/MasKitTests/Errors/MASErrorTestCase.swift rename to Tests/masTests/Errors/MASErrorTestCase.swift index 503b1ce..fe0248b 100644 --- a/Tests/MasKitTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -1,6 +1,6 @@ // // MASErrorTestCase.swift -// mas-tests +// masTests // // Created by Ben Chatelain on 2/11/18. // Copyright © 2018 Andrew Naylor. All rights reserved. @@ -9,7 +9,7 @@ import Foundation import XCTest -@testable import MasKit +@testable import mas class MASErrorTestCase: XCTestCase { private let errorDomain = "MAS" @@ -30,7 +30,7 @@ class MASErrorTestCase: XCTestCase { override func setUp() { super.setUp() - MasKit.initialize() + Mas.initialize() nserror = NSError(domain: errorDomain, code: 999) localizedDescription = "foo" } diff --git a/Tests/MasKitTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift similarity index 95% rename from Tests/MasKitTests/Extensions/Bundle+JSON.swift rename to Tests/masTests/Extensions/Bundle+JSON.swift index 1159243..34cd6ac 100644 --- a/Tests/MasKitTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -1,6 +1,6 @@ // // Bundle+JSON.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -28,7 +28,7 @@ extension Bundle { let bundleURL = Bundle(for: NetworkSessionMock.self) .bundleURL .deletingLastPathComponent() - .appendingPathComponent("mas_MasKitTests.bundle") + .appendingPathComponent("mas_masTests.bundle") guard let bundle = Bundle(url: bundleURL), let url = bundle.url(for: fileName) else { diff --git a/Tests/MasKitTests/Extensions/String+FileExtension.swift b/Tests/masTests/Extensions/String+FileExtension.swift similarity index 96% rename from Tests/MasKitTests/Extensions/String+FileExtension.swift rename to Tests/masTests/Extensions/String+FileExtension.swift index 1c08969..6f8649a 100644 --- a/Tests/MasKitTests/Extensions/String+FileExtension.swift +++ b/Tests/masTests/Extensions/String+FileExtension.swift @@ -1,6 +1,6 @@ // // String+FileExtension.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift similarity index 92% rename from Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift rename to Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift index 96b56df..7d9aebc 100644 --- a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift @@ -1,6 +1,6 @@ // // OpenSystemCommandMock.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/4/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import Foundation -@testable import MasKit +@testable import mas class OpenSystemCommandMock: ExternalCommand { // Stub out protocol logic diff --git a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift similarity index 91% rename from Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift rename to Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index 9e479fc..d2709ef 100644 --- a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -1,6 +1,6 @@ // // OpenSystemCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2/24/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class OpenSystemCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("open system command") { context("binary path") { diff --git a/Tests/MasKitTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift similarity index 96% rename from Tests/MasKitTests/Formatters/AppListFormatterSpec.swift rename to Tests/masTests/Formatters/AppListFormatterSpec.swift index 91b6faf..c454691 100644 --- a/Tests/MasKitTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -1,6 +1,6 @@ // // AppListFormatterSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 8/23/2020. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class AppListsFormatterSpec: QuickSpec { override public func spec() { @@ -18,7 +18,7 @@ public class AppListsFormatterSpec: QuickSpec { var products: [SoftwareProduct] = [] beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("app list formatter") { beforeEach { diff --git a/Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift similarity index 97% rename from Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift rename to Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 1fe7fb7..211445b 100644 --- a/Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -1,6 +1,6 @@ // // SearchResultFormatterSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/14/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class SearchResultsFormatterSpec: QuickSpec { override public func spec() { @@ -18,7 +18,7 @@ public class SearchResultsFormatterSpec: QuickSpec { var results: [SearchResult] = [] beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("search results formatter") { beforeEach { diff --git a/Tests/MasKitTests/JSON/lookup/fantastical.json b/Tests/masTests/JSON/lookup/fantastical.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/fantastical.json rename to Tests/masTests/JSON/lookup/fantastical.json diff --git a/Tests/MasKitTests/JSON/lookup/notability.json b/Tests/masTests/JSON/lookup/notability.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/notability.json rename to Tests/masTests/JSON/lookup/notability.json diff --git a/Tests/MasKitTests/JSON/lookup/slack.json b/Tests/masTests/JSON/lookup/slack.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/slack.json rename to Tests/masTests/JSON/lookup/slack.json diff --git a/Tests/MasKitTests/JSON/lookup/things.json b/Tests/masTests/JSON/lookup/things.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/things.json rename to Tests/masTests/JSON/lookup/things.json diff --git a/Tests/MasKitTests/JSON/search/bbedit.json b/Tests/masTests/JSON/search/bbedit.json similarity index 100% rename from Tests/MasKitTests/JSON/search/bbedit.json rename to Tests/masTests/JSON/search/bbedit.json diff --git a/Tests/MasKitTests/JSON/search/bear.json b/Tests/masTests/JSON/search/bear.json similarity index 100% rename from Tests/MasKitTests/JSON/search/bear.json rename to Tests/masTests/JSON/search/bear.json diff --git a/Tests/MasKitTests/JSON/search/deliveries.json b/Tests/masTests/JSON/search/deliveries.json similarity index 100% rename from Tests/MasKitTests/JSON/search/deliveries.json rename to Tests/masTests/JSON/search/deliveries.json diff --git a/Tests/MasKitTests/JSON/search/fantastical.json b/Tests/masTests/JSON/search/fantastical.json similarity index 100% rename from Tests/MasKitTests/JSON/search/fantastical.json rename to Tests/masTests/JSON/search/fantastical.json diff --git a/Tests/MasKitTests/JSON/search/mojave.json b/Tests/masTests/JSON/search/mojave.json similarity index 100% rename from Tests/MasKitTests/JSON/search/mojave.json rename to Tests/masTests/JSON/search/mojave.json diff --git a/Tests/MasKitTests/JSON/search/nonexistent.json b/Tests/masTests/JSON/search/nonexistent.json similarity index 100% rename from Tests/MasKitTests/JSON/search/nonexistent.json rename to Tests/masTests/JSON/search/nonexistent.json diff --git a/Tests/MasKitTests/JSON/search/notability.json b/Tests/masTests/JSON/search/notability.json similarity index 100% rename from Tests/MasKitTests/JSON/search/notability.json rename to Tests/masTests/JSON/search/notability.json diff --git a/Tests/MasKitTests/JSON/search/slack.json b/Tests/masTests/JSON/search/slack.json similarity index 100% rename from Tests/MasKitTests/JSON/search/slack.json rename to Tests/masTests/JSON/search/slack.json diff --git a/Tests/MasKitTests/JSON/search/things-3.json b/Tests/masTests/JSON/search/things-3.json similarity index 100% rename from Tests/MasKitTests/JSON/search/things-3.json rename to Tests/masTests/JSON/search/things-3.json diff --git a/Tests/MasKitTests/JSON/search/things-that-go-bump.json b/Tests/masTests/JSON/search/things-that-go-bump.json similarity index 100% rename from Tests/MasKitTests/JSON/search/things-that-go-bump.json rename to Tests/masTests/JSON/search/things-that-go-bump.json diff --git a/Tests/MasKitTests/JSON/search/things.json b/Tests/masTests/JSON/search/things.json similarity index 100% rename from Tests/MasKitTests/JSON/search/things.json rename to Tests/masTests/JSON/search/things.json diff --git a/Tests/MasKitTests/JSON/search/tweetbot.json b/Tests/masTests/JSON/search/tweetbot.json similarity index 100% rename from Tests/MasKitTests/JSON/search/tweetbot.json rename to Tests/masTests/JSON/search/tweetbot.json diff --git a/Tests/MasKitTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift similarity index 92% rename from Tests/MasKitTests/Models/SearchResultListSpec.swift rename to Tests/masTests/Models/SearchResultListSpec.swift index 137c907..a7fadf6 100644 --- a/Tests/MasKitTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -1,6 +1,6 @@ // // SearchResultListSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 9/2/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -10,12 +10,12 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class SearchResultListSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("search result list") { it("can parse bbedit") { diff --git a/Tests/MasKitTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift similarity index 90% rename from Tests/MasKitTests/Models/SearchResultSpec.swift rename to Tests/masTests/Models/SearchResultSpec.swift index 563946b..4617edd 100644 --- a/Tests/MasKitTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -1,6 +1,6 @@ // // SearchResultSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 9/2/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -10,12 +10,12 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class SearchResultSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("search result") { it("can parse things") { diff --git a/Tests/MasKitTests/Models/SoftwareProductMock.swift b/Tests/masTests/Models/SoftwareProductMock.swift similarity index 89% rename from Tests/MasKitTests/Models/SoftwareProductMock.swift rename to Tests/masTests/Models/SoftwareProductMock.swift index c12bfa6..91de06b 100644 --- a/Tests/MasKitTests/Models/SoftwareProductMock.swift +++ b/Tests/masTests/Models/SoftwareProductMock.swift @@ -1,6 +1,6 @@ // // SoftwareProductMock.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import Foundation -@testable import MasKit +@testable import mas struct SoftwareProductMock: SoftwareProduct { var appName: String diff --git a/Tests/MasKitTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift similarity index 95% rename from Tests/MasKitTests/Models/SoftwareProductSpec.swift rename to Tests/masTests/Models/SoftwareProductSpec.swift index 73806a4..80cbde3 100644 --- a/Tests/MasKitTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -1,6 +1,6 @@ // // SoftwareProductSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 9/30/21. // Copyright © 2018 mas-cli. All rights reserved. @@ -10,12 +10,12 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class SoftwareProductSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("software product") { let app = SoftwareProductMock( diff --git a/Tests/MasKitTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift similarity index 97% rename from Tests/MasKitTests/Network/NetworkManagerTests.swift rename to Tests/masTests/Network/NetworkManagerTests.swift index 1d73534..a92227c 100644 --- a/Tests/MasKitTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -1,6 +1,6 @@ // // NetworkManagerTests.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,12 +8,12 @@ import XCTest -@testable import MasKit +@testable import mas class NetworkManagerTests: XCTestCase { override public func setUp() { super.setUp() - MasKit.initialize() + Mas.initialize() } func testSuccessfulAsyncResponse() throws { diff --git a/Tests/MasKitTests/Network/NetworkSessionMock.swift b/Tests/masTests/Network/NetworkSessionMock.swift similarity index 95% rename from Tests/MasKitTests/Network/NetworkSessionMock.swift rename to Tests/masTests/Network/NetworkSessionMock.swift index 4be4b1b..a286bbb 100644 --- a/Tests/MasKitTests/Network/NetworkSessionMock.swift +++ b/Tests/masTests/Network/NetworkSessionMock.swift @@ -1,6 +1,6 @@ // // NetworkSessionMock -// MasKitTests +// masTests // // Created by Ben Chatelain on 11/13/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Foundation import PromiseKit -@testable import MasKit +@testable import mas /// Mock NetworkSession for testing. class NetworkSessionMock: NetworkSession { diff --git a/Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift similarity index 98% rename from Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift rename to Tests/masTests/Network/NetworkSessionMockFromFile.swift index 52c5d12..46d4ca2 100644 --- a/Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -1,6 +1,6 @@ // // NetworkSessionMockFromFile.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2019-01-05. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Tests/MasKitTests/Nimble/ResultPredicates.swift b/Tests/masTests/Nimble/ResultPredicates.swift similarity index 96% rename from Tests/MasKitTests/Nimble/ResultPredicates.swift rename to Tests/masTests/Nimble/ResultPredicates.swift index 209a3f6..c4f6be8 100644 --- a/Tests/MasKitTests/Nimble/ResultPredicates.swift +++ b/Tests/masTests/Nimble/ResultPredicates.swift @@ -1,6 +1,6 @@ // // ResultPredicates.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import Nimble -@testable import MasKit +@testable import mas /// Nimble predicate for result enum success case, no associated value func beSuccess() -> Predicate> { diff --git a/Tests/MasKitTests/OutputListener.swift b/Tests/masTests/OutputListener.swift similarity index 93% rename from Tests/MasKitTests/OutputListener.swift rename to Tests/masTests/OutputListener.swift index 434655e..14298f6 100644 --- a/Tests/MasKitTests/OutputListener.swift +++ b/Tests/masTests/OutputListener.swift @@ -1,12 +1,12 @@ // // OutputListener.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/7/19. // Copyright © 2019 mas-cli. All rights reserved. // -@testable import MasKit +@testable import mas /// Test helper for monitoring strings written to stdout. Modified from: /// https://stackoverflow.com/a/53569018 diff --git a/Tests/MasKitTests/OutputListenerSpec.swift b/Tests/masTests/OutputListenerSpec.swift similarity index 92% rename from Tests/MasKitTests/OutputListenerSpec.swift rename to Tests/masTests/OutputListenerSpec.swift index f793f19..b95240f 100644 --- a/Tests/MasKitTests/OutputListenerSpec.swift +++ b/Tests/masTests/OutputListenerSpec.swift @@ -1,6 +1,6 @@ // // OutputListenerSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/8/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,12 +9,12 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class OutputListenerSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + Mas.initialize() } describe("output listener") { it("can intercept a single line written stdout") { diff --git a/Tests/MasKitTests/Strongify.swift b/Tests/masTests/Strongify.swift similarity index 95% rename from Tests/MasKitTests/Strongify.swift rename to Tests/masTests/Strongify.swift index 6ab81fd..a87ce9a 100644 --- a/Tests/MasKitTests/Strongify.swift +++ b/Tests/masTests/Strongify.swift @@ -1,6 +1,6 @@ // // Strongify.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/8/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/script/version b/script/version index bbc2219..2f977ff 100755 --- a/script/version +++ b/script/version @@ -19,7 +19,7 @@ fi VERSION=$(git describe --abbrev=0 --tags) VERSION=${VERSION#v} -cat <"Sources/MasKit/Package.swift" +cat <"Sources/mas/Package.swift" // Generated by: script/version enum Package { static let version = "${VERSION}" From 6793a91e037815c4cc8554174f6c22b9e686b833 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:11:58 -0400 Subject: [PATCH 20/81] Remove unnecessary explicit inits. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Account.swift | 2 -- Sources/mas/Commands/Reset.swift | 2 -- Sources/mas/Commands/SignIn.swift | 2 -- Sources/mas/Commands/SignOut.swift | 2 -- Sources/mas/Commands/Version.swift | 2 -- Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift | 2 -- 6 files changed, 12 deletions(-) diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift index 6e58cb2..55d9c5a 100644 --- a/Sources/mas/Commands/Account.swift +++ b/Sources/mas/Commands/Account.swift @@ -14,8 +14,6 @@ public struct AccountCommand: CommandProtocol { public let verb = "account" public let function = "Prints the primary account Apple ID" - public init() {} - /// Runs the command. public func run(_: Options) -> Result { if #available(macOS 12, *) { diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index cefcaf2..212549d 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -15,8 +15,6 @@ public struct ResetCommand: CommandProtocol { public let verb = "reset" public let function = "Resets the Mac App Store" - public init() {} - /// Runs the command. public func run(_ options: Options) -> Result { // The "Reset Application" command in the Mac App Store debug menu performs diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift index 8ed6d1c..3720806 100644 --- a/Sources/mas/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -15,8 +15,6 @@ public struct SignInCommand: CommandProtocol { public let verb = "signin" public let function = "Sign in to the Mac App Store" - public init() {} - /// Runs the command. public func run(_ options: Options) -> Result { do { diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift index 65e213a..711f06f 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/SignOut.swift @@ -14,8 +14,6 @@ public struct SignOutCommand: CommandProtocol { public let verb = "signout" public let function = "Sign out of the Mac App Store" - public init() {} - /// Runs the command. public func run(_: Options) -> Result { if #available(macOS 10.13, *) { diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift index 1d2d580..f5957a3 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/Version.swift @@ -14,8 +14,6 @@ public struct VersionCommand: CommandProtocol { public let verb = "version" public let function = "Print version number" - public init() {} - /// Runs the command. public func run(_: Options) -> Result { print(Package.version) diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift index 7d9aebc..95c409b 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift @@ -21,8 +21,6 @@ class OpenSystemCommandMock: ExternalCommand { var stdoutPipe = Pipe() var stderrPipe = Pipe() - init() {} - func run(arguments: String...) throws { self.arguments = arguments } From 2535e3da420ee82af3e68349b718ad939fdbff6e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:55:04 -0400 Subject: [PATCH 21/81] Use Swift Argument Parser instead of Commandant. Command structs are nested types of Mas. Renamed structs. Limit code visibility as much as possible. Standardize variable names. Standardize spacing. Fix a few tests. Disable a useless test. Remove unnecessary test stdout output. Get swift-format from Brewfile instead of from Package.swift since swift-format depends on an old version of swift-argument-parser. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Package.resolved | 34 ++-- Package.swift | 8 +- Sources/mas/Commands/Account.swift | 41 +++-- Sources/mas/Commands/Home.swift | 103 +++++------- Sources/mas/Commands/Info.swift | 75 ++++----- Sources/mas/Commands/Install.swift | 80 ++++------ Sources/mas/Commands/List.swift | 53 +++---- Sources/mas/Commands/Lucky.swift | 143 +++++++---------- Sources/mas/Commands/Open.swift | 141 +++++++---------- Sources/mas/Commands/Outdated.swift | 121 ++++++-------- Sources/mas/Commands/Purchase.swift | 72 ++++----- Sources/mas/Commands/Reset.swift | 143 +++++++++-------- Sources/mas/Commands/Search.swift | 82 ++++------ Sources/mas/Commands/SignIn.swift | 64 ++++---- Sources/mas/Commands/SignOut.swift | 38 +++-- Sources/mas/Commands/Uninstall.swift | 97 +++++------- Sources/mas/Commands/Upgrade.swift | 148 ++++++++---------- Sources/mas/Commands/Vendor.swift | 111 ++++++------- Sources/mas/Commands/Version.swift | 29 ++-- Sources/mas/Errors/MASError.swift | 4 +- Sources/mas/Formatters/Utilities.swift | 4 +- Sources/mas/Mas.swift | 33 +++- .../Network/URLSession+NetworkSession.swift | 2 +- Sources/mas/main.swift | 37 ----- ...untCommandSpec.swift => AccountSpec.swift} | 11 +- .../{HomeCommandSpec.swift => HomeSpec.swift} | 39 +++-- .../{InfoCommandSpec.swift => InfoSpec.swift} | 40 ++--- .../Commands/InstallCommandSpec.swift | 13 +- .../{ListCommandSpec.swift => ListSpec.swift} | 11 +- ...LuckyCommandSpec.swift => LuckySpec.swift} | 11 +- .../{OpenCommandSpec.swift => OpenSpec.swift} | 46 +++--- ...edCommandSpec.swift => OutdatedSpec.swift} | 13 +- .../Commands/PurchaseCommandSpec.swift | 13 +- ...ResetCommandSpec.swift => ResetSpec.swift} | 11 +- ...archCommandSpec.swift => SearchSpec.swift} | 29 ++-- ...gnInCommandSpec.swift => SignInSpec.swift} | 11 +- ...OutCommandSpec.swift => SignOutSpec.swift} | 11 +- ...lCommandSpec.swift => UninstallSpec.swift} | 63 ++++---- ...adeCommandSpec.swift => UpgradeSpec.swift} | 11 +- ...ndorCommandSpec.swift => VendorSpec.swift} | 41 ++--- ...ionCommandSpec.swift => VersionSpec.swift} | 11 +- Tests/masTests/OutputListenerSpec.swift | 12 +- 42 files changed, 952 insertions(+), 1108 deletions(-) delete mode 100644 Sources/mas/main.swift rename Tests/masTests/Commands/{AccountCommandSpec.swift => AccountSpec.swift} (69%) rename Tests/masTests/Commands/{HomeCommandSpec.swift => HomeSpec.swift} (53%) rename Tests/masTests/Commands/{InfoCommandSpec.swift => InfoSpec.swift} (63%) rename Tests/masTests/Commands/{ListCommandSpec.swift => ListSpec.swift} (62%) rename Tests/masTests/Commands/{LuckyCommandSpec.swift => LuckySpec.swift} (67%) rename Tests/masTests/Commands/{OpenCommandSpec.swift => OpenSpec.swift} (56%) rename Tests/masTests/Commands/{OutdatedCommandSpec.swift => OutdatedSpec.swift} (57%) rename Tests/masTests/Commands/{ResetCommandSpec.swift => ResetSpec.swift} (62%) rename Tests/masTests/Commands/{SearchCommandSpec.swift => SearchSpec.swift} (52%) rename Tests/masTests/Commands/{SignInCommandSpec.swift => SignInSpec.swift} (65%) rename Tests/masTests/Commands/{SignOutCommandSpec.swift => SignOutSpec.swift} (61%) rename Tests/masTests/Commands/{UninstallCommandSpec.swift => UninstallSpec.swift} (53%) rename Tests/masTests/Commands/{UpgradeCommandSpec.swift => UpgradeSpec.swift} (59%) rename Tests/masTests/Commands/{VendorCommandSpec.swift => VendorSpec.swift} (53%) rename Tests/masTests/Commands/{VersionCommandSpec.swift => VersionSpec.swift} (62%) diff --git a/Package.resolved b/Package.resolved index a4d32c7..80d3efe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,12 @@ { "pins" : [ - { - "identity" : "commandant", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Carthage/Commandant.git", - "state" : { - "revision" : "a1671cf728db837cf5ec1980a80d276bbba748f6", - "version" : "0.18.0" - } - }, { "identity" : "cwlcatchexception", "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlCatchException.git", "state" : { - "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", - "version" : "2.1.1" + "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c", + "version" : "2.2.1" } }, { @@ -23,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state" : { - "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", - "version" : "2.1.0" + "revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071", + "version" : "2.2.2" } }, { @@ -41,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mxcl/PromiseKit.git", "state" : { - "revision" : "43772616c46a44a9977e41924ae01d0e55f2f9ca", - "version" : "6.18.1" + "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version" : "6.22.1" } }, { @@ -63,13 +54,22 @@ "version" : "2.1.1" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, { "identity" : "version", "kind" : "remoteSourceControl", "location" : "https://github.com/mxcl/Version.git", "state" : { - "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", - "version" : "2.0.1" + "revision" : "303a0f916772545e1e8667d3104f83be708a723c", + "version" : "2.1.0" } } ], diff --git a/Package.swift b/Package.swift index 402c576..f56b7ec 100644 --- a/Package.swift +++ b/Package.swift @@ -17,11 +17,11 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Carthage/Commandant.git", from: "0.18.0"), .package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"), .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), - .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.16.2"), - .package(url: "https://github.com/mxcl/Version.git", from: "2.0.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"), + .package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"), .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"), ], targets: [ @@ -30,7 +30,7 @@ let package = Package( .executableTarget( name: "mas", dependencies: [ - "Commandant", + .product(name: "ArgumentParser", package: "swift-argument-parser"), "PromiseKit", "Regex", "Version", diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift index 55d9c5a..8306bb2 100644 --- a/Sources/mas/Commands/Account.swift +++ b/Sources/mas/Commands/Account.swift @@ -6,27 +6,36 @@ // Copyright (c) 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import StoreFoundation -public struct AccountCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "account" - public let function = "Prints the primary account Apple ID" +extension Mas { + struct Account: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Prints the primary account Apple ID" + ) - /// Runs the command. - public func run(_: Options) -> Result { - if #available(macOS 12, *) { - // Account information is no longer available as of Monterey. - // https://github.com/mas-cli/mas/issues/417 - return .failure(.notSupported) + /// Runs the command. + func run() throws { + let result = runInternal() + if case .failure = result { + try result.get() + } } - do { - print(try ISStoreAccount.primaryAccount.wait().identifier) - return .success(()) - } catch { - return .failure(error as? MASError ?? .failed(error: error as NSError)) + func runInternal() -> Result { + if #available(macOS 12, *) { + // Account information is no longer available as of Monterey. + // https://github.com/mas-cli/mas/issues/417 + return .failure(.notSupported) + } + + do { + print(try ISStoreAccount.primaryAccount.wait().identifier) + return .success(()) + } catch { + return .failure(error as? MASError ?? .failed(error: error as NSError)) + } } } } diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index 896ae30..417ca8f 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -6,74 +6,53 @@ // Copyright © 2016 mas-cli. All rights reserved. // -import Commandant +import ArgumentParser -/// Opens app page on MAS Preview. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct HomeCommand: CommandProtocol { - public typealias Options = HomeOptions - - public let verb = "home" - public let function = "Opens MAS Preview app page in a browser" - - private let storeSearch: StoreSearch - private var openCommand: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() +extension Mas { + /// Opens app page on MAS Preview. Uses the iTunes Lookup API: + /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + struct Home: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens MAS Preview app page in a browser" ) - } - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - self.openCommand = openCommand - } + @Argument(help: "ID of app to show on MAS Preview") + var appId: Int - /// Runs the command. - public func run(_ options: HomeOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() else { - return .failure(.noSearchResultsFound) + /// Runs the command. + func run() throws { + let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + if case .failure = result { + try result.get() } - - do { - try openCommand.run(arguments: result.trackViewUrl) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) } - return .success(()) - } -} - -public struct HomeOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> HomeOptions { - HomeOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to show on MAS Preview") + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + do { + guard let result = try storeSearch.lookup(app: appId).wait() else { + return .failure(.noSearchResultsFound) + } + + do { + try openCommand.run(arguments: result.trackViewUrl) + } catch { + printError("Unable to launch open command") + return .failure(.searchFailed) + } + if openCommand.failed { + let reason = openCommand.process.terminationReason + printError("Open failed: (\(reason)) \(openCommand.stderr)") + return .failure(.searchFailed) + } + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index 5530c4d..0083220 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -6,55 +6,44 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import Foundation -/// Displays app details. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct InfoCommand: CommandProtocol { - public let verb = "info" - public let function = "Display app information from the Mac App Store" +extension Mas { + /// Displays app details. Uses the iTunes Lookup API: + /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + struct Info: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Display app information from the Mac App Store" + ) - private let storeSearch: StoreSearch + @Argument(help: "ID of app to show info") + var appId: Int - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: InfoOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() else { - return .failure(.noSearchResultsFound) + /// Runs the command. + func run() throws { + let result = run(storeSearch: MasStoreSearch()) + if case .failure = result { + try result.get() } - - print(AppInfoFormatter.format(app: result)) - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) } - return .success(()) - } -} - -public struct InfoOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> InfoOptions { - InfoOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to show info") + func run(storeSearch: StoreSearch) -> Result { + do { + guard let result = try storeSearch.lookup(app: appId).wait() else { + return .failure(.noSearchResultsFound) + } + + print(AppInfoFormatter.format(app: result)) + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 12b5378..b3d79af 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -6,63 +6,47 @@ // Copyright (c) 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit -/// Installs previously purchased apps from the Mac App Store. -public struct InstallCommand: CommandProtocol { - public typealias Options = InstallOptions - public let verb = "install" - public let function = "Install from the Mac App Store" +extension Mas { + /// Installs previously purchased apps from the Mac App Store. + struct Install: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Install from the Mac App Store" + ) - private let appLibrary: AppLibrary + @Flag(help: "force reinstall") + var force = false + @Argument(help: "app ID(s) to install") + var appIds: [UInt64] - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary()) + if case .failure = result { + try result.get() + } + } - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } + func run(appLibrary: AppLibrary) -> Result { + // Try to download applications with given identifiers and collect results + let appIds = appIds.filter { appId in + if let product = appLibrary.installedApp(forId: appId), !force { + printWarning("\(product.appName) is already installed") + return false + } - /// Runs the command. - public func run(_ options: Options) -> Result { - // Try to download applications with given identifiers and collect results - let appIds = options.appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { - printWarning("\(product.appName) is already installed") - return false + return true } - return true - } + do { + try downloadAll(appIds).wait() + } catch { + return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + } - do { - try downloadAll(appIds).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + return .success(()) } - - return .success(()) - } -} - -public struct InstallOptions: OptionsProtocol { - let appIds: [UInt64] - let forceInstall: Bool - - public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions { - { forceInstall in - InstallOptions(appIds: appIds.map { UInt64($0) }, forceInstall: forceInstall) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "app ID(s) to install") - <*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall") } } diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index e280a4b..564ddfe 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -6,39 +6,34 @@ // Copyright (c) 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser -/// Command which lists all installed apps. -public struct ListCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "list" - public let function = "Lists apps from the Mac App Store which are currently installed" +extension Mas { + /// Command which lists all installed apps. + struct List: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Lists apps from the Mac App Store which are currently installed" + ) - private let appLibrary: AppLibrary - - /// Public initializer. - /// - Parameter appLibrary: AppLibrary manager. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the command. - public func run(_: Options) -> Result { - let products = appLibrary.installedApps - if products.isEmpty { - printError("No installed apps found") - return .success(()) + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary()) + if case .failure = result { + try result.get() + } } - let output = AppListFormatter.format(products: products) - print(output) + func run(appLibrary: AppLibrary) -> Result { + let products = appLibrary.installedApps + if products.isEmpty { + printError("No installed apps found") + return .success(()) + } - return .success(()) + let output = AppListFormatter.format(products: products) + print(output) + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index f1c7805..d9d774a 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -6,101 +6,74 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit -/// Command which installs the first search result. This is handy as many MAS titles -/// can be long with embedded keywords. -public struct LuckyCommand: CommandProtocol { - public typealias Options = LuckyOptions - public let verb = "lucky" - public let function = "Install the first result from the Mac App Store" +extension Mas { + /// Command which installs the first search result. This is handy as many MAS titles + /// can be long with embedded keywords. + struct Lucky: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Install the first result from the Mac App Store" + ) - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch + @Flag(help: "force reinstall") + var force = false + @Argument(help: "the app name to install") + var appName: String - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - /// - Parameter storeSearch: Search manager. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: Search manager. - init( - appLibrary: AppLibrary = MasAppLibrary(), - storeSearch: StoreSearch = MasStoreSearch() - ) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - var appId: Int? - - do { - let results = try storeSearch.search(for: options.appName).wait() - guard let result = results.first else { - printError("No results found") - return .failure(.noSearchResultsFound) + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + if case .failure = result { + try result.get() } - - appId = result.trackId - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) } - guard let identifier = appId else { fatalError() } + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + var appId: Int? - return install(UInt64(identifier), options: options) - } + do { + let results = try storeSearch.search(for: appName).wait() + guard let result = results.first else { + printError("No results found") + return .failure(.noSearchResultsFound) + } + + appId = result.trackId + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) + } + + guard let identifier = appId else { fatalError() } + + return install(UInt64(identifier), appLibrary: appLibrary) + } + + /// Installs an app. + /// + /// - Parameters: + /// - appId: App identifier + /// - appLibrary: Library of installed apps + /// - Returns: Result of the operation. + fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) -> Result { + // Try to download applications with given identifiers and collect results + if let product = appLibrary.installedApp(forId: appId), !force { + printWarning("\(product.appName) is already installed") + return .success(()) + } + + do { + try downloadAll([appId]).wait() + } catch { + return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + } - /// Installs an app. - /// - /// - Parameters: - /// - appId: App identifier - /// - options: command options. - /// - Returns: Result of the operation. - fileprivate func install(_ appId: UInt64, options: Options) -> Result { - // Try to download applications with given identifiers and collect results - if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { - printWarning("\(product.appName) is already installed") return .success(()) } - - do { - try downloadAll([appId]).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) - } -} - -public struct LuckyOptions: OptionsProtocol { - let appName: String - let forceInstall: Bool - - public static func create(_ appName: String) -> (_ forceInstall: Bool) -> LuckyOptions { - { forceInstall in - LuckyOptions(appName: appName, forceInstall: forceInstall) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app name to install") - <*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall") } } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 1a0817e..5d4dc64 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -6,97 +6,76 @@ // Copyright © 2016 mas-cli. All rights reserved. // -import Commandant +import ArgumentParser import Foundation private let markerValue = "appstore" private let masScheme = "macappstore" -/// Opens app page in MAS app. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct OpenCommand: CommandProtocol { - public typealias Options = OpenOptions - - public let verb = "open" - public let function = "Opens app page in AppStore.app" - - private let storeSearch: StoreSearch - private var systemOpen: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() +extension Mas { + /// Opens app page in MAS app. Uses the iTunes Lookup API: + /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + struct Open: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens app page in AppStore.app" ) - } - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - systemOpen = openCommand - } + @Argument(help: "the app ID") + var appId: String = markerValue - /// Runs the command. - public func run(_ options: OpenOptions) -> Result { - do { - if options.appId == markerValue { - // If no app ID is given, just open the MAS GUI app - try systemOpen.run(arguments: masScheme + "://") - return .success(()) + /// Runs the command. + func run() throws { + let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + if case .failure = result { + try result.get() } - - guard let appId = Int(options.appId) - else { - printError("Invalid app ID") - return .failure(.noSearchResultsFound) - } - - guard let result = try storeSearch.lookup(app: appId).wait() - else { - return .failure(.noSearchResultsFound) - } - - guard var url = URLComponents(string: result.trackViewUrl) - else { - return .failure(.searchFailed) - } - url.scheme = masScheme - - do { - try systemOpen.run(arguments: url.string!) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if systemOpen.failed { - let reason = systemOpen.process.terminationReason - printError("Open failed: (\(reason)) \(systemOpen.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) } - return .success(()) - } -} - -public struct OpenOptions: OptionsProtocol { - var appId: String - - static func create(_ appId: String) -> OpenOptions { - OpenOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(defaultValue: markerValue, usage: "the app ID") + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + do { + if appId == markerValue { + // If no app ID is given, just open the MAS GUI app + try openCommand.run(arguments: masScheme + "://") + return .success(()) + } + + guard let appId = Int(appId) + else { + printError("Invalid app ID") + return .failure(.noSearchResultsFound) + } + + guard let result = try storeSearch.lookup(app: appId).wait() + else { + return .failure(.noSearchResultsFound) + } + + guard var url = URLComponents(string: result.trackViewUrl) + else { + return .failure(.searchFailed) + } + url.scheme = masScheme + + do { + try openCommand.run(arguments: url.string!) + } catch { + printError("Unable to launch open command") + return .failure(.searchFailed) + } + if openCommand.failed { + let reason = openCommand.process.terminationReason + printError("Open failed: (\(reason)) \(openCommand.stderr)") + return .failure(.searchFailed) + } + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 893b4b7..42466d6 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -6,84 +6,67 @@ // Copyright (c) 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import Foundation import PromiseKit import enum Swift.Result -/// Command which displays a list of installed apps which have available updates -/// ready to be installed from the Mac App Store. -public struct OutdatedCommand: CommandProtocol { - public typealias Options = OutdatedOptions - public let verb = "outdated" - public let function = "Lists pending updates from the Mac App Store" +extension Mas { + /// Command which displays a list of installed apps which have available updates + /// ready to be installed from the Mac App Store. + struct Outdated: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Lists pending updates from the Mac App Store" + ) - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch + @Flag(help: "Show warnings about apps") + var verbose = false - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: StoreSearch manager. - init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - let promises = appLibrary.installedApps.map { installedApp in - firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) - }.done { storeApp in - guard let storeApp else { - if options.verbose { - printWarning( - """ - Identifier \(installedApp.itemIdentifier) not found in store. \ - Was expected to identify \(installedApp.appName). - """) - } - return - } - - if installedApp.isOutdatedWhenComparedTo(storeApp) { - print( - """ - \(installedApp.itemIdentifier) \(installedApp.appName) \ - (\(installedApp.bundleVersion) -> \(storeApp.version)) - """) - } + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + if case .failure = result { + try result.get() } } - return firstly { - when(fulfilled: promises) - }.map { - Result.success(()) - }.recover { error in - // Bubble up MASErrors - .value(Result.failure(error as? MASError ?? .searchFailed)) - }.wait() - } -} - -public struct OutdatedOptions: OptionsProtocol { - public typealias ClientError = MASError - - let verbose: Bool - - static func create(verbose: Bool) -> OutdatedOptions { - OutdatedOptions(verbose: verbose) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps") + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + let promises = appLibrary.installedApps.map { installedApp in + firstly { + storeSearch.lookup(app: installedApp.itemIdentifier.intValue) + }.done { storeApp in + guard let storeApp else { + if verbose { + printWarning( + """ + Identifier \(installedApp.itemIdentifier) not found in store. \ + Was expected to identify \(installedApp.appName). + """ + ) + } + return + } + + if installedApp.isOutdatedWhenComparedTo(storeApp) { + print( + """ + \(installedApp.itemIdentifier) \(installedApp.appName) \ + (\(installedApp.bundleVersion) -> \(storeApp.version)) + """ + ) + } + } + } + + return firstly { + when(fulfilled: promises) + }.map { + Result.success(()) + }.recover { error in + // Bubble up MASErrors + .value(Result.failure(error as? MASError ?? .searchFailed)) + }.wait() + } } } diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 77df061..95dce65 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -6,58 +6,44 @@ // Copyright (c) 2017 Jakob Rieck. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit -public struct PurchaseCommand: CommandProtocol { - public typealias Options = PurchaseOptions - public let verb = "purchase" - public let function = "Purchase and download free apps from the Mac App Store" +extension Mas { + struct Purchase: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Purchase and download free apps from the Mac App Store" + ) - private let appLibrary: AppLibrary + @Argument(help: "app ID(s) to install") + var appIds: [UInt64] - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary()) + if case .failure = result { + try result.get() + } + } - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } + func run(appLibrary: AppLibrary) -> Result { + // Try to download applications with given identifiers and collect results + let appIds = appIds.filter { appId in + if let product = appLibrary.installedApp(forId: appId) { + printWarning("\(product.appName) has already been purchased.") + return false + } - /// Runs the command. - public func run(_ options: Options) -> Result { - // Try to download applications with given identifiers and collect results - let appIds = options.appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId) { - printWarning("\(product.appName) has already been purchased.") - return false + return true } - return true - } + do { + try downloadAll(appIds, purchase: true).wait() + } catch { + return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + } - do { - try downloadAll(appIds, purchase: true).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + return .success(()) } - - return .success(()) - } -} - -public struct PurchaseOptions: OptionsProtocol { - let appIds: [UInt64] - - public static func create(_ appIds: [Int]) -> PurchaseOptions { - PurchaseOptions(appIds: appIds.map { UInt64($0) }) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "app ID(s) to install") } } diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 212549d..a4f51bd 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -6,84 +6,83 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit -/// Kills several macOS processes as a means to reset the app store. -public struct ResetCommand: CommandProtocol { - public typealias Options = ResetOptions - public let verb = "reset" - public let function = "Resets the Mac App Store" +extension Mas { + /// Kills several macOS processes as a means to reset the app store. + struct Reset: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Resets the Mac App Store" + ) - /// Runs the command. - public func run(_ options: Options) -> Result { - // The "Reset Application" command in the Mac App Store debug menu performs - // the following steps - // - // - killall Dock - // - killall storeagent (storeagent no longer exists) - // - rm com.apple.appstore download directory - // - clear cookies (appears to be a no-op) - // - // As storeagent no longer exists we will implement a slight variant and kill all - // App Store-associated processes - // - storeaccountd - // - storeassetd - // - storedownloadd - // - storeinstalld - // - storelegacy + @Flag(help: "Enable debug mode") + var debug = false - // Kill processes - let killProcs = [ - "Dock", - "storeaccountd", - "storeassetd", - "storedownloadd", - "storeinstalld", - "storelegacy", - ] - - let kill = Process() - let stdout = Pipe() - let stderr = Pipe() - - kill.launchPath = "/usr/bin/killall" - kill.arguments = killProcs - kill.standardOutput = stdout - kill.standardError = stderr - - kill.launch() - kill.waitUntilExit() - - if kill.terminationStatus != 0, options.debug { - let output = stderr.fileHandleForReading.readDataToEndOfFile() - printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") - } - - // Wipe Download Directory - if let directory = CKDownloadDirectory(nil) { - do { - try FileManager.default.removeItem(atPath: directory) - } catch { - if options.debug { - printError("removeItemAtPath:\"\(directory)\" failed, \(error)") - } + /// Runs the command. + func run() throws { + let result = runInternal() + if case .failure = result { + try result.get() } } - return .success(()) - } -} - -public struct ResetOptions: OptionsProtocol { - let debug: Bool - - public static func create(debug: Bool) -> ResetOptions { - ResetOptions(debug: debug) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode") + func runInternal() -> Result { + // The "Reset Application" command in the Mac App Store debug menu performs + // the following steps + // + // - killall Dock + // - killall storeagent (storeagent no longer exists) + // - rm com.apple.appstore download directory + // - clear cookies (appears to be a no-op) + // + // As storeagent no longer exists we will implement a slight variant and kill all + // App Store-associated processes + // - storeaccountd + // - storeassetd + // - storedownloadd + // - storeinstalld + // - storelegacy + + // Kill processes + let killProcs = [ + "Dock", + "storeaccountd", + "storeassetd", + "storedownloadd", + "storeinstalld", + "storelegacy", + ] + + let kill = Process() + let stdout = Pipe() + let stderr = Pipe() + + kill.launchPath = "/usr/bin/killall" + kill.arguments = killProcs + kill.standardOutput = stdout + kill.standardError = stderr + + kill.launch() + kill.waitUntilExit() + + if kill.terminationStatus != 0, debug { + let output = stderr.fileHandleForReading.readDataToEndOfFile() + printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") + } + + // Wipe Download Directory + if let directory = CKDownloadDirectory(nil) { + do { + try FileManager.default.removeItem(atPath: directory) + } catch { + if debug { + printError("removeItemAtPath:\"\(directory)\" failed, \(error)") + } + } + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 3d2af78..a7535f7 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -6,62 +6,46 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser -/// Search the Mac App Store using the iTunes Search API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ -public struct SearchCommand: CommandProtocol { - public typealias Options = SearchOptions - public let verb = "search" - public let function = "Search for apps from the Mac App Store" +extension Mas { + /// Search the Mac App Store using the iTunes Search API: + /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ + struct Search: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Search for apps from the Mac App Store" + ) - private let storeSearch: StoreSearch + @Flag(help: "Show price of found apps") + var price = false + @Argument(help: "the app name to search") + var appName: String - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - /// - /// - Parameter storeSearch: Search manager. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.storeSearch = storeSearch - } - - public func run(_ options: Options) -> Result { - do { - let results = try storeSearch.search(for: options.appName).wait() - if results.isEmpty { - return .failure(.noSearchResultsFound) + func run() throws { + let result = run(storeSearch: MasStoreSearch()) + if case .failure = result { + try result.get() } + } - let output = SearchResultFormatter.format(results: results, includePrice: options.price) - print(output) + func run(storeSearch: StoreSearch) -> Result { + do { + let results = try storeSearch.search(for: appName).wait() + if results.isEmpty { + return .failure(.noSearchResultsFound) + } - return .success(()) - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) + let output = SearchResultFormatter.format(results: results, includePrice: price) + print(output) + + return .success(()) + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) } - return .failure(.searchFailed) } } } - -public struct SearchOptions: OptionsProtocol { - let appName: String - let price: Bool - - public static func create(_ appName: String) -> (_ price: Bool) -> SearchOptions { - { price in - SearchOptions(appName: appName, price: price) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app name to search") - <*> mode <| Option(key: "price", defaultValue: false, usage: "Show price of found apps") - } -} diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift index 3720806..1f06d76 100644 --- a/Sources/mas/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -6,46 +6,38 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import StoreFoundation -public struct SignInCommand: CommandProtocol { - public typealias Options = SignInOptions +extension Mas { + struct SignIn: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "signin", + abstract: "Sign in to the Mac App Store" + ) - public let verb = "signin" - public let function = "Sign in to the Mac App Store" + @Flag(help: "Complete login with graphical dialog") + var dialog = false + @Argument(help: "Apple ID") + var username: String + @Argument(help: "Password") + var password: String = "" - /// Runs the command. - public func run(_ options: Options) -> Result { - do { - _ = try ISStoreAccount.signIn( - username: options.username, - password: options.password, - systemDialog: options.dialog - ) - .wait() - return .success(()) - } catch { - return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) + /// Runs the command. + func run() throws { + let result = runInternal() + if case .failure = result { + try result.get() + } + } + + func runInternal() -> Result { + do { + _ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait() + return .success(()) + } catch { + return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) + } } } } - -public struct SignInOptions: OptionsProtocol { - public typealias ClientError = MASError - - let username: String - let password: String - let dialog: Bool - - static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions { - { password in { dialog in SignInOptions(username: username, password: password, dialog: dialog) } } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "Apple ID") - <*> mode <| Argument(defaultValue: "", usage: "Password") - <*> mode <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog") - } -} diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift index 711f06f..cf31423 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/SignOut.swift @@ -6,24 +6,34 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit -public struct SignOutCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "signout" - public let function = "Sign out of the Mac App Store" +extension Mas { + struct SignOut: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "signout", + abstract: "Sign out of the Mac App Store" + ) - /// Runs the command. - public func run(_: Options) -> Result { - if #available(macOS 10.13, *) { - ISServiceProxy.genericShared().accountService.signOut() - } else { - // Using CKAccountStore to sign out does nothing on High Sierra - // https://github.com/mas-cli/mas/issues/129 - CKAccountStore.shared().signOut() + /// Runs the command. + func run() throws { + let result = runInternal() + if case .failure = result { + try result.get() + } } - return .success(()) + func runInternal() -> Result { + if #available(macOS 10.13, *) { + ISServiceProxy.genericShared().accountService.signOut() + } else { + // Using CKAccountStore to sign out does nothing on High Sierra + // https://github.com/mas-cli/mas/issues/129 + CKAccountStore.shared().signOut() + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 0f3570e..cb6af8d 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -6,75 +6,52 @@ // Copyright © 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import CommerceKit import StoreFoundation -/// Command which uninstalls apps managed by the Mac App Store. -public struct UninstallCommand: CommandProtocol { - public typealias Options = UninstallOptions - public let verb = "uninstall" - public let function = "Uninstall app installed from the Mac App Store" +extension Mas { + /// Command which uninstalls apps managed by the Mac App Store. + struct Uninstall: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Uninstall app installed from the Mac App Store" + ) - private let appLibrary: AppLibrary + /// Flag indicating that removal shouldn't be performed + @Flag(help: "dry run") + var dryRun = false + @Argument(help: "ID of app to uninstall") + var appId: Int - /// Public initializer. - /// - Parameter appLibrary: AppLibrary manager. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the uninstall command. - /// - /// - Parameter options: UninstallOptions (arguments) for this command - /// - Returns: Success or an error. - public func run(_ options: Options) -> Result { - let appId = UInt64(options.appId) - - guard let product = appLibrary.installedApp(forId: appId) else { - return .failure(.notInstalled) + /// Runs the uninstall command. + func run() throws { + let result = run(appLibrary: MasAppLibrary()) + if case .failure = result { + try result.get() + } } - if options.dryRun { - printInfo("\(product.appName) \(product.bundlePath)") - printInfo("(not removed, dry run)") + func run(appLibrary: AppLibrary) -> Result { + let appId = UInt64(appId) + + guard let product = appLibrary.installedApp(forId: appId) else { + return .failure(.notInstalled) + } + + if dryRun { + printInfo("\(product.appName) \(product.bundlePath)") + printInfo("(not removed, dry run)") + + return .success(()) + } + + do { + try appLibrary.uninstallApp(app: product) + } catch { + return .failure(.uninstallFailed) + } return .success(()) } - - do { - try appLibrary.uninstallApp(app: product) - } catch { - return .failure(.uninstallFailed) - } - - return .success(()) - } -} - -/// Options for the uninstall command. -public struct UninstallOptions: OptionsProtocol { - /// Numeric app ID - let appId: Int - - /// Flag indicating that removal shouldn't be performed - let dryRun: Bool - - static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions { - { dryRun in - UninstallOptions(appId: appId, dryRun: dryRun) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to uninstall") - <*> mode <| Switch(flag: nil, key: "dry-run", usage: "dry run") } } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index a6ba781..5a8e74c 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -6,104 +6,90 @@ // Copyright © 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser import Foundation import PromiseKit import enum Swift.Result -/// Command which upgrades apps with new versions available in the Mac App Store. -public struct UpgradeCommand: CommandProtocol { - public typealias Options = UpgradeOptions - public let verb = "upgrade" - public let function = "Upgrade outdated apps from the Mac App Store" +extension Mas { + /// Command which upgrades apps with new versions available in the Mac App Store. + struct Upgrade: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Upgrade outdated apps from the Mac App Store" + ) - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch + @Argument(help: "app(s) to upgrade") + var apps: [String] = [] - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: StoreSearch manager. - init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] - do { - apps = try findOutdatedApps(options) - } catch { - // Bubble up MASErrors - return .failure(error as? MASError ?? .searchFailed) + /// Runs the command. + func run() throws { + let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + if case .failure = result { + try result.get() + } } - guard apps.count > 0 else { - printWarning("Nothing found to upgrade") + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] + do { + apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch) + } catch { + // Bubble up MASErrors + return .failure(error as? MASError ?? .searchFailed) + } + + guard apps.count > 0 else { + printWarning("Nothing found to upgrade") + return .success(()) + } + + print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") + print( + apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } + .joined(separator: "\n")) + + let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value) + do { + try downloadAll(appIds).wait() + } catch { + return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + } + return .success(()) } - print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") - print( - apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } - .joined(separator: "\n")) + private func findOutdatedApps( + appLibrary: AppLibrary, + storeSearch: StoreSearch + ) throws -> [(SoftwareProduct, SearchResult)] { + let apps: [SoftwareProduct] = + apps.isEmpty + ? appLibrary.installedApps + : apps.compactMap { + if let appId = UInt64($0) { + // if argument a UInt64, lookup app by id using argument + return appLibrary.installedApp(forId: appId) + } else { + // if argument not a UInt64, lookup app by name using argument + return appLibrary.installedApp(named: $0) + } + } - let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value) - do { - try downloadAll(appIds).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } + let promises = apps.map { installedApp in + // only upgrade apps whose local version differs from the store version + firstly { + storeSearch.lookup(app: installedApp.itemIdentifier.intValue) + }.map { result -> (SoftwareProduct, SearchResult)? in + guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { + return nil + } - return .success(()) - } - - private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] { - let apps: [SoftwareProduct] = - options.apps.isEmpty - ? appLibrary.installedApps - : options.apps.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument - return appLibrary.installedApp(forId: appId) - } else { - // if argument not a UInt64, lookup app by name using argument - return appLibrary.installedApp(named: $0) + return (installedApp, storeApp) } } - let promises = apps.map { installedApp in - // only upgrade apps whose local version differs from the store version - firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) - }.map { result -> (SoftwareProduct, SearchResult)? in - guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { - return nil - } - - return (installedApp, storeApp) - } + return try when(fulfilled: promises).wait().compactMap { $0 } } - - return try when(fulfilled: promises).wait().compactMap { $0 } - } -} - -public struct UpgradeOptions: OptionsProtocol { - let apps: [String] - - static func create(_ apps: [String]) -> UpgradeOptions { - UpgradeOptions(apps: apps) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(defaultValue: [], usage: "app(s) to upgrade") } } diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index f0f45f5..5057144 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -6,78 +6,57 @@ // Copyright © 2016 mas-cli. All rights reserved. // -import Commandant +import ArgumentParser -/// Opens vendor's app page in a browser. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct VendorCommand: CommandProtocol { - public typealias Options = VendorOptions - - public let verb = "vendor" - public let function = "Opens vendor's app page in a browser" - - private let storeSearch: StoreSearch - private var openCommand: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() +extension Mas { + /// Opens vendor's app page in a browser. Uses the iTunes Lookup API: + /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + struct Vendor: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens vendor's app page in a browser" ) - } - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - self.openCommand = openCommand - } + @Argument(help: "the app ID to show the vendor's website") + var appId: Int - /// Runs the command. - public func run(_ options: VendorOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() - else { - return .failure(.noSearchResultsFound) + /// Runs the command. + func run() throws { + let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + if case .failure = result { + try result.get() } - - guard let vendorWebsite = result.sellerUrl - else { throw MASError.noVendorWebsite } - - do { - try openCommand.run(arguments: vendorWebsite) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) } - return .success(()) - } -} - -public struct VendorOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> VendorOptions { - VendorOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app ID to show the vendor's website") + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + do { + guard let result = try storeSearch.lookup(app: appId).wait() + else { + return .failure(.noSearchResultsFound) + } + + guard let vendorWebsite = result.sellerUrl + else { throw MASError.noVendorWebsite } + + do { + try openCommand.run(arguments: vendorWebsite) + } catch { + printError("Unable to launch open command") + return .failure(.searchFailed) + } + if openCommand.failed { + let reason = openCommand.process.terminationReason + printError("Open failed: (\(reason)) \(openCommand.stderr)") + return .failure(.searchFailed) + } + } catch { + // Bubble up MASErrors + if let error = error as? MASError { + return .failure(error) + } + return .failure(.searchFailed) + } + + return .success(()) + } } } diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift index f5957a3..a2a0e51 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/Version.swift @@ -6,17 +6,26 @@ // Copyright © 2015 Andrew Naylor. All rights reserved. // -import Commandant +import ArgumentParser -/// Command which displays the version of the mas tool. -public struct VersionCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "version" - public let function = "Print version number" +extension Mas { + /// Command which displays the version of the mas tool. + struct Version: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Print version number" + ) - /// Runs the command. - public func run(_: Options) -> Result { - print(Package.version) - return .success(()) + /// Runs the command. + func run() throws { + let result = runInternal() + if case .failure = result { + try result.get() + } + } + + func runInternal() -> Result { + print(Package.version) + return .success(()) + } } } diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 9008c78..c799352 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -8,7 +8,7 @@ import Foundation -public enum MASError: Error, Equatable { +enum MASError: Error, Equatable { case notSupported case failed(error: NSError?) @@ -36,7 +36,7 @@ public enum MASError: Error, Equatable { // MARK: - CustomStringConvertible extension MASError: CustomStringConvertible { - public var description: String { + var description: String { switch self { case .notSignedIn: return "Not signed in" diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index 7092d57..f168e2c 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -91,7 +91,7 @@ func printInfo(_ message: String) { } /// Prints a message to stderr prefixed with "Warning:" underlined in yellow. -public func printWarning(_ message: String) { +func printWarning(_ message: String) { guard isatty(fileno(stderr)) != 0 else { print("Warning: \(message)", to: &standardError) return @@ -102,7 +102,7 @@ public func printWarning(_ message: String) { } /// Prints a message to stderr prefixed with "Error:" underlined in red. -public func printError(_ message: String) { +func printError(_ message: String) { guard isatty(fileno(stderr)) != 0 else { print("Error: \(message)", to: &standardError) return diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 5000efe..1b95c02 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -6,10 +6,39 @@ // Copyright © 2021 mas-cli. All rights reserved. // +import ArgumentParser import PromiseKit -public enum Mas { - public static func initialize() { +@main +struct Mas: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Mac App Store command-line interface", + subcommands: [ + Account.self, + Home.self, + Info.self, + Install.self, + List.self, + Lucky.self, + Open.self, + Outdated.self, + Purchase.self, + Reset.self, + Search.self, + SignIn.self, + SignOut.self, + Uninstall.self, + Upgrade.self, + Vendor.self, + Version.self, + ] + ) + + func validate() throws { + Mas.initialize() + } + + static func initialize() { PromiseKit.conf.Q.map = .global() PromiseKit.conf.Q.return = .global() PromiseKit.conf.logHandler = { event in diff --git a/Sources/mas/Network/URLSession+NetworkSession.swift b/Sources/mas/Network/URLSession+NetworkSession.swift index 04db152..5dca9dd 100644 --- a/Sources/mas/Network/URLSession+NetworkSession.swift +++ b/Sources/mas/Network/URLSession+NetworkSession.swift @@ -10,7 +10,7 @@ import Foundation import PromiseKit extension URLSession: NetworkSession { - public func loadData(from url: URL) -> Promise { + func loadData(from url: URL) -> Promise { Promise { seal in dataTask(with: url) { data, _, error in if let data { diff --git a/Sources/mas/main.swift b/Sources/mas/main.swift deleted file mode 100644 index 3c0776e..0000000 --- a/Sources/mas/main.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// main.swift -// mas -// -// Created by Andrew Naylor on 11/07/2015. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -import Commandant - -Mas.initialize() - -let registry = CommandRegistry() -let helpCommand = HelpCommand(registry: registry) - -registry.register(AccountCommand()) -registry.register(HomeCommand()) -registry.register(InfoCommand()) -registry.register(InstallCommand()) -registry.register(PurchaseCommand()) -registry.register(ListCommand()) -registry.register(LuckyCommand()) -registry.register(OpenCommand()) -registry.register(OutdatedCommand()) -registry.register(ResetCommand()) -registry.register(SearchCommand()) -registry.register(SignInCommand()) -registry.register(SignOutCommand()) -registry.register(UninstallCommand()) -registry.register(UpgradeCommand()) -registry.register(VendorCommand()) -registry.register(VersionCommand()) -registry.register(helpCommand) - -registry.main(defaultVerb: helpCommand.verb) { error in - printError(String(describing: error)) -} diff --git a/Tests/masTests/Commands/AccountCommandSpec.swift b/Tests/masTests/Commands/AccountSpec.swift similarity index 69% rename from Tests/masTests/Commands/AccountCommandSpec.swift rename to Tests/masTests/Commands/AccountSpec.swift index 73a187f..4195031 100644 --- a/Tests/masTests/Commands/AccountCommandSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -1,5 +1,5 @@ // -// AccountCommandSpec.swift +// AccountSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -12,7 +12,7 @@ import Quick @testable import mas // Deprecated test -public class AccountCommandSpec: QuickSpec { +public class AccountSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() @@ -20,9 +20,10 @@ public class AccountCommandSpec: QuickSpec { // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues xdescribe("Account command") { xit("displays active account") { - let cmd = AccountCommand() - let result = cmd.run(AccountCommand.Options()) - expect(result).to(beSuccess()) + expect { + try Mas.Account.parse([]).runInternal() + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/HomeCommandSpec.swift b/Tests/masTests/Commands/HomeSpec.swift similarity index 53% rename from Tests/masTests/Commands/HomeCommandSpec.swift rename to Tests/masTests/Commands/HomeSpec.swift index bfbe5c4..bd8bab8 100644 --- a/Tests/masTests/Commands/HomeCommandSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -1,5 +1,5 @@ // -// HomeCommandSpec.swift +// HomeSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-29. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class HomeCommandSpec: QuickSpec { +public class HomeSpec: QuickSpec { override public func spec() { let result = SearchResult( trackId: 1111, @@ -20,7 +20,6 @@ public class HomeCommandSpec: QuickSpec { ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() - let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { Mas.initialize() @@ -30,26 +29,32 @@ public class HomeCommandSpec: QuickSpec { storeSearch.reset() } it("fails to open app with invalid ID") { - let result = cmd.run(HomeCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) + expect { + try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .searchFailed + } + ) } it("can't find app with unknown ID") { - let result = cmd.run(HomeCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) + expect { + try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .noSearchResultsFound + } + ) } it("opens app on MAS Preview") { storeSearch.apps[result.trackId] = result - let cmdResult = cmd.run(HomeCommand.Options(appId: result.trackId)) - expect(cmdResult).to(beSuccess()) + expect { + try Mas.Home.parse([String(result.trackId)]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to(beSuccess()) expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments!.first!) == result.trackViewUrl } diff --git a/Tests/masTests/Commands/InfoCommandSpec.swift b/Tests/masTests/Commands/InfoSpec.swift similarity index 63% rename from Tests/masTests/Commands/InfoCommandSpec.swift rename to Tests/masTests/Commands/InfoSpec.swift index 2cd8982..237b9bf 100644 --- a/Tests/masTests/Commands/InfoCommandSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -1,5 +1,5 @@ // -// InfoCommandSpec.swift +// InfoSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class InfoCommandSpec: QuickSpec { +public class InfoSpec: QuickSpec { override public func spec() { let result = SearchResult( currentVersionReleaseDate: "2019-01-07T18:53:13Z", @@ -25,7 +25,6 @@ public class InfoCommandSpec: QuickSpec { version: "1.0" ) let storeSearch = StoreSearchMock() - let cmd = InfoCommand(storeSearch: storeSearch) let expectedOutput = """ Awesome App 1.0 [2.0] By: Awesome Dev @@ -44,28 +43,33 @@ public class InfoCommandSpec: QuickSpec { storeSearch.reset() } it("fails to open app with invalid ID") { - let result = cmd.run(InfoCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) + expect { + try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) + } + .to( + beFailure { error in + expect(error) == .searchFailed + } + ) } it("can't find app with unknown ID") { - let result = cmd.run(InfoCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) + expect { + try Mas.Info.parse(["999"]).run(storeSearch: storeSearch) + } + .to( + beFailure { error in + expect(error) == .noSearchResultsFound + } + ) } it("displays app details") { storeSearch.apps[result.trackId] = result let output = OutputListener() - let result = cmd.run(InfoCommand.Options(appId: result.trackId)) - - expect(result).to(beSuccess()) + expect { + try Mas.Info.parse([String(result.trackId)]).run(storeSearch: storeSearch) + } + .to(beSuccess()) expect(output.contents) == expectedOutput } } diff --git a/Tests/masTests/Commands/InstallCommandSpec.swift b/Tests/masTests/Commands/InstallCommandSpec.swift index a3a009e..ac5d77f 100644 --- a/Tests/masTests/Commands/InstallCommandSpec.swift +++ b/Tests/masTests/Commands/InstallCommandSpec.swift @@ -11,16 +11,17 @@ import Quick @testable import mas -public class InstallCommandSpec: QuickSpec { +public class InstallSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } - describe("install command") { - it("installs apps") { - let cmd = InstallCommand() - let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false)) - expect(result).to(beSuccess()) + xdescribe("install command") { + xit("installs apps") { + expect { + try Mas.Install.parse([]).run(appLibrary: AppLibraryMock()) + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/ListCommandSpec.swift b/Tests/masTests/Commands/ListSpec.swift similarity index 62% rename from Tests/masTests/Commands/ListCommandSpec.swift rename to Tests/masTests/Commands/ListSpec.swift index 300322e..8e12a7e 100644 --- a/Tests/masTests/Commands/ListCommandSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -1,5 +1,5 @@ // -// ListCommandSpec.swift +// ListSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-27. @@ -11,16 +11,17 @@ import Quick @testable import mas -public class ListCommandSpec: QuickSpec { +public class ListSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("list command") { it("lists apps") { - let list = ListCommand() - let result = list.run(ListCommand.Options()) - expect(result).to(beSuccess()) + expect { + try Mas.List.parse([]).run(appLibrary: AppLibraryMock()) + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/LuckyCommandSpec.swift b/Tests/masTests/Commands/LuckySpec.swift similarity index 67% rename from Tests/masTests/Commands/LuckyCommandSpec.swift rename to Tests/masTests/Commands/LuckySpec.swift index 8fbda3c..dadbe3c 100644 --- a/Tests/masTests/Commands/LuckyCommandSpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -1,5 +1,5 @@ // -// LuckyCommandSpec.swift +// LuckySpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class LuckyCommandSpec: QuickSpec { +public class LuckySpec: QuickSpec { override public func spec() { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) @@ -21,9 +21,10 @@ public class LuckyCommandSpec: QuickSpec { } describe("lucky command") { xit("installs the first app matching a search") { - let cmd = LuckyCommand(storeSearch: storeSearch) - let result = cmd.run(LuckyCommand.Options(appName: "Slack", forceInstall: false)) - expect(result).to(beSuccess()) + expect { + try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/OpenCommandSpec.swift b/Tests/masTests/Commands/OpenSpec.swift similarity index 56% rename from Tests/masTests/Commands/OpenCommandSpec.swift rename to Tests/masTests/Commands/OpenSpec.swift index 7a8562e..6c4642f 100644 --- a/Tests/masTests/Commands/OpenCommandSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -1,5 +1,5 @@ // -// OpenCommandSpec.swift +// OpenSpec.swift // masTests // // Created by Ben Chatelain on 2019-01-03. @@ -12,7 +12,7 @@ import Quick @testable import mas -public class OpenCommandSpec: QuickSpec { +public class OpenSpec: QuickSpec { override public func spec() { let result = SearchResult( trackId: 1111, @@ -21,7 +21,6 @@ public class OpenCommandSpec: QuickSpec { ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() - let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { Mas.initialize() @@ -31,34 +30,43 @@ public class OpenCommandSpec: QuickSpec { storeSearch.reset() } it("fails to open app with invalid ID") { - let result = cmd.run(OpenCommand.Options(appId: "-999")) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) + expect { + try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .searchFailed + } + ) } it("can't find app with unknown ID") { - let result = cmd.run(OpenCommand.Options(appId: "999")) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) + expect { + try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .noSearchResultsFound + } + ) } it("opens app in MAS") { storeSearch.apps[result.trackId] = result - let cmdResult = cmd.run(OpenCommand.Options(appId: result.trackId.description)) - expect(cmdResult).to(beSuccess()) + expect { + try Mas.Open.parse([result.trackId.description]) + .run(storeSearch: storeSearch, openCommand: openCommand) + } + .to(beSuccess()) expect(openCommand.arguments).toNot(beNil()) let url = URL(string: openCommand.arguments!.first!) expect(url).toNot(beNil()) expect(url?.scheme) == "macappstore" } it("just opens MAS if no app specified") { - let cmdResult = cmd.run(OpenCommand.Options(appId: "appstore")) - expect(cmdResult).to(beSuccess()) + expect { + try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to(beSuccess()) expect(openCommand.arguments).toNot(beNil()) let url = URL(string: openCommand.arguments!.first!) expect(url).toNot(beNil()) diff --git a/Tests/masTests/Commands/OutdatedCommandSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift similarity index 57% rename from Tests/masTests/Commands/OutdatedCommandSpec.swift rename to Tests/masTests/Commands/OutdatedSpec.swift index 22a6947..be831ba 100644 --- a/Tests/masTests/Commands/OutdatedCommandSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -1,5 +1,5 @@ // -// OutdatedCommandSpec.swift +// OutdatedSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,17 +11,18 @@ import Quick @testable import mas -public class OutdatedCommandSpec: QuickSpec { +public class OutdatedSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("outdated command") { it("displays apps with pending updates") { - let cmd = OutdatedCommand() - let result = cmd.run(OutdatedCommand.Options(verbose: true)) - print(result) - expect(result).to(beSuccess()) + expect { + try Mas.Outdated.parse(["--verbose"]) + .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/PurchaseCommandSpec.swift b/Tests/masTests/Commands/PurchaseCommandSpec.swift index 4db873f..f75a557 100644 --- a/Tests/masTests/Commands/PurchaseCommandSpec.swift +++ b/Tests/masTests/Commands/PurchaseCommandSpec.swift @@ -11,16 +11,17 @@ import Quick @testable import mas -public class PurchaseCommandSpec: QuickSpec { +public class PurchaseSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } - describe("purchase command") { - it("purchases apps") { - let cmd = PurchaseCommand() - let result = cmd.run(PurchaseCommand.Options(appIds: [])) - expect(result).to(beSuccess()) + xdescribe("purchase command") { + xit("purchases apps") { + expect { + try Mas.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock()) + } + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/ResetCommandSpec.swift b/Tests/masTests/Commands/ResetSpec.swift similarity index 62% rename from Tests/masTests/Commands/ResetCommandSpec.swift rename to Tests/masTests/Commands/ResetSpec.swift index 6405025..53e3020 100644 --- a/Tests/masTests/Commands/ResetCommandSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -1,5 +1,5 @@ // -// ResetCommandSpec.swift +// ResetSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,16 +11,17 @@ import Quick @testable import mas -public class ResetCommandSpec: QuickSpec { +public class ResetSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("reset command") { it("resets the App Store state") { - let cmd = ResetCommand() - let result = cmd.run(ResetCommand.Options(debug: false)) - expect(result).to(beSuccess()) + expect { + try Mas.Reset.parse([]).runInternal() + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/SearchCommandSpec.swift b/Tests/masTests/Commands/SearchSpec.swift similarity index 52% rename from Tests/masTests/Commands/SearchCommandSpec.swift rename to Tests/masTests/Commands/SearchSpec.swift index f15f812..4c0d9dc 100644 --- a/Tests/masTests/Commands/SearchCommandSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -1,5 +1,5 @@ // -// SearchCommandSpec.swift +// SearchSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class SearchCommandSpec: QuickSpec { +public class SearchSpec: QuickSpec { override public func spec() { let result = SearchResult( trackId: 1111, @@ -30,21 +30,20 @@ public class SearchCommandSpec: QuickSpec { } it("can find slack") { storeSearch.apps[result.trackId] = result - - let search = SearchCommand(storeSearch: storeSearch) - let searchOptions = SearchOptions(appName: "slack", price: false) - let result = search.run(searchOptions) - expect(result).to(beSuccess()) + expect { + try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + } + .to(beSuccess()) } it("fails when searching for nonexistent app") { - let search = SearchCommand(storeSearch: storeSearch) - let searchOptions = SearchOptions(appName: "nonexistent", price: false) - let result = search.run(searchOptions) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) + expect { + try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch) + } + .to( + beFailure { error in + expect(error) == .noSearchResultsFound + } + ) } } } diff --git a/Tests/masTests/Commands/SignInCommandSpec.swift b/Tests/masTests/Commands/SignInSpec.swift similarity index 65% rename from Tests/masTests/Commands/SignInCommandSpec.swift rename to Tests/masTests/Commands/SignInSpec.swift index 183b2d5..9b70fd2 100644 --- a/Tests/masTests/Commands/SignInCommandSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -1,5 +1,5 @@ // -// SignInCommandSpec.swift +// SignInSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -12,7 +12,7 @@ import Quick @testable import mas // Deprecated test -public class SignInCommandSpec: QuickSpec { +public class SignInSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() @@ -20,9 +20,10 @@ public class SignInCommandSpec: QuickSpec { // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues xdescribe("signin command") { xit("signs in") { - let cmd = SignInCommand() - let result = cmd.run(SignInCommand.Options(username: "", password: "", dialog: false)) - expect(result).to(beSuccess()) + expect { + try Mas.SignIn.parse(["", ""]).runInternal() + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/SignOutCommandSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift similarity index 61% rename from Tests/masTests/Commands/SignOutCommandSpec.swift rename to Tests/masTests/Commands/SignOutSpec.swift index c746deb..a987d5c 100644 --- a/Tests/masTests/Commands/SignOutCommandSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -1,5 +1,5 @@ // -// SignOutCommandSpec.swift +// SignOutSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,16 +11,17 @@ import Quick @testable import mas -public class SignOutCommandSpec: QuickSpec { +public class SignOutSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("signout command") { it("signs out") { - let cmd = SignOutCommand() - let result = cmd.run(SignOutCommand.Options()) - expect(result).to(beSuccess()) + expect { + try Mas.SignOut.parse([]).runInternal() + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/UninstallCommandSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift similarity index 53% rename from Tests/masTests/Commands/UninstallCommandSpec.swift rename to Tests/masTests/Commands/UninstallSpec.swift index 53a7410..1d41a1e 100644 --- a/Tests/masTests/Commands/UninstallCommandSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -1,5 +1,5 @@ // -// UninstallCommandSpec.swift +// UninstallSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-27. @@ -12,7 +12,7 @@ import Quick @testable import mas -public class UninstallCommandSpec: QuickSpec { +public class UninstallSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() @@ -27,60 +27,69 @@ public class UninstallCommandSpec: QuickSpec { itemIdentifier: NSNumber(value: appId) ) let mockLibrary = AppLibraryMock() - let uninstall = UninstallCommand(appLibrary: mockLibrary) context("dry run") { - let options = UninstallCommand.Options(appId: appId, dryRun: true) + let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appId)]) beforeEach { mockLibrary.reset() } it("can't remove a missing app") { - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .notInstalled - }) + expect { + uninstall.run(appLibrary: mockLibrary) + } + .to( + beFailure { error in + expect(error) == .notInstalled + } + ) } it("finds an app") { mockLibrary.installedApps.append(app) - let result = uninstall.run(options) - expect(result).to(beSuccess()) + expect { + uninstall.run(appLibrary: mockLibrary) + } + .to(beSuccess()) } } context("wet run") { - let options = UninstallCommand.Options(appId: appId, dryRun: false) + let uninstall = try! Mas.Uninstall.parse([String(appId)]) beforeEach { mockLibrary.reset() } it("can't remove a missing app") { - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .notInstalled - }) + expect { + uninstall.run(appLibrary: mockLibrary) + } + .to( + beFailure { error in + expect(error) == .notInstalled + } + ) } it("removes an app") { mockLibrary.installedApps.append(app) - let result = uninstall.run(options) - expect(result).to(beSuccess()) + expect { + uninstall.run(appLibrary: mockLibrary) + } + .to(beSuccess()) } it("fails if there is a problem with the trash command") { var brokenUninstall = app // make mutable copy brokenUninstall.bundlePath = "/dev/null" mockLibrary.installedApps.append(brokenUninstall) - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .uninstallFailed - }) + expect { + uninstall.run(appLibrary: mockLibrary) + } + .to( + beFailure { error in + expect(error) == .uninstallFailed + } + ) } } } diff --git a/Tests/masTests/Commands/UpgradeCommandSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift similarity index 59% rename from Tests/masTests/Commands/UpgradeCommandSpec.swift rename to Tests/masTests/Commands/UpgradeSpec.swift index 2b9b173..0b281b8 100644 --- a/Tests/masTests/Commands/UpgradeCommandSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -1,5 +1,5 @@ // -// UpgradeCommandSpec.swift +// UpgradeSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,16 +11,17 @@ import Quick @testable import mas -public class UpgradeCommandSpec: QuickSpec { +public class UpgradeSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("upgrade command") { it("upgrades stuff") { - let cmd = UpgradeCommand() - let result = cmd.run(UpgradeCommand.Options(apps: [""])) - expect(result).to(beSuccess()) + expect { + try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/Commands/VendorCommandSpec.swift b/Tests/masTests/Commands/VendorSpec.swift similarity index 53% rename from Tests/masTests/Commands/VendorCommandSpec.swift rename to Tests/masTests/Commands/VendorSpec.swift index 2c5fe94..173cfea 100644 --- a/Tests/masTests/Commands/VendorCommandSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -1,5 +1,5 @@ // -// VendorCommandSpec.swift +// VendorSpec.swift // masTests // // Created by Ben Chatelain on 2019-01-03. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class VendorCommandSpec: QuickSpec { +public class VendorSpec: QuickSpec { override public func spec() { let result = SearchResult( trackId: 1111, @@ -20,7 +20,6 @@ public class VendorCommandSpec: QuickSpec { ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() - let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand) beforeSuite { Mas.initialize() @@ -30,26 +29,32 @@ public class VendorCommandSpec: QuickSpec { storeSearch.reset() } it("fails to open app with invalid ID") { - let result = cmd.run(VendorCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) + expect { + try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .searchFailed + } + ) } it("can't find app with unknown ID") { - let result = cmd.run(VendorCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) + expect { + try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + } + .to( + beFailure { error in + expect(error) == .noSearchResultsFound + } + ) } it("opens vendor app page in browser") { storeSearch.apps[result.trackId] = result - - let cmdResult = cmd.run(VendorCommand.Options(appId: result.trackId)) - expect(cmdResult).to(beSuccess()) + expect { + try Mas.Vendor.parse([String(result.trackId)]) + .run(storeSearch: storeSearch, openCommand: openCommand) + } + .to(beSuccess()) expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments!.first!) == result.sellerUrl } diff --git a/Tests/masTests/Commands/VersionCommandSpec.swift b/Tests/masTests/Commands/VersionSpec.swift similarity index 62% rename from Tests/masTests/Commands/VersionCommandSpec.swift rename to Tests/masTests/Commands/VersionSpec.swift index 13ea5d4..5b2c773 100644 --- a/Tests/masTests/Commands/VersionCommandSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -1,5 +1,5 @@ // -// VersionCommandSpec.swift +// VersionSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. @@ -11,16 +11,17 @@ import Quick @testable import mas -public class VersionCommandSpec: QuickSpec { +public class VersionSpec: QuickSpec { override public func spec() { beforeSuite { Mas.initialize() } describe("version command") { it("displays the current version") { - let cmd = VersionCommand() - let result = cmd.run(VersionCommand.Options()) - expect(result).to(beSuccess()) + expect { + try Mas.Version.parse([]).runInternal() + } + .to(beSuccess()) } } } diff --git a/Tests/masTests/OutputListenerSpec.swift b/Tests/masTests/OutputListenerSpec.swift index b95240f..faef8c7 100644 --- a/Tests/masTests/OutputListenerSpec.swift +++ b/Tests/masTests/OutputListenerSpec.swift @@ -19,22 +19,20 @@ public class OutputListenerSpec: QuickSpec { describe("output listener") { it("can intercept a single line written stdout") { let output = OutputListener() - let expectedOutput = "hi there" print("hi there", terminator: "") - expect(output.contents) == expectedOutput + expect(output.contents) == "hi there" } it("can intercept multiple lines written stdout") { let output = OutputListener() - let expectedOutput = """ - hi there - - """ print("hi there") - expect(output.contents) == expectedOutput + expect(output.contents) == """ + hi there + + """ } } } From 3b86deb63e43b859e9d2410bc2d658fda4d4ac83 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 5 Oct 2024 23:41:55 -0400 Subject: [PATCH 22/81] Rename 2 test files that weren't renamed in previous commit to preserve history. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Commands/{InstallCommandSpec.swift => InstallSpec.swift} | 2 +- .../Commands/{PurchaseCommandSpec.swift => PurchaseSpec.swift} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Tests/masTests/Commands/{InstallCommandSpec.swift => InstallSpec.swift} (95%) rename Tests/masTests/Commands/{PurchaseCommandSpec.swift => PurchaseSpec.swift} (95%) diff --git a/Tests/masTests/Commands/InstallCommandSpec.swift b/Tests/masTests/Commands/InstallSpec.swift similarity index 95% rename from Tests/masTests/Commands/InstallCommandSpec.swift rename to Tests/masTests/Commands/InstallSpec.swift index ac5d77f..55a648a 100644 --- a/Tests/masTests/Commands/InstallCommandSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -1,5 +1,5 @@ // -// InstallCommandSpec.swift +// InstallSpec.swift // masTests // // Created by Ben Chatelain on 2018-12-28. diff --git a/Tests/masTests/Commands/PurchaseCommandSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift similarity index 95% rename from Tests/masTests/Commands/PurchaseCommandSpec.swift rename to Tests/masTests/Commands/PurchaseSpec.swift index f75a557..51f84f5 100644 --- a/Tests/masTests/Commands/PurchaseCommandSpec.swift +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -1,5 +1,5 @@ // -// PurchaseCommandSpec.swift +// PurchaseSpec.swift // masTests // // Created by Maximilian Blochberger on 2020-03-21. From 388d963cd1eea3605dd045e917757af9d6a8ae73 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:54:15 -0400 Subject: [PATCH 23/81] =?UTF-8?q?Do=20not=20return=20Result=20(or=20anythi?= =?UTF-8?q?ng=20else)=20from=20command=20run(=E2=80=A6)=20functions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Throw when failure. Normal Void return when success. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Account.swift | 12 ++----- Sources/mas/Commands/Home.swift | 21 ++++-------- Sources/mas/Commands/Info.swift | 17 +++------- Sources/mas/Commands/Install.swift | 11 ++---- Sources/mas/Commands/List.swift | 15 +++------ Sources/mas/Commands/Lucky.swift | 35 +++++++------------- Sources/mas/Commands/Open.swift | 27 +++++---------- Sources/mas/Commands/Outdated.swift | 10 ++---- Sources/mas/Commands/Purchase.swift | 11 ++---- Sources/mas/Commands/Reset.swift | 9 ----- Sources/mas/Commands/Search.swift | 17 +++------- Sources/mas/Commands/SignIn.swift | 10 +----- Sources/mas/Commands/SignOut.swift | 9 ----- Sources/mas/Commands/Uninstall.swift | 25 +++++--------- Sources/mas/Commands/Upgrade.swift | 18 +++------- Sources/mas/Commands/Vendor.swift | 21 ++++-------- Sources/mas/Commands/Version.swift | 8 ----- Tests/masTests/Commands/AccountSpec.swift | 4 +-- Tests/masTests/Commands/HomeSpec.swift | 15 ++------- Tests/masTests/Commands/InfoSpec.swift | 15 ++------- Tests/masTests/Commands/InstallSpec.swift | 2 +- Tests/masTests/Commands/ListSpec.swift | 2 +- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 17 +++------- Tests/masTests/Commands/OutdatedSpec.swift | 2 +- Tests/masTests/Commands/ResetSpec.swift | 4 +-- Tests/masTests/Commands/SearchSpec.swift | 8 ++--- Tests/masTests/Commands/SignInSpec.swift | 4 +-- Tests/masTests/Commands/SignOutSpec.swift | 4 +-- Tests/masTests/Commands/UninstallSpec.swift | 35 ++++++-------------- Tests/masTests/Commands/UpgradeSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 14 ++------ Tests/masTests/Commands/VersionSpec.swift | 4 +-- Tests/masTests/Nimble/ResultPredicates.swift | 32 ------------------ 34 files changed, 111 insertions(+), 331 deletions(-) delete mode 100644 Tests/masTests/Nimble/ResultPredicates.swift diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift index 8306bb2..bd172f6 100644 --- a/Sources/mas/Commands/Account.swift +++ b/Sources/mas/Commands/Account.swift @@ -17,24 +17,16 @@ extension Mas { /// Runs the command. func run() throws { - let result = runInternal() - if case .failure = result { - try result.get() - } - } - - func runInternal() -> Result { if #available(macOS 12, *) { // Account information is no longer available as of Monterey. // https://github.com/mas-cli/mas/issues/417 - return .failure(.notSupported) + throw MASError.notSupported } do { print(try ISStoreAccount.primaryAccount.wait().identifier) - return .success(()) } catch { - return .failure(error as? MASError ?? .failed(error: error as NSError)) + throw error as? MASError ?? MASError.failed(error: error as NSError) } } } diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index 417ca8f..37162af 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -21,38 +21,29 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) - if case .failure = result { - try result.get() - } + try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { guard let result = try storeSearch.lookup(app: appId).wait() else { - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } do { try openCommand.run(arguments: result.trackViewUrl) } catch { printError("Unable to launch open command") - return .failure(.searchFailed) + throw MASError.searchFailed } if openCommand.failed { let reason = openCommand.process.terminationReason printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) + throw MASError.searchFailed } } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index 0083220..cf0b686 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -22,28 +22,19 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(storeSearch: MasStoreSearch()) - if case .failure = result { - try result.get() - } + try run(storeSearch: MasStoreSearch()) } - func run(storeSearch: StoreSearch) -> Result { + func run(storeSearch: StoreSearch) throws { do { guard let result = try storeSearch.lookup(app: appId).wait() else { - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } print(AppInfoFormatter.format(app: result)) } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index b3d79af..a7d55f7 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -23,13 +23,10 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary()) } - func run(appLibrary: AppLibrary) -> Result { + func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results let appIds = appIds.filter { appId in if let product = appLibrary.installedApp(forId: appId), !force { @@ -43,10 +40,8 @@ extension Mas { do { try downloadAll(appIds).wait() } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + throw error as? MASError ?? .downloadFailed(error: error as NSError) } - - return .success(()) } } } diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index 564ddfe..492f58c 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -17,23 +17,16 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary()) } - func run(appLibrary: AppLibrary) -> Result { + func run(appLibrary: AppLibrary) throws { let products = appLibrary.installedApps if products.isEmpty { printError("No installed apps found") - return .success(()) + } else { + print(AppListFormatter.format(products: products)) } - - let output = AppListFormatter.format(products: products) - print(output) - - return .success(()) } } } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index d9d774a..de16507 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -24,34 +24,27 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { var appId: Int? do { let results = try storeSearch.search(for: appName).wait() guard let result = results.first else { printError("No results found") - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } appId = result.trackId } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } guard let identifier = appId else { fatalError() } - return install(UInt64(identifier), appLibrary: appLibrary) + try install(UInt64(identifier), appLibrary: appLibrary) } /// Installs an app. @@ -59,21 +52,17 @@ extension Mas { /// - Parameters: /// - appId: App identifier /// - appLibrary: Library of installed apps - /// - Returns: Result of the operation. - fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) -> Result { + fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(forId: appId), !force { printWarning("\(product.appName) is already installed") - return .success(()) + } else { + do { + try downloadAll([appId]).wait() + } catch { + throw error as? MASError ?? .downloadFailed(error: error as NSError) + } } - - do { - try downloadAll([appId]).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 5d4dc64..a7dd37e 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -25,34 +25,31 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) - if case .failure = result { - try result.get() - } + try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { if appId == markerValue { // If no app ID is given, just open the MAS GUI app try openCommand.run(arguments: masScheme + "://") - return .success(()) + return } guard let appId = Int(appId) else { printError("Invalid app ID") - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } guard let result = try storeSearch.lookup(app: appId).wait() else { - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } guard var url = URLComponents(string: result.trackViewUrl) else { - return .failure(.searchFailed) + throw MASError.searchFailed } url.scheme = masScheme @@ -60,22 +57,16 @@ extension Mas { try openCommand.run(arguments: url.string!) } catch { printError("Unable to launch open command") - return .failure(.searchFailed) + throw MASError.searchFailed } if openCommand.failed { let reason = openCommand.process.terminationReason printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) + throw MASError.searchFailed } } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 42466d6..9a8575b 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -25,13 +25,10 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { let promises = appLibrary.installedApps.map { installedApp in firstly { storeSearch.lookup(app: installedApp.itemIdentifier.intValue) @@ -59,12 +56,11 @@ extension Mas { } } - return firstly { + _ = firstly { when(fulfilled: promises) }.map { Result.success(()) }.recover { error in - // Bubble up MASErrors .value(Result.failure(error as? MASError ?? .searchFailed)) }.wait() } diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 95dce65..7ac1c75 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -20,13 +20,10 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary()) } - func run(appLibrary: AppLibrary) -> Result { + func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results let appIds = appIds.filter { appId in if let product = appLibrary.installedApp(forId: appId) { @@ -40,10 +37,8 @@ extension Mas { do { try downloadAll(appIds, purchase: true).wait() } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + throw error as? MASError ?? .downloadFailed(error: error as NSError) } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index a4f51bd..0ddc210 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -21,13 +21,6 @@ extension Mas { /// Runs the command. func run() throws { - let result = runInternal() - if case .failure = result { - try result.get() - } - } - - func runInternal() -> Result { // The "Reset Application" command in the Mac App Store debug menu performs // the following steps // @@ -81,8 +74,6 @@ extension Mas { } } } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index a7535f7..ec9e344 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -22,29 +22,20 @@ extension Mas { var appName: String func run() throws { - let result = run(storeSearch: MasStoreSearch()) - if case .failure = result { - try result.get() - } + try run(storeSearch: MasStoreSearch()) } - func run(storeSearch: StoreSearch) -> Result { + func run(storeSearch: StoreSearch) throws { do { let results = try storeSearch.search(for: appName).wait() if results.isEmpty { - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } let output = SearchResultFormatter.format(results: results, includePrice: price) print(output) - - return .success(()) } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } } } diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift index 1f06d76..ea332a3 100644 --- a/Sources/mas/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -25,18 +25,10 @@ extension Mas { /// Runs the command. func run() throws { - let result = runInternal() - if case .failure = result { - try result.get() - } - } - - func runInternal() -> Result { do { _ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait() - return .success(()) } catch { - return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) + throw error as? MASError ?? MASError.signInFailed(error: error as NSError) } } } diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift index cf31423..3384d8e 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/SignOut.swift @@ -18,13 +18,6 @@ extension Mas { /// Runs the command. func run() throws { - let result = runInternal() - if case .failure = result { - try result.get() - } - } - - func runInternal() -> Result { if #available(macOS 10.13, *) { ISServiceProxy.genericShared().accountService.signOut() } else { @@ -32,8 +25,6 @@ extension Mas { // https://github.com/mas-cli/mas/issues/129 CKAccountStore.shared().signOut() } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index cb6af8d..2d37cef 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -25,33 +25,26 @@ extension Mas { /// Runs the uninstall command. func run() throws { - let result = run(appLibrary: MasAppLibrary()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary()) } - func run(appLibrary: AppLibrary) -> Result { + func run(appLibrary: AppLibrary) throws { let appId = UInt64(appId) guard let product = appLibrary.installedApp(forId: appId) else { - return .failure(.notInstalled) + throw MASError.notInstalled } if dryRun { printInfo("\(product.appName) \(product.bundlePath)") printInfo("(not removed, dry run)") - - return .success(()) + } else { + do { + try appLibrary.uninstallApp(app: product) + } catch { + throw MASError.uninstallFailed + } } - - do { - try appLibrary.uninstallApp(app: product) - } catch { - return .failure(.uninstallFailed) - } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 5a8e74c..ce7be35 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -10,8 +10,6 @@ import ArgumentParser import Foundation import PromiseKit -import enum Swift.Result - extension Mas { /// Command which upgrades apps with new versions available in the Mac App Store. struct Upgrade: ParsableCommand { @@ -24,24 +22,20 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) - if case .failure = result { - try result.get() - } + try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result { + func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] do { apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch) } catch { - // Bubble up MASErrors - return .failure(error as? MASError ?? .searchFailed) + throw error as? MASError ?? .searchFailed } guard apps.count > 0 else { printWarning("Nothing found to upgrade") - return .success(()) + return } print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") @@ -53,10 +47,8 @@ extension Mas { do { try downloadAll(appIds).wait() } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) + throw error as? MASError ?? .downloadFailed(error: error as NSError) } - - return .success(()) } private func findOutdatedApps( diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index 5057144..10e4eee 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -21,17 +21,14 @@ extension Mas { /// Runs the command. func run() throws { - let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) - if case .failure = result { - try result.get() - } + try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result { + func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { guard let result = try storeSearch.lookup(app: appId).wait() else { - return .failure(.noSearchResultsFound) + throw MASError.noSearchResultsFound } guard let vendorWebsite = result.sellerUrl @@ -41,22 +38,16 @@ extension Mas { try openCommand.run(arguments: vendorWebsite) } catch { printError("Unable to launch open command") - return .failure(.searchFailed) + throw MASError.searchFailed } if openCommand.failed { let reason = openCommand.process.terminationReason printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) + throw MASError.searchFailed } } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) + throw error as? MASError ?? .searchFailed } - - return .success(()) } } } diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift index a2a0e51..00c0e5b 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/Version.swift @@ -17,15 +17,7 @@ extension Mas { /// Runs the command. func run() throws { - let result = runInternal() - if case .failure = result { - try result.get() - } - } - - func runInternal() -> Result { print(Package.version) - return .success(()) } } } diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 4195031..b4c6277 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -21,9 +21,9 @@ public class AccountSpec: QuickSpec { xdescribe("Account command") { xit("displays active account") { expect { - try Mas.Account.parse([]).runInternal() + try Mas.Account.parse([]).run() } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index bd8bab8..158ae77 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -32,29 +32,20 @@ public class HomeSpec: QuickSpec { expect { try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .searchFailed - } - ) + .to(throwError(MASError.searchFailed)) } it("can't find app with unknown ID") { expect { try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - } - ) + .to(throwError(MASError.noSearchResultsFound)) } it("opens app on MAS Preview") { storeSearch.apps[result.trackId] = result - expect { try Mas.Home.parse([String(result.trackId)]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to(beSuccess()) + .toNot(throwError()) expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments!.first!) == result.trackViewUrl } diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 237b9bf..99604ef 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -46,30 +46,21 @@ public class InfoSpec: QuickSpec { expect { try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) } - .to( - beFailure { error in - expect(error) == .searchFailed - } - ) + .to(throwError(MASError.searchFailed)) } it("can't find app with unknown ID") { expect { try Mas.Info.parse(["999"]).run(storeSearch: storeSearch) } - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - } - ) + .to(throwError(MASError.noSearchResultsFound)) } it("displays app details") { storeSearch.apps[result.trackId] = result let output = OutputListener() - expect { try Mas.Info.parse([String(result.trackId)]).run(storeSearch: storeSearch) } - .to(beSuccess()) + .toNot(throwError()) expect(output.contents) == expectedOutput } } diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift index 55a648a..4baeead 100644 --- a/Tests/masTests/Commands/InstallSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -21,7 +21,7 @@ public class InstallSpec: QuickSpec { expect { try Mas.Install.parse([]).run(appLibrary: AppLibraryMock()) } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index 8e12a7e..352ccdd 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -21,7 +21,7 @@ public class ListSpec: QuickSpec { expect { try Mas.List.parse([]).run(appLibrary: AppLibraryMock()) } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index dadbe3c..51da88f 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -24,7 +24,7 @@ public class LuckySpec: QuickSpec { expect { try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 6c4642f..54d3bbe 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -33,30 +33,21 @@ public class OpenSpec: QuickSpec { expect { try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .searchFailed - } - ) + .to(throwError(MASError.searchFailed)) } it("can't find app with unknown ID") { expect { try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - } - ) + .to(throwError(MASError.noSearchResultsFound)) } it("opens app in MAS") { storeSearch.apps[result.trackId] = result - expect { try Mas.Open.parse([result.trackId.description]) .run(storeSearch: storeSearch, openCommand: openCommand) } - .to(beSuccess()) + .toNot(throwError()) expect(openCommand.arguments).toNot(beNil()) let url = URL(string: openCommand.arguments!.first!) expect(url).toNot(beNil()) @@ -66,7 +57,7 @@ public class OpenSpec: QuickSpec { expect { try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to(beSuccess()) + .toNot(throwError()) expect(openCommand.arguments).toNot(beNil()) let url = URL(string: openCommand.arguments!.first!) expect(url).toNot(beNil()) diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index be831ba..9acd3da 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -22,7 +22,7 @@ public class OutdatedSpec: QuickSpec { try Mas.Outdated.parse(["--verbose"]) .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/ResetSpec.swift b/Tests/masTests/Commands/ResetSpec.swift index 53e3020..ca39e2b 100644 --- a/Tests/masTests/Commands/ResetSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -19,9 +19,9 @@ public class ResetSpec: QuickSpec { describe("reset command") { it("resets the App Store state") { expect { - try Mas.Reset.parse([]).runInternal() + try Mas.Reset.parse([]).run() } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 4c0d9dc..c5fdc84 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -33,17 +33,13 @@ public class SearchSpec: QuickSpec { expect { try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) } - .to(beSuccess()) + .toNot(throwError()) } it("fails when searching for nonexistent app") { expect { try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch) } - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - } - ) + .to(throwError(MASError.noSearchResultsFound)) } } } diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index 9b70fd2..f0866c7 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -21,9 +21,9 @@ public class SignInSpec: QuickSpec { xdescribe("signin command") { xit("signs in") { expect { - try Mas.SignIn.parse(["", ""]).runInternal() + try Mas.SignIn.parse(["", ""]).run() } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/SignOutSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift index a987d5c..a5b5466 100644 --- a/Tests/masTests/Commands/SignOutSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -19,9 +19,9 @@ public class SignOutSpec: QuickSpec { describe("signout command") { it("signs out") { expect { - try Mas.SignOut.parse([]).runInternal() + try Mas.SignOut.parse([]).run() } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 1d41a1e..e779c9d 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -36,21 +36,16 @@ public class UninstallSpec: QuickSpec { } it("can't remove a missing app") { expect { - uninstall.run(appLibrary: mockLibrary) + try uninstall.run(appLibrary: mockLibrary) } - .to( - beFailure { error in - expect(error) == .notInstalled - } - ) + .to(throwError(MASError.notInstalled)) } it("finds an app") { mockLibrary.installedApps.append(app) - expect { - uninstall.run(appLibrary: mockLibrary) + try uninstall.run(appLibrary: mockLibrary) } - .to(beSuccess()) + .toNot(throwError()) } } context("wet run") { @@ -61,35 +56,25 @@ public class UninstallSpec: QuickSpec { } it("can't remove a missing app") { expect { - uninstall.run(appLibrary: mockLibrary) + try uninstall.run(appLibrary: mockLibrary) } - .to( - beFailure { error in - expect(error) == .notInstalled - } - ) + .to(throwError(MASError.notInstalled)) } it("removes an app") { mockLibrary.installedApps.append(app) - expect { - uninstall.run(appLibrary: mockLibrary) + try uninstall.run(appLibrary: mockLibrary) } - .to(beSuccess()) + .toNot(throwError()) } it("fails if there is a problem with the trash command") { var brokenUninstall = app // make mutable copy brokenUninstall.bundlePath = "/dev/null" mockLibrary.installedApps.append(brokenUninstall) - expect { - uninstall.run(appLibrary: mockLibrary) + try uninstall.run(appLibrary: mockLibrary) } - .to( - beFailure { error in - expect(error) == .uninstallFailed - } - ) + .to(throwError(MASError.uninstallFailed)) } } } diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index 0b281b8..78a3c93 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -21,7 +21,7 @@ public class UpgradeSpec: QuickSpec { expect { try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index 173cfea..de38f2a 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -32,21 +32,13 @@ public class VendorSpec: QuickSpec { expect { try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .searchFailed - } - ) + .to(throwError(MASError.searchFailed)) } it("can't find app with unknown ID") { expect { try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - } - ) + .to(throwError(MASError.noSearchResultsFound)) } it("opens vendor app page in browser") { storeSearch.apps[result.trackId] = result @@ -54,7 +46,7 @@ public class VendorSpec: QuickSpec { try Mas.Vendor.parse([String(result.trackId)]) .run(storeSearch: storeSearch, openCommand: openCommand) } - .to(beSuccess()) + .toNot(throwError()) expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments!.first!) == result.sellerUrl } diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index 5b2c773..36adb0e 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -19,9 +19,9 @@ public class VersionSpec: QuickSpec { describe("version command") { it("displays the current version") { expect { - try Mas.Version.parse([]).runInternal() + try Mas.Version.parse([]).run() } - .to(beSuccess()) + .toNot(throwError()) } } } diff --git a/Tests/masTests/Nimble/ResultPredicates.swift b/Tests/masTests/Nimble/ResultPredicates.swift deleted file mode 100644 index c4f6be8..0000000 --- a/Tests/masTests/Nimble/ResultPredicates.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ResultPredicates.swift -// masTests -// -// Created by Ben Chatelain on 12/27/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble - -@testable import mas - -/// Nimble predicate for result enum success case, no associated value -func beSuccess() -> Predicate> { - Predicate.define("be ") { expression, message in - if case .success = try expression.evaluate() { - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} - -/// Nimble predicate for result enum failure with associated error -func beFailure(test: @escaping (MASError) -> Void = { _ in }) -> Predicate> { - Predicate.define("be ") { expression, message in - if case .failure(let error) = try expression.evaluate() { - test(error) - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} From cd421768599dc086b42725d558b4bcf076d292a7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:30:01 -0400 Subject: [PATCH 24/81] Rename apps property of Upgrade as appIds. Resolve #542 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index ce7be35..603902e 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -18,7 +18,7 @@ extension Mas { ) @Argument(help: "app(s) to upgrade") - var apps: [String] = [] + var appIds: [String] = [] /// Runs the command. func run() throws { @@ -43,9 +43,8 @@ extension Mas { apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } .joined(separator: "\n")) - let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value) do { - try downloadAll(appIds).wait() + try downloadAll(apps.map(\.installedApp.itemIdentifier.uint64Value)).wait() } catch { throw error as? MASError ?? .downloadFailed(error: error as NSError) } @@ -56,9 +55,9 @@ extension Mas { storeSearch: StoreSearch ) throws -> [(SoftwareProduct, SearchResult)] { let apps: [SoftwareProduct] = - apps.isEmpty + appIds.isEmpty ? appLibrary.installedApps - : apps.compactMap { + : appIds.compactMap { if let appId = UInt64($0) { // if argument a UInt64, lookup app by id using argument return appLibrary.installedApp(forId: appId) From 0efd73a5ee17bbe720f3d099ad905174dfdd84a7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:11:51 -0400 Subject: [PATCH 25/81] =?UTF-8?q?Simplify=20`Outdated.run(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #542 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Outdated.swift | 60 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 9a8575b..ab78ab4 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -29,40 +29,36 @@ extension Mas { } func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { - let promises = appLibrary.installedApps.map { installedApp in - firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) - }.done { storeApp in - guard let storeApp else { - if verbose { - printWarning( - """ - Identifier \(installedApp.itemIdentifier) not found in store. \ - Was expected to identify \(installedApp.appName). - """ - ) + _ = try when( + fulfilled: + appLibrary.installedApps.map { installedApp in + firstly { + storeSearch.lookup(app: installedApp.itemIdentifier.intValue) + }.done { storeApp in + guard let storeApp else { + if verbose { + printWarning( + """ + Identifier \(installedApp.itemIdentifier) not found in store. \ + Was expected to identify \(installedApp.appName). + """ + ) + } + return + } + + if installedApp.isOutdatedWhenComparedTo(storeApp) { + print( + """ + \(installedApp.itemIdentifier) \(installedApp.appName) \ + (\(installedApp.bundleVersion) -> \(storeApp.version)) + """ + ) + } } - return } - - if installedApp.isOutdatedWhenComparedTo(storeApp) { - print( - """ - \(installedApp.itemIdentifier) \(installedApp.appName) \ - (\(installedApp.bundleVersion) -> \(storeApp.version)) - """ - ) - } - } - } - - _ = firstly { - when(fulfilled: promises) - }.map { - Result.success(()) - }.recover { error in - .value(Result.failure(error as? MASError ?? .searchFailed)) - }.wait() + ) + .wait() } } } From 39f77c01a9c1889d6f26a5d24ade9ea53684a3f3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:22:47 -0400 Subject: [PATCH 26/81] Create `typealias AppID = UInt64`. Use `AppID` everywhere appropriate. Associated appID cleanup. Partial #478 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 4 ++-- Sources/mas/AppStore/SSPurchase.swift | 2 +- Sources/mas/Commands/Home.swift | 2 +- Sources/mas/Commands/Info.swift | 2 +- Sources/mas/Commands/Install.swift | 2 +- Sources/mas/Commands/Lucky.swift | 6 +++--- Sources/mas/Commands/Open.swift | 11 ++--------- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Purchase.swift | 2 +- Sources/mas/Commands/Uninstall.swift | 4 +--- Sources/mas/Commands/Upgrade.swift | 8 ++++---- Sources/mas/Commands/Vendor.swift | 2 +- Sources/mas/Controllers/AppLibrary.swift | 4 ++-- Sources/mas/Controllers/MasStoreSearch.swift | 4 ++-- Sources/mas/Controllers/StoreSearch.swift | 4 ++-- Sources/mas/Formatters/SearchResultFormatter.swift | 4 ++-- Sources/mas/Mas.swift | 2 ++ Sources/mas/Models/SearchResult.swift | 4 ++-- Tests/masTests/Commands/HomeSpec.swift | 2 +- Tests/masTests/Commands/InfoSpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 4 ++-- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 2 +- Tests/masTests/Controllers/MasStoreSearchSpec.swift | 2 +- Tests/masTests/Controllers/StoreSearchMock.swift | 9 ++------- 25 files changed, 40 insertions(+), 52 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index b832335..3dd44a5 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -17,7 +17,7 @@ import StoreFoundation /// Only works for free apps. Defaults to false. /// - Returns: A promise that completes when the downloads are complete. If any fail, /// the promise is rejected with the first error, after all remaining downloads are attempted. -func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise { +func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? return appIDs.reduce(Guarantee.value(())) { previous, appID in previous.then { @@ -34,7 +34,7 @@ func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise { } } -private func downloadWithRetries(_ appID: UInt64, purchase: Bool = false, attempts: Int = 3) -> Promise { +private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { SSPurchase().perform(adamId: appID, purchase: purchase) .recover { error -> Promise in guard attempts > 1 else { diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index 3fd5703..62f2651 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -11,7 +11,7 @@ import PromiseKit import StoreFoundation extension SSPurchase { - func perform(adamId: UInt64, purchase: Bool) -> Promise { + func perform(adamId: AppID, purchase: Bool) -> Promise { var parameters: [String: Any] = [ "productType": "C", "price": 0, diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index 37162af..f538f2e 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -17,7 +17,7 @@ extension Mas { ) @Argument(help: "ID of app to show on MAS Preview") - var appId: Int + var appId: AppID /// Runs the command. func run() throws { diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index cf0b686..a330f42 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -18,7 +18,7 @@ extension Mas { ) @Argument(help: "ID of app to show info") - var appId: Int + var appId: AppID /// Runs the command. func run() throws { diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index a7d55f7..ff0b0e8 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -19,7 +19,7 @@ extension Mas { @Flag(help: "force reinstall") var force = false @Argument(help: "app ID(s) to install") - var appIds: [UInt64] + var appIds: [AppID] /// Runs the command. func run() throws { diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index de16507..bad0511 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -28,7 +28,7 @@ extension Mas { } func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { - var appId: Int? + var appId: AppID? do { let results = try storeSearch.search(for: appName).wait() @@ -44,7 +44,7 @@ extension Mas { guard let identifier = appId else { fatalError() } - try install(UInt64(identifier), appLibrary: appLibrary) + try install(identifier, appLibrary: appLibrary) } /// Installs an app. @@ -52,7 +52,7 @@ extension Mas { /// - Parameters: /// - appId: App identifier /// - appLibrary: Library of installed apps - fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) throws { + fileprivate func install(_ appId: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(forId: appId), !force { printWarning("\(product.appName) is already installed") diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index a7dd37e..bc0afc9 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -9,7 +9,6 @@ import ArgumentParser import Foundation -private let markerValue = "appstore" private let masScheme = "macappstore" extension Mas { @@ -21,7 +20,7 @@ extension Mas { ) @Argument(help: "the app ID") - var appId: String = markerValue + var appId: AppID? /// Runs the command. func run() throws { @@ -30,18 +29,12 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - if appId == markerValue { + guard let appId else { // If no app ID is given, just open the MAS GUI app try openCommand.run(arguments: masScheme + "://") return } - guard let appId = Int(appId) - else { - printError("Invalid app ID") - throw MASError.noSearchResultsFound - } - guard let result = try storeSearch.lookup(app: appId).wait() else { throw MASError.noSearchResultsFound diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index ab78ab4..aeb99f0 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -33,7 +33,7 @@ extension Mas { fulfilled: appLibrary.installedApps.map { installedApp in firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) + storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value) }.done { storeApp in guard let storeApp else { if verbose { diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 7ac1c75..601f6b2 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -16,7 +16,7 @@ extension Mas { ) @Argument(help: "app ID(s) to install") - var appIds: [UInt64] + var appIds: [AppID] /// Runs the command. func run() throws { diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 2d37cef..96d572a 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -21,7 +21,7 @@ extension Mas { @Flag(help: "dry run") var dryRun = false @Argument(help: "ID of app to uninstall") - var appId: Int + var appId: AppID /// Runs the uninstall command. func run() throws { @@ -29,8 +29,6 @@ extension Mas { } func run(appLibrary: AppLibrary) throws { - let appId = UInt64(appId) - guard let product = appLibrary.installedApp(forId: appId) else { throw MASError.notInstalled } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 603902e..fc602b2 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -58,11 +58,11 @@ extension Mas { appIds.isEmpty ? appLibrary.installedApps : appIds.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument + if let appId = AppID($0) { + // if argument an AppID, lookup app by id using argument return appLibrary.installedApp(forId: appId) } else { - // if argument not a UInt64, lookup app by name using argument + // if argument not an AppID, lookup app by name using argument return appLibrary.installedApp(named: $0) } } @@ -70,7 +70,7 @@ extension Mas { let promises = apps.map { installedApp in // only upgrade apps whose local version differs from the store version firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) + storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value) }.map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { return nil diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index 10e4eee..32fa0fb 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -17,7 +17,7 @@ extension Mas { ) @Argument(help: "the app ID to show the vendor's website") - var appId: Int + var appId: AppID /// Runs the command. func run() throws { diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index d481a52..477876f 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -17,7 +17,7 @@ protocol AppLibrary { /// /// - Parameter forId: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId: UInt64) -> SoftwareProduct? + func installedApp(forId: AppID) -> SoftwareProduct? /// Uninstalls an app. /// @@ -32,7 +32,7 @@ extension AppLibrary { /// /// - Parameter forId: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId identifier: UInt64) -> SoftwareProduct? { + func installedApp(forId identifier: AppID) -> SoftwareProduct? { let appId = NSNumber(value: identifier) return installedApps.first { $0.itemIdentifier == appId } } diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index dda28f8..b5738ed 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -53,7 +53,7 @@ class MasStoreSearch: StoreSearch { } // Combine the results, removing any duplicates. - var seenAppIDs = Set() + var seenAppIDs = Set() return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in seenAppIDs.insert(result.trackId).inserted } @@ -64,7 +64,7 @@ class MasStoreSearch: StoreSearch { /// - Parameter appId: MAS ID of app /// - Returns: A Promise for the search result record of app, or nil if no apps match the ID, /// or an Error if there is a problem with the network request. - func lookup(app appId: Int) -> Promise { + func lookup(app appId: AppID) -> Promise { guard let url = lookupURL(forApp: appId, inCountry: country) else { fatalError("Failed to build URL for \(appId)") } diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index c326699..2b4e11e 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -11,7 +11,7 @@ import PromiseKit /// Protocol for searching the MAS catalog. protocol StoreSearch { - func lookup(app appId: Int) -> Promise + func lookup(app appId: AppID) -> Promise func search(for appName: String) -> Promise<[SearchResult]> } @@ -49,7 +49,7 @@ extension StoreSearch { /// /// - Parameter appId: MAS app identifier. /// - Returns: URL for the lookup service or nil if appId can't be encoded. - func lookupURL(forApp appId: Int, inCountry country: String?) -> URL? { + func lookupURL(forApp appId: AppID, inCountry country: String?) -> URL? { guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { return nil } diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index 15fa4aa..ed01448 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -26,9 +26,9 @@ enum SearchResultFormatter { let price = result.price ?? 0.0 if includePrice { - output += String(format: "%12d %@ $%5.2f (%@)\n", appId, appName, price, version) + output += String(format: "%12lu %@ $%5.2f (%@)\n", appId, appName, price, version) } else { - output += String(format: "%12d %@ (%@)\n", appId, appName, version) + output += String(format: "%12lu %@ (%@)\n", appId, appName, version) } } diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 1b95c02..a3a28ec 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -9,6 +9,8 @@ import ArgumentParser import PromiseKit +typealias AppID = UInt64 + @main struct Mas: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift index 7ac757b..129df7c 100644 --- a/Sources/mas/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -14,7 +14,7 @@ struct SearchResult: Decodable { var price: Double? var sellerName: String var sellerUrl: String? - var trackId: Int + var trackId: AppID var trackName: String var trackViewUrl: String var version: String @@ -27,7 +27,7 @@ struct SearchResult: Decodable { price: Double = 0.0, sellerName: String = "", sellerUrl: String = "", - trackId: Int = 0, + trackId: AppID = 0, trackName: String = "", trackViewUrl: String = "", version: String = "" diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 158ae77..6f740d3 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -32,7 +32,7 @@ public class HomeSpec: QuickSpec { expect { try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to(throwError(MASError.searchFailed)) + .to(throwError()) } it("can't find app with unknown ID") { expect { diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 99604ef..8047c95 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -46,7 +46,7 @@ public class InfoSpec: QuickSpec { expect { try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) } - .to(throwError(MASError.searchFailed)) + .to(throwError()) } it("can't find app with unknown ID") { expect { diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 54d3bbe..c0cca20 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -33,7 +33,7 @@ public class OpenSpec: QuickSpec { expect { try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to(throwError(MASError.searchFailed)) + .to(throwError()) } it("can't find app with unknown ID") { expect { @@ -55,7 +55,7 @@ public class OpenSpec: QuickSpec { } it("just opens MAS if no app specified") { expect { - try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand) + try Mas.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand) } .toNot(throwError()) expect(openCommand.arguments).toNot(beNil()) diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index e779c9d..691692d 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -18,7 +18,7 @@ public class UninstallSpec: QuickSpec { Mas.initialize() } describe("uninstall command") { - let appId = 12345 + let appId: AppID = 12345 let app = SoftwareProductMock( appName: "Some App", bundleIdentifier: "com.some.app", diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index de38f2a..499534a 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -32,7 +32,7 @@ public class VendorSpec: QuickSpec { expect { try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } - .to(throwError(MASError.searchFailed)) + .to(throwError()) } it("can't find app with unknown ID") { expect { diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index 99e1bdb..335700a 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -54,7 +54,7 @@ public class MasStoreSearchSpec: QuickSpec { context("when lookup used") { it("can find slack") { - let appId = 803_453_959 + let appId: AppID = 803_453_959 let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index 5037b92..a276951 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -11,7 +11,7 @@ import PromiseKit @testable import mas class StoreSearchMock: StoreSearch { - var apps: [Int: SearchResult] = [:] + var apps: [AppID: SearchResult] = [:] func search(for appName: String) -> Promise<[SearchResult]> { let filtered = apps.filter { $1.trackName.contains(appName) } @@ -19,12 +19,7 @@ class StoreSearchMock: StoreSearch { return .value(results) } - func lookup(app appId: Int) -> Promise { - // Negative numbers are invalid - guard appId > 0 else { - return Promise(error: MASError.searchFailed) - } - + func lookup(app appId: AppID) -> Promise { guard let result = apps[appId] else { return Promise(error: MASError.noSearchResultsFound) From 006273bb8104515e6d3f18c76f62519a5d32fe2a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:50:10 -0400 Subject: [PATCH 27/81] Standardize names of variables & parameters relating to AppIDs. Resolve #478 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 2 +- Sources/mas/AppStore/SSPurchase.swift | 8 ++++---- Sources/mas/Commands/Home.swift | 4 ++-- Sources/mas/Commands/Info.swift | 4 ++-- Sources/mas/Commands/Install.swift | 8 ++++---- Sources/mas/Commands/Lucky.swift | 18 ++++++++++-------- Sources/mas/Commands/Open.swift | 6 +++--- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Purchase.swift | 8 ++++---- Sources/mas/Commands/Uninstall.swift | 4 ++-- Sources/mas/Commands/Upgrade.swift | 12 ++++++------ Sources/mas/Commands/Vendor.swift | 4 ++-- Sources/mas/Controllers/AppLibrary.swift | 12 ++++++------ Sources/mas/Controllers/MasStoreSearch.swift | 8 ++++---- Sources/mas/Controllers/StoreSearch.swift | 10 +++++----- Sources/mas/Formatters/AppListFormatter.swift | 4 ++-- .../mas/Formatters/SearchResultFormatter.swift | 6 +++--- Tests/masTests/Commands/UninstallSpec.swift | 8 ++++---- .../Controllers/MasStoreSearchSpec.swift | 6 +++--- .../masTests/Controllers/StoreSearchMock.swift | 4 ++-- 20 files changed, 70 insertions(+), 68 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 3dd44a5..65a2373 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -35,7 +35,7 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { } private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { - SSPurchase().perform(adamId: appID, purchase: purchase) + SSPurchase().perform(appID: appID, purchase: purchase) .recover { error -> Promise in guard attempts > 1 else { throw error diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index 62f2651..cabfa51 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -11,11 +11,11 @@ import PromiseKit import StoreFoundation extension SSPurchase { - func perform(adamId: AppID, purchase: Bool) -> Promise { + func perform(appID: AppID, purchase: Bool) -> Promise { var parameters: [String: Any] = [ "productType": "C", "price": 0, - "salableAdamId": adamId, + "salableAdamId": appID, "pg": "default", "appExtVrsId": 0, ] @@ -34,7 +34,7 @@ extension SSPurchase { } .joined(separator: "&") - itemIdentifier = adamId + itemIdentifier = appID // Not sure if this is needed… if purchase { @@ -43,7 +43,7 @@ extension SSPurchase { downloadMetadata = SSDownloadMetadata() downloadMetadata.kind = "software" - downloadMetadata.itemIdentifier = adamId + downloadMetadata.itemIdentifier = appID // Monterey obscures the user's App Store account, but allows // redownloads without passing any account IDs to SSPurchase. diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index f538f2e..be04111 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -17,7 +17,7 @@ extension Mas { ) @Argument(help: "ID of app to show on MAS Preview") - var appId: AppID + var appID: AppID /// Runs the command. func run() throws { @@ -26,7 +26,7 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(app: appId).wait() else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index a330f42..0e6b71d 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -18,7 +18,7 @@ extension Mas { ) @Argument(help: "ID of app to show info") - var appId: AppID + var appID: AppID /// Runs the command. func run() throws { @@ -27,7 +27,7 @@ extension Mas { func run(storeSearch: StoreSearch) throws { do { - guard let result = try storeSearch.lookup(app: appId).wait() else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index ff0b0e8..88c2688 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -19,7 +19,7 @@ extension Mas { @Flag(help: "force reinstall") var force = false @Argument(help: "app ID(s) to install") - var appIds: [AppID] + var appIDs: [AppID] /// Runs the command. func run() throws { @@ -28,8 +28,8 @@ extension Mas { func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results - let appIds = appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId), !force { + let appIDs = appIDs.filter { appID in + if let product = appLibrary.installedApp(withAppID: appID), !force { printWarning("\(product.appName) is already installed") return false } @@ -38,7 +38,7 @@ extension Mas { } do { - try downloadAll(appIds).wait() + try downloadAll(appIDs).wait() } catch { throw error as? MASError ?? .downloadFailed(error: error as NSError) } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index bad0511..3b567fd 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -28,7 +28,7 @@ extension Mas { } func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { - var appId: AppID? + var appID: AppID? do { let results = try storeSearch.search(for: appName).wait() @@ -37,28 +37,30 @@ extension Mas { throw MASError.noSearchResultsFound } - appId = result.trackId + appID = result.trackId } catch { throw error as? MASError ?? .searchFailed } - guard let identifier = appId else { fatalError() } + guard let appID else { + fatalError() + } - try install(identifier, appLibrary: appLibrary) + try install(appID: appID, appLibrary: appLibrary) } /// Installs an app. /// /// - Parameters: - /// - appId: App identifier + /// - appID: App identifier /// - appLibrary: Library of installed apps - fileprivate func install(_ appId: AppID, appLibrary: AppLibrary) throws { + fileprivate func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results - if let product = appLibrary.installedApp(forId: appId), !force { + if let product = appLibrary.installedApp(withAppID: appID), !force { printWarning("\(product.appName) is already installed") } else { do { - try downloadAll([appId]).wait() + try downloadAll([appID]).wait() } catch { throw error as? MASError ?? .downloadFailed(error: error as NSError) } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index bc0afc9..e2a143f 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -20,7 +20,7 @@ extension Mas { ) @Argument(help: "the app ID") - var appId: AppID? + var appID: AppID? /// Runs the command. func run() throws { @@ -29,13 +29,13 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - guard let appId else { + guard let appID else { // If no app ID is given, just open the MAS GUI app try openCommand.run(arguments: masScheme + "://") return } - guard let result = try storeSearch.lookup(app: appId).wait() + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index aeb99f0..52b12ba 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -33,7 +33,7 @@ extension Mas { fulfilled: appLibrary.installedApps.map { installedApp in firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value) + storeSearch.lookup(appID: installedApp.itemIdentifier.uint64Value) }.done { storeApp in guard let storeApp else { if verbose { diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 601f6b2..841d26d 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -16,7 +16,7 @@ extension Mas { ) @Argument(help: "app ID(s) to install") - var appIds: [AppID] + var appIDs: [AppID] /// Runs the command. func run() throws { @@ -25,8 +25,8 @@ extension Mas { func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results - let appIds = appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId) { + let appIDs = appIDs.filter { appID in + if let product = appLibrary.installedApp(withAppID: appID) { printWarning("\(product.appName) has already been purchased.") return false } @@ -35,7 +35,7 @@ extension Mas { } do { - try downloadAll(appIds, purchase: true).wait() + try downloadAll(appIDs, purchase: true).wait() } catch { throw error as? MASError ?? .downloadFailed(error: error as NSError) } diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 96d572a..8265296 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -21,7 +21,7 @@ extension Mas { @Flag(help: "dry run") var dryRun = false @Argument(help: "ID of app to uninstall") - var appId: AppID + var appID: AppID /// Runs the uninstall command. func run() throws { @@ -29,7 +29,7 @@ extension Mas { } func run(appLibrary: AppLibrary) throws { - guard let product = appLibrary.installedApp(forId: appId) else { + guard let product = appLibrary.installedApp(withAppID: appID) else { throw MASError.notInstalled } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index fc602b2..afa1b5d 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -18,7 +18,7 @@ extension Mas { ) @Argument(help: "app(s) to upgrade") - var appIds: [String] = [] + var appIDs: [String] = [] /// Runs the command. func run() throws { @@ -55,12 +55,12 @@ extension Mas { storeSearch: StoreSearch ) throws -> [(SoftwareProduct, SearchResult)] { let apps: [SoftwareProduct] = - appIds.isEmpty + appIDs.isEmpty ? appLibrary.installedApps - : appIds.compactMap { - if let appId = AppID($0) { + : appIDs.compactMap { + if let appID = AppID($0) { // if argument an AppID, lookup app by id using argument - return appLibrary.installedApp(forId: appId) + return appLibrary.installedApp(withAppID: appID) } else { // if argument not an AppID, lookup app by name using argument return appLibrary.installedApp(named: $0) @@ -70,7 +70,7 @@ extension Mas { let promises = apps.map { installedApp in // only upgrade apps whose local version differs from the store version firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value) + storeSearch.lookup(appID: installedApp.itemIdentifier.uint64Value) }.map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { return nil diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index 32fa0fb..546865e 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -17,7 +17,7 @@ extension Mas { ) @Argument(help: "the app ID to show the vendor's website") - var appId: AppID + var appID: AppID /// Runs the command. func run() throws { @@ -26,7 +26,7 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(app: appId).wait() + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index 477876f..1db3ff9 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -15,9 +15,9 @@ protocol AppLibrary { /// Finds an app by ID. /// - /// - Parameter forId: MAS ID for app. + /// - Parameter withAppID: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId: AppID) -> SoftwareProduct? + func installedApp(withAppID appID: AppID) -> SoftwareProduct? /// Uninstalls an app. /// @@ -30,11 +30,11 @@ protocol AppLibrary { extension AppLibrary { /// Finds an app by ID. /// - /// - Parameter forId: MAS ID for app. + /// - Parameter withAppID: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId identifier: AppID) -> SoftwareProduct? { - let appId = NSNumber(value: identifier) - return installedApps.first { $0.itemIdentifier == appId } + func installedApp(withAppID appID: AppID) -> SoftwareProduct? { + let appID = NSNumber(value: appID) + return installedApps.first { $0.itemIdentifier == appID } } /// Finds an app by name. diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index b5738ed..b921d1e 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -61,12 +61,12 @@ class MasStoreSearch: StoreSearch { /// Looks up app details. /// - /// - Parameter appId: MAS ID of app + /// - Parameter appID: MAS ID of app /// - Returns: A Promise for the search result record of app, or nil if no apps match the ID, /// or an Error if there is a problem with the network request. - func lookup(app appId: AppID) -> Promise { - guard let url = lookupURL(forApp: appId, inCountry: country) else { - fatalError("Failed to build URL for \(appId)") + func lookup(appID: AppID) -> Promise { + guard let url = lookupURL(forAppID: appID, inCountry: country) else { + fatalError("Failed to build URL for \(appID)") } return firstly { loadSearchResults(url) diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index 2b4e11e..b04ef13 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -11,7 +11,7 @@ import PromiseKit /// Protocol for searching the MAS catalog. protocol StoreSearch { - func lookup(app appId: AppID) -> Promise + func lookup(appID: AppID) -> Promise func search(for appName: String) -> Promise<[SearchResult]> } @@ -47,15 +47,15 @@ extension StoreSearch { /// Builds the lookup URL for an app. /// - /// - Parameter appId: MAS app identifier. - /// - Returns: URL for the lookup service or nil if appId can't be encoded. - func lookupURL(forApp appId: AppID, inCountry country: String?) -> URL? { + /// - Parameter appID: MAS app identifier. + /// - Returns: URL for the lookup service or nil if appID can't be encoded. + func lookupURL(forAppID appID: AppID, inCountry country: String?) -> URL? { guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { return nil } components.queryItems = [ - URLQueryItem(name: "id", value: "\(appId)"), + URLQueryItem(name: "id", value: "\(appID)"), URLQueryItem(name: "entity", value: "desktopSoftware"), ] diff --git a/Sources/mas/Formatters/AppListFormatter.swift b/Sources/mas/Formatters/AppListFormatter.swift index 5edd055..8bafbf8 100644 --- a/Sources/mas/Formatters/AppListFormatter.swift +++ b/Sources/mas/Formatters/AppListFormatter.swift @@ -24,12 +24,12 @@ enum AppListFormatter { var output = "" for product in products { - let appId = product.itemIdentifier.stringValue + let appID = product.itemIdentifier.stringValue .padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0) let appName = product.appNameOrBundleIdentifier.padding(toLength: maxLength, withPad: " ", startingAt: 0) let version = product.bundleVersion - output += "\(appId) \(appName) (\(version))\n" + output += "\(appID) \(appName) (\(version))\n" } return output.trimmingCharacters(in: .newlines) diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index ed01448..eac80a7 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -20,15 +20,15 @@ enum SearchResultFormatter { var output = "" for result in results { - let appId = result.trackId + let appID = result.trackId let appName = result.trackName.padding(toLength: maxLength, withPad: " ", startingAt: 0) let version = result.version let price = result.price ?? 0.0 if includePrice { - output += String(format: "%12lu %@ $%5.2f (%@)\n", appId, appName, price, version) + output += String(format: "%12lu %@ $%5.2f (%@)\n", appID, appName, price, version) } else { - output += String(format: "%12lu %@ (%@)\n", appId, appName, version) + output += String(format: "%12lu %@ (%@)\n", appID, appName, version) } } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 691692d..76018ed 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -18,18 +18,18 @@ public class UninstallSpec: QuickSpec { Mas.initialize() } describe("uninstall command") { - let appId: AppID = 12345 + let appID: AppID = 12345 let app = SoftwareProductMock( appName: "Some App", bundleIdentifier: "com.some.app", bundlePath: "/tmp/Some.app", bundleVersion: "1.0", - itemIdentifier: NSNumber(value: appId) + itemIdentifier: NSNumber(value: appID) ) let mockLibrary = AppLibraryMock() context("dry run") { - let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appId)]) + let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appID)]) beforeEach { mockLibrary.reset() @@ -49,7 +49,7 @@ public class UninstallSpec: QuickSpec { } } context("wet run") { - let uninstall = try! Mas.Uninstall.parse([String(appId)]) + let uninstall = try! Mas.Uninstall.parse([String(appID)]) beforeEach { mockLibrary.reset() diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index 335700a..e05a2a6 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -54,13 +54,13 @@ public class MasStoreSearchSpec: QuickSpec { context("when lookup used") { it("can find slack") { - let appId: AppID = 803_453_959 + let appID: AppID = 803_453_959 let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) var lookup: SearchResult? do { - lookup = try storeSearch.lookup(app: appId).wait() + lookup = try storeSearch.lookup(appID: appID).wait() } catch { let maserror = error as! MASError if case .jsonParsing(let nserror) = maserror { @@ -70,7 +70,7 @@ public class MasStoreSearchSpec: QuickSpec { guard let result = lookup else { fatalError("lookup result was nil") } - expect(result.trackId) == appId + expect(result.trackId) == appID expect(result.bundleId) == "com.tinyspeck.slackmacgap" expect(result.price) == 0 expect(result.sellerName) == "Slack Technologies, Inc." diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index a276951..3b83df8 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -19,8 +19,8 @@ class StoreSearchMock: StoreSearch { return .value(results) } - func lookup(app appId: AppID) -> Promise { - guard let result = apps[appId] + func lookup(appID: AppID) -> Promise { + guard let result = apps[appID] else { return Promise(error: MASError.noSearchResultsFound) } From f8d7a36a4c3a66f7ae4466c4a7eae1345080eb65 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 2 Oct 2024 06:24:02 -0400 Subject: [PATCH 28/81] Upgrade test dependencies. Quick upgrade necessary if we ever switch from PromiseKit to Swift concurrency. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Package.resolved | 26 ++++++++++++++++--- Package.swift | 4 +-- Tests/masTests/Commands/AccountSpec.swift | 2 +- Tests/masTests/Commands/HomeSpec.swift | 2 +- Tests/masTests/Commands/InfoSpec.swift | 2 +- Tests/masTests/Commands/InstallSpec.swift | 2 +- Tests/masTests/Commands/ListSpec.swift | 2 +- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 2 +- Tests/masTests/Commands/OutdatedSpec.swift | 2 +- Tests/masTests/Commands/PurchaseSpec.swift | 2 +- Tests/masTests/Commands/ResetSpec.swift | 2 +- Tests/masTests/Commands/SearchSpec.swift | 2 +- Tests/masTests/Commands/SignInSpec.swift | 2 +- Tests/masTests/Commands/SignOutSpec.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/UpgradeSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 2 +- Tests/masTests/Commands/VersionSpec.swift | 2 +- .../Controllers/MasAppLibrarySpec.swift | 2 +- .../Controllers/MasStoreSearchSpec.swift | 2 +- .../OpenSystemCommandSpec.swift | 2 +- .../Formatters/AppListFormatterSpec.swift | 2 +- .../SearchResultFormatterSpec.swift | 2 +- .../Models/SearchResultListSpec.swift | 2 +- Tests/masTests/Models/SearchResultSpec.swift | 2 +- .../masTests/Models/SoftwareProductSpec.swift | 2 +- Tests/masTests/OutputListenerSpec.swift | 2 +- 28 files changed, 50 insertions(+), 32 deletions(-) diff --git a/Package.resolved b/Package.resolved index 80d3efe..dfcbb2b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Nimble.git", "state" : { - "revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc", - "version" : "10.0.0" + "revision" : "6416749c3c0488664fff6b42f8bf3ea8dc282ca1", + "version" : "13.6.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Quick.git", "state" : { - "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", - "version" : "5.0.1" + "revision" : "1163a1b1b114a657c7432b63dd1f92ce99fe11a6", + "version" : "7.6.2" } }, { @@ -54,6 +54,15 @@ "version" : "2.1.1" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -63,6 +72,15 @@ "version" : "1.5.0" } }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, { "identity" : "version", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index f56b7ec..021e71f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,8 +17,8 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"), - .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "13.6.0"), + .package(url: "https://github.com/Quick/Quick.git", from: "7.6.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"), .package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"), diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index b4c6277..2021577 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -13,7 +13,7 @@ import Quick // Deprecated test public class AccountSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 6f740d3..e366ee4 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class HomeSpec: QuickSpec { - override public func spec() { + override public static func spec() { let result = SearchResult( trackId: 1111, trackViewUrl: "mas preview url", diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 8047c95..bbedfdd 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class InfoSpec: QuickSpec { - override public func spec() { + override public static func spec() { let result = SearchResult( currentVersionReleaseDate: "2019-01-07T18:53:13Z", fileSizeBytes: "1024", diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift index 4baeead..8580fe0 100644 --- a/Tests/masTests/Commands/InstallSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class InstallSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index 352ccdd..5cb373d 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class ListSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index 51da88f..879c5dd 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class LuckySpec: QuickSpec { - override public func spec() { + override public static func spec() { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index c0cca20..bedab44 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class OpenSpec: QuickSpec { - override public func spec() { + override public static func spec() { let result = SearchResult( trackId: 1111, trackViewUrl: "fakescheme://some/url", diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 9acd3da..6152645 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class OutdatedSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift index 51f84f5..e186f55 100644 --- a/Tests/masTests/Commands/PurchaseSpec.swift +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class PurchaseSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/ResetSpec.swift b/Tests/masTests/Commands/ResetSpec.swift index ca39e2b..411c101 100644 --- a/Tests/masTests/Commands/ResetSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class ResetSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index c5fdc84..fba2caf 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SearchSpec: QuickSpec { - override public func spec() { + override public static func spec() { let result = SearchResult( trackId: 1111, trackName: "slack", diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index f0866c7..5336c99 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -13,7 +13,7 @@ import Quick // Deprecated test public class SignInSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/SignOutSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift index a5b5466..3424ef2 100644 --- a/Tests/masTests/Commands/SignOutSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SignOutSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 76018ed..072473e 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class UninstallSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index 78a3c93..f22d450 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class UpgradeSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index 499534a..b5bf8e5 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class VendorSpec: QuickSpec { - override public func spec() { + override public static func spec() { let result = SearchResult( trackId: 1111, trackViewUrl: "https://awesome.app", diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index 36adb0e..46c2baa 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class VersionSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift index 9d823a4..6fe8628 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class MasAppLibrarySpec: QuickSpec { - override public func spec() { + override public static func spec() { let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) beforeSuite { diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index e05a2a6..938c82c 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class MasStoreSearchSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index d2709ef..41db44a 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class OpenSystemCommandSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index c454691..ebc9130 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class AppListsFormatterSpec: QuickSpec { - override public func spec() { + override public static func spec() { // static func reference let format = AppListFormatter.format(products:) var products: [SoftwareProduct] = [] diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 211445b..368e8e6 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SearchResultsFormatterSpec: QuickSpec { - override public func spec() { + override public static func spec() { // static func reference let format = SearchResultFormatter.format(results:includePrice:) var results: [SearchResult] = [] diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift index a7fadf6..4f6f3e5 100644 --- a/Tests/masTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SearchResultListSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift index 4617edd..b3ddc24 100644 --- a/Tests/masTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SearchResultSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift index 80cbde3..a223ef3 100644 --- a/Tests/masTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SoftwareProductSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/OutputListenerSpec.swift b/Tests/masTests/OutputListenerSpec.swift index faef8c7..c5d02a6 100644 --- a/Tests/masTests/OutputListenerSpec.swift +++ b/Tests/masTests/OutputListenerSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class OutputListenerSpec: QuickSpec { - override public func spec() { + override public static func spec() { beforeSuite { Mas.initialize() } From dccac33abb435fea76557c0a1eacfe49dc82f9d5 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:16:06 -0400 Subject: [PATCH 29/81] Improve tests. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 1 + Tests/masTests/Commands/AccountSpec.swift | 6 +-- Tests/masTests/Commands/HomeSpec.swift | 20 ++++----- Tests/masTests/Commands/InfoSpec.swift | 45 +++++++++---------- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 28 +++++------- Tests/masTests/Commands/OutdatedSpec.swift | 3 +- Tests/masTests/Commands/SearchSpec.swift | 14 +++--- Tests/masTests/Commands/SignInSpec.swift | 6 +-- Tests/masTests/Commands/VendorSpec.swift | 20 ++++----- .../Controllers/MasAppLibrarySpec.swift | 5 +-- .../Controllers/MasStoreSearchSpec.swift | 38 +++++++--------- .../Controllers/StoreSearchMock.swift | 4 +- .../OpenSystemCommandMock.swift | 2 +- .../OpenSystemCommandSpec.swift | 6 +-- .../Formatters/AppListFormatterSpec.swift | 10 ++--- .../SearchResultFormatterSpec.swift | 16 +++---- .../Models/SearchResultListSpec.swift | 18 ++++---- Tests/masTests/Models/SearchResultSpec.swift | 11 ++--- .../Network/NetworkSessionMockFromFile.swift | 3 +- 20 files changed, 116 insertions(+), 142 deletions(-) diff --git a/.swiftformat b/.swiftformat index f727bd2..9e09924 100644 --- a/.swiftformat +++ b/.swiftformat @@ -10,6 +10,7 @@ # Disabled rules --disable blankLinesAroundMark --disable consecutiveSpaces +--disable hoistAwait --disable hoistPatternLet --disable hoistTry --disable indent diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 2021577..301f28b 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -18,12 +18,12 @@ public class AccountSpec: QuickSpec { Mas.initialize() } // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues - xdescribe("Account command") { - xit("displays active account") { + describe("Account command") { + it("displays active account") { expect { try Mas.Account.parse([]).run() } - .toNot(throwError()) + .to(throwError(MASError.notSupported)) } } } diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index e366ee4..3e33524 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -13,11 +13,6 @@ import Quick public class HomeSpec: QuickSpec { override public static func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "mas preview url", - version: "0.0" - ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() @@ -41,13 +36,18 @@ public class HomeSpec: QuickSpec { .to(throwError(MASError.noSearchResultsFound)) } it("opens app on MAS Preview") { - storeSearch.apps[result.trackId] = result + let mockResult = SearchResult( + trackId: 1111, + trackViewUrl: "mas preview url", + version: "0.0" + ) + storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Home.parse([String(result.trackId)]).run(storeSearch: storeSearch, openCommand: openCommand) + try Mas.Home.parse([String(mockResult.trackId)]) + .run(storeSearch: storeSearch, openCommand: openCommand) + return openCommand.arguments } - .toNot(throwError()) - expect(openCommand.arguments).toNot(beNil()) - expect(openCommand.arguments!.first!) == result.trackViewUrl + == [mockResult.trackViewUrl] } } } diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index bbedfdd..4e787ba 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -13,27 +13,7 @@ import Quick public class InfoSpec: QuickSpec { override public static func spec() { - let result = SearchResult( - currentVersionReleaseDate: "2019-01-07T18:53:13Z", - fileSizeBytes: "1024", - minimumOsVersion: "10.14", - price: 2.0, - sellerName: "Awesome Dev", - trackId: 1111, - trackName: "Awesome App", - trackViewUrl: "https://awesome.app", - version: "1.0" - ) let storeSearch = StoreSearchMock() - let expectedOutput = """ - Awesome App 1.0 [2.0] - By: Awesome Dev - Released: 2019-01-07 - Minimum OS: 10.14 - Size: 1 KB - From: https://awesome.app - - """ beforeSuite { Mas.initialize() @@ -55,13 +35,32 @@ public class InfoSpec: QuickSpec { .to(throwError(MASError.noSearchResultsFound)) } it("displays app details") { - storeSearch.apps[result.trackId] = result + let mockResult = SearchResult( + currentVersionReleaseDate: "2019-01-07T18:53:13Z", + fileSizeBytes: "1024", + minimumOsVersion: "10.14", + price: 2.0, + sellerName: "Awesome Dev", + trackId: 1111, + trackName: "Awesome App", + trackViewUrl: "https://awesome.app", + version: "1.0" + ) + storeSearch.apps[mockResult.trackId] = mockResult let output = OutputListener() expect { - try Mas.Info.parse([String(result.trackId)]).run(storeSearch: storeSearch) + try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) } .toNot(throwError()) - expect(output.contents) == expectedOutput + expect(output.contents) == """ + Awesome App 1.0 [2.0] + By: Awesome Dev + Released: 2019-01-07 + Minimum OS: 10.14 + Size: 1 KB + From: https://awesome.app + + """ } } } diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index 879c5dd..5a7adb0 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -19,7 +19,7 @@ public class LuckySpec: QuickSpec { beforeSuite { Mas.initialize() } - describe("lucky command") { + xdescribe("lucky command") { xit("installs the first app matching a search") { expect { try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index bedab44..610d91c 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -14,11 +14,6 @@ import Quick public class OpenSpec: QuickSpec { override public static func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "fakescheme://some/url", - version: "0.0" - ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() @@ -42,26 +37,25 @@ public class OpenSpec: QuickSpec { .to(throwError(MASError.noSearchResultsFound)) } it("opens app in MAS") { - storeSearch.apps[result.trackId] = result + let mockResult = SearchResult( + trackId: 1111, + trackViewUrl: "fakescheme://some/url", + version: "0.0" + ) + storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Open.parse([result.trackId.description]) + try Mas.Open.parse([mockResult.trackId.description]) .run(storeSearch: storeSearch, openCommand: openCommand) + return openCommand.arguments } - .toNot(throwError()) - expect(openCommand.arguments).toNot(beNil()) - let url = URL(string: openCommand.arguments!.first!) - expect(url).toNot(beNil()) - expect(url?.scheme) == "macappstore" + == ["macappstore://some/url"] } it("just opens MAS if no app specified") { expect { try Mas.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand) + return openCommand.arguments } - .toNot(throwError()) - expect(openCommand.arguments).toNot(beNil()) - let url = URL(string: openCommand.arguments!.first!) - expect(url).toNot(beNil()) - expect(url) == URL(string: "macappstore://") + == ["macappstore://"] } } } diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 6152645..71ef84b 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -19,8 +19,7 @@ public class OutdatedSpec: QuickSpec { describe("outdated command") { it("displays apps with pending updates") { expect { - try Mas.Outdated.parse(["--verbose"]) - .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + try Mas.Outdated.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index fba2caf..6a1faa7 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -13,12 +13,6 @@ import Quick public class SearchSpec: QuickSpec { override public static func spec() { - let result = SearchResult( - trackId: 1111, - trackName: "slack", - trackViewUrl: "mas preview url", - version: "0.0" - ) let storeSearch = StoreSearchMock() beforeSuite { @@ -29,7 +23,13 @@ public class SearchSpec: QuickSpec { storeSearch.reset() } it("can find slack") { - storeSearch.apps[result.trackId] = result + let mockResult = SearchResult( + trackId: 1111, + trackName: "slack", + trackViewUrl: "mas preview url", + version: "0.0" + ) + storeSearch.apps[mockResult.trackId] = mockResult expect { try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) } diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index 5336c99..dca1e06 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -18,12 +18,12 @@ public class SignInSpec: QuickSpec { Mas.initialize() } // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues - xdescribe("signin command") { - xit("signs in") { + describe("signin command") { + it("signs in") { expect { try Mas.SignIn.parse(["", ""]).run() } - .toNot(throwError()) + .to(throwError(MASError.notSupported)) } } } diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index b5bf8e5..ac7bc84 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -13,11 +13,6 @@ import Quick public class VendorSpec: QuickSpec { override public static func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "https://awesome.app", - version: "0.0" - ) let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() @@ -41,14 +36,19 @@ public class VendorSpec: QuickSpec { .to(throwError(MASError.noSearchResultsFound)) } it("opens vendor app page in browser") { - storeSearch.apps[result.trackId] = result + let mockResult = SearchResult( + sellerUrl: "https://awesome.app", + trackId: 1111, + trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4", + version: "0.0" + ) + storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Vendor.parse([String(result.trackId)]) + try Mas.Vendor.parse([String(mockResult.trackId)]) .run(storeSearch: storeSearch, openCommand: openCommand) + return openCommand.arguments } - .toNot(throwError()) - expect(openCommand.arguments).toNot(beNil()) - expect(openCommand.arguments!.first!) == result.sellerUrl + == [mockResult.sellerUrl] } } } diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift index 6fe8628..4580b0f 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -20,12 +20,11 @@ public class MasAppLibrarySpec: QuickSpec { } describe("mas app library") { it("contains all installed apps") { - expect(library.installedApps.count) == apps.count + expect(library.installedApps).to(haveCount(apps.count)) expect(library.installedApps.first!.appName) == myApp.appName } it("can locate an app by bundle id") { - let app = library.installedApp(forBundleId: "com.example")! - expect(app.bundleIdentifier) == myApp.bundleIdentifier + expect(library.installedApp(forBundleId: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier } } } diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index 938c82c..8d9c6f7 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -19,18 +19,16 @@ public class MasStoreSearchSpec: QuickSpec { describe("url string") { it("contains the app name") { let appName = "myapp" - let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString - expect(urlString) == """ - https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US - """ + expect { + MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString + } + == "https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US" } it("contains the encoded app name") { - let appName = "My App" - let appNameEncoded = "My%20App" - let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString - expect(urlString) == """ - https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appNameEncoded)&country=US - """ + expect { + MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString + } + == "https://itunes.apple.com/search?media=software&entity=macSoftware&term=My%20App&country=US" } } describe("store") { @@ -39,16 +37,10 @@ public class MasStoreSearchSpec: QuickSpec { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - var results: [SearchResult] - do { - results = try storeSearch.search(for: "slack").wait() - expect(results.count) == 39 - } catch { - let maserror = error as! MASError - if case .jsonParsing(let nserror) = maserror { - fail("\(maserror) \(nserror!)") - } + expect { + try storeSearch.search(for: "slack").wait() } + .to(haveCount(39)) } } @@ -58,9 +50,9 @@ public class MasStoreSearchSpec: QuickSpec { let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - var lookup: SearchResult? + var result: SearchResult? do { - lookup = try storeSearch.lookup(appID: appID).wait() + result = try storeSearch.lookup(appID: appID).wait() } catch { let maserror = error as! MASError if case .jsonParsing(let nserror) = maserror { @@ -68,7 +60,9 @@ public class MasStoreSearchSpec: QuickSpec { } } - guard let result = lookup else { fatalError("lookup result was nil") } + guard let result else { + fatalError("lookup result was nil") + } expect(result.trackId) == appID expect(result.bundleId) == "com.tinyspeck.slackmacgap" diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index 3b83df8..f97a1d3 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -14,9 +14,7 @@ class StoreSearchMock: StoreSearch { var apps: [AppID: SearchResult] = [:] func search(for appName: String) -> Promise<[SearchResult]> { - let filtered = apps.filter { $1.trackName.contains(appName) } - let results = filtered.map { $1 } - return .value(results) + .value(apps.filter { $1.trackName.contains(appName) }.map { $1 }) } func lookup(appID: AppID) -> Promise { diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift index 95c409b..08d0f4b 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift @@ -13,7 +13,7 @@ import Foundation class OpenSystemCommandMock: ExternalCommand { // Stub out protocol logic var succeeded = true - var arguments: [String]? + var arguments: [String] = [] // unused var binaryPath = "/dev/null" diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index 41db44a..db3e484 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -19,12 +19,10 @@ public class OpenSystemCommandSpec: QuickSpec { describe("open system command") { context("binary path") { it("defaults to the macOS open command") { - let cmd = OpenSystemCommand() - expect(cmd.binaryPath) == "/usr/bin/open" + expect(OpenSystemCommand().binaryPath) == "/usr/bin/open" } it("can be overridden") { - let cmd = OpenSystemCommand(binaryPath: "/dev/null") - expect(cmd.binaryPath) == "/dev/null" + expect(OpenSystemCommand(binaryPath: "/dev/null").binaryPath) == "/dev/null" } } } diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index ebc9130..a9e23ab 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -25,8 +25,7 @@ public class AppListsFormatterSpec: QuickSpec { products = [] } it("formats nothing as empty string") { - let output = format(products) - expect(output) == "" + expect(format(products)) == "" } it("can format a single product") { let product = SoftwareProductMock( @@ -36,8 +35,7 @@ public class AppListsFormatterSpec: QuickSpec { bundleVersion: "19.2.1", itemIdentifier: 12345 ) - let output = format([product]) - expect(output) == "12345 Awesome App (19.2.1)" + expect(format([product])) == "12345 Awesome App (19.2.1)" } it("can format two products") { products = [ @@ -56,8 +54,8 @@ public class AppListsFormatterSpec: QuickSpec { itemIdentifier: 67890 ), ] - let output = format(products) - expect(output) == "12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)" + expect(format(products)) + == "12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)" } } } diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 368e8e6..29ee188 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -25,8 +25,7 @@ public class SearchResultsFormatterSpec: QuickSpec { results = [] } it("formats nothing as empty string") { - let output = format(results, false) - expect(output) == "" + expect(format(results, false)) == "" } it("can format a single result") { let result = SearchResult( @@ -35,8 +34,7 @@ public class SearchResultsFormatterSpec: QuickSpec { trackName: "Awesome App", version: "19.2.1" ) - let output = format([result], false) - expect(output) == " 12345 Awesome App (19.2.1)" + expect(format([result], false)) == " 12345 Awesome App (19.2.1)" } it("can format a single result with price") { let result = SearchResult( @@ -45,8 +43,7 @@ public class SearchResultsFormatterSpec: QuickSpec { trackName: "Awesome App", version: "19.2.1" ) - let output = format([result], true) - expect(output) == " 12345 Awesome App $ 9.87 (19.2.1)" + expect(format([result], true)) == " 12345 Awesome App $ 9.87 (19.2.1)" } it("can format a two results") { results = [ @@ -63,8 +60,8 @@ public class SearchResultsFormatterSpec: QuickSpec { version: "1.2.0" ), ] - let output = format(results, false) - expect(output) == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" + expect(format(results, false)) + == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" } it("can format a two results with prices") { results = [ @@ -81,8 +78,7 @@ public class SearchResultsFormatterSpec: QuickSpec { version: "1.2.0" ), ] - let output = format(results, true) - expect(output) + expect(format(results, true)) == " 12345 Awesome App $ 9.87 (19.2.1)\n 67890 Even Better App $ 0.01 (1.2.0)" } } diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift index 4f6f3e5..86b2575 100644 --- a/Tests/masTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -19,18 +19,16 @@ public class SearchResultListSpec: QuickSpec { } describe("search result list") { it("can parse bbedit") { - let data = Data(from: "search/bbedit.json") - let decoder = JSONDecoder() - let results = try decoder.decode(SearchResultList.self, from: data) - - expect(results.resultCount) == 1 + expect( + try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/bbedit.json")).resultCount + ) + == 1 } it("can parse things") { - let data = Data(from: "search/things.json") - let decoder = JSONDecoder() - let results = try decoder.decode(SearchResultList.self, from: data) - - expect(results.resultCount) == 50 + expect( + try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/things.json")).resultCount + ) + == 50 } } } diff --git a/Tests/masTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift index b3ddc24..7330da8 100644 --- a/Tests/masTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -19,11 +19,12 @@ public class SearchResultSpec: QuickSpec { } describe("search result") { it("can parse things") { - let data = Data(from: "search/things-that-go-bump.json") - let decoder = JSONDecoder() - let result = try decoder.decode(SearchResult.self, from: data) - - expect(result.bundleId) == "uikitformac.com.tinybop.thingamabops" + expect( + try JSONDecoder() + .decode(SearchResult.self, from: Data(from: "search/things-that-go-bump.json")) + .bundleId + ) + == "uikitformac.com.tinybop.thingamabops" } } } diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift index 46d4ca2..a68220d 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -31,8 +31,7 @@ class NetworkSessionMockFromFile: NetworkSessionMock { else { fatalError("Unable to load file \(responseFile)") } do { - let data = try Data(contentsOf: fileURL, options: .mappedIfSafe) - return .value(data) + return .value(try Data(contentsOf: fileURL, options: .mappedIfSafe)) } catch { print("Error opening file: \(error)") return Promise(error: error) From 265326dedec528fcb504846e7a26b0ddcba63145 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:49:30 -0400 Subject: [PATCH 30/81] =?UTF-8?q?Add=20`captureStream(=E2=80=A6)`=20to=20o?= =?UTF-8?q?bserve=20stdout=20&=20stderr=20in=20tests.?= 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/Formatters/Utilities.swift | 84 +++++++-------------- Tests/masTests/Commands/InfoSpec.swift | 9 ++- Tests/masTests/Commands/ListSpec.swift | 7 +- Tests/masTests/Commands/OutdatedSpec.swift | 34 ++++++++- Tests/masTests/Commands/UninstallSpec.swift | 6 +- Tests/masTests/Commands/UpgradeSpec.swift | 10 ++- Tests/masTests/Commands/VersionSpec.swift | 7 +- Tests/masTests/OutputListener.swift | 28 ------- Tests/masTests/OutputListenerSpec.swift | 39 ---------- Tests/masTests/Strongify.swift | 14 ---- 10 files changed, 85 insertions(+), 153 deletions(-) delete mode 100644 Tests/masTests/OutputListener.swift delete mode 100644 Tests/masTests/OutputListenerSpec.swift delete mode 100644 Tests/masTests/Strongify.swift diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index f168e2c..c5c940e 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -13,63 +13,6 @@ import Foundation /// Terminal Control Sequence Indicator let csi = "\u{001B}[" -#if DEBUG - - var printObserver: ((String) -> Void)? - - // Override global print for testability. - // See masTests/OutputListener.swift. - func print( - _ items: Any..., - separator: String = " ", - terminator: String = "\n" - ) { - if let observer = printObserver { - let output = - items - .map { "\($0)" } - .joined(separator: separator) - .appending(terminator) - observer(output) - } - - var prefix = "" - for item in items { - Swift.print(prefix, terminator: "") - Swift.print(item, terminator: "") - prefix = separator - } - - Swift.print(terminator, terminator: "") - } - - func print( - _ items: Any..., - separator: String = " ", - terminator: String = "\n", - to output: inout some TextOutputStream - ) { - if let observer = printObserver { - let output = - items - .map { "\($0)" } - .joined(separator: separator) - .appending(terminator) - observer(output) - } - - var prefix = "" - for item in items { - Swift.print(prefix, terminator: "", to: &output) - Swift.print(item, terminator: "", to: &output) - prefix = separator - } - - Swift.print(terminator, terminator: "", to: &output) - } - -#endif - private var standardError = FileHandle.standardError extension FileHandle: TextOutputStream { @@ -121,3 +64,30 @@ func clearLine() { print("\(csi)2K\(csi)0G", terminator: "") fflush(stdout) } + +func captureStream( + _ stream: UnsafeMutablePointer, + encoding: String.Encoding = .utf8, + _ block: @escaping () throws -> Void +) throws -> String { + let originalFd = fileno(stream) + let duplicateFd = dup(originalFd) + defer { + close(duplicateFd) + } + + let pipe = Pipe() + dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd) + + do { + defer { + fflush(stream) + dup2(duplicateFd, originalFd) + pipe.fileHandleForWriting.closeFile() + } + + try block() + } + + return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? "" +} diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 4e787ba..35ac88a 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -47,12 +48,12 @@ public class InfoSpec: QuickSpec { version: "1.0" ) storeSearch.apps[mockResult.trackId] = mockResult - let output = OutputListener() expect { - try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) + try captureStream(stdout) { + try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) + } } - .toNot(throwError()) - expect(output.contents) == """ + == """ Awesome App 1.0 [2.0] By: Awesome Dev Released: 2019-01-07 diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index 5cb373d..811ed7f 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -19,9 +20,11 @@ public class ListSpec: QuickSpec { describe("list command") { it("lists apps") { expect { - try Mas.List.parse([]).run(appLibrary: AppLibraryMock()) + try captureStream(stderr) { + try Mas.List.parse([]).run(appLibrary: AppLibraryMock()) + } } - .toNot(throwError()) + == "Error: No installed apps found\n" } } } diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 71ef84b..f1890de 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -18,10 +19,39 @@ public class OutdatedSpec: QuickSpec { } describe("outdated command") { it("displays apps with pending updates") { + let mockSearchResult = + SearchResult( + bundleId: "au.id.haroldchu.mac.Bandwidth", + currentVersionReleaseDate: "2024-09-02T00:27:00Z", + fileSizeBytes: "998130", + minimumOsVersion: "10.13", + price: 0, + sellerName: "Harold Chu", + sellerUrl: "https://example.com", + trackId: 490_461_369, + trackName: "Bandwidth+", + trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4", + version: "1.28" + ) + let mockStoreSearch = StoreSearchMock() + mockStoreSearch.apps[mockSearchResult.trackId] = mockSearchResult + + let mockAppLibrary = AppLibraryMock() + mockAppLibrary.installedApps.append( + SoftwareProductMock( + appName: mockSearchResult.trackName, + bundleIdentifier: mockSearchResult.bundleId, + bundlePath: "/Applications/Bandwidth+.app", + bundleVersion: "1.27", + itemIdentifier: NSNumber(value: mockSearchResult.trackId) + ) + ) expect { - try Mas.Outdated.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + try captureStream(stdout) { + try Mas.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch) + } } - .toNot(throwError()) + == "490461369 Bandwidth+ (1.27 -> 1.28)\n" } } } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 072473e..d650044 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -63,9 +63,11 @@ public class UninstallSpec: QuickSpec { it("removes an app") { mockLibrary.installedApps.append(app) expect { - try uninstall.run(appLibrary: mockLibrary) + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } } - .toNot(throwError()) + == " 1111 slack (0.0)\n==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" } it("fails if there is a problem with the trash command") { var brokenUninstall = app // make mutable copy diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index f22d450..e5a8406 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -17,11 +18,14 @@ public class UpgradeSpec: QuickSpec { Mas.initialize() } describe("upgrade command") { - it("upgrades stuff") { + it("finds no upgrades") { expect { - try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + try captureStream(stderr) { + try Mas.Upgrade.parse([]) + .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + } } - .toNot(throwError()) + == "Warning: Nothing found to upgrade\n" } } } diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index 46c2baa..ec08193 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -19,9 +20,11 @@ public class VersionSpec: QuickSpec { describe("version command") { it("displays the current version") { expect { - try Mas.Version.parse([]).run() + try captureStream(stdout) { + try Mas.Version.parse([]).run() + } } - .toNot(throwError()) + == Package.version + "\n" } } } diff --git a/Tests/masTests/OutputListener.swift b/Tests/masTests/OutputListener.swift deleted file mode 100644 index 14298f6..0000000 --- a/Tests/masTests/OutputListener.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// OutputListener.swift -// masTests -// -// Created by Ben Chatelain on 1/7/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -@testable import mas - -/// Test helper for monitoring strings written to stdout. Modified from: -/// https://stackoverflow.com/a/53569018 -class OutputListener { - /// Buffers strings written to stdout - var contents = "" - - init() { - printObserver = { [weak self] text in - strongify(self) { context in - context.contents += text - } - } - } - - deinit { - printObserver = nil - } -} diff --git a/Tests/masTests/OutputListenerSpec.swift b/Tests/masTests/OutputListenerSpec.swift deleted file mode 100644 index c5d02a6..0000000 --- a/Tests/masTests/OutputListenerSpec.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// OutputListenerSpec.swift -// masTests -// -// Created by Ben Chatelain on 1/8/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import mas - -public class OutputListenerSpec: QuickSpec { - override public static func spec() { - beforeSuite { - Mas.initialize() - } - describe("output listener") { - it("can intercept a single line written stdout") { - let output = OutputListener() - - print("hi there", terminator: "") - - expect(output.contents) == "hi there" - } - it("can intercept multiple lines written stdout") { - let output = OutputListener() - - print("hi there") - - expect(output.contents) == """ - hi there - - """ - } - } - } -} diff --git a/Tests/masTests/Strongify.swift b/Tests/masTests/Strongify.swift deleted file mode 100644 index a87ce9a..0000000 --- a/Tests/masTests/Strongify.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Strongify.swift -// masTests -// -// Created by Ben Chatelain on 1/8/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -// https://medium.com/@merowing_/stop-weak-strong-dance-in-swift-3aec6d3563d4 - -func strongify(_ context: Context?, closure: (Context) -> Void) { - guard let strongContext = context else { return } - closure(strongContext) -} From 6489daa0e34b718ce887fef0d2f4425440407abd Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:12:39 -0400 Subject: [PATCH 31/81] Update iTunes Search API documentation URL. Partial #561 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Home.swift | 2 +- Sources/mas/Commands/Info.swift | 2 +- Sources/mas/Commands/Open.swift | 2 +- Sources/mas/Commands/Search.swift | 2 +- Sources/mas/Commands/Vendor.swift | 2 +- Sources/mas/Controllers/MasStoreSearch.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index be04111..18a7a53 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -10,7 +10,7 @@ import ArgumentParser extension Mas { /// Opens app page on MAS Preview. Uses the iTunes Lookup API: - /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + /// https://performance-partners.apple.com/search-api struct Home: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Opens MAS Preview app page in a browser" diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index 0e6b71d..ca862d2 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -11,7 +11,7 @@ import Foundation extension Mas { /// Displays app details. Uses the iTunes Lookup API: - /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + /// https://performance-partners.apple.com/search-api struct Info: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Display app information from the Mac App Store" diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index e2a143f..4cc0c99 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -13,7 +13,7 @@ private let masScheme = "macappstore" extension Mas { /// Opens app page in MAS app. Uses the iTunes Lookup API: - /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + /// https://performance-partners.apple.com/search-api struct Open: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Opens app page in AppStore.app" diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index ec9e344..036c61c 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -10,7 +10,7 @@ import ArgumentParser extension Mas { /// Search the Mac App Store using the iTunes Search API: - /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ + /// https://performance-partners.apple.com/search-api struct Search: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Search for apps from the Mac App Store" diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index 546865e..bdf13b1 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -10,7 +10,7 @@ import ArgumentParser extension Mas { /// Opens vendor's app page in a browser. Uses the iTunes Lookup API: - /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup + /// https://performance-partners.apple.com/search-api struct Vendor: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Opens vendor's app page in a browser" diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index b921d1e..f0f6481 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -19,7 +19,7 @@ class MasStoreSearch: StoreSearch { // into the App Store. Instead, we'll make an educated guess that it matches the currently // selected locale in macOS. This obviously isn't always going to match, but it's probably // better than passing no "country" at all to the iTunes Search API. - // https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ + // https://performance-partners.apple.com/search-api private let country: String? private let networkManager: NetworkManager From 6c5a277ad9003d538f01dcc8a8211d495a655a3e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:24:39 -0400 Subject: [PATCH 32/81] Add & use `Entity.desktopSoftware` instead of `macSoftware`. `desktopSoftware` returns info (including version & description) that aligns with what is displayed earlier in the App Store, while `macSoftware` is at least sometimes out of alignment. Partial #561 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/MasStoreSearch.swift | 2 +- Sources/mas/Controllers/StoreSearch.swift | 7 ++++++- Tests/masTests/Controllers/MasStoreSearchSpec.swift | 7 +++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index f0f6481..9cf2405 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -40,7 +40,7 @@ class MasStoreSearch: StoreSearch { func search(for appName: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. - var entities = [Entity.macSoftware] + var entities = [Entity.desktopSoftware] if SysCtlSystemCommand.isAppleSilicon { entities += [.iPadSoftware, .iPhoneSoftware] } diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index b04ef13..62775cc 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -16,6 +16,7 @@ protocol StoreSearch { } enum Entity: String { + case desktopSoftware case macSoftware case iPadSoftware case iPhoneSoftware = "software" @@ -27,7 +28,11 @@ extension StoreSearch { /// /// - Parameter appName: MAS app identifier. /// - Returns: URL for the search service or nil if appName can't be encoded. - func searchURL(for appName: String, inCountry country: String?, ofEntity entity: Entity = .macSoftware) -> URL? { + func searchURL( + for appName: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { guard var components = URLComponents(string: "https://itunes.apple.com/search") else { return nil } diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index 8d9c6f7..c85aea1 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -18,17 +18,16 @@ public class MasStoreSearchSpec: QuickSpec { } describe("url string") { it("contains the app name") { - let appName = "myapp" expect { - MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString + MasStoreSearch().searchURL(for: "myapp", inCountry: "US")?.absoluteString } - == "https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US" + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&term=myapp&country=US" } it("contains the encoded app name") { expect { MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString } - == "https://itunes.apple.com/search?media=software&entity=macSoftware&term=My%20App&country=US" + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&term=My%20App&country=US" } } describe("store") { From 280b38dfe8fda4edd369a325e0eaf41b90c49b03 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:29:47 -0400 Subject: [PATCH 33/81] Add `media=software` query item to lookup URL to improve results. Reorder query items for both lookup URLs & search URLs. Partial #561 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/StoreSearch.swift | 7 +++++-- Tests/masTests/Controllers/MasStoreSearchSpec.swift | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index 62775cc..a0de0ef 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -40,13 +40,14 @@ extension StoreSearch { components.queryItems = [ URLQueryItem(name: "media", value: "software"), URLQueryItem(name: "entity", value: entity.rawValue), - URLQueryItem(name: "term", value: appName), ] if let country { components.queryItems!.append(URLQueryItem(name: "country", value: country)) } + components.queryItems!.append(URLQueryItem(name: "term", value: appName)) + return components.url } @@ -60,7 +61,7 @@ extension StoreSearch { } components.queryItems = [ - URLQueryItem(name: "id", value: "\(appID)"), + URLQueryItem(name: "media", value: "software"), URLQueryItem(name: "entity", value: "desktopSoftware"), ] @@ -68,6 +69,8 @@ extension StoreSearch { components.queryItems!.append(URLQueryItem(name: "country", value: country)) } + components.queryItems!.append(URLQueryItem(name: "id", value: "\(appID)")) + return components.url } } diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index c85aea1..9a3b7ff 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -21,13 +21,13 @@ public class MasStoreSearchSpec: QuickSpec { expect { MasStoreSearch().searchURL(for: "myapp", inCountry: "US")?.absoluteString } - == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&term=myapp&country=US" + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp" } it("contains the encoded app name") { expect { MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString } - == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&term=My%20App&country=US" + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App" } } describe("store") { From a44655ac31dde8d4b617d6cef000624a93db5691 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:44:56 -0400 Subject: [PATCH 34/81] =?UTF-8?q?Refactor=20`StoreSearch.lookup(=E2=80=A6)?= =?UTF-8?q?`=20&=20`StoreSearch.search(=E2=80=A6)`=20to=20share=20implemen?= =?UTF-8?q?tation,=20which=20allows=20`lookup`=20to=20accept=20an=20`Entit?= =?UTF-8?q?y`=20argument=20instead=20of=20being=20hardcoded=20to=20`deskto?= =?UTF-8?q?pSoftware`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #561 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/StoreSearch.swift | 68 +++++++++++++---------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index a0de0ef..e906cd9 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -22,18 +22,53 @@ enum Entity: String { case iPhoneSoftware = "software" } +private enum URLAction { + case lookup + case search + + var queryItemName: String { + switch self { + case .lookup: + return "id" + case .search: + return "term" + } + } +} + // MARK: - Common methods extension StoreSearch { /// Builds the search URL for an app. /// - /// - Parameter appName: MAS app identifier. - /// - Returns: URL for the search service or nil if appName can't be encoded. + /// - Parameter searchTerm: term for which to search in MAS. + /// - Returns: URL for the search service or nil if searchTerm can't be encoded. func searchURL( - for appName: String, + for searchTerm: String, inCountry country: String?, ofEntity entity: Entity = .desktopSoftware ) -> URL? { - guard var components = URLComponents(string: "https://itunes.apple.com/search") else { + url(.search, searchTerm, inCountry: country, ofEntity: entity) + } + + /// Builds the lookup URL for an app. + /// + /// - Parameter appID: MAS app identifier. + /// - Returns: URL for the lookup service or nil if appID can't be encoded. + func lookupURL( + forAppID appID: AppID, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + url(.lookup, String(appID), inCountry: country, ofEntity: entity) + } + + private func url( + _ action: URLAction, + _ queryItemValue: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else { return nil } @@ -46,30 +81,7 @@ extension StoreSearch { components.queryItems!.append(URLQueryItem(name: "country", value: country)) } - components.queryItems!.append(URLQueryItem(name: "term", value: appName)) - - return components.url - } - - /// Builds the lookup URL for an app. - /// - /// - Parameter appID: MAS app identifier. - /// - Returns: URL for the lookup service or nil if appID can't be encoded. - func lookupURL(forAppID appID: AppID, inCountry country: String?) -> URL? { - guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { - return nil - } - - components.queryItems = [ - URLQueryItem(name: "media", value: "software"), - URLQueryItem(name: "entity", value: "desktopSoftware"), - ] - - if let country { - components.queryItems!.append(URLQueryItem(name: "country", value: country)) - } - - components.queryItems!.append(URLQueryItem(name: "id", value: "\(appID)")) + components.queryItems!.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) return components.url } From 20c08eb8557777b750709d30211a5f4b0af5afd3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 15 Oct 2024 03:30:08 -0400 Subject: [PATCH 35/81] Update `README.md`. Resolve #559 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- README.md | 303 ++++++++++++++-------- Sources/mas/Errors/MASError.swift | 2 +- Tests/masTests/Commands/AccountSpec.swift | 2 +- Tests/masTests/Commands/SignInSpec.swift | 2 +- 4 files changed, 196 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 8b76753..5f8c452 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -

mas-cli

+

mas-cli

-# mas-cli +# mas -A simple command line interface for the Mac App Store. Designed for scripting and automation. +A command-line interface for the Mac App Store. Designed for scripting and automation. -[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/mas-cli/mas/blob/main/LICENSE) -[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org) [![GitHub Release](https://img.shields.io/github/release/mas-cli/mas.svg)](https://github.com/mas-cli/mas/releases) +[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)]( + https://github.com/mas-cli/mas/blob/main/LICENSE +) +[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) -[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain) +[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)]( + https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain +) -## 📲 Install +## 📲 Installation ### 🍺 Homebrew @@ -20,20 +24,24 @@ A simple command line interface for the Mac App Store. Designed for scripting an brew install mas ``` -### MacPorts +⚠️ macOS 10.15 (Catalina) or newer is required to install mas from the Homebrew core formula. -[MacPorts](https://www.macports.org/install.php) works as well: +### 🔌 MacPorts + +[MacPorts](https://www.macports.org/install.php) is an alternative way to install: ```bash sudo port install mas ``` -⚠️ Note that macOS 10.15 (Catalina) is required to install mas from MacPorts or the core Homebrew formula. +⚠️ macOS 10.15 (Catalina) or newer is required to install mas from MacPorts. ### ☎️ Older macOS Versions +#### 🍻 Custom Homebrew tap + We provide a [custom Homebrew tap](https://github.com/mas-cli/homebrew-tap) with pre-built bottles -for all macOS versions since 10.11. +for all macOS versions since 10.11 (El Capitan). To install mas from our tap: @@ -41,38 +49,48 @@ To install mas from our tap: brew install mas-cli/tap/mas ``` -#### Swift 5 Runtime Support +#### 🐙 GitHub Releases -mas requires Swift 5 runtime support. macOS 10.14.4 and later include it, but earlier releases did not. -Without it, running `mas` may report an error similar to this: +Alternatively, binaries and sources are available from the [GitHub Releases](https://github.com/mas-cli/mas/releases). + +#### 🕊 Swift 5 Runtime Support + +mas requires Swift 5 runtime support. macOS 10.14.4 (Mojave) and newer include it, but earlier releases do not. +Without it, running mas might report errors similar to: > dyld: Symbol not found: _$s11SubSequenceSlTl -To get Swift 5 support, you have a few options: +To get Swift 5 support on macOS versions older than 10.14.4 (Mojave), you can: +- Upgrade to macOS 10.14.4 (Mojave) or newer. - Install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998). -- Update to macOS 10.14.4 or later. -- Install Xcode 10.2 or later to `/Applications/Xcode.app`. - -### 🐙 GitHub Releases - -Alternatively, binaries are available in the [GitHub Releases](https://github.com/mas-cli/mas/releases). +- Install Xcode 10.2 or newer to `/Applications/Xcode.app`. ## 🤳🏻 Usage -Each application in the Mac App Store has a product identifier which is also -used for mas-cli commands. Using `mas list` will show all installed -applications and their product identifiers. +### 🪪 App IDs -```bash -$ mas list -446107677 Screens -407963104 Pixelmator -497799835 Xcode -``` +Each application in the Mac App Store has an integer app identifier (app ID). +mas commands accept app IDs as arguments and output App IDs to uniquely identify apps. -It is possible to search for applications by name using `mas search` which -will search the Mac App Store and return matching identifiers. -Include the `--price` flag to include prices in the result. +`mas search` and `mas list` can be used to find the app IDs of relevant apps. + +Alternatively, to find an app's app ID: + +1. Find the app in the Mac App Store +2. Select `Share` > `Copy Link` +3. Extract the app ID from the URL. e.g., the Mac App Store URL for Xcode, + [https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12), + has app ID `497799835` + +### 🛍 Info from the Mac App Store + +None of the commands in this section require you to be logged into an Apple ID, +neither for your macOS user, nor in the Mac App Store. + +#### `mas search` + +`mas search ` searches by name for applications available from the Mac App Store. +Providing the `--price` flag includes each app's price in the output. ```bash $ mas search Xcode @@ -82,91 +100,139 @@ $ mas search Xcode [...] ``` -Another way to find the identifier for an app is to +#### `mas info` -1. Find the app in the Mac App Store -2. Select `Share` > `Copy Link` -3. Grab the identifier from the string, e.g. for Xcode, - [https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12) - has identifier `497799835` - -To install or update an application simply run `mas install` with an -application identifier: +`mas info ` displays more detailed information about an application available from the Mac App Store. ```bash -$ mas install 808809998 -==> Downloading PaintCode 2 -==> Installed PaintCode 2 +$ mas info 497799835 +Xcode 16.0 [0.0] +By: Apple Inc. +Released: 2024-09-16 +Minimum OS: 14.5 +Size: 2.98 GB +From: https://apps.apple.com/us/app/xcode/id497799835?mt=12&uo=4 ``` -If you want to install the first result that the `search` command returns, use the `lucky` command. +### 📚 Info from Your Local App Library + +All the commands in this section require you to be logged into an Apple ID for your macOS user. + +#### `mas list` + +`mas list` displays all the applications on your Mac that were installed from the Mac App Store. ```bash -$ mas lucky twitter -==> Downloading Twitter -==> Installed Twitter +$ mas list +497799835 Xcode (15.4) +640199958 Developer (10.6.5) +899247664 TestFlight (3.5.2) ``` -> Please note that this command will not allow you to install (or even purchase) an app for the first time: -use the `purchase` command in that case. +#### `mas outdated` -```bash -$ mas purchase 768053424 -==> Downloading Gapplin -==> Installed Gapplin -``` - -> Please note that you may have to re-authenticate yourself in the App Store to complete the purchase. -This is the case if the application is not free or if you configured your account not to remember the -credentials for free purchases. - -Use `mas outdated` to list all applications with pending updates. +`mas outdated` displays all applications installed from the Mac App Store on your computer that have pending upgrades. ```bash $ mas outdated -497799835 Xcode (7.0) -446107677 Screens VNC - Access Your Computer From Anywhere (3.6.7) +497799835 Xcode (15.4 -> 16.0) +640199958 Developer (10.6.5 -> 10.6.6) ``` -> `mas` is only able to install/update applications that are listed in the Mac App Store itself. -Use [`softwareupdate(8)`] utility for downloading system updates (e.g. Xcode Command Line Tools) +Run [`mas upgrade`](#mas-upgrade) to install pending upgrades. -To install all pending updates run `mas upgrade`. +### ⬇️ Installing Apps + +All the commands in this section require you to be logged into an Apple ID in the Mac App Store. + +> Depending on your Apple ID settings, you might need to re-authenticate yourself in the Mac App Store to perform a +> purchase, install, lucky, or upgrade, even if you are already signed in to an Apple ID in the Mac App Store. + +#### `mas purchase` + +`mas purchase …` installs free applications that you haven't yet gotten/"purchased" from the Mac App Store. + +> `purchase` is currently a misnomer, because it currently can only "purchase" free +> apps. To purchase apps that cost money, please purchase them directly in the Mac App Store. + +```bash +$ mas purchase 497799835 +==> Downloading Xcode +==> Installed Xcode +``` + +#### `mas install` + +`mas install …` installs apps that you have already gotten/"purchased" from the Mac App Store. +Providing the `--force` flag re-installs the app even if it is already installed on your computer. + +```bash +$ mas install 497799835 +==> Downloading Xcode +==> Installed Xcode +``` + +#### `mas lucky` + +`mas lucky ` installs the first result that would be returned by `mas search `. +Like `mas install`, `mas lucky` can only install apps that have previously been gotten/"purchased". + +```bash +$ mas lucky Xcode +==> Downloading Xcode +==> Installed Xcode +``` + +### 🆕 Upgrading Apps + +All the commands in this section require you to be logged into an Apple ID in the Mac App Store. + +> mas only installs/upgrades applications from the Mac App Store. +Use [`softwareupdate(8)`] to install system updates (e.g., Xcode Command Line Tools, Safari, etc.) + +#### `mas upgrade` + +`mas upgrade` upgrades outdated apps installed from the Mac App Store. Without any arguments, it upgrades all such apps. ```bash $ mas upgrade Upgrading 2 outdated applications: -Xcode (7.0), Screens VNC - Access Your Computer From Anywhere (3.6.7) +Xcode (15.4) -> (16.0) +Developer (10.6.5) -> (10.6.6) ==> Downloading Xcode ==> Installed Xcode -==> Downloading iFlicks -==> Installed iFlicks +==> Downloading Developer +==> Installed Developer ``` -Updates can be performed selectively by providing the app identifier(s) to -`mas upgrade` +Upgrades can be performed selectively by providing app IDs to `mas upgrade`. ```bash $ mas upgrade 715768417 Upgrading 1 outdated application: -Xcode (8.0) +Xcode (15.4) -> (16.0) ==> Downloading Xcode ==> Installed Xcode ``` -### 🚏📥 Sign-in +### Mac App Store Account Management -> ⛔ The `signin` command is not supported as of macOS 10.13 High Sierra. Please see [Known Issues](#%EF%B8%8F-known-issues). +All the commands in this section interact with the Apple ID for which you are signed in in the Mac App Store. +These commands do not interact with the Apple ID for which your macOS user is signed in. -To sign into the Mac App Store for the first time run `mas signin`. +#### `mas signin` + +> ⛔ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. On those macOS versions, please +> sign in via the Mac App Store instead. Please see [Known Issues](#known-issues). + +On macOS 10.12 (Sierra) or older, `mas signin ` signs in to the specified Apple ID in the Mac App Store. ```bash $ mas signin mas@example.com Password: ``` -If you experience issues signing in this way, you can ask to sign in using a graphical dialog -(provided by Mac App Store application): +Providing the `--dialog` flag signs in using a graphical dialog provided by Mac App Store. ```bash mas signin --dialog mas@example.com @@ -175,53 +241,59 @@ mas signin --dialog mas@example.com You can also embed your password in the command. ```bash -mas signin mas@example.com 'ZdkM4f$gzF;gX3ABXNLf8KcCt.x.np' +mas signin mas@example.com MyPassword ``` -Use `mas signout` to sign out from the Mac App Store. +#### `mas signout` + +`mas signout` signs out from the current Apple ID in the Mac App Store. ## 🍺 Homebrew integration -`mas` is integrated with [homebrew-bundle]. If `mas` is installed, and you run `brew bundle dump`, -then your Mac App Store apps will be included in the Brewfile created. See the [homebrew-bundle] +mas integrates with [homebrew-bundle]. If mas is installed, when you run `brew bundle dump`, +your Mac App Store apps will be included in the created Brewfile. See the [homebrew-bundle] docs for more details. -## ⚠️ Known Issues + +## ⚠️ Known Issues -Over time, Apple has changed the APIs used by `mas` to manage App Store apps, limiting its capabilities. Please sign in -using the App Store app instead. Subsequent redownloads can be performed with `mas install`. +### 💥 Changed Apple Private Frameworks -- ⛔️ The `signin` command is not supported as of macOS 10.13 High Sierra. [#164](https://github.com/mas-cli/mas/issues/164) -- ⛔️ The `account` command is not supported as of macOS 12 Monterey. [#417](https://github.com/mas-cli/mas/issues/417) +mas uses multiple undocumented Apple private frameworks to implement much of its functionality. +Over time, Apple has silently changed these frameworks, breaking some functionality. Known issues include: -The versions `mas` sees from the app bundles on your Mac don't always match the versions reported by the App Store for -the same app bundles. This leads to some confusion when the `outdated` and `upgrade` commands differ in behavior from -what is shown as outdated in the App Store app. Further confusing matters, there is often some delay due to CDN -propagation and caching between the time a new app version is released to the App Store, and the time it appears -available in the App Store app or via the `mas` command. These issues cause symptoms like +- ⛔️ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. [#164]( + https://github.com/mas-cli/mas/issues/164 + ) +- ⛔️ The `account` command is not supported on macOS 12 (Monterey) or newer. [#417]( + https://github.com/mas-cli/mas/issues/417 + ) + +### 👀 Version Consistency + +mas might be using suboptimal app version sources to compare local app versions with Mac App Store app versions. +That current sources are frequently consistent with the Mac App Store, but are infrequently inconsistent. +This might cause symptoms like [#384](https://github.com/mas-cli/mas/issues/384) and +[#387](https://github.com/mas-cli/mas/issues/387). mas will be updated soon to fix any such problems, if possible. + +### ⏳ Eventual Consistency + +The Mac App Store operates on eventual consistency, so the versions seen by various parts of mas or the Mac App Store +might be inconsistent for some period of time. This might cause symptoms like [#384](https://github.com/mas-cli/mas/issues/384) and [#387](https://github.com/mas-cli/mas/issues/387). -Macs with Apple silicon can install and run iOS and iPadOS apps from the App Store. `mas` is not yet aware of these -apps, and is not yet able to install or update them. [#321](https://github.com/mas-cli/mas/issues/321) +### 📱 iOS and iPadOS Apps -## 💥 When something doesn't work +Macs with Apple Silicon can install and run iOS and iPadOS apps from the Mac App Store. mas is not yet aware of these +apps, and is not yet able to install or upgrade them. [#321](https://github.com/mas-cli/mas/issues/321) -If you see this error, it's probably because you haven't installed the app through the App Store yet. -See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233). -> This redownload is not available for this Apple ID either because it was bought by a different user or the -> item was refunded or cancelled. +### 📺 Using `tmux` -If `mas` doesn't work for you as expected (e.g. you can't update/download apps), run `mas reset` and try again. -If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new). -All your feedback is much appreciated! ✨ - -## 📺 Using `tmux` - -`mas` operates via the same system services as the Mac App Store. These exist as -separate processes with communication through XPC. As a result of this, `mas` +mas operates via the same system services as the Mac App Store. These exist as +separate processes with communication through XPC. As a result of this, mas experiences similar problems as the pasteboard when running inside `tmux`. A [wrapper tool exists](https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard) to -fix pasteboard behaviour which also works for `mas`. +fix pasteboard behaviour which also works for mas. You should consider configuring `tmux` to use the wrapper but if you do not wish to do this it can be used on a one-off basis as follows: @@ -231,9 +303,20 @@ brew install reattach-to-user-namespace reattach-to-user-namespace mas install ``` +## 🚫 When something doesn't work + +If you see the following error, it's probably because you haven't yet "purchased" the app through the Mac App Store. +See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233). +> This redownload is not available for this Apple ID either because it was bought by a different user or the +> item was refunded or cancelled. + +If mas doesn't work for you as expected (e.g. you can't install/upgrade apps), run `mas reset`, then try again. +If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new). +All feedback is much appreciated! ✨ + ## ℹ️ Build from source -You can build from Xcode by opening the root `mas` directory, or from the Terminal: +You can build from Xcode by opening the root mas directory, or from the Terminal: ```bash script/bootstrap @@ -255,7 +338,7 @@ script/test ## 📄 License -mas-cli was created by [@argon](https://github.com/argon). +mas was created by [@argon](https://github.com/argon). Code is under the [MIT license](LICENSE). [homebrew-bundle]: https://github.com/Homebrew/homebrew-bundle diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index c799352..4d4155a 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -46,7 +46,7 @@ extension MASError: CustomStringConvertible { return """ This command is not supported on this macOS version due to changes in macOS. \ For more information see: \ - https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + https://github.com/mas-cli/mas#known-issues """ case .failed(let error): if let error { diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 301f28b..23dd488 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -17,7 +17,7 @@ public class AccountSpec: QuickSpec { beforeSuite { Mas.initialize() } - // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#known-issues describe("Account command") { it("displays active account") { expect { diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index dca1e06..20c0f96 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -17,7 +17,7 @@ public class SignInSpec: QuickSpec { beforeSuite { Mas.initialize() } - // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#known-issues describe("signin command") { it("signs in") { expect { From b0d0f4aacdf12bf73c3b84e8229152840b3dfa60 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:53:26 -0400 Subject: [PATCH 36/81] `appIDValue` to encapsulate `uint64Value`. Resolve #574 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Upgrade.swift | 4 ++-- Sources/mas/Mas.swift | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 52b12ba..f7b3ce5 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -33,7 +33,7 @@ extension Mas { fulfilled: appLibrary.installedApps.map { installedApp in firstly { - storeSearch.lookup(appID: installedApp.itemIdentifier.uint64Value) + storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) }.done { storeApp in guard let storeApp else { if verbose { diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index afa1b5d..cfc7b66 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -44,7 +44,7 @@ extension Mas { .joined(separator: "\n")) do { - try downloadAll(apps.map(\.installedApp.itemIdentifier.uint64Value)).wait() + try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait() } catch { throw error as? MASError ?? .downloadFailed(error: error as NSError) } @@ -70,7 +70,7 @@ extension Mas { let promises = apps.map { installedApp in // only upgrade apps whose local version differs from the store version firstly { - storeSearch.lookup(appID: installedApp.itemIdentifier.uint64Value) + storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) }.map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { return nil diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index a3a28ec..1d273fa 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -7,10 +7,9 @@ // import ArgumentParser +import Foundation import PromiseKit -typealias AppID = UInt64 - @main struct Mas: ParsableCommand { static let configuration = CommandConfiguration( @@ -56,3 +55,11 @@ struct Mas: ParsableCommand { } } } + +typealias AppID = UInt64 + +extension NSNumber { + var appIDValue: AppID { + uint64Value + } +} From bc961cf0d917121948e7c3c00f7efeb08e5c74fc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:55:34 -0400 Subject: [PATCH 37/81] Add `trash` to `Brewfile`. Partial #576 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Brewfile | 1 + Brewfile.lock.json | 291 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 223 insertions(+), 69 deletions(-) diff --git a/Brewfile b/Brewfile index 9362a2f..8d4cb99 100644 --- a/Brewfile +++ b/Brewfile @@ -3,6 +3,7 @@ brew "shellcheck" brew "shfmt" brew "swift-format" brew "swiftformat" +brew "trash" # Already installed on GitHub Actions runner. # brew "swiftlint" diff --git a/Brewfile.lock.json b/Brewfile.lock.json index 0858043..64c9cdf 100644 --- a/Brewfile.lock.json +++ b/Brewfile.lock.json @@ -2,132 +2,285 @@ "entries": { "brew": { "markdownlint-cli": { - "version": "0.32.2", + "version": "0.42.0", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { - "all": { + "arm64_sequoia": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7be52e16473a658becde9b817f86c868bcb9e41e79856d9dce542218b9515860", - "sha256": "7be52e16473a658becde9b817f86c868bcb9e41e79856d9dce542218b9515860" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" + }, + "sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5", + "sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5", + "sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5" + }, + "x86_64_linux": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" } } } }, "shellcheck": { - "version": "0.8.0", + "version": "0.10.0", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364", + "sha256": "5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:ef742b6992cfcdcd7289718ac64b27174e421d29ce3ad9b81e1856349059b117", + "sha256": "ef742b6992cfcdcd7289718ac64b27174e421d29ce3ad9b81e1856349059b117" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:6e60ee03edb09ac5bc852b8eb813849fa654400e21ffb4c746989678172f5a26", + "sha256": "6e60ee03edb09ac5bc852b8eb813849fa654400e21ffb4c746989678172f5a26" + }, "arm64_monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:625466bcd245a36da12ee088877d582c7e9fec1622418d1165a7d7d8f204ecc3", - "sha256": "625466bcd245a36da12ee088877d582c7e9fec1622418d1165a7d7d8f204ecc3" + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:d5e8407806dbf757e71930ce2cb9b0d23bae286f0c058d9ff246d851dd7aa871", + "sha256": "d5e8407806dbf757e71930ce2cb9b0d23bae286f0c058d9ff246d851dd7aa871" }, - "arm64_big_sur": { + "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:883ba5ee45554568cd1ce106dc6c090ec0745f576a4a6708332de951b03c7423", - "sha256": "883ba5ee45554568cd1ce106dc6c090ec0745f576a4a6708332de951b03c7423" + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:b53cf1e5464406ee49743fc2db84850b6d34d3a2098cf729e629b23f9d6dd6e0", + "sha256": "b53cf1e5464406ee49743fc2db84850b6d34d3a2098cf729e629b23f9d6dd6e0" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:15ba88c48a5ae3b08e085791e3c5e514d9d78ce88414c96bd21ed33f29fb4aca", + "sha256": "15ba88c48a5ae3b08e085791e3c5e514d9d78ce88414c96bd21ed33f29fb4aca" }, "monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:cfd8c8e8d8927dfd4b83593f539690a6083b075b0a1ff8a66578e8bb810d3db9", - "sha256": "cfd8c8e8d8927dfd4b83593f539690a6083b075b0a1ff8a66578e8bb810d3db9" - }, - "big_sur": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:d88edc1ae7db555ec5da01d4a1272da8260eb62073d2cdfa5fa3dce37d51fbe6", - "sha256": "d88edc1ae7db555ec5da01d4a1272da8260eb62073d2cdfa5fa3dce37d51fbe6" - }, - "catalina": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:24a67cd4f2b66a02cb77a1c705d7dcf25b4410209435a0b1136398da1fa6f766", - "sha256": "24a67cd4f2b66a02cb77a1c705d7dcf25b4410209435a0b1136398da1fa6f766" + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:b3d14cb62e325d0f7221cd24a7fb4533936feae4ed4dce00e8983ec6e55123f8", + "sha256": "b3d14cb62e325d0f7221cd24a7fb4533936feae4ed4dce00e8983ec6e55123f8" }, "x86_64_linux": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:961b2f3d75cf86dd5bc767cf689eee8f8e88bb30d716cf208b4bb89d61e5a553", - "sha256": "961b2f3d75cf86dd5bc767cf689eee8f8e88bb30d716cf208b4bb89d61e5a553" + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:6d0867f144686a5caa025cb15ecac49286654b78e7b89979a54eedc9a0cc9b6b", + "sha256": "6d0867f144686a5caa025cb15ecac49286654b78e7b89979a54eedc9a0cc9b6b" } } } }, "shfmt": { - "version": "3.5.1", + "version": "3.9.0", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:6b2151cc6266a7c0a21a6ec4edab774168671d81406bd127980dd5b5ebdad9d9", + "sha256": "6b2151cc6266a7c0a21a6ec4edab774168671d81406bd127980dd5b5ebdad9d9" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb", + "sha256": "d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb", + "sha256": "d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb" + }, "arm64_monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:633116b598a60ad576a79753208e13388f6a2460139c8aca44e5a25befdb017c", - "sha256": "633116b598a60ad576a79753208e13388f6a2460139c8aca44e5a25befdb017c" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb", + "sha256": "d15598743aa7c7688b49b4a0df839805f605faaa692def2f36554c26e5136eeb" }, - "arm64_big_sur": { + "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:1b0653c0a44f7db5e78c5c6d67de534a52c4f588fb65e3acbb8211d06b871bd9", - "sha256": "1b0653c0a44f7db5e78c5c6d67de534a52c4f588fb65e3acbb8211d06b871bd9" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7", + "sha256": "70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7", + "sha256": "70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7" }, "monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:e7168603f81cf1357c2460c5c476fa66bf5421183d4dedeafe9cf38550fe8855", - "sha256": "e7168603f81cf1357c2460c5c476fa66bf5421183d4dedeafe9cf38550fe8855" - }, - "big_sur": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0e0683566d83cceecd4d02596e3c899a640918ff067b6e15e10f8aee424f1759", - "sha256": "0e0683566d83cceecd4d02596e3c899a640918ff067b6e15e10f8aee424f1759" - }, - "catalina": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:4fabb118ba0da244f2b0ffe280b28e343712fac23e738ddf1db29fad68526d73", - "sha256": "4fabb118ba0da244f2b0ffe280b28e343712fac23e738ddf1db29fad68526d73" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7", + "sha256": "70107f7fdf986b706bc63652fd16355f426c7789088bfd5beb0fe83fc5069fe7" }, "x86_64_linux": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:faa60f70812132e10f94477676499a1e2bacb0d06fbe437e8480a997695c2203", - "sha256": "faa60f70812132e10f94477676499a1e2bacb0d06fbe437e8480a997695c2203" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:6f9f1e653f63d0d4c9042b9dca3063074326998b19b5b216478b682bd437ee17", + "sha256": "6f9f1e653f63d0d4c9042b9dca3063074326998b19b5b216478b682bd437ee17" + } + } + } + }, + "swift-format": { + "version": "510.1.0", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:87797001883127b2279ca6c852090fd1fe2e268ada680161c1b2d86be113191e", + "sha256": "87797001883127b2279ca6c852090fd1fe2e268ada680161c1b2d86be113191e" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:38473895d6a806d2ebef3634f0a0f7fe9b10c733a6e538a33d2cd0bbead6bccd", + "sha256": "38473895d6a806d2ebef3634f0a0f7fe9b10c733a6e538a33d2cd0bbead6bccd" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:0c7accbe185dba810d3fcd3d734705335e733359ccfa7a5d5ae0cbed480ac058", + "sha256": "0c7accbe185dba810d3fcd3d734705335e733359ccfa7a5d5ae0cbed480ac058" + }, + "sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:10ebf6d40cc0c2d4d4cb3309e357e1b6a8d8b5991b5d30c479f3e81587e0c6cd", + "sha256": "10ebf6d40cc0c2d4d4cb3309e357e1b6a8d8b5991b5d30c479f3e81587e0c6cd" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:b12bf9258d6b73fd87d356978e93fa363b4239b50a37c8c2f16b313ad53c5364", + "sha256": "b12bf9258d6b73fd87d356978e93fa363b4239b50a37c8c2f16b313ad53c5364" + }, + "x86_64_linux": { + "cellar": "/home/linuxbrew/.linuxbrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:b350f38deae722dfb00b33610c82d17d6dcd0db3109f171adb56a3fafe0545a0", + "sha256": "b350f38deae722dfb00b33610c82d17d6dcd0db3109f171adb56a3fafe0545a0" } } } }, "swiftformat": { - "version": "0.49.18", + "version": "0.54.6", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { - "arm64_monterey": { + "arm64_sequoia": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:6362f6087bc3821f4271c3d17b3a4f180b1e1326646ddfb60f6d27bfb5a2a357", - "sha256": "6362f6087bc3821f4271c3d17b3a4f180b1e1326646ddfb60f6d27bfb5a2a357" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:fda0a46091e8c4a1a913e08e29a92159ed747d83403508e0b5408e88e68cdf0c", + "sha256": "fda0a46091e8c4a1a913e08e29a92159ed747d83403508e0b5408e88e68cdf0c" }, - "arm64_big_sur": { + "arm64_sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:e94cf1b66df0d712bbfbf509b98efaf31d39a61b82999314e1f3c0e45195c51a", - "sha256": "e94cf1b66df0d712bbfbf509b98efaf31d39a61b82999314e1f3c0e45195c51a" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:2c937e3425e9b44a73eb5ae4a83604b4476f901866014c01c1ebd3f3a8d9c198", + "sha256": "2c937e3425e9b44a73eb5ae4a83604b4476f901866014c01c1ebd3f3a8d9c198" }, - "monterey": { + "arm64_ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:456e0c95a565adbb45a29747abfadf41c838a7f09fae052a874e59429a94ef14", - "sha256": "456e0c95a565adbb45a29747abfadf41c838a7f09fae052a874e59429a94ef14" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:d68be9490bce8cb196933f1f421f791b0b9758a759956edfaf166f88dfca78e1", + "sha256": "d68be9490bce8cb196933f1f421f791b0b9758a759956edfaf166f88dfca78e1" }, - "big_sur": { + "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:d00204be714789fa8b35d4c6f6eea5813604aa09f3911635059973aa827d2e8c", - "sha256": "d00204be714789fa8b35d4c6f6eea5813604aa09f3911635059973aa827d2e8c" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:784d61fca33bdbbdf96f8f23db2a0ea849ef62cb251eedfe83863869db84359b", + "sha256": "784d61fca33bdbbdf96f8f23db2a0ea849ef62cb251eedfe83863869db84359b" }, - "catalina": { + "ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:b07f7221f3c5225ad0037293cecb95bde4f0dba4fa19797d84a3376dd1ad02ea", - "sha256": "b07f7221f3c5225ad0037293cecb95bde4f0dba4fa19797d84a3376dd1ad02ea" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:7845bd9bf8f0f94980f38d0ac322a5ee41bde07d18ec0c93a343c4aa7d2606fe", + "sha256": "7845bd9bf8f0f94980f38d0ac322a5ee41bde07d18ec0c93a343c4aa7d2606fe" }, "x86_64_linux": { "cellar": "/home/linuxbrew/.linuxbrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:c4a4ebd2f3f54b8f399551efaf47b3e419db2c729ffaf18a09e64bbf62d82f38", - "sha256": "c4a4ebd2f3f54b8f399551efaf47b3e419db2c729ffaf18a09e64bbf62d82f38" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:14756d1f83aedf183be980541393c3e4d9cfa47dee3dbfdb665a3461f5045e13", + "sha256": "14756d1f83aedf183be980541393c3e4d9cfa47dee3dbfdb665a3461f5045e13" + } + } + } + }, + "trash": { + "version": "0.9.2", + "bottle": { + "rebuild": 1, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f", + "sha256": "f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76", + "sha256": "90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099", + "sha256": "60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099" + }, + "arm64_monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0", + "sha256": "541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0" + }, + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481", + "sha256": "11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481" + }, + "sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923", + "sha256": "c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e", + "sha256": "539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e" + }, + "monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6", + "sha256": "09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094", + "sha256": "403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6", + "sha256": "b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6" + }, + "mojave": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551", + "sha256": "d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551" + }, + "high_sierra": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a", + "sha256": "0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a" } } } @@ -135,7 +288,7 @@ }, "tap": { "peripheryapp/periphery": { - "revision": "020a0b79994d454dbc8dbd886009137fd741b836" + "revision": "e3f6467b552f08e874875bfbc7703cb917085e0e" } }, "cask": { @@ -150,12 +303,12 @@ "system": { "macos": { "monterey": { - "HOMEBREW_VERSION": "3.5.3-75-g39c9e2d", - "HOMEBREW_PREFIX": "/opt/homebrew", - "Homebrew/homebrew-core": "00e5383db36fddc6b26df896229289b42462e63e", - "CLT": "13.4.0.0.1.1651278267", - "Xcode": "13.4.1", - "macOS": "12.4" + "HOMEBREW_VERSION": "4.4.1-17-gf957e0e", + "HOMEBREW_PREFIX": "/usr/local", + "Homebrew/homebrew-core": "api", + "CLT": "14.2.0.0.1.1668646533", + "Xcode": "14.2", + "macOS": "12.7.6" }, "ventura": { "HOMEBREW_VERSION": "3.6.2-17-g0b602f6", From f06fc7bb75f3f184b53ca12023aa67364bcebc68 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:13:27 -0400 Subject: [PATCH 38/81] Use `trash -F` instead of `trash` to facilitate better undelete. Resolve #576 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- script/uninstall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/uninstall b/script/uninstall index 8e2af57..5a92704 100755 --- a/script/uninstall +++ b/script/uninstall @@ -17,4 +17,4 @@ fi echo "==> 🔥 Uninstalling mas from $PREFIX" -trash "$PREFIX/bin/mas" +trash -F "$PREFIX/bin/mas" From f08f0a37d776625cda192f4313d9a5ac25f2cf90 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:23:13 -0400 Subject: [PATCH 39/81] Improve `bootstrap`. Partial #580 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- script/bootstrap | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index fcfc38d..7448a03 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -13,22 +13,15 @@ if ! cd -- "${mas_dir}"; then exit 1 fi -main() { - script/clean +script/clean - printf $'==> 👢 Bootstrapping\n' +printf $'==> 👢 Bootstrapping (%s)\n' "$(script/version)" - # Install Homebrew tools - rm -f Brewfile.lock.json - brew bundle install --no-upgrade --verbose +# Install Homebrew tools +rm -f Brewfile.lock.json +brew bundle install --no-upgrade --verbose - # Already installed on GitHub Actions runner. - if [[ ! -x "$(command -v swiftlint)" ]]; then - brew install swiftlint - fi - - # Generate Package.swift - script/version -} - -main +# swiftlint is already installed on GitHub Actions runners. +if [[ ! -x "$(command -v swiftlint)" ]]; then + brew install swiftlint +fi From 967e8db9ba3a65ae2a979ac30e521f0a8c1466ae Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:36:18 -0400 Subject: [PATCH 40/81] Only install periphery from brew if macOS version is >= 13. Resolve #580 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Brewfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Brewfile b/Brewfile index 8d4cb99..2638c4e 100644 --- a/Brewfile +++ b/Brewfile @@ -9,4 +9,6 @@ brew "trash" # brew "swiftlint" tap "peripheryapp/periphery" -cask "periphery" +if OS.mac? && MacOS.version >= :ventura + cask "periphery" +end From 7cd8c1436ba615329a1a8334d7aa81c67c613435 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:28:44 -0400 Subject: [PATCH 41/81] Downgrade Nimble & Quick to fix testing on newer Xcode versions. Resolve #583 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Package.resolved | 26 +++---------------- Package.swift | 4 +-- Tests/masTests/Commands/AccountSpec.swift | 2 +- Tests/masTests/Commands/HomeSpec.swift | 2 +- Tests/masTests/Commands/InfoSpec.swift | 2 +- Tests/masTests/Commands/InstallSpec.swift | 2 +- Tests/masTests/Commands/ListSpec.swift | 2 +- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 2 +- Tests/masTests/Commands/OutdatedSpec.swift | 2 +- Tests/masTests/Commands/PurchaseSpec.swift | 2 +- Tests/masTests/Commands/ResetSpec.swift | 2 +- Tests/masTests/Commands/SearchSpec.swift | 2 +- Tests/masTests/Commands/SignInSpec.swift | 2 +- Tests/masTests/Commands/SignOutSpec.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/UpgradeSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 2 +- Tests/masTests/Commands/VersionSpec.swift | 2 +- .../Controllers/MasAppLibrarySpec.swift | 2 +- .../Controllers/MasStoreSearchSpec.swift | 2 +- .../OpenSystemCommandSpec.swift | 2 +- .../Formatters/AppListFormatterSpec.swift | 2 +- .../SearchResultFormatterSpec.swift | 2 +- .../Models/SearchResultListSpec.swift | 2 +- Tests/masTests/Models/SearchResultSpec.swift | 2 +- .../masTests/Models/SoftwareProductSpec.swift | 2 +- 27 files changed, 31 insertions(+), 49 deletions(-) diff --git a/Package.resolved b/Package.resolved index dfcbb2b..80d3efe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Nimble.git", "state" : { - "revision" : "6416749c3c0488664fff6b42f8bf3ea8dc282ca1", - "version" : "13.6.0" + "revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc", + "version" : "10.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Quick.git", "state" : { - "revision" : "1163a1b1b114a657c7432b63dd1f92ce99fe11a6", - "version" : "7.6.2" + "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", + "version" : "5.0.1" } }, { @@ -54,15 +54,6 @@ "version" : "2.1.1" } }, - { - "identity" : "swift-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-algorithms.git", - "state" : { - "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", - "version" : "1.2.0" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -72,15 +63,6 @@ "version" : "1.5.0" } }, - { - "identity" : "swift-numerics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", - "state" : { - "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", - "version" : "1.0.2" - } - }, { "identity" : "version", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 021e71f..4e48ff7 100644 --- a/Package.swift +++ b/Package.swift @@ -17,8 +17,8 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Quick/Nimble.git", from: "13.6.0"), - .package(url: "https://github.com/Quick/Quick.git", from: "7.6.2"), + .package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"), + .package(url: "https://github.com/Quick/Quick.git", from: "5.0.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"), .package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"), diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 23dd488..4cfb4da 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -13,7 +13,7 @@ import Quick // Deprecated test public class AccountSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 3e33524..097855e 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class HomeSpec: QuickSpec { - override public static func spec() { + override public func spec() { let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 35ac88a..7271451 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class InfoSpec: QuickSpec { - override public static func spec() { + override public func spec() { let storeSearch = StoreSearchMock() beforeSuite { diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift index 8580fe0..4baeead 100644 --- a/Tests/masTests/Commands/InstallSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class InstallSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index 811ed7f..7388e80 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class ListSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index 5a7adb0..4e373c2 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class LuckySpec: QuickSpec { - override public static func spec() { + override public func spec() { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 610d91c..13693fb 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class OpenSpec: QuickSpec { - override public static func spec() { + override public func spec() { let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index f1890de..d47b067 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class OutdatedSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift index e186f55..51f84f5 100644 --- a/Tests/masTests/Commands/PurchaseSpec.swift +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class PurchaseSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/ResetSpec.swift b/Tests/masTests/Commands/ResetSpec.swift index 411c101..ca39e2b 100644 --- a/Tests/masTests/Commands/ResetSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class ResetSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 6a1faa7..2496ad0 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SearchSpec: QuickSpec { - override public static func spec() { + override public func spec() { let storeSearch = StoreSearchMock() beforeSuite { diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index 20c0f96..799d093 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -13,7 +13,7 @@ import Quick // Deprecated test public class SignInSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/SignOutSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift index 3424ef2..a5b5466 100644 --- a/Tests/masTests/Commands/SignOutSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SignOutSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index d650044..13613b8 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class UninstallSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index e5a8406..5c104bb 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class UpgradeSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index ac7bc84..624f3a3 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class VendorSpec: QuickSpec { - override public static func spec() { + override public func spec() { let storeSearch = StoreSearchMock() let openCommand = OpenSystemCommandMock() diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index ec08193..ced12bc 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class VersionSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift index 4580b0f..fbb85fb 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class MasAppLibrarySpec: QuickSpec { - override public static func spec() { + override public func spec() { let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) beforeSuite { diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index 9a3b7ff..b6ce5df 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class MasStoreSearchSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index db3e484..a9a5f0e 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class OpenSystemCommandSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index a9e23ab..98ed50f 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class AppListsFormatterSpec: QuickSpec { - override public static func spec() { + override public func spec() { // static func reference let format = AppListFormatter.format(products:) var products: [SoftwareProduct] = [] diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 29ee188..f92731d 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -12,7 +12,7 @@ import Quick @testable import mas public class SearchResultsFormatterSpec: QuickSpec { - override public static func spec() { + override public func spec() { // static func reference let format = SearchResultFormatter.format(results:includePrice:) var results: [SearchResult] = [] diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift index 86b2575..2804491 100644 --- a/Tests/masTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SearchResultListSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift index 7330da8..460e961 100644 --- a/Tests/masTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SearchResultSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } diff --git a/Tests/masTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift index a223ef3..80cbde3 100644 --- a/Tests/masTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -13,7 +13,7 @@ import Quick @testable import mas public class SoftwareProductSpec: QuickSpec { - override public static func spec() { + override public func spec() { beforeSuite { Mas.initialize() } From c2892626d7679aaee6872afe20f3b7bdc99c8c09 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:13:29 -0400 Subject: [PATCH 42/81] Fix search & uninstall tests. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/masTests/Commands/SearchSpec.swift | 7 +++++-- Tests/masTests/Commands/UninstallSpec.swift | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 2496ad0..91a3a45 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -31,9 +32,11 @@ public class SearchSpec: QuickSpec { ) storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + try captureStream(stdout) { + try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + } } - .toNot(throwError()) + == " 1111 slack (0.0)\n" } it("fails when searching for nonexistent app") { expect { diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 13613b8..bf5890f 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -67,7 +67,7 @@ public class UninstallSpec: QuickSpec { try uninstall.run(appLibrary: mockLibrary) } } - == " 1111 slack (0.0)\n==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" + == "==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" } it("fails if there is a problem with the trash command") { var brokenUninstall = app // make mutable copy From 57da9e0f51d48e6c71cf5838e75fa33ef3358b51 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:05:11 -0400 Subject: [PATCH 43/81] Improve `style.md`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- docs/style.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/style.md b/docs/style.md index e4c48cc..c0487ba 100644 --- a/docs/style.md +++ b/docs/style.md @@ -5,7 +5,9 @@ - Use `script/lint` to look for these before committing. - Note that [two trailing spaces](https://gist.github.com/shaunlebron/746476e6e7a4d698b373) is intentional in markdown documents to create a line break like `
`, so these should _not_ be removed. -- End each file with a [newline character](https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789). +- End each file with a [newline character]( + https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789 + ). ## Swift @@ -14,13 +16,13 @@ - Avoid [force unwrapping optionals](https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals/) with `!` in production code - Production code is what gets shipped with the app. Basically, everything under the - [`mas-cli/`](https://github.com/mas-cli/mas/tree/main/mas-cli) folder. + [`Sources/mas`](https://github.com/mas-cli/mas/tree/main/Sources/mas) folder. - However, force unwrapping is **encouraged** in tests for less code and tests _should_ break when any expected conditions aren't met. - Prefer `struct`s over `class`es wherever possible - Default to marking classes as `final` - Prefer protocol conformance to class inheritance -- Break long lines after 120 characters +- Break lines at 120 characters - Use 4 spaces for indentation - Use `let` whenever possible to make immutable variables - Name all parameters in functions and enum cases @@ -28,10 +30,7 @@ with `!` in production code - Let the compiler infer the type whenever possible - Group computed properties below stored properties - Use a blank line above and below computed properties -- Group methods into specific extensions for each level of access control +- Group functions into separate extensions for each level of access control - When capitalizing acronyms or initialisms, follow the capitalization of the first letter. - When using `Void` in function signatures, prefer `()` for arguments and `Void` for return types. -- Prefer strong `IBOutlet` references. - Avoid evaluating a weak reference multiple times in the same scope. Strongify first, then use the strong reference. -- Prefer to name `IBAction` and target/action methods using a verb describing the action it will trigger, instead - of the user action (e.g., `edit:` instead of `editTapped:`) From 3d9ea972f9fcc829a5bdfa2ac5d5d3314f718246 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:09:01 -0400 Subject: [PATCH 44/81] Cleanup code. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 2 +- Sources/mas/Commands/Outdated.swift | 2 -- Sources/mas/Controllers/MasStoreSearch.swift | 8 +++++--- Sources/mas/Formatters/Utilities.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/VersionSpec.swift | 2 +- Tests/masTests/Controllers/AppLibraryMock.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 65a2373..5f90035 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -36,7 +36,7 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { SSPurchase().perform(appID: appID, purchase: purchase) - .recover { error -> Promise in + .recover { error in guard attempts > 1 else { throw error } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index f7b3ce5..9c0b1be 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -10,8 +10,6 @@ import ArgumentParser import Foundation import PromiseKit -import enum Swift.Result - extension Mas { /// Command which displays a list of installed apps which have available updates /// ready to be installed from the Mac App Store. diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9cf2405..6e9325a 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -45,7 +45,7 @@ class MasStoreSearch: StoreSearch { entities += [.iPadSoftware, .iPhoneSoftware] } - let results = entities.map { entity -> Promise<[SearchResult]> in + let results = entities.map { entity in guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { fatalError("Failed to build URL for \(appName)") } @@ -70,7 +70,8 @@ class MasStoreSearch: StoreSearch { } return firstly { loadSearchResults(url) - }.then { results -> Guarantee in + } + .then { results -> Guarantee in guard let result = results.first else { return .value(nil) } @@ -104,7 +105,8 @@ class MasStoreSearch: StoreSearch { private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> { firstly { networkManager.loadData(from: url) - }.map { data -> [SearchResult] in + } + .map { data in do { return try JSONDecoder().decode(SearchResultList.self, from: data).results } catch { diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index c5c940e..fae8ad3 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -69,7 +69,7 @@ func captureStream( _ stream: UnsafeMutablePointer, encoding: String.Encoding = .utf8, _ block: @escaping () throws -> Void -) throws -> String { +) rethrows -> String { let originalFd = fileno(stream) let duplicateFd = dup(originalFd) defer { diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index bf5890f..c6426c3 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -70,7 +70,7 @@ public class UninstallSpec: QuickSpec { == "==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" } it("fails if there is a problem with the trash command") { - var brokenUninstall = app // make mutable copy + var brokenUninstall = app brokenUninstall.bundlePath = "/dev/null" mockLibrary.installedApps.append(brokenUninstall) expect { diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index ced12bc..0828749 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -24,7 +24,7 @@ public class VersionSpec: QuickSpec { try Mas.Version.parse([]).run() } } - == Package.version + "\n" + == "\(Package.version)\n" } } } diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift index 4a87784..f763280 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -9,7 +9,7 @@ @testable import mas class AppLibraryMock: AppLibrary { - var installedApps = [SoftwareProduct]() + var installedApps: [SoftwareProduct] = [] func uninstallApp(app: SoftwareProduct) throws { if !installedApps.contains(where: { product -> Bool in diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index fe0248b..f9e13dd 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -22,7 +22,7 @@ class MASErrorTestCase: XCTestCase { var localizedDescription: String { get { "dummy value" } set { - NSError.setUserInfoValueProvider(forDomain: errorDomain) { (_: Error, _: String) -> Any? in + NSError.setUserInfoValueProvider(forDomain: errorDomain) { _, _ in newValue } } From a100f3acd0612db9248a3901f0dcf718d394fb82 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:39:35 -0400 Subject: [PATCH 45/81] Remove unnecessary generics. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 2 +- Sources/mas/AppStore/ISStoreAccount.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 5f90035..7e0b338 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -19,7 +19,7 @@ import StoreFoundation /// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? - return appIDs.reduce(Guarantee.value(())) { previous, appID in + return appIDs.reduce(Guarantee.value(())) { previous, appID in previous.then { downloadWithRetries(appID, purchase: purchase).recover { error in if firstError == nil { diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 7dcfd30..7758ea4 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -14,7 +14,7 @@ extension ISStoreAccount: StoreAccount { static var primaryAccount: Promise { if #available(macOS 10.13, *) { return race( - Promise { seal in + Promise { seal in ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in seal.fulfill(storeAccount) } From 3eaffa5c3e3e0926b2b1d8e6bf77b8c15e726d63 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:10:31 -0400 Subject: [PATCH 46/81] Improve DocC. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 9 +++++---- Sources/mas/Commands/Lucky.swift | 6 ++++-- Sources/mas/Commands/Search.swift | 5 +++-- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Controllers/AppLibrary.swift | 12 ++++++------ Sources/mas/Controllers/MasStoreSearch.swift | 13 +++++-------- Sources/mas/Controllers/SoftwareMap.swift | 2 +- Sources/mas/Controllers/StoreSearch.swift | 10 ++++++++-- Sources/mas/ExternalCommands/ExternalCommand.swift | 2 +- .../mas/ExternalCommands/OpenSystemCommand.swift | 3 +-- .../mas/ExternalCommands/SysCtlSystemCommand.swift | 5 +++-- Sources/mas/Formatters/SearchResultFormatter.swift | 6 ++++-- Sources/mas/Formatters/Utilities.swift | 5 +++-- Sources/mas/Models/SoftwareProduct.swift | 7 +++++-- Sources/mas/Network/NetworkManager.swift | 7 +++---- Tests/masTests/Commands/AccountSpec.swift | 2 +- Tests/masTests/Commands/SignInSpec.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 4 +++- Tests/masTests/Extensions/Bundle+JSON.swift | 3 ++- Tests/masTests/Network/NetworkSessionMock.swift | 5 ----- .../Network/NetworkSessionMockFromFile.swift | 5 ----- script/version | 2 +- 22 files changed, 61 insertions(+), 56 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 7e0b338..cb5bbc3 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -12,11 +12,12 @@ import StoreFoundation /// Downloads a list of apps, one after the other, printing progress to the console. /// -/// - Parameter appIDs: The IDs of the apps to be downloaded -/// - Parameter purchase: Flag indicating whether the apps needs to be purchased. -/// Only works for free apps. Defaults to false. +/// - Parameters: +/// - appIDs: The IDs of the apps to be downloaded +/// - purchase: Flag indicating whether the apps needs to be purchased. +/// Only works for free apps. Defaults to false. /// - Returns: A promise that completes when the downloads are complete. If any fail, -/// the promise is rejected with the first error, after all remaining downloads are attempted. +/// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? return appIDs.reduce(Guarantee.value(())) { previous, appID in diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 3b567fd..a75fbea 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -10,8 +10,9 @@ import ArgumentParser import CommerceKit extension Mas { - /// Command which installs the first search result. This is handy as many MAS titles - /// can be long with embedded keywords. + /// Command which installs the first search result. + /// + /// This is handy as many MAS titles can be long with embedded keywords. struct Lucky: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Install the first result from the Mac App Store" @@ -54,6 +55,7 @@ extension Mas { /// - Parameters: /// - appID: App identifier /// - appLibrary: Library of installed apps + /// - Throws: Any error that occurs while attempting to install the app. fileprivate func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(withAppID: appID), !force { diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 036c61c..79ef211 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -9,8 +9,9 @@ import ArgumentParser extension Mas { - /// Search the Mac App Store using the iTunes Search API: - /// https://performance-partners.apple.com/search-api + /// Search the Mac App Store using the iTunes Search API. + /// + /// See - https://performance-partners.apple.com/search-api struct Search: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Search for apps from the Mac App Store" diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 8265296..632c84b 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -17,7 +17,7 @@ extension Mas { abstract: "Uninstall app installed from the Mac App Store" ) - /// Flag indicating that removal shouldn't be performed + /// Flag indicating that removal shouldn't be performed. @Flag(help: "dry run") var dryRun = false @Argument(help: "ID of app to uninstall") diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index 1db3ff9..ebdcd70 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -13,10 +13,10 @@ protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } - /// Finds an app by ID. + /// Finds an app for appID. /// - /// - Parameter withAppID: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. + /// - Parameter appID: app ID for app. + /// - Returns: SoftwareProduct of app if found; nil otherwise. func installedApp(withAppID appID: AppID) -> SoftwareProduct? /// Uninstalls an app. @@ -28,10 +28,10 @@ protocol AppLibrary { /// Common logic extension AppLibrary { - /// Finds an app by ID. + /// Finds an app for appID. /// - /// - Parameter withAppID: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. + /// - Parameter appID: app ID for app. + /// - Returns: SoftwareProduct of app if found; nil otherwise. func installedApp(withAppID appID: AppID) -> SoftwareProduct? { let appID = NSNumber(value: appID) return installedApps.first { $0.itemIdentifier == appID } diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 6e9325a..9e64b72 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -34,9 +34,8 @@ class MasStoreSearch: StoreSearch { /// Searches for an app. /// - /// - Parameter appName: MAS ID of app - /// - Parameter completion: A closure that receives the search results or an Error if there is a - /// problem with the network request. Results array will be empty if there were no matches. + /// - Parameter searchTerm: a search term matched against app names + /// - Returns: A Promise of an Array of SearchResults matching searchTerm func search(for appName: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. @@ -115,11 +114,9 @@ class MasStoreSearch: StoreSearch { } } - // App Store pages indicate: - // - compatibility with Macs with Apple Silicon - // - (often) a version that is newer than what is listed in search results - // - // We attempt to scrape this information here. + /// Scrape the app version from the App Store webpage at the given URL. + /// + /// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API. private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { firstly { networkManager.loadData(from: pageUrl) diff --git a/Sources/mas/Controllers/SoftwareMap.swift b/Sources/mas/Controllers/SoftwareMap.swift index 46abea8..b131d42 100644 --- a/Sources/mas/Controllers/SoftwareMap.swift +++ b/Sources/mas/Controllers/SoftwareMap.swift @@ -6,7 +6,7 @@ // Copyright © 2020 mas-cli. All rights reserved. // -/// Somewhat analogous to CKSoftwareMap +/// Somewhat analogous to CKSoftwareMap. protocol SoftwareMap { func allSoftwareProducts() -> [SoftwareProduct] func product(for bundleIdentifier: String) -> SoftwareProduct? diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index e906cd9..12a46e0 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -40,7 +40,10 @@ private enum URLAction { extension StoreSearch { /// Builds the search URL for an app. /// - /// - Parameter searchTerm: term for which to search in MAS. + /// - Parameters: + /// - searchTerm: term for which to search in MAS. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. /// - Returns: URL for the search service or nil if searchTerm can't be encoded. func searchURL( for searchTerm: String, @@ -52,7 +55,10 @@ extension StoreSearch { /// Builds the lookup URL for an app. /// - /// - Parameter appID: MAS app identifier. + /// - Parameters: + /// - appID: MAS app identifier. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. /// - Returns: URL for the lookup service or nil if appID can't be encoded. func lookupURL( forAppID appID: AppID, diff --git a/Sources/mas/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift index 40946be..2168a05 100644 --- a/Sources/mas/ExternalCommands/ExternalCommand.swift +++ b/Sources/mas/ExternalCommands/ExternalCommand.swift @@ -8,7 +8,7 @@ import Foundation -/// CLI command +/// Represents a CLI command. protocol ExternalCommand { var binaryPath: String { get set } diff --git a/Sources/mas/ExternalCommands/OpenSystemCommand.swift b/Sources/mas/ExternalCommands/OpenSystemCommand.swift index ebce90d..5d5e3cc 100644 --- a/Sources/mas/ExternalCommands/OpenSystemCommand.swift +++ b/Sources/mas/ExternalCommands/OpenSystemCommand.swift @@ -8,8 +8,7 @@ import Foundation -/// Wrapper for the external open system command. -/// https://ss64.com/osx/open.html +/// Wrapper for the external 'open' system command (https://ss64.com/osx/open.html). struct OpenSystemCommand: ExternalCommand { var binaryPath: String diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index 41d3dcb..cbb24d9 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -8,8 +8,9 @@ import Foundation -/// Wrapper for the external sysctl system command. -/// https://ss64.com/osx/sysctl.html +/// Wrapper for the external 'sysctl' system command. +/// +/// See - https://ss64.com/osx/sysctl.html struct SysCtlSystemCommand: ExternalCommand { var binaryPath: String diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index eac80a7..96e9414 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -10,9 +10,11 @@ import Foundation /// Formats text output for the search command. enum SearchResultFormatter { - /// Formats text output with search results. + /// Formats search results as text. /// - /// - Parameter results: Search results with app data + /// - Parameters: + /// - results: Search results containing app data + /// - includePrice: Indicates whether to include prices in the output /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { // find longest appName for formatting, default 50 diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index fae8ad3..3f41341 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -8,14 +8,15 @@ import Foundation -/// A collection of output formatting helpers +// A collection of output formatting helpers -/// Terminal Control Sequence Indicator +/// Terminal Control Sequence Indicator. let csi = "\u{001B}[" private var standardError = FileHandle.standardError extension FileHandle: TextOutputStream { + /// Appends the given string to the stream. public func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } write(data) diff --git a/Sources/mas/Models/SoftwareProduct.swift b/Sources/mas/Models/SoftwareProduct.swift index aa49d81..7b882c5 100644 --- a/Sources/mas/Models/SoftwareProduct.swift +++ b/Sources/mas/Models/SoftwareProduct.swift @@ -19,12 +19,15 @@ protocol SoftwareProduct { } extension SoftwareProduct { - /// Returns bundleIdentifier if appName is empty string. + /// - Returns: bundleIdentifier if appName is empty string. var appNameOrBundleIdentifier: String { appName.isEmpty ? bundleIdentifier : appName } - /// Determines whether the app is considered outdated. Updates that require a higher OS version are excluded. + /// Determines whether the app is considered outdated. + /// + /// Updates that require a higher OS version are excluded. + /// /// - Parameter storeApp: App from search result. /// - Returns: true if the app is outdated; false otherwise. func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool { diff --git a/Sources/mas/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift index 45fa7b8..9c4f496 100644 --- a/Sources/mas/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -9,11 +9,11 @@ import Foundation import PromiseKit -/// Network abstraction +/// Network abstraction. class NetworkManager { private let session: NetworkSession - /// Designated initializer + /// Designated initializer. /// /// - Parameter session: A networking session. init(session: NetworkSession = URLSession(configuration: .ephemeral)) { @@ -28,8 +28,7 @@ class NetworkManager { /// Loads data asynchronously. /// - /// - Parameters: - /// - url: URL to load data from. + /// - Parameter url: URL from which to load data. /// - Returns: A Promise for the Data of the response. func loadData(from url: URL) -> Promise { session.loadData(from: url) diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 4cfb4da..db220c4 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -// Deprecated test +/// Deprecated test. public class AccountSpec: QuickSpec { override public func spec() { beforeSuite { diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index 799d093..f7f5d33 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -// Deprecated test +/// Deprecated test. public class SignInSpec: QuickSpec { override public func spec() { beforeSuite { diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index f9e13dd..0d59cb2 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -17,7 +17,9 @@ class MASErrorTestCase: XCTestCase { var nserror: NSError! /// Convenience property for setting the value which will be use for the localized description - /// value of the next NSError created. Only used when the NSError does not have a user info + /// value of the next NSError created. + /// + /// Only used when the NSError does not have a user info /// entry for localized description. var localizedDescription: String { get { "dummy value" } diff --git a/Tests/masTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift index 34cd6ac..3c9628c 100644 --- a/Tests/masTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -10,7 +10,8 @@ import Foundation extension Data { /// Unsafe initializer for loading data from string paths. - /// - Parameter file: Relative path within the JSON folder + /// + /// - Parameter fileName: Relative path within the JSON folder init(from fileName: String) { let fileURL = Bundle.url(for: fileName)! try! self.init(contentsOf: fileURL, options: .mappedIfSafe) diff --git a/Tests/masTests/Network/NetworkSessionMock.swift b/Tests/masTests/Network/NetworkSessionMock.swift index a286bbb..31274cc 100644 --- a/Tests/masTests/Network/NetworkSessionMock.swift +++ b/Tests/masTests/Network/NetworkSessionMock.swift @@ -18,11 +18,6 @@ class NetworkSessionMock: NetworkSession { var data: Data? var error: Error? - /// Immediately passes data and error to completion handler. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. func loadData(from _: URL) -> Promise { guard let data else { return Promise(error: error ?? MASError.noData) diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift index a68220d..07b25aa 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -21,11 +21,6 @@ class NetworkSessionMockFromFile: NetworkSessionMock { self.responseFile = responseFile } - /// Loads data from a file. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. override func loadData(from _: URL) -> Promise { guard let fileURL = Bundle.url(for: responseFile) else { fatalError("Unable to load file \(responseFile)") } diff --git a/script/version b/script/version index 2f977ff..0a3e177 100755 --- a/script/version +++ b/script/version @@ -20,7 +20,7 @@ VERSION=$(git describe --abbrev=0 --tags) VERSION=${VERSION#v} cat <"Sources/mas/Package.swift" -// Generated by: script/version +/// Generated by \`script/version\`. enum Package { static let version = "${VERSION}" } From 71fbe2e444168c715f8a2e57923c0545fd5f2682 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:12:32 -0400 Subject: [PATCH 47/81] Improve spacing. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 24 +++++++++++-------- Sources/mas/AppStore/ISStoreAccount.swift | 21 +++++++++------- .../AppStore/PurchaseDownloadObserver.swift | 3 ++- Sources/mas/AppStore/SSPurchase.swift | 24 +++++++++---------- Sources/mas/Commands/Open.swift | 6 ++--- Sources/mas/Commands/Outdated.swift | 3 ++- Sources/mas/Commands/Upgrade.swift | 6 +++-- Sources/mas/Commands/Vendor.swift | 8 +++---- Sources/mas/Controllers/MasAppLibrary.swift | 7 +++--- Sources/mas/Controllers/MasStoreSearch.swift | 20 +++++++++------- Sources/mas/Formatters/Utilities.swift | 4 +++- .../Controllers/StoreSearchMock.swift | 3 +-- .../Network/NetworkSessionMockFromFile.swift | 5 ++-- 13 files changed, 75 insertions(+), 59 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index cb5bbc3..bb09f3d 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -20,19 +20,23 @@ import StoreFoundation /// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? - return appIDs.reduce(Guarantee.value(())) { previous, appID in - previous.then { - downloadWithRetries(appID, purchase: purchase).recover { error in - if firstError == nil { - firstError = error - } + return + appIDs + .reduce(Guarantee.value(())) { previous, appID in + previous.then { + downloadWithRetries(appID, purchase: purchase) + .recover { error in + if firstError == nil { + firstError = error + } + } } } - }.done { - if let error = firstError { - throw error + .done { + if let error = firstError { + throw error + } } - } } private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 7758ea4..f1d4fe0 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -15,13 +15,15 @@ extension ISStoreAccount: StoreAccount { if #available(macOS 10.13, *) { return race( Promise { seal in - ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in - seal.fulfill(storeAccount) - } + ISServiceProxy.genericShared().accountService + .primaryAccount { storeAccount in + seal.fulfill(storeAccount) + } }, - after(seconds: 30).then { - Promise(error: MASError.notSignedIn) - } + after(seconds: 30) + .then { + Promise(error: MASError.notSignedIn) + } ) } else { return .value(CKAccountStore.shared().primaryAccount) @@ -76,9 +78,10 @@ extension ISStoreAccount: StoreAccount { return race( signInPromise, - after(seconds: 30).then { - Promise(error: MASError.signInFailed(error: nil)) - } + after(seconds: 30) + .then { + Promise(error: MASError.signInFailed(error: nil)) + } ) } } diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index 343b49c..fd4add4 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -9,7 +9,8 @@ import CommerceKit import StoreFoundation -@objc class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { +@objc +class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { let purchase: SSPurchase var completionHandler: (() -> Void)? var errorHandler: ((MASError) -> Void)? diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index cabfa51..cf712cd 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -23,7 +23,6 @@ extension SSPurchase { if purchase { parameters["macappinstalledconfirmed"] = 1 parameters["pricingParameters"] = "STDQ" - } else { parameters["pricingParameters"] = "STDRDL" } @@ -63,19 +62,20 @@ extension SSPurchase { private func perform() -> Promise { Promise { seal in - CKPurchaseController.shared().perform(self, withOptions: 0) { purchase, _, error, response in - if let error { - seal.reject(MASError.purchaseFailed(error: error as NSError?)) - return - } + CKPurchaseController.shared() + .perform(self, withOptions: 0) { purchase, _, error, response in + if let error { + seal.reject(MASError.purchaseFailed(error: error as NSError?)) + return + } - guard response?.downloads.isEmpty == false, let purchase else { - seal.reject(MASError.noDownloads) - return - } + guard response?.downloads.isEmpty == false, let purchase else { + seal.reject(MASError.noDownloads) + return + } - seal.fulfill(purchase) - } + seal.fulfill(purchase) + } } .then { purchase in let observer = PurchaseDownloadObserver(purchase: purchase) diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 4cc0c99..4ae4247 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -35,13 +35,11 @@ extension Mas { return } - guard let result = try storeSearch.lookup(appID: appID).wait() - else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } - guard var url = URLComponents(string: result.trackViewUrl) - else { + guard var url = URLComponents(string: result.trackViewUrl) else { throw MASError.searchFailed } url.scheme = masScheme diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 9c0b1be..2164a4b 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -32,7 +32,8 @@ extension Mas { appLibrary.installedApps.map { installedApp in firstly { storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) - }.done { storeApp in + } + .done { storeApp in guard let storeApp else { if verbose { printWarning( diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index cfc7b66..a588af2 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -41,7 +41,8 @@ extension Mas { print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") print( apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } - .joined(separator: "\n")) + .joined(separator: "\n") + ) do { try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait() @@ -71,7 +72,8 @@ extension Mas { // only upgrade apps whose local version differs from the store version firstly { storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) - }.map { result -> (SoftwareProduct, SearchResult)? in + } + .map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { return nil } diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index bdf13b1..7380ca0 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -26,13 +26,13 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(appID: appID).wait() - else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } - guard let vendorWebsite = result.sellerUrl - else { throw MASError.noVendorWebsite } + guard let vendorWebsite = result.sellerUrl else { + throw MASError.noVendorWebsite + } do { try openCommand.run(arguments: vendorWebsite) diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index d842194..9325af4 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -14,9 +14,10 @@ class MasAppLibrary: AppLibrary { private let softwareMap: SoftwareMap /// Array of installed software products. - lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts().filter { product in - product.bundlePath.starts(with: "/Applications/") - } + lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts() + .filter { product in + product.bundlePath.starts(with: "/Applications/") + } /// Internal initializer for providing a mock software map. /// - Parameter softwareMap: SoftwareMap to use diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9e64b72..9680070 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -53,9 +53,11 @@ class MasStoreSearch: StoreSearch { // Combine the results, removing any duplicates. var seenAppIDs = Set() - return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in - seenAppIDs.insert(result.trackId).inserted - } + return when(fulfilled: results) + .flatMapValues { $0 } + .filterValues { result in + seenAppIDs.insert(result.trackId).inserted + } } /// Looks up app details. @@ -75,14 +77,14 @@ class MasStoreSearch: StoreSearch { return .value(nil) } - guard let pageUrl = URL(string: result.trackViewUrl) - else { + guard let pageUrl = URL(string: result.trackViewUrl) else { return .value(result) } return firstly { self.scrapeAppStoreVersion(pageUrl) - }.map { pageVersion in + } + .map { pageVersion in guard let pageVersion, let searchVersion = Version(tolerant: result.version), pageVersion > searchVersion @@ -94,7 +96,8 @@ class MasStoreSearch: StoreSearch { var result = result result.version = pageVersion.description return result - }.recover { _ in + } + .recover { _ in // If we were unable to scrape the App Store page, assume compatibility. .value(result) } @@ -120,7 +123,8 @@ class MasStoreSearch: StoreSearch { private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { firstly { networkManager.loadData(from: pageUrl) - }.map { data in + } + .map { data in guard let html = String(data: data, encoding: .utf8), let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index 3f41341..63e436c 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -18,7 +18,9 @@ private var standardError = FileHandle.standardError extension FileHandle: TextOutputStream { /// Appends the given string to the stream. public func write(_ string: String) { - guard let data = string.data(using: .utf8) else { return } + guard let data = string.data(using: .utf8) else { + return + } write(data) } } diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index f97a1d3..938c2b0 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -18,8 +18,7 @@ class StoreSearchMock: StoreSearch { } func lookup(appID: AppID) -> Promise { - guard let result = apps[appID] - else { + guard let result = apps[appID] else { return Promise(error: MASError.noSearchResultsFound) } diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift index 07b25aa..2ab1fa1 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -22,8 +22,9 @@ class NetworkSessionMockFromFile: NetworkSessionMock { } override func loadData(from _: URL) -> Promise { - guard let fileURL = Bundle.url(for: responseFile) - else { fatalError("Unable to load file \(responseFile)") } + guard let fileURL = Bundle.url(for: responseFile) else { + fatalError("Unable to load file \(responseFile)") + } do { return .value(try Data(contentsOf: fileURL, options: .mappedIfSafe)) From d7074db06f28e405793a65803c1c7719caffc93e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:29:13 -0400 Subject: [PATCH 48/81] Remove unnecessary `else` blocks. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 30 +++++++++++------------ Sources/mas/Commands/Upgrade.swift | 8 +++--- Sources/mas/Errors/MASError.swift | 18 +++++--------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index f1d4fe0..414dc29 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -25,9 +25,9 @@ extension ISStoreAccount: StoreAccount { Promise(error: MASError.notSignedIn) } ) - } else { - return .value(CKAccountStore.shared().primaryAccount) } + + return .value(CKAccountStore.shared().primaryAccount) } static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { @@ -70,20 +70,20 @@ extension ISStoreAccount: StoreAccount { if systemDialog { return signInPromise - } else { - context.demoMode = true - context.demoAccountName = username - context.demoAccountPassword = password - context.demoAutologinMode = true - - return race( - signInPromise, - after(seconds: 30) - .then { - Promise(error: MASError.signInFailed(error: nil)) - } - ) } + + context.demoMode = true + context.demoAccountName = username + context.demoAccountPassword = password + context.demoAutologinMode = true + + return race( + signInPromise, + after(seconds: 30) + .then { + Promise(error: MASError.signInFailed(error: nil)) + } + ) } } } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index a588af2..bd23d93 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -60,12 +60,12 @@ extension Mas { ? appLibrary.installedApps : appIDs.compactMap { if let appID = AppID($0) { - // if argument an AppID, lookup app by id using argument + // argument is an AppID, lookup app by id using argument return appLibrary.installedApp(withAppID: appID) - } else { - // if argument not an AppID, lookup app by name using argument - return appLibrary.installedApp(named: $0) } + + // argument is not an AppID, lookup app by name using argument + return appLibrary.installedApp(named: $0) } let promises = apps.map { installedApp in diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 4d4155a..28e6754 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -51,29 +51,25 @@ extension MASError: CustomStringConvertible { case .failed(let error): if let error { return "Failed: \(error.localizedDescription)" - } else { - return "Failed" } + return "Failed" case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" - } else { - return "Sign in failed" } + return "Sign in failed" case .alreadySignedIn(let accountId): return "Already signed in as \(accountId)" case .purchaseFailed(let error): if let error { return "Download request failed: \(error.localizedDescription)" - } else { - return "Download request failed" } + return "Download request failed" case .downloadFailed(let error): if let error { return "Download failed: \(error.localizedDescription)" - } else { - return "Download failed" } + return "Download failed" case .noDownloads: return "No downloads began" case .cancelled: @@ -94,12 +90,10 @@ extension MASError: CustomStringConvertible { if let data { if let unparsable = String(data: data, encoding: .utf8) { return "Unable to parse response as JSON: \n\(unparsable)" - } else { - return "Received defective response" } - } else { - return "Received empty response" + return "Received defective response" } + return "Received empty response" } } } From 06a347c4501f94ad2ba528f07134d9d01319379b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:35:16 -0400 Subject: [PATCH 49/81] Use `isEmpty` / `beEmpty()`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 2 +- Tests/masTests/Formatters/AppListFormatterSpec.swift | 2 +- Tests/masTests/Formatters/SearchResultFormatterSpec.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index bd23d93..db825b6 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -33,7 +33,7 @@ extension Mas { throw error as? MASError ?? .searchFailed } - guard apps.count > 0 else { + guard !apps.isEmpty else { printWarning("Nothing found to upgrade") return } diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index 98ed50f..42346ae 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -25,7 +25,7 @@ public class AppListsFormatterSpec: QuickSpec { products = [] } it("formats nothing as empty string") { - expect(format(products)) == "" + expect(format(products)).to(beEmpty()) } it("can format a single product") { let product = SoftwareProductMock( diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index f92731d..10fec4b 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -25,7 +25,7 @@ public class SearchResultsFormatterSpec: QuickSpec { results = [] } it("formats nothing as empty string") { - expect(format(results, false)) == "" + expect(format(results, false)).to(beEmpty()) } it("can format a single result") { let result = SearchResult( From e927466dceb9801176e8d7b6f3463f7644b0ca2c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:38:04 -0400 Subject: [PATCH 50/81] Use `Self`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/MasStoreSearch.swift | 2 +- Sources/mas/ExternalCommands/SysCtlSystemCommand.swift | 2 +- Sources/mas/Mas.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9680070..a2cf5b2 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -126,7 +126,7 @@ class MasStoreSearch: StoreSearch { } .map { data in guard let html = String(data: data, encoding: .utf8), - let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], + let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) else { return nil diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index cbb24d9..abd5fb8 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -24,7 +24,7 @@ struct SysCtlSystemCommand: ExternalCommand { } static var isAppleSilicon: Bool = { - let sysctl = SysCtlSystemCommand() + let sysctl = Self() do { // Returns 1 on Apple Silicon even when run in an Intel context in Rosetta. try sysctl.run(arguments: "-in", "hw.optional.arm64") diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 1d273fa..5fb8dda 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -36,7 +36,7 @@ struct Mas: ParsableCommand { ) func validate() throws { - Mas.initialize() + Self.initialize() } static func initialize() { From 490ee2d3385f8cf854923ebaf8b887742c916708 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:38:51 -0400 Subject: [PATCH 51/81] Fix class names. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/masTests/Formatters/AppListFormatterSpec.swift | 2 +- Tests/masTests/Formatters/SearchResultFormatterSpec.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index 42346ae..47b3b18 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -public class AppListsFormatterSpec: QuickSpec { +public class AppListFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = AppListFormatter.format(products:) diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 10fec4b..1689fa0 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -public class SearchResultsFormatterSpec: QuickSpec { +public class SearchResultFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = SearchResultFormatter.format(results:includePrice:) From 586de073892a6a5f704d1dca3654f1e94a60e97c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:40:14 -0400 Subject: [PATCH 52/81] Improve access control. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 6 +++--- Tests/masTests/Network/NetworkManagerTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index a75fbea..b79c776 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -56,7 +56,7 @@ extension Mas { /// - appID: App identifier /// - appLibrary: Library of installed apps /// - Throws: Any error that occurs while attempting to install the app. - fileprivate func install(appID: AppID, appLibrary: AppLibrary) throws { + private func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(withAppID: appID), !force { printWarning("\(product.appName) is already installed") diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index 0d59cb2..fdc044e 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -13,15 +13,15 @@ import XCTest class MASErrorTestCase: XCTestCase { private let errorDomain = "MAS" - var error: MASError! - var nserror: NSError! + private var error: MASError! + private var nserror: NSError! /// Convenience property for setting the value which will be use for the localized description /// value of the next NSError created. /// /// Only used when the NSError does not have a user info /// entry for localized description. - var localizedDescription: String { + private var localizedDescription: String { get { "dummy value" } set { NSError.setUserInfoValueProvider(forDomain: errorDomain) { _, _ in diff --git a/Tests/masTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift index a92227c..450bb39 100644 --- a/Tests/masTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -11,7 +11,7 @@ import XCTest @testable import mas class NetworkManagerTests: XCTestCase { - override public func setUp() { + override func setUp() { super.setUp() Mas.initialize() } From 2af2a42a88893f4173796dc429d667d2fad71c1f Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:42:30 -0400 Subject: [PATCH 53/81] Improve parameter names. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 6 +++--- Sources/mas/Controllers/MasStoreSearch.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index db825b6..41054d2 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -58,14 +58,14 @@ extension Mas { let apps: [SoftwareProduct] = appIDs.isEmpty ? appLibrary.installedApps - : appIDs.compactMap { - if let appID = AppID($0) { + : appIDs.compactMap { appID in + if let appID = AppID(appID) { // argument is an AppID, lookup app by id using argument return appLibrary.installedApp(withAppID: appID) } // argument is not an AppID, lookup app by name using argument - return appLibrary.installedApp(named: $0) + return appLibrary.installedApp(named: appID) } let promises = apps.map { installedApp in diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index a2cf5b2..2dc7ff4 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -36,7 +36,7 @@ class MasStoreSearch: StoreSearch { /// /// - Parameter searchTerm: a search term matched against app names /// - Returns: A Promise of an Array of SearchResults matching searchTerm - func search(for appName: String) -> Promise<[SearchResult]> { + func search(for searchTerm: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. var entities = [Entity.desktopSoftware] @@ -45,8 +45,8 @@ class MasStoreSearch: StoreSearch { } let results = entities.map { entity in - guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { - fatalError("Failed to build URL for \(appName)") + guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else { + fatalError("Failed to build URL for \(searchTerm)") } return loadSearchResults(url) } From 3a6d6724c9a2743e02581881e97be77d099bd42e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:43:13 -0400 Subject: [PATCH 54/81] Reorder elements. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../SysCtlSystemCommand.swift | 22 +++++++++---------- Sources/mas/Mas.swift | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index abd5fb8..1d04a2e 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -12,17 +12,6 @@ import Foundation /// /// See - https://ss64.com/osx/sysctl.html struct SysCtlSystemCommand: ExternalCommand { - var binaryPath: String - - let process = Process() - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - - init(binaryPath: String = "/usr/sbin/sysctl") { - self.binaryPath = binaryPath - } - static var isAppleSilicon: Bool = { let sysctl = Self() do { @@ -38,4 +27,15 @@ struct SysCtlSystemCommand: ExternalCommand { return sysctl.stdout.trimmingCharacters(in: .newlines) == "1" }() + + let process = Process() + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + var binaryPath: String + + init(binaryPath: String = "/usr/sbin/sysctl") { + self.binaryPath = binaryPath + } } diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 5fb8dda..89aeacb 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -35,10 +35,6 @@ struct Mas: ParsableCommand { ] ) - func validate() throws { - Self.initialize() - } - static func initialize() { PromiseKit.conf.Q.map = .global() PromiseKit.conf.Q.return = .global() @@ -54,6 +50,10 @@ struct Mas: ParsableCommand { } } } + + func validate() throws { + Self.initialize() + } } typealias AppID = UInt64 From 65218eff7485b89ab678160ba338d588cf909289 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:43:45 -0400 Subject: [PATCH 55/81] Comment that catch ignores error. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Network/NetworkManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/mas/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift index 9c4f496..5f0ab66 100644 --- a/Sources/mas/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -23,7 +23,9 @@ class NetworkManager { do { let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Caches/com.mphys.mas-cli") try FileManager.default.removeItem(at: url) - } catch {} + } catch { + // Ignore + } } /// Loads data asynchronously. From fcf8ba29f6ff2acfefd40203840c571bef91ff6b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:41:22 -0400 Subject: [PATCH 56/81] Improve error output. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 2 +- Sources/mas/Commands/Reset.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index b79c776..16c7948 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -44,7 +44,7 @@ extension Mas { } guard let appID else { - fatalError() + fatalError("app ID returned from Apple is null") } try install(appID: appID, appLibrary: appLibrary) diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 0ddc210..6698780 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -61,7 +61,7 @@ extension Mas { if kill.terminationStatus != 0, debug { let output = stderr.fileHandleForReading.readDataToEndOfFile() - printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") + printError("killall failed:\n\(String(data: output, encoding: String.Encoding.utf8)!)") } // Wipe Download Directory From ee28af69df9dc419ad8b817a4e536fbcbeba9a31 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 08:58:18 -0400 Subject: [PATCH 57/81] Eliminate magic numbers. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 6 ++++-- Sources/mas/AppStore/PurchaseDownloadObserver.swift | 1 + Sources/mas/Formatters/SearchResultFormatter.swift | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 414dc29..1c8afa0 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -10,6 +10,8 @@ import CommerceKit import PromiseKit import StoreFoundation +private let timeout = 30.0 + extension ISStoreAccount: StoreAccount { static var primaryAccount: Promise { if #available(macOS 10.13, *) { @@ -20,7 +22,7 @@ extension ISStoreAccount: StoreAccount { seal.fulfill(storeAccount) } }, - after(seconds: 30) + after(seconds: timeout) .then { Promise(error: MASError.notSignedIn) } @@ -79,7 +81,7 @@ extension ISStoreAccount: StoreAccount { return race( signInPromise, - after(seconds: 30) + after(seconds: timeout) .then { Promise(error: MASError.signInFailed(error: nil)) } diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index fd4add4..c6709cf 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -65,6 +65,7 @@ struct ProgressState { let phase: String var percentage: String { + // swiftlint:disable:next no_magic_numbers String(format: "%.1f%%", floor(percentComplete * 1000) / 10) } } diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index 96e9414..7145d87 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -17,8 +17,10 @@ enum SearchResultFormatter { /// - includePrice: Indicates whether to include prices in the output /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { - // find longest appName for formatting, default 50 - let maxLength = results.map(\.trackName.count).max() ?? 50 + guard let maxLength = results.map(\.trackName.count).max() else { + return "" + } + var output = "" for result in results { From 87aaba555db65cc3353a059c78502f17c88eeb49 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:39:27 -0400 Subject: [PATCH 58/81] Remove unnecessary variable type. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 41054d2..4b3740b 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -55,7 +55,7 @@ extension Mas { appLibrary: AppLibrary, storeSearch: StoreSearch ) throws -> [(SoftwareProduct, SearchResult)] { - let apps: [SoftwareProduct] = + let apps = appIDs.isEmpty ? appLibrary.installedApps : appIDs.compactMap { appID in From 2edd21803bc40679d989cc28e06dc31e4b89e3f3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:32:16 -0400 Subject: [PATCH 59/81] Improve unwrapping. Cleanup code. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 2 +- Sources/mas/Commands/Open.swift | 9 ++++++--- Sources/mas/Commands/Reset.swift | 2 +- Sources/mas/Controllers/StoreSearch.swift | 8 +++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 1c8afa0..f0d27ec 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -47,7 +47,7 @@ extension ISStoreAccount: StoreAccount { let password = password.isEmpty && !systemDialog - ? String(validatingUTF8: getpass("Password: "))! + ? String(validatingUTF8: getpass("Password: ")) ?? "" : password guard !password.isEmpty || systemDialog else { diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 4ae4247..82e98de 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -44,15 +44,18 @@ extension Mas { } url.scheme = masScheme + guard let urlString = url.string else { + printError("Unable to construct URL") + throw MASError.searchFailed + } do { - try openCommand.run(arguments: url.string!) + try openCommand.run(arguments: urlString) } catch { printError("Unable to launch open command") throw MASError.searchFailed } if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") + printError("Open failed: (\(openCommand.process.terminationReason)) \(openCommand.stderr)") throw MASError.searchFailed } } catch { diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 6698780..76a32bd 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -61,7 +61,7 @@ extension Mas { if kill.terminationStatus != 0, debug { let output = stderr.fileHandleForReading.readDataToEndOfFile() - printError("killall failed:\n\(String(data: output, encoding: String.Encoding.utf8)!)") + printError("killall failed:\n\(String(data: output, encoding: .utf8) ?? "Error info not available")") } // Wipe Download Directory diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index 12a46e0..166e7fd 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -78,16 +78,18 @@ extension StoreSearch { return nil } - components.queryItems = [ + var queryItems = [ URLQueryItem(name: "media", value: "software"), URLQueryItem(name: "entity", value: entity.rawValue), ] if let country { - components.queryItems!.append(URLQueryItem(name: "country", value: country)) + queryItems.append(URLQueryItem(name: "country", value: country)) } - components.queryItems!.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + + components.queryItems = queryItems return components.url } From 4d4dd02cd3d68d5ada3d72388176f08e0173f9aa Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:47:39 -0400 Subject: [PATCH 60/81] Improve lint configurations. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swift-format | 95 ++++++++++------ .swiftformat | 21 +++- .swiftlint.yml | 132 +++++++++++++++++++++- Sources/mas/AppStore/ISStoreAccount.swift | 2 + Tests/masTests/.swift-format | 62 ++++++++++ Tests/masTests/.swiftlint.yml | 2 + 6 files changed, 266 insertions(+), 48 deletions(-) create mode 100644 Tests/masTests/.swift-format diff --git a/.swift-format b/.swift-format index 9c12fd6..693d879 100644 --- a/.swift-format +++ b/.swift-format @@ -1,41 +1,62 @@ { - "indentation" : { - "spaces" : 4 + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 }, - "lineLength" : 120, - "rules" : { - "AllPublicDeclarationsHaveDocumentation" : false, - "AlwaysUseLowerCamelCase" : true, - "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : false, - "DoNotUseSemicolons" : true, - "DontRepeatTypeInStaticProperties" : true, - "FileScopedDeclarationPrivacy" : true, - "FullyIndirectEnum" : true, - "GroupNumericLiterals" : true, - "IdentifiersMustBeASCII" : true, - "NeverForceUnwrap" : false, - "NeverUseForceTry" : false, - "NeverUseImplicitlyUnwrappedOptionals" : false, - "NoAccessLevelOnExtensionDeclaration" : false, - "NoBlockComments" : true, - "NoCasesWithOnlyFallthrough" : true, - "NoEmptyTrailingClosureParentheses" : true, - "NoLabelsInCasePatterns" : true, - "NoLeadingUnderscores" : false, - "NoParensAroundConditions" : true, - "NoVoidReturnOnFunctionSignature" : true, - "OneCasePerLine" : true, - "OneVariableDeclarationPerLine" : true, - "OnlyOneTrailingClosureArgument" : true, - "OrderedImports" : true, - "ReturnVoidInsteadOfEmptyTuple" : true, - "UseLetInEveryBoundCaseVariable" : true, - "UseShorthandTypeNames" : true, - "UseSingleLinePropertyGetter" : true, - "UseSynthesizedInitializer" : true, - "UseTripleSlashForDocumentationComments" : true, - "ValidateDocumentationComments" : false + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": true, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": true, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true }, - "version" : 1 + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 } diff --git a/.swiftformat b/.swiftformat index 9e09924..90ad275 100644 --- a/.swiftformat +++ b/.swiftformat @@ -5,22 +5,33 @@ # https://github.com/nicklockwood/SwiftFormat#config-file # ---exclude docs/ - # Disabled rules ---disable blankLinesAroundMark ---disable consecutiveSpaces --disable hoistAwait --disable hoistPatternLet --disable hoistTry + +# Enable later --disable indent --disable trailingCommas # Enabled rules (disabled by default) ---enable trailingClosures +#--enable acronyms +#--enable blankLinesBetweenImports +--enable blockComments +--enable docComments +--enable isEmpty +--enable noExplicitOwnership +#--enable organizeDeclarations +--enable redundantProperty +--enable sortSwitchCases +--enable wrapConditionalBodies +--enable wrapEnumCases +--enable wrapMultilineConditionalAssignment +--enable wrapSwitchCases # Rule options --commas always --extensionacl on-declarations --importgrouping testable-last +--lineaftermarks false --ranges no-space diff --git a/.swiftlint.yml b/.swiftlint.yml index 1c9c9e3..4897eef 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,11 +5,131 @@ # https://github.com/realm/SwiftLint#configuration # --- +opt_in_rules: +- accessibility_label_for_image +- accessibility_trait_for_button +- anonymous_argument_in_multiline_closure +- array_init +- attributes +- closure_end_indentation +- closure_spacing +- collection_alignment +- comma_inheritance +- conditional_returns_on_newline +- contains_over_filter_count +- contains_over_filter_is_empty +- contains_over_first_not_nil +- contains_over_range_nil_comparison +- convenience_type +- direct_return +- discarded_notification_center_observer +- discouraged_assert +- discouraged_none_name +- discouraged_object_literal +- discouraged_optional_boolean +- discouraged_optional_collection +- empty_collection_literal +- empty_count +- empty_string +- empty_xctest_method +- enum_case_associated_values_count +- expiring_todo +- explicit_init +- extension_access_modifier +- fallthrough +- fatal_error_message +- file_name_no_space +- file_types_order +- first_where +- flatmap_over_map_reduce +- function_default_parameter_at_end +- ibinspectable_in_extension +- identical_operands +- implicit_return +- implicitly_unwrapped_optional +- indentation_width +- joined_default_parameter +- last_where +- legacy_multiple +- let_var_whitespace +- literal_expression_end_indentation +- local_doc_comment +- lower_acl_than_parent +- missing_docs +- modifier_order +- multiline_arguments +- multiline_arguments_brackets +- multiline_function_chains +- multiline_literal_brackets +- multiline_parameters +- multiline_parameters_brackets +- nimble_operator +- no_empty_block +- no_extension_access_modifier +- no_magic_numbers +- non_overridable_class_declaration +- nslocalizedstring_key +- nslocalizedstring_require_bundle +- object_literal +- operator_usage_whitespace +- optional_enum_case_matching +- overridden_super_call +- override_in_extension +- pattern_matching_keywords +- period_spacing +- prefer_key_path +- prefer_self_in_static_references +- prefer_self_type_over_type_of_self +- prefer_zero_over_explicit_init +- private_action +- private_outlet +- private_subject +- private_swiftui_state +- prohibited_interface_builder +- prohibited_super_call +- quick_discouraged_focused_test +- raw_value_for_camel_cased_codable_enum +- reduce_into +- redundant_nil_coalescing +- redundant_self_in_closure +- redundant_type_annotation +- required_enum_case +- return_value_from_void_function +- self_binding +- shorthand_argument +- shorthand_optional_binding +- single_test_class +- sorted_first_last +- sorted_imports +- static_operator +- strict_fileprivate +- strong_iboutlet +- superfluous_else +- switch_case_on_newline +- test_case_accessibility +- toggle_bool +- trailing_closure +- type_contents_order +- unavailable_function +- unhandled_throwing_task +- unneeded_parentheses_in_closure_argument +- unowned_variable_capture +- untyped_error_in_catch +- unused_parameter +- vertical_parameter_alignment_on_call +- vertical_whitespace_closing_braces +- vertical_whitespace_opening_braces +- weak_delegate +- xct_specific_matcher +- yoda_condition disabled_rules: -- non_optional_string_data_conversion +- function_body_length - trailing_comma -excluded: -- docs -opening_brace: - ignore_multiline_function_signatures: true - ignore_multiline_statement_conditions: true +file_types_order: + order: [ + [main_type], + [supporting_type], + [extension], + [preview_provider], + [library_content_provider] + ] diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index f0d27ec..ecb3313 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -33,10 +33,12 @@ extension ISStoreAccount: StoreAccount { } static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { + // swift-format-ignore: UseEarlyExits if #available(macOS 10.13, *) { // Signing in is no longer possible as of High Sierra. // https://github.com/mas-cli/mas/issues/164 return Promise(error: MASError.notSupported) + // swiftlint:disable:next superfluous_else } else { return primaryAccount diff --git a/Tests/masTests/.swift-format b/Tests/masTests/.swift-format new file mode 100644 index 0000000..571f02f --- /dev/null +++ b/Tests/masTests/.swift-format @@ -0,0 +1,62 @@ +{ + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 + }, + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + }, + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 +} diff --git a/Tests/masTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml index d234b91..409fabb 100644 --- a/Tests/masTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -9,3 +9,5 @@ disabled_rules: - force_cast - force_try - function_body_length +- implicitly_unwrapped_optional +- no_magic_numbers From c36a797ac2d5a092fd5971c22a6f9e9acc1021ec Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:54:31 -0400 Subject: [PATCH 61/81] SwiftLint opt_in_rules: - all Resolve #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 141 ++++++---------------------------- Tests/masTests/.swiftlint.yml | 1 - 2 files changed, 25 insertions(+), 117 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 4897eef..3d19626 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -6,125 +6,34 @@ # --- opt_in_rules: -- accessibility_label_for_image -- accessibility_trait_for_button -- anonymous_argument_in_multiline_closure -- array_init -- attributes -- closure_end_indentation -- closure_spacing -- collection_alignment -- comma_inheritance -- conditional_returns_on_newline -- contains_over_filter_count -- contains_over_filter_is_empty -- contains_over_first_not_nil -- contains_over_range_nil_comparison -- convenience_type -- direct_return -- discarded_notification_center_observer -- discouraged_assert -- discouraged_none_name -- discouraged_object_literal -- discouraged_optional_boolean -- discouraged_optional_collection -- empty_collection_literal -- empty_count -- empty_string -- empty_xctest_method -- enum_case_associated_values_count -- expiring_todo -- explicit_init -- extension_access_modifier -- fallthrough -- fatal_error_message -- file_name_no_space -- file_types_order -- first_where -- flatmap_over_map_reduce -- function_default_parameter_at_end -- ibinspectable_in_extension -- identical_operands -- implicit_return -- implicitly_unwrapped_optional -- indentation_width -- joined_default_parameter -- last_where -- legacy_multiple -- let_var_whitespace -- literal_expression_end_indentation -- local_doc_comment -- lower_acl_than_parent -- missing_docs -- modifier_order -- multiline_arguments -- multiline_arguments_brackets -- multiline_function_chains -- multiline_literal_brackets -- multiline_parameters -- multiline_parameters_brackets -- nimble_operator -- no_empty_block -- no_extension_access_modifier -- no_magic_numbers -- non_overridable_class_declaration -- nslocalizedstring_key -- nslocalizedstring_require_bundle -- object_literal -- operator_usage_whitespace -- optional_enum_case_matching -- overridden_super_call -- override_in_extension -- pattern_matching_keywords -- period_spacing -- prefer_key_path -- prefer_self_in_static_references -- prefer_self_type_over_type_of_self -- prefer_zero_over_explicit_init -- private_action -- private_outlet -- private_subject -- private_swiftui_state -- prohibited_interface_builder -- prohibited_super_call -- quick_discouraged_focused_test -- raw_value_for_camel_cased_codable_enum -- reduce_into -- redundant_nil_coalescing -- redundant_self_in_closure -- redundant_type_annotation -- required_enum_case -- return_value_from_void_function -- self_binding -- shorthand_argument -- shorthand_optional_binding -- single_test_class -- sorted_first_last -- sorted_imports -- static_operator -- strict_fileprivate -- strong_iboutlet -- superfluous_else -- switch_case_on_newline -- test_case_accessibility -- toggle_bool -- trailing_closure -- type_contents_order -- unavailable_function -- unhandled_throwing_task -- unneeded_parentheses_in_closure_argument -- unowned_variable_capture -- untyped_error_in_catch -- unused_parameter -- vertical_parameter_alignment_on_call -- vertical_whitespace_closing_braces -- vertical_whitespace_opening_braces -- weak_delegate -- xct_specific_matcher -- yoda_condition +- all disabled_rules: +- balanced_xctest_lifecycle +- closure_body_length +- contrasted_opening_brace +- explicit_acl +- explicit_enum_raw_value +- explicit_top_level_acl +- explicit_type_interface +- file_header +- file_name +- final_test_case +- force_unwrapping - function_body_length +- inert_defer +- legacy_objc_type +- no_grouping_extension +- number_separator +- one_declaration_per_file +- prefer_nimble +- prefixed_toplevel_constant +- quick_discouraged_call +- quick_discouraged_pending_test +- required_deinit +- sorted_enum_cases - trailing_comma +- unused_capture_list +- vertical_whitespace_between_cases file_types_order: order: [ [main_type], diff --git a/Tests/masTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml index 409fabb..df984c3 100644 --- a/Tests/masTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -8,6 +8,5 @@ disabled_rules: - force_cast - force_try -- function_body_length - implicitly_unwrapped_optional - no_magic_numbers From 417fb824b4490ec64ca6c746612e4663cd0c632a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 07:56:23 -0400 Subject: [PATCH 62/81] Upgrade GitHub runners from `macos-14` to `macos-15`. Resolve #594 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9e42026..6c21b68 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -31,7 +31,7 @@ jobs: # https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # https://github.com/mas-cli/mas/actions/runners - runs-on: macos-14 + runs-on: macos-15 steps: # https://github.com/actions/checkout#usage From 9e3b079ec83a1314f7f97527574f6e8953f7d30d Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:46:50 -0400 Subject: [PATCH 63/81] Update `lint`. - Change Swift bird emoji to ensure consistent spacing. - Suppress output on GitHub runners. - Simplify `grep` patterns. Partial #594 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- script/lint | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/script/lint b/script/lint index c362dbb..58de5aa 100755 --- a/script/lint +++ b/script/lint @@ -30,14 +30,14 @@ done exit_code=0 for source in Package.swift Sources Tests; do - printf -- $'--> 🕊 %s swift-format\n' "${source}" + printf -- $'--> 🦅 %s swift-format\n' "${source}" swift-format lint --strict --recursive "${source}" ((exit_code |= "${?}")) - printf -- $'--> 🕊 %s swiftformat\n' "${source}" + printf -- $'--> 🦅 %s swiftformat\n' "${source}" script -q /dev/null swiftformat --lint --strict "${source}" | - (grep -vxE $'Running SwiftFormat\\.\\.\\.\r|\\(lint mode - no files will be changed\\.\\)\r|Reading (?:config|swift-version) file at .*|\033\[32mSwiftFormat completed in \\d+\\.\\d+s\\.\033\\[0m\r|0/\\d+ files require formatting\\.\r' || true) + (grep -vxE '(?:\^D\x08{2})?Running SwiftFormat\.{3}\r|\(lint mode - no files will be changed\.\)\r|Reading (?:config|swift-version) file at .*|\x1b\[32mSwiftFormat completed in \d+\.\d+s\.\x1b\[0m\r|0/\d+ files require formatting\.\r' || true) ((exit_code |= "${?}")) - printf -- $'--> 🕊 %s swiftlint\n' "${source}" + printf -- $'--> 🦅 %s swiftlint\n' "${source}" swiftlint --strict --quiet "${source}" 2> \ >((grep -vxF $'warning: Configuration option \'allow_multiline_func\' in \'opening_brace\' rule is deprecated. Use the option \'ignore_multiline_function_signatures\' instead.' || true) >&2) ((exit_code |= "${?}")) @@ -66,7 +66,7 @@ PAGER='cat' git diff --check printf -- $'--> 🌀 Periphery\n' script -q /dev/null periphery scan --strict --quiet --disable-update-check | - (grep -vxF $'\033[0;1;32m* \033[0;0m\033[0;1mNo unused code detected.\033[0;0m\r\n' || true) + (grep -vxE '(?:\x1b\[0;1;32m|\^D\x08{2})\* (?:\x1b\[0;0m\x1b\[0;1m)?No unused code detected\.(?:\x1b\[0;0m)?\r' || true) ((exit_code |= "${?}")) exit "${exit_code}" From cab684ba600c9d99b39c7552ad138e820ec286da Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:49:43 -0400 Subject: [PATCH 64/81] Install `swiftlint` via `Brewfile` if macOS >= 13 (install breaks on macOS <= 12). Only install `peripheryapp/periphery` tap if macOS >= 13 (install breaks on macOS <= 12). Resolve #594 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Brewfile | 6 ++---- script/bootstrap | 5 ----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Brewfile b/Brewfile index 2638c4e..b198231 100644 --- a/Brewfile +++ b/Brewfile @@ -5,10 +5,8 @@ brew "swift-format" brew "swiftformat" brew "trash" -# Already installed on GitHub Actions runner. -# brew "swiftlint" - -tap "peripheryapp/periphery" if OS.mac? && MacOS.version >= :ventura + brew "swiftlint" + tap "peripheryapp/periphery" cask "periphery" end diff --git a/script/bootstrap b/script/bootstrap index 7448a03..09c06ac 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -20,8 +20,3 @@ printf $'==> 👢 Bootstrapping (%s)\n' "$(script/version)" # Install Homebrew tools rm -f Brewfile.lock.json brew bundle install --no-upgrade --verbose - -# swiftlint is already installed on GitHub Actions runners. -if [[ ! -x "$(command -v swiftlint)" ]]; then - brew install swiftlint -fi From 4e0a471d81c9e11103236a58cfc1c1d2bddf71a5 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:00:34 -0400 Subject: [PATCH 65/81] Remove unnecessary function from `AppLibrary`. Partial #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/AppLibrary.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index ebdcd70..d05e180 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -13,12 +13,6 @@ protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } - /// Finds an app for appID. - /// - /// - Parameter appID: app ID for app. - /// - Returns: SoftwareProduct of app if found; nil otherwise. - func installedApp(withAppID appID: AppID) -> SoftwareProduct? - /// Uninstalls an app. /// /// - Parameter app: App to be removed. From 222646159d2a0d4162d29d02c1bafad16753bfa0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:51:59 -0400 Subject: [PATCH 66/81] =?UTF-8?q?Refactor=20`AppLibrary.installedApp(?= =?UTF-8?q?=E2=80=A6)`=20as=20`AppLibrary.installedApps(=E2=80=A6)`=20beca?= =?UTF-8?q?use=20multiple=20installed=20apps=20can=20have=20the=20same=20a?= =?UTF-8?q?pp=20ID=20or=20app=20name.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Install.swift | 4 ++-- Sources/mas/Commands/Lucky.swift | 4 ++-- Sources/mas/Commands/Purchase.swift | 4 ++-- Sources/mas/Commands/Uninstall.swift | 14 +++++++++----- Sources/mas/Commands/Upgrade.swift | 10 +++++----- Sources/mas/Controllers/AppLibrary.swift | 20 ++++++++++---------- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 88c2688..1ba12b0 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -29,8 +29,8 @@ extension Mas { func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results let appIDs = appIDs.filter { appID in - if let product = appLibrary.installedApp(withAppID: appID), !force { - printWarning("\(product.appName) is already installed") + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force { + printWarning("\(appName) is already installed") return false } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 16c7948..296dde7 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -58,8 +58,8 @@ extension Mas { /// - Throws: Any error that occurs while attempting to install the app. private func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results - if let product = appLibrary.installedApp(withAppID: appID), !force { - printWarning("\(product.appName) is already installed") + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force { + printWarning("\(appName) is already installed") } else { do { try downloadAll([appID]).wait() diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 841d26d..58a7a48 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -26,8 +26,8 @@ extension Mas { func run(appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results let appIDs = appIDs.filter { appID in - if let product = appLibrary.installedApp(withAppID: appID) { - printWarning("\(product.appName) has already been purchased.") + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName { + printWarning("\(appName) has already been purchased.") return false } diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 632c84b..afb4b12 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -7,8 +7,7 @@ // import ArgumentParser -import CommerceKit -import StoreFoundation +import Foundation extension Mas { /// Command which uninstalls apps managed by the Mac App Store. @@ -29,16 +28,21 @@ extension Mas { } func run(appLibrary: AppLibrary) throws { - guard let product = appLibrary.installedApp(withAppID: appID) else { + let installedApps = appLibrary.installedApps(withAppID: appID) + guard !installedApps.isEmpty else { throw MASError.notInstalled } if dryRun { - printInfo("\(product.appName) \(product.bundlePath)") + for installedApp in installedApps { + printInfo("\(installedApp.appName) \(installedApp.bundlePath)") + } printInfo("(not removed, dry run)") } else { do { - try appLibrary.uninstallApp(app: product) + for installedApp in installedApps { + try appLibrary.uninstallApp(app: installedApp) + } } catch { throw MASError.uninstallFailed } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 4b3740b..83c4788 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -58,14 +58,14 @@ extension Mas { let apps = appIDs.isEmpty ? appLibrary.installedApps - : appIDs.compactMap { appID in + : appIDs.flatMap { appID in if let appID = AppID(appID) { - // argument is an AppID, lookup app by id using argument - return appLibrary.installedApp(withAppID: appID) + // argument is an AppID, lookup apps by id using argument + return appLibrary.installedApps(withAppID: appID) } - // argument is not an AppID, lookup app by name using argument - return appLibrary.installedApp(named: appID) + // argument is not an AppID, lookup apps by name using argument + return appLibrary.installedApps(named: appID) } let promises = apps.map { installedApp in diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index d05e180..9bcebc7 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -22,20 +22,20 @@ protocol AppLibrary { /// Common logic extension AppLibrary { - /// Finds an app for appID. + /// Finds all installed instances of apps whose app ID is `appID`. /// - /// - Parameter appID: app ID for app. - /// - Returns: SoftwareProduct of app if found; nil otherwise. - func installedApp(withAppID appID: AppID) -> SoftwareProduct? { + /// - Parameter appID: app ID for app(s). + /// - Returns: [SoftwareProduct] of matching apps. + func installedApps(withAppID appID: AppID) -> [SoftwareProduct] { let appID = NSNumber(value: appID) - return installedApps.first { $0.itemIdentifier == appID } + return installedApps.filter { $0.itemIdentifier == appID } } - /// Finds an app by name. + /// Finds all installed instances of apps whose name is `appName`. /// - /// - Parameter appName: Full title of an app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(named appName: String) -> SoftwareProduct? { - installedApps.first { $0.appName == appName } + /// - Parameter appName: Full name of app(s). + /// - Returns: [SoftwareProduct] of matching apps. + func installedApps(named appName: String) -> [SoftwareProduct] { + installedApps.filter { $0.appName == appName } } } From b0d2f23465f01bc82b06d9d98524f79f2af801c3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:57:34 -0400 Subject: [PATCH 67/81] Add `appID` parameter to `MASError.notInstalled()`. Partial #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Errors/MASError.swift | 6 +++--- Tests/masTests/Commands/UninstallSpec.swift | 4 ++-- Tests/masTests/Controllers/AppLibraryMock.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index afb4b12..25f8a6f 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -30,7 +30,7 @@ extension Mas { func run(appLibrary: AppLibrary) throws { let installedApps = appLibrary.installedApps(withAppID: appID) guard !installedApps.isEmpty else { - throw MASError.notInstalled + throw MASError.notInstalled(appID: appID) } if dryRun { diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 28e6754..7065673 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -27,7 +27,7 @@ enum MASError: Error, Equatable { case noSearchResultsFound case noVendorWebsite - case notInstalled + case notInstalled(appID: AppID) case uninstallFailed case noData @@ -80,8 +80,8 @@ extension MASError: CustomStringConvertible { return "No results found" case .noVendorWebsite: return "App does not have a vendor website" - case .notInstalled: - return "Not installed" + case .notInstalled(let appID): + return "No apps installed with app ID \(appID)" case .uninstallFailed: return "Uninstall failed" case .noData: diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index c6426c3..2d171cf 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -38,7 +38,7 @@ public class UninstallSpec: QuickSpec { expect { try uninstall.run(appLibrary: mockLibrary) } - .to(throwError(MASError.notInstalled)) + .to(throwError(MASError.notInstalled(appID: appID))) } it("finds an app") { mockLibrary.installedApps.append(app) @@ -58,7 +58,7 @@ public class UninstallSpec: QuickSpec { expect { try uninstall.run(appLibrary: mockLibrary) } - .to(throwError(MASError.notInstalled)) + .to(throwError(MASError.notInstalled(appID: appID))) } it("removes an app") { mockLibrary.installedApps.append(app) diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift index f763280..b2a9215 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -15,7 +15,7 @@ class AppLibraryMock: AppLibrary { if !installedApps.contains(where: { product -> Bool in app.itemIdentifier == product.itemIdentifier }) { - throw MASError.notInstalled + throw MASError.notInstalled(appID: app.itemIdentifier.appIDValue) } // Special case for testing where we pretend the trash command failed diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index fdc044e..06e1649 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -109,8 +109,8 @@ class MASErrorTestCase: XCTestCase { } func testNotInstalled() { - error = .notInstalled - XCTAssertEqual(error.description, "Not installed") + error = .notInstalled(appID: 123) + XCTAssertEqual(error.description, "No apps installed with app ID 123") } func testUninstallFailed() { From 06ee9608be63ba88e5a4396800e331f70fc8c2ca Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:04:02 -0400 Subject: [PATCH 68/81] Improve output. Improve errors. Simplify code. Partial #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Controllers/MasAppLibrary.swift | 11 ++--------- Sources/mas/Errors/MASError.swift | 3 +++ Tests/masTests/Commands/UninstallSpec.swift | 14 ++++++++------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 25f8a6f..4e1e591 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -35,7 +35,7 @@ extension Mas { if dryRun { for installedApp in installedApps { - printInfo("\(installedApp.appName) \(installedApp.bundlePath)") + printInfo("'\(installedApp.appName)' '\(installedApp.bundlePath)'") } printInfo("(not removed, dry run)") } else { diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index 9325af4..cb3b45f 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -38,8 +38,8 @@ class MasAppLibrary: AppLibrary { /// - Parameter app: App to be removed. /// - Throws: Error if there is a problem. func uninstallApp(app: SoftwareProduct) throws { - if !userIsRoot() { - printWarning("Apps installed from the Mac App Store require root permission to remove.") + if NSUserName() != "root" { + throw MASError.macOSUserMustBeRoot } let appUrl = URL(fileURLWithPath: app.bundlePath) @@ -55,11 +55,4 @@ class MasAppLibrary: AppLibrary { throw MASError.uninstallFailed } } - - /// Detects whether the current user is root. - /// - /// - Returns: true if the current user is root; false otherwise - private func userIsRoot() -> Bool { - NSUserName() == "root" - } } diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 7065673..4eba734 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -29,6 +29,7 @@ enum MASError: Error, Equatable { case notInstalled(appID: AppID) case uninstallFailed + case macOSUserMustBeRoot case noData case jsonParsing(data: Data?) @@ -84,6 +85,8 @@ extension MASError: CustomStringConvertible { return "No apps installed with app ID \(appID)" case .uninstallFailed: return "Uninstall failed" + case .macOSUserMustBeRoot: + return "Apps installed from the Mac App Store require root permission to remove." case .noData: return "Service did not return data" case .jsonParsing(let data): diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 2d171cf..c2587fc 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -43,9 +43,11 @@ public class UninstallSpec: QuickSpec { it("finds an app") { mockLibrary.installedApps.append(app) expect { - try uninstall.run(appLibrary: mockLibrary) + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } } - .toNot(throwError()) + == "==> 'Some App' '/tmp/Some.app'\n==> (not removed, dry run)\n" } } context("wet run") { @@ -67,12 +69,12 @@ public class UninstallSpec: QuickSpec { try uninstall.run(appLibrary: mockLibrary) } } - == "==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" + .toNot(throwError()) } it("fails if there is a problem with the trash command") { - var brokenUninstall = app - brokenUninstall.bundlePath = "/dev/null" - mockLibrary.installedApps.append(brokenUninstall) + var brokenApp = app + brokenApp.bundlePath = "/dev/null" + mockLibrary.installedApps.append(brokenApp) expect { try uninstall.run(appLibrary: mockLibrary) } From 53c64b17581429caa3a1ad0fcf6b677574231916 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:11:52 -0400 Subject: [PATCH 69/81] Improve errors. Partial #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Controllers/MasAppLibrary.swift | 3 +-- Sources/mas/Errors/MASError.swift | 7 +++++-- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Controllers/AppLibraryMock.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 4e1e591..9d7c3a9 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -44,7 +44,7 @@ extension Mas { try appLibrary.uninstallApp(app: installedApp) } } catch { - throw MASError.uninstallFailed + throw error as? MASError ?? MASError.uninstallFailed(error: error as NSError) } } } diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index cb3b45f..7554d3c 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -51,8 +51,7 @@ class MasAppLibrary: AppLibrary { printInfo("App moved to trash: \(path)") } } catch { - printError("Unable to move app to trash.") - throw MASError.uninstallFailed + throw MASError.uninstallFailed(error: error as NSError) } } } diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 4eba734..564afb6 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -28,7 +28,7 @@ enum MASError: Error, Equatable { case noVendorWebsite case notInstalled(appID: AppID) - case uninstallFailed + case uninstallFailed(error: NSError?) case macOSUserMustBeRoot case noData @@ -83,7 +83,10 @@ extension MASError: CustomStringConvertible { return "App does not have a vendor website" case .notInstalled(let appID): return "No apps installed with app ID \(appID)" - case .uninstallFailed: + case .uninstallFailed(let error): + if let error { + return "Uninstall failed: \(error.localizedDescription)" + } return "Uninstall failed" case .macOSUserMustBeRoot: return "Apps installed from the Mac App Store require root permission to remove." diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index c2587fc..dd342a0 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -78,7 +78,7 @@ public class UninstallSpec: QuickSpec { expect { try uninstall.run(appLibrary: mockLibrary) } - .to(throwError(MASError.uninstallFailed)) + .to(throwError(MASError.uninstallFailed(error: nil))) } } } diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift index b2a9215..e4daad1 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -20,7 +20,7 @@ class AppLibraryMock: AppLibrary { // Special case for testing where we pretend the trash command failed if app.bundlePath == "/dev/null" { - throw MASError.uninstallFailed + throw MASError.uninstallFailed(error: nil) } // Success is the default, watch out for false positives! diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index 06e1649..47c83d6 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -114,7 +114,7 @@ class MASErrorTestCase: XCTestCase { } func testUninstallFailed() { - error = .uninstallFailed + error = .uninstallFailed(error: nil) XCTAssertEqual(error.description, "Uninstall failed") } From 98c85ac6d628a881d53b4e32027a6832e6f57ccc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:20:31 -0400 Subject: [PATCH 70/81] Delete apps via Scripting Bridge to Finder. Resolve #313 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 1 + Sources/mas/Commands/Uninstall.swift | 25 +- Sources/mas/Controllers/AppLibrary.swift | 8 +- Sources/mas/Controllers/Finder.swift | 733 ++++++++++++++++++ Sources/mas/Controllers/MasAppLibrary.swift | 134 +++- Sources/mas/Errors/MASError.swift | 4 + Tests/masTests/Commands/UninstallSpec.swift | 6 +- .../masTests/Controllers/AppLibraryMock.swift | 12 +- 8 files changed, 885 insertions(+), 38 deletions(-) create mode 100644 Sources/mas/Controllers/Finder.swift diff --git a/.swiftformat b/.swiftformat index 90ad275..9089de8 100644 --- a/.swiftformat +++ b/.swiftformat @@ -32,6 +32,7 @@ # Rule options --commas always --extensionacl on-declarations +--hexliteralcase lowercase --importgrouping testable-last --lineaftermarks false --ranges no-space diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 9d7c3a9..b73378b 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -28,6 +28,21 @@ extension Mas { } func run(appLibrary: AppLibrary) throws { + guard NSUserName() == "root" else { + throw MASError.macOSUserMustBeRoot + } + + guard let username = getSudoUsername() else { + throw MASError.runtimeError("Could not determine the original username") + } + + guard + let uid = getSudoUID(), + seteuid(uid) == 0 + else { + throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'") + } + let installedApps = appLibrary.installedApps(withAppID: appID) guard !installedApps.isEmpty else { throw MASError.notInstalled(appID: appID) @@ -39,13 +54,11 @@ extension Mas { } printInfo("(not removed, dry run)") } else { - do { - for installedApp in installedApps { - try appLibrary.uninstallApp(app: installedApp) - } - } catch { - throw error as? MASError ?? MASError.uninstallFailed(error: error as NSError) + guard seteuid(0) == 0 else { + throw MASError.runtimeError("Failed to revert effective user from '\(username)' back to 'root'") } + + try appLibrary.uninstallApps(atPaths: installedApps.map(\.bundlePath)) } } } diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index 9bcebc7..cd0432e 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -13,11 +13,11 @@ protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } - /// Uninstalls an app. + /// Uninstalls all apps located at any of the elements of `appPaths`. /// - /// - Parameter app: App to be removed. - /// - Throws: Error if there is a problem. - func uninstallApp(app: SoftwareProduct) throws + /// - Parameter appPaths: Paths to apps to be uninstalled. + /// - Throws: Error if any problem occurs. + func uninstallApps(atPaths appPaths: [String]) throws } /// Common logic diff --git a/Sources/mas/Controllers/Finder.swift b/Sources/mas/Controllers/Finder.swift new file mode 100644 index 0000000..a8133c2 --- /dev/null +++ b/Sources/mas/Controllers/Finder.swift @@ -0,0 +1,733 @@ +// swift-format-ignore-file +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable attributes discouraged_none_name file_length file_types_order identifier_name +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable implicitly_unwrapped_optional line_length missing_docs +import AppKit +import ScriptingBridge + +// MARK: FinderEdfm +@objc +public enum FinderEdfm: AEKeyword { + case macOSFormat = 0x6466_6866 // 'dfhf' + case macOSExtendedFormat = 0x6466_682b // 'dfh+' + case ufsFormat = 0x6466_7566 // 'dfuf' + case nfsFormat = 0x6466_6e66 // 'dfnf' + case audioFormat = 0x6466_6175 // 'dfau' + case proDOSFormat = 0x6466_7072 // 'dfpr' + case msdosFormat = 0x6466_6d73 // 'dfms' + case ntfsFormat = 0x6466_6e74 // 'dfnt' + case iso9660Format = 0x6466_3936 // 'df96' + case highSierraFormat = 0x6466_6873 // 'dfhs' + case quickTakeFormat = 0x6466_7174 // 'dfqt' + case applePhotoFormat = 0x6466_7068 // 'dfph' + case appleShareFormat = 0x6466_6173 // 'dfas' + case udfFormat = 0x6466_7564 // 'dfud' + case webDAVFormat = 0x6466_7764 // 'dfwd' + case ftpFormat = 0x6466_6674 // 'dfft' + case packetWrittenUDFFormat = 0x6466_7075 // 'dfpu' + case xsanFormat = 0x6466_6163 // 'dfac' + case apfsFormat = 0x6466_6170 // 'dfap' + case exFATFormat = 0x6466_7866 // 'dfxf' + case smbFormat = 0x6466_736d // 'dfsm' + case unknownFormat = 0x6466_3f3f // 'df??' +} + +// MARK: FinderIpnl +@objc +public enum FinderIpnl: AEKeyword { + case generalInformationPanel = 0x6770_6e6c // 'gpnl' + case sharingPanel = 0x7370_6e6c // 'spnl' + case memoryPanel = 0x6d70_6e6c // 'mpnl' + case previewPanel = 0x7670_6e6c // 'vpnl' + case applicationPanel = 0x6170_6e6c // 'apnl' + case languagesPanel = 0x706b_6c67 // 'pklg' + case pluginsPanel = 0x706b_7067 // 'pkpg' + case nameExtensionPanel = 0x6e70_6e6c // 'npnl' + case commentsPanel = 0x6370_6e6c // 'cpnl' + case contentIndexPanel = 0x6369_6e6c // 'cinl' + case burningPanel = 0x6270_6e6c // 'bpnl' + case moreInfoPanel = 0x6d69_6e6c // 'minl' + case simpleHeaderPanel = 0x7368_6e6c // 'shnl' +} + +// MARK: FinderPple +@objc +public enum FinderPple: AEKeyword { + case generalPreferencesPanel = 0x7067_6e70 // 'pgnp' + case labelPreferencesPanel = 0x706c_6270 // 'plbp' + case sidebarPreferencesPanel = 0x7073_6964 // 'psid' + case advancedPreferencesPanel = 0x7061_6476 // 'padv' +} + +// MARK: FinderPriv +@objc +public enum FinderPriv: AEKeyword { + case readOnly = 0x7265_6164 // 'read' + case readWrite = 0x7264_7772 // 'rdwr' + case writeOnly = 0x7772_6974 // 'writ' + case none = 0x6e6f_6e65 // 'none' +} + +// MARK: FinderEcvw +@objc +public enum FinderEcvw: AEKeyword { + case iconView = 0x6963_6e76 // 'icnv' + case listView = 0x6c73_7677 // 'lsvw' + case columnView = 0x636c_7677 // 'clvw' + case groupView = 0x6772_7677 // 'grvw' + case flowView = 0x666c_7677 // 'flvw' +} + +// MARK: FinderEarr +@objc +public enum FinderEarr: AEKeyword { + case notArranged = 0x6e61_7272 // 'narr' + case snapToGrid = 0x6772_6461 // 'grda' + case arrangedByName = 0x6e61_6d61 // 'nama' + case arrangedByModificationDate = 0x6d64_7461 // 'mdta' + case arrangedByCreationDate = 0x6364_7461 // 'cdta' + case arrangedBySize = 0x7369_7a61 // 'siza' + case arrangedByKind = 0x6b69_6e61 // 'kina' + case arrangedByLabel = 0x6c61_6261 // 'laba' +} + +// MARK: FinderEpos +@objc +public enum FinderEpos: AEKeyword { + case right = 0x6c72_6774 // 'lrgt' + case bottom = 0x6c62_6f74 // 'lbot' +} + +// MARK: FinderSodr +@objc +public enum FinderSodr: AEKeyword { + case normal = 0x736e_726d // 'snrm' + case reversed = 0x7372_7673 // 'srvs' +} + +// MARK: FinderElsv +@objc +public enum FinderElsv: AEKeyword { + case nameColumn = 0x656c_736e // 'elsn' + case modificationDateColumn = 0x656c_736d // 'elsm' + case creationDateColumn = 0x656c_7363 // 'elsc' + case sizeColumn = 0x656c_7373 // 'elss' + case kindColumn = 0x656c_736b // 'elsk' + case labelColumn = 0x656c_736c // 'elsl' + case versionColumn = 0x656c_7376 // 'elsv' + case commentColumn = 0x656c_7343 // 'elsC' +} + +// MARK: FinderLvic +@objc +public enum FinderLvic: AEKeyword { + case smallIcon = 0x736d_6963 // 'smic' + case largeIcon = 0x6c67_6963 // 'lgic' +} + +@objc +public protocol SBObjectProtocol: NSObjectProtocol { + func get() -> Any! +} + +@objc +public protocol SBApplicationProtocol: SBObjectProtocol { + var delegate: SBApplicationDelegate! { get set } + var isRunning: Bool { get } + + func activate() +} + +// MARK: FinderGenericMethods +@objc +public protocol FinderGenericMethods { + @objc optional func openUsing(_ using_: SBObject!, withProperties: [AnyHashable: Any]!) // Open the specified object(s) + @objc optional func printWithProperties(_ withProperties: [AnyHashable: Any]!) // Print the specified object(s) + @objc optional func activate() // Activate the specified window (or the Finder) + @objc optional func close() // Close an object + @objc optional func dataSizeAs(_ as: NSNumber!) -> Int // Return the size in bytes of an object + @objc optional func delete() -> SBObject // Move an item from its container to the trash + @objc optional func duplicateTo(_ to: SBObject!, replacing: Bool, routingSuppressed: Bool, exactCopy: Bool) -> SBObject // Duplicate one or more object(s) + @objc optional func exists() -> Bool // Verify if an object exists + @objc optional func moveTo(_ to: SBObject!, replacing: Bool, positionedAt: [Any]!, routingSuppressed: Bool) -> SBObject // Move object(s) to a new location + @objc optional func select() // Select the specified object(s) + @objc optional func sortBy(_ by: Selector) -> SBObject // Return the specified object(s) in a sorted list + @objc optional func cleanUpBy(_ by: Selector) // Arrange items in window nicely (only applies to open windows in icon view that are not kept arranged) + @objc optional func eject() // Eject the specified disk(s) + @objc optional func emptySecurity(_ security: Bool) // Empty the trash + @objc optional func erase() // (NOT AVAILABLE) Erase the specified disk(s) + @objc optional func reveal() // Bring the specified object(s) into view + @objc optional func updateNecessity(_ necessity: Bool, registeringApplications: Bool) // Update the display of the specified object(s) to match their on-disk representation +} + +// MARK: FinderApplication +@objc +public protocol FinderApplication: SBApplicationProtocol { + @objc optional var clipboard: SBObject { get } // (NOT AVAILABLE YET) the Finder’s clipboard window (copy) + @objc optional var name: String { get } // the Finder’s name (copy) + @objc optional var visible: Bool { get } // Is the Finder’s layer visible? + @objc optional var frontmost: Bool { get } // Is the Finder the frontmost process? + @objc optional var selection: SBObject { get } // the selection in the frontmost Finder window (copy) + @objc optional var insertionLocation: SBObject { get } // the container in which a new folder would appear if “New Folder” was selected (copy) + @objc optional var productVersion: String { get } // the version of the System software running on this computer (copy) + @objc optional var version: String { get } // the version of the Finder (copy) + @objc optional var startupDisk: FinderDisk { get } // the startup disk (copy) + @objc optional var desktop: FinderDesktopObject { get } // the desktop (copy) + @objc optional var trash: FinderTrashObject { get } // the trash (copy) + @objc optional var home: FinderFolder { get } // the home directory (copy) + @objc optional var computerContainer: FinderComputerObject { get } // the computer location (as in Go > Computer) (copy) + @objc optional var FinderPreferences: FinderPreferences { get } // Various preferences that apply to the Finder as a whole (copy) + + @objc optional var desktopPicture: FinderFile { get } // the desktop picture of the main monitor + + @objc optional func setDesktopPicture(_ desktopPicture: FinderFile!) // the desktop picture of the main monitor + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func disks() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray + @objc optional func windows() -> SBElementArray + @objc optional func FinderWindows() -> SBElementArray + @objc optional func clippingWindows() -> SBElementArray + + @objc optional func quit() // Quit the Finder + @objc optional func activate() // Activate the specified window (or the Finder) + @objc optional func copy() // (NOT AVAILABLE YET) Copy the selected items to the clipboard (the Finder must be the front application) + @objc optional func eject() // Eject the specified disk(s) + @objc optional func emptySecurity(_ security: Bool) // Empty the trash + @objc optional func restart() // Restart the computer + @objc optional func shutDown() // Shut Down the computer + @objc optional func sleep() // Put the computer to sleep + @objc optional func setVisible(_ visible: Bool) // Is the Finder’s layer visible? + @objc optional func setFrontmost(_ frontmost: Bool) // Is the Finder the frontmost process? + @objc optional func setSelection(_ selection: SBObject!) // the selection in the frontmost Finder window +} + +extension SBApplication: FinderApplication {} + +// MARK: FinderItem +@objc +public protocol FinderItem: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name of the item (copy) + @objc optional var displayedName: String { get } // the user-visible name of the item (copy) + @objc optional var nameExtension: String { get } // the name extension of the item (such as “txt”) (copy) + @objc optional var extensionHidden: Bool { get } // Is the item's extension hidden from the user? + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var container: SBObject { get } // the container of the item (copy) + @objc optional var disk: SBObject { get } // the disk on which the item is stored (copy) + @objc optional var position: NSPoint { get } // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons) + @objc optional var desktopPosition: NSPoint { get } // the position of the item on the desktop + @objc optional var bounds: NSRect { get } // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons) + @objc optional var labelIndex: Int { get } // the label of the item + @objc optional var locked: Bool { get } // Is the file locked? + @objc optional var kind: String { get } // the kind of the item (copy) + @objc optional var objectDescription: String { get } // a description of the item (copy) + @objc optional var comment: String { get } // the comment of the item, displayed in the “Get Info” window (copy) + @objc optional var size: Int64 { get } // the logical size of the item + @objc optional var physicalSize: Int64 { get } // the actual space used by the item on disk + @objc optional var creationDate: Date { get } // the date on which the item was created (copy) + @objc optional var modificationDate: Date { get } // the date on which the item was last modified (copy) + @objc optional var icon: FinderIconFamily { get } // the icon bitmap of the item (copy) + @objc optional var URL: String { get } // the URL of the item (copy) + @objc optional var owner: String { get } // the user that owns the container (copy) + @objc optional var group: String { get } // the user or group that has special access to the container (copy) + @objc optional var ownerPrivileges: FinderPriv { get } + @objc optional var groupPrivileges: FinderPriv { get } + @objc optional var everyonesPrivileges: FinderPriv { get } + @objc optional var informationWindow: SBObject { get } // the information window for the item (copy) + @objc optional var properties: [AnyHashable: Any] { get } // every property of an item (copy) + + @objc optional func setName(_ name: String!) // the name of the item + @objc optional func setNameExtension(_ nameExtension: String!) // the name extension of the item (such as “txt”) + @objc optional func setExtensionHidden(_ extensionHidden: Bool) // Is the item's extension hidden from the user? + @objc optional func setPosition(_ position: NSPoint) // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons) + @objc optional func setDesktopPosition(_ desktopPosition: NSPoint) // the position of the item on the desktop + @objc optional func setBounds(_ bounds: NSRect) // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons) + @objc optional func setLabelIndex(_ labelIndex: Int) // the label of the item + @objc optional func setLocked(_ locked: Bool) // Is the file locked? + @objc optional func setComment(_ comment: String!) // the comment of the item, displayed in the “Get Info” window + @objc optional func setModificationDate(_ modificationDate: Date!) // the date on which the item was last modified + @objc optional func setIcon(_ icon: FinderIconFamily!) // the icon bitmap of the item + @objc optional func setOwner(_ owner: String!) // the user that owns the container + @objc optional func setGroup(_ group: String!) // the user or group that has special access to the container + @objc optional func setOwnerPrivileges(_ ownerPrivileges: FinderPriv) + @objc optional func setGroupPrivileges(_ groupPrivileges: FinderPriv) + @objc optional func setEveryonesPrivileges(_ everyonesPrivileges: FinderPriv) + @objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of an item +} + +extension SBObject: FinderItem {} + +// MARK: FinderContainer +@objc +public protocol FinderContainer: FinderItem { + @objc optional var entireContents: SBObject { get } // the entire contents of the container, including the contents of its children (copy) + @objc optional var expandable: Bool { get } // (NOT AVAILABLE YET) Is the container capable of being expanded as an outline? + @objc optional var expanded: Bool { get } // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists) + @objc optional var completelyExpanded: Bool { get } // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists) + @objc optional var containerWindow: SBObject { get } // the container window for this folder (copy) + + @objc optional func setExpanded(_ expanded: Bool) // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists) + @objc optional func setCompletelyExpanded(_ completelyExpanded: Bool) // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists) + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderContainer {} + +// MARK: FinderComputerObject +@objc +public protocol FinderComputerObject: FinderItem {} + +extension SBObject: FinderComputerObject {} + +// MARK: FinderDisk +@objc +public protocol FinderDisk: FinderContainer { + @objc optional var capacity: Int64 { get } // the total number of bytes (free or used) on the disk + @objc optional var freeSpace: Int64 { get } // the number of free bytes left on the disk + @objc optional var ejectable: Bool { get } // Can the media be ejected (floppies, CDs, and so on)? + @objc optional var localVolume: Bool { get } // Is the media a local volume (as opposed to a file server)? + @objc optional var startup: Bool { get } // Is this disk the boot disk? + @objc optional var format: FinderEdfm { get } // the filesystem format of this disk + @objc optional var journalingEnabled: Bool { get } // Does this disk do file system journaling? + @objc optional var ignorePrivileges: Bool { get } // Ignore permissions on this disk? + + @objc optional func setIgnorePrivileges(_ ignorePrivileges: Bool) // Ignore permissions on this disk? + + @objc optional func id() -> Int // the unique id for this disk (unchanged while disk remains connected and Finder remains running) + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderDisk {} + +// MARK: FinderFolder +@objc +public protocol FinderFolder: FinderContainer { + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderFolder {} + +// MARK: FinderDesktopObject +@objc +public protocol FinderDesktopObject: FinderContainer { + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func disks() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderDesktopObject {} + +// MARK: FinderTrashObject +@objc +public protocol FinderTrashObject: FinderContainer { + @objc optional var warnsBeforeEmptying: Bool { get } // Display a dialog when emptying the trash? + + @objc optional func setWarnsBeforeEmptying(_ warnsBeforeEmptying: Bool) // Display a dialog when emptying the trash? + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderTrashObject {} + +// MARK: FinderFile +@objc +public protocol FinderFile: FinderItem { + @objc optional var fileType: NSNumber { get } // the OSType identifying the type of data contained in the item (copy) + @objc optional var creatorType: NSNumber { get } // the OSType identifying the application that created the item (copy) + @objc optional var stationery: Bool { get } // Is the file a stationery pad? + @objc optional var productVersion: String { get } // the version of the product (visible at the top of the “Get Info” window) (copy) + @objc optional var version: String { get } // the version of the file (visible at the bottom of the “Get Info” window) (copy) + + @objc optional func setFileType(_ fileType: NSNumber!) // the OSType identifying the type of data contained in the item + @objc optional func setCreatorType(_ creatorType: NSNumber!) // the OSType identifying the application that created the item + @objc optional func setStationery(_ stationery: Bool) // Is the file a stationery pad? +} + +extension SBObject: FinderFile {} + +// MARK: FinderAliasFile +@objc +public protocol FinderAliasFile: FinderFile { + @objc optional var originalItem: SBObject { get } // the original item pointed to by the alias (copy) + + @objc optional func setOriginalItem(_ originalItem: SBObject!) // the original item pointed to by the alias +} + +extension SBObject: FinderAliasFile {} + +// MARK: FinderApplicationFile +@objc +public protocol FinderApplicationFile: FinderFile { + @objc optional var suggestedSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the developer recommends the application be launched + @objc optional var minimumSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched + @objc optional var preferredSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched + @objc optional var acceptsHighLevelEvents: Bool { get } // Is the application high-level event aware? (OBSOLETE: always returns true) + @objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted? + @objc optional var opensInClassic: Bool { get } // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment? + + @objc optional func setMinimumSize(_ minimumSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched + @objc optional func setPreferredSize(_ preferredSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched + @objc optional func setOpensInClassic(_ opensInClassic: Bool) // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment? + + @objc optional func id() -> String // the bundle identifier or creator type of the application +} + +extension SBObject: FinderApplicationFile {} + +// MARK: FinderDocumentFile +@objc +public protocol FinderDocumentFile: FinderFile {} + +extension SBObject: FinderDocumentFile {} + +// MARK: FinderInternetLocationFile +@objc +public protocol FinderInternetLocationFile: FinderFile { + @objc optional var location: String { get } // the internet location (copy) +} + +extension SBObject: FinderInternetLocationFile {} + +// MARK: FinderClipping +@objc +public protocol FinderClipping: FinderFile { + @objc optional var clippingWindow: SBObject { get } // (NOT AVAILABLE YET) the clipping window for this clipping (copy) +} + +extension SBObject: FinderClipping {} + +// MARK: FinderPackage +@objc +public protocol FinderPackage: FinderItem {} + +extension SBObject: FinderPackage {} + +// MARK: FinderWindow +@objc +public protocol FinderWindow: SBObjectProtocol, FinderGenericMethods { + @objc optional var position: NSPoint { get } // the upper left position of the window + @objc optional var bounds: NSRect { get } // the boundary rectangle for the window + @objc optional var titled: Bool { get } // Does the window have a title bar? + @objc optional var name: String { get } // the name of the window (copy) + @objc optional var index: Int { get } // the number of the window in the front-to-back layer ordering + @objc optional var closeable: Bool { get } // Does the window have a close box? + @objc optional var floating: Bool { get } // Does the window have a title bar? + @objc optional var modal: Bool { get } // Is the window modal? + @objc optional var resizable: Bool { get } // Is the window resizable? + @objc optional var zoomable: Bool { get } // Is the window zoomable? + @objc optional var zoomed: Bool { get } // Is the window zoomed? + @objc optional var visible: Bool { get } // Is the window visible (always true for open Finder windows)? + @objc optional var collapsed: Bool { get } // Is the window collapsed + @objc optional var properties: [AnyHashable: Any] { get } // every property of a window (copy) + + @objc optional func setPosition(_ position: NSPoint) // the upper left position of the window + @objc optional func setBounds(_ bounds: NSRect) // the boundary rectangle for the window + @objc optional func setIndex(_ index: Int) // the number of the window in the front-to-back layer ordering + @objc optional func setZoomed(_ zoomed: Bool) // Is the window zoomed? + @objc optional func setCollapsed(_ collapsed: Bool) // Is the window collapsed + @objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of a window + + @objc optional func id() -> Int // the unique id for this window +} + +extension SBObject: FinderWindow {} + +// MARK: FinderFinderWindow +@objc +public protocol FinderFinderWindow: FinderWindow { + @objc optional var target: SBObject { get } // the container at which this file viewer is targeted (copy) + @objc optional var currentView: FinderEcvw { get } // the current view for the container window + @objc optional var iconViewOptions: FinderIconViewOptions { get } // the icon view options for the container window (copy) + @objc optional var listViewOptions: FinderListViewOptions { get } // the list view options for the container window (copy) + @objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for the container window (copy) + @objc optional var toolbarVisible: Bool { get } // Is the window's toolbar visible? + @objc optional var statusbarVisible: Bool { get } // Is the window's status bar visible? + @objc optional var sidebarWidth: Int { get } // the width of the sidebar for the container window + + @objc optional func setTarget(_ target: SBObject!) // the container at which this file viewer is targeted + @objc optional func setCurrentView(_ currentView: FinderEcvw) // the current view for the container window + @objc optional func setToolbarVisible(_ toolbarVisible: Bool) // Is the window's toolbar visible? + @objc optional func setStatusbarVisible(_ statusbarVisible: Bool) // Is the window's status bar visible? + @objc optional func setSidebarWidth(_ sidebarWidth: Int) // the width of the sidebar for the container window +} + +extension SBObject: FinderFinderWindow {} + +// MARK: FinderDesktopWindow +@objc +public protocol FinderDesktopWindow: FinderFinderWindow {} + +extension SBObject: FinderDesktopWindow {} + +// MARK: FinderInformationWindow +@objc +public protocol FinderInformationWindow: FinderWindow { + @objc optional var item: SBObject { get } // the item from which this window was opened (copy) + @objc optional var currentPanel: FinderIpnl { get } // the current panel in the information window + + @objc optional func setCurrentPanel(_ currentPanel: FinderIpnl) // the current panel in the information window +} + +extension SBObject: FinderInformationWindow {} + +// MARK: FinderPreferencesWindow +@objc +public protocol FinderPreferencesWindow: FinderWindow { + @objc optional var currentPanel: FinderPple { get } // The current panel in the Finder preferences window + + @objc optional func setCurrentPanel(_ currentPanel: FinderPple) // The current panel in the Finder preferences window +} + +extension SBObject: FinderPreferencesWindow {} + +// MARK: FinderClippingWindow +@objc +public protocol FinderClippingWindow: FinderWindow {} + +extension SBObject: FinderClippingWindow {} + +// MARK: FinderProcess +@objc +public protocol FinderProcess: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name of the process (copy) + @objc optional var visible: Bool { get } // Is the process' layer visible? + @objc optional var frontmost: Bool { get } // Is the process the frontmost process? + @objc optional var file: SBObject { get } // the file from which the process was launched (copy) + @objc optional var fileType: NSNumber { get } // the OSType of the file type of the process (copy) + @objc optional var creatorType: NSNumber { get } // the OSType of the creator of the process (the signature) (copy) + @objc optional var acceptsHighLevelEvents: Bool { get } // Is the process high-level event aware (accepts open application, open document, print document, and quit)? + @objc optional var acceptsRemoteEvents: Bool { get } // Does the process accept remote events? + @objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted? + @objc optional var totalPartitionSize: Int { get } // the size of the partition with which the process was launched + @objc optional var partitionSpaceUsed: Int { get } // the number of bytes currently used in the process' partition + + @objc optional func setVisible(_ visible: Bool) // Is the process' layer visible? + @objc optional func setFrontmost(_ frontmost: Bool) // Is the process the frontmost process? +} + +extension SBObject: FinderProcess {} + +// MARK: FinderApplicationProcess +@objc +public protocol FinderApplicationProcess: FinderProcess { + @objc optional var applicationFile: FinderApplicationFile { get } // the application file from which this process was launched (copy) +} + +extension SBObject: FinderApplicationProcess {} + +// MARK: FinderDeskAccessoryProcess +@objc +public protocol FinderDeskAccessoryProcess: FinderProcess { + @objc optional var deskAccessoryFile: SBObject { get } // the desk accessory file from which this process was launched (copy) +} + +extension SBObject: FinderDeskAccessoryProcess {} + +// MARK: FinderPreferences +@objc +public protocol FinderPreferences: SBObjectProtocol, FinderGenericMethods { + @objc optional var window: FinderPreferencesWindow { get } // the window that would open if Finder preferences was opened (copy) + @objc optional var iconViewOptions: FinderIconViewOptions { get } // the default icon view options (copy) + @objc optional var listViewOptions: FinderListViewOptions { get } // the default list view options (copy) + @objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for all windows (copy) + @objc optional var foldersSpringOpen: Bool { get } // Spring open folders after the specified delay? + @objc optional var delayBeforeSpringing: Double { get } // the delay before springing open a container in seconds (from 0.167 to 1.169) + @objc optional var desktopShowsHardDisks: Bool { get } // Hard disks appear on the desktop? + @objc optional var desktopShowsExternalHardDisks: Bool { get } // External hard disks appear on the desktop? + @objc optional var desktopShowsRemovableMedia: Bool { get } // CDs, DVDs, and iPods appear on the desktop? + @objc optional var desktopShowsConnectedServers: Bool { get } // Connected servers appear on the desktop? + @objc optional var newWindowTarget: SBObject { get } // target location for a newly-opened Finder window (copy) + @objc optional var foldersOpenInNewWindows: Bool { get } // Folders open into new windows? + @objc optional var foldersOpenInNewTabs: Bool { get } // Folders open into new tabs? + @objc optional var newWindowsOpenInColumnView: Bool { get } // Open new windows in column view? + @objc optional var allNameExtensionsShowing: Bool { get } // Show name extensions, even for items whose “extension hidden” is true? + + @objc optional func setFoldersSpringOpen(_ foldersSpringOpen: Bool) // Spring open folders after the specified delay? + @objc optional func setDelayBeforeSpringing(_ delayBeforeSpringing: Double) // the delay before springing open a container in seconds (from 0.167 to 1.169) + @objc optional func setDesktopShowsHardDisks(_ desktopShowsHardDisks: Bool) // Hard disks appear on the desktop? + @objc optional func setDesktopShowsExternalHardDisks(_ desktopShowsExternalHardDisks: Bool) // External hard disks appear on the desktop? + @objc optional func setDesktopShowsRemovableMedia(_ desktopShowsRemovableMedia: Bool) // CDs, DVDs, and iPods appear on the desktop? + @objc optional func setDesktopShowsConnectedServers(_ desktopShowsConnectedServers: Bool) // Connected servers appear on the desktop? + @objc optional func setNewWindowTarget(_ newWindowTarget: SBObject!) // target location for a newly-opened Finder window + @objc optional func setFoldersOpenInNewWindows(_ foldersOpenInNewWindows: Bool) // Folders open into new windows? + @objc optional func setFoldersOpenInNewTabs(_ foldersOpenInNewTabs: Bool) // Folders open into new tabs? + @objc optional func setNewWindowsOpenInColumnView(_ newWindowsOpenInColumnView: Bool) // Open new windows in column view? + @objc optional func setAllNameExtensionsShowing(_ allNameExtensionsShowing: Bool) // Show name extensions, even for items whose “extension hidden” is true? +} + +extension SBObject: FinderPreferences {} + +// MARK: FinderLabel +@objc +public protocol FinderLabel: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name associated with the label (copy) + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var color: NSColor { get } // the color associated with the label (copy) + + @objc optional func setName(_ name: String!) // the name associated with the label + @objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container + @objc optional func setColor(_ color: NSColor!) // the color associated with the label +} + +extension SBObject: FinderLabel {} + +// MARK: FinderIconFamily +@objc +public protocol FinderIconFamily: SBObjectProtocol, FinderGenericMethods { + @objc optional var largeMonochromeIconAndMask: Any { get } // the large black-and-white icon and the mask for large icons (copy) + @objc optional var large8BitMask: Any { get } // the large 8-bit mask for large 32-bit icons (copy) + @objc optional var large32BitIcon: Any { get } // the large 32-bit color icon (copy) + @objc optional var large8BitIcon: Any { get } // the large 8-bit color icon (copy) + @objc optional var large4BitIcon: Any { get } // the large 4-bit color icon (copy) + @objc optional var smallMonochromeIconAndMask: Any { get } // the small black-and-white icon and the mask for small icons (copy) + @objc optional var small8BitMask: Any { get } // the small 8-bit mask for small 32-bit icons (copy) + @objc optional var small32BitIcon: Any { get } // the small 32-bit color icon (copy) + @objc optional var small8BitIcon: Any { get } // the small 8-bit color icon (copy) + @objc optional var small4BitIcon: Any { get } // the small 4-bit color icon (copy) +} + +extension SBObject: FinderIconFamily {} + +// MARK: FinderIconViewOptions +@objc +public protocol FinderIconViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var arrangement: FinderEarr { get } // the property by which to keep icons arranged + @objc optional var iconSize: Int { get } // the size of icons displayed in the icon view + @objc optional var showsItemInfo: Bool { get } // additional info about an item displayed in icon view + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in icon view + @objc optional var textSize: Int { get } // the size of the text displayed in the icon view + @objc optional var labelPosition: FinderEpos { get } // the location of the label in reference to the icon + @objc optional var backgroundPicture: FinderFile { get } // the background picture of the icon view (copy) + @objc optional var backgroundColor: NSColor { get } // the background color of the icon view (copy) + + @objc optional func setArrangement(_ arrangement: FinderEarr) // the property by which to keep icons arranged + @objc optional func setIconSize(_ iconSize: Int) // the size of icons displayed in the icon view + @objc optional func setShowsItemInfo(_ showsItemInfo: Bool) // additional info about an item displayed in icon view + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in icon view + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the icon view + @objc optional func setLabelPosition(_ labelPosition: FinderEpos) // the location of the label in reference to the icon + @objc optional func setBackgroundPicture(_ backgroundPicture: FinderFile!) // the background picture of the icon view + @objc optional func setBackgroundColor(_ backgroundColor: NSColor!) // the background color of the icon view +} + +extension SBObject: FinderIconViewOptions {} + +// MARK: FinderColumnViewOptions +@objc +public protocol FinderColumnViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var textSize: Int { get } // the size of the text displayed in the column view + @objc optional var showsIcon: Bool { get } // displays an icon next to the label in column view + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in column view + @objc optional var showsPreviewColumn: Bool { get } // displays the preview column in column view + @objc optional var disclosesPreviewPane: Bool { get } // discloses the preview pane of the preview column in column view + + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the column view + @objc optional func setShowsIcon(_ showsIcon: Bool) // displays an icon next to the label in column view + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in column view + @objc optional func setShowsPreviewColumn(_ showsPreviewColumn: Bool) // displays the preview column in column view + @objc optional func setDisclosesPreviewPane(_ disclosesPreviewPane: Bool) // discloses the preview pane of the preview column in column view +} + +extension SBObject: FinderColumnViewOptions {} + +// MARK: FinderListViewOptions +@objc +public protocol FinderListViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var calculatesFolderSizes: Bool { get } // Are folder sizes calculated and displayed in the window? + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in list view + @objc optional var iconSize: FinderLvic { get } // the size of icons displayed in the list view + @objc optional var textSize: Int { get } // the size of the text displayed in the list view + @objc optional var sortColumn: FinderColumn { get } // the column that the list view is sorted on (copy) + @objc optional var usesRelativeDates: Bool { get } // Are relative dates (e.g., today, yesterday) shown in the list view? + + @objc optional func setCalculatesFolderSizes(_ calculatesFolderSizes: Bool) // Are folder sizes calculated and displayed in the window? + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in list view + @objc optional func setIconSize(_ iconSize: FinderLvic) // the size of icons displayed in the list view + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the list view + @objc optional func setSortColumn(_ sortColumn: FinderColumn!) // the column that the list view is sorted on + @objc optional func setUsesRelativeDates(_ usesRelativeDates: Bool) // Are relative dates (e.g., today, yesterday) shown in the list view? + + @objc optional func columns() -> SBElementArray +} + +extension SBObject: FinderListViewOptions {} + +// MARK: FinderColumn +@objc +public protocol FinderColumn: SBObjectProtocol, FinderGenericMethods { + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var name: FinderElsv { get } // the column name + @objc optional var sortDirection: FinderSodr { get } // The direction in which the window is sorted + @objc optional var width: Int { get } // the width of this column + @objc optional var minimumWidth: Int { get } // the minimum allowed width of this column + @objc optional var maximumWidth: Int { get } // the maximum allowed width of this column + @objc optional var visible: Bool { get } // is this column visible + + @objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container + @objc optional func setSortDirection(_ sortDirection: FinderSodr) // The direction in which the window is sorted + @objc optional func setWidth(_ width: Int) // the width of this column + @objc optional func setVisible(_ visible: Bool) // is this column visible +} + +extension SBObject: FinderColumn {} + +// MARK: FinderAliasList +@objc +public protocol FinderAliasList: SBObjectProtocol, FinderGenericMethods {} + +extension SBObject: FinderAliasList {} diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index 7554d3c..8e27760 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -7,6 +7,7 @@ // import CommerceKit +import ScriptingBridge /// Utility for managing installed apps. class MasAppLibrary: AppLibrary { @@ -33,25 +34,126 @@ class MasAppLibrary: AppLibrary { softwareMap.product(for: bundleId) } - /// Uninstalls an app. + /// Uninstalls all apps located at any of the elements of `appPaths`. /// - /// - Parameter app: App to be removed. - /// - Throws: Error if there is a problem. - func uninstallApp(app: SoftwareProduct) throws { - if NSUserName() != "root" { - throw MASError.macOSUserMustBeRoot + /// - Parameter appPaths: Paths to apps to be uninstalled. + /// - Throws: Error if any problem occurs. + func uninstallApps(atPaths appPaths: [String]) throws { + try delete(pathsFromOwnerIDsByPath: try chown(paths: appPaths)) + } +} + +func getSudoUsername() -> String? { + ProcessInfo.processInfo.environment["SUDO_USER"] +} + +func getSudoUID() -> uid_t? { + guard let uid = ProcessInfo.processInfo.environment["SUDO_UID"] else { + return nil + } + return uid_t(uid) +} + +func getSudoGID() -> gid_t? { + guard let gid = ProcessInfo.processInfo.environment["SUDO_GID"] else { + return nil + } + return gid_t(gid) +} + +private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t) { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: path) + guard + let uid = attributes[.ownerAccountID] as? uid_t, + let gid = attributes[.groupOwnerAccountID] as? gid_t + else { + throw MASError.runtimeError("Failed to determine running user's uid & gid") + } + return (uid, gid) + } +} + +private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] { + guard let sudoUID = getSudoUID() else { + throw MASError.runtimeError("Failed to get original uid") + } + + guard let sudoGID = getSudoGID() else { + throw MASError.runtimeError("Failed to get original gid") + } + + let ownerIDsByPath = try paths.reduce(into: [String: (uid_t, gid_t)]()) { dict, path in + dict[path] = try getOwnerAndGroupOfItem(atPath: path) + } + + var chownedIDsByPath: [String: (uid_t, gid_t)] = [:] + for (path, ownerIDs) in ownerIDsByPath { + guard chown(path, sudoUID, sudoGID) == 0 else { + for (chownedPath, chownedIDs) in chownedIDsByPath + where chown(chownedPath, chownedIDs.0, chownedIDs.1) != 0 { + printError("Failed to revert ownership of '\(path)' back to uid \(chownedIDs.0) & gid \(chownedIDs.1)") + } + throw MASError.runtimeError("Failed to change ownership of '\(path)' to uid \(sudoUID) & gid \(sudoGID)") } - let appUrl = URL(fileURLWithPath: app.bundlePath) - do { - // Move item to trash - var trashUrl: NSURL? - try FileManager().trashItem(at: appUrl, resultingItemURL: &trashUrl) - if let path = trashUrl?.path { - printInfo("App moved to trash: \(path)") - } - } catch { - throw MASError.uninstallFailed(error: error as NSError) + chownedIDsByPath[path] = ownerIDs + } + + return ownerIDsByPath +} + +private func delete(pathsFromOwnerIDsByPath ownerIDsByPath: [String: (uid_t, gid_t)]) throws { + guard let finder: FinderApplication = SBApplication(bundleIdentifier: "com.apple.finder") else { + throw MASError.runtimeError("Failed to obtain Finder access: com.apple.finder does not exist") + } + + guard let items = finder.items else { + throw MASError.runtimeError("Failed to obtain Finder access: finder.items does not exist") + } + + for (path, ownerIDs) in ownerIDsByPath { + let object = items().object(atLocation: URL(fileURLWithPath: path)) + + guard let item = object as? FinderItem else { + throw MASError.runtimeError( + """ + Failed to obtain Finder access: finder.items().object(atLocation: URL(fileURLWithPath: \ + \"\(path)\") is a '\(type(of: object))' that does not conform to 'FinderItem' + """ + ) + } + + guard let delete = item.delete else { + throw MASError.runtimeError("Failed to obtain Finder access: FinderItem.delete does not exist") + } + + let uid = ownerIDs.0 + let gid = ownerIDs.1 + guard let deletedURLString = (delete() as FinderItem).URL else { + throw MASError.runtimeError( + """ + Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \ + delete result did not have a URL + """ + ) + } + + guard let deletedURL = URL(string: deletedURLString) else { + throw MASError.runtimeError( + """ + Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \ + delete result URL is invalid: \(deletedURLString) + """ + ) + } + + let deletedPath = deletedURL.path + print("Deleted '\(path)' to '\(deletedPath)'") + guard chown(deletedPath, uid, gid) == 0 else { + throw MASError.runtimeError( + "Failed to revert ownership of deleted '\(deletedPath)' back to uid \(uid) & gid \(gid)" + ) } } } diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 564afb6..7aea57f 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -13,6 +13,8 @@ enum MASError: Error, Equatable { case failed(error: NSError?) + case runtimeError(String) + case notSignedIn case noPasswordProvided case signInFailed(error: NSError?) @@ -54,6 +56,8 @@ extension MASError: CustomStringConvertible { return "Failed: \(error.localizedDescription)" } return "Failed" + case .runtimeError(let message): + return "Runtime Error: \(message)" case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index dd342a0..69cb44e 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -17,7 +17,7 @@ public class UninstallSpec: QuickSpec { beforeSuite { Mas.initialize() } - describe("uninstall command") { + xdescribe("uninstall command") { let appID: AppID = 12345 let app = SoftwareProductMock( appName: "Some App", @@ -76,7 +76,9 @@ public class UninstallSpec: QuickSpec { brokenApp.bundlePath = "/dev/null" mockLibrary.installedApps.append(brokenApp) expect { - try uninstall.run(appLibrary: mockLibrary) + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } } .to(throwError(MASError.uninstallFailed(error: nil))) } diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift index e4daad1..96ac534 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -11,19 +11,11 @@ class AppLibraryMock: AppLibrary { var installedApps: [SoftwareProduct] = [] - func uninstallApp(app: SoftwareProduct) throws { - if !installedApps.contains(where: { product -> Bool in - app.itemIdentifier == product.itemIdentifier - }) { - throw MASError.notInstalled(appID: app.itemIdentifier.appIDValue) - } - + func uninstallApps(atPaths appPaths: [String]) throws { // Special case for testing where we pretend the trash command failed - if app.bundlePath == "/dev/null" { + if appPaths.contains("/dev/null") { throw MASError.uninstallFailed(error: nil) } - - // Success is the default, watch out for false positives! } } From 3a5593c12d2857c5d37668effb5ecbb95dbca8d5 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:14:14 -0400 Subject: [PATCH 71/81] Rename `*accountId` & `username` as `*appleID`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 8 ++++---- Sources/mas/Commands/SignIn.swift | 4 ++-- Sources/mas/Errors/MASError.swift | 6 +++--- Tests/masTests/Errors/MASErrorTestCase.swift | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index ecb3313..56b7bd6 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -32,7 +32,7 @@ extension ISStoreAccount: StoreAccount { return .value(CKAccountStore.shared().primaryAccount) } - static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { + static func signIn(appleID: String, password: String, systemDialog: Bool) -> Promise { // swift-format-ignore: UseEarlyExits if #available(macOS 10.13, *) { // Signing in is no longer possible as of High Sierra. @@ -44,7 +44,7 @@ extension ISStoreAccount: StoreAccount { primaryAccount .then { account -> Promise in if account.isSignedIn { - return Promise(error: MASError.alreadySignedIn(asAccountId: account.identifier)) + return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier)) } let password = @@ -57,7 +57,7 @@ extension ISStoreAccount: StoreAccount { } let context = ISAuthenticationContext(accountID: 0) - context.appleIDOverride = username + context.appleIDOverride = appleID let signInPromise = Promise { seal in @@ -77,7 +77,7 @@ extension ISStoreAccount: StoreAccount { } context.demoMode = true - context.demoAccountName = username + context.demoAccountName = appleID context.demoAccountPassword = password context.demoAutologinMode = true diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift index ea332a3..aaa4a96 100644 --- a/Sources/mas/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -19,14 +19,14 @@ extension Mas { @Flag(help: "Complete login with graphical dialog") var dialog = false @Argument(help: "Apple ID") - var username: String + var appleID: String @Argument(help: "Password") var password: String = "" /// Runs the command. func run() throws { do { - _ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait() + _ = try ISStoreAccount.signIn(appleID: appleID, password: password, systemDialog: dialog).wait() } catch { throw error as? MASError ?? MASError.signInFailed(error: error as NSError) } diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 7aea57f..1d31b7f 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -18,7 +18,7 @@ enum MASError: Error, Equatable { case notSignedIn case noPasswordProvided case signInFailed(error: NSError?) - case alreadySignedIn(asAccountId: String) + case alreadySignedIn(asAppleID: String) case purchaseFailed(error: NSError?) case downloadFailed(error: NSError?) @@ -63,8 +63,8 @@ extension MASError: CustomStringConvertible { return "Sign in failed: \(error.localizedDescription)" } return "Sign in failed" - case .alreadySignedIn(let accountId): - return "Already signed in as \(accountId)" + case .alreadySignedIn(let appleID): + return "Already signed in as \(appleID)" case .purchaseFailed(let error): if let error { return "Download request failed: \(error.localizedDescription)" diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index 47c83d6..15cecfc 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -59,7 +59,7 @@ class MASErrorTestCase: XCTestCase { } func testAlreadySignedIn() { - error = .alreadySignedIn(asAccountId: "person@example.com") + error = .alreadySignedIn(asAppleID: "person@example.com") XCTAssertEqual(error.description, "Already signed in as person@example.com") } From 0e49da7bf1fc5eed50ab6308fb2edf93961d76f2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:49:21 -0400 Subject: [PATCH 72/81] Rename `*bundleId` as `*bundleID`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/MasAppLibrary.swift | 8 ++++---- Tests/masTests/Controllers/MasAppLibrarySpec.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index 8e27760..d9e2f7d 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -28,10 +28,10 @@ class MasAppLibrary: AppLibrary { /// Finds an app using a bundle identifier. /// - /// - Parameter bundleId: Bundle identifier of app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forBundleId bundleId: String) -> SoftwareProduct? { - softwareMap.product(for: bundleId) + /// - Parameter bundleID: Bundle identifier of app. + /// - Returns: `SoftwareProduct` for app if found; `nil` otherwise. + func installedApp(forBundleID bundleID: String) -> SoftwareProduct? { + softwareMap.product(for: bundleID) } /// Uninstalls all apps located at any of the elements of `appPaths`. diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift index fbb85fb..69c2cc9 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -24,7 +24,7 @@ public class MasAppLibrarySpec: QuickSpec { expect(library.installedApps.first!.appName) == myApp.appName } it("can locate an app by bundle id") { - expect(library.installedApp(forBundleId: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier + expect(library.installedApp(forBundleID: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier } } } From 0b11f3737c92148e47ecafb40b22571cb276988e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:04:06 -0400 Subject: [PATCH 73/81] Rename some uses of `appName` as `searchTerm`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 4 ++-- Sources/mas/Commands/Search.swift | 4 ++-- Sources/mas/Controllers/StoreSearch.swift | 2 +- Tests/masTests/Controllers/StoreSearchMock.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 296dde7..886b864 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -21,7 +21,7 @@ extension Mas { @Flag(help: "force reinstall") var force = false @Argument(help: "the app name to install") - var appName: String + var searchTerm: String /// Runs the command. func run() throws { @@ -32,7 +32,7 @@ extension Mas { var appID: AppID? do { - let results = try storeSearch.search(for: appName).wait() + let results = try storeSearch.search(for: searchTerm).wait() guard let result = results.first else { printError("No results found") throw MASError.noSearchResultsFound diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 79ef211..6e6c07e 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -20,7 +20,7 @@ extension Mas { @Flag(help: "Show price of found apps") var price = false @Argument(help: "the app name to search") - var appName: String + var searchTerm: String func run() throws { try run(storeSearch: MasStoreSearch()) @@ -28,7 +28,7 @@ extension Mas { func run(storeSearch: StoreSearch) throws { do { - let results = try storeSearch.search(for: appName).wait() + let results = try storeSearch.search(for: searchTerm).wait() if results.isEmpty { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index 166e7fd..0b8194c 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -12,7 +12,7 @@ import PromiseKit /// Protocol for searching the MAS catalog. protocol StoreSearch { func lookup(appID: AppID) -> Promise - func search(for appName: String) -> Promise<[SearchResult]> + func search(for searchTerm: String) -> Promise<[SearchResult]> } enum Entity: String { diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index 938c2b0..4b5143d 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -13,8 +13,8 @@ import PromiseKit class StoreSearchMock: StoreSearch { var apps: [AppID: SearchResult] = [:] - func search(for appName: String) -> Promise<[SearchResult]> { - .value(apps.filter { $1.trackName.contains(appName) }.map { $1 }) + func search(for searchTerm: String) -> Promise<[SearchResult]> { + .value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 }) } func lookup(appID: AppID) -> Promise { From 59a642590a347c2afd890566f457c84704a2ea93 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:08:50 -0400 Subject: [PATCH 74/81] Rename some uses of `*Url` as `*URL`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/MasStoreSearch.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 2dc7ff4..e7bb192 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -77,12 +77,12 @@ class MasStoreSearch: StoreSearch { return .value(nil) } - guard let pageUrl = URL(string: result.trackViewUrl) else { + guard let pageURL = URL(string: result.trackViewUrl) else { return .value(result) } return firstly { - self.scrapeAppStoreVersion(pageUrl) + self.scrapeAppStoreVersion(pageURL) } .map { pageVersion in guard let pageVersion, @@ -120,9 +120,9 @@ class MasStoreSearch: StoreSearch { /// Scrape the app version from the App Store webpage at the given URL. /// /// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API. - private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { + private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise { firstly { - networkManager.loadData(from: pageUrl) + networkManager.loadData(from: pageURL) } .map { data in guard let html = String(data: data, encoding: .utf8), From 6bf5696093a89f86ab0450ab05a2fa7619f817dc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:17:23 -0400 Subject: [PATCH 75/81] Chop down multiline `guard` clauses. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 3 ++- Sources/mas/AppStore/PurchaseDownloadObserver.swift | 6 ++++-- Sources/mas/Controllers/MasStoreSearch.swift | 6 ++++-- Sources/mas/Models/SoftwareProduct.swift | 3 ++- Tests/masTests/Extensions/Bundle+JSON.swift | 3 ++- docs/sample.swift | 11 +++++++---- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index bb09f3d..6435ff6 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -47,7 +47,8 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt } // If the download failed due to network issues, try again. Otherwise, fail immediately. - guard case MASError.downloadFailed(let downloadError) = error, + guard + case MASError.downloadFailed(let downloadError) = error, case NSURLErrorDomain = downloadError?.domain else { throw error diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index c6709cf..30b968c 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -20,7 +20,8 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { } func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { - guard download.metadata.itemIdentifier == purchase.itemIdentifier, + guard + download.metadata.itemIdentifier == purchase.itemIdentifier, let status = download.status else { return @@ -42,7 +43,8 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { } func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) { - guard download.metadata.itemIdentifier == purchase.itemIdentifier, + guard + download.metadata.itemIdentifier == purchase.itemIdentifier, let status = download.status else { return diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index e7bb192..a046778 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -85,7 +85,8 @@ class MasStoreSearch: StoreSearch { self.scrapeAppStoreVersion(pageURL) } .map { pageVersion in - guard let pageVersion, + guard + let pageVersion, let searchVersion = Version(tolerant: result.version), pageVersion > searchVersion else { @@ -125,7 +126,8 @@ class MasStoreSearch: StoreSearch { networkManager.loadData(from: pageURL) } .map { data in - guard let html = String(data: data, encoding: .utf8), + guard + let html = String(data: data, encoding: .utf8), let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) else { diff --git a/Sources/mas/Models/SoftwareProduct.swift b/Sources/mas/Models/SoftwareProduct.swift index 7b882c5..29c393f 100644 --- a/Sources/mas/Models/SoftwareProduct.swift +++ b/Sources/mas/Models/SoftwareProduct.swift @@ -45,7 +45,8 @@ extension SoftwareProduct { // The App Store does not enforce semantic versioning, but we assume most apps follow versioning // schemes that increase numerically over time. - guard let semanticBundleVersion = Version(tolerant: bundleVersion), + guard + let semanticBundleVersion = Version(tolerant: bundleVersion), let semanticAppStoreVersion = Version(tolerant: storeApp.version) else { // If a version string can't be parsed as a Semantic Version, our best effort is to check for diff --git a/Tests/masTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift index 3c9628c..d74c2c0 100644 --- a/Tests/masTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -30,7 +30,8 @@ extension Bundle { .bundleURL .deletingLastPathComponent() .appendingPathComponent("mas_masTests.bundle") - guard let bundle = Bundle(url: bundleURL), + guard + let bundle = Bundle(url: bundleURL), let url = bundle.url(for: fileName) else { fatalError("Unable to load file \(fileName)") diff --git a/docs/sample.swift b/docs/sample.swift index fd7091e..17fc5d4 100644 --- a/docs/sample.swift +++ b/docs/sample.swift @@ -90,11 +90,14 @@ private extension MyClass { guard let singleTest = somethingFailable() else { return } guard statementThatShouldBeTrue else { return } -// If there is one long expression to guard or multiple expressions -// move else to next line -guard let oneItem = somethingFailable(), +// If a guard clause requires multiple lines, chop down, then start `else` new line +// In this case, always chop down else clause. +guard + let oneItem = somethingFailable(), let secondItem = somethingFailable2() -else { return } +else { + return +} // If the return in else is long, move to next line guard let something = somethingFailable() else { From ab22e22ace31d269da36a0afe8bea2b9bec2ac33 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:21:19 -0400 Subject: [PATCH 76/81] Rename `Mas` as `MAS`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Account.swift | 2 +- Sources/mas/Commands/Home.swift | 2 +- Sources/mas/Commands/Info.swift | 2 +- Sources/mas/Commands/Install.swift | 2 +- Sources/mas/Commands/List.swift | 2 +- Sources/mas/Commands/Lucky.swift | 2 +- Sources/mas/Commands/Open.swift | 2 +- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Purchase.swift | 2 +- Sources/mas/Commands/Reset.swift | 2 +- Sources/mas/Commands/Search.swift | 2 +- Sources/mas/Commands/SignIn.swift | 2 +- Sources/mas/Commands/SignOut.swift | 2 +- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Commands/Upgrade.swift | 2 +- Sources/mas/Commands/Vendor.swift | 2 +- Sources/mas/Commands/Version.swift | 2 +- Sources/mas/{Mas.swift => MAS.swift} | 4 ++-- Tests/masTests/Commands/AccountSpec.swift | 4 ++-- Tests/masTests/Commands/HomeSpec.swift | 8 ++++---- Tests/masTests/Commands/InfoSpec.swift | 8 ++++---- Tests/masTests/Commands/InstallSpec.swift | 4 ++-- Tests/masTests/Commands/ListSpec.swift | 4 ++-- Tests/masTests/Commands/LuckySpec.swift | 4 ++-- Tests/masTests/Commands/OpenSpec.swift | 10 +++++----- Tests/masTests/Commands/OutdatedSpec.swift | 4 ++-- Tests/masTests/Commands/PurchaseSpec.swift | 4 ++-- Tests/masTests/Commands/ResetSpec.swift | 4 ++-- Tests/masTests/Commands/SearchSpec.swift | 6 +++--- Tests/masTests/Commands/SignInSpec.swift | 4 ++-- Tests/masTests/Commands/SignOutSpec.swift | 4 ++-- Tests/masTests/Commands/UninstallSpec.swift | 6 +++--- Tests/masTests/Commands/UpgradeSpec.swift | 4 ++-- Tests/masTests/Commands/VendorSpec.swift | 8 ++++---- Tests/masTests/Commands/VersionSpec.swift | 4 ++-- Tests/masTests/Controllers/MasAppLibrarySpec.swift | 2 +- Tests/masTests/Controllers/MasStoreSearchSpec.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 2 +- .../ExternalCommands/OpenSystemCommandSpec.swift | 2 +- Tests/masTests/Formatters/AppListFormatterSpec.swift | 2 +- .../Formatters/SearchResultFormatterSpec.swift | 2 +- Tests/masTests/Models/SearchResultListSpec.swift | 2 +- Tests/masTests/Models/SearchResultSpec.swift | 2 +- Tests/masTests/Models/SoftwareProductSpec.swift | 2 +- Tests/masTests/Network/NetworkManagerTests.swift | 2 +- 45 files changed, 74 insertions(+), 74 deletions(-) rename Sources/mas/{Mas.swift => MAS.swift} (97%) diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift index bd172f6..a9f90a6 100644 --- a/Sources/mas/Commands/Account.swift +++ b/Sources/mas/Commands/Account.swift @@ -9,7 +9,7 @@ import ArgumentParser import StoreFoundation -extension Mas { +extension MAS { struct Account: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Prints the primary account Apple ID" diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index 18a7a53..d9d3b23 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -8,7 +8,7 @@ import ArgumentParser -extension Mas { +extension MAS { /// Opens app page on MAS Preview. Uses the iTunes Lookup API: /// https://performance-partners.apple.com/search-api struct Home: ParsableCommand { diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index ca862d2..e832d20 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation -extension Mas { +extension MAS { /// Displays app details. Uses the iTunes Lookup API: /// https://performance-partners.apple.com/search-api struct Info: ParsableCommand { diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 1ba12b0..2b13081 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -9,7 +9,7 @@ import ArgumentParser import CommerceKit -extension Mas { +extension MAS { /// Installs previously purchased apps from the Mac App Store. struct Install: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index 492f58c..eb782e2 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -8,7 +8,7 @@ import ArgumentParser -extension Mas { +extension MAS { /// Command which lists all installed apps. struct List: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 886b864..f2a33c3 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -9,7 +9,7 @@ import ArgumentParser import CommerceKit -extension Mas { +extension MAS { /// Command which installs the first search result. /// /// This is handy as many MAS titles can be long with embedded keywords. diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 82e98de..c2369e9 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -11,7 +11,7 @@ import Foundation private let masScheme = "macappstore" -extension Mas { +extension MAS { /// Opens app page in MAS app. Uses the iTunes Lookup API: /// https://performance-partners.apple.com/search-api struct Open: ParsableCommand { diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 2164a4b..77325ba 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -10,7 +10,7 @@ import ArgumentParser import Foundation import PromiseKit -extension Mas { +extension MAS { /// Command which displays a list of installed apps which have available updates /// ready to be installed from the Mac App Store. struct Outdated: ParsableCommand { diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 58a7a48..66f4c92 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -9,7 +9,7 @@ import ArgumentParser import CommerceKit -extension Mas { +extension MAS { struct Purchase: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Purchase and download free apps from the Mac App Store" diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 76a32bd..37ff3e4 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -9,7 +9,7 @@ import ArgumentParser import CommerceKit -extension Mas { +extension MAS { /// Kills several macOS processes as a means to reset the app store. struct Reset: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 6e6c07e..ef14e1e 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -8,7 +8,7 @@ import ArgumentParser -extension Mas { +extension MAS { /// Search the Mac App Store using the iTunes Search API. /// /// See - https://performance-partners.apple.com/search-api diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift index aaa4a96..c94da79 100644 --- a/Sources/mas/Commands/SignIn.swift +++ b/Sources/mas/Commands/SignIn.swift @@ -9,7 +9,7 @@ import ArgumentParser import StoreFoundation -extension Mas { +extension MAS { struct SignIn: ParsableCommand { static let configuration = CommandConfiguration( commandName: "signin", diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift index 3384d8e..1c0d49d 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/SignOut.swift @@ -9,7 +9,7 @@ import ArgumentParser import CommerceKit -extension Mas { +extension MAS { struct SignOut: ParsableCommand { static let configuration = CommandConfiguration( commandName: "signout", diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index b73378b..68561c7 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation -extension Mas { +extension MAS { /// Command which uninstalls apps managed by the Mac App Store. struct Uninstall: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 83c4788..079550e 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -10,7 +10,7 @@ import ArgumentParser import Foundation import PromiseKit -extension Mas { +extension MAS { /// Command which upgrades apps with new versions available in the Mac App Store. struct Upgrade: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index 7380ca0..d3004a1 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -8,7 +8,7 @@ import ArgumentParser -extension Mas { +extension MAS { /// Opens vendor's app page in a browser. Uses the iTunes Lookup API: /// https://performance-partners.apple.com/search-api struct Vendor: ParsableCommand { diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift index 00c0e5b..4493088 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/Version.swift @@ -8,7 +8,7 @@ import ArgumentParser -extension Mas { +extension MAS { /// Command which displays the version of the mas tool. struct Version: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/mas/Mas.swift b/Sources/mas/MAS.swift similarity index 97% rename from Sources/mas/Mas.swift rename to Sources/mas/MAS.swift index 89aeacb..aee1262 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/MAS.swift @@ -1,5 +1,5 @@ // -// Mas.swift +// MAS.swift // mas // // Created by Chris Araman on 4/22/21. @@ -11,7 +11,7 @@ import Foundation import PromiseKit @main -struct Mas: ParsableCommand { +struct MAS: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Mac App Store command-line interface", subcommands: [ diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index db220c4..8885916 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -15,13 +15,13 @@ import Quick public class AccountSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#known-issues describe("Account command") { it("displays active account") { expect { - try Mas.Account.parse([]).run() + try MAS.Account.parse([]).run() } .to(throwError(MASError.notSupported)) } diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 097855e..46d22f6 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -17,7 +17,7 @@ public class HomeSpec: QuickSpec { let openCommand = OpenSystemCommandMock() beforeSuite { - Mas.initialize() + MAS.initialize() } describe("home command") { beforeEach { @@ -25,13 +25,13 @@ public class HomeSpec: QuickSpec { } it("fails to open app with invalid ID") { expect { - try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -43,7 +43,7 @@ public class HomeSpec: QuickSpec { ) storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Home.parse([String(mockResult.trackId)]) + try MAS.Home.parse([String(mockResult.trackId)]) .run(storeSearch: storeSearch, openCommand: openCommand) return openCommand.arguments } diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 7271451..400ddb6 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -17,7 +17,7 @@ public class InfoSpec: QuickSpec { let storeSearch = StoreSearchMock() beforeSuite { - Mas.initialize() + MAS.initialize() } describe("Info command") { beforeEach { @@ -25,13 +25,13 @@ public class InfoSpec: QuickSpec { } it("fails to open app with invalid ID") { expect { - try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) + try MAS.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try Mas.Info.parse(["999"]).run(storeSearch: storeSearch) + try MAS.Info.parse(["999"]).run(storeSearch: storeSearch) } .to(throwError(MASError.noSearchResultsFound)) } @@ -50,7 +50,7 @@ public class InfoSpec: QuickSpec { storeSearch.apps[mockResult.trackId] = mockResult expect { try captureStream(stdout) { - try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) + try MAS.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) } } == """ diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift index 4baeead..88ad308 100644 --- a/Tests/masTests/Commands/InstallSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -14,12 +14,12 @@ import Quick public class InstallSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } xdescribe("install command") { xit("installs apps") { expect { - try Mas.Install.parse([]).run(appLibrary: AppLibraryMock()) + try MAS.Install.parse([]).run(appLibrary: AppLibraryMock()) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index 7388e80..c72bdf6 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -15,13 +15,13 @@ import Quick public class ListSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("list command") { it("lists apps") { expect { try captureStream(stderr) { - try Mas.List.parse([]).run(appLibrary: AppLibraryMock()) + try MAS.List.parse([]).run(appLibrary: AppLibraryMock()) } } == "Error: No installed apps found\n" diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index 4e373c2..fa73ab9 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -17,12 +17,12 @@ public class LuckySpec: QuickSpec { let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) beforeSuite { - Mas.initialize() + MAS.initialize() } xdescribe("lucky command") { xit("installs the first app matching a search") { expect { - try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) + try MAS.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 13693fb..23b7119 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -18,7 +18,7 @@ public class OpenSpec: QuickSpec { let openCommand = OpenSystemCommandMock() beforeSuite { - Mas.initialize() + MAS.initialize() } describe("open command") { beforeEach { @@ -26,13 +26,13 @@ public class OpenSpec: QuickSpec { } it("fails to open app with invalid ID") { expect { - try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -44,7 +44,7 @@ public class OpenSpec: QuickSpec { ) storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Open.parse([mockResult.trackId.description]) + try MAS.Open.parse([mockResult.trackId.description]) .run(storeSearch: storeSearch, openCommand: openCommand) return openCommand.arguments } @@ -52,7 +52,7 @@ public class OpenSpec: QuickSpec { } it("just opens MAS if no app specified") { expect { - try Mas.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand) return openCommand.arguments } == ["macappstore://"] diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index d47b067..72e0f50 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -15,7 +15,7 @@ import Quick public class OutdatedSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("outdated command") { it("displays apps with pending updates") { @@ -48,7 +48,7 @@ public class OutdatedSpec: QuickSpec { ) expect { try captureStream(stdout) { - try Mas.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch) + try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch) } } == "490461369 Bandwidth+ (1.27 -> 1.28)\n" diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift index 51f84f5..798a5e8 100644 --- a/Tests/masTests/Commands/PurchaseSpec.swift +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -14,12 +14,12 @@ import Quick public class PurchaseSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } xdescribe("purchase command") { xit("purchases apps") { expect { - try Mas.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock()) + try MAS.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock()) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/ResetSpec.swift b/Tests/masTests/Commands/ResetSpec.swift index ca39e2b..96e8adb 100644 --- a/Tests/masTests/Commands/ResetSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -14,12 +14,12 @@ import Quick public class ResetSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("reset command") { it("resets the App Store state") { expect { - try Mas.Reset.parse([]).run() + try MAS.Reset.parse([]).run() } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 91a3a45..3984fa1 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -17,7 +17,7 @@ public class SearchSpec: QuickSpec { let storeSearch = StoreSearchMock() beforeSuite { - Mas.initialize() + MAS.initialize() } describe("search command") { beforeEach { @@ -33,14 +33,14 @@ public class SearchSpec: QuickSpec { storeSearch.apps[mockResult.trackId] = mockResult expect { try captureStream(stdout) { - try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + try MAS.Search.parse(["slack"]).run(storeSearch: storeSearch) } } == " 1111 slack (0.0)\n" } it("fails when searching for nonexistent app") { expect { - try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch) + try MAS.Search.parse(["nonexistent"]).run(storeSearch: storeSearch) } .to(throwError(MASError.noSearchResultsFound)) } diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index f7f5d33..ceea01c 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -15,13 +15,13 @@ import Quick public class SignInSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#known-issues describe("signin command") { it("signs in") { expect { - try Mas.SignIn.parse(["", ""]).run() + try MAS.SignIn.parse(["", ""]).run() } .to(throwError(MASError.notSupported)) } diff --git a/Tests/masTests/Commands/SignOutSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift index a5b5466..7aeaa01 100644 --- a/Tests/masTests/Commands/SignOutSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -14,12 +14,12 @@ import Quick public class SignOutSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("signout command") { it("signs out") { expect { - try Mas.SignOut.parse([]).run() + try MAS.SignOut.parse([]).run() } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 69cb44e..fbc3c1a 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -15,7 +15,7 @@ import Quick public class UninstallSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } xdescribe("uninstall command") { let appID: AppID = 12345 @@ -29,7 +29,7 @@ public class UninstallSpec: QuickSpec { let mockLibrary = AppLibraryMock() context("dry run") { - let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appID)]) + let uninstall = try! MAS.Uninstall.parse(["--dry-run", String(appID)]) beforeEach { mockLibrary.reset() @@ -51,7 +51,7 @@ public class UninstallSpec: QuickSpec { } } context("wet run") { - let uninstall = try! Mas.Uninstall.parse([String(appID)]) + let uninstall = try! MAS.Uninstall.parse([String(appID)]) beforeEach { mockLibrary.reset() diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index 5c104bb..585e751 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -15,13 +15,13 @@ import Quick public class UpgradeSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("upgrade command") { it("finds no upgrades") { expect { try captureStream(stderr) { - try Mas.Upgrade.parse([]) + try MAS.Upgrade.parse([]) .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) } } diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index 624f3a3..a73d1e1 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -17,7 +17,7 @@ public class VendorSpec: QuickSpec { let openCommand = OpenSystemCommandMock() beforeSuite { - Mas.initialize() + MAS.initialize() } describe("vendor command") { beforeEach { @@ -25,13 +25,13 @@ public class VendorSpec: QuickSpec { } it("fails to open app with invalid ID") { expect { - try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -44,7 +44,7 @@ public class VendorSpec: QuickSpec { ) storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Vendor.parse([String(mockResult.trackId)]) + try MAS.Vendor.parse([String(mockResult.trackId)]) .run(storeSearch: storeSearch, openCommand: openCommand) return openCommand.arguments } diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index 0828749..ec66b55 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -15,13 +15,13 @@ import Quick public class VersionSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("version command") { it("displays the current version") { expect { try captureStream(stdout) { - try Mas.Version.parse([]).run() + try MAS.Version.parse([]).run() } } == "\(Package.version)\n" diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/MasAppLibrarySpec.swift index 69c2cc9..f484eb5 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/MasAppLibrarySpec.swift @@ -16,7 +16,7 @@ public class MasAppLibrarySpec: QuickSpec { let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) beforeSuite { - Mas.initialize() + MAS.initialize() } describe("mas app library") { it("contains all installed apps") { diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/MasStoreSearchSpec.swift index b6ce5df..3d75ff9 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/MasStoreSearchSpec.swift @@ -14,7 +14,7 @@ import Quick public class MasStoreSearchSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("url string") { it("contains the app name") { diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index 15cecfc..027a196 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -32,7 +32,7 @@ class MASErrorTestCase: XCTestCase { override func setUp() { super.setUp() - Mas.initialize() + MAS.initialize() nserror = NSError(domain: errorDomain, code: 999) localizedDescription = "foo" } diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index a9a5f0e..9b8c1f5 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -14,7 +14,7 @@ import Quick public class OpenSystemCommandSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("open system command") { context("binary path") { diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index 47b3b18..e8c61d6 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -18,7 +18,7 @@ public class AppListFormatterSpec: QuickSpec { var products: [SoftwareProduct] = [] beforeSuite { - Mas.initialize() + MAS.initialize() } describe("app list formatter") { beforeEach { diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 1689fa0..f35a44d 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -18,7 +18,7 @@ public class SearchResultFormatterSpec: QuickSpec { var results: [SearchResult] = [] beforeSuite { - Mas.initialize() + MAS.initialize() } describe("search results formatter") { beforeEach { diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift index 2804491..7895b1c 100644 --- a/Tests/masTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -15,7 +15,7 @@ import Quick public class SearchResultListSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("search result list") { it("can parse bbedit") { diff --git a/Tests/masTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift index 460e961..9462456 100644 --- a/Tests/masTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -15,7 +15,7 @@ import Quick public class SearchResultSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("search result") { it("can parse things") { diff --git a/Tests/masTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift index 80cbde3..ef0e6f1 100644 --- a/Tests/masTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -15,7 +15,7 @@ import Quick public class SoftwareProductSpec: QuickSpec { override public func spec() { beforeSuite { - Mas.initialize() + MAS.initialize() } describe("software product") { let app = SoftwareProductMock( diff --git a/Tests/masTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift index 450bb39..751271c 100644 --- a/Tests/masTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -13,7 +13,7 @@ import XCTest class NetworkManagerTests: XCTestCase { override func setUp() { super.setUp() - Mas.initialize() + MAS.initialize() } func testSuccessfulAsyncResponse() throws { From d7072fc66dda4e2b40bea8afc6a59e8d0cba603a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:29:17 -0400 Subject: [PATCH 77/81] Rename `*StoreSearch` as `*AppStoreSearcher`. Rename `MasStoreSearch` as `ITunesSearchAppStoreSearcher`. Rename `StoreSearchMock` as `MockAppStoreSearcher`. Rename variables/parameters of the above types as `searcher`. Fix 'App Store.app' mention in help. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Home.swift | 6 +++--- Sources/mas/Commands/Info.swift | 6 +++--- Sources/mas/Commands/Lucky.swift | 6 +++--- Sources/mas/Commands/Open.swift | 8 ++++---- Sources/mas/Commands/Outdated.swift | 6 +++--- Sources/mas/Commands/Search.swift | 6 +++--- Sources/mas/Commands/Upgrade.swift | 10 +++++----- Sources/mas/Commands/Vendor.swift | 6 +++--- ...{StoreSearch.swift => AppStoreSearcher.swift} | 6 +++--- ....swift => ITunesSearchAppStoreSearcher.swift} | 4 ++-- Tests/masTests/Commands/HomeSpec.swift | 12 ++++++------ Tests/masTests/Commands/InfoSpec.swift | 12 ++++++------ Tests/masTests/Commands/LuckySpec.swift | 4 ++-- Tests/masTests/Commands/OpenSpec.swift | 14 +++++++------- Tests/masTests/Commands/OutdatedSpec.swift | 6 +++--- Tests/masTests/Commands/SearchSpec.swift | 10 +++++----- Tests/masTests/Commands/UpgradeSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 12 ++++++------ ...ft => ITunesSearchAppStoreSearcherSpec.swift} | 16 ++++++++-------- ...archMock.swift => MockAppStoreSearcher.swift} | 4 ++-- contrib/completion/mas.fish | 2 +- 21 files changed, 79 insertions(+), 79 deletions(-) rename Sources/mas/Controllers/{StoreSearch.swift => AppStoreSearcher.swift} (96%) rename Sources/mas/Controllers/{MasStoreSearch.swift => ITunesSearchAppStoreSearcher.swift} (98%) rename Tests/masTests/Controllers/{MasStoreSearchSpec.swift => ITunesSearchAppStoreSearcherSpec.swift} (77%) rename Tests/masTests/Controllers/{StoreSearchMock.swift => MockAppStoreSearcher.swift} (88%) diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index d9d3b23..c9e186e 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -21,12 +21,12 @@ extension MAS { /// Runs the command. func run() throws { - try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(appID: appID).wait() else { + guard let result = try searcher.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index e832d20..5352097 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -22,12 +22,12 @@ extension MAS { /// Runs the command. func run() throws { - try run(storeSearch: MasStoreSearch()) + try run(searcher: ITunesSearchAppStoreSearcher()) } - func run(storeSearch: StoreSearch) throws { + func run(searcher: AppStoreSearcher) throws { do { - guard let result = try storeSearch.lookup(appID: appID).wait() else { + guard let result = try searcher.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index f2a33c3..eba32b1 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -25,14 +25,14 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { var appID: AppID? do { - let results = try storeSearch.search(for: searchTerm).wait() + let results = try searcher.search(for: searchTerm).wait() guard let result = results.first else { printError("No results found") throw MASError.noSearchResultsFound diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index c2369e9..a924895 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -16,7 +16,7 @@ extension MAS { /// https://performance-partners.apple.com/search-api struct Open: ParsableCommand { static let configuration = CommandConfiguration( - abstract: "Opens app page in AppStore.app" + abstract: "Opens app page in 'App Store.app'" ) @Argument(help: "the app ID") @@ -24,10 +24,10 @@ extension MAS { /// Runs the command. func run() throws { - try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { do { guard let appID else { // If no app ID is given, just open the MAS GUI app @@ -35,7 +35,7 @@ extension MAS { return } - guard let result = try storeSearch.lookup(appID: appID).wait() else { + guard let result = try searcher.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 77325ba..94f5039 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -23,15 +23,15 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { _ = try when( fulfilled: appLibrary.installedApps.map { installedApp in firstly { - storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) + searcher.lookup(appID: installedApp.itemIdentifier.appIDValue) } .done { storeApp in guard let storeApp else { diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index ef14e1e..ae36163 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -23,12 +23,12 @@ extension MAS { var searchTerm: String func run() throws { - try run(storeSearch: MasStoreSearch()) + try run(searcher: ITunesSearchAppStoreSearcher()) } - func run(storeSearch: StoreSearch) throws { + func run(searcher: AppStoreSearcher) throws { do { - let results = try storeSearch.search(for: searchTerm).wait() + let results = try searcher.search(for: searchTerm).wait() if results.isEmpty { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 079550e..a0db6f8 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -22,13 +22,13 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch()) + try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } - func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] do { - apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch) + apps = try findOutdatedApps(appLibrary: appLibrary, searcher: searcher) } catch { throw error as? MASError ?? .searchFailed } @@ -53,7 +53,7 @@ extension MAS { private func findOutdatedApps( appLibrary: AppLibrary, - storeSearch: StoreSearch + searcher: AppStoreSearcher ) throws -> [(SoftwareProduct, SearchResult)] { let apps = appIDs.isEmpty @@ -71,7 +71,7 @@ extension MAS { let promises = apps.map { installedApp in // only upgrade apps whose local version differs from the store version firstly { - storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) + searcher.lookup(appID: installedApp.itemIdentifier.appIDValue) } .map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index d3004a1..31189e3 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -21,12 +21,12 @@ extension MAS { /// Runs the command. func run() throws { - try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand()) + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) } - func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(appID: appID).wait() else { + guard let result = try searcher.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/AppStoreSearcher.swift similarity index 96% rename from Sources/mas/Controllers/StoreSearch.swift rename to Sources/mas/Controllers/AppStoreSearcher.swift index 0b8194c..4e5b65e 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/AppStoreSearcher.swift @@ -1,5 +1,5 @@ // -// StoreSearch.swift +// AppStoreSearcher.swift // mas // // Created by Ben Chatelain on 12/29/18. @@ -10,7 +10,7 @@ import Foundation import PromiseKit /// Protocol for searching the MAS catalog. -protocol StoreSearch { +protocol AppStoreSearcher { func lookup(appID: AppID) -> Promise func search(for searchTerm: String) -> Promise<[SearchResult]> } @@ -37,7 +37,7 @@ private enum URLAction { } // MARK: - Common methods -extension StoreSearch { +extension AppStoreSearcher { /// Builds the search URL for an app. /// /// - Parameters: diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift similarity index 98% rename from Sources/mas/Controllers/MasStoreSearch.swift rename to Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift index a046778..c6de321 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift @@ -1,5 +1,5 @@ // -// MasStoreSearch.swift +// ITunesSearchAppStoreSearcher.swift // mas // // Created by Ben Chatelain on 12/29/18. @@ -12,7 +12,7 @@ import Regex import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. -class MasStoreSearch: StoreSearch { +class ITunesSearchAppStoreSearcher: AppStoreSearcher { private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#) // CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 46d22f6..8e04cf9 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -13,7 +13,7 @@ import Quick public class HomeSpec: QuickSpec { override public func spec() { - let storeSearch = StoreSearchMock() + let searcher = MockAppStoreSearcher() let openCommand = OpenSystemCommandMock() beforeSuite { @@ -21,17 +21,17 @@ public class HomeSpec: QuickSpec { } describe("home command") { beforeEach { - storeSearch.reset() + searcher.reset() } it("fails to open app with invalid ID") { expect { - try MAS.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Home.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try MAS.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Home.parse(["999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -41,10 +41,10 @@ public class HomeSpec: QuickSpec { trackViewUrl: "mas preview url", version: "0.0" ) - storeSearch.apps[mockResult.trackId] = mockResult + searcher.apps[mockResult.trackId] = mockResult expect { try MAS.Home.parse([String(mockResult.trackId)]) - .run(storeSearch: storeSearch, openCommand: openCommand) + .run(searcher: searcher, openCommand: openCommand) return openCommand.arguments } == [mockResult.trackViewUrl] diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 400ddb6..3632480 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -14,24 +14,24 @@ import Quick public class InfoSpec: QuickSpec { override public func spec() { - let storeSearch = StoreSearchMock() + let searcher = MockAppStoreSearcher() beforeSuite { MAS.initialize() } describe("Info command") { beforeEach { - storeSearch.reset() + searcher.reset() } it("fails to open app with invalid ID") { expect { - try MAS.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) + try MAS.Info.parse(["--", "-999"]).run(searcher: searcher) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try MAS.Info.parse(["999"]).run(storeSearch: storeSearch) + try MAS.Info.parse(["999"]).run(searcher: searcher) } .to(throwError(MASError.noSearchResultsFound)) } @@ -47,10 +47,10 @@ public class InfoSpec: QuickSpec { trackViewUrl: "https://awesome.app", version: "1.0" ) - storeSearch.apps[mockResult.trackId] = mockResult + searcher.apps[mockResult.trackId] = mockResult expect { try captureStream(stdout) { - try MAS.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch) + try MAS.Info.parse([String(mockResult.trackId)]).run(searcher: searcher) } } == """ diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index fa73ab9..26eff44 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -14,7 +14,7 @@ import Quick public class LuckySpec: QuickSpec { override public func spec() { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) beforeSuite { MAS.initialize() @@ -22,7 +22,7 @@ public class LuckySpec: QuickSpec { xdescribe("lucky command") { xit("installs the first app matching a search") { expect { - try MAS.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch) + try MAS.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), searcher: searcher) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 23b7119..8459f7b 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -14,7 +14,7 @@ import Quick public class OpenSpec: QuickSpec { override public func spec() { - let storeSearch = StoreSearchMock() + let searcher = MockAppStoreSearcher() let openCommand = OpenSystemCommandMock() beforeSuite { @@ -22,17 +22,17 @@ public class OpenSpec: QuickSpec { } describe("open command") { beforeEach { - storeSearch.reset() + searcher.reset() } it("fails to open app with invalid ID") { expect { - try MAS.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try MAS.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse(["999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -42,17 +42,17 @@ public class OpenSpec: QuickSpec { trackViewUrl: "fakescheme://some/url", version: "0.0" ) - storeSearch.apps[mockResult.trackId] = mockResult + searcher.apps[mockResult.trackId] = mockResult expect { try MAS.Open.parse([mockResult.trackId.description]) - .run(storeSearch: storeSearch, openCommand: openCommand) + .run(searcher: searcher, openCommand: openCommand) return openCommand.arguments } == ["macappstore://some/url"] } it("just opens MAS if no app specified") { expect { - try MAS.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Open.parse([]).run(searcher: searcher, openCommand: openCommand) return openCommand.arguments } == ["macappstore://"] diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 72e0f50..6ca3d01 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -33,8 +33,8 @@ public class OutdatedSpec: QuickSpec { trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4", version: "1.28" ) - let mockStoreSearch = StoreSearchMock() - mockStoreSearch.apps[mockSearchResult.trackId] = mockSearchResult + let searcher = MockAppStoreSearcher() + searcher.apps[mockSearchResult.trackId] = mockSearchResult let mockAppLibrary = AppLibraryMock() mockAppLibrary.installedApps.append( @@ -48,7 +48,7 @@ public class OutdatedSpec: QuickSpec { ) expect { try captureStream(stdout) { - try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch) + try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, searcher: searcher) } } == "490461369 Bandwidth+ (1.27 -> 1.28)\n" diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 3984fa1..058f5c4 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -14,14 +14,14 @@ import Quick public class SearchSpec: QuickSpec { override public func spec() { - let storeSearch = StoreSearchMock() + let searcher = MockAppStoreSearcher() beforeSuite { MAS.initialize() } describe("search command") { beforeEach { - storeSearch.reset() + searcher.reset() } it("can find slack") { let mockResult = SearchResult( @@ -30,17 +30,17 @@ public class SearchSpec: QuickSpec { trackViewUrl: "mas preview url", version: "0.0" ) - storeSearch.apps[mockResult.trackId] = mockResult + searcher.apps[mockResult.trackId] = mockResult expect { try captureStream(stdout) { - try MAS.Search.parse(["slack"]).run(storeSearch: storeSearch) + try MAS.Search.parse(["slack"]).run(searcher: searcher) } } == " 1111 slack (0.0)\n" } it("fails when searching for nonexistent app") { expect { - try MAS.Search.parse(["nonexistent"]).run(storeSearch: storeSearch) + try MAS.Search.parse(["nonexistent"]).run(searcher: searcher) } .to(throwError(MASError.noSearchResultsFound)) } diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index 585e751..e2d42f6 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -22,7 +22,7 @@ public class UpgradeSpec: QuickSpec { expect { try captureStream(stderr) { try MAS.Upgrade.parse([]) - .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock()) + .run(appLibrary: AppLibraryMock(), searcher: MockAppStoreSearcher()) } } == "Warning: Nothing found to upgrade\n" diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index a73d1e1..8291658 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -13,7 +13,7 @@ import Quick public class VendorSpec: QuickSpec { override public func spec() { - let storeSearch = StoreSearchMock() + let searcher = MockAppStoreSearcher() let openCommand = OpenSystemCommandMock() beforeSuite { @@ -21,17 +21,17 @@ public class VendorSpec: QuickSpec { } describe("vendor command") { beforeEach { - storeSearch.reset() + searcher.reset() } it("fails to open app with invalid ID") { expect { - try MAS.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError()) } it("can't find app with unknown ID") { expect { - try MAS.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand) + try MAS.Vendor.parse(["999"]).run(searcher: searcher, openCommand: openCommand) } .to(throwError(MASError.noSearchResultsFound)) } @@ -42,10 +42,10 @@ public class VendorSpec: QuickSpec { trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4", version: "0.0" ) - storeSearch.apps[mockResult.trackId] = mockResult + searcher.apps[mockResult.trackId] = mockResult expect { try MAS.Vendor.parse([String(mockResult.trackId)]) - .run(storeSearch: storeSearch, openCommand: openCommand) + .run(searcher: searcher, openCommand: openCommand) return openCommand.arguments } == [mockResult.sellerUrl] diff --git a/Tests/masTests/Controllers/MasStoreSearchSpec.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift similarity index 77% rename from Tests/masTests/Controllers/MasStoreSearchSpec.swift rename to Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift index 3d75ff9..3e9f9cd 100644 --- a/Tests/masTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift @@ -1,5 +1,5 @@ // -// MasStoreSearchSpec.swift +// ITunesSearchAppStoreSearcherSpec.swift // masTests // // Created by Ben Chatelain on 1/4/19. @@ -11,7 +11,7 @@ import Quick @testable import mas -public class MasStoreSearchSpec: QuickSpec { +public class ITunesSearchAppStoreSearcherSpec: QuickSpec { override public func spec() { beforeSuite { MAS.initialize() @@ -19,13 +19,13 @@ public class MasStoreSearchSpec: QuickSpec { describe("url string") { it("contains the app name") { expect { - MasStoreSearch().searchURL(for: "myapp", inCountry: "US")?.absoluteString + ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString } == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp" } it("contains the encoded app name") { expect { - MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString + ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString } == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App" } @@ -34,10 +34,10 @@ public class MasStoreSearchSpec: QuickSpec { context("when searched") { it("can find slack") { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) expect { - try storeSearch.search(for: "slack").wait() + try searcher.search(for: "slack").wait() } .to(haveCount(39)) } @@ -47,11 +47,11 @@ public class MasStoreSearchSpec: QuickSpec { it("can find slack") { let appID: AppID = 803_453_959 let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) var result: SearchResult? do { - result = try storeSearch.lookup(appID: appID).wait() + result = try searcher.lookup(appID: appID).wait() } catch { let maserror = error as! MASError if case .jsonParsing(let nserror) = maserror { diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/MockAppStoreSearcher.swift similarity index 88% rename from Tests/masTests/Controllers/StoreSearchMock.swift rename to Tests/masTests/Controllers/MockAppStoreSearcher.swift index 4b5143d..1df0ef6 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/MockAppStoreSearcher.swift @@ -1,5 +1,5 @@ // -// StoreSearchMock.swift +// MockAppStoreSearcher.swift // masTests // // Created by Ben Chatelain on 1/4/19. @@ -10,7 +10,7 @@ import PromiseKit @testable import mas -class StoreSearchMock: StoreSearch { +class MockAppStoreSearcher: AppStoreSearcher { var apps: [AppID: SearchResult] = [:] func search(for searchTerm: String) -> Promise<[SearchResult]> { diff --git a/contrib/completion/mas.fish b/contrib/completion/mas.fish index 592218a..de9576f 100644 --- a/contrib/completion/mas.fish +++ b/contrib/completion/mas.fish @@ -46,7 +46,7 @@ complete -c mas -n "__fish_seen_subcommand_from help" -xa "list" complete -c mas -n "__fish_use_subcommand" -f -a lucky -d "Install the first result from the Mac App Store" complete -c mas -n "__fish_seen_subcommand_from help" -xa "lucky" ### open -complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in AppStore.app" +complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in 'App Store.app'" complete -c mas -n "__fish_seen_subcommand_from help" -xa "open" ### outdated complete -c mas -n "__fish_use_subcommand" -f -a outdated -d "Lists pending updates from the Mac App Store" From cc219fe644a9ff667e664c32811b4b13439675fd Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:48:03 -0400 Subject: [PATCH 78/81] Rename `MasAppLibrary` as `SoftwareMapAppLibrary`. Rename `AppLibraryMock` as `MockAppLibrary`. Partial #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Install.swift | 2 +- Sources/mas/Commands/List.swift | 2 +- Sources/mas/Commands/Lucky.swift | 2 +- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Purchase.swift | 2 +- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Commands/Upgrade.swift | 2 +- .../{MasAppLibrary.swift => SoftwareMapAppLibrary.swift} | 4 ++-- Tests/masTests/Commands/InstallSpec.swift | 2 +- Tests/masTests/Commands/ListSpec.swift | 2 +- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OutdatedSpec.swift | 2 +- Tests/masTests/Commands/PurchaseSpec.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/UpgradeSpec.swift | 2 +- .../{AppLibraryMock.swift => MockAppLibrary.swift} | 6 +++--- ...AppLibrarySpec.swift => SoftwareMapAppLibrarySpec.swift} | 6 +++--- 17 files changed, 22 insertions(+), 22 deletions(-) rename Sources/mas/Controllers/{MasAppLibrary.swift => SoftwareMapAppLibrary.swift} (98%) rename Tests/masTests/Controllers/{AppLibraryMock.swift => MockAppLibrary.swift} (87%) rename Tests/masTests/Controllers/{MasAppLibrarySpec.swift => SoftwareMapAppLibrarySpec.swift} (88%) diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 2b13081..9b051e7 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -23,7 +23,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary()) + try run(appLibrary: SoftwareMapAppLibrary()) } func run(appLibrary: AppLibrary) throws { diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index eb782e2..94544c9 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -17,7 +17,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary()) + try run(appLibrary: SoftwareMapAppLibrary()) } func run(appLibrary: AppLibrary) throws { diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index eba32b1..781f55f 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -25,7 +25,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 94f5039..a495c1b 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -23,7 +23,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 66f4c92..d775c64 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -20,7 +20,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary()) + try run(appLibrary: SoftwareMapAppLibrary()) } func run(appLibrary: AppLibrary) throws { diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 68561c7..d9997a7 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -24,7 +24,7 @@ extension MAS { /// Runs the uninstall command. func run() throws { - try run(appLibrary: MasAppLibrary()) + try run(appLibrary: SoftwareMapAppLibrary()) } func run(appLibrary: AppLibrary) throws { diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index a0db6f8..1c1630d 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -22,7 +22,7 @@ extension MAS { /// Runs the command. func run() throws { - try run(appLibrary: MasAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) } func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift similarity index 98% rename from Sources/mas/Controllers/MasAppLibrary.swift rename to Sources/mas/Controllers/SoftwareMapAppLibrary.swift index d9e2f7d..7fc48e7 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift @@ -1,5 +1,5 @@ // -// MasAppLibrary.swift +// SoftwareMapAppLibrary.swift // mas // // Created by Ben Chatelain on 12/27/18. @@ -10,7 +10,7 @@ import CommerceKit import ScriptingBridge /// Utility for managing installed apps. -class MasAppLibrary: AppLibrary { +class SoftwareMapAppLibrary: AppLibrary { /// CommerceKit's singleton manager of installed software. private let softwareMap: SoftwareMap diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift index 88ad308..e2074c6 100644 --- a/Tests/masTests/Commands/InstallSpec.swift +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -19,7 +19,7 @@ public class InstallSpec: QuickSpec { xdescribe("install command") { xit("installs apps") { expect { - try MAS.Install.parse([]).run(appLibrary: AppLibraryMock()) + try MAS.Install.parse([]).run(appLibrary: MockAppLibrary()) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift index c72bdf6..e9c02d6 100644 --- a/Tests/masTests/Commands/ListSpec.swift +++ b/Tests/masTests/Commands/ListSpec.swift @@ -21,7 +21,7 @@ public class ListSpec: QuickSpec { it("lists apps") { expect { try captureStream(stderr) { - try MAS.List.parse([]).run(appLibrary: AppLibraryMock()) + try MAS.List.parse([]).run(appLibrary: MockAppLibrary()) } } == "Error: No installed apps found\n" diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index 26eff44..cf35504 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -22,7 +22,7 @@ public class LuckySpec: QuickSpec { xdescribe("lucky command") { xit("installs the first app matching a search") { expect { - try MAS.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), searcher: searcher) + try MAS.Lucky.parse(["Slack"]).run(appLibrary: MockAppLibrary(), searcher: searcher) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 6ca3d01..5831c56 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -36,7 +36,7 @@ public class OutdatedSpec: QuickSpec { let searcher = MockAppStoreSearcher() searcher.apps[mockSearchResult.trackId] = mockSearchResult - let mockAppLibrary = AppLibraryMock() + let mockAppLibrary = MockAppLibrary() mockAppLibrary.installedApps.append( SoftwareProductMock( appName: mockSearchResult.trackName, diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift index 798a5e8..ac9db18 100644 --- a/Tests/masTests/Commands/PurchaseSpec.swift +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -19,7 +19,7 @@ public class PurchaseSpec: QuickSpec { xdescribe("purchase command") { xit("purchases apps") { expect { - try MAS.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock()) + try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary()) } .toNot(throwError()) } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index fbc3c1a..bfd68d0 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -26,7 +26,7 @@ public class UninstallSpec: QuickSpec { bundleVersion: "1.0", itemIdentifier: NSNumber(value: appID) ) - let mockLibrary = AppLibraryMock() + let mockLibrary = MockAppLibrary() context("dry run") { let uninstall = try! MAS.Uninstall.parse(["--dry-run", String(appID)]) diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift index e2d42f6..6630777 100644 --- a/Tests/masTests/Commands/UpgradeSpec.swift +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -22,7 +22,7 @@ public class UpgradeSpec: QuickSpec { expect { try captureStream(stderr) { try MAS.Upgrade.parse([]) - .run(appLibrary: AppLibraryMock(), searcher: MockAppStoreSearcher()) + .run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher()) } } == "Warning: Nothing found to upgrade\n" diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/MockAppLibrary.swift similarity index 87% rename from Tests/masTests/Controllers/AppLibraryMock.swift rename to Tests/masTests/Controllers/MockAppLibrary.swift index 96ac534..c992abc 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/MockAppLibrary.swift @@ -1,5 +1,5 @@ // -// AppLibraryMock.swift +// MockAppLibrary.swift // masTests // // Created by Ben Chatelain on 12/27/18. @@ -8,7 +8,7 @@ @testable import mas -class AppLibraryMock: AppLibrary { +class MockAppLibrary: AppLibrary { var installedApps: [SoftwareProduct] = [] func uninstallApps(atPaths appPaths: [String]) throws { @@ -20,7 +20,7 @@ class AppLibraryMock: AppLibrary { } /// Members not part of the AppLibrary protocol that are only for test state management. -extension AppLibraryMock { +extension MockAppLibrary { /// Clears out the list of installed apps. func reset() { installedApps = [] diff --git a/Tests/masTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift similarity index 88% rename from Tests/masTests/Controllers/MasAppLibrarySpec.swift rename to Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift index f484eb5..47b29f4 100644 --- a/Tests/masTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift @@ -1,5 +1,5 @@ // -// MasAppLibrarySpec.swift +// SoftwareMapAppLibrarySpec.swift // masTests // // Created by Ben Chatelain on 3/1/20. @@ -11,9 +11,9 @@ import Quick @testable import mas -public class MasAppLibrarySpec: QuickSpec { +public class SoftwareMapAppLibrarySpec: QuickSpec { override public func spec() { - let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) + let library = SoftwareMapAppLibrary(softwareMap: SoftwareMapMock(products: apps)) beforeSuite { MAS.initialize() From 880843d8e646d5f91a41003882b9d8666c0b439e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:09:31 -0400 Subject: [PATCH 79/81] Rename `*Mock` as `Mock*`. Resolve #585 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/masTests/Commands/HomeSpec.swift | 2 +- Tests/masTests/Commands/LuckySpec.swift | 2 +- Tests/masTests/Commands/OpenSpec.swift | 2 +- Tests/masTests/Commands/OutdatedSpec.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/VendorSpec.swift | 2 +- .../Controllers/ITunesSearchAppStoreSearcherSpec.swift | 4 ++-- .../masTests/Controllers/SoftwareMapAppLibrarySpec.swift | 8 ++++---- Tests/masTests/Extensions/Bundle+JSON.swift | 2 +- ...ystemCommandMock.swift => MockOpenSystemCommand.swift} | 4 ++-- Tests/masTests/Formatters/AppListFormatterSpec.swift | 6 +++--- ...oftwareProductMock.swift => MockSoftwareProduct.swift} | 4 ++-- Tests/masTests/Models/SoftwareProductSpec.swift | 2 +- ...ockFromFile.swift => MockFromFileNetworkSession.swift} | 4 ++-- ...{NetworkSessionMock.swift => MockNetworkSession.swift} | 4 ++-- Tests/masTests/Network/NetworkManagerTests.swift | 8 ++++---- 16 files changed, 29 insertions(+), 29 deletions(-) rename Tests/masTests/ExternalCommands/{OpenSystemCommandMock.swift => MockOpenSystemCommand.swift} (85%) rename Tests/masTests/Models/{SoftwareProductMock.swift => MockSoftwareProduct.swift} (80%) rename Tests/masTests/Network/{NetworkSessionMockFromFile.swift => MockFromFileNetworkSession.swift} (91%) rename Tests/masTests/Network/{NetworkSessionMock.swift => MockNetworkSession.swift} (89%) diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift index 8e04cf9..8d3faac 100644 --- a/Tests/masTests/Commands/HomeSpec.swift +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -14,7 +14,7 @@ import Quick public class HomeSpec: QuickSpec { override public func spec() { let searcher = MockAppStoreSearcher() - let openCommand = OpenSystemCommandMock() + let openCommand = MockOpenSystemCommand() beforeSuite { MAS.initialize() diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift index cf35504..e288739 100644 --- a/Tests/masTests/Commands/LuckySpec.swift +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -13,7 +13,7 @@ import Quick public class LuckySpec: QuickSpec { override public func spec() { - let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") + let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json") let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) beforeSuite { diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift index 8459f7b..5f3ee72 100644 --- a/Tests/masTests/Commands/OpenSpec.swift +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -15,7 +15,7 @@ import Quick public class OpenSpec: QuickSpec { override public func spec() { let searcher = MockAppStoreSearcher() - let openCommand = OpenSystemCommandMock() + let openCommand = MockOpenSystemCommand() beforeSuite { MAS.initialize() diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index 5831c56..95ced8e 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -38,7 +38,7 @@ public class OutdatedSpec: QuickSpec { let mockAppLibrary = MockAppLibrary() mockAppLibrary.installedApps.append( - SoftwareProductMock( + MockSoftwareProduct( appName: mockSearchResult.trackName, bundleIdentifier: mockSearchResult.bundleId, bundlePath: "/Applications/Bandwidth+.app", diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index bfd68d0..7ea7c44 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -19,7 +19,7 @@ public class UninstallSpec: QuickSpec { } xdescribe("uninstall command") { let appID: AppID = 12345 - let app = SoftwareProductMock( + let app = MockSoftwareProduct( appName: "Some App", bundleIdentifier: "com.some.app", bundlePath: "/tmp/Some.app", diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift index 8291658..1834cb3 100644 --- a/Tests/masTests/Commands/VendorSpec.swift +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -14,7 +14,7 @@ import Quick public class VendorSpec: QuickSpec { override public func spec() { let searcher = MockAppStoreSearcher() - let openCommand = OpenSystemCommandMock() + let openCommand = MockOpenSystemCommand() beforeSuite { MAS.initialize() diff --git a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift index 3e9f9cd..3b72ee8 100644 --- a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift +++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift @@ -33,7 +33,7 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec { describe("store") { context("when searched") { it("can find slack") { - let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") + let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json") let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) expect { @@ -46,7 +46,7 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec { context("when lookup used") { it("can find slack") { let appID: AppID = 803_453_959 - let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") + let networkSession = MockFromFileNetworkSession(responseFile: "lookup/slack.json") let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) var result: SearchResult? diff --git a/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift index 47b29f4..c4e50ca 100644 --- a/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift @@ -13,7 +13,7 @@ import Quick public class SoftwareMapAppLibrarySpec: QuickSpec { override public func spec() { - let library = SoftwareMapAppLibrary(softwareMap: SoftwareMapMock(products: apps)) + let library = SoftwareMapAppLibrary(softwareMap: MockSoftwareMap(products: apps)) beforeSuite { MAS.initialize() @@ -31,7 +31,7 @@ public class SoftwareMapAppLibrarySpec: QuickSpec { } // MARK: - Test Data -let myApp = SoftwareProductMock( +let myApp = MockSoftwareProduct( appName: "MyApp", bundleIdentifier: "com.example", bundlePath: "/Applications/MyApp.app", @@ -41,8 +41,8 @@ let myApp = SoftwareProductMock( var apps: [SoftwareProduct] = [myApp] -// MARK: - SoftwareMapMock -struct SoftwareMapMock: SoftwareMap { +// MARK: - MockSoftwareMap +struct MockSoftwareMap: SoftwareMap { var products: [SoftwareProduct] = [] func allSoftwareProducts() -> [SoftwareProduct] { diff --git a/Tests/masTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift index d74c2c0..19c2c0d 100644 --- a/Tests/masTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -26,7 +26,7 @@ extension Bundle { static func url(for fileName: String) -> URL? { // The Swift Package Manager places resources in a separate bundle from the executable. // https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051 - let bundleURL = Bundle(for: NetworkSessionMock.self) + let bundleURL = Bundle(for: MockNetworkSession.self) .bundleURL .deletingLastPathComponent() .appendingPathComponent("mas_masTests.bundle") diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift b/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift similarity index 85% rename from Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift rename to Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift index 08d0f4b..da30e13 100644 --- a/Tests/masTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift @@ -1,5 +1,5 @@ // -// OpenSystemCommandMock.swift +// MockOpenSystemCommand.swift // masTests // // Created by Ben Chatelain on 1/4/19. @@ -10,7 +10,7 @@ import Foundation @testable import mas -class OpenSystemCommandMock: ExternalCommand { +class MockOpenSystemCommand: ExternalCommand { // Stub out protocol logic var succeeded = true var arguments: [String] = [] diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index e8c61d6..1b92397 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -28,7 +28,7 @@ public class AppListFormatterSpec: QuickSpec { expect(format(products)).to(beEmpty()) } it("can format a single product") { - let product = SoftwareProductMock( + let product = MockSoftwareProduct( appName: "Awesome App", bundleIdentifier: "", bundlePath: "", @@ -39,14 +39,14 @@ public class AppListFormatterSpec: QuickSpec { } it("can format two products") { products = [ - SoftwareProductMock( + MockSoftwareProduct( appName: "Awesome App", bundleIdentifier: "", bundlePath: "", bundleVersion: "19.2.1", itemIdentifier: 12345 ), - SoftwareProductMock( + MockSoftwareProduct( appName: "Even Better App", bundleIdentifier: "", bundlePath: "", diff --git a/Tests/masTests/Models/SoftwareProductMock.swift b/Tests/masTests/Models/MockSoftwareProduct.swift similarity index 80% rename from Tests/masTests/Models/SoftwareProductMock.swift rename to Tests/masTests/Models/MockSoftwareProduct.swift index 91de06b..ca03bc7 100644 --- a/Tests/masTests/Models/SoftwareProductMock.swift +++ b/Tests/masTests/Models/MockSoftwareProduct.swift @@ -1,5 +1,5 @@ // -// SoftwareProductMock.swift +// MockSoftwareProduct.swift // masTests // // Created by Ben Chatelain on 12/27/18. @@ -10,7 +10,7 @@ import Foundation @testable import mas -struct SoftwareProductMock: SoftwareProduct { +struct MockSoftwareProduct: SoftwareProduct { var appName: String var bundleIdentifier: String var bundlePath: String diff --git a/Tests/masTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift index ef0e6f1..cd5e15a 100644 --- a/Tests/masTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -18,7 +18,7 @@ public class SoftwareProductSpec: QuickSpec { MAS.initialize() } describe("software product") { - let app = SoftwareProductMock( + let app = MockSoftwareProduct( appName: "App", bundleIdentifier: "", bundlePath: "", diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/MockFromFileNetworkSession.swift similarity index 91% rename from Tests/masTests/Network/NetworkSessionMockFromFile.swift rename to Tests/masTests/Network/MockFromFileNetworkSession.swift index 2ab1fa1..893f15e 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/MockFromFileNetworkSession.swift @@ -1,5 +1,5 @@ // -// NetworkSessionMockFromFile.swift +// MockFromFileNetworkSession.swift // masTests // // Created by Ben Chatelain on 2019-01-05. @@ -10,7 +10,7 @@ import Foundation import PromiseKit /// Mock NetworkSession for testing with saved JSON response payload files. -class NetworkSessionMockFromFile: NetworkSessionMock { +class MockFromFileNetworkSession: MockNetworkSession { /// Path to response payload file relative to test bundle. private let responseFile: String diff --git a/Tests/masTests/Network/NetworkSessionMock.swift b/Tests/masTests/Network/MockNetworkSession.swift similarity index 89% rename from Tests/masTests/Network/NetworkSessionMock.swift rename to Tests/masTests/Network/MockNetworkSession.swift index 31274cc..504ff32 100644 --- a/Tests/masTests/Network/NetworkSessionMock.swift +++ b/Tests/masTests/Network/MockNetworkSession.swift @@ -1,5 +1,5 @@ // -// NetworkSessionMock +// MockNetworkSession // masTests // // Created by Ben Chatelain on 11/13/18. @@ -12,7 +12,7 @@ import PromiseKit @testable import mas /// Mock NetworkSession for testing. -class NetworkSessionMock: NetworkSession { +class MockNetworkSession: NetworkSession { // Properties that enable us to set exactly what data or error // we want our mocked URLSession to return for any request. var data: Data? diff --git a/Tests/masTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift index 751271c..6f3e520 100644 --- a/Tests/masTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -18,7 +18,7 @@ class NetworkManagerTests: XCTestCase { func testSuccessfulAsyncResponse() throws { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) // Create data and tell the session to always return it @@ -35,7 +35,7 @@ class NetworkManagerTests: XCTestCase { func testSuccessfulSyncResponse() throws { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) // Create data and tell the session to always return it @@ -52,7 +52,7 @@ class NetworkManagerTests: XCTestCase { func testFailureAsyncResponse() { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) session.error = MASError.noData @@ -73,7 +73,7 @@ class NetworkManagerTests: XCTestCase { func testFailureSyncResponse() { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) session.error = MASError.noData From 1b4c97f652bd97e0a1db896983df105fd0d535c8 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 26 Oct 2024 01:54:41 -0400 Subject: [PATCH 80/81] Use `formattedPrice` instead of `price`. Improve price output. Partial #597 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Formatters/AppInfoFormatter.swift | 2 +- .../Formatters/SearchResultFormatter.swift | 5 +- Sources/mas/Models/SearchResult.swift | 3 + Tests/masTests/Commands/InfoSpec.swift | 4 +- Tests/masTests/Commands/SearchSpec.swift | 2 +- .../SearchResultFormatterSpec.swift | 20 +- .../JSON/search/things-that-go-bump.json | 2 + Tests/masTests/JSON/search/things.json | 4202 ++++------------- 8 files changed, 891 insertions(+), 3349 deletions(-) diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift index 3d5f114..0e70a55 100644 --- a/Sources/mas/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -18,7 +18,7 @@ enum AppInfoFormatter { let headline = [ "\(app.trackName)", "\(app.version)", - "[\(app.price ?? 0)]", + "[\(app.formattedPrice)]", ] .joined(separator: " ") diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index 7145d87..c0bb513 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -27,12 +27,11 @@ enum SearchResultFormatter { let appID = result.trackId let appName = result.trackName.padding(toLength: maxLength, withPad: " ", startingAt: 0) let version = result.version - let price = result.price ?? 0.0 if includePrice { - output += String(format: "%12lu %@ $%5.2f (%@)\n", appID, appName, price, version) + output += String(format: "%12lu %@ (%@) %@\n", appID, appName, version, result.formattedPrice) } else { - output += String(format: "%12lu %@ (%@)\n", appID, appName, version) + output += String(format: "%12lu %@ (%@)\n", appID, appName, version) } } diff --git a/Sources/mas/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift index 129df7c..0c26618 100644 --- a/Sources/mas/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -10,6 +10,7 @@ struct SearchResult: Decodable { var bundleId: String var currentVersionReleaseDate: String var fileSizeBytes: String? + var formattedPrice: String var minimumOsVersion: String var price: Double? var sellerName: String @@ -23,6 +24,7 @@ struct SearchResult: Decodable { bundleId: String = "", currentVersionReleaseDate: String = "", fileSizeBytes: String = "0", + formattedPrice: String = "0", minimumOsVersion: String = "", price: Double = 0.0, sellerName: String = "", @@ -35,6 +37,7 @@ struct SearchResult: Decodable { self.bundleId = bundleId self.currentVersionReleaseDate = currentVersionReleaseDate self.fileSizeBytes = fileSizeBytes + self.formattedPrice = formattedPrice self.minimumOsVersion = minimumOsVersion self.price = price self.sellerName = sellerName diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index 3632480..04b393c 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -39,8 +39,8 @@ public class InfoSpec: QuickSpec { let mockResult = SearchResult( currentVersionReleaseDate: "2019-01-07T18:53:13Z", fileSizeBytes: "1024", + formattedPrice: "$2.00", minimumOsVersion: "10.14", - price: 2.0, sellerName: "Awesome Dev", trackId: 1111, trackName: "Awesome App", @@ -54,7 +54,7 @@ public class InfoSpec: QuickSpec { } } == """ - Awesome App 1.0 [2.0] + Awesome App 1.0 [$2.00] By: Awesome Dev Released: 2019-01-07 Minimum OS: 10.14 diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 058f5c4..183e79f 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -36,7 +36,7 @@ public class SearchSpec: QuickSpec { try MAS.Search.parse(["slack"]).run(searcher: searcher) } } - == " 1111 slack (0.0)\n" + == " 1111 slack (0.0)\n" } it("fails when searching for nonexistent app") { expect { diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index f35a44d..2db3e52 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -29,57 +29,57 @@ public class SearchResultFormatterSpec: QuickSpec { } it("can format a single result") { let result = SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ) - expect(format([result], false)) == " 12345 Awesome App (19.2.1)" + expect(format([result], false)) == " 12345 Awesome App (19.2.1)" } it("can format a single result with price") { let result = SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ) - expect(format([result], true)) == " 12345 Awesome App $ 9.87 (19.2.1)" + expect(format([result], true)) == " 12345 Awesome App (19.2.1) $9.87" } it("can format a two results") { results = [ SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ), SearchResult( - price: 0.01, + formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0" ), ] expect(format(results, false)) - == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" + == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" } it("can format a two results with prices") { results = [ SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ), SearchResult( - price: 0.01, + formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0" ), ] expect(format(results, true)) - == " 12345 Awesome App $ 9.87 (19.2.1)\n 67890 Even Better App $ 0.01 (1.2.0)" + == " 12345 Awesome App (19.2.1) $9.87\n 67890 Even Better App (1.2.0) $0.01" } } } diff --git a/Tests/masTests/JSON/search/things-that-go-bump.json b/Tests/masTests/JSON/search/things-that-go-bump.json index c413b22..91633f3 100644 --- a/Tests/masTests/JSON/search/things-that-go-bump.json +++ b/Tests/masTests/JSON/search/things-that-go-bump.json @@ -19,6 +19,8 @@ "trackName": "Things That Go Bump", "trackId": 1472954003, "sellerName": "Tinybop Inc.", + "price": 0.99, + "formattedPrice": "$0.99", "releaseNotes": "* BOOM *, this is a BIG update. The house spawns a game room, complete with video games you can ENTER INTO. It's fun and a little bit weird! Try it! \n»-(¯`·.·´¯)->", "primaryGenreId": 6014, "primaryGenreName": "Games", diff --git a/Tests/masTests/JSON/search/things.json b/Tests/masTests/JSON/search/things.json index 091e35d..2c0bfb7 100644 --- a/Tests/masTests/JSON/search/things.json +++ b/Tests/masTests/JSON/search/things.json @@ -1,3333 +1,871 @@ { - "resultCount": 50, - "results": [ - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/6f/4c/e5/6f4ce5d6-7caa-d1eb-bbc9-86558e97d2ba/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/92/b4/f8/92b4f8f5-f133-abd8-db17-135ac27bb1fa/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/72/63/63/726363b9-45ff-f93e-975c-fb69836eaf1a/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/29/fa/63/29fa63e3-3cb2-8b8a-8541-31fa9b7ef27f/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/da/17/5f/da175f95-c2cd-e5df-8cbc-d800d6770c64/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/f1/82/37/f182376c-4f25-6dbb-c6a8-5e6c1c617620/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/cultured-code-gmbh-co-kg/id284971784?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13.0", - "trackName": "Things 3", - "trackId": 904280696, - "sellerName": "Cultured Code GmbH & Co. KG", - "releaseNotes": "• Moved the database file to a new location (now at /Library/Group Containers/).\n• Increased the clickable area of items in the sidebar.\n• Improved the formatting of years in Japanese.\n• Fixed some crashes that could occur when hitting Cmd+[ or ] in Quick Entry while the When popover was visible.\n• Updated the crash reporter.\n• Some sync improvements.\n\n\nNEW IN 3.12\n\nWe’re excited to release Things 3.12 – a big update for our Watch app!\n\nWe’ve entirely rebuilt its foundation to allow it to sync and operate without your phone being nearby. We’ve also taken this opportunity to add some often-requested features to the app. For more information about this release, please visit our blog: thingsapp.com\n\nThere are no huge changes in this release for Mac, but there’s one great new feature you should know about: you can now edit the Tags or Deadlines of collapsed to-dos – even for multiple to-dos at once – by hitting Cmd+Shift+T or D. It’s super convenient :)", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-05-18T16:42:04Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-08-04T07:57:44Z", - "trackCensoredName": "Things 3", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "RU", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "17474797", - "sellerUrl": "https://culturedcode.com/things/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-3/id904280696?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Get things done! The award-winning Things app helps you plan your day, manage your projects, and make real progress toward your goals.\n\nBest of all, it’s easy to use. Within the hour, you’ll have everything off your mind and neatly organized—from routine tasks to your biggest life goals—and you can start focusing on what matters today.\n\n“Things offers the best combination of design and functionality of any app we tested, with nearly all the features of other power user applications and a delightful interface that never gets in the way of your work.”\n—Wirecutter, The New York Times\n\n\nKEY FEATURES\n\n• Your To-Dos\nYour basic building block is the almighty To-Do—each a small step toward a great accomplishment. You can add notes, tag it, schedule it, and break it down into smaller steps.\n\n• Your Projects\nCreate a Project for any big goal, then add the to-dos to reach it. Use headings to structure your list as you outline your plan. There’s also a place to jot down your notes, and a deadline to keep you on schedule.\n\n• Your Areas\nCreate an Area for each sphere of your life, such as Work, Family, Finance, and so on. This keeps everything neatly organized, and helps you see the big picture as you set your plans in motion.\n\n• Your Plan\nEverything on your schedule is neatly laid out in the Today and Upcoming lists, which show your to-dos and calendar events. Each morning, see what you planned for Today and decide what you want to do. The rest is down to you :)\n\n\nMORE THINGS TO LOVE\n\nAs you dive deeper, you’ll find Things packed with helpful features. Here are just a few:\n\n• Reminders — set a time and Things will remind you.\n• Repeaters — automatically repeat to-dos on a schedule you set.\n• This Evening — a special place for your evening plans.\n• Calendar integration — see your events and to-dos together.\n• Tags — categorize your to-dos and quickly filter lists.\n• Quick Entry — create to-dos from anywhere, as soon as the thought hits you.\n• Quick Find — instantly locate to-dos, headings, or tags.\n• Type Travel — jump from list to list with your keyboard; just start typing!\n• Mail to Things — forward an email to Things; now it’s a to-do.\n• And much more!\n\n\nMADE FOR MAC\n\nThings is tailored to the Mac with deep system integrations as well. A great example is Quick Entry with Autofill: a shortcut that grabs content from other apps and adds it to Things for you, such as a link to a website or an email you want to get back to.\n\nYou can also enjoy a beautiful dark mode at sunset, connect your calendars, enable a Things widget, use your Mac’s Touch Bar, import from Reminders—Things can do it all! There’s even AppleScript support if you need powerful automation.\n\n\nSTAY PRODUCTIVE ON THE GO\n\nThings has full-featured apps for iPhone, iPad, and Apple Watch as well (sold separately). All your devices sync seamlessly via our free Things Cloud service. It’s great to have everything at your fingertips when you need it!\n\n\nAWARD-WINNING DESIGN\n\nMade in Stuttgart, with two Apple Design Awards to its name, Things is a fine example of German engineering: designed, not only to look fantastic, but to be perfectly functional as well. Every detail is thoughtfully considered, then polished to perfection.\n\n“It’s like the unicorn of productivity tools: deep enough for serious work, surprisingly easy to use, and gorgeous enough to enjoy staring at.”\n—Apple\n\n\nGET THINGS TODAY\n\nWhatever it is you want to accomplish in life, Things can help you get there. Install the app today and see what you can do!\n\nVisit our website now and get a free 15-day trial for your Mac: thingsapp.com\n\nIf you have any questions, please get in touch. We provide professional support and will be glad to help you!", - "currency": "USD", - "artistId": 284971784, - "artistName": "Cultured Code GmbH & Co. KG", - "genres": [ - "Productivity", - "Business" - ], - "price": 49.99, - "bundleId": "com.culturedcode.ThingsMac", - "version": "3.12.6", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c9/5d/af/c95daf17-c405-56f0-90f5-9411828e44d2/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/94/32/3b/94323b37-f81b-7ba8-a280-b951e7e840de/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/82/f1/35/82f1356d-1e68-8f9d-3967-566e256f9265/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/dc/ad/83/dcad839b-7705-e4c0-180a-2f97cb68054d/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/21/50/6a/21506a09-c48c-d25e-dfa5-c0d6aa4cdd9d/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/appest-limited/id434073155?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "TickTick: Things & Tasks To Do", - "trackId": 966085870, - "sellerName": "Appest Limited", - "releaseNotes": "- Bug fixes and improvements.\n\nRecent Updates:\n- Customizable Section in List View.\n- Tag names can be capitalized.\n- The number of Pomos can now be estimated beforehand.\n- Lists under different folders can share the same name.\n- New city themes! Los Angeles and Cairo.\n\nThanks for using TickTick! We'll bring regular updates to give you more pleasant experience with performance and stability.\nWe'll read all reviews in App Store and evaluate your feedbacks carefully. Any issues encountered during the use, you may write to us via Avatar -> Feedback & Suggestions -> Submit feedback, we will get back to you asap.\nTickTick team with love.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-03-04T06:37:31Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-27T01:27:34Z", - "trackCensoredName": "TickTick: Things & Tasks To Do", - "languageCodesISO2A": [ - "EN", - "ZH" - ], - "fileSizeBytes": "24698702", - "sellerUrl": "https://ticktick.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/ticktick-things-tasks-to-do/id966085870?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Design exclusively for macOS, TickTick is your daily must-have to-do & task list to get all things done.\nTickTick can be accessed on more than 10 different platforms including Mac, iPhone, iPad, Apple Watch which enables you to manage tasks on all your devices/Web.\n\nKey features: \n- Add task via shortcut (Command+Shift+A)\n- Instant reminder\n- Set priority levels to tasks\n- Set flexible recurring tasks \n- Create checklists within tasks \n- Sort tasks by order/date/name/priority \n- Sync all your tasks across all devices \n\nTickTick is free but you can also upgrade to Premium account for full access of premium features for $2.99 a month or $27.99 a year through an auto-renewing subscription.\n\nPremium Features: \n- Grid view and Timeline view of calendar\n- Duration\n- Custom Smart List\n- Description for checklist\n- Reminders for sub-tasks\n- More lists and tasks (299 lists, 999 tasks in each list, 199 subtasks in each task)\n- Add at most 5 reminders to each task\n- Share a task list up to 19 members for better task collaboration\n- Upload up to 99 attachments every day\n\nSubscriptions for Premium account will be charged to your credit card through your iTunes account. Your subscription will automatically renew unless cancelled at least 24-hours before the end of the current period. You will not be able to cancel a subscription during the active period. You can manage your subscriptions in the Account Settings after purchase. \n\nHow TickTick makes you productive: \n- Get all things done \n- Never miss a schedule\n- Make work more productive \n- Keep life on track \n\nConnect with us: \nFacebook: https://www.facebook.com/TickTickApp\nTwitter: https://twitter.com/TickTickTeam @TickTickTeam\nHelp Center: https://help.ticktick.com/\n\nPrivacy Policy: https://www.ticktick.com/about/privacy\nTerms of Use: https://www.ticktick.com/about/tos", - "currency": "USD", - "artistId": 434073155, - "artistName": "Appest Limited", - "genres": [ - "Productivity" - ], - "price": 0.00, - "bundleId": "com.TickTick.task.mac", - "version": "3.7.11", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/79/5c/63/795c63aa-698c-1c6c-b6da-e7ebba718d01/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/15/4d/40/154d4071-4a6f-dcd7-0d15-2e495f6f4710/mzm.mvtkjcyn.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e0/31/dc/e031dc74-ce06-afe3-fd8e-8693f6c7c50c/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/fc/8d/23/fc8d2367-725d-11dd-6da9-816a7780a1d9/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "Simple Antnotes", - "trackId": 846599902, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-03-28T12:49:14Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2016-09-24T17:06:52Z", - "trackCensoredName": "Simple Antnotes", - "languageCodesISO2A": [ - "EN", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "1002100", - "sellerUrl": "https://www.antlogic.com/apps/antnotes", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/simple-antnotes/id846599902?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- pin note to desktop to make it stay atop of other windows\n- translucent notes\n- make new notes by dragging text, images and files to the menu bar icon\n- drag images and sounds to note contents\n- automatically hide notes when inactive\n- quick access via menu bar icon\n- configurable global shortcuts to create new note or show/hide all notes\n- integration with services: create new note from any text in any application\n- snap to screen bounds and other notes\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Let us know, or check out our Antnotes application!\n\nVisit our support forums: https://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "ua.com.AntLogic.SimpleAntnotes", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/2b/ce/8f/2bce8ffa-545b-050c-1dd9-2aeef532facd/pr_source.png/406x228bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/fb/36/14/fb36142e-17ba-fdab-90c6-e8f9d3c080ef/pr_source.png/406x228bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/4e/e2/de/4ee2de74-d0ef-010b-19f6-63755aa0175c/pr_source.png/406x228bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/46/8e/bd/468ebdd3-73a9-ec6a-b4da-6931ce887cff/pr_source.png/406x228bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/a4/9c/fa/a49cfa14-69e0-f1cf-3924-6ff878027b2d/pr_source.png/406x228bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c1/94/cc/c194ccb6-c15a-c0a5-47a6-3ddd625fd98d/pr_source.png/406x228bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/3f/23/5e/3f235e16-c049-8ee8-ebdc-3d52f25f2636/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/80/48/1d/80481dff-e404-721c-920e-4688f860cf27/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/58/a2/c9/58a2c970-1bd3-6f4d-1bdc-502f75faaa6a/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/2c/a6/06/2ca606eb-8b40-219a-34c5-626f79b7e593/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/d7/04/c7d70441-51bd-1417-c7bf-a5d2702380e4/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/87/e0/75/87e075fd-a979-6151-5744-56ab76ac8f18/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "Random: Lists & Decision Maker", - "trackId": 1128190780, - "sellerName": "Volodymyr Yahenskyi", - "releaseNotes": "• Fixed crash when adding items to a new list\n• Fixed lists sync on Apple Watch\n\nThanks for using the Random!\nThis release also contains bug fixes and performance improvements.", - "primaryGenreId": 6012, - "primaryGenreName": "Lifestyle", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-07-05T22:00:04Z", - "genreIds": [ - "6012", - "7009", - "6014", - "7004" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-29T19:21:51Z", - "trackCensoredName": "Random: Lists & Decision Maker", - "languageCodesISO2A": [ - "EN", - "RU", - "UK" - ], - "fileSizeBytes": "76392448", - "sellerUrl": "https://yahenskyi.dev/random/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.6104900000000004212097337585873901844024658203125, - "userRatingCountForCurrentVersion": 1525, - "averageUserRating": 4.6104900000000004212097337585873901844024658203125, - "trackViewUrl": "https://apps.apple.com/us/app/random-lists-decision-maker/id1128190780?uo=4", - "trackContentRating": "4+", - "description": "Need a random number? Or can’t you decide what to do? Random is a powerful app that will solve all such problems.\n\nFeatures:\n• Number generator (from a range 0 - 999999999)\n• Letter generator\n• Dice roller (roll up to 4 regular dices in one go)\n• A custom item from a list generator\n• Yes or No \n• Coin flipper\n• Card generator\n• Rock-Paper-Scissors\n• Map Point\n\nGenerate a new random number simply by tapping a ​randomize button or by touching the Apple Watch screen. For those who want a bit of additional exercise, shaking your iOS device will also result in a new random response.\n\nUse Force Touch for setting the minimum or maximum values in your Apple Watch app. Same for the number of dices​, cards, and selection of lists.\n\nRandom Premium subscription benefits:\n• Sync: Get access to your data from all your devices.\n• Themes: Customize the app with various themes and background images.\n• No advertising.\n\nIf you decide to get Random Premium subscription, your purchase will be charged to your iTunes account. 1 month costs $2.99 and 1 year costs $11.99. Active subscriptions will be auto-renewed 24 hours before the expiry date. You can manage subscriptions from Account in iTunes after subscribing, you’ll also be able to cancel the auto-renewing subscription from there at any time. Any unused portion of the free trial period will be forfeited if you purchase a subscription to Random Premium before your trial expires.\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", - "currency": "USD", - "artistId": 961335645, - "artistName": "Volodymyr Yahenskyi", - "genres": [ - "Lifestyle", - "Family", - "Games", - "Board" - ], - "price": 0.00, - "bundleId": "com.yahenskyi.random", - "version": "2.2.10", - "wrapperType": "software", - "userRatingCount": 1525 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/0e/74/bb/0e74bb9a-5ac2-5d5f-516a-5f1c12e95328/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/36/54/f0/3654f064-4013-95e6-2683-c89ab8e51102/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/1a/75/86/1a758637-9db5-007c-595b-b724e9083321/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/any-case-solutions/id1396419026?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Task Planner - To Do List", - "trackId": 1063681909, - "sellerName": "Any Case Solutions, OOO", - "releaseNotes": "We’ve updated the app! In the new version:\n- less bugs;\n- minor changes in the interface;\n- some general improvements.\nYour opinion is important to us! Please, leave your feedback - we will gladly consider all your wishes and suggestions.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-01-07T00:04:36Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-17T23:48:12Z", - "trackCensoredName": "Task Planner - To Do List", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "27930644", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/task-planner-to-do-list/id1063681909?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Plan Your Tasks is a productivity tool that allows you to capture your ideas and duties in one place. \nManage everything you have to do while working with many different tasks!\n\nEasy task management - create, organize, and prioritize tasks;\n- Set notifications;\n- Add comments;\n- Sort tasks by categories;\n- Track due dates.\n\nNew approach to agenda\n- Build-in calendar;\n- Coherent tutorial mode;\n- Magic Trackpad 2 support.\n\nCapture all your flash ideas and duties in the calendar and manage your to dos while working with many tasks more effectively.\n\n\nPrivacy Policy: https://anycasesolutions.com/privacy\nTerms Of Use: https://anycasesolutions.com/tos", - "currency": "USD", - "artistId": 1396419026, - "artistName": "Any Case Solutions", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.iPlanTasksinapp", - "version": "2.1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/69/a0/58/69a0583d-02fd-1d37-cb33-19b80578e9e5/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/09/be/29/09be2981-4d08-a021-423a-29cc212c1b59/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple7/v4/28/5a/f4/285af4d8-37e6-118a-ff28-a4211eeb1122/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/50/4d/52/504d520c-4f8c-b011-d1e0-18addb5700a8/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/f2/6e/07/f26e0760-efb1-0353-3ebd-9fd2803f4b3d/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/realmac-software/id310591643?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Clear – Tasks, Reminders & To-Do Lists", - "trackId": 504544917, - "sellerName": "Realmac Software Limited", - "releaseNotes": "Thanks for using Clear! Just two small enhancements in today’s update:\n\n- We’ve tweaked (increased) the delay before “Click to Clear” appears.\n- We’ve ensured compatibility with OS X El Capitan.\n\nStay productive, and follow @realmacsoftware on Twitter for the latest news!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-11-08T08:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2015-08-19T14:05:32Z", - "trackCensoredName": "Clear – Tasks, Reminders & To-Do Lists", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "13109875", - "sellerUrl": "http://impending.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/clear-tasks-reminders-to-do-lists/id504544917?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Over 2.5 million people de-clutter their lives with Clear, so stop stalling and start organizing your daily routine.\n\nClear is the revolutionary to-do and reminders app that makes you more productive. Just start typing to add to-dos, and once you start organizing your life with Clear you’ll wonder how you ever managed without it.\n\n- Simple gestural design that allows you to focus on your to-dos. Designed for the Magic Trackpad, but works great with a mouse too!\n- Full keyboard navigation. Just start typing to create to-dos.\n- Use separate lists to organize every aspect of your life.\n- iCloud sync built-in so you can be productive everywhere.\n- Set reminders so you’ll never forget important tasks.\n- Personalize your Clear lists with themes and make them your own.\n- Syncs with Clear for iOS (available separately on the App Store).\n\nClear is built by a small team, dedicated to bringing you frequent free feature updates. We’d love to know how we can make you even more productive, so get in touch via the App Store “Support” link, or tweet us @UseClear.\n\nClear for Mac and Clear for iOS are not affiliated with or endorsed by CLEAR Wireless.", - "currency": "USD", - "artistId": 310591643, - "artistName": "Realmac Software", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 9.99, - "bundleId": "com.realmacsoftware.clear.mac", - "version": "1.1.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/cd/bd/44/cdbd44af-06eb-21d6-a793-43dae1077c47/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/e9/8f/17/e98f17c6-787b-b180-6f8d-fb8385ceedd3/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/43/12/a2/4312a25b-f773-9c1a-ddd4-2515d948cc27/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/97/0d/b4/970db444-bbd9-ed77-439e-001bce006e17/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/shenzhen-tomato-software-technology-co-ltd/id966057212?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Focus To-Do: Pomodoro & Tasks", - "trackId": 1258530160, - "sellerName": "Shenzhen Tomato Software Technology Co., Ltd.", - "releaseNotes": "1.Support new languages\n2.Bug fix", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-08-02T03:45:26Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-03T04:38:29Z", - "trackCensoredName": "Focus To-Do: Pomodoro & Tasks", - "languageCodesISO2A": [ - "CS", - "EN", - "FR", - "DE", - "ID", - "IT", - "JA", - "KO", - "PL", - "PT", - "RO", - "RU", - "ZH", - "ES", - "ZH", - "TR", - "VI" - ], - "fileSizeBytes": "12135791", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/focus-to-do-pomodoro-tasks/id1258530160?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Focus To-Do combines Pomodoro Timer with Task Management, it is a science-based app that will motivate you to stay focused and get things done. \n\nIt brings Pomodoro Technique and To Do List into one place, you can capture and organize tasks into your todo lists, start focus timer and focus on work & study, set reminders for important tasks and errands, check the time spent at work. \n\nIt's the ultimate app for managing Tasks, Reminders, Lists, Calendar events, Grocery lists, checklist, helping you focus on work & study and tracking your working hours.\n\nFocus To-Do syncs between your phone and computer, so you can access your lists from anywhere.\n\nHow it works:\n 1. Pick a task you need to accomplish.\n 2. Set a timer for 25 minutes, keep focused and start working.\n 3. When the pomodoro timer rings, take a 5 minute break.\n \nKey Features:\n\n- Pomodoro Timer:Stay focused and get more things done.\n Pause and resume Pomodoro\n Customizable pomodoro/breaks lengths\n Notification before the end of a Pomodoro\n Support for short and long breaks\n Skip a break after the end of a Pomodoro\n Continuous Mode\n \n- Tasks Management: Task Organizer, Schedule Planner, Reminder, Habit Tracker, Time Tracker\n Tasks and projects: Organise your day with Focus To-Do and complete your to do, study, work, homework or housework you need to get done.\n Recurring tasks: Build lasting habits with powerful recurring due dates like \"Every Monday\".\n Reminders: Setting a Reminder ensures you never forget important things ever again, you can set up recurring due dates to remind you each and every time. \n Sub-tasks: Break down your task into smaller, actionable items or add a checklist .\n Task Priority: Highlight your day’s most important To-Do with color-coded priority levels.\n Estimated Pomodoro Number: Estimate the workload or set a goal.\n Note: Record more detailed about the task.\n\n- Report: Detailed statistics of your time distribution, tasks completed.\n Support the calculation of the total time of Focus Time.\n Gantt Chart of the Focus Time.\n Statistics on completed To Do. \n Statistics on time distribution of project.\n Trend chart of the completed To Do and the focus time.\n\n- All-Platform synchronization: View and manage your goals wherever you are for better goal achieving.\n Support seamless synchronization within iPhone、Mac、iPad、Apple Watch and other platforms.\n \n- Various Reminding:\n Focus Timer finished alarm, vibration reminding.\n Various white noise to help you focus on work & study.\n\nContact Us: focustodo@163.com, reply within 24 hours.\nWebsite: http://www.focustodo.cn\nPomodoro ™ and Pomodoro Technique ® are registered trademarks of Francesco Cirillo. This app is not affiliated with Francesco Cirillo.\n\nUsers have been focused on our app for 200 million hours, join us and we help you to be focused and increase your productivity, reduce procrastination and anxiety.", - "currency": "USD", - "artistId": 966057212, - "artistName": "Shenzhen Tomato Software Technology Co., Ltd.", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.macpomodoro", - "version": "6.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/95/33/5f/95335f94-26d3-3567-93ac-77d60ab821dd/pr_source.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/03/53/b9/0353b9b1-ef2a-7ff1-a1b8-4124867af41b/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/ef/63/ce/ef63ce41-371a-2508-b101-fb99e9c7758f/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/19/bb/4319bb4b-5700-0f6b-2c19-7bd386bf186c/pr_source.jpg/552x414bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/5d/51/1a/5d511a30-7fab-fd18-6967-c0caf9674d55/pr_source.jpg/552x414bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/60x60bb.jpg", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/512x512bb.jpg", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/kevin-reutter/id1273424431?uo=4", - "isGameCenterEnabled": true, - "advisories": [], - "features": [ - "gameCenter", - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "13.0", - "trackName": "Planny 3 - Smart To Do List", - "trackId": 1289070327, - "sellerName": "Kevin Reutter", - "releaseNotes": "Stay tuned! Planny 4 ships in a few week and will be a free update with many great features!\n\n• SwiftUI \nNow Planny uses SwiftUI in some parts of the app. SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Over time more and more of the app will be created with SwiftUI to avoid crashes and improve performance. \n\n• Advanced Cursor Support\nWhen using a Trackpad on iPadOS or on the Mac, specific Elements become larger when you come closer to make clicking easier\n\n• Alternative App icons\nChoose the icon color you’d like in settings (iOS for iPhone only)\n\n• New Onboarding Experience\nA new tutorial shows the key features \n\n• New Purchase View\nThe purchase view is now much simpler. Feel free to subscribe :) \n\n• Fixed deadlines on macOS\n• Direct Deadlines now support days and time \n• Fixed issues with overdue tasks \n\nDo you have any wishes for Planny 4? Feel free to submit ideas on the website!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-10-13T19:16:40Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-30T17:37:31Z", - "trackCensoredName": "Planny 3 - Smart To Do List", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "RU", - "ZH", - "ES", - "TR" - ], - "fileSizeBytes": "47687680", - "sellerUrl": "https://www.kevinreutter.de/planny-3/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.3897300000000001318767317570745944976806640625, - "userRatingCountForCurrentVersion": 331, - "averageUserRating": 4.3897300000000001318767317570745944976806640625, - "trackViewUrl": "https://apps.apple.com/us/app/planny-3-smart-to-do-list/id1289070327?uo=4", - "trackContentRating": "4+", - "description": "++ Planny was part of Apples favorite Apps from October ++\n\nPlanny is all new and has been rethought from the ground up.\n\nPlanny is your new friend helping you to be more productive. Planny learned everything important from common to do list apps but combines them with intelligence and gamification. In the morning and during the day Planny intelligently recommends tasks and also reminds you if you tend to forget them. You earn productivity points for adding and completing tasks, and also lose them if you shift tasks or forget them. Users can compare their productivity with friends over the week. \n\nPlanny also features all the important features like deadlines, lists / projects, tagging, location based reminders, notes and attachments, routines and more. \n\nKey features\n• Daily list to focus on today's tasks\n• Assistant for creating a productive daily plan\n• Daily review of the last day\n• Routines to train your habits\n• Deadlines and reminders\n• Smart reminders if you tend to forget your tasks\n• Notes for your tasks\n• Weekly productivity ranking of your contacts\n• Rewards\n• Dark mode\n• Lists\n• Siri support\n• Advanced Apple Watch app\n\nPlanny Premium offers additional features like:\n• Calendar view\n• Teamwork with your friends\n• Add Photos from your library to tasks\n• Add Photos from your camera to tasks\n• Location based reminders\n• iCloud sync\n• iCloud backup \n• FaceID Unlock\n• More than 2 lists\n• Printing\n• Sketches\n• Review your recent days\n• Tagging\n\n+++ Planny Premium - Unlock all features and use Planny on iPhone, iPad and Apple Watch (Mac soon) - And get free feature updates over time! +++ \n\nA Planny Premium subscription unlocks all features. Note that iCloud features require an iCloud-Account. \n\nPlanny offers two auto-renewing subscriptions\n\nPremium 3 Months\n$6,99 / 3 Months (may differ in your country & currency)\n\nPremium Annual\n$19,99 / Year (may differ in your country & currency)\n\nPayment will be charged to iTunes Account at confirmation of purchase\nSubscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period\nAccount will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal\n\nSubscriptions may be managed by the user and auto-renewal may be turned off by going to the user's Account Settings after purchase\n\nWhen your subscription is cancelled and expires, all the features of Planny Pro won't be available any longer. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.\n\nPrivacy policy for Planny: http://kevinreutter.de/privacy\nTerms of use / Conditions: http://kevinreutter.de/privacy", - "currency": "USD", - "artistId": 1273424431, - "artistName": "Kevin Reutter", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.kevinreutter.Callisto", - "version": "3.4.2", - "wrapperType": "software", - "userRatingCount": 331 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/4f/ff/f9/4ffff968-2932-48af-431f-fd1b086026cf/mzl.srudbvwp.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/2a/4f/9f/2a4f9fad-c1a7-fa80-56d7-fea2d3beaa0a/mzl.dcyubghz.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple6/v4/56/3a/a7/563aa771-8288-e21a-cfe8-e28e77ffad83/mzl.lzjpfyct.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/1e/8d/4e/1e8d4eaa-17f7-5295-1f36-975b62164d19/mzl.yufjavxy.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/fb/9b/85/fb9b851d-6ef5-792f-0945-ff2f1a78ce7a/mzl.gycjiioz.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "To-do Lists", - "trackId": 416993121, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "fixed accidentally broken compatibility for Mac OS 10.6-10.7", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-03-01T03:09:22Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2015-04-16T19:07:37Z", - "trackCensoredName": "To-do Lists", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "2095731", - "sellerUrl": "http://www.antlogic.com/apps/todo-lists/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/to-do-lists/id416993121?mt=12&uo=4", - "trackContentRating": "4+", - "description": "To-do Lists provides simple but powerful interface for tasks management.\n\nTo-do Lists features:\n- Quick, one-click tasks addition/removal.\n- Rich-text editing, in-text links support.\n- Seamless iCloud Reminders synchronization.\n- DropBox synchronization between computers and To-do Lists Mobile for iOS\n- Import/export of to-do lists via text files.\n- Printing of to-do lists or mailing them directly from the application.\n- Backup and restore of whole to-do database.\n- Full drag'n'drop support (make new to-do from web link, file, document, e-mail, or any other text by simply dropping them on to-do list).\n- System services support (make new to-do from any text in any application).\n- Rolled-up, translucent or floating to-do lists.\n- Customized background color, text color, font and checkbox appearance.\n- Reminders.\n- Quick-access icon in system menu.\n\nTo-do Lists usage video:\nhttp://youtu.be/5KB-4sYcelo (or http://youtube.com/AntlogicCompany )\n\nFor more information, visit our site at http://www.antlogic.com/\nor Facebook page:\nhttp://facebook.com/AntlogicCompany\n\nIf you have any problems or questions using To-do Lists - visit our support forums at http://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Business" - ], - "price": 4.99, - "bundleId": "ua.com.AntLogic.ToDoLists", - "version": "1.7.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/e9/42/a8e942ec-8eea-03b3-ea37-cc6e2837fb5e/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/62/5c/5f625c38-c559-3b8d-5042-94e241735ef1/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/aa/a1/e7/aaa1e746-e660-2b6e-6833-d751e7879752/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c0/6f/7d/c06f7de4-17b9-7475-8778-22a97c13cdce/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/84/3d/8d/843d8de0-6257-7bf0-6a66-6f3ce41af803/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/0d/28/ad0d28c6-ff1d-c394-266a-fdff0b8e9cc6/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/36/28/a3/3628a3e9-6073-0ce4-17d7-9d9a5f479c64/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a9/ac/b9/a9acb983-76e2-d76d-fbc8-c35388dcee48/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/8f/41/90/8f4190f5-ebb8-370b-c8a6-afb47c56140d/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/09/37/83/09378396-de11-2185-24f4-360be20dbcac/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/the-omni-group/id281731738?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "OmniFocus 3", - "trackId": 1346203938, - "sellerName": "The Omni Group", - "releaseNotes": "OmniFocus 3.9.2 is a minor update focused on bug fixes.\n\n• Omni Automation — OmniFocus now recognizes simple plug-ins that use the .omnifocusjs file extention.\n• First Run — Improved reliability of the first run flow.\n• Notice Bar — Fixed bugs related to the Trial Mode & Free Viewer notice bars.\n\nIf you have any feedback or questions, we’d love to hear from you! The Omni Group offers free tech support; you can email omnifocus@omnigroup.com, call 1–800–315–6664 or 1–206–523–4152, or tweet @OmniFocus.\n\nIf OmniFocus empowers you, we would appreciate an App Store review. Your review will help other people find OmniFocus and make them more productive too.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-09-24T12:28:36Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-27T17:54:49Z", - "trackCensoredName": "OmniFocus 3", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "64931473", - "sellerUrl": "https://www.omnigroup.com/omnifocus/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/omnifocus-3/id1346203938?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Two-week free trial! OmniFocus Standard and Pro are in-app purchases, with discounts for people who bought earlier versions of OmniFocus for Mac through the Mac App Store. Or you can get OmniFocus for iOS, Mac, and web for just one price with the OmniFocus Subscription. Download the app for details.\n\nUse OmniFocus to accomplish more every day. Create projects and tasks, organize them with tags, focus on what you can do right now — and get stuff done.\n\nOmniFocus — now celebrating 10 years as the trusted, gold-standard to-do list app — brings unrivaled power and flexibility to your Mac, making it easy to work the way you want to work.\n\nOmniFocus manages everything in your busy life. Use projects to organize tasks naturally, and then add tags to organize across projects. Easily enter tasks when you’re on the go, and process them when you have time. Tap the Forecast view — which shows both tasks and calendar events — to get a handle on your day. Use the Review perspective to keep your projects and tasks on track.\n\nThen let our free syncing system make sure your data is the same on every Mac. (And on OmniFocus for iOS and Web, available separately.) Because your data is encrypted, it’s safe in the cloud.\n\nSTANDARD FEATURES (VIA IN-APP PURCHASE)\n\n• NEW: Tags add a powerful additional organizing tool. Create tags for people, energy levels, priorities, locations, and more.\n• NEW: The Forecast view shows your tasks and calendar events in order, so you can better see what’s coming up in your day.\n• NEW: Enhanced repeating tasks are easier than ever to set up — and they work with real-world examples such as the first weekday of the month.\n• NEW: The Modern, fresh-but-familiar design helps you focus on your content.\n• Inbox is where you quickly add tasks — save them when you think of them, and organize them later.\n• Syncing supports end-to-end encryption so that your data is safe wherever it’s stored, on our server or yours.\n• Notes can be attached to your tasks, so you have all the information you need.\n• Attachments — graphics, video, audio, whatever you want — add richness to your tasks.\n• View Options let you customize each perspective by deciding what it should show and how it should filter your tasks.\n• The Review perspective takes you through your projects and tasks — so you stay on track.\n• OmniFocus Mail Drop adds tasks via email and works with services like IFTTT and Zapier (if you’re using our free syncing server).\n• The Today Widget shows you your most important items — you don’t even have to switch to the app to know what’s up.\n• Support for TaskPaper Text and omnifocus:///add and /paste lets you automate using URLs.\n\nPro features make OmniFocus even more powerful:\n\nPRO FEATURES (VIA IN-APP PURCHASE)\n\n• Custom perspectives help you create new ways to see your data by filtering and grouping projects and tags. NEW: The filtering rules are simpler to use while being more powerful than ever, letting you combine rules with “all,” “any,” and “none.” You can also choose any image to use as your custom perspective’s icon, and a custom tint color to go with it.\n• NEW: Today’s Forecast can include items with a specific tag, and you can reorder those tasks however you choose, so you can plan your day better.\n• The customizable sidebar lets you organize your perspectives the way you want to, for super-fast access.\n• The Today Widget shows a perspective of your choice in Notification Center.\n• AppleScript support opens up a world of automation, using Apple’s Mac scripting language.\n\nDownload OmniFocus right now and start your free trial! The app includes a manual, and there’s plenty more documentation on the website.\n\nSUPPORT\n\nIf you have feedback or questions, our Support Humans would love to hear from you! Send email to omnifocus@omnigroup.com, call us at at 1-800-315-6664 or +1-206-523-4152, or reach us on Twitter at @omnifocus.\n\n\nSubscription Terms of Service: https://www.omnigroup.com/legal", - "currency": "USD", - "artistId": 281731738, - "artistName": "The Omni Group", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.omnigroup.OmniFocus3.MacAppStore", - "version": "3.9.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/72/1d/1a721d98-fbc4-ed9e-2aae-ef9d5b538693/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a2/b1/10/a2b110fd-aa90-286a-658b-2abd85bd1c68/mzl.menowpkq.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/26/f3/32/26f3322f-8ef9-6171-2864-715f571300e6/mzl.qxibkqwt.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a4/1b/12/a41b12dc-6e1d-74ff-7ad3-1d6888c31462/mzl.fdlcjqnh.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/83/83/24/83832412-85d1-78ff-a9ab-86a37b31121d/mzl.ateekpxr.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e6/7c/c7/e67cc703-d274-a1fc-88af-b5a8ce9cbfd8/mzl.grtjmgef.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/18/a8/f2/18a8f211-61b5-7b7e-b343-784b260de31d/mzl.ulsghntx.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/masterbuilders/id896347016?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "Focus - Time Management", - "trackId": 777233759, - "sellerName": "Masterbuilders", - "releaseNotes": "Subscription status is now properly unlocked on all devices.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2013-12-19T19:16:50Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-02-12T20:37:50Z", - "trackCensoredName": "Focus - Time Management", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "JA", - "ZH", - "ES" - ], - "fileSizeBytes": "24637530", - "sellerUrl": "https://www.focusapp.io", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/focus-time-management/id777233759?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Meet Focus: the best time manager for iPhone, iPad, Apple Watch and Mac. Focus is the most elegant and professional way to get more wore done, working in highly efficient work sessions, one task at a time.\n\n“[…] a tool that can genuinely make people more productive\" – MacStories.net\n\n“[…] a must-have for anyone who finds themselves easily getting distracted or forgetting to take occasional breaks.\" – iDownloadBlog.com\n\n======================\nFEATURES\n======================\n\nFOCUS SESSIONS\nFocus Sessions are a highly efficient way to work. Focus for 25 minutes, then take a short break to relax your mind. After four sessions, take a 15 to 20 minute break. This method maximizes energy, stimulates creativity and promotes a sense of achievement.\n\nTASK MANAGER\nFocus includes a lightweight task manager that lets you organize the things you want to work on intuitively. By working on one task at a time, you won’t be distracted and can focus all your attention towards completing that goal. That way you’ll be perfectly organized on your path to success.\n\nIN-DEPTH STATISTICS\nCheck what you’ve already done! Focus keeps track of your work and offers in-depth and motivating statistics. See your daily, weekly and monthly activity so you don’t lose sight of the big picture. \n\nFOCUS EVERYWHERE\nSeamlessly use Focus on your Mac, iPad, iPhone, and Apple Watch. Sync across your devices using iCloud; use Handoff to pick up your current work on another device and get up-to-the-second data with iCloud Push. You can also use the Today widget to quickly glance at your progress, import tasks using the handy Action extension, and more.\n\nFOCUS & APPLE WATCH: A PERFECT FIT\nUsing Focus on your wrist is a natural fit. The independent Apple Watch app is made for for easy and lightweight interactions that lets you control sessions and track your progress throughout the day. With the Focus complication, you can customize your watch face to see your current progress at a glance.\n\nBEAUTIFUL INTERFACE\nThe name says it all: Focus draws your attention to the most important things. It’s designed to be unobtrusive, accessible and easy-to-use. You’ll intuitively master its collection of features just by using them.\n\n======================\nSUBSCRIPTION PRICING\n======================\n\nFocus offers two subscription options: \nFocus Monthly at $4.99/ month \nFocus Yearly at $39.99/ year\n\nThe subscription unlocks all features on all devices (Mac, iPhone, iPad and Apple Watch).\n\nTRY IT FREE \nFocus Monthly comes with a 3-day free trial period, Focus Yearly with a 7-day free trial period. If you cancel before the end of the trial, you will not be charged for the subscription.\n\nSUBSCRIPTION TERMS\nPayment will be charged to your Apple ID account at the confirmation of purchase or after the free trial period if offered. \n\nYou subscription will automatically renew unless it is canceled at least 24 hours before the end of the current period. Your account will be charged 24 hours prior to the end of the current period. \n\nYou can manage and cancel your subscriptions by going to your account settings in the App Store after purchase. Any unused portion of a free trial will be forfeited when you purchase a subscription\n\n======================\nCONTACT\n======================\n\nIf you have any questions or ideas, please write us at hello@masterbuilders.io\n\nTwitter: @focusappio\nhttps://www.masterbuilders.io\n\n\n\nPrivacy Policy: https://www.masterbuilders.io/privacy\nTerms of Service: https://www.masterbuilders.io/terms", - "currency": "USD", - "artistId": 896347016, - "artistName": "Masterbuilders", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.malteundjan.focus-osx", - "version": "6.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/35/03/5a/35035a62-e2da-2f4b-6ece-63475bd7cd02/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/b5/ac/1f/b5ac1fe2-431d-e45d-63e0-57ddbfbd525f/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/6c/18/ee/6c18eeff-ca66-2b01-82f3-f81576336ab7/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/niklas-behrens/id969210609?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "1Focus: Website & App Blocker", - "trackId": 969210610, - "sellerName": "Niklas Behrens", - "releaseNotes": "- Allows updating 1Focus while blocking is active\n- Fixed toolbar overflow on macOS High Sierra\n- Improved status item width\n- Fixed quick start menu starting wrong task\n- Other bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-03-15T05:54:46Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-18T23:51:25Z", - "trackCensoredName": "1Focus: Website & App Blocker", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "JA", - "KO", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "8338821", - "sellerUrl": "https://onefocusapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/1focus-website-app-blocker/id969210610?mt=12&uo=4", - "trackContentRating": "4+", - "description": "1Focus creates an oasis for focused work by disabling access to specific websites and apps. Use it to schedule a bit of automated self-restraint when you find yourself clicking away from what really needs to get done. Ideal for students, freelancers and writers.\n\n\"If you find yourself on Facebook or checking your email every five minutes, you need 1Focus.\" – Pagoda Technologies\n\n\"1Focus is one of the best apps for tuning out the diversions that are most distracting for you.\" – Tyler Horvath, CEO of Tyton Media\n\n\nFREE FEATURES\n\n• Block websites in Google Chrome, Safari, Opera, Microsoft Edge and Brave\n• Block apps (e.g. email, games)\n• Block internet access by blocking web browsers\n• You cannot cancel active blocks once you close the 1Focus window\n• Create your own task presets (up to 2)\n• Dark Mode\n\n\n1FOCUS PRO\n\n• Schedule recurring block events (e.g. Mon - Fri)\n• Work break timer\n• Unlimited task presets\n• Block all websites/apps except specific ones\n• Suspend blocking for a limited time\n• Block URL keywords (e.g. *gaming*)\n• Block popular websites by category (e.g. Social Media)\n\nTry it free for 14 days. $1.99/month or $9.99/year after.\n\nPrices may vary by location. Subscriptions are charged to your iTunes Account. They automatically renew unless you cancel them in your Account Settings at least 24 hours before the end of the current period. Your Account is charged for renewal within 24 hours prior to the end of the current period. Terms of use: https://onefocusapp.com/terms\n\n\nCUSTOMER SUPPORT\n\nDo you have any questions or suggestions?\nonefocusapp.com/support", - "currency": "USD", - "artistId": 969210609, - "artistName": "Niklas Behrens", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.onefocusapp.OneFocus", - "version": "3.4.4", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/6c/af/5f6cafb3-8f9d-5165-285e-b3da4d640d58/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/9e/15/3a/9e153a86-f869-6972-5144-9c45c699678a/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/52/bd/4b/52bd4b45-da16-b678-19e1-76ff109252b4/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/ec/24/3f/ec243f38-e6a5-88ed-ca7e-e503042e2888/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/a8/a8/a8a8a8be-4d88-e2aa-8683-fcfc2c6c8e63/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/nozbe-com/id303308791?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Nozbe: Productive team", - "trackId": 508957583, - "sellerName": "apivision.com", - "releaseNotes": "• Fixed a problem with stats for completed tasks", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-05-23T07:00:00Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-02-03T09:32:43Z", - "trackCensoredName": "Nozbe: Productive team", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PL", - "PT", - "RU", - "ZH", - "ES", - "ZH", - "TR" - ], - "fileSizeBytes": "9348964", - "sellerUrl": "https://nozbe.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/nozbe-productive-team/id508957583?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Nozbe – Collaborate with your team efficiently and get everything done!\n\nKEY NOZBE FEATURES: FOR TEAMS AND INDIVIDUALS\n\nAn invaluable project management app for teams\n\n• Collaborate to get your work done – quickly create and share projects with your team \n• Communicate smoothly and ditch chaotic e-mails – use task comments to exchange information with your colleagues and customers, and have all the info required in one safe system\n• Stay up-to-date – set up reminders and productivity reports\n• Move your projects forward – delegate tasks, and use deadlines and handy integrations\n• Organize your team's work – using a flat structure, task categories and project labels\n• Access your team projects from anywhere – install Nozbe on your desktop and mobile devices\n• Make sure your data is safe – all Nozbe connections are secure and encrypted using SSL\n• Integrations – Dropbox, Box, One Drive, Google Drive, Evernote, Google Calendar\n\nPersonal to-do list and task manager\n\n• Easily add 'to-do's and tasks on the go – with Siri, through e-mail and more\n• Access your tasks from anywhere – install Nozbe on all your devices, including Apple Watch\n• Manage your priorities – add deadlines, time needed and categories\n• Run your projects efficiently – keep all tasks, comments and files in one place, manage projects, and use labels\n• Never miss a thing thanks to reminders and calendar integrations\n• Use templates and repeat tasks to automate things and save your time\n• Work offline and have your data synchronized once you're back online\n\nJoin over 500,000 professionals and their teams from all over the world in using Nozbe and boost your efficiency. Use Nozbe's flexibility to create your own productivity system. Get your projects done on time and stay organized – alone and with your team.\n\nNOZBE Subscriptions (monthly or yearly):\n\n• NOZBE FREE – The full version of the app with up to 5 active projects and max. 100MB of data. Available after 30 days of the Nozbe Trial. Exclusively for single-user accounts.\n• NOZBE SOLO/DUO - For busy professionals, unlimited projects, shared projects, unlimited storage.\n• NOZBE SMALL BUSINESS - For small growing teams, unlimited projects, shared projects, unlimited storage.\n• NOZBE BUSINESS - For growing teams and businesses, additional shared projects features, more comprehensive productivity reports, dedicated premium support.\n\nYour payment will be charged to your iTunes Account once you confirm your purchase. Your iTunes account will be charged again when your subscription automatically, renews at the end of your current subscription period unless auto-renew is turned off at least 24 hours prior to the end of the current period. You can manage or turn off auto-renew in your Apple ID Account Settings at any time after purchase.\n\nTerms and Privacy policy: nozbe.com/terms", - "currency": "USD", - "artistId": 303308791, - "artistName": "Nozbe.com", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.nozbe.mac", - "version": "3.13", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/cd/0a/55/cd0a552e-00e7-a1ef-b9be-8cf55a1f22bd/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/da/df/a2/dadfa2c7-67de-d41b-c862-c860be7ec80b/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/89/89/3b/89893baf-cb17-bc37-da1b-4acaac52313e/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/ac/71/9f/ac719fb2-2e24-c746-3e3c-e1990ae45430/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/cf/c9/6f/cfc96f41-5169-ba4e-7c91-d306a7affdbf/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ec/cf/bf/eccfbf43-32ed-9df3-5f59-251a326d6522/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ind-ie/id1042780453?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "Better Blocker", - "trackId": 1121192229, - "sellerName": "Article 12", - "releaseNotes": "# Good news!\n\nIn the last update, we informed you that that version would be the last update to Better under the Ind.ie account as we are moving to Small Technology Foundation. Since then, Apple has stepped in and answered our call for help so we can keep updating the app from our current account.\n\n## What this means for you:\n\n - Everything will continue as before, you don’t need to do anything.\n - This app will continue to get updates.\n - At some point, you will see our organisation name change from Ind.ie (Article 12) to Small Technology Foundation.\n\nLaura and I would like to take this opportunity to thank all of you who expressed your support during this period and donated to our not for profit to help us during this difficult time.", - "primaryGenreId": 6002, - "primaryGenreName": "Utilities", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-09-22T02:55:17Z", - "genreIds": [ - "6002", - "6007" - ], - "formattedPrice": "$1.99", - "currentVersionReleaseDate": "2020-01-20T21:31:19Z", - "trackCensoredName": "Better Blocker", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "10318394", - "sellerUrl": "https://better.fyi", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/better-blocker/id1121192229?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Better is a privacy tool for Safari that protects you from trackers and privacy-eroding ads on the web.\n\nMake your web experience safer, lighter, and faster in Safari on Mac. \n\nHow is Better different?\n• Blocks tracking scripts, tracking pixels, and behavioural advertising.\n• Uses our unique, hand-curated, and open tracker database based on our own web crawls. Browse the encyclopaedia-like tracker entries on our web site if you fancy learning more. \n• We identify and block the most prevalent trackers. This makes Better’s block list lightweight and effective.\n• We base our blocking decisions on the principles of the Ethical Design Manifesto (https://ind.ie/ethical-design). \n• Developed and curated by Ind.ie – that’s Laura and Aral – a tiny two-person-and-one-husky not-for-profit striving for social justice in the digital age.\n• Open, transparent, and free as in freedom.\n\nOther features:\n• Improves the readability of popular web sites. \n• Spotlights the worst offenders… Did you know that the worst site we found on the web had 172 trackers? Better blocks them all and makes that site 4× lighter and 15.8× faster!\n• Blocks blocker blockers (try saying that quickly a few times!) Whenever we can, we try to protect you from sites that try to put you at risk by forcing you to turn off your privacy tools.\n• Your personal Do Not Block list enables you to turn Better off for sites that don’t play well with Better. Easily contact us about those sites so we can fix the experience for everyone.\n\n5 star App Store reviews from around the world:\n• “Best blocker I've tried yet: Clear and straight forward business model plus actually works. Feel safe blocking trackers and knowing the developers won't sell you out. Can't beat that.” (USA)\n• “This app does everything it promises to do and sets a new gold standard in content blockers.” (Belgium)\n• “This piece of software is just THE most essential thing to stay sane on today’s world wide web.” (Germany)\n• “Thanks for such an incredibly simple app to make the web just that bit better!” (UK)\n• “I love better, I use it on my phone and I’m very happy to see it on the Mac!” (Germany)\n• “Does exactly what it advertises, constantly updated, and I really support the mission of this company.” (USA)\n• “Fantastic blocker with a rational, ethical approach to privacy and monetization in this digital age.” (USA)\n• “Honest privacy: Thanks for defending the public against surveillance capitalism!” (USA)\n• “Buying this not only benefits you, it actively supports the ethos of internet privacy.” (USA)\n• “Fantastic: The best adblocker I have tried.” (USA)\n• “Best Adblocker out there: Unobtrusive. Works fast & reliable” (Germany)\n• “It just works!” (Germany)\n• “A fantastic approach to ad blocking & preservation of user privacy” (Australia)\n• “Thanks to these two brave people who made this app. It gives me at least some peace of mind about the nasty surveillance practises of the big tech companies.” (Netherlands)\n• “Better web: Fixes so many sites without you even noticing it.” (Finland)\n• “Best ad & tracking blocker-type app I've used on the appstore!” (Norway)\n• “Great app: Does exactly as described, works it’s magic in the background whilst I’m browsing the web.” (Sweden)\n• “Speed and privacy: Easy to install and does exactly what it’s supposed to: frees your browsing of megabytes of privacy-invading tracking. Even if you don’t mind unknown third parties building up and selling profiles of your browsing behaviour (and everything that can be inferred form it), the speed increases of both download and rendering by removing those trackers really surprised me. It’s simply made browsing the web far quicker and is worth the money for that alone.” (UK)\n• “It's good to see such nice people stand up to tracking and malvertising. This app does everything it promises to do and sets a new gold standard in content blockers. Spend a few bucks to take back the web from disrespectful business models. You won't regret it.” (Belgium)", - "currency": "USD", - "artistId": 1042780453, - "artistName": "Ind.ie", - "genres": [ - "Utilities", - "Productivity" - ], - "price": 1.99, - "bundleId": "better.fyi.mac", - "version": "2020.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/e4/4a/75/e44a75a7-624c-d60b-0e40-3f0e655da61a/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/34/9e/03/349e0304-90fc-3838-9cec-6c706efc92ea/mzl.zwihxdxb.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/1f/23/2e/1f232eaf-95d1-e5af-1f9e-2d7c4061bb55/mzl.qvfjfjne.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/8d/6c/0b/8d6c0b27-fc9e-c290-1329-7e2049f34412/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/ce/62/8f/ce628f72-a55d-cbca-5544-a327e12762be/mzl.pqwcptst.tif/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/delullo-software-llc/id409729333?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "All Things Money Lite", - "trackId": 892274130, - "sellerName": "DeLullo Software, LLC", - "releaseNotes": "The All Things Money Lite 1.4.0 update improves functionality and stability, and is recommended for all users.\n\nNew features include:\n\n- Enabled QFX files with transactions to be imported.\n- When importing a file, if transaction categories are not specified then they will be automatically set to the last category used by the payee.\n- Added stock market technical indicators for Aroon, Money Flow, Relative Strength, and Fast Stochastic.\n- Added the ability to simultaneously view weekly and daily stock technicals.\n\nStability improvements include:\n\n- Added support for importing files with international character encodings.\n- Fixed two bugs that affected printing functionality.\n- Fixed QIF exports, which incorrectly had a percent symbol in the date field.\n- Fixed international currency formatting of the payment and deposit columns.\n\nFor detailed release notes, please visit: http://www.delullosoftware.com/apps/AllThingsMoney/ReleaseNotes.pdf", - "primaryGenreId": 6015, - "primaryGenreName": "Finance", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-07-07T07:00:00Z", - "genreIds": [ - "6015", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-09-15T12:55:11Z", - "trackCensoredName": "All Things Money Lite", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "1358452", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-money-lite/id892274130?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Money (ATM) was created with the mindset that good finance software doesn’t need to be expensive, to contain advertisements, or to log into your bank account. It should be comprehensive and provide interconnected services. It should also be efficient by supporting multiple input formats, auto-completing certain fields, and automatically logging recurring transactions. ATM delivers on these goals and more.\n\nIf you are curious, please give it a try. Download All Things Money Lite for free to determine whether ATM is a good fit for you. When you want to upgrade, use the export and import menu options to transfer data between apps.\n\nATM capabilities include the following.\n\nAccount Dashboard\n- The innovative layout gives perspective to all of your liquid assets.\n- Track bank, credit card, stock market, and U.S. savings bond accounts.\n- Calculate net worth.\n- Sort transactions.\n- Split transactions.\n- Add memos to transactions.\n- Import bank, credit card, and stock market transactions from QIF and CSV files.\n- Import bank and credit card transactions from OFX files. (At this time, stock market transactions are not supported via OFX imports.)\n- Import U.S. savings bonds from treasurydirect.gov HTML files.\n- ATM auto-fills payee names and categories.\n- The bank-account paradigm enables you to earmark money.\n\nBill Planner\n- Track payments and deposits.\n- Track automatic stock market trades that are typical of 401k-type accounts.\n- Elect to automatically move up bill dates to avoid weekends and U.S. holidays. \n- ATM supports the following payment intervals: daily, weekly, bi-weekly, semi-monthly, monthly, bi-monthly, quarterly, once every four months, semi-annually, annually, bi-annually.\n\nStay on Budget\n- Create a budget based on transactions from the prior year.\n- Update budget goals based on your expected bills and deposits.\n- Update actuals from account transactions.\n- ATM supports annual, monthly, and custom-period budgets.\n- ATM provides color-coded alerts to track spending excess.\n\nPlan for Retirement\n- Seed the retirement calculator with your account information.\n- Calculate how much money you will have when you retire.\n- Calculate the annuity that your retirement will generate.\n- ATM also includes a general-purpose, compound-interest calculator that has six unique equations and two combination options for expediency. This calculator enables reverse computations; it calculates how much money you need to retire based on your desired annuity, interest rate, and life expectancy.\n\nMonitor the Stock Market\n- Create a watch list that saves stock symbols and time intervals.\n- Display a stock’s price and volume alongside technical indicators like Bollinger Bands and Moving Average Convergence Divergence (MACD).\n- ATM supports daily and weekly stock charts.\n\nMortgage Analysis\n- Create an amortization table and track your mortgage balance as you pay down principal. \n- Compare the costs of buying and renting.\n\nInventory Tracking\n- Keep track of your collections.\n\nCreate Reports\n- Generate plots of net worth and stock prices.\n\nLite Version Restrictions\n- One bank\n- Two accounts\n- Three bills\n- Two stock watch list entries\n- Two inventory categories with three items each\n- Two retirement calculations per session\n- One compound-interest calculation per session\n- One amortization calculation per session\n\nMinimum requirements: screen resolution of 1280 x 800 or higher.", - "currency": "USD", - "artistId": 409729333, - "artistName": "DeLullo Software, LLC", - "genres": [ - "Finance", - "Business" - ], - "price": 0.00, - "bundleId": "com.delullosoftware.AllThingsMoneyLite", - "version": "1.4.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/36/fe/ff/36feffbc-a07b-e61e-f0e5-88dcc4455871/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/c6/85/09/c68509b2-c2c8-3000-bf85-4ead056b26f3/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/18/42/aa/1842aab5-0500-b08b-b9a5-fc364f83fbdb/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/de/b9/99/deb99962-f1d0-a7ad-0fc8-ef4bf906515b/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/41/70/7d/41707d88-8ba1-5a28-1f2f-0f2e43a73706/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/be/a3/a2/bea3a233-d82f-34bf-b0cd-38f262b04939/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e5/41/b4/e541b49d-06ed-9ec6-1544-3df88c8dc340/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/8f/08/49/8f0849f4-7d20-567f-47e6-ef1bfb901619/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/7d/74/8a/7d748af9-50fa-e009-39a8-b5eb7774b2be/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/tinybop-inc/id682046582?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15.0", - "trackName": "Things That Go Bump", - "trackId": 1472954003, - "sellerName": "Tinybop Inc.", - "releaseNotes": "* BOOM *, this is a BIG update. The house spawns a game room, complete with video games you can ENTER INTO. It's fun and a little bit weird! Try it! \n»-(¯`·.·´¯)->", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-10-18T07:00:00Z", - "genreIds": [ - "6014", - "7001", - "7009" - ], - "currentVersionReleaseDate": "2020-03-18T17:39:23Z", - "trackCensoredName": "Things That Go Bump", - "languageCodesISO2A": [ - "EN" - ], - "sellerUrl": "http://tinybop.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-that-go-bump/id1472954003?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Have you ever heard something go bump in the night? \nPerhaps you’ve caught wind of a spirit or sprite. \nWhen the house is asleep,\nand there’s dark all around, \nspirits from objects awake and abound. \n\nThe spirits are crafty and like to cause trouble. \nThey're called yōkai and together, they double. \nMixing and mashing, they join to fight. \nCan you help them conquer this mysterious night? \n\nPlay with one person, two, three or four. \nFirst you’ll need to escape the dark junk drawer. \n\n. . . . . . . . . . . . . . . . . . . .\nIn Things That go Bump, familiar objects and rooms come to life every night, and nothing looks quite as does in the day. Create your creature, and battle your friends, but beware the house spirits! They can destroy and they can give life. Battle, create, and make your way through the rooms of the house, and slowly you will unravel the secret of Things that Go Bump. \n\nFeatures:\n * Spirits wake up objects and create yōkai (spirit creatures)\n * Combine everyday objects like umbrellas, staplers, cheese graters and more to create everchanging characters \n * Connect to other players via Game Center and face-off against other spirit creatures and house spirits\n * Add or swap objects to give your spirit creature new skills\n * Gain energy by making mischief, defeating other yōkai, and conquering the house spirits\n * Advance through the house (new rooms will be added throughout the year)\n * Test your curiosity and creativity with new challenges in every room\n * Play with 1-4 players across iPads, iPhones, iPods, AppleTVs and Macs\n * Fun and challenging for the whole family\n * Intuitive, safe, hilarious kid-friendly design\n * New levels introduced roughly every 2 months\n * Original artwork by Adrian Fernandez\n * Original sound design\n\nTinybop, Inc. is a Brooklyn-based studio of designers, engineers, and artists. We make toys for tomorrow. We’re all over the internet.\n\n Visit us: www.tinybop.com\n Follow us: twitter.com/tinybop\n Like us: facebook.com/tinybop\n Peek behind the scenes: instagram.com/tinybop\n\nWe love hearing your stories! If you have ideas, or something isn’t working as you expect it to, please contact us: support@tinybop.com.\n\nPsst! It's not Tiny Bop, or Tiny Bob, or Tiny Pop. It's Tinybop. :)", - "currency": "USD", - "artistId": 682046582, - "artistName": "Tinybop Inc.", - "genres": [ - "Games", - "Action", - "Family" - ], - "bundleId": "uikitformac.com.tinybop.thingamabops", - "version": "1.3.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/8d/13/b2/8d13b22b-e96a-a0dd-219c-892ec2ba6f60/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/bf/53/6a/bf536a44-dd1e-43c3-89d0-5fc1928001e8/mzl.lphzucrn.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/7e/09/9c/7e099c9b-f7c8-3079-f1a0-a0fac192807b/mzl.fpmmmnpy.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/16/a9/6c/16a96c08-ad0d-fd57-4b66-baeb37cd02a2/mzl.jaithmaa.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0e/13/90/0e139010-77ef-9d9e-2171-1d146d4467ad/mzl.kquqgdzh.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/36/07/3f/36073f03-7e1e-208d-35c4-9b1e3f57029a/mzl.skrcrjvg.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/adolfo-vera-blasco/id898601649?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "All Things Done", - "trackId": 1062002496, - "sellerName": "Adolfo Vera Blasco", - "releaseNotes": "New year, new name\n\nSay hello to All Thing Done", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-12-17T01:13:07Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2019-03-25T05:37:45Z", - "trackCensoredName": "All Things Done", - "languageCodesISO2A": [ - "EN", - "ZH", - "ES" - ], - "fileSizeBytes": "8243793", - "sellerUrl": "http://tomates.desappstre.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-done/id1062002496?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Done helps you to improve your productivity using one of the most effective management method in personal or pair working environments.\n\nThe app is highly configurable in aspects like time for breaks or tasks, notifications, task series, all of it in a beautiful and detailed interface.\n\nWe say Hi! to Touch Bar\nProud to announce All Things Done adds support to Touch Bar on the new MacBook Pro.\n\nInternational.\nSince version 5 we speak english and Español también.\n\nInside All Things Done you will find...\n+ iCloud sync with all your devices\n+ Theme support\n+ Reports to compare your productivity\n+ Beautiful graphs to check your progress in a blink of an eye\n+ Customize task time\n+ Customize short and long break time\n+ Choose to play a sound or not when a break or a task has finished\n+ Set how many tasks are a Work Serie\n+ The task timer can be paused/resumed/reseted\n+ Session and Goal counters can be reseted", - "currency": "USD", - "artistId": 898601649, - "artistName": "Adolfo Vera Blasco", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "com.desappstre.Pomodoro", - "version": "10", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/b3/3f/0eb33fce-c85e-01ca-93ad-951ad273c1d9/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/6e/6e/84/6e6e8457-d30d-8055-cd0b-3064bbdba0cf/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/78/e7/e4/78e7e488-2a0a-8a13-75cd-6dac68e52e3e/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e2/29/99/e2299950-e368-2b21-3de9-6ef021488150/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d7/50/0f/d7500f21-56b5-9884-3f4b-018b17bddc85/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/44/72/40/44724058-efac-9d28-950c-e546dfc772be/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/2c/c0/05/2cc0056c-97cf-3cf8-f596-0ca192744dd4/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/96/9c/3b/969c3b88-3180-6daf-7766-9c3b626a8ca8/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/41/7b/68/417b68f8-e692-dd05-e8e4-032aa225d43a/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/eduard-metzger/id1137020763?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "NotePlan 2", - "trackId": 1137020764, - "sellerName": "Eduard Metzger", - "releaseNotes": "- Fixed sidebar crash.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-10-18T11:04:27Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-22T23:44:00Z", - "trackCensoredName": "NotePlan 2", - "languageCodesISO2A": [ - "EN", - "DE" - ], - "fileSizeBytes": "15096159", - "sellerUrl": "http://noteplan.co", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/noteplan-2/id1137020764?mt=12&uo=4", - "trackContentRating": "4+", - "description": "NotePlan is the All in One Digital Planner to help you organize your life, your work, and everything in between. You can manage your calendar, notes, and tasks all in one intelligent interface on both Mac and iOS (sold separately). Try NotePlan for free for 14 days, no payment info required. \n\nINCREDIBLE FEATURES\n\n- Integrates with your iCloud Calendars (supporting Google Calendar, Exchange, Yahoo, and more.)\n- Dynamic tasks allow you to schedule tasks, see upcoming, overdue, and unscheduled.\n- Syncs with your Apple Reminders, use Siri on the go to create reminders in NotePlan.\n- Powerful calendar views to see events, reminders, and tasks in one place.\n- Organize thoughts in Nested Tags\n- Easily Add (and remove) image attachments to your notes.\n- Syncs your notes via iCloud Drive to all your Apple devices (this is your data, not ours!).\n- Global search across calendar events, reminders, tasks, and notes.\n- Multiple dark and light themes to suit your style. \n- Works offline, no internet required (sync does require connection).\n- Built from day one to support Markdown editing within notes!\n\n\nQUOTES FROM OUR AMAZING CUSTOMERS\n\n- “To keep up with all the stuff I have to do, I have tried most to-do apps. The simplicity of NotePlan’s approach makes it the winner in this category for me.”\n- “The new design and layout is awesome. I’m a big fan of NotePlan and this makes information even more accessible and logically laid out.”\n- “This is a fantastic app that solves a lot of pain points for me. You can start small and gather complexity at your own pace.”\n- “Finally, an app that does exactly what I want.”\n\n\nDownload and try today for 14 days, no signup required, no payment info required. NotePlan is a one time purchase (no subscription) if you like the app!\n\nHERE IS THE BACKSTORY\n\nIf you’re like us, work life and personal life is starting to merge. Managing meetings, notes, next steps, projects, tasks, reminders in multiple apps, sites, or tools - can be pretty hard - but it doesn’t have to be! With NotePlan for the Mac and iOS, you can manage your calendar, notes, and tasks in one place. Instead of constantly context switching, you can stay focused and accomplish what you set your mind to.\n\nWe recommend starting by getting everything out of your head and captured in one place - such as a “Planning” note. After gathering your thoughts, NotePlan allows you to seamlessly translate items into tasks. Set dates to these tasks, such as “tomorrow” or a specific day of the week. \n\nBy integrating with Apple’s Calendar and Reminders, NotePlan can show you your meetings and events next to the tasks you set. Weekly or Monthly calendar view gives you a quick snapshot of what is scheduled, and where you may have gaps in your day so you can quickly schedule additional things to accomplish. \n \nReviewing and managing open tasks and deadlines is incredibly easy. On top of that, notes are perfect for brainstorming, saving links, taking meeting notes, or storing reference materials. NotePlan is highly-customizable too with handsome light and dark themes as well as tagging for organizing and wiki-style linking between notes.\n\n\nQUESTIONS?\n\nIf you have any questions, suggestions or problems, please contact us. We provide fast and professional support: hello@noteplan.co", - "currency": "USD", - "artistId": 1137020763, - "artistName": "Eduard Metzger", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "co.noteplan.NotePlan", - "version": "2.4.6", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/9f/ec/16/9fec16c9-a8e4-109f-68da-bcdbdbac0f02/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/18/81/9f/18819f96-b13a-5194-db9f-998ed8831875/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/adam-mathes/id453295387?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "DoOneThing - Single Task Manager", - "trackId": 464910442, - "sellerName": "Adam Mathes", - "releaseNotes": "Bug fixes, dark mode support, icon change", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-09-16T18:48:52Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-07-22T03:11:21Z", - "trackCensoredName": "DoOneThing - Single Task Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "834152", - "sellerUrl": "http://mermodynamics.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doonething-single-task-manager/id464910442?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Instead of being a source of constant distraction, your computer should help you accomplish what's most important to you every day.\n\nIt's easy to spend hours sitting in front of a computer immersed in doing little things, while avoiding the most important one.\n\nDoOneThing won't nag you, or beep at you, or harass you.\n\nIts presence at the top of your screen in a place of primacy, near the time, is intended to bring your focus back to what is most important for the day, to make your primary goal present in your computer screen and life.", - "currency": "USD", - "artistId": 453295387, - "artistName": "Adam Mathes", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.mermodynamics.DoOneThing", - "version": "2.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/9a/f3/34/9af334d9-953f-52b6-2943-5e7105edd267/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/7a/73/fc/7a73fc94-5100-48f6-947a-a9d487e73b50/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/d3/88/f0/d388f0a9-8b1a-761b-8c95-d56985c422bd/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/85/a6/cf/85a6cfd6-6c45-e5f1-5189-cff16a2796a8/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/ac/f1/45/acf1450b-9380-b2b9-db1c-770f2ed81f76/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/1f/bb/de/1fbbde79-a7d4-e630-a51b-b389fceffd51/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/busy-apps-fze/id409966805?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "BusyCal", - "trackId": 1173663647, - "sellerName": "Busy Apps FZE", - "releaseNotes": "* NEW: Accessibility option added to Appearance Preferences for turning on darker grid lines\n* NEW: Optionally switch between Apple Contacts or BusyContacts for Birthdays / Anniversaries from under General Preferences\n* NEW: URL links, inside of notes displayed in the menu bar helper app, are now clickable\n* NEW: Clickable and selectable links / urls / locations in events displayed in the Menu app\n* Improved email address validation checks when dragging and dropping from Apple Contacts\n* Sound alarm option disabled for Google Calendar as these are not supported by Google and would get silently ignored\n* Attendee email parsing improved for emails containing apostrophes\n* Fixed a crash experienced by some users when importing large .ics files\n* Fixed a bug where manually selecting \"email attendees\" would open multiple copies of the compose email sheet\n* Fixed duplicate set of snooze options displaying when using Notification Center alerts\n* Fixed a macOS 10.11 issue with NotificationCenter and alerts\n* Ongoing stability and performance improvements", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-11-21T02:20:39Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-06-26T02:53:21Z", - "trackCensoredName": "BusyCal", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "ZH", - "ES" - ], - "fileSizeBytes": "24084319", - "sellerUrl": "https://www.busymac.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/busycal/id1173663647?mt=12&uo=4", - "trackContentRating": "4+", - "description": "BusyCal 3 is the most powerful, flexible, reliable calendar app for macOS. It's packed with innovative, time-saving features including customizable views, calendar sets, integrated to dos, travel time, smart filters, natural language input, weather, moons, graphics, tags, do not disturb mode and much more.\n\nCUSTOMIZABLE VIEWS\n\nBusyCal displays your calendar in Day, Week, Month, Year and List views. What makes BusyCal unique is its ability to customize these views to more precisely meet your needs. You can choose the number of weeks shown per month, or days shown per week, and you can even customize the calendar's appearance by choosing a font face and size, calendar colors, time format and more.\n\nINTEGRATED TO DOS\n\nIn BusyCal, To Dos are integrated into your calendar, display on the date or time they are due, and carry forward until completed. You can also display a To Do List in the sidebar. NOTE: Reminders in iOS 13 / macOS 10.15 is not compatible with BusyCal.\n\nINFO PANEL\n\nBusyCal's info panel enables you to quickly view and edit event details with speed and precision. The info panel can be displayed in the sidebar, as a popup or a floating window. And it's completely customizable, you can choose from a wide range of attributes to display including time zones, tags, maps, private notes, last edit time and more.\n\nNATURAL LANGUAGE INPUT\n\nBusyCal enables you to create events and to dos using natural language. A preview of the event details are displayed while you type as it recognizes titles, dates and times, locations and more. You can even use it to add contacts or attendees to an event, set alarms, add URLs, and to indicate the calendar to create the event on.\n\nTRAVEL TIME\n\nBusyCal allows you to block out time for walking, driving or taking mass transit to an event or location. You can set a fixed amount of travel time or determine it automatically using the integrated support for Location Services and Apple Maps. You can even receive alerts when it's time to leave as traffic conditions change.\n\nMENU BAR APP\n\nThe BusyCal menu bar app is always running, even when the main BusyCal app is not running, so you always have access to your schedule. \n\nSMART FILTERS\n\nBusyCal's Smart Filters are a powerful tool for managing your calendar. Smart Filters can be accessed with a keyboard shortcut or a button on the toolbar to display calendar sets (showing/hiding multiple calendars), perform saved searches (events that contain 'Joe'), apply view settings (an 8-week month view) and much more.\n\nALARMS\n\nBusyCal displays alarms in a movable, resizable floating window that offers the ability to snooze an alarm for any number of minutes from now or before the start of an event, or snooze multiple alarms at once. And BusyCal Alarms trigger even when the main BusyCal app isn't running, so you'll never miss an important appointment.\n\nWEATHER & MOONS\n\nBusyCal displays a live 10-day weather forecast, phases of the moon, and sunrise and sunset times. \n\nGRAPHICS\n\nBusyCal lets you add graphics to your calendar to highlight holidays and special events. You can choose from the built-in Emoji and IconFinder images, or drag images from your desktop or web.\n\nBUSYCONTACTS INTEGRATION\n\nBusyCal integrates with its sister app, BusyContacts, forming a flexible easy-to-use CRM solution. By adding contacts to events in BusyCal, you have instant access to a contact's email address and phone number, as well as a record in BusyContacts of your interactions with them.\n\nSYNC AND SHARE CALENDARS\n\nBusyCal supports iCloud, Reminders, Google, Exchange, Office 365, CalDAV, LAN sharing as well as WebDAV subscriptions, enabling you to sync calendars with other Macs and iOS devices running BusyCal or the built-in Calendar app. This includes the ability to share calendars, schedule meetings, and view the availability of others.", - "currency": "USD", - "artistId": 409966805, - "artistName": "Busy Apps FZE", - "genres": [ - "Productivity", - "Business" - ], - "price": 49.99, - "bundleId": "com.busymac.busycal3", - "version": "3.10.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/b1/75/14/b17514b7-25fd-2aa6-b147-a1ede3bc2952/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/d0/0c/d6/d00cd62c-9865-6602-3f1d-e7a50e62067a/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/f3/f0/39/f3f03912-aa89-8b53-81b0-8faa70b5611f/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/1d/3b/78/1d3b780c-d23a-78de-d6fc-d03985d772dd/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/9f/7a/e3/9f7ae3e3-40d1-6880-c27b-6e293cd39656/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/massimo-moiso/id566833668?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "InerziaThings Lite", - "trackId": 642103566, - "sellerName": "Massimo Moiso", - "releaseNotes": "New\n- added a \"Measure\" field. It can contain dimensions info as \"12x3 inch\" or similar. You can add it also to already present objects.\n\nBug fixes\n- Some locale string was missed.\n- Fixed a bug when importing from proprietary files on OS 10.10 through 10.13\n- Some optimisation.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2013-05-07T17:54:45Z", - "genreIds": [ - "6007", - "6006" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-04T18:49:46Z", - "trackCensoredName": "InerziaThings Lite", - "languageCodesISO2A": [ - "EN", - "FR", - "IT" - ], - "fileSizeBytes": "6157827", - "sellerUrl": "https://inerziasoft.eu/products/showcase/inerziathings/0", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/inerziathings-lite/id642103566?mt=12&uo=4", - "trackContentRating": "4+", - "description": "If you need assistance, please refer to our Technical Support Team by clicking the link on the right of this page.\nIf you have an idea for a new feature that could be useful for you, please contact us and we will be glad to consider it!\n\n===\n\nInerziaThings is your personal archive to keep all your Things under control.\n\nWhen did I purchase my new Mac? How much did I pay my last car? When the warranty of the washing machine will expire? I would like to read the technical data of the audio amplifier without having to remove the whole rack...\n\nInerziaThings can answer to all of these questions!\n\nKey features:\n\n• Management of all the important data (name, brand, model, serial number, warranty expiring date, shop, the date we sold it at what price, etc….)\n\n• Formatted free text notes.\n\n• Tags management (name change, one or more tag per Thing, automatic tag creation, auto-complete while typing).\n\n• Insert and manage images linked to a Thing: import, export, delete, open in default application.\n\n• Assign a Thing to a room within a place; a place can have more than one room. A Thing can be assigned to nowhere (not related to a room).\n\n• Search by name, brand, model, text within notes\n\n• Filter Things on place, room, tags.\n\n• Searching and filtering are one another compatible (that is: I want to find all objects in a room of a defined brand and with certain tags).\n\n• Automatic calculation of the total value of the visible objects\n\n• Unified management of more than one identical Things and their partial selling in one record\n\n• Complete Undo support.\n\n• Export Things in a text file (comma or tab delimited, ready to be imported in a Office suite).\n\n• Export all Things data, with Tag and Images in a proprietary format for backup or transport purposes\n\n• Objects multi-page print.\n\n• Dark theme support\n\n• New exciting features to come!\n\n\nNOTE: This Lite version is limited with regards to the number of Places, Room and Things.", - "currency": "USD", - "artistId": 566833668, - "artistName": "Massimo Moiso", - "genres": [ - "Productivity", - "Reference" - ], - "price": 0.00, - "bundleId": "eu.inerziasoft.InerziaThings-Lite", - "version": "3.7.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/b5/d0/e0/b5d0e04e-380a-19e7-e1ea-42d9090306a2/22518e83-8e14-495b-b780-843bb635a853_Desktop_01.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/99/03/09/990309a8-f057-8553-aaf6-9289dfa6cdec/ad0253ed-cdb3-4202-9afd-656f9b574d7d_Desktop_02.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/13/3a/5f/133a5f6c-d302-9af7-0859-d82f9cee2330/5b80f3aa-8722-414c-a621-58a97cf82103_Desktop_03.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/05/aa/6b/05aa6b80-9dbb-4968-9444-874cb9583fc7/de77889b-3f4d-438b-adb8-4c6bb715d6df_Desktop_04.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/86/c3/d7/86c3d72b-6e29-bc68-729c-7e0bc29290cb/668c81b6-a7db-402d-a036-e31bb109ffc7_Desktop_05.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/17/b8/c3/17b8c3bc-ab5b-6532-e59e-c804f70f40ad/f802848c-264f-4cbb-a322-a699dd0b69c2_Desktop_06.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/82/7b/5a/827b5acc-ea24-4ffc-ad2d-f008131dbedc/9d573032-de53-4221-b1d9-9195c845d6c9_Desktop_07.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ciarlo-software-llc/id1066322955?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "Doo 3: Get Things Done", - "trackId": 1515371334, - "sellerName": "Ciarlo Software, LLC", - "releaseNotes": "Fixes a bug that prevented scrolling of tasks", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-08-25T07:00:00Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2020-08-29T18:17:38Z", - "trackCensoredName": "Doo 3: Get Things Done", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "PT", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "3021396", - "sellerUrl": "https://www.getdooapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doo-3-get-things-done/id1515371334?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Doo is a simple way to be more productive. Create a card for each of your tasks. The best cards are easy to start and have a clear goal. Pick a few tasks each day and snooze the rest. Doo helps you build consistent, sustainable habits that keep you focused. Over time, your completed cards become finished projects, without the stress and anxiety of an endless list.\n\nFEATURE HIGHLIGHTS\n• Be more productive with a unique, card-based interface\n• Separate cards into custom groups\n• Create open-ended, calendar, or location-based tasks\n• Create tasks within other apps with the share extension\n• Schedule tasks with custom intervals and alerts\n• Sync with iCloud across iOS and Mac devices\n• Collaborate on tasks with other Doo users (requires iCloud)\n• View upcoming tasks with the Doo Widget\n• Complete or snooze tasks from notifications\n• Run Doo from the dock or menu bar", - "currency": "USD", - "artistId": 1066322955, - "artistName": "Ciarlo Software, LLC", - "genres": [ - "Productivity" - ], - "price": 9.99, - "bundleId": "com.mciarlo.Everyminder.CiarloMacDoo3", - "version": "3.0.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/af/ee/c4/afeec4b1-a276-3845-148d-78a0862331a5/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple62/v4/59/9f/22/599f22d1-b0c5-d6b0-8c67-7e59609d9a68/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple41/v4/3b/5b/db/3b5bdbe0-ee09-7933-e6f1-24ee0d6a0f55/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/68/10/9c/68109c13-9add-40b9-5c7e-1bc2b4a1898c/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/new-technologies/id762050392?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Oh! My Mind Mapping: Idea Flow", - "trackId": 1140048806, - "sellerName": "New Technologies, LLC.", - "releaseNotes": "This update \n• Includes bug fixes and feature enhancements.\n• Fixes several stability issues.\nThanks to everyone for your support! It really helps us out!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-09-03T19:18:06Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-14T07:52:27Z", - "trackCensoredName": "Oh! My Mind Mapping: Idea Flow", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "13457808", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/oh-my-mind-mapping-idea-flow/id1140048806?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Pick all your ideas together and create a clear mind map. Visual diagrams will help you keep in mind any flow of information! \n\nDIFFERENT PRE-INSTALLED TYPES OF VISUALIZATION\n- One-sided map;\n- Tree structured;\n- Fish-shaped;\n- Sun-shaped.\n\nSMART REPRESENTATION\n- Different layout structures vary depending on the purpose;\n- 3 elements to work with: arrows, stickers and images;\n- Applying images or icons to stickers;\n- Opportunity to interconnect stickers.\n\nVISUAL BRAINSTORMING RESULTS\n- Instant Saving & Sharing;\n- You can also print out your projects.\n\nWith user-friendly control and understandable functions of Oh! My Mind Mapping 2 the brainstorming process becomes easy and convenient!\n\nOur app offers subscription:\nhttps://newtech-ltd.com/privacy\nhttps://newtech-ltd.com/tos", - "currency": "USD", - "artistId": 762050392, - "artistName": "New Technologies", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.OhMyMindMapping2lessia", - "version": "1.7.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple4/v4/98/2f/a7/982fa7f4-3dbe-7039-d2a0-b788eff5aaa0/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple4/v4/4b/23/ea/4b23eab3-063f-6ed0-03d1-0ea71f779bb6/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/5d/cb/a3/5dcba3bd-eb2e-3cc2-5c13-307fd6203b2c/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e2/2d/ca/e22dcadd-d7d5-3150-03c1-9f4a43c3b0f4/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "Antnotes", - "trackId": 808734429, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-03-08T02:52:03Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2016-09-24T17:04:28Z", - "trackCensoredName": "Antnotes", - "languageCodesISO2A": [ - "EN", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "1014384", - "sellerUrl": "https://www.antlogic.com/apps/antnotes", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/antnotes/id808734429?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- snap to screen bounds and other notes\n- translucent notes\n- attach note to any application so it will be shown when this application is activated/hidden when switching to other application\n- automatically hide notes when inactive\n- pin note to desktop to make it stay atop of other windows\n- quick access via the menu bar icon\n- make new notes by dragging text, images, files to menu bar icon\n- integration with services: create new note from any text in any application\n- drag images and sounds to note contents\n- configurable global shortcuts to create new note or show/hide all notes\n- resizable\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Just let us know, we'll consider almost anything unless it’s cooking, coffee making or walking your dog!\n\nVideo on how to use Antnotes: http://youtu.be/E7wKKeGZDFw ( or http://www.youtube.com/AntlogicCompany )\nFor more information visit our site: http://www.antlogic.com/ or Facebook page: http://facebook.com/AntlogicCompany\n\nIf you need support, have feature request or any complaints, you are welcome to our support forums: http://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "ua.com.AntLogic.Antnotes", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple6/v4/47/e2/1a/47e21af8-c05e-57d5-98a4-3a596c897646/mzl.hflpafaz.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/e1/40/16/e1401647-5ee0-4819-20cd-2a4b6188b38c/mzl.mobmauoe.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/3e/14/4e/3e144eb6-08f4-eb44-9465-c6cad39ef787/mzl.tnfvxdpb.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/appigo/id282769260?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.8", - "trackName": "Todo", - "trackId": 408975584, - "sellerName": "Appigo, Inc.", - "releaseNotes": "Easier to share lists with co-workers, and edit list information.\nBug fixes.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-03T01:41:59Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$14.99", - "currentVersionReleaseDate": "2016-01-14T21:39:56Z", - "trackCensoredName": "Todo", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "PT", - "RU", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "8661615", - "sellerUrl": "http://www.appigo.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/todo/id408975584?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Todo® empowers millions of people around the world to get things done. Todo’s beautiful and intuitive interface enables you to easily create and share tasks, checklists and projects. Whether you are managing a complex work project with a team, planning a family reunion or simply sharing a shopping list, Todo has what you need.\n\n\nKEY FEATURES\n· Automatically sync tasks across the computers and devices you use\n· Share Lists: Create lists and share them with your co-workers, family and friends\n· Collaboration: Add comments to tasks that will be seen by everyone in your shared task list\n· Stay Informed: Receive email notifications when someone in your shared list updates a task\n· Simply reply to task emails and your comments will automatically be added to that task\n\nSO MUCH MORE…\n· Geofence your tasks with Todo and your iPhone alerts you when you arrive\n· Share Tasks: Delegate tasks and get things done as a team\n· Manage Advanced Projects: Use checklists inside of your projects for better organization\n· Keep Tasks Updated: Tasks are updated at lightning speed to each of your devices \n· Drag and drop content from emails to quickly create tasks\n· Multiple reminder alerts\n· Organize with projects and checklists (subtasks)\n· Schedule repeating tasks\n· Starred tasks\n· Drag and drop sorting\n· Full task searching including notes\n· GTD support with contexts and tags\n\nTODO SYNCS WITH \n· Todo-Cloud (premium paid sync service)\n· iCloud (Note: iCloud is used for synchronizing tasks to other copies of the Todo app on iOS and Mac).\n· Dropbox\n· Toodledo.com\n· Wi-Fi\n\nMaximize Todo’s power with a Todo-Cloud Premium Account. Download the Todo app and try Todo-Cloud syncing service with a free 14 day trial. Todo is also available for iPhone, iPad, and iPod touch on the App Store. Additionally, you can always access your tasks online at www.todo-cloud.com, even without a premium account.\n\n\nEXCELLENT CUSTOMER SUPPORT\n\nWe value our customers and strive to answer every question, however, we are unable to reply directly to app reviews. If you are looking for help or have a specific question, please send us a message by visiting our Help Center. If you love to get things done, feel free to help others understand how this app has helped you by writing a review on the App Store. \n\nAppigo Help Center: http://help.appigo.com/\n\n\nSTAY IN THE KNOW\n\nBe sure to follow us online! We frequently publish updates about what's new and what's coming in the app on Facebook and Twitter.\n\nhttp://twitter.com/appigo\nhttp://www.facebook.com/appigo", - "currency": "USD", - "artistId": 282769260, - "artistName": "Appigo", - "genres": [ - "Productivity", - "Business" - ], - "price": 14.99, - "bundleId": "com.appigo.todomac", - "version": "3.0.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/2b/3b/ae/2b3baea6-3b6a-5575-9201-7b1f9374b59e/25d73a4f-71f0-4651-9a8d-d05a1438c099_iPhone-8Plus-1-final.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/40/95/c3/4095c368-0233-609c-cfbf-f7b8e54b1680/d66420dd-510c-4b78-9c73-fbf6f1732c69_iPhone-8Plus-2-final_3.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/57/f2/57/57f257ec-f627-c968-70f4-c65d1c6f8f13/8caf2452-0f43-4631-9561-8742fc682b83_iPhone-8Plus-3-final.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/b6/f4/a1/b6f4a184-95bc-ecae-6d7e-46155694ca2b/52661be5-0db9-4231-90d1-0673bfe42bb7_iPhone-8Plus-4-final.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/81/9a/73/819a739d-a936-3c80-aa89-3393c5fc5835/7044a241-d38d-4da6-abca-03afc47d06dc_iPhone-8Plus-5-final.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/ec/6e/32/ec6e32ff-d2ee-e5c5-adda-0a93c256a749/146a5b09-1775-498d-b16a-48b05b6d52be_iPad-Pro-1-final.png/576x768bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/ce/f6/1c/cef61c51-bd09-0a33-5a10-f33ac328a772/c2057935-2438-4efc-9d85-f8d72756376e_iPad-Pro-2-final_3.png/576x768bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/4e/1b/88/4e1b88b2-6f91-5996-5aa3-52bbb8fd3a71/c49adc41-3b71-4301-933d-671384616ede_iPad-Pro-3-final.png/576x768bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/08/3a/99/083a9917-7ac1-2503-dc51-b1f84a1cba7f/54b5f7cd-d35b-4bfe-a300-5e8621085081_iPad-Pro-4-final.png/576x768bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/2b/9d/d8/2b9dd8fc-afa4-2194-37a0-6490cbe9db63/2ea461a8-57cd-4bf8-b6fe-aeef6e9914f0_iPad-Pro-5-final.png/576x768bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "Tally Counter & Habit Tracker", - "trackId": 1412716242, - "sellerName": "Volodymyr Yahenskyi", - "releaseNotes": "Thanks for using my app!\nThis release contains bug fixes and performance improvements.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-09-08T04:54:49Z", - "genreIds": [ - "6007", - "7009", - "6014", - "7004" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-29T14:09:17Z", - "trackCensoredName": "Tally Counter & Habit Tracker", - "languageCodesISO2A": [ - "EN", - "RU", - "UK" - ], - "fileSizeBytes": "11422720", - "sellerUrl": "https://yahenskyi.dev/tally/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.89655000000000040216718844021670520305633544921875, - "userRatingCountForCurrentVersion": 29, - "averageUserRating": 4.89655000000000040216718844021670520305633544921875, - "trackViewUrl": "https://apps.apple.com/us/app/tally-counter-habit-tracker/id1412716242?uo=4", - "trackContentRating": "4+", - "description": "Tally Counter & Habit Tracker will help you to count items, days, score points of games, count drinks you've been drinking, count training sessions, or anything else.\n\nFeatures:\n• Simple interface\n• Dark mode\n• Counting history\n• Total counters value\n• Last change timer\n• Colorful app icons\n• Reorder counters using a long press gesture\n• Rename each counter as you want\n• Assign different colors to each counter\n• Sound confirmation of counts\n• Haptic feedback on supported devices\n• Select the step size for your count\n• Reset, delete and export all counter data at once\n\nMultiple counters, as many as you need. To delete, reset or edit the counter, just swipe left on it. To increase or decrease the counter value, tap at any point on the ​left or right side of it. Long press to reorder counters.\n\nTally Premium benefits:\n• iCloud sync\n• Unlimited counters\n• Export to CSV/Excel\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", - "currency": "USD", - "artistId": 961335645, - "artistName": "Volodymyr Yahenskyi", - "genres": [ - "Productivity", - "Family", - "Games", - "Board" - ], - "price": 0.00, - "bundleId": "com.yahenskyi.tally", - "version": "1.6.4", - "wrapperType": "software", - "userRatingCount": 29 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple18/v4/5e/ae/76/5eae7683-6038-ba05-121b-4b6d77bc6a55/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple20/v4/6f/2d/83/6f2d8388-00eb-cb2b-6d10-905d823021c0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/14/09/5c/14095c40-9a33-3685-cd67-7e0c8d5f6421/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple60/v4/b6/43/26/b6432664-84ce-ce21-c592-f6d2b137abdf/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ping-lv/id1436949017?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.7", - "trackName": "iLove Sticky Notes", - "trackId": 1096650860, - "sellerName": "Ping Lv", - "releaseNotes": "1 Some minor improvements", - "primaryGenreId": 6012, - "primaryGenreName": "Lifestyle", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-05-23T02:32:02Z", - "genreIds": [ - "6012", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-05T19:39:54Z", - "trackCensoredName": "iLove Sticky Notes", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "988745", - "sellerUrl": "http://ilovemacapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/ilove-sticky-notes/id1096650860?mt=12&uo=4", - "trackContentRating": "4+", - "description": "iLove Sticky Notes is a very useful memo assistant for Mac users. With iLove Sticky Notes you can pin notes to your desktop which can help remind you of something urgent or important! You can customize the background color of your notes and it always stay in sight so you won't forget them.\n\nHow to use:\n1. Open iLove Sticky Notes.\n2. Click on the app icon on system tray to go into Edit Mode. \n3. Click on the top left \"Add\" button of note to create a new one, hit the top right \"cross\" button to delete the note.\n4. Click on the app icon on system tray to Quit Edit Mode.", - "currency": "USD", - "artistId": 1436949017, - "artistName": "Ping Lv", - "genres": [ - "Lifestyle", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.ilove.desktopnotes", - "version": "2.1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple125/v4/5d/cf/06/5dcf06fc-2767-3b5e-ce1a-ae71d65d7949/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/20/8a/d6/208ad6df-1f33-2ec5-fb54-33dc416bebb8/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/fiplab-ltd/id320240050?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "TaskTab: Simple To Do List", - "trackId": 1395414535, - "sellerName": "FIPLAB Ltd", - "releaseNotes": "- Bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-07-23T03:22:17Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-09-06T19:50:30Z", - "trackCensoredName": "TaskTab: Simple To Do List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "2261951", - "sellerUrl": "https://fiplab.com/apps/task-tab-for-mac", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/tasktab-simple-to-do-list/id1395414535?mt=12&uo=4", - "trackContentRating": "4+", - "description": "TaskTab places the focus squarely on your to do list. We've intentionally kept the app as simple as possible to allow you to quickly add, check off and manage your tasks without being distracted by pointless features that overly complicate the key purpose of a to do list app. The app lives in your menubar and is available at a click of a button or via its customizable hot key.\n\n- A beautifully designed native app for macOS that supports both light and dark mode. \n\n- You can easily import a to do list as well as export them to share with others.\n\n- You can optionally choose to show the number of remaining items in your menubar.\n\nWe've worked hard to make TaskTab as powerful and efficient as possible for you to use. We would love to hear your thoughts via email and make any improvements to future versions of this app. We intend to have an active development cycle powered by your feedback and support!", - "currency": "USD", - "artistId": 320240050, - "artistName": "FIPLAB Ltd", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.fiplab.tasktabmac", - "version": "1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/d9/b6/7a/d9b67a64-4d35-ab91-6d9a-f31050c96b29/x5ijyUu7fLDtHIabWVSOW0-temp-upload.nuezlxgp.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/v4/24/a4/fc/24a4fc65-960e-8c82-c129-bf0ef04b0fc6/mza_7830980125492921569.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/fb/ed/cf/fbedcfe1-9b32-a73c-41b2-8a17f98e9f75/mza_6440831072027067066.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/e1/a1/d5/e1a1d549-8810-7bce-9bae-4cb7de91b02d/mza_4557184267443726008.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/56/38/4c/56384c59-f7b4-2d9d-4dce-edbd66d63737/mza_5206003488144523712.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/midnight-beep-softworks/id364896535?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "Inbox Classic", - "trackId": 528008223, - "sellerName": "Midnight Beep Softworks", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-06-11T21:16:59Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2012-06-11T21:16:59Z", - "trackCensoredName": "Inbox Classic", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "8862330", - "sellerUrl": "http://www.midnightbeep.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/inbox-classic/id528008223?mt=12&uo=4", - "trackContentRating": "4+", - "description": "This is the original version of Midnight Inbox that brings Getting Things Done (GTD) productivity to the desktop in true delicious style, now updated for the latest versions of Mac OS X with native support for 10.6 and 10.7! And now available for free!\n\nWe have new versions of Inbox for the iPhone and iPad for you to check out, and are working on a whole new desktop version of Midnight Inbox. See what's new with Inbox on our website: http://www.midnightbeep.com\n\nThe workflow.\nTop to bottom, Inbox provides an automated workflow of simple input, intuitive organizing, and contextual outputs. Just jump right to the part that best fits your situation: from collecting your thoughts, to concentrating on your work.\n\nAutomatic zero.\nGetting to the bottom of your inbox is what this app is all about. As things pile up you need to get them sorted and acted upon; Inbox makes this quick and easy by giving you shortcuts to the places where things need to be, and the assistance to get them done.\n\nGTD complete.\nMidnight Inbox is inspired by the Getting Things Done methodologies created by David Allen. If you've never heard of GTD, just let Inbox be your guide. It will help you be stress-free, following the GTD workflow for you, so you can concentrate on what really matters.\n\nPLEASE NOTE:\n• Inbox Classic is -not- compatible with current iOS versions (Inbox Mobile for iPhone, Inbox Touch for iPad) for data or syncing. The forthcoming version \"Midnight Inbox 2\" will bring full feature parity and iCloud-based syncing to the desktop version of Inbox, and import of Classic version's data.\n• Support for Mac OS 10.4 and 10.5 and PowerPC-based Macs is available in version 1.5.1 of Inbox Classic from our Web site.", - "currency": "USD", - "artistId": 364896535, - "artistName": "Midnight Beep Softworks", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.midnightbeep.InboxClassic", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/16/c9/0d/16c90d93-849c-f870-5eed-d92500ca8ab8/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/68/77/5b/68775b4c-55b6-2dae-b4f8-358515f1ecfe/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/busy-apps-fze/id409966805?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "BusyContacts", - "trackId": 964258399, - "sellerName": "Busy Apps FZE", - "releaseNotes": "* Fixed a bug where column sizes in List view would not correctly restore for some users\n* Fixed a bug where new tags for a new contact syncing to a Google account would not sync correctly\n* Fixed a bug where manually selecting \"email\" from the share menu would open multiple copies of the compose email sheet\n* Ongoing stability and performance improvements", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-03-06T23:53:17Z", - "genreIds": [ - "6000", - "6002" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-06-10T23:58:36Z", - "trackCensoredName": "BusyContacts", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "ZH", - "ES" - ], - "fileSizeBytes": "12809192", - "sellerUrl": "https://www.busymac.com/busycontacts/index.html", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/busycontacts/id964258399?mt=12&uo=4", - "trackContentRating": "4+", - "description": "BusyContacts is a contact manager for macOS that makes creating, finding, and managing contacts faster and more efficient.\n\nBusyContacts brings to contact management the same power, flexibility, and sharing capabilities that BusyCal users have enjoyed with their calendars. What's more, BusyContacts integrates seamlessly with BusyCal forming a flexible, easy to use CRM solution that works the way you do.\n\nBusyContacts syncs with the built-in Contacts app on macOS and iOS and supports all leading cloud services, including iCloud, Google, Exchange, Facebook & Twitter.\n\nCUSTOMIZABLE VIEWS\n\nContacts can be displayed in two views: A single column list view, or a multi-column table view that allows you to control the columns displayed (e.g. company, last name, first name, email, phone, etc.) and the sort order.\n\nTAGS\n\nTags are an extremely flexible way to manage contacts in BusyContacts. You can assign multiple tags to each contact and a tag cloud allows you to easily filter the list of contacts by tag (e.g. family, client, prospect, coworker, etc.).\n\nACTIVITY LIST\n\nThe Activity List shows a chronological listing of activities associated with the selected contact including meetings, to dos and other calendar events (requires BusyCal), communication through email and messaging, and social network posts. Note: Email communication is only available on macOS 10.14 and below.\n\nBUSYCAL INTEGRATION\n\nBusyContacts integrates with BusyCal allowing you to link contacts to events and to dos in your calendar, providing flexible CRM capabilities for scheduling meetings, follow up tasks, and tracking past activities.\n\nSOCIAL NETWORK INTEGRATION\n\nBusyContacts syncs with leading social networks including Facebook, Twitter and LinkedIn, allowing you to integrate photos, birthdays and other information from social networks with your contacts.\n\nSMART FILTERS\n\nSmart Filters are a powerful tool for filtering contacts and creating saved searches that can be applied with a single click. You can create Smart Filters to display contacts that match certain conditions, such as a text string, tag, or birthdate. Or you can create Smart Filters to remember view settings such as columns displayed and sort order.\n\nSYNC\n\nBusyContacts syncs with all leading cloud services including iCloud, Google, Exchange, and other CardDAV servers, and syncs with the built-in Contacts app on macOS and iOS.\n\nSHARE\n\nBusyContacts allows you to share address books with other BusyContacts users with read-only or read/write privileges. Address Books can be shared through Exchange, Fruux, Kerio, over the LAN, and through other CardDAV servers that support sharing.", - "currency": "USD", - "artistId": 409966805, - "artistName": "Busy Apps FZE", - "genres": [ - "Business", - "Utilities" - ], - "price": 49.99, - "bundleId": "com.busymac.busycontacts", - "version": "1.4.8", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/19/ed/39/19ed390e-0d46-20a9-3a07-959f5da8fef2/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/1a/ad/11/1aad1160-0e74-6135-14bf-1d43839ee5b8/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/24/2a/a4/242aa49d-ca64-c44a-46bd-b826a2114b56/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/05/3e/45/053e45b0-97a9-69bd-eb2d-1f5de77e8b2a/mzl.vcqfhklr.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/whetstone-apps/id460983180?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "DayMap", - "trackId": 460983177, - "sellerName": "Whetstone Apps, LLC", - "releaseNotes": "Support for dark system appearance. (e.g. Dark Mode).", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-11-17T07:39:46Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$12.99", - "currentVersionReleaseDate": "2019-09-04T22:42:44Z", - "trackCensoredName": "DayMap", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ZH" - ], - "fileSizeBytes": "1988675", - "sellerUrl": "https://www.daymapapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/daymap/id460983177?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Have you ever been confused by your day planner? Do you think and plan visually? We created DayMap because we believe visual people deserve an organizer tailored to their strengths.\n\nDayMap is a visual planning app which helps you plan your days and weeks for optimum productivity. The project outliner places your projects side by side in columns so that you can see more information with less scrolling.\n\nThe calendar located below the project outliner makes it easy to schedule important todo’s.\n\n- Sync your DayMap data between your Mac and iPhone (or any iOS Device) thanks to integrated iCloud sync support! (Requires iOS 8 or later and Mac OS X Yosemite or later)\n- System-wide keyboard shortcut to quickly add new tasks to inbox without having to switch apps.\n- Create deep hierarchies of tasks with subtasks.\n- View all your projects and tasks at a glance.\n\nDayMap helps you make the most of every day, every week.\n\nWe are interested in hearing what you think, so share your feedback with us on our website.", - "currency": "USD", - "artistId": 460983180, - "artistName": "Whetstone Apps", - "genres": [ - "Productivity", - "Business" - ], - "price": 12.99, - "bundleId": "com.whetstoneapps.daymap", - "version": "2.1.5", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/4a/8b/b1/4a8bb1a9-54fc-7214-4b40-7bb92fc9f2bc/pr_source.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/ec/5b/41/ec5b4185-2f49-1bb3-1441-637595930db9/pr_source.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/18/3b/26/183b2664-2c17-1bac-1e8b-5759df726eeb/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/21/ab/f2/21abf2ea-672e-517a-9941-bd8057196b77/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/7f/a0/03/7fa00345-db22-bf9b-31b2-4d22490ca1f6/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/4a/6d/ab/4a6dab7d-1162-0235-6012-207038d00244/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/63/35/7b/63357bc9-3e1c-7758-83d2-f36bb05ac12c/pr_source.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/11/c4/a4/11c4a431-474c-1c52-93df-2a2b948bc4e7/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/79/9c/bd/799cbdc4-b67f-7d7f-e54f-0b2b24ac185a/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/87/c4/f6/87c4f65a-62c1-ea9d-036a-e77cf4921423/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/60x60bb.jpg", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/512x512bb.jpg", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/shihab-mehboob/id1012903170?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "13.0", - "trackName": "Allegory", - "trackId": 1470828583, - "sellerName": "Shihab Mehboob", - "releaseNotes": "- Added a new default app icon\n- Added cursor support for iPad\n- Pulling down when viewing a full-screen list will now dismiss the list\n- Improved iCloud sync\n- Improved accessibility and contrast across the app\n- Improved keyboard shortcut commands\n- Added more alternative app icons\n- Fixed various issues where the app may crash\n- Minor UI changes\n- Various bug fixes and improvements\n\nIf you have any questions or feedback, please get in touch. Allegory was created by an incredibly small team of one and I'd love to hear your thoughts. I'm available through email: shihab@allegoryapp.info or Twitter: @AllegoryApp or @JPEGuin.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-09-19T07:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-29T17:39:24Z", - "trackCensoredName": "Allegory", - "languageCodesISO2A": [ - "AR", - "NL", - "EN", - "FR", - "DE", - "HE", - "IT", - "PL", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "21512192", - "sellerUrl": "https://www.allegoryapp.info/press", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.07017000000000006565414878423325717449188232421875, - "userRatingCountForCurrentVersion": 57, - "averageUserRating": 4.07017000000000006565414878423325717449188232421875, - "trackViewUrl": "https://apps.apple.com/us/app/allegory/id1470828583?uo=4", - "trackContentRating": "4+", - "description": "Allegory is a powerful notes app with a beautiful iOS-centric UI. The minimal and focused writing experience, combined with an impressive set of powerful features, makes this the ideal choice for everyone. Allegory removes the complexity of most markdown editors, presenting a minimal UI that’s welcoming for casual writers and note-takers as well as more attuned authors, making it a great app for you to pick up regardless of your background and skill level.\n\n—\n\nWHY YOU'LL LOVE ALLEGORY:\n\nAllegory immediately launches to the clean and simple note editor screen, boosting your productivity and reducing friction between wanting to write down what you’ve just thought of, and actually getting it written down. Support for valid markdown as well as custom markdown makes it easier to create your best work.\n\nAllegory also includes a brilliant hand-crafted drawing area which lets you select from a variety of colors, long-holding on any of the tools brings them into focus with sliders to adjust their opacity and stroke size with gorgeous animations that strike awe.\n\nYou can bookmark and set due dates on any note from anywhere in the app regardless of where you are, which makes performing essential actions entirely seamless, further reducing friction for users. Seamless iCloud sync adds upon the notion of keeping your notes exactly where they need to be. Notes can be searched for within the app in ways that make sense to you, by words and dates, or by using language that comes naturally such as ‘today’, ‘yesterday’, and ‘last week’. A summary of your current note also provides further context on your writing by displaying character/word/line/paragraph counts, sentiment analysis to portray how positive or negative your writing is, quick links to open the specific note, and more.\n\nInspiration can strike anywhere, and that is why Allegory supports custom action extensions that allow creating notes and putting down ideas from other apps. You can also use Siri to do the same. Scan documents and text to convert it to notes, or add images within notes to provide more context. When you want to share your work with the world, use one of the many export options to create files that work best for you, from TXT and MD, to HTML and PDF. Or simply export as an image or plain text. It's built in ways that make sense to your workflow.\n\nTweak and adjust the app to make it look and behave the way that you want. From app icons and tints, to markdown settings and haptics, there's a settings option to make the app your own. URL schemes and Siri shortcuts allow you to perform useful actions in Allegory from anywhere. Allegory is available on your iPhone, your iPad, and your Apple Watch, and supports seamless iCloud sync to share notes without lifting a finger.\n\nAll the power of iOS is in your hands with Allegory. It is fully integrated with the latest technologies such as Dark Mode, a standalone Apple Watch app, Siri, Today Widget, Quick Actions, Action Extension, Context Menus, iCloud sync, Notifications, document scanning, biometric locks, custom haptics, Pencil support, keyboard shortcuts, URL schemes, custom app icons, iMessage stickers, fully accessible, and more.\n\n—\n\nGET IN TOUCH:\n\nIf you have any questions or feedback, please get in touch. Allegory was created by an incredibly small team of one and I'd love to hear your thoughts. I'm available through email shihab@allegoryapp.info or Twitter @AllegoryApp or @JPEGuin.\n\nhttps://www.allegoryapp.info/terms\nhttps://www.allegoryapp.info/privacy", - "currency": "USD", - "artistId": 1012903170, - "artistName": "Shihab Mehboob", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 0.00, - "bundleId": "com.shi.Allegory", - "version": "1.2.8", - "wrapperType": "software", - "userRatingCount": 57 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple/e4/aa/fe/mzl.rpignxpp.jpeg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/5e/01/c1/mzl.rifpmnid.jpeg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/9f/ea/f4/mzl.jdgitdao.jpeg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/6f/e8/97/mzl.grngihjg.jpeg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/d8/a8/a1/mzl.seplyubx.jpeg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/focus-home-interactive/id406173890?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "The Next Big Thing - Lite", - "trackId": 435972315, - "sellerName": "Focus Home Interactive SAS", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-02T08:38:51Z", - "genreIds": [ - "6014", - "7002", - "6016", - "7012" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2011-06-02T08:38:51Z", - "trackCensoredName": "The Next Big Thing - Lite", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ES" - ], - "fileSizeBytes": "858856057", - "sellerUrl": "http://www.thenextbig-game.com", - "contentAdvisoryRating": "12+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-next-big-thing-lite/id435972315?mt=12&uo=4", - "trackContentRating": "12+", - "description": "The Next BIG Thing is the new, hilarious adventure gem from the creators of Runaway. A great adventure game in high definition, loaded with laughs, tributes, mysteries and wacky puzzles!\n \nThanks to a production worthy of a great animated movie, an awesome soundtrack, delightful dialogue and a great art style, The Next BIG Thing will make you live an unforgettable adventure which brilliantly takes players across the fantastic movie genres of a totally crazy Hollywood.\n \nHollywood\nWhat if horror movies' monsters were actually played by real monsters? And what if they were now forced to play in movies for kids, romantic comedies or even musicals? And what would happen if, eventually, they were to rebel?\n \nIn that context, Liz Allaire, talented journalist who can't count up to 4, and Dan Murray, a tough macho who can't stand beetles, attend the horror movies award ceremony… there is the starting point of an incredible story, full of twists and turns! Help Liz and Dan solve the numerous mysteries of an amazing adventure that you won't forget any time soon!\n \n- Solve numerous puzzles and mysteries\n \n- Meet wacky characters and monsters\n \n- Explore dozens of colorful places\n \n- A production worthy of a great animated movie\n \n- Movie-like musical score and English voice-over", - "currency": "USD", - "artistId": 406173890, - "artistName": "FOCUS HOME INTERACTIVE", - "genres": [ - "Games", - "Adventure", - "Entertainment", - "Puzzle" - ], - "price": 0.00, - "bundleId": "com.focushome.TNBTD", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/b8/2c/8b/b82c8b3c-9b55-1c06-b735-80fb423912e7/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/fb/ef/b4/fbefb447-7011-d6d9-1064-8fad269e6cdc/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple122/v4/5e/7f/b1/5e7fb1a8-db6c-c17f-7522-87832c29ab28/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/39/fe/f8/39fef803-5fee-7074-e0f7-9e58a366aa59/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/24/6e/4c/246e4c5e-3aa3-4dc3-2feb-7533c7676fc8/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/new-technologies/id762050392?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "iMap Builder - Mind Mapping", - "trackId": 1108898500, - "sellerName": "New Technologies, LLC.", - "releaseNotes": "We made some changes one can't see with a nude eye, which improve the app's performance and will help us to provide more good quality content in the future.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-05-17T05:23:15Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-26T22:38:59Z", - "trackCensoredName": "iMap Builder - Mind Mapping", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "26579691", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/imap-builder-mind-mapping/id1108898500?mt=12&uo=4", - "trackContentRating": "4+", - "description": "iMap Builder is a simple app designed for bringing your thoughts in order. Pick all your ideas together and create a clear mind map.\n\nUNIVERSAL DATABASE\n- Storage of any thought and ideas;\n- Images and diagrams adding;\n- Convenient built-in calendar;\n- Quick and easy tutorial;\n\nWORKFLOW ORGANIZATION\n- create several \"central\" blocks inside of your project;\n- add different colors to your subtasks;\n- set due dates for each block;\n- add icons or pictures to your blocks;\n\nSMART REPRESENTATION\n- 3 different templates to work with (1 basic hierarchy and 2 additional templates to unlock to organize your thoughts more creatively);\n- mark your tasks as done;\n- connect different elements of blocks into one \"cloud\";\n- make comments for any connections;\n- share your project in social media;\n\nNECESSARY TOOLS\n- search for necessary objects inside your mind map;\n- export to PNG file or print out your projects;\n- Fonts, colors, icon – everything is adjustable;\n- Delete, undo/redo, export – full set of operations.\n\nNo good idea will ever be lost with iMap Builder.\n\n\nPREMIUM ACCESS\n- The length of subscription is 1, 6 or 12 months.\n- Your subscription will be automatically renewed within 1 day before the current subscription ends. \n- Auto-renew option can be turned off in your iTunes Account Settings. \n- Payment will be charged to iTunes Account at confirmation of purchase. \n- No cancellation of the current subscription is allowed during active subscription period.\n\nPrivacy Policy: https://newtech-ltd.com/privacy\n\nTerms Of Use: https://newtech-ltd.com/tos", - "currency": "USD", - "artistId": 762050392, - "artistName": "New Technologies", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.iMapBuilderlessia", - "version": "2.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/91/39/5e/91395ebc-f4fe-74c6-c171-27b735574bca/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/b5/e8/53/b5e8534e-850c-07ba-896e-19d2b40e41e0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/9c/e9/bc9ce902-e5c8-e96c-c911-b37867105070/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/04/1e/cf/041ecfa3-2352-b6c9-0a84-fcb2fc5dfddb/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/07/2a/c7072a47-f560-ab3c-0444-19d6ac98c23e/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/dorian-brown/id1240209534?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "Guso - Quick Web Search", - "trackId": 1434234265, - "sellerName": "Dorian Brown", - "releaseNotes": "- Added new search feature, you can quickly search Amazon, Youtube, Pinterest, Stackoverflow, Reddit...\n- Improved performance and stability \n- Minor Bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-08-30T12:01:47Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-04-24T22:40:06Z", - "trackCensoredName": "Guso - Quick Web Search", - "languageCodesISO2A": [ - "EN", - "ES" - ], - "fileSizeBytes": "5658895", - "contentAdvisoryRating": "17+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/guso-quick-web-search/id1434234265?mt=12&uo=4", - "trackContentRating": "17+", - "description": "Google at your fingertips\n\nTake your productivity and efficiency to the next level with Guso.\n\nKey Features:\n- Search Amazon, Youtube, Pinterest, Stackoverflow\n- Lightweight and extremely fast!\n- Instant search suggestions from Google\n- Keyboard shortcut to help you search faster\n- Support All Browsers (Safari, Chrome, Firefox, Internet...)\n- Operated via Menu Bar Icon\n- Beautiful & Intuitive User Interface\n- Dark Mode Support!\n\n\n \"Build for Google lovers\" \n\nIf you have any questions or feedback, please feel free to get in touch. Guso was created by an incredibly small team of one.\n\n\n\n\nNote: Guso is NOT affiliated with, funded, sponsored, or in any way associated with the prominent technology company known as \"Google\".", - "currency": "USD", - "artistId": 1240209534, - "artistName": "Dorian Brown", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.suji.googler", - "version": "1.0.12", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/3c/51/4a/3c514a48-7fd2-ddc4-c945-889cd3612867/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/18/54/8d/18548da9-05b0-be27-7eb6-8c7484385d88/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/4a/9d/ff/4a9dff6d-7b3e-7f49-f534-1bf59f8bf122/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/e3/a7/45/e3a745b0-1ddd-1480-8b9c-3cf32ae5716a/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/6c/a9/cf/6ca9cfc2-69e9-9450-3b6e-83bf9c30ecfb/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/karelia-software-llc/id404126469?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9.5", - "trackName": "The Hit List", - "trackId": 432764806, - "sellerName": "Karelia Software LLC", - "releaseNotes": "• Mojave compatibility fixes\n• Improved handling of sync push notifications", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-05-10T07:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2018-09-27T07:32:52Z", - "trackCensoredName": "The Hit List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "5761638", - "sellerUrl": "http://www.karelia.com/products/the-hit-list/mac.html", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-hit-list/id432764806?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Powerful, flexible and simple task management that is a pleasure to use, The Hit List can handle personal tasks and professional projects. Use it with The Hit List for iPhone v2.3 (available on the iOS App Store) which includes The Hit List for Apple Watch and enjoy fast, reliable sync service included with the purchase of the app.\n\nIt is as easy to use as making lists.\n\nNeutralize chaos, and recapture your time and focus. Your life is complicated enough as it is. The tool to manage your life shouldn't be. The Hit List keeps things simple by not forcing you to learn a system. It can be as simple as just keeping a list of things to do as you would on a piece of paper. However, if you do use a task management system such as Getting Things Done by David Allen, The Hit List is flexible enough to support you.\n\nCapture and forget what you need to do later with The Hit List and get your \"now\" back, with confidence.\n\nThe Hit List includes sync service to sync with The Hit List for iPhone and The Hit List for Apple Watch (sold separately on the iOS App Store) or other Macs.", - "currency": "USD", - "artistId": 404126469, - "artistName": "Karelia Software LLC", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 49.99, - "bundleId": "com.potionfactory.TheHitList", - "version": "1.1.32", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/4c/c2/59/4cc25958-545b-21be-4358-81949570b2c9/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/ad/c7/0f/adc70f0a-beb9-92a9-5280-14b2d6ecd215/mzl.rfwynuee.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple4/v4/b1/de/d9/b1ded952-9ca7-fdd5-fb5b-8c2a706843d2/mzl.kwhtbdtp.tif/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/32/97/39/32973957-855d-8d20-c0c2-04877a933a99/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/b4/32/c7/b432c76a-1def-d380-062d-928571cc50cd/mzl.bqwyjhwr.tif/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/delullo-software-llc/id409729333?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "All Things Money", - "trackId": 782925777, - "sellerName": "DeLullo Software, LLC", - "releaseNotes": "The All Things Money 1.4.0 update improves functionality and stability, and is recommended for all users.\n\nNew features include:\n\n- Enabled QFX files with transactions to be imported.\n- When importing a file, if transaction categories are not specified then they will be automatically set to the last category used by the payee.\n- Added stock market technical indicators for Aroon, Money Flow, Relative Strength, and Fast Stochastic.\n- Added the ability to simultaneously view weekly and daily stock technicals.\n\nStability improvements include:\n\n- Added support for importing files with international character encodings.\n- Fixed two bugs that affected printing functionality.\n- Fixed QIF exports, which incorrectly had a percent symbol in the date field.\n- Fixed international currency formatting of the payment and deposit columns.\n\nFor detailed release notes, please visit: http://www.delullosoftware.com/apps/AllThingsMoney/ReleaseNotes.pdf", - "primaryGenreId": 6015, - "primaryGenreName": "Finance", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-02-07T23:58:42Z", - "genreIds": [ - "6015", - "6000" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2015-09-15T13:07:44Z", - "trackCensoredName": "All Things Money", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "1456751", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-money/id782925777?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Money (ATM) was created with the mindset that good finance software doesn’t need to be expensive, to contain advertisements, or to log into your bank account. It should be comprehensive and provide interconnected services. It should also be efficient by supporting multiple input formats, auto-completing certain fields, and automatically logging recurring transactions. ATM delivers on these goals and more.\n\nIf you are curious, please give it a try. Download All Things Money Lite for free to determine whether ATM is a good fit for you. When you want to upgrade, use the export and import menu options to transfer data between apps.\n\nATM capabilities include the following.\n\nAccount Dashboard\n- The innovative layout gives perspective to all of your liquid assets.\n- Track bank, credit card, stock market, and U.S. savings bond accounts.\n- Calculate net worth.\n- Sort transactions.\n- Split transactions.\n- Add memos to transactions.\n- Import bank, credit card, and stock market transactions from QIF and CSV files.\n- Import bank and credit card transactions from OFX files. (At this time, stock market transactions are not supported via OFX imports.)\n- Import U.S. savings bonds from treasurydirect.gov HTML files.\n- ATM auto-fills payee names and categories.\n- The bank-account paradigm enables you to earmark money.\n\nBill Planner\n- Track payments and deposits.\n- Track automatic stock market trades that are typical of 401k-type accounts.\n- Elect to automatically move up bill dates to avoid weekends and U.S. holidays. \n- ATM supports the following payment intervals: daily, weekly, bi-weekly, semi-monthly, monthly, bi-monthly, quarterly, once every four months, semi-annually, annually, bi-annually.\n\nStay on Budget\n- Create a budget based on transactions from the prior year.\n- Update budget goals based on your expected bills and deposits.\n- Update actuals from account transactions.\n- ATM supports annual, monthly, and custom-period budgets.\n- ATM provides color-coded alerts to track spending excess.\n\nPlan for Retirement\n- Seed the retirement calculator with your account information.\n- Calculate how much money you will have when you retire.\n- Calculate the annuity that your retirement will generate.\n- ATM also includes a general-purpose, compound-interest calculator that has six unique equations and two combination options for expediency. This calculator enables reverse computations; it calculates how much money you need to retire based on your desired annuity, interest rate, and life expectancy.\n\nMonitor the Stock Market\n- Create a watch list that saves stock symbols and time intervals.\n- Display a stock’s price and volume alongside technical indicators like Bollinger Bands and Moving Average Convergence Divergence (MACD).\n- ATM supports daily and weekly stock charts.\n\nMortgage Analysis\n- Create an amortization table and track your mortgage balance as you pay down principal. \n- Compare the costs of buying and renting.\n\nInventory Tracking\n- Keep track of your collections.\n\nCreate Reports\n- Generate plots of net worth and stock prices.\n\nMinimum requirements: screen resolution of 1280 x 800 or higher.", - "currency": "USD", - "artistId": 409729333, - "artistName": "DeLullo Software, LLC", - "genres": [ - "Finance", - "Business" - ], - "price": 9.99, - "bundleId": "com.delullosoftware.AllThingsMoney", - "version": "1.4.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/35/ab/f3/35abf398-3d14-0260-8aa0-94c8fe338524/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/22/8a/50/228a50d9-e1c0-4c13-2dab-720277a5d135/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/hypercritical-llc/id1493996621?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "SwitchGlass", - "trackId": 1498546559, - "sellerName": "Hypercritical LLC", - "releaseNotes": "• Minor layout adjustments to the Preferences and Exclude windows.\n• Updates to improve support for future operating systems.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-02-12T08:00:00Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2020-07-24T16:20:19Z", - "trackCensoredName": "SwitchGlass", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "2955121", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/switchglass/id1498546559?mt=12&uo=4", - "trackContentRating": "4+", - "description": "SwitchGlass adds a dedicated application switcher to your Mac. You can customize its appearance, size, and position on each attached display, including hiding it on selected displays. Use it to bring one or all of an app’s windows to the front, or as a drag-and-drop target to open files.\n\nThe following app switcher attributes can be customized individually for each attached display:\n\n• Visibility - Permanently hide, show, or auto-hide the app switcher. Auto-hide keeps the app switcher hidden until the mouse cursor touches the region of the screen edge where the app switcher is positioned.\n• Position - Move the app switcher to one of eight different positions on each screen.\n• Orientation - Make the app switcher horizontal or vertical in a given position.\n• Material - Change the appearance of the app switcher background to one of four possible choices (see screenshot above) including a mode where it automatically matches the system appearance (e.g, dark when dark mode is active, light otherwise).\n• Corner Style and Radius - Choose from three corner styles: square, circle, or “squircle” (a “continuous” rounded corner style). The corner radius can also be adjusted.\n• Icon Size - Adjust the size of app icons in the switcher from 16 to 256 points.\n• Icon Padding - Adjust the padding between icons from 0% to 100% of the configured icon size.\n• Margin - Adjust the amount of space around the app switcher.\n\nAdditionally, the following global settings may be customized:\n\n• App Sort Order - Sort apps by name or launch order, descending or ascending.\n• App Filtering - Show all apps or just those with windows on the current display.\n• Menu Bar Icon - Choose from three possible menu bar icons.\n• App Exclusions - Select apps that should never appear in the app switcher.\n\nHere’s how the app switcher works in its default configuration:\n\n• Click an app icon to bring all windows from that app to the front.\n• Hold down the Shift key while clicking an app icon to bring just one window from that app to the front.\n• Hold down the Option key while clicking an app icon to hide the current app before bringing the clicked app to the front. (Option-clicking the currently active app will hide it.)\n• Hold down the Option and Shift keys while clicking an app icon to hide the current app before bringing just one window from the clicked app to the front. (Option-Shift-clicking the currently active app will hide it.)\n• Right-click (or Control-click) an app icon to activate a context menu from which you can perform any of the supported actions (described below).\n• Drag one or more files or folders onto an app icon to open those items with the app.\n• Hold down the Command key while clicking on an app icon to reveal the app in the Finder.\n\nThe click and Shift-click actions are configurable. Each may be set to any one of these actions:\n\n• Show All Windows - Activate an app and bring all its window to the front.\n• Activate - Activate an app and bring a single window to the front.\n• Reopen - Activate an app in a way that triggers its “reopen” handler. Apps can choose to do whatever they want in response to a “reopen” action, but most bring a single window to the front, opening a new window (or un-minimizing a window from the Dock) if none are open.\n• Reopen and Show All - This is the same as “Reopen” except that SwitchGlass will also bring all the app’s windows to the front after sending the “reopen” action.\n\nFor more information, click the SwitchGlass menu bar icon and choose “Help”, or visit https://hypercritical.co/switchglass/#help", - "currency": "USD", - "artistId": 1493996621, - "artistName": "Hypercritical LLC", - "genres": [ - "Productivity" - ], - "price": 4.99, - "bundleId": "co.hypercritical.SwitchGlass", - "version": "1.4.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/63/5c/bf/635cbf44-3c63-4737-28a7-b5290a7e32b5/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a6/a3/e6/a6a3e643-f2b1-1110-24a7-be0f1367c143/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/63/74/8f/63748fcc-365e-7a02-09d5-4107c770c6b7/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/46/98/bb/4698bb7e-13b7-684d-c0ab-e31400f65139/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/90/3c/17/903c1770-37ca-0c8a-e22d-bd43b703259a/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/abdusodiq-saidov/id600067303?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Things for Work - Templates", - "trackId": 1275071331, - "sellerName": "Abdusodiq Saidov", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-04-23T04:19:28Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-04-23T04:19:28Z", - "trackCensoredName": "Things for Work - Templates", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "11704916", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-for-work-templates/id1275071331?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Writing a smart cover letter or resume is not always an easy task. For most people, writing a good resume and letter is tough, and it takes time. \n\nWouldn't it be wonderful if you could figure out how to make a resume that would get you an interview almost every time you applied for a job?\n\nResume is not a log of your job history. It is not a summary of your skills. It is not going to automatically get you a job if you don't prepare them in proper ways.\n\nThis app will help you to prepare better cover letter and resume step-by-step.\n\nIt comes with sample Cover Letters and Resumes.\n\nFeel Free to contact us if you need any other templates!\n\n*** Microsoft Word and/or Pages app is required to work with and open templates ***", - "currency": "USD", - "artistId": 600067303, - "artistName": "Abdusodiq Saidov", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.iconshots.VectorClliparts", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/e3/c1/49/e3c1499b-0756-9e1e-51e3-ce92b9ce79b5/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/1f/89/e1/1f89e162-34a2-7bfd-41ef-975d13ca5d9b/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/30/97/85/30978568-5806-42e3-b541-8c1182f8a152/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/67/7d/36/677d368d-fc13-3fe4-4f47-4cd91c1d026e/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/3c/39/f0/3c39f04e-187e-4a97-10f7-2538625e7c6f/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/lucien-dupont/id292886202?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9", - "trackName": "Check Off - A Task Manager", - "trackId": 458418895, - "sellerName": "Lucien Dupont", - "releaseNotes": "- fixed a crash when deleting a URL in the notes area. Thanks to Reza for reporting it!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-08-31T07:11:42Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2020-06-22T23:20:32Z", - "trackCensoredName": "Check Off - A Task Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "6115501", - "sellerUrl": "http://www.chromedomesoftware.com/CheckOff/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/check-off-a-task-manager/id458418895?mt=12&uo=4", - "trackContentRating": "4+", - "description": "CheckOff is a task management application that runs as a stand alone application or a menu bar app. \n\nFeatures: \n\n• Your choice: runs in a menu bar mode so that your dock doesn't get cluttered up, or as a normal application.\n• Tasks can be placed within folders, and have labels attached to each task.\n• Tasks can be put inside other tasks\n• Tasks can have notes with plain text or rich text with fonts, colors, and sizes. \n• Notes can also contain links to websites or local files, allowing one click access to resources that can be used to complete the task. \n• Menu bar icon color can be customized.\n• Hot key support to open the main window.\n• Export your tasks as text, RTF, html and opml\n• Dark mode support for macOS Mojave and Catalina", - "currency": "USD", - "artistId": 292886202, - "artistName": "Lucien Dupont", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 4.99, - "bundleId": "com.chromedomesoftware.checkoff", - "version": "5.8.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/e2/c3/1e/e2c31ea8-a561-5200-57bb-ca3826c0f670/mzl.jantsuya.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple4/v4/a8/30/32/a830321b-5e72-23e3-eb23-7160c4832b19/mzl.agzqdpdf.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/83/34/45/83344504-6351-a6c7-5df4-27856a80a0bb/mzl.hckkslpf.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/dc/f6/02/dcf602c6-df13-f41f-26ff-69c9fb6beca9/mzl.rrlwevnr.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/d9/3d/94/d93d9406-9410-ef3b-8dae-28125d2dad8a/mzl.fkekfvts.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/doit-im/id350974724?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.7", - "trackName": "Doit.im", - "trackId": 533391459, - "sellerName": "Xu Zhe", - "releaseNotes": "Fixed the bug that the background color of Smart Add popup is black.\nFixed the bug that repeat tasks will be created before the repeat strategy start time.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-10-23T19:25:14Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-07-09T22:51:34Z", - "trackCensoredName": "Doit.im", - "languageCodesISO2A": [ - "EN", - "JA", - "ZH" - ], - "fileSizeBytes": "3971688", - "sellerUrl": "http://doit.im", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doit-im/id533391459?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Doit.im is the smartest way to manage your tasks with the implementation of Getting Things Done (GTD) methodology. It helps you efficiently handle your tasks, whether you are busy executives or smart staff.\n\nWe’ve redesigned the user interface to make it simpler and more convenient. The brand-new task view of Today and Next Actions makes our tasks more focused and organized. Doit.im supports for multi-platforms, including Web/iOS/Mac etc, with Mac version exclusive for the Pro members.\n\nFeatures:\n \n 1. Keep your tasks in sync with Doit.im Cloud to make you in control of your tasks anywhere, anytime.\n 2. Completely implement GTD theory.\n 3. Support multi-level views: goals, projects, tasks, subtasks.\n 4. Manually sort your goals, projects, next actions, subtasks and contexts.\n 5. Forward tasks to your companions and track the status of the tasks.\n 6. Support the customization of your avatar.\n 7. Support Daily Plan.\n 8. Support Daily Review and Statistics, which you can share in Twitter and Facebook.\n \n* Still use to do list? It's time to try GTD and enjoy the completely different upgrading!", - "currency": "USD", - "artistId": 350974724, - "artistName": "Doit.im", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.snoworange.Doitim-Mac", - "version": "4.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/81/95/41/819541ad-9e99-a68d-680a-f6787be06dfb/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/ab/bb/6d/abbb6dd6-e56c-3617-40f2-689b337d7acd/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/5a/d1/05/5ad1059c-7e90-34a4-a257-f5c21c40c065/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/86/1d/bc/861dbc65-4574-6ae2-4e69-eda306db4a45/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/20/c0/52/20c052a4-00fc-db15-3859-1bee09157fd0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/4e/c2/71/4ec27160-f36d-dd23-66d8-234d77ff0f74/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/ca/2b/36/ca2b36b9-656f-c556-372b-14cf2504e020/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/quantum-quinn/id284974414?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13", - "trackName": "JotNow - Sticky Note Manager", - "trackId": 402371718, - "sellerName": "Christopher Bess", - "releaseNotes": "Thanks to everyone who reported bugs and unexpected issues!\n\n- Improved email integration, fixes email note bug\n- Fixed accent character input bug\n- Other bug fixes and enhancements", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-01-02T01:23:15Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2019-07-05T19:09:03Z", - "trackCensoredName": "JotNow - Sticky Note Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "3795543", - "sellerUrl": "https://www.quantumquinn.com/overview/jotnow", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/jotnow-sticky-note-manager/id402371718?mt=12&uo=4", - "trackContentRating": "4+", - "description": "It's the Stickies-like app you have been waiting for all this time…\n\nPlace notes in notebooks and further organize them with categories. Resize, dock, and collapse notes to keep them out of your way but still in plain sight. Even use your favorite color to add some style to your notes. Why write down notes when you can have JotNow store, search, and organize them?\n\nOrganize your thoughts and find them just as fast. Move between notes just as easy as you create them. Find and display notes with a click of a mouse or the press of a key. So much effort has been put on functionality and simplicity you will wonder how you ever used your computer without JotNow installed.\n\nVideos available at: http://jotnow.com/overview/jotnow\n\nNote Features:\n• Notes can float on top of other windows\n• Syncs note tasks reminders (due dates) with Reminders.app\n• All notes support rich text (RTF). Bold, italic, different font styles, etc.\n• Auto-saves, never worry about notes being saved\n• Note tasks, each note has it's own list of to-dos\n• Auto-indent lists\n• Email note\n• Custom note colors and note transparency, style your notes\n• Dock and collapse notes\n• Resizable (with scrollbars)\n• Dock notes to any part or edge of your screen (multi-monitors supported)\n• Manage notes without using the mouse\n• Search within notes\n• Swipe/flick notes left or right to dock them (trackpad only, two or three fingers)\n• Text-to-speech support for all notes (notes can be read aloud by your Mac)\n\nNote Manager Features:\n• Search Notes\n• Import and export notes (txt, rtf, etc)\n• Drag selected text or existing RTF and Text files to create notes\n• Create notes from empty searches\n• Find recent notes quickly using search operators (:today, :recent, :new)\n• No mouse required to manage notes (keyboard friendly)\n\nGeneral Features:\n• Easy to use\n• Hide dock icon\n• Quickly search for notes from anywhere (using Open Notes window)\n• Can be used without the mouse\n• Print directly from notes\n• Unlimited notes\n• Unlimited notebooks and tags\n• Global/system keyboard shortcuts (hot keys) allow access to JotNow from any application\n• Note trash, safely delete any note, you can see it later, if needed\n• Dock and Status Bar menus to access notes\n• Keyboard user friendly (many helpful shortcuts), you can command key features without touching your mouse\n• Stable and reliable, your notes are safe\n• Resource friendly, low memory footprint\nand much more...\n\nOnline documentation and videos are available in our support section.\nhttp://jotnow.com/overview/jotnow/support/\n\nSoli Deo Gloria", - "currency": "USD", - "artistId": 284974414, - "artistName": "Quantum Quinn", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "com.quantumquinn.JotNowDesktop", - "version": "1.8.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/c6/6e/4c/c66e4c72-28ed-8b4d-be7f-a91b41d89b30/ba6f5198-ca10-4672-96b0-e58af9e56c9c_MacOS-1_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/0f/92/90/0f9290b2-c11b-ca5d-e772-e891860270b4/1da0f90b-57fe-46b7-9f8d-db0272ab6e9c_MacOS-2_en.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/a1/61/d7/a161d7a1-e500-c169-d240-0bcc6a453b6e/5c43427a-7d9a-4394-b1e2-0a8b030c7e6e_MacOS-3_en.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/38/00/53/380053ec-128f-ba09-2971-6ef163f34c2c/2d5e948e-badb-46d0-81be-97102ecccb9a_MacOS-4_en.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/a8/7b/4c/a87b4c75-e493-635c-6e8b-68a44168c552/bbd91ce7-f028-4434-989c-e7583bc636f7_MacOS-5_en.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/58/33/62/583362d6-3e4d-5bd8-0241-8db2481456e4/e33d62f5-aafb-4281-9f81-610723031b52_MacOS-6_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/12/f8/e1/12f8e1cb-c20f-e1d3-3b98-5448130d9654/83d068bb-a3d9-4c96-a43d-830a7bafd964_MacOS-7_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/37/b4/86/37b48654-a9b8-1903-209e-b950400875d0/c777c2c3-ace7-46ee-b26b-ba88d9aa7507_MacOS-8_en.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/tarasov-mobile/id547656503?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13.0", - "trackName": "Chaos Control™: GTD Task List", - "trackId": 1071244525, - "sellerName": "Dmitriy Tarasov", - "releaseNotes": "From now on projects with Due Dates are shown in the Daily Plan section of the app. You can switch off this feature in the Settings menu of the app if you prefer to see tasks only. \n+ various improvements and optimisations.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-01-20T20:29:38Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-09-01T21:01:42Z", - "trackCensoredName": "Chaos Control™: GTD Task List", - "languageCodesISO2A": [ - "EN", - "RU" - ], - "fileSizeBytes": "48546730", - "sellerUrl": "http://chaos-control.mobi/?utm_source=appstore&utm_medium=mac&utm_campaign=appstore_mac_ver_1", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/chaos-control-gtd-task-list/id1071244525?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Chaos Control is a task manager based on the best ideas of GTD (Getting Things Done) methodology created by David Allen. Whether you are running a business, launching an app, working on a project or simply planning your holiday trip, Chaos Control is a perfect tool to manage your goals, juggle your priorities, and organize your tasks to get things done. And the best part is, you can handle both heavyweight project planning and simple daily routine like shopping list management in one flexible app. Also, Chaos Control is available across all major mobile and desktop platforms with seamless sync.\n\nHERE IS HOW IT WORKS:\n\n1) MANAGE YOUR PROJECTS\nProject is a goal combined with a set of tasks you need to complete in order to achieve it. Create as many projects as you like to write down all the desired outcomes you have\n\n2) ORGANIZE YOUR GOALS\nCreate unlimited number of projects and group them by category using Folders\n\n3) USE GTD CONTEXTS\nOrganize tasks from different projects using flexible context lists. If you are familiar with GTD you would just love this feature\n\n4) PLAN YOUR DAY\nSet due dates for tasks and make plans for any particular day\n\n5) USE CHAOS BOX\nPut all the incoming tasks, notes and ideas into Chaos Box in order to process them later. It works similar to GTD inbox, but you can use it as a simple to-do list\n\n6) SYNC YOUR DATA\nChaos Control works on both desktop and mobile devices. Setup an account and sync your projects across all of your devices\n\nThis app is designed with creative people in mind. Designers, writers, developers, startup founders, entrepreneurs of all kinds and pretty much anyone with ideas and desire to make them happen. We combined the power of GTD with the convenient interface to help you with:\n• personal goal setting\n• task management\n• time management\n• planning your business and personal activities\n• building your routine\n• handling simple to do lists, checklists and shopping lists\n• catching your ideas and thoughts to process them later\n\nKEY FEATURES\n• Seamless cloud sync across all major mobile and desktop platforms\n• GTD-inspired Projects and Contexts supplemented with Folders, sub-folders and sub-contexts\n• Recurring tasks (daily, weekly, monthly and chosen days of the week)\n• Chaos Box - Inbox for your unstructured tasks, notes, memos, ideas and thoughts. Great tool for staying on track inspired by GTD ideas\n• Notes for tasks, projects, folders and contexts\n• Fast and smart search\n\nHave a productive day!\n\nTerms Of Use:\nhttp://chaos-control.mobi/toc.pdf", - "currency": "USD", - "artistId": 547656503, - "artistName": "Tarasov Mobile", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.tarasovmobile.ChaosControlMac", - "version": "1.10", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/6e/8d/56/6e8d56ea-494a-23ca-cfdd-0ba065d8e8f2/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/6e/ed/49/6eed4931-c254-abba-7823-1e8ebd7d652c/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/e8/77/3f/e8773fd7-7541-e9e4-2882-65c400434ad5/pr_source.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/68/62/71/686271c4-38df-ea7b-f95f-90fad57c6def/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6d/a5/53/6da55373-9f5e-671f-eaf4-7f32ad79123b/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a0/6c/07/a06c07bd-7542-f57e-92d0-49067c144111/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/68/1a/4d/681a4d23-d0df-ea50-8afa-0b2959640395/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/93/4f/86/934f86be-be87-63a1-af42-b1183dca6f93/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/1a/f4/77/1af4771a-e086-5085-d937-8458a2356e43/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/40/7d/ba/407dba14-e345-9644-8c49-6ea8d07ee73f/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/2a/3e/df/2a3edf37-3580-b911-ece9-b22afba71583/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/48/43/f0/4843f065-030a-9721-5d08-971021f3c922/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/37/db/25/37db2594-602c-d411-1826-fc0446632cd3/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/6c/8e/0a/6c8e0ab0-1519-474e-d7c4-c81e44be3925/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c1/72/4e/c1724e7a-07e9-b9ff-7e1a-3dc3296a70dd/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e1/4d/6d/e14d6dee-571b-a277-5a6f-4e8a1966b235/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/24/63/30/246330e7-7632-5f13-4288-ec4847c524fc/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/7f/12/47/7f124738-f363-4d4d-e35c-05777ee3e472/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/93/95/cf/9395cf14-4ae2-7303-61ac-d83a72c85af6/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/94/cb/2d/94cb2d43-9999-3eda-22ff-8eadf003b561/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/tiger-ng/id356989572?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.4", - "trackName": "Book Generator", - "trackId": 1471872257, - "sellerName": "Tiger Ng", - "releaseNotes": "New features:\n1. Import Microsoft Word Document\n2. Import Google Docs\n3. Import Google Sheets\n4. Import Google Classroom's assignment submissions by students\n5. CSV: start from scratch & import from clipboard\n6. Markdown: start from scratch & import from clipboard\n7. Bug fix", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-07-25T07:00:00Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-09-16T14:30:50Z", - "trackCensoredName": "Book Generator", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "16542720", - "sellerUrl": "https://getbookgenerator.blogspot.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 1, - "userRatingCountForCurrentVersion": 1, - "averageUserRating": 1, - "trackViewUrl": "https://apps.apple.com/us/app/book-generator/id1471872257?uo=4", - "trackContentRating": "4+", - "description": "Book Generator is a new way to convert OneNote notebooks, Trello boards, Word Documents, Excel spreadsheets, Google Docs, Google Sheets, Google Classroom, CSV and Markdown files into EPUB or PDF.\n\nCore Features\n* OneNote: each notebook represents a book, each section represents a group, each page represents a chapter.\n* Trello: each board represents a book, each list represents a chapter, each card represents an element.\n* Word: each heading represents a page.\n* Excel: each row represents a page, each record represents an element.\n* Google Docs: each heading represents a page.\n* Google Sheets: each row represents a page, each record represents an element.\n* Google Classroom: each classwork represents a book, each assignment submission represents a page.\n* CSV: each row represents a page, each record represents an element.\n* Markdown: convert to HTML\n* Support image, video and audio attachment.\n\nBasic Editing Features\n* Edit cover image.\n* Edit book style.\n* Configure book information.\n* Delete chapter.\n* Reorder chapter.\n\nThe generated book can be imported into Creative Book Builder for further editing.", - "currency": "USD", - "artistId": 356989572, - "artistName": "Tiger Ng", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.tigernghk.ios.bookgenerator", - "version": "1.2", - "wrapperType": "software", - "userRatingCount": 1 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/fb/ca/09/fbca09ec-b32e-57f0-1b1f-7d81d40a9151/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/98/82/fa/9882fa60-c5ee-1885-032b-a1c1b84e8b00/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/13/d4/c2/13d4c2b0-1a22-d904-e3e7-043f1e557fdd/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e1/d8/be/e1d8bee3-051b-6e5f-1e7d-27962dbf2def/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/haibo-tang/id1501309035?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "TomatoThings 2", - "trackId": 1501309036, - "sellerName": "Haibo Tang", - "primaryGenreId": 6002, - "primaryGenreName": "Utilities", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-03-03T08:00:00Z", - "genreIds": [ - "6002", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-03-03T21:17:35Z", - "trackCensoredName": "TomatoThings 2", - "languageCodesISO2A": [ - "EN", - "ZH" - ], - "fileSizeBytes": "4494776", - "sellerUrl": "http://ihugo.cc", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/tomatothings-2/id1501309036?mt=12&uo=4", - "trackContentRating": "4+", - "description": "一款结合TODO和番茄工作法的工具。让您能够专注的进行工作。第一个版本包含一些基础的功能比如TODO,番茄工作法,统计。\n\n这是我个人的第一款应用。我想长期进行维护。所以您如果有任何想法,请通过邮件直接和我进行联系。\n\n联系方式: dusty@vip.qq.com", - "currency": "USD", - "artistId": 1501309035, - "artistName": "Haibo Tang", - "genres": [ - "Utilities", - "Productivity" - ], - "price": 0.00, - "bundleId": "cc.ihugo.app.TomatoThings2", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple/5e/01/c1/mzl.ycuysdmk.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple/9f/ea/f4/mzl.kllojlbn.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple/d8/a8/a1/mzl.rdtnfjfa.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple/e4/aa/fe/mzl.qaehltfv.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/6f/e8/97/mzl.vhjtxmra.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/focus-home-interactive/id406173890?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "The Next Big Thing", - "trackId": 430203668, - "sellerName": "Focus Home Interactive SAS", - "releaseNotes": "Update 1.1: The game is now playable with voices in English, French and German.", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-02T07:00:00Z", - "genreIds": [ - "6014", - "7009", - "6016", - "7002" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2011-06-17T00:48:43Z", - "trackCensoredName": "The Next Big Thing", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ES" - ], - "fileSizeBytes": "5271779975", - "sellerUrl": "http://www.thenextbig-game.com/", - "contentAdvisoryRating": "12+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-next-big-thing/id430203668?mt=12&uo=4", - "trackContentRating": "12+", - "description": "The Next BIG Thing is the new, hilarious adventure gem from the creators of Runaway. A great adventure game in high definition, loaded with laughs, tributes, mysteries and wacky puzzles!\n \nThanks to a production worthy of a great animated movie, an awesome soundtrack, delightful dialogue and a great art style, The Next BIG Thing will make you live an unforgettable adventure which brilliantly takes players across the fantastic movie genres of a totally crazy Hollywood.\n \nHollywood\nWhat if horror movies' monsters were actually played by real monsters? And what if they were now forced to play in movies for kids, romantic comedies or even musicals? And what would happen if, eventually, they were to rebel?\n \nIn that context, Liz Allaire, talented journalist who can't count up to 4, and Dan Murray, a tough macho who can't stand beetles, attend the horror movies award ceremony… there is the starting point of an incredible story, full of twists and turns! Help Liz and Dan solve the numerous mysteries of an amazing adventure that you won't forget any time soon!\n \n- Solve numerous puzzles and mysteries\n \n- Meet wacky characters and monsters\n \n- Explore dozens of colorful places\n \n- A production worthy of a great animated movie\n \n- Movie-like musical score and English voice-over", - "currency": "USD", - "artistId": 406173890, - "artistName": "FOCUS HOME INTERACTIVE", - "genres": [ - "Games", - "Family", - "Entertainment", - "Adventure" - ], - "price": 9.99, - "bundleId": "com.focus-home.TNBT", - "version": "1.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/24/cb/ac/24cbac33-03a6-c33c-3c7e-f1d3acff2d74/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple69/v4/e8/08/8f/e8088f7d-c688-7663-b738-efd7b64ce6ad/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/f2/c3/32/f2c3323c-db7a-5015-5a36-8cb4ee8d6d5d/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple49/v4/be/98/fd/be98fd4a-75e9-22e9-819b-c006d09badef/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/explain-3d/id1019479194?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "Explain 3D How things work", - "trackId": 1020146983, - "sellerName": "ePic design s.r.o.", - "releaseNotes": "- added settings so you can adjust move, rotation and zoom speed\n- small performance fixes", - "primaryGenreId": 6017, - "primaryGenreName": "Education", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-07-30T05:32:31Z", - "genreIds": [ - "6017", - "6014", - "7015" - ], - "formattedPrice": "$3.99", - "currentVersionReleaseDate": "2016-09-03T02:14:31Z", - "trackCensoredName": "Explain 3D How things work", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "90413684", - "sellerUrl": "http://explain3d.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/explain-3d-how-things-work/id1020146983?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Explain3D success:\n***TOP 25 GEW - Global start up competition finalist 2013 (http://www.cnbc.com/id/101168347/page/6)***\n\nSimulations:\n- Car workshop - 4 stroke engine, Car clutch, Manual transmission, Disc brakes, Ignition system\n- Discover the Universe - ISS space station, Space shuttle, Solar system\n- Electricity around us - Hydroelectric dam, Wind turbine, Nuclear power plant\n- Transport simulations - Railroad switch, Submarine, Jet engine\n- Our homes - Lock and key, Elevator, Toilet, Desk Lamp\n- Tools - Jackhammer, Hand pump\n\nExplain3D is system of interactive simulations, that can help you to explain, how things work. Simulations are built in interactive 3D environment, which brings fun to education and helps understand the stuff. \n\nFeatures:\n- Interactive 3D environment\n- simple and intuitive interface\n- description of main parts of simulations / objects\n- nice 3D models and animations\n- great to make education more engaging and interesting", - "currency": "USD", - "artistId": 1019479194, - "artistName": "Explain 3D", - "genres": [ - "Education", - "Games", - "Simulation" - ], - "price": 3.99, - "bundleId": "com.Explain3D.HowThingsWork", - "version": "2.4", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple18/v4/a8/55/f1/a855f17c-73a0-3266-561f-c1c2c7fd0dc7/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/34/bc/d4/34bcd4cf-4143-9f6c-04d5-de3556bb75a8/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple60/v4/c9/ed/2f/c9ed2f0a-1064-2e6f-7e84-49f9e3d0d79e/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/4b/1c/fe/4b1cfee9-6a9b-4fe9-e9fa-41147ba4bc93/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple60/v4/ea/a4/37/eaa4378b-c323-7876-ea9b-992554c58208/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ithought-llc/id936777941?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9", - "trackName": "Actio: The App for Action.", - "trackId": 994990008, - "sellerName": "iThought LLC", - "releaseNotes": "Bug fixes: Drag 'N Drop Bug & Minor refresh bug.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-06-24T18:33:48Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2017-12-05T23:51:13Z", - "trackCensoredName": "Actio: The App for Action.", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "29735536", - "sellerUrl": "http://www.iThought.biz", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/actio-the-app-for-action/id994990008?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Actio: The App for Action\n\nActio is an action tracking app - great for project managers and home users alike. \n\nIt is built to be fast and flexible, intuitive to use and powerful enough for even the largest projects.\n\nYou can try the full version of Actio via in in-App free one month trial and then purchase either a full license (US$19.99*) or a monthly (US$0.99*) or yearly (US$9.99*) auto-renew subscription**. We hope you love using it.\n\nACTIO HELPS YOU:\n\nManage, organize and prioritise your actions\n• Create user-defined Flags.\n• Organize with user-definable Indexing.\n• Filter by Flag, Priority, Owner or item color to quickly focus on the important actions.\n• Sort by Index or Due Date. Hide unimportant actions.\n• Easily Promote and Demote actions into Projects and sub-Projects.\n• Define action owners in Actio, or copy from your Apple Contacts.\n\nGet stuff done quickly and efficiently\n• Fast mouse-free input of large scale projects with multiple levels of sub-projects.\n• Item and Note windows with full typographical control.\n• Unlimited Undo and Redo.\n\nTrack actions with robust detail\n• DueDate automatically colors actions, rolling them up to parent projects to easily see those with priority\n• Per-action customizable RAG (Red/Amber/Green) early warning periods.\n• Use multiple tracker views to show Lists, Details and Pageviews.\n• Rapid drill-down/up into sub-projects.\n• Drag & Drop and Drag & Duplicate actions to quickly reorganize.\n• Drag in Emails and WebPages to keep all your project information in one place.\n• Unlimited notes attached to every action.\n• Search across all title, detail and notes fields.\n• Receive Apple notifications when the state of each action item changes.\n\nGet your small business off the ground...\n• PageView window gives WYSIWYG display and control over included content. Printer ready.\n• Full-screen mode. Meeting ready.\n• File based to allow easy sharing of your project with others (e.g. via email).\n• Print Preview functions of all three views.\n\nOr reign in your priorities at home\n• DueDates, Repeating Dates and Anniversaries.\n• Check off important personal events, holidays and more\n\nUnlock Actio Unlimited via In-App-Purchase for:\n• All Free features\n• Unlimited actions and sub-actions (free version has 25 items).\n• Printing of all three views.\n• Multiple Open Projects (free version has one open project).\n• Project Tabs.\n• Drag and Drop between open Projects.\n• Drag and Duplicate between open Projects.\n• Search all open Projects.\n• Share and sync projects on file sharing servers (e.g. iCloud, Dropbox, GoogleDrive etc).\n• Save detail text to multiple formats (pdf, txt, html, doc etc)\n\nUse Actio for your day-to-day management to ensure that nothing falls through the cracks.\n\n-----------------\n* Denotes pricing on the US App Store. \n-----------------\n**Actio Unlimited Monthly and Yearly are auto-renewing subscriptions and automatically renew unless auto-renew is turned off at least 24-hours before the end of the current period. Subscriptions may be managed by the user in iTunes Account Settings.\n-----------------\nTerms of Use: www.iThought.biz/terms", - "currency": "USD", - "artistId": 936777941, - "artistName": "iThought LLC", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "biz.iThought.ActioMAS", - "version": "1.4.5", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/0c/8f/c8/0c8fc884-c87a-7004-f44a-e3b333222e38/40f0748e-65b8-412e-b055-c74f457a8be7_i8-1.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/6d/12/99/6d12990d-2c4a-a09f-17a2-ab53b05004ba/91a4eb3f-6d59-480b-8926-a10b54dc200c_i8-2.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/cf/d0/76/cfd07658-e436-69be-dad1-f40a1d858855/97bc8ecd-7411-4eae-8a79-ccb18eecd5bf_i8-3.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/91/02/8a/91028ab6-979a-04ec-0e06-3cfc9957e754/33ef7be7-bd63-4d0e-a8e3-05bfed454285_i8-4.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/b9/a1/94/b9a194da-8062-7139-429a-c41336f46052/0a8e5d96-36ef-4e68-8460-060fb6532871_i8-5.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/83/91/27/8391279d-43e4-399b-373a-5ea2740da32b/0bd98917-79f5-436a-9e59-3aae11a966c4_i8-6.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/0b/8f/6e/0b8f6e54-2469-388c-6113-05e6c5a3692a/92d898e0-06ba-45b3-8ccd-3da8b20df8de_i8-7.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/5f/db/33/5fdb336c-82ed-fa17-a821-90027a51db67/8a478a41-3be5-4d73-a2aa-1e365a146e5f_i8-8.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/dd/9f/e4/dd9fe411-85c4-298b-60da-4b373ee5740c/05a10972-3a84-447c-9ea5-bf44268efefa_i8-9.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/25/2f/0c/252f0c7d-4c9f-d411-bf16-d415ecec293f/d1425972-f4d6-46d1-a868-f09a9f6572ef_i8-10.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/bd/50/cf/bd50cf55-97ad-adec-55ec-e43ed2f26b44/594b53a3-48bd-4745-a1a3-d6457e03a426_ipad-v-1.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/69/01/0b/69010bd8-f9ce-05a4-80ac-beba32694866/861e0887-e2d3-40fe-bfba-041ab513fd50_ipad-v-2.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/91/16/ea/9116eaaa-3ded-38b1-a4d8-e1d89313b031/c156ce19-989c-4f5b-93bc-a9307fa0a9be_ipad-v-3.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/4a/cf/42/4acf4251-f8da-bdea-724c-5384be8ddb87/fb9c1e3d-856a-4fa4-9314-ea7ae0f790b4_ipad-v-4.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/ca/39/35/ca393565-6225-5729-18d2-3ea2d23abc52/4d9c971d-6096-459e-9ee2-f0af77ff6bfb_ipad-v-5.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/bc/c0/82/bcc0822b-ce04-33ff-d9bd-2380477b52a0/7795ceea-43bf-41c6-828e-5bd91d6bf4e7_ipad-v-6.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/d4/62/8e/d4628ebd-2504-6506-1e60-637beda2fcbb/ae1d6142-b2a1-45e3-8a3d-30984c075daf_ipad-v-7.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/fa/d3/43/fad3436d-30c4-daff-dc10-5c7d4d5da752/f90faa3c-4351-43bc-be53-6da74403e829_ipad-v-8.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/63/e4/d0/63e4d099-5ba4-ac02-6ac6-c56969003f66/c46efc17-1b62-43a9-8974-0bede1a4766f_ipad-v-9.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/easylife-srls/id1459685996?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "easyPlanner 3 - Task manager", - "trackId": 1459685997, - "sellerName": "easyLife Srl Semplificata", - "releaseNotes": "Added \"Urgent\" quick button in task's page.\n\n• Fixed minor bugs.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-04-30T21:02:33Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-25T19:35:33Z", - "trackCensoredName": "easyPlanner 3 - Task manager", - "languageCodesISO2A": [ - "EN", - "FR", - "IT", - "RU", - "ZH" - ], - "fileSizeBytes": "90359808", - "sellerUrl": "https://www.easylife.biz", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.888889999999999957935870043002068996429443359375, - "userRatingCountForCurrentVersion": 9, - "averageUserRating": 4.888889999999999957935870043002068996429443359375, - "trackViewUrl": "https://apps.apple.com/us/app/easyplanner-3-task-manager/id1459685997?uo=4", - "trackContentRating": "4+", - "description": "easyPlanner is the innovative app that will manage for you the things to do using the tasks and projects and the things you need to remember using memos. You can manage your appointments and tasks on multiple levels (multi-levels) as your mind would! You can also monitor your progress with the Reports and see how productive you are. If you are not happy with what you are producing you can try to increase your productivity by using our FocusTimer, a tool that will allow you to maintain concentration and carry out more tasks simultaneously.\neasyPlanner aims to make your life easier and “to work” in place of your memory. Getting organized means having more free time for you and for those you love, dedicating the time you have saved to create unforgettable moments with the people of your life.\n\nMANAGE THE THINGS YOU HAVE TO DO AND THE THINGS YOU HAVE TO REMEMBER\u2028\n\nWe provide you tasks, projects and memos management in a single app.\n\nTASKS\n\neasyPlanner allows you to manage the “things you need to do” through our “Tasks”. Each task can contain infinite sub-tasks (multi-tasks) and can be grouped into folders. \n\nPROJECTS\n\nWhen a certain number of tasks are closely related to each other and they concern significant commitments, it is possible to manage them as a single project. Even the project, like the task, can contain infinite sub-tasks (multi-tasks).\n\nMEMOS\n\neasyPlanner allows you to manage the “things you need to remember” both in free format and in a structured format using memos.\n\nMONITOR YOUR REPETITIVE TASKS WITH REPORT\u2028\n\nThe \"Report\" function was born of the need to represent a set of complex data or complex tasks in the form of a table so that they can be printed and read more easily. Thanks to this function you will be able to graphically visualize a set of recurring tasks in a simple way and you will be able to monitor the progress of these tasks daily.\n\n\nINCREASE YOUR PRODUCTIVITY WITH FOCUS TIMER\n\nFocus Timer has been designed after a careful study based on several time management theories. It is a very advanced “time management” system which consists in dividing time into intervals, each of which is characterized by a task that you have to do. This division leads the subjects who use it to be much more efficient, to better manage their time and to constantly develop their mental abilities.\n\nAGENDA\n\u2028Thanks to the Agenda view easyPlanner will allow you to keep track of your schedule and your appointments at any time.\nAgenda will also integrate the Apple calendar. \nYou can add your events in the calendars directly from here in a practical and fast way.\nYou can decide which calendars to see and which to hide.\n\nICLOUD SYNC\n\nWith easyPlanner you can archive your photos, your documents, your tasks, etc. on ICloud and you can synchronize all your devices automatically and simultaneously. Regardless of the device you are using (IPhone or IPad), you will always have the latest version of your documents available.\n\nDELEGATE\n\nDelegate urgent tasks to other people by sharing them by email.\n\nAND SO MUCH MORE…\n\nYou will find out many more features trying our app or going to our website www.easyLife.biz/easyPlanner.\u2028\nCHOOSE THE VERSION THAT BEST SUITS YOU AND YOUR NEEDS\n\nTry easyPlanner – Task manager for free.\u2028You can use this version without time limits and in all its functions in order to appreciate all its features. However this version only allows 40 entries.\u2028If you start to get tight, upgrade to the easyPlanner – PRO version, available via in-app purchase or as a stand-alone version.\u2028easyPlanner – PRO Version allows you to get an unlimited number of entries, giving you the opportunity to always have everything you need with you and to take advantage of Reports over time. \nOne-time purchase. Without subscription. \n\nIf you have any question, any suggestions or if you want to tell us what you thing of easyPlanner you can contact us:\u2028\n\nwww.easylife.biz\nsupport@easylife.biz\n\nTerms of service: https://www.easylife.biz/terms/", - "currency": "USD", - "artistId": 1459685996, - "artistName": "easyLife Srls", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.easyPlanner", - "version": "3.0.6", - "wrapperType": "software", - "userRatingCount": 9 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e7/cb/a4/e7cba421-087e-0923-8e7f-3fb2fcff872c/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/2f/2a/e9/2f2ae9f8-6954-dc16-81b8-cf44deb66ac0/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/e3/aa/5c/e3aa5ca0-2625-d0a3-6e30-8d22d058c810/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/6b/5f/14/6b5f14e7-95a7-3c3d-7532-817d844d5f7a/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/01/13/c701130f-ca88-ee48-6f49-dbbad63bcdd3/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/64/3a/4b/643a4b2d-3661-8203-7325-fc47d208f0d7/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/b7/7d/9c/b77d9c68-3707-078b-596e-53d82cc82528/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/f0/f8/adf0f884-e81f-567b-53ec-59d860132354/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/%E6%9D%A8-%E7%94%B0/id1228008538?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "CleanTodo • Simplest Task List", - "trackId": 1228466824, - "sellerName": "杨 田", - "releaseNotes": "Thank you for downloading Clean Todo! Here's what's new: \n- Bug fixes and improvements. \n\nPlease contact us if you have any suggestions. support@cleantodo.com", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-06-22T00:46:33Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-04-25T21:38:32Z", - "trackCensoredName": "CleanTodo • Simplest Task List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "3571392", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/cleantodo-simplest-task-list/id1228466824?mt=12&uo=4", - "trackContentRating": "4+", - "description": "CleanTodo is a very simple app for managing your to-do list.\nThe app provides you two separate lists. One list records all your need to do. The other list only records your important tasks. \n\n\"The platform has a tidy, minimalist look that allows users to keep all of their tasks in one convenient place.\" - The American Genius\n\n\"On top of a simple operation, it also offers you the cleanest interface.\" - KOCPC,TW\n\n\"A simple to-do work list makes writing down important work items easy.\" - iPhone, iPad玩樂誌 Vol.49.\n\n\nThe Benefits of Clean Todo:\n\n• Because recording your tasks has become so simple, you will be happy to write things down and will never forget what you need to do.\n• You decide what you should do every day, rather than being forced to plan ahead and have your software urge you.\n• Avoid wasting time managing your to-do list and save time doing something meaningful.\n• Look at your weekly review and feel a great sense of accomplishment.\n• The simple and elegant software interface accentuates your fine taste.\n\nFeatures:\n\n- The cloud sync feature ensures your data remains exactly the same across devices.\n- No matter how many tasks you have, they are simply divided into to be done now and to be done later. This allows you to check your to-do list and place the important things into your \"Today\" list. Do the most important thing every day.\n- Use tags to distinguish between different categories of tasks. These tags are very easy to create and use.\n- Flip Card View: You can flip through and complete tasks one by one. This helps you focus on the present task.\n- You can create reminders that will be added directly to Calendar, which gives you accurate alerts even without Internet connection.\n- Weekly Review: helps you to check the tasks you’ve completed over the week and export a report easily.\n- Drag&Drop to reorder tasks.\n- Quickly add task, the default shortcut is Ctrl+Cmd+Z.\n\nUpgrade to premium membership to gain access to all the features. Once subscribed, all these features will be available to you for use on all platforms without additional costs.\n\nPremium Subscription: $12.99/Year\n\n• Subscription is auto-renewable which means that once purchased it will be auto-renewed every year until you cancel it 24 hours prior to the end of the current period. \n• Your subscription will be charged to your iTunes account at confirmation of purchase and will automatically renew unless auto-renew is turned off at least 24 hours before the end of the current period.\n• Current subscription may not be canceled during the active subscription period; however, you can manage your subscription and/or turn off auto-renewal by visiting your iTunes Account Settings after purchase.\n\nPrivacy Policy: https://www.cleantodo.com/privacy\nTerms & Conditions: https://www.cleantodo.com/terms", - "currency": "USD", - "artistId": 1228008538, - "artistName": "杨 田", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.cleantodo.osx", - "version": "1.10", - "wrapperType": "software", - "userRatingCount": 0 - } - ] - } + "resultCount": 50, + "results": [ + { + "screenshotUrls": [ + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/6f/4c/e5/6f4ce5d6-7caa-d1eb-bbc9-86558e97d2ba/pr_source.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/92/b4/f8/92b4f8f5-f133-abd8-db17-135ac27bb1fa/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/72/63/63/726363b9-45ff-f93e-975c-fb69836eaf1a/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/29/fa/63/29fa63e3-3cb2-8b8a-8541-31fa9b7ef27f/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/da/17/5f/da175f95-c2cd-e5df-8cbc-d800d6770c64/pr_source.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/f1/82/37/f182376c-4f25-6dbb-c6a8-5e6c1c617620/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/60x60bb.png", + "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/512x512bb.png", + "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/cultured-code-gmbh-co-kg/id284971784?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.13.0", + "trackName": "Things 3", + "trackId": 904280696, + "sellerName": "Cultured Code GmbH & Co. KG", + "releaseNotes": "• Moved the database file to a new location (now at /Library/Group Containers/).\n• Increased the clickable area of items in the sidebar.\n• Improved the formatting of years in Japanese.\n• Fixed some crashes that could occur when hitting Cmd+[ or ] in Quick Entry while the When popover was visible.\n• Updated the crash reporter.\n• Some sync improvements.\n\n\nNEW IN 3.12\n\nWe’re excited to release Things 3.12 – a big update for our Watch app!\n\nWe’ve entirely rebuilt its foundation to allow it to sync and operate without your phone being nearby. We’ve also taken this opportunity to add some often-requested features to the app. For more information about this release, please visit our blog: thingsapp.com\n\nThere are no huge changes in this release for Mac, but there’s one great new feature you should know about: you can now edit the Tags or Deadlines of collapsed to-dos – even for multiple to-dos at once – by hitting Cmd+Shift+T or D. It’s super convenient :)", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-05-18T16:42:04Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "$49.99", + "currentVersionReleaseDate": "2020-08-04T07:57:44Z", + "trackCensoredName": "Things 3", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "JA", + "RU", + "ZH", + "ES", + "ZH" + ], + "fileSizeBytes": "17474797", + "sellerUrl": "https://culturedcode.com/things/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/things-3/id904280696?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Get things done! The award-winning Things app helps you plan your day, manage your projects, and make real progress toward your goals.\n\nBest of all, it’s easy to use. Within the hour, you’ll have everything off your mind and neatly organized—from routine tasks to your biggest life goals—and you can start focusing on what matters today.\n\n“Things offers the best combination of design and functionality of any app we tested, with nearly all the features of other power user applications and a delightful interface that never gets in the way of your work.”\n—Wirecutter, The New York Times\n\n\nKEY FEATURES\n\n• Your To-Dos\nYour basic building block is the almighty To-Do—each a small step toward a great accomplishment. You can add notes, tag it, schedule it, and break it down into smaller steps.\n\n• Your Projects\nCreate a Project for any big goal, then add the to-dos to reach it. Use headings to structure your list as you outline your plan. There’s also a place to jot down your notes, and a deadline to keep you on schedule.\n\n• Your Areas\nCreate an Area for each sphere of your life, such as Work, Family, Finance, and so on. This keeps everything neatly organized, and helps you see the big picture as you set your plans in motion.\n\n• Your Plan\nEverything on your schedule is neatly laid out in the Today and Upcoming lists, which show your to-dos and calendar events. Each morning, see what you planned for Today and decide what you want to do. The rest is down to you :)\n\n\nMORE THINGS TO LOVE\n\nAs you dive deeper, you’ll find Things packed with helpful features. Here are just a few:\n\n• Reminders — set a time and Things will remind you.\n• Repeaters — automatically repeat to-dos on a schedule you set.\n• This Evening — a special place for your evening plans.\n• Calendar integration — see your events and to-dos together.\n• Tags — categorize your to-dos and quickly filter lists.\n• Quick Entry — create to-dos from anywhere, as soon as the thought hits you.\n• Quick Find — instantly locate to-dos, headings, or tags.\n• Type Travel — jump from list to list with your keyboard; just start typing!\n• Mail to Things — forward an email to Things; now it’s a to-do.\n• And much more!\n\n\nMADE FOR MAC\n\nThings is tailored to the Mac with deep system integrations as well. A great example is Quick Entry with Autofill: a shortcut that grabs content from other apps and adds it to Things for you, such as a link to a website or an email you want to get back to.\n\nYou can also enjoy a beautiful dark mode at sunset, connect your calendars, enable a Things widget, use your Mac’s Touch Bar, import from Reminders—Things can do it all! There’s even AppleScript support if you need powerful automation.\n\n\nSTAY PRODUCTIVE ON THE GO\n\nThings has full-featured apps for iPhone, iPad, and Apple Watch as well (sold separately). All your devices sync seamlessly via our free Things Cloud service. It’s great to have everything at your fingertips when you need it!\n\n\nAWARD-WINNING DESIGN\n\nMade in Stuttgart, with two Apple Design Awards to its name, Things is a fine example of German engineering: designed, not only to look fantastic, but to be perfectly functional as well. Every detail is thoughtfully considered, then polished to perfection.\n\n“It’s like the unicorn of productivity tools: deep enough for serious work, surprisingly easy to use, and gorgeous enough to enjoy staring at.”\n—Apple\n\n\nGET THINGS TODAY\n\nWhatever it is you want to accomplish in life, Things can help you get there. Install the app today and see what you can do!\n\nVisit our website now and get a free 15-day trial for your Mac: thingsapp.com\n\nIf you have any questions, please get in touch. We provide professional support and will be glad to help you!", + "currency": "USD", + "artistId": 284971784, + "artistName": "Cultured Code GmbH & Co. KG", + "genres": [ + "Productivity", + "Business" + ], + "price": 49.99, + "bundleId": "com.culturedcode.ThingsMac", + "version": "3.12.6", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c9/5d/af/c95daf17-c405-56f0-90f5-9411828e44d2/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/94/32/3b/94323b37-f81b-7ba8-a280-b951e7e840de/pr_source.jpg/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/82/f1/35/82f1356d-1e68-8f9d-3967-566e256f9265/pr_source.jpg/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/dc/ad/83/dcad839b-7705-e4c0-180a-2f97cb68054d/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/21/50/6a/21506a09-c48c-d25e-dfa5-c0d6aa4cdd9d/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/appest-limited/id434073155?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.12", + "trackName": "TickTick: Things & Tasks To Do", + "trackId": 966085870, + "sellerName": "Appest Limited", + "releaseNotes": "- Bug fixes and improvements.\n\nRecent Updates:\n- Customizable Section in List View.\n- Tag names can be capitalized.\n- The number of Pomos can now be estimated beforehand.\n- Lists under different folders can share the same name.\n- New city themes! Los Angeles and Cairo.\n\nThanks for using TickTick! We'll bring regular updates to give you more pleasant experience with performance and stability.\nWe'll read all reviews in App Store and evaluate your feedbacks carefully. Any issues encountered during the use, you may write to us via Avatar -> Feedback & Suggestions -> Submit feedback, we will get back to you asap.\nTickTick team with love.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-03-04T06:37:31Z", + "genreIds": [ + "6007" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-27T01:27:34Z", + "trackCensoredName": "TickTick: Things & Tasks To Do", + "languageCodesISO2A": [ + "EN", + "ZH" + ], + "fileSizeBytes": "24698702", + "sellerUrl": "https://ticktick.com", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/ticktick-things-tasks-to-do/id966085870?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Design exclusively for macOS, TickTick is your daily must-have to-do & task list to get all things done.\nTickTick can be accessed on more than 10 different platforms including Mac, iPhone, iPad, Apple Watch which enables you to manage tasks on all your devices/Web.\n\nKey features: \n- Add task via shortcut (Command+Shift+A)\n- Instant reminder\n- Set priority levels to tasks\n- Set flexible recurring tasks \n- Create checklists within tasks \n- Sort tasks by order/date/name/priority \n- Sync all your tasks across all devices \n\nTickTick is free but you can also upgrade to Premium account for full access of premium features for $2.99 a month or $27.99 a year through an auto-renewing subscription.\n\nPremium Features: \n- Grid view and Timeline view of calendar\n- Duration\n- Custom Smart List\n- Description for checklist\n- Reminders for sub-tasks\n- More lists and tasks (299 lists, 999 tasks in each list, 199 subtasks in each task)\n- Add at most 5 reminders to each task\n- Share a task list up to 19 members for better task collaboration\n- Upload up to 99 attachments every day\n\nSubscriptions for Premium account will be charged to your credit card through your iTunes account. Your subscription will automatically renew unless cancelled at least 24-hours before the end of the current period. You will not be able to cancel a subscription during the active period. You can manage your subscriptions in the Account Settings after purchase. \n\nHow TickTick makes you productive: \n- Get all things done \n- Never miss a schedule\n- Make work more productive \n- Keep life on track \n\nConnect with us: \nFacebook: https://www.facebook.com/TickTickApp\nTwitter: https://twitter.com/TickTickTeam @TickTickTeam\nHelp Center: https://help.ticktick.com/\n\nPrivacy Policy: https://www.ticktick.com/about/privacy\nTerms of Use: https://www.ticktick.com/about/tos", + "currency": "USD", + "artistId": 434073155, + "artistName": "Appest Limited", + "genres": [ + "Productivity" + ], + "price": 0.00, + "bundleId": "com.TickTick.task.mac", + "version": "3.7.11", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/79/5c/63/795c63aa-698c-1c6c-b6da-e7ebba718d01/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/15/4d/40/154d4071-4a6f-dcd7-0d15-2e495f6f4710/mzm.mvtkjcyn.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e0/31/dc/e031dc74-ce06-afe3-fd8e-8693f6c7c50c/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/fc/8d/23/fc8d2367-725d-11dd-6da9-816a7780a1d9/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.6", + "trackName": "Simple Antnotes", + "trackId": 846599902, + "sellerName": "Mykola Olshevskyi", + "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2014-03-28T12:49:14Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2016-09-24T17:06:52Z", + "trackCensoredName": "Simple Antnotes", + "languageCodesISO2A": [ + "EN", + "DE", + "RU", + "UK" + ], + "fileSizeBytes": "1002100", + "sellerUrl": "https://www.antlogic.com/apps/antnotes", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/simple-antnotes/id846599902?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- pin note to desktop to make it stay atop of other windows\n- translucent notes\n- make new notes by dragging text, images and files to the menu bar icon\n- drag images and sounds to note contents\n- automatically hide notes when inactive\n- quick access via menu bar icon\n- configurable global shortcuts to create new note or show/hide all notes\n- integration with services: create new note from any text in any application\n- snap to screen bounds and other notes\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Let us know, or check out our Antnotes application!\n\nVisit our support forums: https://www.antlogic.com/forum/", + "currency": "USD", + "artistId": 364746702, + "artistName": "AntLogic", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "ua.com.AntLogic.SimpleAntnotes", + "version": "1.6.1", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "appletvScreenshotUrls": [], + "supportedDevices": [ + "iPadMini4-iPadMini4", + "iPadProSecondGen-iPadProSecondGen", + "iPhone11-iPhone11", + "iPad71-iPad71", + "iPadMiniRetinaCellular-iPadMiniRetinaCellular", + "iPhone8Plus-iPhone8Plus", + "iPhone6sPlus-iPhone6sPlus", + "iPadMini5-iPadMini5", + "iPadProFourthGen-iPadProFourthGen", + "iPhoneXS-iPhoneXS", + "iPadAir3Cellular-iPadAir3Cellular", + "iPadAir3-iPadAir3", + "iPadMini4Cellular-iPadMini4Cellular", + "iPadProCellular-iPadProCellular", + "MacDesktop-MacDesktop", + "iPadMini3-iPadMini3", + "iPhoneXR-iPhoneXR", + "iPhoneSE-iPhoneSE", + "iPad611-iPad611", + "iPhone7-iPhone7", + "iPad73-iPad73", + "iPad812-iPad812", + "iPadAir2Cellular-iPadAir2Cellular", + "iPhoneX-iPhoneX", + "iPadMini5Cellular-iPadMini5Cellular", + "iPadPro97-iPadPro97", + "iPad834-iPad834", + "iPadProSecondGenCellular-iPadProSecondGenCellular", + "iPhone5s-iPhone5s", + "iPad75-iPad75", + "iPadMini3Cellular-iPadMini3Cellular", + "iPad878-iPad878", + "iPhone6-iPhone6", + "iPadAir-iPadAir", + "iPadPro97Cellular-iPadPro97Cellular", + "iPadSeventhGen-iPadSeventhGen", + "iPodTouchSixthGen-iPodTouchSixthGen", + "iPhoneXSMax-iPhoneXSMax", + "iPad612-iPad612", + "iPadPro-iPadPro", + "iPodTouchSeventhGen-iPodTouchSeventhGen", + "iPhone11ProMax-iPhone11ProMax", + "iPadMiniRetina-iPadMiniRetina", + "iPad76-iPad76", + "iPadProFourthGenCellular-iPadProFourthGenCellular", + "iPadSeventhGenCellular-iPadSeventhGenCellular", + "iPhoneSESecondGen-iPhoneSESecondGen", + "iPad74-iPad74", + "iPhone6s-iPhone6s", + "iPhone7Plus-iPhone7Plus", + "iPadAir2-iPadAir2", + "iPad72-iPad72", + "iPhone6Plus-iPhone6Plus", + "iPadAirCellular-iPadAirCellular", + "Watch4-Watch4", + "iPhone8-iPhone8", + "iPad856-iPad856", + "iPhone11Pro-iPhone11Pro" + ], + "screenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/2b/ce/8f/2bce8ffa-545b-050c-1dd9-2aeef532facd/pr_source.png/406x228bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/fb/36/14/fb36142e-17ba-fdab-90c6-e8f9d3c080ef/pr_source.png/406x228bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/4e/e2/de/4ee2de74-d0ef-010b-19f6-63755aa0175c/pr_source.png/406x228bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/46/8e/bd/468ebdd3-73a9-ec6a-b4da-6931ce887cff/pr_source.png/406x228bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/a4/9c/fa/a49cfa14-69e0-f1cf-3924-6ff878027b2d/pr_source.png/406x228bb.png", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c1/94/cc/c194ccb6-c15a-c0a5-47a6-3ddd625fd98d/pr_source.png/406x228bb.png" + ], + "ipadScreenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/3f/23/5e/3f235e16-c049-8ee8-ebdc-3d52f25f2636/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/80/48/1d/80481dff-e404-721c-920e-4688f860cf27/pr_source.png/552x414bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/58/a2/c9/58a2c970-1bd3-6f4d-1bdc-502f75faaa6a/pr_source.png/552x414bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/2c/a6/06/2ca606eb-8b40-219a-34c5-626f79b7e593/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/d7/04/c7d70441-51bd-1417-c7bf-a5d2702380e4/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/87/e0/75/87e075fd-a979-6151-5744-56ab76ac8f18/pr_source.png/552x414bb.png" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/60x60bb.jpg", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/512x512bb.jpg", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/100x100bb.jpg", + "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", + "isGameCenterEnabled": false, + "advisories": [], + "features": [ + "iosUniversal" + ], + "kind": "software", + "minimumOsVersion": "11.0", + "trackName": "Random: Lists & Decision Maker", + "trackId": 1128190780, + "sellerName": "Volodymyr Yahenskyi", + "releaseNotes": "• Fixed crash when adding items to a new list\n• Fixed lists sync on Apple Watch\n\nThanks for using the Random!\nThis release also contains bug fixes and performance improvements.", + "primaryGenreId": 6012, + "primaryGenreName": "Lifestyle", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-07-05T22:00:04Z", + "genreIds": [ + "6012", + "7009", + "6014", + "7004" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-29T19:21:51Z", + "trackCensoredName": "Random: Lists & Decision Maker", + "languageCodesISO2A": [ + "EN", + "RU", + "UK" + ], + "fileSizeBytes": "76392448", + "sellerUrl": "https://yahenskyi.dev/random/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 4.6104900000000004212097337585873901844024658203125, + "userRatingCountForCurrentVersion": 1525, + "averageUserRating": 4.6104900000000004212097337585873901844024658203125, + "trackViewUrl": "https://apps.apple.com/us/app/random-lists-decision-maker/id1128190780?uo=4", + "trackContentRating": "4+", + "description": "Need a random number? Or can’t you decide what to do? Random is a powerful app that will solve all such problems.\n\nFeatures:\n• Number generator (from a range 0 - 999999999)\n• Letter generator\n• Dice roller (roll up to 4 regular dices in one go)\n• A custom item from a list generator\n• Yes or No \n• Coin flipper\n• Card generator\n• Rock-Paper-Scissors\n• Map Point\n\nGenerate a new random number simply by tapping a ​randomize button or by touching the Apple Watch screen. For those who want a bit of additional exercise, shaking your iOS device will also result in a new random response.\n\nUse Force Touch for setting the minimum or maximum values in your Apple Watch app. Same for the number of dices​, cards, and selection of lists.\n\nRandom Premium subscription benefits:\n• Sync: Get access to your data from all your devices.\n• Themes: Customize the app with various themes and background images.\n• No advertising.\n\nIf you decide to get Random Premium subscription, your purchase will be charged to your iTunes account. 1 month costs $2.99 and 1 year costs $11.99. Active subscriptions will be auto-renewed 24 hours before the expiry date. You can manage subscriptions from Account in iTunes after subscribing, you’ll also be able to cancel the auto-renewing subscription from there at any time. Any unused portion of the free trial period will be forfeited if you purchase a subscription to Random Premium before your trial expires.\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", + "currency": "USD", + "artistId": 961335645, + "artistName": "Volodymyr Yahenskyi", + "genres": [ + "Lifestyle", + "Family", + "Games", + "Board" + ], + "price": 0.00, + "bundleId": "com.yahenskyi.random", + "version": "2.2.10", + "wrapperType": "software", + "userRatingCount": 1525 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/0e/74/bb/0e74bb9a-5ac2-5d5f-516a-5f1c12e95328/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/36/54/f0/3654f064-4013-95e6-2683-c89ab8e51102/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/1a/75/86/1a758637-9db5-007c-595b-b724e9083321/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/any-case-solutions/id1396419026?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "Task Planner - To Do List", + "trackId": 1063681909, + "sellerName": "Any Case Solutions, OOO", + "releaseNotes": "We’ve updated the app! In the new version:\n- less bugs;\n- minor changes in the interface;\n- some general improvements.\nYour opinion is important to us! Please, leave your feedback - we will gladly consider all your wishes and suggestions.", + "primaryGenreId": 6000, + "primaryGenreName": "Business", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-01-07T00:04:36Z", + "genreIds": [ + "6000", + "6007" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-17T23:48:12Z", + "trackCensoredName": "Task Planner - To Do List", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "JA", + "KO", + "PT", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "27930644", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/task-planner-to-do-list/id1063681909?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Plan Your Tasks is a productivity tool that allows you to capture your ideas and duties in one place. \nManage everything you have to do while working with many different tasks!\n\nEasy task management - create, organize, and prioritize tasks;\n- Set notifications;\n- Add comments;\n- Sort tasks by categories;\n- Track due dates.\n\nNew approach to agenda\n- Build-in calendar;\n- Coherent tutorial mode;\n- Magic Trackpad 2 support.\n\nCapture all your flash ideas and duties in the calendar and manage your to dos while working with many tasks more effectively.\n\n\nPrivacy Policy: https://anycasesolutions.com/privacy\nTerms Of Use: https://anycasesolutions.com/tos", + "currency": "USD", + "artistId": 1396419026, + "artistName": "Any Case Solutions", + "genres": [ + "Business", + "Productivity" + ], + "price": 0.00, + "bundleId": "com.newtechnologies.iPlanTasksinapp", + "version": "2.1.2", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/69/a0/58/69a0583d-02fd-1d37-cb33-19b80578e9e5/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/09/be/29/09be2981-4d08-a021-423a-29cc212c1b59/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple7/v4/28/5a/f4/285af4d8-37e6-118a-ff28-a4211eeb1122/pr_source.jpg/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/50/4d/52/504d520c-4f8c-b011-d1e0-18addb5700a8/pr_source.jpg/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/f2/6e/07/f26e0760-efb1-0353-3ebd-9fd2803f4b3d/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/60x60bb.png", + "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/512x512bb.png", + "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/realmac-software/id310591643?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "Clear – Tasks, Reminders & To-Do Lists", + "trackId": 504544917, + "sellerName": "Realmac Software Limited", + "releaseNotes": "Thanks for using Clear! Just two small enhancements in today’s update:\n\n- We’ve tweaked (increased) the delay before “Click to Clear” appears.\n- We’ve ensured compatibility with OS X El Capitan.\n\nStay productive, and follow @realmacsoftware on Twitter for the latest news!", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2012-11-08T08:00:00Z", + "genreIds": [ + "6007", + "6012" + ], + "formattedPrice": "$9.99", + "currentVersionReleaseDate": "2015-08-19T14:05:32Z", + "trackCensoredName": "Clear – Tasks, Reminders & To-Do Lists", + "languageCodesISO2A": [ + "EN" + ], + "fileSizeBytes": "13109875", + "sellerUrl": "http://impending.com/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/clear-tasks-reminders-to-do-lists/id504544917?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Over 2.5 million people de-clutter their lives with Clear, so stop stalling and start organizing your daily routine.\n\nClear is the revolutionary to-do and reminders app that makes you more productive. Just start typing to add to-dos, and once you start organizing your life with Clear you’ll wonder how you ever managed without it.\n\n- Simple gestural design that allows you to focus on your to-dos. Designed for the Magic Trackpad, but works great with a mouse too!\n- Full keyboard navigation. Just start typing to create to-dos.\n- Use separate lists to organize every aspect of your life.\n- iCloud sync built-in so you can be productive everywhere.\n- Set reminders so you’ll never forget important tasks.\n- Personalize your Clear lists with themes and make them your own.\n- Syncs with Clear for iOS (available separately on the App Store).\n\nClear is built by a small team, dedicated to bringing you frequent free feature updates. We’d love to know how we can make you even more productive, so get in touch via the App Store “Support” link, or tweet us @UseClear.\n\nClear for Mac and Clear for iOS are not affiliated with or endorsed by CLEAR Wireless.", + "currency": "USD", + "artistId": 310591643, + "artistName": "Realmac Software", + "genres": [ + "Productivity", + "Lifestyle" + ], + "price": 9.99, + "bundleId": "com.realmacsoftware.clear.mac", + "version": "1.1.7", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/cd/bd/44/cdbd44af-06eb-21d6-a793-43dae1077c47/pr_source.jpg/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/e9/8f/17/e98f17c6-787b-b180-6f8d-fb8385ceedd3/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/43/12/a2/4312a25b-f773-9c1a-ddd4-2515d948cc27/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/97/0d/b4/970db444-bbd9-ed77-439e-001bce006e17/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/shenzhen-tomato-software-technology-co-ltd/id966057212?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.12", + "trackName": "Focus To-Do: Pomodoro & Tasks", + "trackId": 1258530160, + "sellerName": "Shenzhen Tomato Software Technology Co., Ltd.", + "releaseNotes": "1.Support new languages\n2.Bug fix", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-08-02T03:45:26Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-05-03T04:38:29Z", + "trackCensoredName": "Focus To-Do: Pomodoro & Tasks", + "languageCodesISO2A": [ + "CS", + "EN", + "FR", + "DE", + "ID", + "IT", + "JA", + "KO", + "PL", + "PT", + "RO", + "RU", + "ZH", + "ES", + "ZH", + "TR", + "VI" + ], + "fileSizeBytes": "12135791", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/focus-to-do-pomodoro-tasks/id1258530160?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Focus To-Do combines Pomodoro Timer with Task Management, it is a science-based app that will motivate you to stay focused and get things done. \n\nIt brings Pomodoro Technique and To Do List into one place, you can capture and organize tasks into your todo lists, start focus timer and focus on work & study, set reminders for important tasks and errands, check the time spent at work. \n\nIt's the ultimate app for managing Tasks, Reminders, Lists, Calendar events, Grocery lists, checklist, helping you focus on work & study and tracking your working hours.\n\nFocus To-Do syncs between your phone and computer, so you can access your lists from anywhere.\n\nHow it works:\n 1. Pick a task you need to accomplish.\n 2. Set a timer for 25 minutes, keep focused and start working.\n 3. When the pomodoro timer rings, take a 5 minute break.\n \nKey Features:\n\n- Pomodoro Timer:Stay focused and get more things done.\n Pause and resume Pomodoro\n Customizable pomodoro/breaks lengths\n Notification before the end of a Pomodoro\n Support for short and long breaks\n Skip a break after the end of a Pomodoro\n Continuous Mode\n \n- Tasks Management: Task Organizer, Schedule Planner, Reminder, Habit Tracker, Time Tracker\n Tasks and projects: Organise your day with Focus To-Do and complete your to do, study, work, homework or housework you need to get done.\n Recurring tasks: Build lasting habits with powerful recurring due dates like \"Every Monday\".\n Reminders: Setting a Reminder ensures you never forget important things ever again, you can set up recurring due dates to remind you each and every time. \n Sub-tasks: Break down your task into smaller, actionable items or add a checklist .\n Task Priority: Highlight your day’s most important To-Do with color-coded priority levels.\n Estimated Pomodoro Number: Estimate the workload or set a goal.\n Note: Record more detailed about the task.\n\n- Report: Detailed statistics of your time distribution, tasks completed.\n Support the calculation of the total time of Focus Time.\n Gantt Chart of the Focus Time.\n Statistics on completed To Do. \n Statistics on time distribution of project.\n Trend chart of the completed To Do and the focus time.\n\n- All-Platform synchronization: View and manage your goals wherever you are for better goal achieving.\n Support seamless synchronization within iPhone、Mac、iPad、Apple Watch and other platforms.\n \n- Various Reminding:\n Focus Timer finished alarm, vibration reminding.\n Various white noise to help you focus on work & study.\n\nContact Us: focustodo@163.com, reply within 24 hours.\nWebsite: http://www.focustodo.cn\nPomodoro ™ and Pomodoro Technique ® are registered trademarks of Francesco Cirillo. This app is not affiliated with Francesco Cirillo.\n\nUsers have been focused on our app for 200 million hours, join us and we help you to be focused and increase your productivity, reduce procrastination and anxiety.", + "currency": "USD", + "artistId": 966057212, + "artistName": "Shenzhen Tomato Software Technology Co., Ltd.", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "com.macpomodoro", + "version": "6.3", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "appletvScreenshotUrls": [], + "supportedDevices": [ + "iPadMini4-iPadMini4", + "iPadProSecondGen-iPadProSecondGen", + "iPhone11-iPhone11", + "iPad71-iPad71", + "iPadMiniRetinaCellular-iPadMiniRetinaCellular", + "iPhone8Plus-iPhone8Plus", + "iPhone6sPlus-iPhone6sPlus", + "iPadMini5-iPadMini5", + "iPadProFourthGen-iPadProFourthGen", + "iPhoneXS-iPhoneXS", + "iPadAir3Cellular-iPadAir3Cellular", + "iPadAir3-iPadAir3", + "iPadMini4Cellular-iPadMini4Cellular", + "iPadProCellular-iPadProCellular", + "MacDesktop-MacDesktop", + "iPadMini3-iPadMini3", + "iPhoneXR-iPhoneXR", + "iPhoneSE-iPhoneSE", + "iPad611-iPad611", + "iPhone7-iPhone7", + "iPad73-iPad73", + "iPad812-iPad812", + "iPadAir2Cellular-iPadAir2Cellular", + "iPhoneX-iPhoneX", + "iPadMini5Cellular-iPadMini5Cellular", + "iPadPro97-iPadPro97", + "iPad834-iPad834", + "iPadProSecondGenCellular-iPadProSecondGenCellular", + "iPhone5s-iPhone5s", + "iPad75-iPad75", + "iPadMini3Cellular-iPadMini3Cellular", + "iPad878-iPad878", + "iPhone6-iPhone6", + "iPadAir-iPadAir", + "iPadPro97Cellular-iPadPro97Cellular", + "iPadSeventhGen-iPadSeventhGen", + "iPodTouchSixthGen-iPodTouchSixthGen", + "iPhoneXSMax-iPhoneXSMax", + "iPad612-iPad612", + "iPadPro-iPadPro", + "iPodTouchSeventhGen-iPodTouchSeventhGen", + "iPhone11ProMax-iPhone11ProMax", + "iPadMiniRetina-iPadMiniRetina", + "iPad76-iPad76", + "iPadProFourthGenCellular-iPadProFourthGenCellular", + "iPadSeventhGenCellular-iPadSeventhGenCellular", + "iPhoneSESecondGen-iPhoneSESecondGen", + "iPad74-iPad74", + "iPhone6s-iPhone6s", + "iPhone7Plus-iPhone7Plus", + "iPadAir2-iPadAir2", + "iPad72-iPad72", + "iPhone6Plus-iPhone6Plus", + "iPadAirCellular-iPadAirCellular", + "Watch4-Watch4", + "iPhone8-iPhone8", + "iPad856-iPad856", + "iPhone11Pro-iPhone11Pro" + ], + "screenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/95/33/5f/95335f94-26d3-3567-93ac-77d60ab821dd/pr_source.png/392x696bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/03/53/b9/0353b9b1-ef2a-7ff1-a1b8-4124867af41b/pr_source.png/392x696bb.png", + "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/ef/63/ce/ef63ce41-371a-2508-b101-fb99e9c7758f/pr_source.png/392x696bb.png" + ], + "ipadScreenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/19/bb/4319bb4b-5700-0f6b-2c19-7bd386bf186c/pr_source.jpg/552x414bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/5d/51/1a/5d511a30-7fab-fd18-6967-c0caf9674d55/pr_source.jpg/552x414bb.jpg" + ], + "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/60x60bb.jpg", + "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/512x512bb.jpg", + "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/100x100bb.jpg", + "artistViewUrl": "https://apps.apple.com/us/developer/kevin-reutter/id1273424431?uo=4", + "isGameCenterEnabled": true, + "advisories": [], + "features": [ + "gameCenter", + "iosUniversal" + ], + "kind": "software", + "minimumOsVersion": "13.0", + "trackName": "Planny 3 - Smart To Do List", + "trackId": 1289070327, + "sellerName": "Kevin Reutter", + "releaseNotes": "Stay tuned! Planny 4 ships in a few week and will be a free update with many great features!\n\n• SwiftUI \nNow Planny uses SwiftUI in some parts of the app. SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Over time more and more of the app will be created with SwiftUI to avoid crashes and improve performance. \n\n• Advanced Cursor Support\nWhen using a Trackpad on iPadOS or on the Mac, specific Elements become larger when you come closer to make clicking easier\n\n• Alternative App icons\nChoose the icon color you’d like in settings (iOS for iPhone only)\n\n• New Onboarding Experience\nA new tutorial shows the key features \n\n• New Purchase View\nThe purchase view is now much simpler. Feel free to subscribe :) \n\n• Fixed deadlines on macOS\n• Direct Deadlines now support days and time \n• Fixed issues with overdue tasks \n\nDo you have any wishes for Planny 4? Feel free to submit ideas on the website!", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-10-13T19:16:40Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-30T17:37:31Z", + "trackCensoredName": "Planny 3 - Smart To Do List", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "RU", + "ZH", + "ES", + "TR" + ], + "fileSizeBytes": "47687680", + "sellerUrl": "https://www.kevinreutter.de/planny-3/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 4.3897300000000001318767317570745944976806640625, + "userRatingCountForCurrentVersion": 331, + "averageUserRating": 4.3897300000000001318767317570745944976806640625, + "trackViewUrl": "https://apps.apple.com/us/app/planny-3-smart-to-do-list/id1289070327?uo=4", + "trackContentRating": "4+", + "description": "++ Planny was part of Apples favorite Apps from October ++\n\nPlanny is all new and has been rethought from the ground up.\n\nPlanny is your new friend helping you to be more productive. Planny learned everything important from common to do list apps but combines them with intelligence and gamification. In the morning and during the day Planny intelligently recommends tasks and also reminds you if you tend to forget them. You earn productivity points for adding and completing tasks, and also lose them if you shift tasks or forget them. Users can compare their productivity with friends over the week. \n\nPlanny also features all the important features like deadlines, lists / projects, tagging, location based reminders, notes and attachments, routines and more. \n\nKey features\n• Daily list to focus on today's tasks\n• Assistant for creating a productive daily plan\n• Daily review of the last day\n• Routines to train your habits\n• Deadlines and reminders\n• Smart reminders if you tend to forget your tasks\n• Notes for your tasks\n• Weekly productivity ranking of your contacts\n• Rewards\n• Dark mode\n• Lists\n• Siri support\n• Advanced Apple Watch app\n\nPlanny Premium offers additional features like:\n• Calendar view\n• Teamwork with your friends\n• Add Photos from your library to tasks\n• Add Photos from your camera to tasks\n• Location based reminders\n• iCloud sync\n• iCloud backup \n• FaceID Unlock\n• More than 2 lists\n• Printing\n• Sketches\n• Review your recent days\n• Tagging\n\n+++ Planny Premium - Unlock all features and use Planny on iPhone, iPad and Apple Watch (Mac soon) - And get free feature updates over time! +++ \n\nA Planny Premium subscription unlocks all features. Note that iCloud features require an iCloud-Account. \n\nPlanny offers two auto-renewing subscriptions\n\nPremium 3 Months\n$6,99 / 3 Months (may differ in your country & currency)\n\nPremium Annual\n$19,99 / Year (may differ in your country & currency)\n\nPayment will be charged to iTunes Account at confirmation of purchase\nSubscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period\nAccount will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal\n\nSubscriptions may be managed by the user and auto-renewal may be turned off by going to the user's Account Settings after purchase\n\nWhen your subscription is cancelled and expires, all the features of Planny Pro won't be available any longer. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.\n\nPrivacy policy for Planny: http://kevinreutter.de/privacy\nTerms of use / Conditions: http://kevinreutter.de/privacy", + "currency": "USD", + "artistId": 1273424431, + "artistName": "Kevin Reutter", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "com.kevinreutter.Callisto", + "version": "3.4.2", + "wrapperType": "software", + "userRatingCount": 331 + }, + { + "screenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/4f/ff/f9/4ffff968-2932-48af-431f-fd1b086026cf/mzl.srudbvwp.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/2a/4f/9f/2a4f9fad-c1a7-fa80-56d7-fea2d3beaa0a/mzl.dcyubghz.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple6/v4/56/3a/a7/563aa771-8288-e21a-cfe8-e28e77ffad83/mzl.lzjpfyct.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/1e/8d/4e/1e8d4eaa-17f7-5295-1f36-975b62164d19/mzl.yufjavxy.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/fb/9b/85/fb9b851d-6ef5-792f-0945-ff2f1a78ce7a/mzl.gycjiioz.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.6.6", + "trackName": "To-do Lists", + "trackId": 416993121, + "sellerName": "Mykola Olshevskyi", + "releaseNotes": "fixed accidentally broken compatibility for Mac OS 10.6-10.7", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2011-03-01T03:09:22Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "$4.99", + "currentVersionReleaseDate": "2015-04-16T19:07:37Z", + "trackCensoredName": "To-do Lists", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "RU", + "UK" + ], + "fileSizeBytes": "2095731", + "sellerUrl": "http://www.antlogic.com/apps/todo-lists/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/to-do-lists/id416993121?mt=12&uo=4", + "trackContentRating": "4+", + "description": "To-do Lists provides simple but powerful interface for tasks management.\n\nTo-do Lists features:\n- Quick, one-click tasks addition/removal.\n- Rich-text editing, in-text links support.\n- Seamless iCloud Reminders synchronization.\n- DropBox synchronization between computers and To-do Lists Mobile for iOS\n- Import/export of to-do lists via text files.\n- Printing of to-do lists or mailing them directly from the application.\n- Backup and restore of whole to-do database.\n- Full drag'n'drop support (make new to-do from web link, file, document, e-mail, or any other text by simply dropping them on to-do list).\n- System services support (make new to-do from any text in any application).\n- Rolled-up, translucent or floating to-do lists.\n- Customized background color, text color, font and checkbox appearance.\n- Reminders.\n- Quick-access icon in system menu.\n\nTo-do Lists usage video:\nhttp://youtu.be/5KB-4sYcelo (or http://youtube.com/AntlogicCompany )\n\nFor more information, visit our site at http://www.antlogic.com/\nor Facebook page:\nhttp://facebook.com/AntlogicCompany\n\nIf you have any problems or questions using To-do Lists - visit our support forums at http://www.antlogic.com/forum/", + "currency": "USD", + "artistId": 364746702, + "artistName": "AntLogic", + "genres": [ + "Productivity", + "Business" + ], + "price": 4.99, + "bundleId": "ua.com.AntLogic.ToDoLists", + "version": "1.7.7", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/e9/42/a8e942ec-8eea-03b3-ea37-cc6e2837fb5e/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/62/5c/5f625c38-c559-3b8d-5042-94e241735ef1/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/aa/a1/e7/aaa1e746-e660-2b6e-6833-d751e7879752/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c0/6f/7d/c06f7de4-17b9-7475-8778-22a97c13cdce/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/84/3d/8d/843d8de0-6257-7bf0-6a66-6f3ce41af803/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/0d/28/ad0d28c6-ff1d-c394-266a-fdff0b8e9cc6/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/36/28/a3/3628a3e9-6073-0ce4-17d7-9d9a5f479c64/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a9/ac/b9/a9acb983-76e2-d76d-fbc8-c35388dcee48/pr_source.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/8f/41/90/8f4190f5-ebb8-370b-c8a6-afb47c56140d/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/09/37/83/09378396-de11-2185-24f4-360be20dbcac/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/the-omni-group/id281731738?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.14", + "trackName": "OmniFocus 3", + "trackId": 1346203938, + "sellerName": "The Omni Group", + "releaseNotes": "OmniFocus 3.9.2 is a minor update focused on bug fixes.\n\n• Omni Automation — OmniFocus now recognizes simple plug-ins that use the .omnifocusjs file extention.\n• First Run — Improved reliability of the first run flow.\n• Notice Bar — Fixed bugs related to the Trial Mode & Free Viewer notice bars.\n\nIf you have any feedback or questions, we’d love to hear from you! The Omni Group offers free tech support; you can email omnifocus@omnigroup.com, call 1–800–315–6664 or 1–206–523–4152, or tweet @OmniFocus.\n\nIf OmniFocus empowers you, we would appreciate an App Store review. Your review will help other people find OmniFocus and make them more productive too.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2018-09-24T12:28:36Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-27T17:54:49Z", + "trackCensoredName": "OmniFocus 3", + "languageCodesISO2A": [ + "NL", + "EN", + "FR", + "DE", + "IT", + "JA", + "KO", + "PT", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "64931473", + "sellerUrl": "https://www.omnigroup.com/omnifocus/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/omnifocus-3/id1346203938?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Two-week free trial! OmniFocus Standard and Pro are in-app purchases, with discounts for people who bought earlier versions of OmniFocus for Mac through the Mac App Store. Or you can get OmniFocus for iOS, Mac, and web for just one price with the OmniFocus Subscription. Download the app for details.\n\nUse OmniFocus to accomplish more every day. Create projects and tasks, organize them with tags, focus on what you can do right now — and get stuff done.\n\nOmniFocus — now celebrating 10 years as the trusted, gold-standard to-do list app — brings unrivaled power and flexibility to your Mac, making it easy to work the way you want to work.\n\nOmniFocus manages everything in your busy life. Use projects to organize tasks naturally, and then add tags to organize across projects. Easily enter tasks when you’re on the go, and process them when you have time. Tap the Forecast view — which shows both tasks and calendar events — to get a handle on your day. Use the Review perspective to keep your projects and tasks on track.\n\nThen let our free syncing system make sure your data is the same on every Mac. (And on OmniFocus for iOS and Web, available separately.) Because your data is encrypted, it’s safe in the cloud.\n\nSTANDARD FEATURES (VIA IN-APP PURCHASE)\n\n• NEW: Tags add a powerful additional organizing tool. Create tags for people, energy levels, priorities, locations, and more.\n• NEW: The Forecast view shows your tasks and calendar events in order, so you can better see what’s coming up in your day.\n• NEW: Enhanced repeating tasks are easier than ever to set up — and they work with real-world examples such as the first weekday of the month.\n• NEW: The Modern, fresh-but-familiar design helps you focus on your content.\n• Inbox is where you quickly add tasks — save them when you think of them, and organize them later.\n• Syncing supports end-to-end encryption so that your data is safe wherever it’s stored, on our server or yours.\n• Notes can be attached to your tasks, so you have all the information you need.\n• Attachments — graphics, video, audio, whatever you want — add richness to your tasks.\n• View Options let you customize each perspective by deciding what it should show and how it should filter your tasks.\n• The Review perspective takes you through your projects and tasks — so you stay on track.\n• OmniFocus Mail Drop adds tasks via email and works with services like IFTTT and Zapier (if you’re using our free syncing server).\n• The Today Widget shows you your most important items — you don’t even have to switch to the app to know what’s up.\n• Support for TaskPaper Text and omnifocus:///add and /paste lets you automate using URLs.\n\nPro features make OmniFocus even more powerful:\n\nPRO FEATURES (VIA IN-APP PURCHASE)\n\n• Custom perspectives help you create new ways to see your data by filtering and grouping projects and tags. NEW: The filtering rules are simpler to use while being more powerful than ever, letting you combine rules with “all,” “any,” and “none.” You can also choose any image to use as your custom perspective’s icon, and a custom tint color to go with it.\n• NEW: Today’s Forecast can include items with a specific tag, and you can reorder those tasks however you choose, so you can plan your day better.\n• The customizable sidebar lets you organize your perspectives the way you want to, for super-fast access.\n• The Today Widget shows a perspective of your choice in Notification Center.\n• AppleScript support opens up a world of automation, using Apple’s Mac scripting language.\n\nDownload OmniFocus right now and start your free trial! The app includes a manual, and there’s plenty more documentation on the website.\n\nSUPPORT\n\nIf you have feedback or questions, our Support Humans would love to hear from you! Send email to omnifocus@omnigroup.com, call us at at 1-800-315-6664 or +1-206-523-4152, or reach us on Twitter at @omnifocus.\n\n\nSubscription Terms of Service: https://www.omnigroup.com/legal", + "currency": "USD", + "artistId": 281731738, + "artistName": "The Omni Group", + "genres": [ + "Productivity", + "Business" + ], + "price": 0.00, + "bundleId": "com.omnigroup.OmniFocus3.MacAppStore", + "version": "3.9.2", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/72/1d/1a721d98-fbc4-ed9e-2aae-ef9d5b538693/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a2/b1/10/a2b110fd-aa90-286a-658b-2abd85bd1c68/mzl.menowpkq.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/26/f3/32/26f3322f-8ef9-6171-2864-715f571300e6/mzl.qxibkqwt.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a4/1b/12/a41b12dc-6e1d-74ff-7ad3-1d6888c31462/mzl.fdlcjqnh.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/83/83/24/83832412-85d1-78ff-a9ab-86a37b31121d/mzl.ateekpxr.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e6/7c/c7/e67cc703-d274-a1fc-88af-b5a8ce9cbfd8/mzl.grtjmgef.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/18/a8/f2/18a8f211-61b5-7b7e-b343-784b260de31d/mzl.ulsghntx.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/60x60bb.png", + "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/512x512bb.png", + "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/masterbuilders/id896347016?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.14", + "trackName": "Focus - Time Management", + "trackId": 777233759, + "sellerName": "Masterbuilders", + "releaseNotes": "Subscription status is now properly unlocked on all devices.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2013-12-19T19:16:50Z", + "genreIds": [ + "6007", + "6017" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-02-12T20:37:50Z", + "trackCensoredName": "Focus - Time Management", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "JA", + "ZH", + "ES" + ], + "fileSizeBytes": "24637530", + "sellerUrl": "https://www.focusapp.io", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/focus-time-management/id777233759?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Meet Focus: the best time manager for iPhone, iPad, Apple Watch and Mac. Focus is the most elegant and professional way to get more wore done, working in highly efficient work sessions, one task at a time.\n\n“[…] a tool that can genuinely make people more productive\" – MacStories.net\n\n“[…] a must-have for anyone who finds themselves easily getting distracted or forgetting to take occasional breaks.\" – iDownloadBlog.com\n\n======================\nFEATURES\n======================\n\nFOCUS SESSIONS\nFocus Sessions are a highly efficient way to work. Focus for 25 minutes, then take a short break to relax your mind. After four sessions, take a 15 to 20 minute break. This method maximizes energy, stimulates creativity and promotes a sense of achievement.\n\nTASK MANAGER\nFocus includes a lightweight task manager that lets you organize the things you want to work on intuitively. By working on one task at a time, you won’t be distracted and can focus all your attention towards completing that goal. That way you’ll be perfectly organized on your path to success.\n\nIN-DEPTH STATISTICS\nCheck what you’ve already done! Focus keeps track of your work and offers in-depth and motivating statistics. See your daily, weekly and monthly activity so you don’t lose sight of the big picture. \n\nFOCUS EVERYWHERE\nSeamlessly use Focus on your Mac, iPad, iPhone, and Apple Watch. Sync across your devices using iCloud; use Handoff to pick up your current work on another device and get up-to-the-second data with iCloud Push. You can also use the Today widget to quickly glance at your progress, import tasks using the handy Action extension, and more.\n\nFOCUS & APPLE WATCH: A PERFECT FIT\nUsing Focus on your wrist is a natural fit. The independent Apple Watch app is made for for easy and lightweight interactions that lets you control sessions and track your progress throughout the day. With the Focus complication, you can customize your watch face to see your current progress at a glance.\n\nBEAUTIFUL INTERFACE\nThe name says it all: Focus draws your attention to the most important things. It’s designed to be unobtrusive, accessible and easy-to-use. You’ll intuitively master its collection of features just by using them.\n\n======================\nSUBSCRIPTION PRICING\n======================\n\nFocus offers two subscription options: \nFocus Monthly at $4.99/ month \nFocus Yearly at $39.99/ year\n\nThe subscription unlocks all features on all devices (Mac, iPhone, iPad and Apple Watch).\n\nTRY IT FREE \nFocus Monthly comes with a 3-day free trial period, Focus Yearly with a 7-day free trial period. If you cancel before the end of the trial, you will not be charged for the subscription.\n\nSUBSCRIPTION TERMS\nPayment will be charged to your Apple ID account at the confirmation of purchase or after the free trial period if offered. \n\nYou subscription will automatically renew unless it is canceled at least 24 hours before the end of the current period. Your account will be charged 24 hours prior to the end of the current period. \n\nYou can manage and cancel your subscriptions by going to your account settings in the App Store after purchase. Any unused portion of a free trial will be forfeited when you purchase a subscription\n\n======================\nCONTACT\n======================\n\nIf you have any questions or ideas, please write us at hello@masterbuilders.io\n\nTwitter: @focusappio\nhttps://www.masterbuilders.io\n\n\n\nPrivacy Policy: https://www.masterbuilders.io/privacy\nTerms of Service: https://www.masterbuilders.io/terms", + "currency": "USD", + "artistId": 896347016, + "artistName": "Masterbuilders", + "genres": [ + "Productivity", + "Education" + ], + "price": 0.00, + "bundleId": "com.malteundjan.focus-osx", + "version": "6.2.3", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/35/03/5a/35035a62-e2da-2f4b-6ece-63475bd7cd02/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/b5/ac/1f/b5ac1fe2-431d-e45d-63e0-57ddbfbd525f/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/6c/18/ee/6c18eeff-ca66-2b01-82f3-f81576336ab7/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/niklas-behrens/id969210609?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "1Focus: Website & App Blocker", + "trackId": 969210610, + "sellerName": "Niklas Behrens", + "releaseNotes": "- Allows updating 1Focus while blocking is active\n- Fixed toolbar overflow on macOS High Sierra\n- Improved status item width\n- Fixed quick start menu starting wrong task\n- Other bug fixes", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2015-03-15T05:54:46Z", + "genreIds": [ + "6007", + "6017" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-18T23:51:25Z", + "trackCensoredName": "1Focus: Website & App Blocker", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "JA", + "KO", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "8338821", + "sellerUrl": "https://onefocusapp.com", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/1focus-website-app-blocker/id969210610?mt=12&uo=4", + "trackContentRating": "4+", + "description": "1Focus creates an oasis for focused work by disabling access to specific websites and apps. Use it to schedule a bit of automated self-restraint when you find yourself clicking away from what really needs to get done. Ideal for students, freelancers and writers.\n\n\"If you find yourself on Facebook or checking your email every five minutes, you need 1Focus.\" – Pagoda Technologies\n\n\"1Focus is one of the best apps for tuning out the diversions that are most distracting for you.\" – Tyler Horvath, CEO of Tyton Media\n\n\nFREE FEATURES\n\n• Block websites in Google Chrome, Safari, Opera, Microsoft Edge and Brave\n• Block apps (e.g. email, games)\n• Block internet access by blocking web browsers\n• You cannot cancel active blocks once you close the 1Focus window\n• Create your own task presets (up to 2)\n• Dark Mode\n\n\n1FOCUS PRO\n\n• Schedule recurring block events (e.g. Mon - Fri)\n• Work break timer\n• Unlimited task presets\n• Block all websites/apps except specific ones\n• Suspend blocking for a limited time\n• Block URL keywords (e.g. *gaming*)\n• Block popular websites by category (e.g. Social Media)\n\nTry it free for 14 days. $1.99/month or $9.99/year after.\n\nPrices may vary by location. Subscriptions are charged to your iTunes Account. They automatically renew unless you cancel them in your Account Settings at least 24 hours before the end of the current period. Your Account is charged for renewal within 24 hours prior to the end of the current period. Terms of use: https://onefocusapp.com/terms\n\n\nCUSTOMER SUPPORT\n\nDo you have any questions or suggestions?\nonefocusapp.com/support", + "currency": "USD", + "artistId": 969210609, + "artistName": "Niklas Behrens", + "genres": [ + "Productivity", + "Education" + ], + "price": 0.00, + "bundleId": "com.onefocusapp.OneFocus", + "version": "3.4.4", + "wrapperType": "software", + "userRatingCount": 0 + } + ] +} From fd051eac1f85cdf7edb7ab27509a9a06a1d28aeb Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:16:41 -0400 Subject: [PATCH 81/81] Require `fileSizeBytes` & `price` for `SearchResult`. Resolve #597 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Formatters/AppInfoFormatter.swift | 2 +- Sources/mas/Models/SearchResult.swift | 4 ++-- Tests/masTests/JSON/search/things-that-go-bump.json | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift index 0e70a55..a162d99 100644 --- a/Sources/mas/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -27,7 +27,7 @@ enum AppInfoFormatter { "By: \(app.sellerName)", "Released: \(humanReadableDate(app.currentVersionReleaseDate))", "Minimum OS: \(app.minimumOsVersion)", - "Size: \(humanReadableSize(app.fileSizeBytes ?? "0"))", + "Size: \(humanReadableSize(app.fileSizeBytes))", "From: \(app.trackViewUrl)", ] .joined(separator: "\n") diff --git a/Sources/mas/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift index 0c26618..0f67287 100644 --- a/Sources/mas/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -9,10 +9,10 @@ struct SearchResult: Decodable { var bundleId: String var currentVersionReleaseDate: String - var fileSizeBytes: String? + var fileSizeBytes: String var formattedPrice: String var minimumOsVersion: String - var price: Double? + var price: Double var sellerName: String var sellerUrl: String? var trackId: AppID diff --git a/Tests/masTests/JSON/search/things-that-go-bump.json b/Tests/masTests/JSON/search/things-that-go-bump.json index 91633f3..f2e873c 100644 --- a/Tests/masTests/JSON/search/things-that-go-bump.json +++ b/Tests/masTests/JSON/search/things-that-go-bump.json @@ -20,6 +20,7 @@ "trackId": 1472954003, "sellerName": "Tinybop Inc.", "price": 0.99, + "fileSizeBytes": "12345678", "formattedPrice": "$0.99", "releaseNotes": "* BOOM *, this is a BIG update. The house spawns a game room, complete with video games you can ENTER INTO. It's fun and a little bit weird! Try it! \n»-(¯`·.·´¯)->", "primaryGenreId": 6014,