From 52a0638f2bfa25f86082135f2d66e71324834d96 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Sat, 26 Oct 2024 11:43:08 -0400
Subject: [PATCH 01/21] Improve help output.
Make fish shell completions more consistent with the help output.
Resolve #543
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Account.swift | 2 +-
Sources/mas/Commands/Home.swift | 4 +--
Sources/mas/Commands/Info.swift | 2 +-
Sources/mas/Commands/Install.swift | 6 ++--
Sources/mas/Commands/List.swift | 2 +-
Sources/mas/Commands/Lucky.swift | 10 +++++--
Sources/mas/Commands/Open.swift | 4 +--
Sources/mas/Commands/Outdated.swift | 4 +--
Sources/mas/Commands/Purchase.swift | 4 +--
Sources/mas/Commands/Reset.swift | 4 +--
Sources/mas/Commands/Search.swift | 4 +--
Sources/mas/Commands/SignIn.swift | 4 +--
Sources/mas/Commands/SignOut.swift | 2 +-
Sources/mas/Commands/Uninstall.swift | 6 ++--
Sources/mas/Commands/Upgrade.swift | 11 ++++----
Sources/mas/Commands/Vendor.swift | 4 +--
Sources/mas/Commands/Version.swift | 2 +-
contrib/completion/mas.fish | 42 +++++++++++++++-------------
18 files changed, 63 insertions(+), 54 deletions(-)
diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift
index a9f90a6..328e612 100644
--- a/Sources/mas/Commands/Account.swift
+++ b/Sources/mas/Commands/Account.swift
@@ -12,7 +12,7 @@ import StoreFoundation
extension MAS {
struct Account: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Prints the primary account Apple ID"
+ abstract: "Display the Apple ID signed in in the Mac App Store"
)
/// Runs the command.
diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift
index c9e186e..8d86a5f 100644
--- a/Sources/mas/Commands/Home.swift
+++ b/Sources/mas/Commands/Home.swift
@@ -13,10 +13,10 @@ extension MAS {
/// https://performance-partners.apple.com/search-api
struct Home: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Opens MAS Preview app page in a browser"
+ abstract: "Open app's Mac App Store web page in the default web browser"
)
- @Argument(help: "ID of app to show on MAS Preview")
+ @Argument(help: "App ID")
var appID: AppID
/// Runs the command.
diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift
index 5352097..5cab3fd 100644
--- a/Sources/mas/Commands/Info.swift
+++ b/Sources/mas/Commands/Info.swift
@@ -17,7 +17,7 @@ extension MAS {
abstract: "Display app information from the Mac App Store"
)
- @Argument(help: "ID of app to show info")
+ @Argument(help: "App ID")
var appID: AppID
/// Runs the command.
diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift
index 9b051e7..7247172 100644
--- a/Sources/mas/Commands/Install.swift
+++ b/Sources/mas/Commands/Install.swift
@@ -13,12 +13,12 @@ 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"
+ abstract: "Install previously purchased app(s) from the Mac App Store"
)
- @Flag(help: "force reinstall")
+ @Flag(help: "Force reinstall")
var force = false
- @Argument(help: "app ID(s) to install")
+ @Argument(help: "App ID(s)")
var appIDs: [AppID]
/// Runs the command.
diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift
index 94544c9..6817fe5 100644
--- a/Sources/mas/Commands/List.swift
+++ b/Sources/mas/Commands/List.swift
@@ -12,7 +12,7 @@ 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"
+ abstract: "List apps installed from the Mac App Store for the Apple ID of the current macOS user"
)
/// Runs the command.
diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift
index 781f55f..400e5ef 100644
--- a/Sources/mas/Commands/Lucky.swift
+++ b/Sources/mas/Commands/Lucky.swift
@@ -15,12 +15,16 @@ extension MAS {
/// 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"
+ abstract:
+ """
+ Install the first app returned from searching the Mac App Store
+ (app must have been previously purchased)
+ """
)
- @Flag(help: "force reinstall")
+ @Flag(help: "Force reinstall")
var force = false
- @Argument(help: "the app name to install")
+ @Argument(help: "Search term")
var searchTerm: String
/// Runs the command.
diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift
index a924895..365715e 100644
--- a/Sources/mas/Commands/Open.swift
+++ b/Sources/mas/Commands/Open.swift
@@ -16,10 +16,10 @@ extension MAS {
/// https://performance-partners.apple.com/search-api
struct Open: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Opens app page in 'App Store.app'"
+ abstract: "Open app page in 'App Store.app'"
)
- @Argument(help: "the app ID")
+ @Argument(help: "App ID")
var appID: AppID?
/// Runs the command.
diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift
index a495c1b..81b447a 100644
--- a/Sources/mas/Commands/Outdated.swift
+++ b/Sources/mas/Commands/Outdated.swift
@@ -15,10 +15,10 @@ extension MAS {
/// 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"
+ abstract: "List pending app updates from the Mac App Store for the Apple ID of the current macOS user"
)
- @Flag(help: "Show warnings about apps")
+ @Flag(help: "Display warnings about apps unknown to the Mac App Store")
var verbose = false
/// Runs the command.
diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift
index d775c64..bdb8a58 100644
--- a/Sources/mas/Commands/Purchase.swift
+++ b/Sources/mas/Commands/Purchase.swift
@@ -12,10 +12,10 @@ import CommerceKit
extension MAS {
struct Purchase: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Purchase and download free apps from the Mac App Store"
+ abstract: "\"Purchase\" and install free apps from the Mac App Store"
)
- @Argument(help: "app ID(s) to install")
+ @Argument(help: "App ID(s)")
var appIDs: [AppID]
/// Runs the command.
diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift
index 37ff3e4..055fd2a 100644
--- a/Sources/mas/Commands/Reset.swift
+++ b/Sources/mas/Commands/Reset.swift
@@ -13,10 +13,10 @@ 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"
+ abstract: "Reset Mac App Store running processes"
)
- @Flag(help: "Enable debug mode")
+ @Flag(help: "Output debug information")
var debug = false
/// Runs the command.
diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift
index ae36163..9fffa22 100644
--- a/Sources/mas/Commands/Search.swift
+++ b/Sources/mas/Commands/Search.swift
@@ -17,9 +17,9 @@ extension MAS {
abstract: "Search for apps from the Mac App Store"
)
- @Flag(help: "Show price of found apps")
+ @Flag(help: "Display the price of each app")
var price = false
- @Argument(help: "the app name to search")
+ @Argument(help: "Search term")
var searchTerm: String
func run() throws {
diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift
index c94da79..300acdf 100644
--- a/Sources/mas/Commands/SignIn.swift
+++ b/Sources/mas/Commands/SignIn.swift
@@ -13,10 +13,10 @@ extension MAS {
struct SignIn: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signin",
- abstract: "Sign in to the Mac App Store"
+ abstract: "Sign in to an Apple ID in the Mac App Store"
)
- @Flag(help: "Complete login with graphical dialog")
+ @Flag(help: "Provide password via graphical dialog")
var dialog = false
@Argument(help: "Apple ID")
var appleID: String
diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift
index 1c0d49d..6f55e76 100644
--- a/Sources/mas/Commands/SignOut.swift
+++ b/Sources/mas/Commands/SignOut.swift
@@ -13,7 +13,7 @@ extension MAS {
struct SignOut: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signout",
- abstract: "Sign out of the Mac App Store"
+ abstract: "Sign out of the Apple ID currently signed in in the Mac App Store"
)
/// Runs the command.
diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift
index d9997a7..343d9df 100644
--- a/Sources/mas/Commands/Uninstall.swift
+++ b/Sources/mas/Commands/Uninstall.swift
@@ -13,13 +13,13 @@ 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"
+ abstract: "Uninstall app installed from the Mac App Store for the Apple ID of the current macOS user"
)
/// Flag indicating that removal shouldn't be performed.
- @Flag(help: "dry run")
+ @Flag(help: "Perform dry run")
var dryRun = false
- @Argument(help: "ID of app to uninstall")
+ @Argument(help: "App ID")
var appID: AppID
/// Runs the uninstall command.
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 1c1630d..25743f0 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -14,11 +14,12 @@ 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"
+ abstract:
+ "Upgrade outdated app(s) installed from the Mac App Store for the Apple ID of the current macOS user"
)
- @Argument(help: "app(s) to upgrade")
- var appIDs: [String] = []
+ @Argument(help: "App ID(s)/app name(s)")
+ var appIDOrNames: [String] = []
/// Runs the command.
func run() throws {
@@ -56,9 +57,9 @@ extension MAS {
searcher: AppStoreSearcher
) throws -> [(SoftwareProduct, SearchResult)] {
let apps =
- appIDs.isEmpty
+ appIDOrNames.isEmpty
? appLibrary.installedApps
- : appIDs.flatMap { appID in
+ : appIDOrNames.flatMap { appID in
if let appID = AppID(appID) {
// argument is an AppID, lookup apps by id using argument
return appLibrary.installedApps(withAppID: appID)
diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift
index 31189e3..d303c94 100644
--- a/Sources/mas/Commands/Vendor.swift
+++ b/Sources/mas/Commands/Vendor.swift
@@ -13,10 +13,10 @@ extension MAS {
/// https://performance-partners.apple.com/search-api
struct Vendor: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Opens vendor's app page in a browser"
+ abstract: "Open vendor's app web page in the default web browser"
)
- @Argument(help: "the app ID to show the vendor's website")
+ @Argument(help: "App ID")
var appID: AppID
/// Runs the command.
diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift
index 4493088..d2f67d7 100644
--- a/Sources/mas/Commands/Version.swift
+++ b/Sources/mas/Commands/Version.swift
@@ -12,7 +12,7 @@ extension MAS {
/// Command which displays the version of the mas tool.
struct Version: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "Print version number"
+ abstract: "Display version number"
)
/// Runs the command.
diff --git a/contrib/completion/mas.fish b/contrib/completion/mas.fish
index de9576f..eccb749 100644
--- a/contrib/completion/mas.fish
+++ b/contrib/completion/mas.fish
@@ -23,61 +23,65 @@ end
complete -c mas -f
### account
-complete -c mas -n "__fish_use_subcommand" -f -a account -d "Prints the primary account Apple ID"
+complete -c mas -n "__fish_use_subcommand" -f -a account -d "Display the Apple ID signed in in the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "account"
### help
complete -c mas -n "__fish_use_subcommand" -f -a help -d "Display general or command-specific help"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "help"
### home
-complete -c mas -n "__fish_use_subcommand" -f -a home -d "Opens MAS Preview app page in a browser"
+complete -c mas -n "__fish_use_subcommand" -f -a home -d "Open app's Mac App Store web page in the default web browser"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "home"
-complete -c mas -n "__fish_seen_subcommand_from home info install open vendor" -xa "(__fish_mas_list_available)"
+complete -c mas -n "__fish_seen_subcommand_from home info install open purchase vendor" -xa "(__fish_mas_list_available)"
### info
complete -c mas -n "__fish_use_subcommand" -f -a info -d "Display app information from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "info"
### install
-complete -c mas -n "__fish_use_subcommand" -f -a install -d "Install from the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a install -d "Install previously purchased app(s) from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "install"
complete -c mas -n "__fish_seen_subcommand_from install lucky" -l force -d "Force reinstall"
### list
-complete -c mas -n "__fish_use_subcommand" -f -a list -d "Lists apps from the Mac App Store which are currently installed"
+complete -c mas -n "__fish_use_subcommand" -f -a list -d "List apps installed from the Mac App Store for the Apple ID of the current macOS user"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "list"
### lucky
-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_use_subcommand" -f -a lucky -d "Install the first app returned from searching the Mac App Store (app must have been previously purchased)"
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 'App Store.app'"
+complete -c mas -n "__fish_use_subcommand" -f -a open -d "Open 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"
+complete -c mas -n "__fish_use_subcommand" -f -a outdated -d "List pending app updates from the Mac App Store for the Apple ID of the current macOS user"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "outdated"
+complete -c mas -n "__fish_seen_subcommand_from outdated" -l verbose -d "Display warnings about apps unknown to the Mac App Store"
+### purchase
+complete -c mas -n "__fish_use_subcommand" -f -a purchase -d "\"Purchase\" and install free apps from the Mac App Store"
+complete -c mas -n "__fish_seen_subcommand_from help" -xa "purchase"
### reset
-complete -c mas -n "__fish_use_subcommand" -f -a reset -d "Resets the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a reset -d "Reset Mac App Store running processes"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "reset"
-complete -c mas -n "__fish_seen_subcommand_from reset" -l debug -d "Enable debug mode"
+complete -c mas -n "__fish_seen_subcommand_from reset" -l debug -d "Output debug information"
### search
complete -c mas -n "__fish_use_subcommand" -f -a search -d "Search for apps from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "search"
-complete -c mas -n "__fish_seen_subcommand_from search" -l price -d "Show price of found apps"
+complete -c mas -n "__fish_seen_subcommand_from search" -l price -d "Display the price of each app"
### signin
-complete -c mas -n "__fish_use_subcommand" -f -a signin -d "Sign in to the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a signin -d "Sign in to an Apple ID in the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "signin"
-complete -c mas -n "__fish_seen_subcommand_from signin" -l dialog -d "Complete login with graphical dialog"
+complete -c mas -n "__fish_seen_subcommand_from signin" -l dialog -d "Provide password via graphical dialog"
### signout
-complete -c mas -n "__fish_use_subcommand" -f -a signout -d "Sign out of the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a signout -d "Sign out of the Apple ID currently signed in in the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "signout"
### uninstall
-complete -c mas -n "__fish_use_subcommand" -f -a uninstall -d "Uninstall app installed from the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a uninstall -d "Uninstall app installed from the Mac App Store for the Apple ID of the current macOS user"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "uninstall"
-complete -c mas -n "__fish_seen_subcommand_from uninstall" -l dry-run -d "Dry run mode"
+complete -c mas -n "__fish_seen_subcommand_from uninstall" -l dry-run -d "Perform dry run"
complete -c mas -n "__fish_seen_subcommand_from uninstall" -x -a "(__fish_mas_list_installed)"
### upgrade
-complete -c mas -n "__fish_use_subcommand" -f -a upgrade -d "Upgrade outdated apps from the Mac App Store"
+complete -c mas -n "__fish_use_subcommand" -f -a upgrade -d "Upgrade outdated app(s) from the Mac App Store for the Apple ID of the current macOS user"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "upgrade"
complete -c mas -n "__fish_seen_subcommand_from upgrade" -x -a "(__fish_mas_outdated_installed)"
### vendor
-complete -c mas -n "__fish_use_subcommand" -f -a vendor -d "Opens vendor's app page in a browser"
+complete -c mas -n "__fish_use_subcommand" -f -a vendor -d "Open vendor's app web page in the default web browser"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "vendor"
### version
-complete -c mas -n "__fish_use_subcommand" -f -a version -d "Print version number"
+complete -c mas -n "__fish_use_subcommand" -f -a version -d "Display version number"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "version"
From f83412bba188aed5076ffb829f4a90435f1d3ac8 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Sun, 27 Oct 2024 13:47:17 -0400
Subject: [PATCH 02/21] Increase minimum macOS version to 10.13, since Swift
5.7 is already used, which requires Xcode 14+ to compile, which only supports
macOS deployment targets 10.13+.
Use Swift 5.7.1, which is the newest version of Swift 5.7.
Resolve #578
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
.swift-version | 2 +-
Makefile | 8 +-
Package.swift | 4 +-
Package/Distribution.plist | 2 +-
README.md | 3 +-
Sources/mas/AppStore/ISStoreAccount.swift | 88 ++++---------------
Sources/mas/Commands/SignOut.swift | 8 +-
.../ExternalCommands/ExternalCommand.swift | 11 +--
Tests/masTests/Commands/SignInSpec.swift | 2 +-
script/bottle | 4 +-
10 files changed, 28 insertions(+), 104 deletions(-)
diff --git a/.swift-version b/.swift-version
index 760606e..64ff7de 100644
--- a/.swift-version
+++ b/.swift-version
@@ -1 +1 @@
-5.7
+5.7.1
diff --git a/Makefile b/Makefile
index 9b85b78..ec483e0 100644
--- a/Makefile
+++ b/Makefile
@@ -12,13 +12,7 @@ CMD_NAME = mas
SHELL = /bin/sh
PREFIX ?= /usr/local
-# trunk
-# SWIFT_VERSION = swift-DEVELOPMENT-SNAPSHOT-2020-04-23-a
-
-# Swift 5.3
-# SWIFT_VERSION = swift-5.3-DEVELOPMENT-SNAPSHOT-2020-04-21-a
-
-SWIFT_VERSION = 5.7
+SWIFT_VERSION = 5.7.1
# set EXECUTABLE_DIRECTORY according to your specific environment
# run swift build and see where the output executable is created
diff --git a/Package.swift b/Package.swift
index 4e48ff7..c86eb3d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.6.1
+// swift-tools-version:5.7.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "mas",
platforms: [
- .macOS(.v10_11)
+ .macOS(.v10_13)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
diff --git a/Package/Distribution.plist b/Package/Distribution.plist
index 09ab7eb..571cb27 100644
--- a/Package/Distribution.plist
+++ b/Package/Distribution.plist
@@ -5,7 +5,7 @@
-
+
diff --git a/README.md b/README.md
index 5f8c452..96b0ebf 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,8 @@ sudo port install mas
#### 🍻 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 (El Capitan).
+for all macOS versions since 10.11 (El Capitan). The newest versions of mas, however, are only available
+for macOS 10.13+ (High Sierra or newer).
To install mas from our tap:
diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift
index 56b7bd6..b8fc765 100644
--- a/Sources/mas/AppStore/ISStoreAccount.swift
+++ b/Sources/mas/AppStore/ISStoreAccount.swift
@@ -14,81 +14,23 @@ private let timeout = 30.0
extension ISStoreAccount: StoreAccount {
static var primaryAccount: Promise {
- if #available(macOS 10.13, *) {
- return race(
- Promise { seal in
- ISServiceProxy.genericShared().accountService
- .primaryAccount { storeAccount in
- seal.fulfill(storeAccount)
- }
- },
- after(seconds: timeout)
- .then {
- Promise(error: MASError.notSignedIn)
+ race(
+ Promise { seal in
+ ISServiceProxy.genericShared().accountService
+ .primaryAccount { storeAccount in
+ seal.fulfill(storeAccount)
}
- )
- }
-
- return .value(CKAccountStore.shared().primaryAccount)
+ },
+ after(seconds: timeout)
+ .then {
+ Promise(error: MASError.notSignedIn)
+ }
+ )
}
- 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.
- // https://github.com/mas-cli/mas/issues/164
- return Promise(error: MASError.notSupported)
- // swiftlint:disable:next superfluous_else
- } else {
- return
- primaryAccount
- .then { account -> Promise in
- if account.isSignedIn {
- return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier))
- }
-
- let password =
- password.isEmpty && !systemDialog
- ? String(validatingUTF8: getpass("Password: ")) ?? ""
- : password
-
- guard !password.isEmpty || systemDialog else {
- return Promise(error: MASError.noPasswordProvided)
- }
-
- let context = ISAuthenticationContext(accountID: 0)
- context.appleIDOverride = appleID
-
- 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?))
- }
- }
- }
-
- if systemDialog {
- return signInPromise
- }
-
- context.demoMode = true
- context.demoAccountName = appleID
- context.demoAccountPassword = password
- context.demoAutologinMode = true
-
- return race(
- signInPromise,
- after(seconds: timeout)
- .then {
- Promise(error: MASError.signInFailed(error: nil))
- }
- )
- }
- }
+ static func signIn(appleID _: String, password _: String, systemDialog _: Bool) -> Promise {
+ // Signing in is no longer possible as of High Sierra.
+ // https://github.com/mas-cli/mas/issues/164
+ Promise(error: MASError.notSupported)
}
}
diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift
index 6f55e76..45cf9a5 100644
--- a/Sources/mas/Commands/SignOut.swift
+++ b/Sources/mas/Commands/SignOut.swift
@@ -18,13 +18,7 @@ extension MAS {
/// Runs the command.
func run() throws {
- 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()
- }
+ ISServiceProxy.genericShared().accountService.signOut()
}
}
}
diff --git a/Sources/mas/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift
index 2168a05..b5a3487 100644
--- a/Sources/mas/ExternalCommands/ExternalCommand.swift
+++ b/Sources/mas/ExternalCommands/ExternalCommand.swift
@@ -56,15 +56,8 @@ extension ExternalCommand {
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
process.arguments = arguments
-
- if #available(macOS 10.13, *) {
- process.executableURL = URL(fileURLWithPath: binaryPath)
- try process.run()
- } else {
- process.launchPath = binaryPath
- process.launch()
- }
-
+ process.executableURL = URL(fileURLWithPath: binaryPath)
+ try process.run()
process.waitUntilExit()
}
}
diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift
index ceea01c..57b8292 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#known-issues
+ // signin command disabled since macOS 10.13 High Sierra: https://github.com/mas-cli/mas#known-issues
describe("signin command") {
it("signs in") {
expect {
diff --git a/script/bottle b/script/bottle
index a54733a..f4cb48c 100755
--- a/script/bottle
+++ b/script/bottle
@@ -17,8 +17,8 @@ BOTTLE_DIR="$BUILD_DIR/bottles"
VERSION=$(script/version)
ROOT_URL="https://github.com/mas-cli/mas/releases/download/v${VERSION}"
-# Supports macOS 10.11 and later
-OS_NAMES=(arm64_monterey monterey arm64_big_sur big_sur catalina mojave high_sierra sierra el_capitan)
+# Supports macOS 10.13 and later
+OS_NAMES=(arm64_monterey monterey arm64_big_sur big_sur catalina mojave high_sierra)
# Semantic version number split into a list using Ugly, bash 3 compatible syntax
IFS=" " read -r -a CURRENT_OS_VERSION <<<"$(sw_vers -productVersion | sed 's/\./ /g'))"
From 705563ced99694dbc05d0c2d2583081575fdfc62 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Sun, 27 Oct 2024 14:53:38 -0400
Subject: [PATCH 03/21] Upgrade PromiseKit to 8.1.2.
Partial #613
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Package.resolved | 4 ++--
Package.swift | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Package.resolved b/Package.resolved
index 80d3efe..b8cf4ef 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/PromiseKit.git",
"state" : {
- "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
- "version" : "6.22.1"
+ "revision" : "6fcc08077124e9747f1ec7bd8bb78f5caffe5a79",
+ "version" : "8.1.2"
}
},
{
diff --git a/Package.swift b/Package.swift
index c86eb3d..79429f6 100644
--- a/Package.swift
+++ b/Package.swift
@@ -20,7 +20,7 @@ let package = Package(
.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/PromiseKit.git", from: "8.1.2"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
],
From e79e6283ac90272a738e57f32dd8e1ff744d7297 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Sun, 27 Oct 2024 14:54:07 -0400
Subject: [PATCH 04/21] Use `ISO8601DateFormatter`.
Resolve #613
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Formatters/AppInfoFormatter.swift | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift
index a162d99..710a7b5 100644
--- a/Sources/mas/Formatters/AppInfoFormatter.swift
+++ b/Sources/mas/Formatters/AppInfoFormatter.swift
@@ -47,12 +47,8 @@ enum AppInfoFormatter {
/// - Parameter serverDate: String containing a date in ISO-8601 format.
/// - Returns: Simple date format.
private static func humanReadableDate(_ serverDate: String) -> String {
- let serverDateFormatter = DateFormatter()
- serverDateFormatter.locale = Locale(identifier: "en_US_POSIX")
- serverDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
-
- let humanDateFormatter = DateFormatter()
- humanDateFormatter.dateFormat = "yyyy-MM-dd"
- return serverDateFormatter.date(from: serverDate).flatMap(humanDateFormatter.string(from:)) ?? ""
+ let humanDateFormatter = ISO8601DateFormatter()
+ humanDateFormatter.formatOptions = [.withFullDate]
+ return ISO8601DateFormatter().date(from: serverDate).map(humanDateFormatter.string(from:)) ?? ""
}
}
From 2a496b1f98218d06f9212611fd69b276e19d17cd Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Sat, 26 Oct 2024 11:41:01 -0400
Subject: [PATCH 05/21] Refactor `AppStoreSearcher` code.
Move code from `AppStoreSearcher` to `ITunesSearchAppStoreSearcher`.
Improve DocC.
Improve Quick test names.
Resolve #607
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
.../mas/Controllers/AppStoreSearcher.swift | 89 ++---------
.../ITunesSearchAppStoreSearcher.swift | 139 ++++++++++++++----
.../ITunesSearchAppStoreSearcherSpec.swift | 4 +-
3 files changed, 119 insertions(+), 113 deletions(-)
diff --git a/Sources/mas/Controllers/AppStoreSearcher.swift b/Sources/mas/Controllers/AppStoreSearcher.swift
index 4e5b65e..13e1663 100644
--- a/Sources/mas/Controllers/AppStoreSearcher.swift
+++ b/Sources/mas/Controllers/AppStoreSearcher.swift
@@ -11,86 +11,15 @@ import PromiseKit
/// Protocol for searching the MAS catalog.
protocol AppStoreSearcher {
+ /// Looks up app details.
+ ///
+ /// - Parameter appID: App ID.
+ /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
+ /// or an `Error` if any problems occur.
func lookup(appID: AppID) -> Promise
+ /// Searches for apps.
+ ///
+ /// - Parameter searchTerm: Term for which to search.
+ /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`.
func search(for searchTerm: String) -> Promise<[SearchResult]>
}
-
-enum Entity: String {
- case desktopSoftware
- case macSoftware
- case iPadSoftware
- 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 AppStoreSearcher {
- /// Builds the search URL for an app.
- ///
- /// - 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,
- inCountry country: String?,
- ofEntity entity: Entity = .desktopSoftware
- ) -> URL? {
- url(.search, searchTerm, inCountry: country, ofEntity: entity)
- }
-
- /// Builds the lookup URL for an app.
- ///
- /// - 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,
- 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
- }
-
- var queryItems = [
- URLQueryItem(name: "media", value: "software"),
- URLQueryItem(name: "entity", value: entity.rawValue),
- ]
-
- if let country {
- queryItems.append(URLQueryItem(name: "country", value: country))
- }
-
- queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue))
-
- components.queryItems = queryItems
-
- return components.url
- }
-}
diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
index c6de321..cc1939f 100644
--- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
+++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
@@ -32,39 +32,11 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
self.networkManager = networkManager
}
- /// Searches for an app.
- ///
- /// - Parameter searchTerm: a search term matched against app names
- /// - Returns: A Promise of an Array of SearchResults matching searchTerm
- 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]
- if SysCtlSystemCommand.isAppleSilicon {
- entities += [.iPadSoftware, .iPhoneSoftware]
- }
-
- let results = entities.map { entity in
- guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else {
- fatalError("Failed to build URL for \(searchTerm)")
- }
- return loadSearchResults(url)
- }
-
- // Combine the results, removing any duplicates.
- var seenAppIDs = Set()
- return when(fulfilled: results)
- .flatMapValues { $0 }
- .filterValues { result in
- seenAppIDs.insert(result.trackId).inserted
- }
- }
-
/// Looks up app details.
///
- /// - 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.
+ /// - Parameter appID: App ID.
+ /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
+ /// or an `Error` if any problems occur.
func lookup(appID: AppID) -> Promise {
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
fatalError("Failed to build URL for \(appID)")
@@ -105,6 +77,34 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
}
}
+ /// Searches for apps from the MAS.
+ ///
+ /// - Parameter searchTerm: Term for which to search in the MAS.
+ /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`.
+ 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]
+ if SysCtlSystemCommand.isAppleSilicon {
+ entities += [.iPadSoftware, .iPhoneSoftware]
+ }
+
+ let results = entities.map { entity in
+ guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else {
+ fatalError("Failed to build URL for \(searchTerm)")
+ }
+ return loadSearchResults(url)
+ }
+
+ // Combine the results, removing any duplicates.
+ var seenAppIDs = Set()
+ return when(fulfilled: results)
+ .flatMapValues { $0 }
+ .filterValues { result in
+ seenAppIDs.insert(result.trackId).inserted
+ }
+ }
+
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
firstly {
networkManager.loadData(from: url)
@@ -137,4 +137,81 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
return version
}
}
+
+ /// Builds the search URL for an app.
+ ///
+ /// - 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,
+ inCountry country: String?,
+ ofEntity entity: Entity = .desktopSoftware
+ ) -> URL? {
+ url(.search, searchTerm, inCountry: country, ofEntity: entity)
+ }
+
+ /// Builds the lookup URL for an app.
+ ///
+ /// - Parameters:
+ /// - appID: App ID.
+ /// - 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.
+ private 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
+ }
+
+ var queryItems = [
+ URLQueryItem(name: "media", value: "software"),
+ URLQueryItem(name: "entity", value: entity.rawValue),
+ ]
+
+ if let country {
+ queryItems.append(URLQueryItem(name: "country", value: country))
+ }
+
+ queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue))
+
+ components.queryItems = queryItems
+
+ return components.url
+ }
+}
+
+enum Entity: String {
+ case desktopSoftware
+ case macSoftware
+ case iPadSoftware
+ case iPhoneSoftware = "software"
+}
+
+private enum URLAction {
+ case lookup
+ case search
+
+ var queryItemName: String {
+ switch self {
+ case .lookup:
+ return "id"
+ case .search:
+ return "term"
+ }
+ }
}
diff --git a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift
index 3b72ee8..828ff31 100644
--- a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift
+++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift
@@ -17,13 +17,13 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
MAS.initialize()
}
describe("url string") {
- it("contains the app name") {
+ it("contains the search term") {
expect {
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") {
+ it("contains the encoded search term") {
expect {
ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString
}
From 5bfd83f3c499b31ea03654e4de232199c04e44a6 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 01:00:48 -0400
Subject: [PATCH 06/21] Cleanup help.
Resolve #616
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/List.swift | 2 +-
Sources/mas/Commands/Outdated.swift | 2 +-
Sources/mas/Commands/Uninstall.swift | 2 +-
Sources/mas/Commands/Upgrade.swift | 2 +-
contrib/completion/mas.fish | 8 ++++----
5 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift
index 6817fe5..6cd6652 100644
--- a/Sources/mas/Commands/List.swift
+++ b/Sources/mas/Commands/List.swift
@@ -12,7 +12,7 @@ extension MAS {
/// Command which lists all installed apps.
struct List: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "List apps installed from the Mac App Store for the Apple ID of the current macOS user"
+ abstract: "List apps installed from the Mac App Store"
)
/// Runs the command.
diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift
index 81b447a..ecabb7e 100644
--- a/Sources/mas/Commands/Outdated.swift
+++ b/Sources/mas/Commands/Outdated.swift
@@ -15,7 +15,7 @@ extension MAS {
/// ready to be installed from the Mac App Store.
struct Outdated: ParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "List pending app updates from the Mac App Store for the Apple ID of the current macOS user"
+ abstract: "List pending app updates from the Mac App Store"
)
@Flag(help: "Display warnings about apps unknown to the Mac App Store")
diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift
index 343d9df..0cada84 100644
--- a/Sources/mas/Commands/Uninstall.swift
+++ b/Sources/mas/Commands/Uninstall.swift
@@ -13,7 +13,7 @@ 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 for the Apple ID of the current macOS user"
+ abstract: "Uninstall app installed from the Mac App Store"
)
/// Flag indicating that removal shouldn't be performed.
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 25743f0..2770f39 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -15,7 +15,7 @@ extension MAS {
struct Upgrade: ParsableCommand {
static let configuration = CommandConfiguration(
abstract:
- "Upgrade outdated app(s) installed from the Mac App Store for the Apple ID of the current macOS user"
+ "Upgrade outdated app(s) installed from the Mac App Store"
)
@Argument(help: "App ID(s)/app name(s)")
diff --git a/contrib/completion/mas.fish b/contrib/completion/mas.fish
index eccb749..05101e5 100644
--- a/contrib/completion/mas.fish
+++ b/contrib/completion/mas.fish
@@ -40,7 +40,7 @@ complete -c mas -n "__fish_use_subcommand" -f -a install -d "Install previously
complete -c mas -n "__fish_seen_subcommand_from help" -xa "install"
complete -c mas -n "__fish_seen_subcommand_from install lucky" -l force -d "Force reinstall"
### list
-complete -c mas -n "__fish_use_subcommand" -f -a list -d "List apps installed from the Mac App Store for the Apple ID of the current macOS user"
+complete -c mas -n "__fish_use_subcommand" -f -a list -d "List apps installed from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "list"
### lucky
complete -c mas -n "__fish_use_subcommand" -f -a lucky -d "Install the first app returned from searching the Mac App Store (app must have been previously purchased)"
@@ -49,7 +49,7 @@ complete -c mas -n "__fish_seen_subcommand_from help" -xa "lucky"
complete -c mas -n "__fish_use_subcommand" -f -a open -d "Open 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 "List pending app updates from the Mac App Store for the Apple ID of the current macOS user"
+complete -c mas -n "__fish_use_subcommand" -f -a outdated -d "List pending app updates from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "outdated"
complete -c mas -n "__fish_seen_subcommand_from outdated" -l verbose -d "Display warnings about apps unknown to the Mac App Store"
### purchase
@@ -71,12 +71,12 @@ complete -c mas -n "__fish_seen_subcommand_from signin" -l dialog -d "Provide pa
complete -c mas -n "__fish_use_subcommand" -f -a signout -d "Sign out of the Apple ID currently signed in in the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "signout"
### uninstall
-complete -c mas -n "__fish_use_subcommand" -f -a uninstall -d "Uninstall app installed from the Mac App Store for the Apple ID of the current macOS user"
+complete -c mas -n "__fish_use_subcommand" -f -a uninstall -d "Uninstall app installed from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "uninstall"
complete -c mas -n "__fish_seen_subcommand_from uninstall" -l dry-run -d "Perform dry run"
complete -c mas -n "__fish_seen_subcommand_from uninstall" -x -a "(__fish_mas_list_installed)"
### upgrade
-complete -c mas -n "__fish_use_subcommand" -f -a upgrade -d "Upgrade outdated app(s) from the Mac App Store for the Apple ID of the current macOS user"
+complete -c mas -n "__fish_use_subcommand" -f -a upgrade -d "Upgrade outdated app(s) from the Mac App Store"
complete -c mas -n "__fish_seen_subcommand_from help" -xa "upgrade"
complete -c mas -n "__fish_seen_subcommand_from upgrade" -x -a "(__fish_mas_outdated_installed)"
### vendor
From c0fffeddf326d12211d1769444ba546308919ea2 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 09:25:16 -0400
Subject: [PATCH 07/21] Open the Mac App Store without any spurious error
dialogs.
Use PromiseKit properly.
Don't use `OpenCommand`.
Resolve #217
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Open.swift | 96 +++++++++++++++++---------
Tests/masTests/Commands/OpenSpec.swift | 18 ++---
2 files changed, 68 insertions(+), 46 deletions(-)
diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift
index 365715e..09f0c8d 100644
--- a/Sources/mas/Commands/Open.swift
+++ b/Sources/mas/Commands/Open.swift
@@ -6,8 +6,10 @@
// Copyright © 2016 mas-cli. All rights reserved.
//
+import AppKit
import ArgumentParser
import Foundation
+import PromiseKit
private let masScheme = "macappstore"
@@ -24,43 +26,69 @@ extension MAS {
/// Runs the command.
func run() throws {
- try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
+ try run(searcher: ITunesSearchAppStoreSearcher())
}
- func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
- do {
- 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 searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
- }
-
- guard var url = URLComponents(string: result.trackViewUrl) else {
- throw MASError.searchFailed
- }
- url.scheme = masScheme
-
- guard let urlString = url.string else {
- printError("Unable to construct URL")
- throw MASError.searchFailed
- }
- do {
- try openCommand.run(arguments: urlString)
- } catch {
- printError("Unable to launch open command")
- throw MASError.searchFailed
- }
- if openCommand.failed {
- printError("Open failed: (\(openCommand.process.terminationReason)) \(openCommand.stderr)")
- throw MASError.searchFailed
- }
- } catch {
- throw error as? MASError ?? .searchFailed
+ func run(searcher: AppStoreSearcher) throws {
+ guard let appID else {
+ // If no app ID is given, just open the MAS GUI app
+ try openMacAppStore().wait()
+ return
}
+ try openInMacAppStore(pageForAppID: appID, searcher: searcher).wait()
+ }
+ }
+}
+
+private func openMacAppStore() -> Promise {
+ Promise { seal in
+ guard let macappstoreSchemeURL = URL(string: "macappstore:") else {
+ throw MASError.notSupported
+ }
+ guard let appURL = NSWorkspace.shared.urlForApplication(toOpen: macappstoreSchemeURL) else {
+ throw MASError.notSupported
+ }
+
+ if #available(macOS 10.15, *) {
+ NSWorkspace.shared.openApplication(at: appURL, configuration: NSWorkspace.OpenConfiguration()) { _, error in
+ if let error {
+ seal.reject(error)
+ }
+ seal.fulfill(())
+ }
+ } else {
+ try NSWorkspace.shared.launchApplication(at: appURL, configuration: [:])
+ seal.fulfill(())
+ }
+ }
+}
+
+private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) -> Promise {
+ Promise { seal in
+ guard let result = try searcher.lookup(appID: appID).wait() else {
+ throw MASError.runtimeError("Unknown app ID \(appID)")
+ }
+
+ guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
+ throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
+ }
+
+ urlComponents.scheme = masScheme
+
+ guard let url = urlComponents.url else {
+ throw MASError.runtimeError("Unable to construct URL from: \(urlComponents)")
+ }
+
+ if #available(macOS 10.15, *) {
+ NSWorkspace.shared.open(url, configuration: NSWorkspace.OpenConfiguration()) { _, error in
+ if let error {
+ seal.reject(error)
+ }
+ seal.fulfill(())
+ }
+ } else {
+ NSWorkspace.shared.open(url)
+ seal.fulfill(())
}
}
}
diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift
index 5f3ee72..081201d 100644
--- a/Tests/masTests/Commands/OpenSpec.swift
+++ b/Tests/masTests/Commands/OpenSpec.swift
@@ -15,7 +15,6 @@ import Quick
public class OpenSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()
- let openCommand = MockOpenSystemCommand()
beforeSuite {
MAS.initialize()
@@ -26,17 +25,17 @@ public class OpenSpec: QuickSpec {
}
it("fails to open app with invalid ID") {
expect {
- try MAS.Open.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Open.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
- try MAS.Open.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Open.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
- it("opens app in MAS") {
+ xit("opens app in MAS") {
let mockResult = SearchResult(
trackId: 1111,
trackViewUrl: "fakescheme://some/url",
@@ -44,18 +43,13 @@ public class OpenSpec: QuickSpec {
)
searcher.apps[mockResult.trackId] = mockResult
expect {
- try MAS.Open.parse([mockResult.trackId.description])
- .run(searcher: searcher, openCommand: openCommand)
- return openCommand.arguments
+ try MAS.Open.parse([mockResult.trackId.description]).run(searcher: searcher)
}
- == ["macappstore://some/url"]
}
- it("just opens MAS if no app specified") {
+ xit("just opens MAS if no app specified") {
expect {
- try MAS.Open.parse([]).run(searcher: searcher, openCommand: openCommand)
- return openCommand.arguments
+ try MAS.Open.parse([]).run(searcher: searcher)
}
- == ["macappstore://"]
}
}
}
From 9ebb01805d735ef83bece9edb3acb405a6888651 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 11:26:11 -0400
Subject: [PATCH 08/21] Replace clunky `ExternalCommand` code that starts new
processes with Apple library calls.
Resolve #620
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Home.swift | 31 ++++-----
Sources/mas/Commands/Open.swift | 45 +++++--------
Sources/mas/Commands/Vendor.swift | 39 +++++-------
.../ITunesSearchAppStoreSearcher.swift | 6 +-
.../ExternalCommands/ExternalCommand.swift | 63 -------------------
.../ExternalCommands/OpenSystemCommand.swift | 23 -------
.../SysCtlSystemCommand.swift | 41 ------------
Sources/mas/Network/URL.swift | 32 ++++++++++
Tests/masTests/Commands/HomeSpec.swift | 10 +--
Tests/masTests/Commands/VendorSpec.swift | 10 +--
.../MockOpenSystemCommand.swift | 27 --------
.../OpenSystemCommandSpec.swift | 30 ---------
12 files changed, 83 insertions(+), 274 deletions(-)
delete mode 100644 Sources/mas/ExternalCommands/ExternalCommand.swift
delete mode 100644 Sources/mas/ExternalCommands/OpenSystemCommand.swift
delete mode 100644 Sources/mas/ExternalCommands/SysCtlSystemCommand.swift
create mode 100644 Sources/mas/Network/URL.swift
delete mode 100644 Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift
delete mode 100644 Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift
diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift
index 8d86a5f..531b6c5 100644
--- a/Sources/mas/Commands/Home.swift
+++ b/Sources/mas/Commands/Home.swift
@@ -7,6 +7,7 @@
//
import ArgumentParser
+import Foundation
extension MAS {
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
@@ -21,29 +22,19 @@ extension MAS {
/// Runs the command.
func run() throws {
- try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
+ try run(searcher: ITunesSearchAppStoreSearcher())
}
- func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
- do {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
- }
-
- do {
- try openCommand.run(arguments: result.trackViewUrl)
- } catch {
- printError("Unable to launch open command")
- throw MASError.searchFailed
- }
- if openCommand.failed {
- let reason = openCommand.process.terminationReason
- printError("Open failed: (\(reason)) \(openCommand.stderr)")
- throw MASError.searchFailed
- }
- } catch {
- throw error as? MASError ?? .searchFailed
+ func run(searcher: AppStoreSearcher) throws {
+ guard let result = try searcher.lookup(appID: appID).wait() else {
+ throw MASError.noSearchResultsFound
}
+
+ guard let url = URL(string: result.trackViewUrl) else {
+ throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
+ }
+
+ try url.open().wait()
}
}
}
diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift
index 09f0c8d..b56f00b 100644
--- a/Sources/mas/Commands/Open.swift
+++ b/Sources/mas/Commands/Open.swift
@@ -8,7 +8,6 @@
import AppKit
import ArgumentParser
-import Foundation
import PromiseKit
private let masScheme = "macappstore"
@@ -35,7 +34,7 @@ extension MAS {
try openMacAppStore().wait()
return
}
- try openInMacAppStore(pageForAppID: appID, searcher: searcher).wait()
+ try openInMacAppStore(pageForAppID: appID, searcher: searcher)
}
}
}
@@ -63,32 +62,20 @@ private func openMacAppStore() -> Promise {
}
}
-private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) -> Promise {
- Promise { seal in
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.runtimeError("Unknown app ID \(appID)")
- }
-
- guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
- throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
- }
-
- urlComponents.scheme = masScheme
-
- guard let url = urlComponents.url else {
- throw MASError.runtimeError("Unable to construct URL from: \(urlComponents)")
- }
-
- if #available(macOS 10.15, *) {
- NSWorkspace.shared.open(url, configuration: NSWorkspace.OpenConfiguration()) { _, error in
- if let error {
- seal.reject(error)
- }
- seal.fulfill(())
- }
- } else {
- NSWorkspace.shared.open(url)
- seal.fulfill(())
- }
+private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
+ guard let result = try searcher.lookup(appID: appID).wait() else {
+ throw MASError.runtimeError("Unknown app ID \(appID)")
}
+
+ guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
+ throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
+ }
+
+ urlComponents.scheme = masScheme
+
+ guard let url = urlComponents.url else {
+ throw MASError.runtimeError("Unable to construct URL from: \(urlComponents)")
+ }
+
+ try url.open().wait()
}
diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift
index d303c94..7256d85 100644
--- a/Sources/mas/Commands/Vendor.swift
+++ b/Sources/mas/Commands/Vendor.swift
@@ -7,6 +7,7 @@
//
import ArgumentParser
+import Foundation
extension MAS {
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
@@ -21,33 +22,23 @@ extension MAS {
/// Runs the command.
func run() throws {
- try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
+ try run(searcher: ITunesSearchAppStoreSearcher())
}
- func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
- do {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
- }
-
- guard let vendorWebsite = result.sellerUrl else {
- throw MASError.noVendorWebsite
- }
-
- do {
- try openCommand.run(arguments: vendorWebsite)
- } catch {
- printError("Unable to launch open command")
- throw MASError.searchFailed
- }
- if openCommand.failed {
- let reason = openCommand.process.terminationReason
- printError("Open failed: (\(reason)) \(openCommand.stderr)")
- throw MASError.searchFailed
- }
- } catch {
- throw error as? MASError ?? .searchFailed
+ func run(searcher: AppStoreSearcher) throws {
+ guard let result = try searcher.lookup(appID: appID).wait() else {
+ throw MASError.noSearchResultsFound
}
+
+ guard let urlString = result.sellerUrl else {
+ throw MASError.noVendorWebsite
+ }
+
+ guard let url = URL(string: urlString) else {
+ throw MASError.runtimeError("Unable to construct URL from: \(urlString)")
+ }
+
+ try url.open().wait()
}
}
}
diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
index cc1939f..babaab9 100644
--- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
+++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
@@ -85,9 +85,9 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
// Search for apps for compatible platforms, in order of preference.
// Macs with Apple Silicon can run iPad and iPhone apps.
var entities = [Entity.desktopSoftware]
- if SysCtlSystemCommand.isAppleSilicon {
- entities += [.iPadSoftware, .iPhoneSoftware]
- }
+ #if arch(arm64)
+ entities += [.iPadSoftware, .iPhoneSoftware]
+ #endif
let results = entities.map { entity in
guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else {
diff --git a/Sources/mas/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift
deleted file mode 100644
index b5a3487..0000000
--- a/Sources/mas/ExternalCommands/ExternalCommand.swift
+++ /dev/null
@@ -1,63 +0,0 @@
-//
-// ExternalCommand.swift
-// mas
-//
-// Created by Ben Chatelain on 1/1/19.
-// Copyright © 2019 mas-cli. All rights reserved.
-//
-
-import Foundation
-
-/// Represents a CLI command.
-protocol ExternalCommand {
- var binaryPath: String { get set }
-
- var process: Process { get }
-
- var stdout: String { get }
- var stderr: String { get }
- var stdoutPipe: Pipe { get }
- var stderrPipe: Pipe { get }
-
- var exitCode: Int32 { get }
- var succeeded: Bool { get }
- var failed: Bool { get }
-
- /// Runs the command.
- func run(arguments: String...) throws
-}
-
-/// Common implementation
-extension ExternalCommand {
- var stdout: String {
- let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
- return String(data: data, encoding: .utf8) ?? ""
- }
-
- var stderr: String {
- let data = stderrPipe.fileHandleForReading.readDataToEndOfFile()
- return String(data: data, encoding: .utf8) ?? ""
- }
-
- var exitCode: Int32 {
- process.terminationStatus
- }
-
- var succeeded: Bool {
- process.terminationReason == .exit && exitCode == 0
- }
-
- var failed: Bool {
- !succeeded
- }
-
- /// Runs the command.
- func run(arguments: String...) throws {
- process.standardOutput = stdoutPipe
- process.standardError = stderrPipe
- process.arguments = arguments
- process.executableURL = URL(fileURLWithPath: binaryPath)
- try process.run()
- process.waitUntilExit()
- }
-}
diff --git a/Sources/mas/ExternalCommands/OpenSystemCommand.swift b/Sources/mas/ExternalCommands/OpenSystemCommand.swift
deleted file mode 100644
index 5d5e3cc..0000000
--- a/Sources/mas/ExternalCommands/OpenSystemCommand.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// OpenSystemCommand.swift
-// mas
-//
-// Created by Ben Chatelain on 1/2/19.
-// Copyright © 2019 mas-cli. All rights reserved.
-//
-
-import Foundation
-
-/// Wrapper for the external 'open' system command (https://ss64.com/osx/open.html).
-struct OpenSystemCommand: ExternalCommand {
- var binaryPath: String
-
- let process = Process()
-
- let stdoutPipe = Pipe()
- let stderrPipe = Pipe()
-
- init(binaryPath: String = "/usr/bin/open") {
- self.binaryPath = binaryPath
- }
-}
diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift
deleted file mode 100644
index 1d04a2e..0000000
--- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// SysCtlSystemCommand.swift
-// mas
-//
-// Created by Chris Araman on 6/3/21.
-// Copyright © 2021 mas-cli. All rights reserved.
-//
-
-import Foundation
-
-/// Wrapper for the external 'sysctl' system command.
-///
-/// See - https://ss64.com/osx/sysctl.html
-struct SysCtlSystemCommand: ExternalCommand {
- static var isAppleSilicon: Bool = {
- 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")
- } catch {
- fatalError("sysctl failed")
- }
-
- guard sysctl.succeeded else {
- fatalError("sysctl failed")
- }
-
- 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/Network/URL.swift b/Sources/mas/Network/URL.swift
new file mode 100644
index 0000000..538c1a9
--- /dev/null
+++ b/Sources/mas/Network/URL.swift
@@ -0,0 +1,32 @@
+//
+// URL.swift
+// mas
+//
+// Created by Ross Goldberg on 2024-10-28.
+// Copyright © 2024 mas-cli. All rights reserved.
+//
+
+import AppKit
+import Foundation
+import PromiseKit
+
+extension URL {
+ func open() -> Promise {
+ Promise { seal in
+ if #available(macOS 10.15, *) {
+ NSWorkspace.shared.open(self, configuration: NSWorkspace.OpenConfiguration()) { _, error in
+ if let error {
+ seal.reject(error)
+ }
+ seal.fulfill(())
+ }
+ } else {
+ if NSWorkspace.shared.open(self) {
+ seal.fulfill(())
+ } else {
+ seal.reject(MASError.runtimeError("Failed to open \(self)"))
+ }
+ }
+ }
+ }
+}
diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift
index 8d3faac..c92b58b 100644
--- a/Tests/masTests/Commands/HomeSpec.swift
+++ b/Tests/masTests/Commands/HomeSpec.swift
@@ -14,7 +14,6 @@ import Quick
public class HomeSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()
- let openCommand = MockOpenSystemCommand()
beforeSuite {
MAS.initialize()
@@ -25,13 +24,13 @@ public class HomeSpec: QuickSpec {
}
it("fails to open app with invalid ID") {
expect {
- try MAS.Home.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Home.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
- try MAS.Home.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Home.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
@@ -43,11 +42,8 @@ public class HomeSpec: QuickSpec {
)
searcher.apps[mockResult.trackId] = mockResult
expect {
- try MAS.Home.parse([String(mockResult.trackId)])
- .run(searcher: searcher, openCommand: openCommand)
- return openCommand.arguments
+ try MAS.Home.parse([String(mockResult.trackId)]).run(searcher: searcher)
}
- == [mockResult.trackViewUrl]
}
}
}
diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift
index 1834cb3..5c7eb95 100644
--- a/Tests/masTests/Commands/VendorSpec.swift
+++ b/Tests/masTests/Commands/VendorSpec.swift
@@ -14,7 +14,6 @@ import Quick
public class VendorSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()
- let openCommand = MockOpenSystemCommand()
beforeSuite {
MAS.initialize()
@@ -25,13 +24,13 @@ public class VendorSpec: QuickSpec {
}
it("fails to open app with invalid ID") {
expect {
- try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
- try MAS.Vendor.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
+ try MAS.Vendor.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
@@ -44,11 +43,8 @@ public class VendorSpec: QuickSpec {
)
searcher.apps[mockResult.trackId] = mockResult
expect {
- try MAS.Vendor.parse([String(mockResult.trackId)])
- .run(searcher: searcher, openCommand: openCommand)
- return openCommand.arguments
+ try MAS.Vendor.parse([String(mockResult.trackId)]).run(searcher: searcher)
}
- == [mockResult.sellerUrl]
}
}
}
diff --git a/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift b/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift
deleted file mode 100644
index da30e13..0000000
--- a/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// MockOpenSystemCommand.swift
-// masTests
-//
-// Created by Ben Chatelain on 1/4/19.
-// Copyright © 2019 mas-cli. All rights reserved.
-//
-
-import Foundation
-
-@testable import mas
-
-class MockOpenSystemCommand: ExternalCommand {
- // Stub out protocol logic
- var succeeded = true
- var arguments: [String] = []
-
- // unused
- var binaryPath = "/dev/null"
- var process = Process()
- var stdoutPipe = Pipe()
- var stderrPipe = Pipe()
-
- func run(arguments: String...) throws {
- self.arguments = arguments
- }
-}
diff --git a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift
deleted file mode 100644
index 9b8c1f5..0000000
--- a/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// OpenSystemCommandSpec.swift
-// masTests
-//
-// Created by Ben Chatelain on 2/24/20.
-// Copyright © 2020 mas-cli. All rights reserved.
-//
-
-import Nimble
-import Quick
-
-@testable import mas
-
-public class OpenSystemCommandSpec: QuickSpec {
- override public func spec() {
- beforeSuite {
- MAS.initialize()
- }
- describe("open system command") {
- context("binary path") {
- it("defaults to the macOS open command") {
- expect(OpenSystemCommand().binaryPath) == "/usr/bin/open"
- }
- it("can be overridden") {
- expect(OpenSystemCommand(binaryPath: "/dev/null").binaryPath) == "/dev/null"
- }
- }
- }
- }
-}
From 14dbd26d3afeb1a418c3fae07163b0a82ef2f310 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:34:18 -0400
Subject: [PATCH 09/21] Simplify `open()` extension func for `URL`.
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Network/URL.swift | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Sources/mas/Network/URL.swift b/Sources/mas/Network/URL.swift
index 538c1a9..8795a85 100644
--- a/Sources/mas/Network/URL.swift
+++ b/Sources/mas/Network/URL.swift
@@ -21,11 +21,11 @@ extension URL {
seal.fulfill(())
}
} else {
- if NSWorkspace.shared.open(self) {
- seal.fulfill(())
- } else {
- seal.reject(MASError.runtimeError("Failed to open \(self)"))
+ guard NSWorkspace.shared.open(self) else {
+ throw MASError.runtimeError("Failed to open \(self)")
}
+
+ seal.fulfill(())
}
}
}
From 0a05cd438f61a5ac75b744c0b327b6b90b6cdfcf Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 14:38:06 -0400
Subject: [PATCH 10/21] Add & use `MASError.unknownAppID(AppID)`.
Partial #533
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/Errors/MASError.swift | 5 +++++
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift
index 531b6c5..f35fa78 100644
--- a/Sources/mas/Commands/Home.swift
+++ b/Sources/mas/Commands/Home.swift
@@ -27,7 +27,7 @@ extension MAS {
func run(searcher: AppStoreSearcher) throws {
guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
+ throw MASError.unknownAppID(appID)
}
guard let url = URL(string: result.trackViewUrl) else {
diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift
index 5cab3fd..49e58c1 100644
--- a/Sources/mas/Commands/Info.swift
+++ b/Sources/mas/Commands/Info.swift
@@ -28,7 +28,7 @@ extension MAS {
func run(searcher: AppStoreSearcher) throws {
do {
guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
+ throw MASError.unknownAppID(appID)
}
print(AppInfoFormatter.format(app: result))
diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift
index 1d31b7f..3b9816a 100644
--- a/Sources/mas/Errors/MASError.swift
+++ b/Sources/mas/Errors/MASError.swift
@@ -27,6 +27,9 @@ enum MASError: Error, Equatable {
case searchFailed
case noSearchResultsFound
+
+ case unknownAppID(AppID)
+
case noVendorWebsite
case notInstalled(appID: AppID)
@@ -83,6 +86,8 @@ extension MASError: CustomStringConvertible {
return "Search failed"
case .noSearchResultsFound:
return "No results found"
+ case .unknownAppID(let appID):
+ return "Unknown app ID \(appID)"
case .noVendorWebsite:
return "App does not have a vendor website"
case .notInstalled(let appID):
From 9eef8b6cb89bad18b313983b3112556ec4bb0bfe Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 14:43:01 -0400
Subject: [PATCH 11/21] Improve download functions.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/AppStore/Downloader.swift | 24 ++++++++++++++----------
Sources/mas/AppStore/SSPurchase.swift | 11 ++++-------
Sources/mas/Commands/Install.swift | 2 +-
Sources/mas/Commands/Lucky.swift | 2 +-
Sources/mas/Commands/Purchase.swift | 2 +-
Sources/mas/Commands/Upgrade.swift | 2 +-
6 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift
index 6435ff6..e95bb1c 100644
--- a/Sources/mas/AppStore/Downloader.swift
+++ b/Sources/mas/AppStore/Downloader.swift
@@ -14,17 +14,16 @@ import StoreFoundation
///
/// - 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.
+/// - purchasing: Flag indicating if the apps will 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.
-func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise {
+func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) -> Promise {
var firstError: Error?
return
appIDs
.reduce(Guarantee.value(())) { previous, appID in
previous.then {
- downloadWithRetries(appID, purchase: purchase)
+ downloadApp(withAppID: appID, purchasing: purchasing)
.recover { error in
if firstError == nil {
firstError = error
@@ -39,10 +38,15 @@ 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)
+private func downloadApp(
+ withAppID appID: AppID,
+ purchasing: Bool = false,
+ withAttemptCount attemptCount: UInt32 = 3
+) -> Promise {
+ SSPurchase()
+ .perform(appID: appID, purchasing: purchasing)
.recover { error in
- guard attempts > 1 else {
+ guard attemptCount > 1 else {
throw error
}
@@ -54,9 +58,9 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt
throw error
}
- let attempts = attempts - 1
+ let attemptCount = attemptCount - 1
printWarning((downloadError ?? error).localizedDescription)
- printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
- return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
+ printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
+ return downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
}
}
diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift
index cf712cd..a12a1c9 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(appID: AppID, purchase: Bool) -> Promise {
+ func perform(appID: AppID, purchasing: Bool) -> Promise {
var parameters: [String: Any] = [
"productType": "C",
"price": 0,
@@ -20,9 +20,11 @@ extension SSPurchase {
"appExtVrsId": 0,
]
- if purchase {
+ if purchasing {
parameters["macappinstalledconfirmed"] = 1
parameters["pricingParameters"] = "STDQ"
+ // Possibly unnecessary…
+ isRedownload = false
} else {
parameters["pricingParameters"] = "STDRDL"
}
@@ -35,11 +37,6 @@ extension SSPurchase {
itemIdentifier = appID
- // Not sure if this is needed…
- if purchase {
- isRedownload = false
- }
-
downloadMetadata = SSDownloadMetadata()
downloadMetadata.kind = "software"
downloadMetadata.itemIdentifier = appID
diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift
index 7247172..32f03fd 100644
--- a/Sources/mas/Commands/Install.swift
+++ b/Sources/mas/Commands/Install.swift
@@ -38,7 +38,7 @@ extension MAS {
}
do {
- try downloadAll(appIDs).wait()
+ try downloadApps(withAppIDs: 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 400e5ef..dc57e41 100644
--- a/Sources/mas/Commands/Lucky.swift
+++ b/Sources/mas/Commands/Lucky.swift
@@ -66,7 +66,7 @@ extension MAS {
printWarning("\(appName) is already installed")
} else {
do {
- try downloadAll([appID]).wait()
+ try downloadApps(withAppIDs: [appID]).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}
diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift
index bdb8a58..dab6d24 100644
--- a/Sources/mas/Commands/Purchase.swift
+++ b/Sources/mas/Commands/Purchase.swift
@@ -35,7 +35,7 @@ extension MAS {
}
do {
- try downloadAll(appIDs, purchase: true).wait()
+ try downloadApps(withAppIDs: appIDs, purchasing: true).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 2770f39..94f8b50 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -46,7 +46,7 @@ extension MAS {
)
do {
- try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
+ try downloadApps(withAppIDs: apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}
From e4bc69cf5d83ed41ebb26c872121ed9254427696 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:23:20 -0400
Subject: [PATCH 12/21] Remove unnecessary tests.
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Tests/masTests/Commands/HomeSpec.swift | 17 -----------------
Tests/masTests/Commands/InfoSpec.swift | 6 ------
Tests/masTests/Commands/OpenSpec.swift | 22 ----------------------
Tests/masTests/Commands/VendorSpec.swift | 18 ------------------
4 files changed, 63 deletions(-)
diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift
index c92b58b..1bfc3f7 100644
--- a/Tests/masTests/Commands/HomeSpec.swift
+++ b/Tests/masTests/Commands/HomeSpec.swift
@@ -22,29 +22,12 @@ public class HomeSpec: QuickSpec {
beforeEach {
searcher.reset()
}
- it("fails to open app with invalid ID") {
- expect {
- try MAS.Home.parse(["--", "-999"]).run(searcher: searcher)
- }
- .to(throwError())
- }
it("can't find app with unknown ID") {
expect {
try MAS.Home.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
- it("opens app on MAS Preview") {
- let mockResult = SearchResult(
- trackId: 1111,
- trackViewUrl: "mas preview url",
- version: "0.0"
- )
- searcher.apps[mockResult.trackId] = mockResult
- expect {
- try MAS.Home.parse([String(mockResult.trackId)]).run(searcher: searcher)
- }
- }
}
}
}
diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift
index 04b393c..b46465c 100644
--- a/Tests/masTests/Commands/InfoSpec.swift
+++ b/Tests/masTests/Commands/InfoSpec.swift
@@ -23,12 +23,6 @@ public class InfoSpec: QuickSpec {
beforeEach {
searcher.reset()
}
- it("fails to open app with invalid ID") {
- expect {
- 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(searcher: searcher)
diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift
index 081201d..6861740 100644
--- a/Tests/masTests/Commands/OpenSpec.swift
+++ b/Tests/masTests/Commands/OpenSpec.swift
@@ -23,34 +23,12 @@ public class OpenSpec: QuickSpec {
beforeEach {
searcher.reset()
}
- it("fails to open app with invalid ID") {
- expect {
- try MAS.Open.parse(["--", "-999"]).run(searcher: searcher)
- }
- .to(throwError())
- }
it("can't find app with unknown ID") {
expect {
try MAS.Open.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
- xit("opens app in MAS") {
- let mockResult = SearchResult(
- trackId: 1111,
- trackViewUrl: "fakescheme://some/url",
- version: "0.0"
- )
- searcher.apps[mockResult.trackId] = mockResult
- expect {
- try MAS.Open.parse([mockResult.trackId.description]).run(searcher: searcher)
- }
- }
- xit("just opens MAS if no app specified") {
- expect {
- try MAS.Open.parse([]).run(searcher: searcher)
- }
- }
}
}
}
diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift
index 5c7eb95..6aa8d8f 100644
--- a/Tests/masTests/Commands/VendorSpec.swift
+++ b/Tests/masTests/Commands/VendorSpec.swift
@@ -22,30 +22,12 @@ public class VendorSpec: QuickSpec {
beforeEach {
searcher.reset()
}
- it("fails to open app with invalid ID") {
- expect {
- try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher)
- }
- .to(throwError())
- }
it("can't find app with unknown ID") {
expect {
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
- it("opens vendor app page in browser") {
- 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"
- )
- searcher.apps[mockResult.trackId] = mockResult
- expect {
- try MAS.Vendor.parse([String(mockResult.trackId)]).run(searcher: searcher)
- }
- }
}
}
}
From e639341d11c052ad9f4ba752c6df8aaeb8687f8d Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Mon, 28 Oct 2024 20:18:49 -0400
Subject: [PATCH 13/21] Refactor to allow `install` & `purchase` to report
unknown app IDs via console instead of cryptically via a dialog.
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/AppStore/Downloader.swift | 37 +++++-
Sources/mas/Commands/Home.swift | 4 +-
Sources/mas/Commands/Info.swift | 6 +-
Sources/mas/Commands/Install.swift | 6 +-
Sources/mas/Commands/Open.swift | 4 +-
Sources/mas/Commands/Outdated.swift | 32 +++---
Sources/mas/Commands/Purchase.swift | 6 +-
Sources/mas/Commands/Upgrade.swift | 21 ++--
Sources/mas/Commands/Vendor.swift | 4 +-
.../mas/Controllers/AppStoreSearcher.swift | 8 +-
.../ITunesSearchAppStoreSearcher.swift | 107 ++++++++----------
Tests/masTests/Commands/HomeSpec.swift | 2 +-
Tests/masTests/Commands/InfoSpec.swift | 2 +-
Tests/masTests/Commands/InstallSpec.swift | 2 +-
Tests/masTests/Commands/OpenSpec.swift | 2 +-
Tests/masTests/Commands/PurchaseSpec.swift | 2 +-
Tests/masTests/Commands/VendorSpec.swift | 2 +-
.../Controllers/MockAppStoreSearcher.swift | 4 +-
18 files changed, 136 insertions(+), 115 deletions(-)
diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift
index e95bb1c..5ec5d61 100644
--- a/Sources/mas/AppStore/Downloader.swift
+++ b/Sources/mas/AppStore/Downloader.swift
@@ -10,10 +10,43 @@ import CommerceKit
import PromiseKit
import StoreFoundation
-/// Downloads a list of apps, one after the other, printing progress to the console.
+/// Sequentially downloads apps, printing progress to the console.
+///
+/// Verifies that each supplied app ID is valid before attempting to download.
///
/// - Parameters:
-/// - appIDs: The IDs of the apps to be downloaded
+/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
+/// - searcher: The `AppStoreSearcher` used to verify app IDs.
+/// - purchasing: Flag indicating if the apps will 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.
+func downloadApps(
+ withAppIDs unverifiedAppIDs: [AppID],
+ verifiedBy searcher: AppStoreSearcher,
+ purchasing: Bool = false
+) -> Promise {
+ when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
+ .then { results in
+ downloadApps(
+ withAppIDs:
+ results.compactMap { result in
+ switch result {
+ case .fulfilled(let searchResult):
+ return searchResult.trackId
+ case .rejected(let error):
+ printError(String(describing: error))
+ return nil
+ }
+ },
+ purchasing: purchasing
+ )
+ }
+}
+
+/// Sequentially downloads apps, printing progress to the console.
+///
+/// - Parameters:
+/// - appIDs: The app IDs of the apps to be downloaded.
/// - purchasing: Flag indicating if the apps will 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.
diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift
index f35fa78..45d4f4f 100644
--- a/Sources/mas/Commands/Home.swift
+++ b/Sources/mas/Commands/Home.swift
@@ -26,9 +26,7 @@ extension MAS {
}
func run(searcher: AppStoreSearcher) throws {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.unknownAppID(appID)
- }
+ let result = try searcher.lookup(appID: appID).wait()
guard let url = URL(string: result.trackViewUrl) else {
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift
index 49e58c1..90f9647 100644
--- a/Sources/mas/Commands/Info.swift
+++ b/Sources/mas/Commands/Info.swift
@@ -27,11 +27,7 @@ extension MAS {
func run(searcher: AppStoreSearcher) throws {
do {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.unknownAppID(appID)
- }
-
- print(AppInfoFormatter.format(app: result))
+ print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
} catch {
throw error as? MASError ?? .searchFailed
}
diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift
index 32f03fd..df79730 100644
--- a/Sources/mas/Commands/Install.swift
+++ b/Sources/mas/Commands/Install.swift
@@ -23,10 +23,10 @@ extension MAS {
/// Runs the command.
func run() throws {
- try run(appLibrary: SoftwareMapAppLibrary())
+ try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
}
- func run(appLibrary: AppLibrary) throws {
+ func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
@@ -38,7 +38,7 @@ extension MAS {
}
do {
- try downloadApps(withAppIDs: appIDs).wait()
+ try downloadApps(withAppIDs: appIDs, verifiedBy: searcher).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 b56f00b..5195f45 100644
--- a/Sources/mas/Commands/Open.swift
+++ b/Sources/mas/Commands/Open.swift
@@ -63,9 +63,7 @@ private func openMacAppStore() -> Promise {
}
private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.runtimeError("Unknown app ID \(appID)")
- }
+ let result = try searcher.lookup(appID: appID).wait()
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift
index ecabb7e..ce0d97b 100644
--- a/Sources/mas/Commands/Outdated.swift
+++ b/Sources/mas/Commands/Outdated.swift
@@ -30,11 +30,22 @@ extension MAS {
_ = try when(
fulfilled:
appLibrary.installedApps.map { installedApp in
- firstly {
- searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
- }
- .done { storeApp in
- guard let storeApp else {
+ searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
+ .done { storeApp in
+ if installedApp.isOutdatedWhenComparedTo(storeApp) {
+ print(
+ """
+ \(installedApp.itemIdentifier) \(installedApp.appName) \
+ (\(installedApp.bundleVersion) -> \(storeApp.version))
+ """
+ )
+ }
+ }
+ .recover { error in
+ guard case MASError.unknownAppID = error else {
+ throw error
+ }
+
if verbose {
printWarning(
"""
@@ -43,18 +54,7 @@ extension MAS {
"""
)
}
- return
}
-
- if installedApp.isOutdatedWhenComparedTo(storeApp) {
- print(
- """
- \(installedApp.itemIdentifier) \(installedApp.appName) \
- (\(installedApp.bundleVersion) -> \(storeApp.version))
- """
- )
- }
- }
}
)
.wait()
diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift
index dab6d24..e624527 100644
--- a/Sources/mas/Commands/Purchase.swift
+++ b/Sources/mas/Commands/Purchase.swift
@@ -20,10 +20,10 @@ extension MAS {
/// Runs the command.
func run() throws {
- try run(appLibrary: SoftwareMapAppLibrary())
+ try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
}
- func run(appLibrary: AppLibrary) throws {
+ func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
@@ -35,7 +35,7 @@ extension MAS {
}
do {
- try downloadApps(withAppIDs: appIDs, purchasing: true).wait()
+ try downloadApps(withAppIDs: appIDs, verifiedBy: searcher, purchasing: true).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 94f8b50..4823976 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -71,16 +71,19 @@ extension MAS {
let promises = apps.map { installedApp in
// only upgrade apps whose local version differs from the store version
- firstly {
- searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
- }
- .map { result -> (SoftwareProduct, SearchResult)? in
- guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
- return nil
+ searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
+ .map { storeApp -> (SoftwareProduct, SearchResult)? in
+ guard installedApp.isOutdatedWhenComparedTo(storeApp) else {
+ return nil
+ }
+ return (installedApp, storeApp)
+ }
+ .recover { error -> Promise<(SoftwareProduct, SearchResult)?> in
+ guard case MASError.unknownAppID = error else {
+ return Promise(error: error)
+ }
+ return .value(nil)
}
-
- return (installedApp, storeApp)
- }
}
return try when(fulfilled: promises).wait().compactMap { $0 }
diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift
index 7256d85..387ee19 100644
--- a/Sources/mas/Commands/Vendor.swift
+++ b/Sources/mas/Commands/Vendor.swift
@@ -26,9 +26,7 @@ extension MAS {
}
func run(searcher: AppStoreSearcher) throws {
- guard let result = try searcher.lookup(appID: appID).wait() else {
- throw MASError.noSearchResultsFound
- }
+ let result = try searcher.lookup(appID: appID).wait()
guard let urlString = result.sellerUrl else {
throw MASError.noVendorWebsite
diff --git a/Sources/mas/Controllers/AppStoreSearcher.swift b/Sources/mas/Controllers/AppStoreSearcher.swift
index 13e1663..51393c2 100644
--- a/Sources/mas/Controllers/AppStoreSearcher.swift
+++ b/Sources/mas/Controllers/AppStoreSearcher.swift
@@ -14,9 +14,11 @@ protocol AppStoreSearcher {
/// Looks up app details.
///
/// - Parameter appID: App ID.
- /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
- /// or an `Error` if any problems occur.
- func lookup(appID: AppID) -> Promise
+ /// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
+ /// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
+ /// An `Promise` for some other `Error` if any problems occur.
+ func lookup(appID: AppID) -> Promise
+
/// Searches for apps.
///
/// - Parameter searchTerm: Term for which to search.
diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
index babaab9..72ef777 100644
--- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
+++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
@@ -32,49 +32,46 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
self.networkManager = networkManager
}
- /// Looks up app details.
- ///
/// - Parameter appID: App ID.
- /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
- /// or an `Error` if any problems occur.
- func lookup(appID: AppID) -> Promise {
+ /// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
+ /// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
+ /// An `Promise` for some other `Error` if any problems occur.
+ func lookup(appID: AppID) -> Promise {
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
fatalError("Failed to build URL for \(appID)")
}
- return firstly {
+ return
loadSearchResults(url)
- }
- .then { results -> Guarantee in
- guard let result = results.first else {
- return .value(nil)
- }
-
- guard let pageURL = URL(string: result.trackViewUrl) else {
- return .value(result)
- }
-
- return firstly {
- self.scrapeAppStoreVersion(pageURL)
- }
- .map { pageVersion in
- guard
- let pageVersion,
- let searchVersion = Version(tolerant: result.version),
- pageVersion > searchVersion
- else {
- return result
+ .then { results -> Guarantee in
+ guard let result = results.first else {
+ throw MASError.unknownAppID(appID)
}
- // Update the search result with the version from the App Store page.
- var result = result
- result.version = pageVersion.description
- return result
+ guard let pageURL = URL(string: result.trackViewUrl) else {
+ return .value(result)
+ }
+
+ return
+ self.scrapeAppStoreVersion(pageURL)
+ .map { pageVersion in
+ guard
+ let pageVersion,
+ let searchVersion = Version(tolerant: result.version),
+ pageVersion > searchVersion
+ else {
+ return result
+ }
+
+ // Update the search result with the version from the App Store page.
+ var result = result
+ result.version = pageVersion.description
+ return result
+ }
+ .recover { _ in
+ // If we were unable to scrape the App Store page, assume compatibility.
+ .value(result)
+ }
}
- .recover { _ in
- // If we were unable to scrape the App Store page, assume compatibility.
- .value(result)
- }
- }
}
/// Searches for apps from the MAS.
@@ -106,36 +103,32 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
}
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
- firstly {
- networkManager.loadData(from: url)
- }
- .map { data in
- do {
- return try JSONDecoder().decode(SearchResultList.self, from: data).results
- } catch {
- throw MASError.jsonParsing(data: data)
+ networkManager.loadData(from: url)
+ .map { data in
+ do {
+ return try JSONDecoder().decode(SearchResultList.self, from: data).results
+ } catch {
+ throw MASError.jsonParsing(data: data)
+ }
}
- }
}
/// 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)
- }
- .map { data in
- guard
- let html = String(data: data, encoding: .utf8),
- let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
- let version = Version(tolerant: capture)
- else {
- return nil
- }
+ networkManager.loadData(from: pageURL)
+ .map { data in
+ guard
+ let html = String(data: data, encoding: .utf8),
+ let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
+ let version = Version(tolerant: capture)
+ else {
+ return nil
+ }
- return version
- }
+ return version
+ }
}
/// Builds the search URL for an app.
diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift
index 1bfc3f7..c5a0455 100644
--- a/Tests/masTests/Commands/HomeSpec.swift
+++ b/Tests/masTests/Commands/HomeSpec.swift
@@ -26,7 +26,7 @@ public class HomeSpec: QuickSpec {
expect {
try MAS.Home.parse(["999"]).run(searcher: searcher)
}
- .to(throwError(MASError.noSearchResultsFound))
+ .to(throwError(MASError.unknownAppID(999)))
}
}
}
diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift
index b46465c..1cad137 100644
--- a/Tests/masTests/Commands/InfoSpec.swift
+++ b/Tests/masTests/Commands/InfoSpec.swift
@@ -27,7 +27,7 @@ public class InfoSpec: QuickSpec {
expect {
try MAS.Info.parse(["999"]).run(searcher: searcher)
}
- .to(throwError(MASError.noSearchResultsFound))
+ .to(throwError(MASError.unknownAppID(999)))
}
it("displays app details") {
let mockResult = SearchResult(
diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift
index e2074c6..ea253f7 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: MockAppLibrary())
+ try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
}
diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift
index 6861740..c9cf874 100644
--- a/Tests/masTests/Commands/OpenSpec.swift
+++ b/Tests/masTests/Commands/OpenSpec.swift
@@ -27,7 +27,7 @@ public class OpenSpec: QuickSpec {
expect {
try MAS.Open.parse(["999"]).run(searcher: searcher)
}
- .to(throwError(MASError.noSearchResultsFound))
+ .to(throwError(MASError.unknownAppID(999)))
}
}
}
diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift
index ac9db18..1ab423b 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: MockAppLibrary())
+ try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
}
diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift
index 6aa8d8f..7eb0f58 100644
--- a/Tests/masTests/Commands/VendorSpec.swift
+++ b/Tests/masTests/Commands/VendorSpec.swift
@@ -26,7 +26,7 @@ public class VendorSpec: QuickSpec {
expect {
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
}
- .to(throwError(MASError.noSearchResultsFound))
+ .to(throwError(MASError.unknownAppID(999)))
}
}
}
diff --git a/Tests/masTests/Controllers/MockAppStoreSearcher.swift b/Tests/masTests/Controllers/MockAppStoreSearcher.swift
index 1df0ef6..c2eb5fe 100644
--- a/Tests/masTests/Controllers/MockAppStoreSearcher.swift
+++ b/Tests/masTests/Controllers/MockAppStoreSearcher.swift
@@ -17,9 +17,9 @@ class MockAppStoreSearcher: AppStoreSearcher {
.value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 })
}
- func lookup(appID: AppID) -> Promise {
+ func lookup(appID: AppID) -> Promise {
guard let result = apps[appID] else {
- return Promise(error: MASError.noSearchResultsFound)
+ return Promise(error: MASError.unknownAppID(appID))
}
return .value(result)
From 3d264675bfd7b4b5add195d462c12d23a4979346 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 00:16:35 -0400
Subject: [PATCH 14/21] Move code to more appropriate files.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Uninstall.swift | 4 +--
.../Controllers/SoftwareMapAppLibrary.swift | 22 ++------------
Sources/mas/MAS.swift | 9 ------
Sources/mas/Models/AppID.swift | 17 +++++++++++
.../{Controllers => Utilities}/Finder.swift | 0
Sources/mas/Utilities/ProcessInfo.swift | 29 +++++++++++++++++++
6 files changed, 50 insertions(+), 31 deletions(-)
create mode 100644 Sources/mas/Models/AppID.swift
rename Sources/mas/{Controllers => Utilities}/Finder.swift (100%)
create mode 100644 Sources/mas/Utilities/ProcessInfo.swift
diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift
index 0cada84..46ea06e 100644
--- a/Sources/mas/Commands/Uninstall.swift
+++ b/Sources/mas/Commands/Uninstall.swift
@@ -32,12 +32,12 @@ extension MAS {
throw MASError.macOSUserMustBeRoot
}
- guard let username = getSudoUsername() else {
+ guard let username = ProcessInfo.processInfo.sudoUsername else {
throw MASError.runtimeError("Could not determine the original username")
}
guard
- let uid = getSudoUID(),
+ let uid = ProcessInfo.processInfo.sudoUID,
seteuid(uid) == 0
else {
throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'")
diff --git a/Sources/mas/Controllers/SoftwareMapAppLibrary.swift b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift
index 7fc48e7..77e47e5 100644
--- a/Sources/mas/Controllers/SoftwareMapAppLibrary.swift
+++ b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift
@@ -43,24 +43,6 @@ class SoftwareMapAppLibrary: AppLibrary {
}
}
-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)
@@ -75,11 +57,11 @@ private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t
}
private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] {
- guard let sudoUID = getSudoUID() else {
+ guard let sudoUID = ProcessInfo.processInfo.sudoUID else {
throw MASError.runtimeError("Failed to get original uid")
}
- guard let sudoGID = getSudoGID() else {
+ guard let sudoGID = ProcessInfo.processInfo.sudoGID else {
throw MASError.runtimeError("Failed to get original gid")
}
diff --git a/Sources/mas/MAS.swift b/Sources/mas/MAS.swift
index aee1262..56e814f 100644
--- a/Sources/mas/MAS.swift
+++ b/Sources/mas/MAS.swift
@@ -7,7 +7,6 @@
//
import ArgumentParser
-import Foundation
import PromiseKit
@main
@@ -55,11 +54,3 @@ struct MAS: ParsableCommand {
Self.initialize()
}
}
-
-typealias AppID = UInt64
-
-extension NSNumber {
- var appIDValue: AppID {
- uint64Value
- }
-}
diff --git a/Sources/mas/Models/AppID.swift b/Sources/mas/Models/AppID.swift
new file mode 100644
index 0000000..0b8689f
--- /dev/null
+++ b/Sources/mas/Models/AppID.swift
@@ -0,0 +1,17 @@
+//
+// AppID.swift
+// mas
+//
+// Created by Ross Goldberg on 2024-10-29.
+// Copyright © 2024 mas-cli. All rights reserved.
+//
+
+import Foundation
+
+typealias AppID = UInt64
+
+extension NSNumber {
+ var appIDValue: AppID {
+ uint64Value
+ }
+}
diff --git a/Sources/mas/Controllers/Finder.swift b/Sources/mas/Utilities/Finder.swift
similarity index 100%
rename from Sources/mas/Controllers/Finder.swift
rename to Sources/mas/Utilities/Finder.swift
diff --git a/Sources/mas/Utilities/ProcessInfo.swift b/Sources/mas/Utilities/ProcessInfo.swift
new file mode 100644
index 0000000..a7e59b1
--- /dev/null
+++ b/Sources/mas/Utilities/ProcessInfo.swift
@@ -0,0 +1,29 @@
+//
+// ProcessInfo.swift
+// mas
+//
+// Created by Ross Goldberg on 2024-10-29.
+// Copyright © 2024 mas-cli. All rights reserved.
+//
+
+import Foundation
+
+extension ProcessInfo {
+ var sudoUsername: String? {
+ environment["SUDO_USER"]
+ }
+
+ var sudoUID: uid_t? {
+ guard let uid = environment["SUDO_UID"] else {
+ return nil
+ }
+ return uid_t(uid)
+ }
+
+ var sudoGID: gid_t? {
+ guard let gid = environment["SUDO_GID"] else {
+ return nil
+ }
+ return gid_t(gid)
+ }
+}
From 64ab55718afd96dddb2b18591db2c395a1d71c15 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 03:38:12 -0400
Subject: [PATCH 15/21] Improve `upgrade` unknown app ID/name error output.
Don't output a warning if nothing requires an upgrade.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Upgrade.swift | 17 ++++++++++++-----
Tests/masTests/Commands/UpgradeSpec.swift | 2 +-
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 4823976..21ec1ba 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -35,7 +35,6 @@ extension MAS {
}
guard !apps.isEmpty else {
- printWarning("Nothing found to upgrade")
return
}
@@ -59,14 +58,22 @@ extension MAS {
let apps =
appIDOrNames.isEmpty
? appLibrary.installedApps
- : appIDOrNames.flatMap { appID in
- if let appID = AppID(appID) {
+ : appIDOrNames.flatMap { appIDOrName in
+ if let appID = AppID(appIDOrName) {
// argument is an AppID, lookup apps by id using argument
- return appLibrary.installedApps(withAppID: appID)
+ let installedApps = appLibrary.installedApps(withAppID: appID)
+ if installedApps.isEmpty {
+ printError("Unknown app ID \(appID)")
+ }
+ return installedApps
}
// argument is not an AppID, lookup apps by name using argument
- return appLibrary.installedApps(named: appID)
+ let installedApps = appLibrary.installedApps(named: appIDOrName)
+ if installedApps.isEmpty {
+ printError("Unknown app name '\(appIDOrName)'")
+ }
+ return installedApps
}
let promises = apps.map { installedApp in
diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift
index 6630777..c3a805f 100644
--- a/Tests/masTests/Commands/UpgradeSpec.swift
+++ b/Tests/masTests/Commands/UpgradeSpec.swift
@@ -25,7 +25,7 @@ public class UpgradeSpec: QuickSpec {
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
}
- == "Warning: Nothing found to upgrade\n"
+ .toNot(throwError())
}
}
}
From 0b9c84bcb881bd2bad9c927b1d2c535218a35d12 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 03:59:14 -0400
Subject: [PATCH 16/21] Improve help output for command arguments.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Install.swift | 2 +-
Sources/mas/Commands/Purchase.swift | 2 +-
Sources/mas/Commands/Upgrade.swift | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift
index df79730..a5c73aa 100644
--- a/Sources/mas/Commands/Install.swift
+++ b/Sources/mas/Commands/Install.swift
@@ -18,7 +18,7 @@ extension MAS {
@Flag(help: "Force reinstall")
var force = false
- @Argument(help: "App ID(s)")
+ @Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
var appIDs: [AppID]
/// Runs the command.
diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift
index e624527..e0a7211 100644
--- a/Sources/mas/Commands/Purchase.swift
+++ b/Sources/mas/Commands/Purchase.swift
@@ -15,7 +15,7 @@ extension MAS {
abstract: "\"Purchase\" and install free apps from the Mac App Store"
)
- @Argument(help: "App ID(s)")
+ @Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
var appIDs: [AppID]
/// Runs the command.
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 21ec1ba..012cb50 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -18,7 +18,7 @@ extension MAS {
"Upgrade outdated app(s) installed from the Mac App Store"
)
- @Argument(help: "App ID(s)/app name(s)")
+ @Argument(help: ArgumentHelp("App ID/app name", valueName: "app-id-or-name"))
var appIDOrNames: [String] = []
/// Runs the command.
From 05674b2534aa4c09cd4dd3c1033b48744475ce85 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 04:02:11 -0400
Subject: [PATCH 17/21] Remove duplicate error output for `lucky`.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Lucky.swift | 1 -
1 file changed, 1 deletion(-)
diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift
index dc57e41..f46a92a 100644
--- a/Sources/mas/Commands/Lucky.swift
+++ b/Sources/mas/Commands/Lucky.swift
@@ -38,7 +38,6 @@ extension MAS {
do {
let results = try searcher.search(for: searchTerm).wait()
guard let result = results.first else {
- printError("No results found")
throw MASError.noSearchResultsFound
}
From 31dfe8117452d125f6a9ac1b64b55d8976e8a442 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 04:08:56 -0400
Subject: [PATCH 18/21] Improve `lucky` & `search` error message.
Partial #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Errors/MASError.swift | 2 +-
Tests/masTests/Errors/MASErrorTestCase.swift | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift
index 3b9816a..f247fe3 100644
--- a/Sources/mas/Errors/MASError.swift
+++ b/Sources/mas/Errors/MASError.swift
@@ -85,7 +85,7 @@ extension MASError: CustomStringConvertible {
case .searchFailed:
return "Search failed"
case .noSearchResultsFound:
- return "No results found"
+ return "No apps found"
case .unknownAppID(let appID):
return "Unknown app ID \(appID)"
case .noVendorWebsite:
diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift
index 027a196..3a115b3 100644
--- a/Tests/masTests/Errors/MASErrorTestCase.swift
+++ b/Tests/masTests/Errors/MASErrorTestCase.swift
@@ -100,7 +100,7 @@ class MASErrorTestCase: XCTestCase {
func testNoSearchResultsFound() {
error = .noSearchResultsFound
- XCTAssertEqual(error.description, "No results found")
+ XCTAssertEqual(error.description, "No apps found")
}
func testNoVendorWebsite() {
From 1b43c89becc7e9f45e241a7d03c534a01caac38c Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 04:56:37 -0400
Subject: [PATCH 19/21] Single source for Unknown app ID message.
Resolve #533
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Commands/Upgrade.swift | 2 +-
Sources/mas/Errors/MASError.swift | 2 +-
Sources/mas/Models/AppID.swift | 6 ++++++
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift
index 012cb50..3f1d3f4 100644
--- a/Sources/mas/Commands/Upgrade.swift
+++ b/Sources/mas/Commands/Upgrade.swift
@@ -63,7 +63,7 @@ extension MAS {
// argument is an AppID, lookup apps by id using argument
let installedApps = appLibrary.installedApps(withAppID: appID)
if installedApps.isEmpty {
- printError("Unknown app ID \(appID)")
+ printError(appID.unknownMessage)
}
return installedApps
}
diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift
index f247fe3..3c6ebfc 100644
--- a/Sources/mas/Errors/MASError.swift
+++ b/Sources/mas/Errors/MASError.swift
@@ -87,7 +87,7 @@ extension MASError: CustomStringConvertible {
case .noSearchResultsFound:
return "No apps found"
case .unknownAppID(let appID):
- return "Unknown app ID \(appID)"
+ return appID.unknownMessage
case .noVendorWebsite:
return "App does not have a vendor website"
case .notInstalled(let appID):
diff --git a/Sources/mas/Models/AppID.swift b/Sources/mas/Models/AppID.swift
index 0b8689f..d7d1788 100644
--- a/Sources/mas/Models/AppID.swift
+++ b/Sources/mas/Models/AppID.swift
@@ -10,6 +10,12 @@ import Foundation
typealias AppID = UInt64
+extension AppID {
+ var unknownMessage: String {
+ "Unknown app ID \(self)"
+ }
+}
+
extension NSNumber {
var appIDValue: AppID {
uint64Value
From 37823cc4dd0e5bba1e8065782ff5ef3f04a9d397 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Tue, 29 Oct 2024 06:14:19 -0400
Subject: [PATCH 20/21] Fix typos in DocC.
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
Sources/mas/Controllers/AppStoreSearcher.swift | 4 ++--
Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Sources/mas/Controllers/AppStoreSearcher.swift b/Sources/mas/Controllers/AppStoreSearcher.swift
index 51393c2..e72c3f3 100644
--- a/Sources/mas/Controllers/AppStoreSearcher.swift
+++ b/Sources/mas/Controllers/AppStoreSearcher.swift
@@ -16,12 +16,12 @@ protocol AppStoreSearcher {
/// - Parameter appID: App ID.
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
- /// An `Promise` for some other `Error` if any problems occur.
+ /// A `Promise` for some other `Error` if any problems occur.
func lookup(appID: AppID) -> Promise
/// Searches for apps.
///
/// - Parameter searchTerm: Term for which to search.
- /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`.
+ /// - Returns: A `Promise` for an `Array` of `SearchResult`s matching `searchTerm`.
func search(for searchTerm: String) -> Promise<[SearchResult]>
}
diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
index 72ef777..100b9d3 100644
--- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
+++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
@@ -35,7 +35,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
/// - Parameter appID: App ID.
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
- /// An `Promise` for some other `Error` if any problems occur.
+ /// A `Promise` for some other `Error` if any problems occur.
func lookup(appID: AppID) -> Promise {
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
fatalError("Failed to build URL for \(appID)")
@@ -77,7 +77,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
/// Searches for apps from the MAS.
///
/// - Parameter searchTerm: Term for which to search in the MAS.
- /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`.
+ /// - Returns: A `Promise` for an `Array` of `SearchResult`s matching `searchTerm`.
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.
From 5209ccd16b3bc8077d3530ec4c01dccddbd84566 Mon Sep 17 00:00:00 2001
From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
Date: Wed, 30 Oct 2024 03:19:30 -0400
Subject: [PATCH 21/21] Improve downloading output.
Update linting for access control on extensions.
Resolve #307
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
---
.swift-format | 2 +-
.swiftformat | 1 -
.swiftlint.yml | 1 +
.../AppStore/PurchaseDownloadObserver.swift | 34 ++++++++++++++++---
Tests/masTests/.swift-format | 2 +-
5 files changed, 33 insertions(+), 7 deletions(-)
diff --git a/.swift-format b/.swift-format
index 693d879..2f15f28 100644
--- a/.swift-format
+++ b/.swift-format
@@ -28,7 +28,7 @@
"NeverForceUnwrap": true,
"NeverUseForceTry": true,
"NeverUseImplicitlyUnwrappedOptionals": true,
- "NoAccessLevelOnExtensionDeclaration": true,
+ "NoAccessLevelOnExtensionDeclaration": false,
"NoAssignmentInExpressions": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
diff --git a/.swiftformat b/.swiftformat
index 9089de8..d9ca163 100644
--- a/.swiftformat
+++ b/.swiftformat
@@ -31,7 +31,6 @@
# Rule options
--commas always
---extensionacl on-declarations
--hexliteralcase lowercase
--importgrouping testable-last
--lineaftermarks false
diff --git a/.swiftlint.yml b/.swiftlint.yml
index 3d19626..8457f4e 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -22,6 +22,7 @@ disabled_rules:
- function_body_length
- inert_defer
- legacy_objc_type
+- no_extension_access_modifier
- no_grouping_extension
- number_separator
- one_declaration_per_file
diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift
index 30b968c..fa3e9e7 100644
--- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift
+++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift
@@ -9,11 +9,16 @@
import CommerceKit
import StoreFoundation
+private let downloadingPhase: Int64 = 0
+private let installingPhase: Int64 = 1
+private let downloadedPhase: Int64 = 5
+
@objc
class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
let purchase: SSPurchase
var completionHandler: (() -> Void)?
var errorHandler: ((MASError) -> Void)?
+ var priorPhaseType: Int64?
init(purchase: SSPurchase) {
self.purchase = purchase
@@ -30,6 +35,21 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
if status.isFailed || status.isCancelled {
queue.removeDownload(withItemIdentifier: download.metadata.itemIdentifier)
} else {
+ if priorPhaseType != status.activePhase.phaseType {
+ switch status.activePhase.phaseType {
+ case downloadedPhase:
+ if priorPhaseType == downloadingPhase {
+ clearLine()
+ printInfo("Downloaded \(download.progressDescription)")
+ }
+ case installingPhase:
+ clearLine()
+ printInfo("Installing \(download.progressDescription)")
+ default:
+ break
+ }
+ priorPhaseType = status.activePhase.phaseType
+ }
progress(status.progressState)
}
}
@@ -39,7 +59,7 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
return
}
clearLine()
- printInfo("Downloading \(download.metadata.title)")
+ printInfo("Downloading \(download.progressDescription)")
}
func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
@@ -56,7 +76,7 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
} else if status.isCancelled {
errorHandler?(.cancelled)
} else {
- printInfo("Installed \(download.metadata.title)")
+ printInfo("Installed \(download.progressDescription)")
completionHandler?()
}
}
@@ -94,6 +114,12 @@ func progress(_ state: ProgressState) {
fflush(stdout)
}
+private extension SSDownload {
+ var progressDescription: String {
+ "\(metadata.title) (\(metadata.bundleVersion ?? "unknown version"))"
+ }
+}
+
extension SSDownloadStatus {
var progressState: ProgressState {
ProgressState(percentComplete: percentComplete, phase: activePhase.phaseDescription)
@@ -103,9 +129,9 @@ extension SSDownloadStatus {
extension SSDownloadPhase {
var phaseDescription: String {
switch phaseType {
- case 0:
+ case downloadingPhase:
return "Downloading"
- case 1:
+ case installingPhase:
return "Installing"
default:
return "Waiting"
diff --git a/Tests/masTests/.swift-format b/Tests/masTests/.swift-format
index 571f02f..9f8e2f7 100644
--- a/Tests/masTests/.swift-format
+++ b/Tests/masTests/.swift-format
@@ -28,7 +28,7 @@
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": true,
- "NoAccessLevelOnExtensionDeclaration": true,
+ "NoAccessLevelOnExtensionDeclaration": false,
"NoAssignmentInExpressions": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,