Use Swift Argument Parser instead of Commandant.

Command structs are nested types of Mas.

Renamed structs.

Limit code visibility as much as possible.

Standardize variable names.

Standardize spacing.

Fix a few tests.

Disable a useless test.

Remove unnecessary test stdout output.

Get swift-format from Brewfile instead of from Package.swift
since swift-format depends on an old version of swift-argument-parser.

Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
This commit is contained in:
Ross Goldberg 2024-10-01 18:55:04 -04:00
parent 6793a91e03
commit 2535e3da42
No known key found for this signature in database
42 changed files with 952 additions and 1108 deletions

View file

@ -1,21 +1,12 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "commandant",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Carthage/Commandant.git",
"state" : {
"revision" : "a1671cf728db837cf5ec1980a80d276bbba748f6",
"version" : "0.18.0"
}
},
{ {
"identity" : "cwlcatchexception", "identity" : "cwlcatchexception",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git", "location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : { "state" : {
"revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
"version" : "2.1.1" "version" : "2.2.1"
} }
}, },
{ {
@ -23,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : { "state" : {
"revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", "revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071",
"version" : "2.1.0" "version" : "2.2.2"
} }
}, },
{ {
@ -41,8 +32,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/PromiseKit.git", "location" : "https://github.com/mxcl/PromiseKit.git",
"state" : { "state" : {
"revision" : "43772616c46a44a9977e41924ae01d0e55f2f9ca", "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
"version" : "6.18.1" "version" : "6.22.1"
} }
}, },
{ {
@ -63,13 +54,22 @@
"version" : "2.1.1" "version" : "2.1.1"
} }
}, },
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{ {
"identity" : "version", "identity" : "version",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/Version.git", "location" : "https://github.com/mxcl/Version.git",
"state" : { "state" : {
"revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", "revision" : "303a0f916772545e1e8667d3104f83be708a723c",
"version" : "2.0.1" "version" : "2.1.0"
} }
} }
], ],

View file

@ -17,11 +17,11 @@ let package = Package(
], ],
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/Carthage/Commandant.git", from: "0.18.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"), .package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"),
.package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"),
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.16.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.0.1"), .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"), .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
], ],
targets: [ targets: [
@ -30,7 +30,7 @@ let package = Package(
.executableTarget( .executableTarget(
name: "mas", name: "mas",
dependencies: [ dependencies: [
"Commandant", .product(name: "ArgumentParser", package: "swift-argument-parser"),
"PromiseKit", "PromiseKit",
"Regex", "Regex",
"Version", "Version",

View file

@ -6,27 +6,36 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved. // Copyright (c) 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import StoreFoundation import StoreFoundation
public struct AccountCommand: CommandProtocol { extension Mas {
public typealias Options = NoOptions<MASError> struct Account: ParsableCommand {
public let verb = "account" static let configuration = CommandConfiguration(
public let function = "Prints the primary account Apple ID" abstract: "Prints the primary account Apple ID"
)
/// Runs the command. /// Runs the command.
public func run(_: Options) -> Result<Void, MASError> { func run() throws {
if #available(macOS 12, *) { let result = runInternal()
// Account information is no longer available as of Monterey. if case .failure = result {
// https://github.com/mas-cli/mas/issues/417 try result.get()
return .failure(.notSupported) }
} }
do { func runInternal() -> Result<Void, MASError> {
print(try ISStoreAccount.primaryAccount.wait().identifier) if #available(macOS 12, *) {
return .success(()) // Account information is no longer available as of Monterey.
} catch { // https://github.com/mas-cli/mas/issues/417
return .failure(error as? MASError ?? .failed(error: error as NSError)) return .failure(.notSupported)
}
do {
print(try ISStoreAccount.primaryAccount.wait().identifier)
return .success(())
} catch {
return .failure(error as? MASError ?? .failed(error: error as NSError))
}
} }
} }
} }

View file

@ -6,74 +6,53 @@
// Copyright © 2016 mas-cli. All rights reserved. // Copyright © 2016 mas-cli. All rights reserved.
// //
import Commandant import ArgumentParser
/// Opens app page on MAS Preview. Uses the iTunes Lookup API: extension Mas {
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup /// Opens app page on MAS Preview. Uses the iTunes Lookup API:
public struct HomeCommand: CommandProtocol { /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public typealias Options = HomeOptions struct Home: ParsableCommand {
static let configuration = CommandConfiguration(
public let verb = "home" abstract: "Opens MAS Preview app page in a browser"
public let function = "Opens MAS Preview app page in a browser"
private let storeSearch: StoreSearch
private var openCommand: ExternalCommand
public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
) )
}
/// Designated initializer. @Argument(help: "ID of app to show on MAS Preview")
init( var appId: Int
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
self.storeSearch = storeSearch
self.openCommand = openCommand
}
/// Runs the command. /// Runs the command.
public func run(_ options: HomeOptions) -> Result<Void, MASError> { func run() throws {
do { let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
guard let result = try storeSearch.lookup(app: options.appId).wait() else { if case .failure = result {
return .failure(.noSearchResultsFound) try result.get()
} }
do {
try openCommand.run(arguments: result.trackViewUrl)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
return .success(()) func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
} do {
} guard let result = try storeSearch.lookup(app: appId).wait() else {
return .failure(.noSearchResultsFound)
public struct HomeOptions: OptionsProtocol { }
let appId: Int
do {
static func create(_ appId: Int) -> HomeOptions { try openCommand.run(arguments: result.trackViewUrl)
HomeOptions(appId: appId) } catch {
} printError("Unable to launch open command")
return .failure(.searchFailed)
public static func evaluate(_ mode: CommandMode) -> Result<HomeOptions, CommandantError<MASError>> { }
create if openCommand.failed {
<*> mode <| Argument(usage: "ID of app to show on MAS Preview") let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
} }
} }

View file

@ -6,55 +6,44 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import Foundation import Foundation
/// Displays app details. Uses the iTunes Lookup API: extension Mas {
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup /// Displays app details. Uses the iTunes Lookup API:
public struct InfoCommand: CommandProtocol { /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public let verb = "info" struct Info: ParsableCommand {
public let function = "Display app information from the Mac App Store" static let configuration = CommandConfiguration(
abstract: "Display app information from the Mac App Store"
)
private let storeSearch: StoreSearch @Argument(help: "ID of app to show info")
var appId: Int
public init() { /// Runs the command.
self.init(storeSearch: MasStoreSearch()) func run() throws {
} let result = run(storeSearch: MasStoreSearch())
if case .failure = result {
/// Designated initializer. try result.get()
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: InfoOptions) -> Result<Void, MASError> {
do {
guard let result = try storeSearch.lookup(app: options.appId).wait() else {
return .failure(.noSearchResultsFound)
} }
print(AppInfoFormatter.format(app: result))
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
return .success(()) func run(storeSearch: StoreSearch) -> Result<Void, MASError> {
} do {
} guard let result = try storeSearch.lookup(app: appId).wait() else {
return .failure(.noSearchResultsFound)
public struct InfoOptions: OptionsProtocol { }
let appId: Int
print(AppInfoFormatter.format(app: result))
static func create(_ appId: Int) -> InfoOptions { } catch {
InfoOptions(appId: appId) // Bubble up MASErrors
} if let error = error as? MASError {
return .failure(error)
public static func evaluate(_ mode: CommandMode) -> Result<InfoOptions, CommandantError<MASError>> { }
create return .failure(.searchFailed)
<*> mode <| Argument(usage: "ID of app to show info") }
return .success(())
}
} }
} }

View file

@ -6,63 +6,47 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved. // Copyright (c) 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
/// Installs previously purchased apps from the Mac App Store. extension Mas {
public struct InstallCommand: CommandProtocol { /// Installs previously purchased apps from the Mac App Store.
public typealias Options = InstallOptions struct Install: ParsableCommand {
public let verb = "install" static let configuration = CommandConfiguration(
public let function = "Install from the Mac App Store" abstract: "Install from the Mac App Store"
)
private let appLibrary: AppLibrary @Flag(help: "force reinstall")
var force = false
@Argument(help: "app ID(s) to install")
var appIds: [UInt64]
/// Public initializer. /// Runs the command.
public init() { func run() throws {
self.init(appLibrary: MasAppLibrary()) let result = run(appLibrary: MasAppLibrary())
} if case .failure = result {
try result.get()
}
}
/// Internal initializer. func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
/// - Parameter appLibrary: AppLibrary manager. // Try to download applications with given identifiers and collect results
init(appLibrary: AppLibrary = MasAppLibrary()) { let appIds = appIds.filter { appId in
self.appLibrary = appLibrary if let product = appLibrary.installedApp(forId: appId), !force {
} printWarning("\(product.appName) is already installed")
return false
}
/// Runs the command. return true
public func run(_ options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let appIds = options.appIds.filter { appId in
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
printWarning("\(product.appName) is already installed")
return false
} }
return true do {
} try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
do { return .success(())
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
} }
return .success(())
}
}
public struct InstallOptions: OptionsProtocol {
let appIds: [UInt64]
let forceInstall: Bool
public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions {
{ forceInstall in
InstallOptions(appIds: appIds.map { UInt64($0) }, forceInstall: forceInstall)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<InstallOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "app ID(s) to install")
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
} }
} }

View file

@ -6,39 +6,34 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved. // Copyright (c) 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
/// Command which lists all installed apps. extension Mas {
public struct ListCommand: CommandProtocol { /// Command which lists all installed apps.
public typealias Options = NoOptions<MASError> struct List: ParsableCommand {
public let verb = "list" static let configuration = CommandConfiguration(
public let function = "Lists apps from the Mac App Store which are currently installed" abstract: "Lists apps from the Mac App Store which are currently installed"
)
private let appLibrary: AppLibrary /// Runs the command.
func run() throws {
/// Public initializer. let result = run(appLibrary: MasAppLibrary())
/// - Parameter appLibrary: AppLibrary manager. if case .failure = result {
public init() { try result.get()
self.init(appLibrary: MasAppLibrary()) }
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
/// Runs the command.
public func run(_: Options) -> Result<Void, MASError> {
let products = appLibrary.installedApps
if products.isEmpty {
printError("No installed apps found")
return .success(())
} }
let output = AppListFormatter.format(products: products) func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
print(output) let products = appLibrary.installedApps
if products.isEmpty {
printError("No installed apps found")
return .success(())
}
return .success(()) let output = AppListFormatter.format(products: products)
print(output)
return .success(())
}
} }
} }

View file

@ -6,101 +6,74 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
/// Command which installs the first search result. This is handy as many MAS titles extension Mas {
/// can be long with embedded keywords. /// Command which installs the first search result. This is handy as many MAS titles
public struct LuckyCommand: CommandProtocol { /// can be long with embedded keywords.
public typealias Options = LuckyOptions struct Lucky: ParsableCommand {
public let verb = "lucky" static let configuration = CommandConfiguration(
public let function = "Install the first result from the Mac App Store" abstract: "Install the first result from the Mac App Store"
)
private let appLibrary: AppLibrary @Flag(help: "force reinstall")
private let storeSearch: StoreSearch var force = false
@Argument(help: "the app name to install")
var appName: String
public init() { /// Runs the command.
self.init(storeSearch: MasStoreSearch()) func run() throws {
} let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
if case .failure = result {
/// Designated initializer. try result.get()
/// - Parameter storeSearch: Search manager.
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch)
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: Search manager.
init(
appLibrary: AppLibrary = MasAppLibrary(),
storeSearch: StoreSearch = MasStoreSearch()
) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
var appId: Int?
do {
let results = try storeSearch.search(for: options.appName).wait()
guard let result = results.first else {
printError("No results found")
return .failure(.noSearchResultsFound)
} }
appId = result.trackId
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
guard let identifier = appId else { fatalError() } func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
var appId: Int?
return install(UInt64(identifier), options: options) do {
} let results = try storeSearch.search(for: appName).wait()
guard let result = results.first else {
printError("No results found")
return .failure(.noSearchResultsFound)
}
appId = result.trackId
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
guard let identifier = appId else { fatalError() }
return install(UInt64(identifier), appLibrary: appLibrary)
}
/// Installs an app.
///
/// - Parameters:
/// - appId: App identifier
/// - appLibrary: Library of installed apps
/// - Returns: Result of the operation.
fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
if let product = appLibrary.installedApp(forId: appId), !force {
printWarning("\(product.appName) is already installed")
return .success(())
}
do {
try downloadAll([appId]).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
/// Installs an app.
///
/// - Parameters:
/// - appId: App identifier
/// - options: command options.
/// - Returns: Result of the operation.
fileprivate func install(_ appId: UInt64, options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
printWarning("\(product.appName) is already installed")
return .success(()) return .success(())
} }
do {
try downloadAll([appId]).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
return .success(())
}
}
public struct LuckyOptions: OptionsProtocol {
let appName: String
let forceInstall: Bool
public static func create(_ appName: String) -> (_ forceInstall: Bool) -> LuckyOptions {
{ forceInstall in
LuckyOptions(appName: appName, forceInstall: forceInstall)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<LuckyOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "the app name to install")
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
} }
} }

View file

@ -6,97 +6,76 @@
// Copyright © 2016 mas-cli. All rights reserved. // Copyright © 2016 mas-cli. All rights reserved.
// //
import Commandant import ArgumentParser
import Foundation import Foundation
private let markerValue = "appstore" private let markerValue = "appstore"
private let masScheme = "macappstore" private let masScheme = "macappstore"
/// Opens app page in MAS app. Uses the iTunes Lookup API: extension Mas {
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup /// Opens app page in MAS app. Uses the iTunes Lookup API:
public struct OpenCommand: CommandProtocol { /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public typealias Options = OpenOptions struct Open: ParsableCommand {
static let configuration = CommandConfiguration(
public let verb = "open" abstract: "Opens app page in AppStore.app"
public let function = "Opens app page in AppStore.app"
private let storeSearch: StoreSearch
private var systemOpen: ExternalCommand
public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
) )
}
/// Designated initializer. @Argument(help: "the app ID")
init( var appId: String = markerValue
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
self.storeSearch = storeSearch
systemOpen = openCommand
}
/// Runs the command. /// Runs the command.
public func run(_ options: OpenOptions) -> Result<Void, MASError> { func run() throws {
do { let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
if options.appId == markerValue { if case .failure = result {
// If no app ID is given, just open the MAS GUI app try result.get()
try systemOpen.run(arguments: masScheme + "://")
return .success(())
} }
guard let appId = Int(options.appId)
else {
printError("Invalid app ID")
return .failure(.noSearchResultsFound)
}
guard let result = try storeSearch.lookup(app: appId).wait()
else {
return .failure(.noSearchResultsFound)
}
guard var url = URLComponents(string: result.trackViewUrl)
else {
return .failure(.searchFailed)
}
url.scheme = masScheme
do {
try systemOpen.run(arguments: url.string!)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if systemOpen.failed {
let reason = systemOpen.process.terminationReason
printError("Open failed: (\(reason)) \(systemOpen.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
return .success(()) func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
} do {
} if appId == markerValue {
// If no app ID is given, just open the MAS GUI app
public struct OpenOptions: OptionsProtocol { try openCommand.run(arguments: masScheme + "://")
var appId: String return .success(())
}
static func create(_ appId: String) -> OpenOptions {
OpenOptions(appId: appId) guard let appId = Int(appId)
} else {
printError("Invalid app ID")
public static func evaluate(_ mode: CommandMode) -> Result<OpenOptions, CommandantError<MASError>> { return .failure(.noSearchResultsFound)
create }
<*> mode <| Argument(defaultValue: markerValue, usage: "the app ID")
guard let result = try storeSearch.lookup(app: appId).wait()
else {
return .failure(.noSearchResultsFound)
}
guard var url = URLComponents(string: result.trackViewUrl)
else {
return .failure(.searchFailed)
}
url.scheme = masScheme
do {
try openCommand.run(arguments: url.string!)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
} }
} }

View file

@ -6,84 +6,67 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved. // Copyright (c) 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import Foundation import Foundation
import PromiseKit import PromiseKit
import enum Swift.Result import enum Swift.Result
/// Command which displays a list of installed apps which have available updates extension Mas {
/// ready to be installed from the Mac App Store. /// Command which displays a list of installed apps which have available updates
public struct OutdatedCommand: CommandProtocol { /// ready to be installed from the Mac App Store.
public typealias Options = OutdatedOptions struct Outdated: ParsableCommand {
public let verb = "outdated" static let configuration = CommandConfiguration(
public let function = "Lists pending updates from the Mac App Store" abstract: "Lists pending updates from the Mac App Store"
)
private let appLibrary: AppLibrary @Flag(help: "Show warnings about apps")
private let storeSearch: StoreSearch var verbose = false
/// Public initializer. /// Runs the command.
public init() { func run() throws {
self.init(appLibrary: MasAppLibrary()) let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
} if case .failure = result {
try result.get()
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: StoreSearch manager.
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
let promises = appLibrary.installedApps.map { installedApp in
firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
}.done { storeApp in
guard let storeApp else {
if options.verbose {
printWarning(
"""
Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.appName).
""")
}
return
}
if installedApp.isOutdatedWhenComparedTo(storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
""")
}
} }
} }
return firstly { func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
when(fulfilled: promises) let promises = appLibrary.installedApps.map { installedApp in
}.map { firstly {
Result<Void, MASError>.success(()) storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
}.recover { error in }.done { storeApp in
// Bubble up MASErrors guard let storeApp else {
.value(Result<Void, MASError>.failure(error as? MASError ?? .searchFailed)) if verbose {
}.wait() printWarning(
} """
} Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.appName).
public struct OutdatedOptions: OptionsProtocol { """
public typealias ClientError = MASError )
}
let verbose: Bool return
}
static func create(verbose: Bool) -> OutdatedOptions {
OutdatedOptions(verbose: verbose) if installedApp.isOutdatedWhenComparedTo(storeApp) {
} print(
"""
public static func evaluate(_ mode: CommandMode) -> Result<OutdatedOptions, CommandantError<MASError>> { \(installedApp.itemIdentifier) \(installedApp.appName) \
create (\(installedApp.bundleVersion) -> \(storeApp.version))
<*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps") """
)
}
}
}
return firstly {
when(fulfilled: promises)
}.map {
Result<Void, MASError>.success(())
}.recover { error in
// Bubble up MASErrors
.value(Result<Void, MASError>.failure(error as? MASError ?? .searchFailed))
}.wait()
}
} }
} }

View file

@ -6,58 +6,44 @@
// Copyright (c) 2017 Jakob Rieck. All rights reserved. // Copyright (c) 2017 Jakob Rieck. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
public struct PurchaseCommand: CommandProtocol { extension Mas {
public typealias Options = PurchaseOptions struct Purchase: ParsableCommand {
public let verb = "purchase" static let configuration = CommandConfiguration(
public let function = "Purchase and download free apps from the Mac App Store" abstract: "Purchase and download free apps from the Mac App Store"
)
private let appLibrary: AppLibrary @Argument(help: "app ID(s) to install")
var appIds: [UInt64]
/// Public initializer. /// Runs the command.
public init() { func run() throws {
self.init(appLibrary: MasAppLibrary()) let result = run(appLibrary: MasAppLibrary())
} if case .failure = result {
try result.get()
}
}
/// Internal initializer. func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
/// - Parameter appLibrary: AppLibrary manager. // Try to download applications with given identifiers and collect results
init(appLibrary: AppLibrary = MasAppLibrary()) { let appIds = appIds.filter { appId in
self.appLibrary = appLibrary if let product = appLibrary.installedApp(forId: appId) {
} printWarning("\(product.appName) has already been purchased.")
return false
}
/// Runs the command. return true
public func run(_ options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let appIds = options.appIds.filter { appId in
if let product = appLibrary.installedApp(forId: appId) {
printWarning("\(product.appName) has already been purchased.")
return false
} }
return true do {
} try downloadAll(appIds, purchase: true).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
do { return .success(())
try downloadAll(appIds, purchase: true).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
} }
return .success(())
}
}
public struct PurchaseOptions: OptionsProtocol {
let appIds: [UInt64]
public static func create(_ appIds: [Int]) -> PurchaseOptions {
PurchaseOptions(appIds: appIds.map { UInt64($0) })
}
public static func evaluate(_ mode: CommandMode) -> Result<PurchaseOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "app ID(s) to install")
} }
} }

View file

@ -6,84 +6,83 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
/// Kills several macOS processes as a means to reset the app store. extension Mas {
public struct ResetCommand: CommandProtocol { /// Kills several macOS processes as a means to reset the app store.
public typealias Options = ResetOptions struct Reset: ParsableCommand {
public let verb = "reset" static let configuration = CommandConfiguration(
public let function = "Resets the Mac App Store" abstract: "Resets the Mac App Store"
)
/// Runs the command. @Flag(help: "Enable debug mode")
public func run(_ options: Options) -> Result<Void, MASError> { var debug = false
// The "Reset Application" command in the Mac App Store debug menu performs
// the following steps
//
// - killall Dock
// - killall storeagent (storeagent no longer exists)
// - rm com.apple.appstore download directory
// - clear cookies (appears to be a no-op)
//
// As storeagent no longer exists we will implement a slight variant and kill all
// App Store-associated processes
// - storeaccountd
// - storeassetd
// - storedownloadd
// - storeinstalld
// - storelegacy
// Kill processes /// Runs the command.
let killProcs = [ func run() throws {
"Dock", let result = runInternal()
"storeaccountd", if case .failure = result {
"storeassetd", try result.get()
"storedownloadd",
"storeinstalld",
"storelegacy",
]
let kill = Process()
let stdout = Pipe()
let stderr = Pipe()
kill.launchPath = "/usr/bin/killall"
kill.arguments = killProcs
kill.standardOutput = stdout
kill.standardError = stderr
kill.launch()
kill.waitUntilExit()
if kill.terminationStatus != 0, options.debug {
let output = stderr.fileHandleForReading.readDataToEndOfFile()
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
}
// Wipe Download Directory
if let directory = CKDownloadDirectory(nil) {
do {
try FileManager.default.removeItem(atPath: directory)
} catch {
if options.debug {
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
}
} }
} }
return .success(()) func runInternal() -> Result<Void, MASError> {
} // The "Reset Application" command in the Mac App Store debug menu performs
} // the following steps
//
public struct ResetOptions: OptionsProtocol { // - killall Dock
let debug: Bool // - killall storeagent (storeagent no longer exists)
// - rm com.apple.appstore download directory
public static func create(debug: Bool) -> ResetOptions { // - clear cookies (appears to be a no-op)
ResetOptions(debug: debug) //
} // As storeagent no longer exists we will implement a slight variant and kill all
// App Store-associated processes
public static func evaluate(_ mode: CommandMode) -> Result<ResetOptions, CommandantError<MASError>> { // - storeaccountd
create // - storeassetd
<*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode") // - storedownloadd
// - storeinstalld
// - storelegacy
// Kill processes
let killProcs = [
"Dock",
"storeaccountd",
"storeassetd",
"storedownloadd",
"storeinstalld",
"storelegacy",
]
let kill = Process()
let stdout = Pipe()
let stderr = Pipe()
kill.launchPath = "/usr/bin/killall"
kill.arguments = killProcs
kill.standardOutput = stdout
kill.standardError = stderr
kill.launch()
kill.waitUntilExit()
if kill.terminationStatus != 0, debug {
let output = stderr.fileHandleForReading.readDataToEndOfFile()
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
}
// Wipe Download Directory
if let directory = CKDownloadDirectory(nil) {
do {
try FileManager.default.removeItem(atPath: directory)
} catch {
if debug {
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
}
}
}
return .success(())
}
} }
} }

View file

@ -6,62 +6,46 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
/// Search the Mac App Store using the iTunes Search API: extension Mas {
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ /// Search the Mac App Store using the iTunes Search API:
public struct SearchCommand: CommandProtocol { /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
public typealias Options = SearchOptions struct Search: ParsableCommand {
public let verb = "search" static let configuration = CommandConfiguration(
public let function = "Search for apps from the Mac App Store" abstract: "Search for apps from the Mac App Store"
)
private let storeSearch: StoreSearch @Flag(help: "Show price of found apps")
var price = false
@Argument(help: "the app name to search")
var appName: String
public init() { func run() throws {
self.init(storeSearch: MasStoreSearch()) let result = run(storeSearch: MasStoreSearch())
} if case .failure = result {
try result.get()
/// Designated initializer.
///
/// - Parameter storeSearch: Search manager.
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.storeSearch = storeSearch
}
public func run(_ options: Options) -> Result<Void, MASError> {
do {
let results = try storeSearch.search(for: options.appName).wait()
if results.isEmpty {
return .failure(.noSearchResultsFound)
} }
}
let output = SearchResultFormatter.format(results: results, includePrice: options.price) func run(storeSearch: StoreSearch) -> Result<Void, MASError> {
print(output) do {
let results = try storeSearch.search(for: appName).wait()
if results.isEmpty {
return .failure(.noSearchResultsFound)
}
return .success(()) let output = SearchResultFormatter.format(results: results, includePrice: price)
} catch { print(output)
// Bubble up MASErrors
if let error = error as? MASError { return .success(())
return .failure(error) } catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
return .failure(.searchFailed)
} }
} }
} }
public struct SearchOptions: OptionsProtocol {
let appName: String
let price: Bool
public static func create(_ appName: String) -> (_ price: Bool) -> SearchOptions {
{ price in
SearchOptions(appName: appName, price: price)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<SearchOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "the app name to search")
<*> mode <| Option(key: "price", defaultValue: false, usage: "Show price of found apps")
}
}

View file

@ -6,46 +6,38 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import StoreFoundation import StoreFoundation
public struct SignInCommand: CommandProtocol { extension Mas {
public typealias Options = SignInOptions struct SignIn: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signin",
abstract: "Sign in to the Mac App Store"
)
public let verb = "signin" @Flag(help: "Complete login with graphical dialog")
public let function = "Sign in to the Mac App Store" var dialog = false
@Argument(help: "Apple ID")
var username: String
@Argument(help: "Password")
var password: String = ""
/// Runs the command. /// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> { func run() throws {
do { let result = runInternal()
_ = try ISStoreAccount.signIn( if case .failure = result {
username: options.username, try result.get()
password: options.password, }
systemDialog: options.dialog }
)
.wait() func runInternal() -> Result<Void, MASError> {
return .success(()) do {
} catch { _ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait()
return .failure(error as? MASError ?? .signInFailed(error: error as NSError)) return .success(())
} catch {
return .failure(error as? MASError ?? .signInFailed(error: error as NSError))
}
} }
} }
} }
public struct SignInOptions: OptionsProtocol {
public typealias ClientError = MASError
let username: String
let password: String
let dialog: Bool
static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions {
{ password in { dialog in SignInOptions(username: username, password: password, dialog: dialog) } }
}
public static func evaluate(_ mode: CommandMode) -> Result<SignInOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "Apple ID")
<*> mode <| Argument(defaultValue: "", usage: "Password")
<*> mode <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog")
}
}

View file

@ -6,24 +6,34 @@
// Copyright © 2016 Andrew Naylor. All rights reserved. // Copyright © 2016 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
public struct SignOutCommand: CommandProtocol { extension Mas {
public typealias Options = NoOptions<MASError> struct SignOut: ParsableCommand {
public let verb = "signout" static let configuration = CommandConfiguration(
public let function = "Sign out of the Mac App Store" commandName: "signout",
abstract: "Sign out of the Mac App Store"
)
/// Runs the command. /// Runs the command.
public func run(_: Options) -> Result<Void, MASError> { func run() throws {
if #available(macOS 10.13, *) { let result = runInternal()
ISServiceProxy.genericShared().accountService.signOut() if case .failure = result {
} else { try result.get()
// Using CKAccountStore to sign out does nothing on High Sierra }
// https://github.com/mas-cli/mas/issues/129
CKAccountStore.shared().signOut()
} }
return .success(()) func runInternal() -> Result<Void, MASError> {
if #available(macOS 10.13, *) {
ISServiceProxy.genericShared().accountService.signOut()
} else {
// Using CKAccountStore to sign out does nothing on High Sierra
// https://github.com/mas-cli/mas/issues/129
CKAccountStore.shared().signOut()
}
return .success(())
}
} }
} }

View file

@ -6,75 +6,52 @@
// Copyright © 2015 Andrew Naylor. All rights reserved. // Copyright © 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import CommerceKit import CommerceKit
import StoreFoundation import StoreFoundation
/// Command which uninstalls apps managed by the Mac App Store. extension Mas {
public struct UninstallCommand: CommandProtocol { /// Command which uninstalls apps managed by the Mac App Store.
public typealias Options = UninstallOptions struct Uninstall: ParsableCommand {
public let verb = "uninstall" static let configuration = CommandConfiguration(
public let function = "Uninstall app installed from the Mac App Store" abstract: "Uninstall app installed from the Mac App Store"
)
private let appLibrary: AppLibrary /// Flag indicating that removal shouldn't be performed
@Flag(help: "dry run")
var dryRun = false
@Argument(help: "ID of app to uninstall")
var appId: Int
/// Public initializer. /// Runs the uninstall command.
/// - Parameter appLibrary: AppLibrary manager. func run() throws {
public init() { let result = run(appLibrary: MasAppLibrary())
self.init(appLibrary: MasAppLibrary()) if case .failure = result {
} try result.get()
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
/// Runs the uninstall command.
///
/// - Parameter options: UninstallOptions (arguments) for this command
/// - Returns: Success or an error.
public func run(_ options: Options) -> Result<Void, MASError> {
let appId = UInt64(options.appId)
guard let product = appLibrary.installedApp(forId: appId) else {
return .failure(.notInstalled)
} }
if options.dryRun { func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
printInfo("\(product.appName) \(product.bundlePath)") let appId = UInt64(appId)
printInfo("(not removed, dry run)")
guard let product = appLibrary.installedApp(forId: appId) else {
return .failure(.notInstalled)
}
if dryRun {
printInfo("\(product.appName) \(product.bundlePath)")
printInfo("(not removed, dry run)")
return .success(())
}
do {
try appLibrary.uninstallApp(app: product)
} catch {
return .failure(.uninstallFailed)
}
return .success(()) return .success(())
} }
do {
try appLibrary.uninstallApp(app: product)
} catch {
return .failure(.uninstallFailed)
}
return .success(())
}
}
/// Options for the uninstall command.
public struct UninstallOptions: OptionsProtocol {
/// Numeric app ID
let appId: Int
/// Flag indicating that removal shouldn't be performed
let dryRun: Bool
static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions {
{ dryRun in
UninstallOptions(appId: appId, dryRun: dryRun)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<UninstallOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "ID of app to uninstall")
<*> mode <| Switch(flag: nil, key: "dry-run", usage: "dry run")
} }
} }

View file

@ -6,104 +6,90 @@
// Copyright © 2015 Andrew Naylor. All rights reserved. // Copyright © 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
import Foundation import Foundation
import PromiseKit import PromiseKit
import enum Swift.Result import enum Swift.Result
/// Command which upgrades apps with new versions available in the Mac App Store. extension Mas {
public struct UpgradeCommand: CommandProtocol { /// Command which upgrades apps with new versions available in the Mac App Store.
public typealias Options = UpgradeOptions struct Upgrade: ParsableCommand {
public let verb = "upgrade" static let configuration = CommandConfiguration(
public let function = "Upgrade outdated apps from the Mac App Store" abstract: "Upgrade outdated apps from the Mac App Store"
)
private let appLibrary: AppLibrary @Argument(help: "app(s) to upgrade")
private let storeSearch: StoreSearch var apps: [String] = []
/// Public initializer. /// Runs the command.
public init() { func run() throws {
self.init(appLibrary: MasAppLibrary()) let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
} if case .failure = result {
try result.get()
/// Internal initializer. }
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: StoreSearch manager.
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
do {
apps = try findOutdatedApps(options)
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
} }
guard apps.count > 0 else { func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
printWarning("Nothing found to upgrade") let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
do {
apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch)
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
}
guard apps.count > 0 else {
printWarning("Nothing found to upgrade")
return .success(())
}
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
print(
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" }
.joined(separator: "\n"))
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value)
do {
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
return .success(()) return .success(())
} }
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") private func findOutdatedApps(
print( appLibrary: AppLibrary,
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } storeSearch: StoreSearch
.joined(separator: "\n")) ) throws -> [(SoftwareProduct, SearchResult)] {
let apps: [SoftwareProduct] =
apps.isEmpty
? appLibrary.installedApps
: apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
}
}
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value) let promises = apps.map { installedApp in
do { // only upgrade apps whose local version differs from the store version
try downloadAll(appIds).wait() firstly {
} catch { storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) }.map { result -> (SoftwareProduct, SearchResult)? in
} guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
}
return .success(()) return (installedApp, storeApp)
}
private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] {
let apps: [SoftwareProduct] =
options.apps.isEmpty
? appLibrary.installedApps
: options.apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
} }
} }
let promises = apps.map { installedApp in return try when(fulfilled: promises).wait().compactMap { $0 }
// only upgrade apps whose local version differs from the store version
firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
}.map { result -> (SoftwareProduct, SearchResult)? in
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
}
return (installedApp, storeApp)
}
} }
return try when(fulfilled: promises).wait().compactMap { $0 }
}
}
public struct UpgradeOptions: OptionsProtocol {
let apps: [String]
static func create(_ apps: [String]) -> UpgradeOptions {
UpgradeOptions(apps: apps)
}
public static func evaluate(_ mode: CommandMode) -> Result<UpgradeOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(defaultValue: [], usage: "app(s) to upgrade")
} }
} }

View file

@ -6,78 +6,57 @@
// Copyright © 2016 mas-cli. All rights reserved. // Copyright © 2016 mas-cli. All rights reserved.
// //
import Commandant import ArgumentParser
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API: extension Mas {
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup /// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
public struct VendorCommand: CommandProtocol { /// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public typealias Options = VendorOptions struct Vendor: ParsableCommand {
static let configuration = CommandConfiguration(
public let verb = "vendor" abstract: "Opens vendor's app page in a browser"
public let function = "Opens vendor's app page in a browser"
private let storeSearch: StoreSearch
private var openCommand: ExternalCommand
public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
) )
}
/// Designated initializer. @Argument(help: "the app ID to show the vendor's website")
init( var appId: Int
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
self.storeSearch = storeSearch
self.openCommand = openCommand
}
/// Runs the command. /// Runs the command.
public func run(_ options: VendorOptions) -> Result<Void, MASError> { func run() throws {
do { let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
guard let result = try storeSearch.lookup(app: options.appId).wait() if case .failure = result {
else { try result.get()
return .failure(.noSearchResultsFound)
} }
guard let vendorWebsite = result.sellerUrl
else { throw MASError.noVendorWebsite }
do {
try openCommand.run(arguments: vendorWebsite)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
} }
return .success(()) func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
} do {
} guard let result = try storeSearch.lookup(app: appId).wait()
else {
public struct VendorOptions: OptionsProtocol { return .failure(.noSearchResultsFound)
let appId: Int }
static func create(_ appId: Int) -> VendorOptions { guard let vendorWebsite = result.sellerUrl
VendorOptions(appId: appId) else { throw MASError.noVendorWebsite }
}
do {
public static func evaluate(_ mode: CommandMode) -> Result<VendorOptions, CommandantError<MASError>> { try openCommand.run(arguments: vendorWebsite)
create } catch {
<*> mode <| Argument(usage: "the app ID to show the vendor's website") printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
} }
} }

View file

@ -6,17 +6,26 @@
// Copyright © 2015 Andrew Naylor. All rights reserved. // Copyright © 2015 Andrew Naylor. All rights reserved.
// //
import Commandant import ArgumentParser
/// Command which displays the version of the mas tool. extension Mas {
public struct VersionCommand: CommandProtocol { /// Command which displays the version of the mas tool.
public typealias Options = NoOptions<MASError> struct Version: ParsableCommand {
public let verb = "version" static let configuration = CommandConfiguration(
public let function = "Print version number" abstract: "Print version number"
)
/// Runs the command. /// Runs the command.
public func run(_: Options) -> Result<Void, MASError> { func run() throws {
print(Package.version) let result = runInternal()
return .success(()) if case .failure = result {
try result.get()
}
}
func runInternal() -> Result<Void, MASError> {
print(Package.version)
return .success(())
}
} }
} }

View file

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum MASError: Error, Equatable { enum MASError: Error, Equatable {
case notSupported case notSupported
case failed(error: NSError?) case failed(error: NSError?)
@ -36,7 +36,7 @@ public enum MASError: Error, Equatable {
// MARK: - CustomStringConvertible // MARK: - CustomStringConvertible
extension MASError: CustomStringConvertible { extension MASError: CustomStringConvertible {
public var description: String { var description: String {
switch self { switch self {
case .notSignedIn: case .notSignedIn:
return "Not signed in" return "Not signed in"

View file

@ -91,7 +91,7 @@ func printInfo(_ message: String) {
} }
/// Prints a message to stderr prefixed with "Warning:" underlined in yellow. /// Prints a message to stderr prefixed with "Warning:" underlined in yellow.
public func printWarning(_ message: String) { func printWarning(_ message: String) {
guard isatty(fileno(stderr)) != 0 else { guard isatty(fileno(stderr)) != 0 else {
print("Warning: \(message)", to: &standardError) print("Warning: \(message)", to: &standardError)
return return
@ -102,7 +102,7 @@ public func printWarning(_ message: String) {
} }
/// Prints a message to stderr prefixed with "Error:" underlined in red. /// Prints a message to stderr prefixed with "Error:" underlined in red.
public func printError(_ message: String) { func printError(_ message: String) {
guard isatty(fileno(stderr)) != 0 else { guard isatty(fileno(stderr)) != 0 else {
print("Error: \(message)", to: &standardError) print("Error: \(message)", to: &standardError)
return return

View file

@ -6,10 +6,39 @@
// Copyright © 2021 mas-cli. All rights reserved. // Copyright © 2021 mas-cli. All rights reserved.
// //
import ArgumentParser
import PromiseKit import PromiseKit
public enum Mas { @main
public static func initialize() { struct Mas: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Mac App Store command-line interface",
subcommands: [
Account.self,
Home.self,
Info.self,
Install.self,
List.self,
Lucky.self,
Open.self,
Outdated.self,
Purchase.self,
Reset.self,
Search.self,
SignIn.self,
SignOut.self,
Uninstall.self,
Upgrade.self,
Vendor.self,
Version.self,
]
)
func validate() throws {
Mas.initialize()
}
static func initialize() {
PromiseKit.conf.Q.map = .global() PromiseKit.conf.Q.map = .global()
PromiseKit.conf.Q.return = .global() PromiseKit.conf.Q.return = .global()
PromiseKit.conf.logHandler = { event in PromiseKit.conf.logHandler = { event in

View file

@ -10,7 +10,7 @@ import Foundation
import PromiseKit import PromiseKit
extension URLSession: NetworkSession { extension URLSession: NetworkSession {
public func loadData(from url: URL) -> Promise<Data> { func loadData(from url: URL) -> Promise<Data> {
Promise { seal in Promise { seal in
dataTask(with: url) { data, _, error in dataTask(with: url) { data, _, error in
if let data { if let data {

View file

@ -1,37 +0,0 @@
//
// main.swift
// mas
//
// Created by Andrew Naylor on 11/07/2015.
// Copyright © 2015 Andrew Naylor. All rights reserved.
//
import Commandant
Mas.initialize()
let registry = CommandRegistry<MASError>()
let helpCommand = HelpCommand(registry: registry)
registry.register(AccountCommand())
registry.register(HomeCommand())
registry.register(InfoCommand())
registry.register(InstallCommand())
registry.register(PurchaseCommand())
registry.register(ListCommand())
registry.register(LuckyCommand())
registry.register(OpenCommand())
registry.register(OutdatedCommand())
registry.register(ResetCommand())
registry.register(SearchCommand())
registry.register(SignInCommand())
registry.register(SignOutCommand())
registry.register(UninstallCommand())
registry.register(UpgradeCommand())
registry.register(VendorCommand())
registry.register(VersionCommand())
registry.register(helpCommand)
registry.main(defaultVerb: helpCommand.verb) { error in
printError(String(describing: error))
}

View file

@ -1,5 +1,5 @@
// //
// AccountCommandSpec.swift // AccountSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -12,7 +12,7 @@ import Quick
@testable import mas @testable import mas
// Deprecated test // Deprecated test
public class AccountCommandSpec: QuickSpec { public class AccountSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -20,9 +20,10 @@ public class AccountCommandSpec: QuickSpec {
// account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
xdescribe("Account command") { xdescribe("Account command") {
xit("displays active account") { xit("displays active account") {
let cmd = AccountCommand() expect {
let result = cmd.run(AccountCommand.Options()) try Mas.Account.parse([]).runInternal()
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// HomeCommandSpec.swift // HomeSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-29. // Created by Ben Chatelain on 2018-12-29.
@ -11,7 +11,7 @@ import Quick
@testable import mas @testable import mas
public class HomeCommandSpec: QuickSpec { public class HomeSpec: QuickSpec {
override public func spec() { override public func spec() {
let result = SearchResult( let result = SearchResult(
trackId: 1111, trackId: 1111,
@ -20,7 +20,6 @@ public class HomeCommandSpec: QuickSpec {
) )
let storeSearch = StoreSearchMock() let storeSearch = StoreSearchMock()
let openCommand = OpenSystemCommandMock() let openCommand = OpenSystemCommandMock()
let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand)
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -30,26 +29,32 @@ public class HomeCommandSpec: QuickSpec {
storeSearch.reset() storeSearch.reset()
} }
it("fails to open app with invalid ID") { it("fails to open app with invalid ID") {
let result = cmd.run(HomeCommand.Options(appId: -999)) expect {
expect(result) try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .searchFailed beFailure { error in
}) expect(error) == .searchFailed
}
)
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
let result = cmd.run(HomeCommand.Options(appId: 999)) expect {
expect(result) try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .noSearchResultsFound beFailure { error in
}) expect(error) == .noSearchResultsFound
}
)
} }
it("opens app on MAS Preview") { it("opens app on MAS Preview") {
storeSearch.apps[result.trackId] = result storeSearch.apps[result.trackId] = result
let cmdResult = cmd.run(HomeCommand.Options(appId: result.trackId)) expect {
expect(cmdResult).to(beSuccess()) try Mas.Home.parse([String(result.trackId)]).run(storeSearch: storeSearch, openCommand: openCommand)
}
.to(beSuccess())
expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments).toNot(beNil())
expect(openCommand.arguments!.first!) == result.trackViewUrl expect(openCommand.arguments!.first!) == result.trackViewUrl
} }

View file

@ -1,5 +1,5 @@
// //
// InfoCommandSpec.swift // InfoSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,7 +11,7 @@ import Quick
@testable import mas @testable import mas
public class InfoCommandSpec: QuickSpec { public class InfoSpec: QuickSpec {
override public func spec() { override public func spec() {
let result = SearchResult( let result = SearchResult(
currentVersionReleaseDate: "2019-01-07T18:53:13Z", currentVersionReleaseDate: "2019-01-07T18:53:13Z",
@ -25,7 +25,6 @@ public class InfoCommandSpec: QuickSpec {
version: "1.0" version: "1.0"
) )
let storeSearch = StoreSearchMock() let storeSearch = StoreSearchMock()
let cmd = InfoCommand(storeSearch: storeSearch)
let expectedOutput = """ let expectedOutput = """
Awesome App 1.0 [2.0] Awesome App 1.0 [2.0]
By: Awesome Dev By: Awesome Dev
@ -44,28 +43,33 @@ public class InfoCommandSpec: QuickSpec {
storeSearch.reset() storeSearch.reset()
} }
it("fails to open app with invalid ID") { it("fails to open app with invalid ID") {
let result = cmd.run(InfoCommand.Options(appId: -999)) expect {
expect(result) try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch)
.to( }
beFailure { error in .to(
expect(error) == .searchFailed beFailure { error in
}) expect(error) == .searchFailed
}
)
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
let result = cmd.run(InfoCommand.Options(appId: 999)) expect {
expect(result) try Mas.Info.parse(["999"]).run(storeSearch: storeSearch)
.to( }
beFailure { error in .to(
expect(error) == .noSearchResultsFound beFailure { error in
}) expect(error) == .noSearchResultsFound
}
)
} }
it("displays app details") { it("displays app details") {
storeSearch.apps[result.trackId] = result storeSearch.apps[result.trackId] = result
let output = OutputListener() let output = OutputListener()
let result = cmd.run(InfoCommand.Options(appId: result.trackId)) expect {
try Mas.Info.parse([String(result.trackId)]).run(storeSearch: storeSearch)
expect(result).to(beSuccess()) }
.to(beSuccess())
expect(output.contents) == expectedOutput expect(output.contents) == expectedOutput
} }
} }

View file

@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class InstallCommandSpec: QuickSpec { public class InstallSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("install command") { xdescribe("install command") {
it("installs apps") { xit("installs apps") {
let cmd = InstallCommand() expect {
let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false)) try Mas.Install.parse([]).run(appLibrary: AppLibraryMock())
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// ListCommandSpec.swift // ListSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-27. // Created by Ben Chatelain on 2018-12-27.
@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class ListCommandSpec: QuickSpec { public class ListSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("list command") { describe("list command") {
it("lists apps") { it("lists apps") {
let list = ListCommand() expect {
let result = list.run(ListCommand.Options()) try Mas.List.parse([]).run(appLibrary: AppLibraryMock())
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// LuckyCommandSpec.swift // LuckySpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,7 +11,7 @@ import Quick
@testable import mas @testable import mas
public class LuckyCommandSpec: QuickSpec { public class LuckySpec: QuickSpec {
override public func spec() { override public func spec() {
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
@ -21,9 +21,10 @@ public class LuckyCommandSpec: QuickSpec {
} }
describe("lucky command") { describe("lucky command") {
xit("installs the first app matching a search") { xit("installs the first app matching a search") {
let cmd = LuckyCommand(storeSearch: storeSearch) expect {
let result = cmd.run(LuckyCommand.Options(appName: "Slack", forceInstall: false)) try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch)
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// OpenCommandSpec.swift // OpenSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2019-01-03. // Created by Ben Chatelain on 2019-01-03.
@ -12,7 +12,7 @@ import Quick
@testable import mas @testable import mas
public class OpenCommandSpec: QuickSpec { public class OpenSpec: QuickSpec {
override public func spec() { override public func spec() {
let result = SearchResult( let result = SearchResult(
trackId: 1111, trackId: 1111,
@ -21,7 +21,6 @@ public class OpenCommandSpec: QuickSpec {
) )
let storeSearch = StoreSearchMock() let storeSearch = StoreSearchMock()
let openCommand = OpenSystemCommandMock() let openCommand = OpenSystemCommandMock()
let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand)
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -31,34 +30,43 @@ public class OpenCommandSpec: QuickSpec {
storeSearch.reset() storeSearch.reset()
} }
it("fails to open app with invalid ID") { it("fails to open app with invalid ID") {
let result = cmd.run(OpenCommand.Options(appId: "-999")) expect {
expect(result) try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .searchFailed beFailure { error in
}) expect(error) == .searchFailed
}
)
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
let result = cmd.run(OpenCommand.Options(appId: "999")) expect {
expect(result) try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .noSearchResultsFound beFailure { error in
}) expect(error) == .noSearchResultsFound
}
)
} }
it("opens app in MAS") { it("opens app in MAS") {
storeSearch.apps[result.trackId] = result storeSearch.apps[result.trackId] = result
let cmdResult = cmd.run(OpenCommand.Options(appId: result.trackId.description)) expect {
expect(cmdResult).to(beSuccess()) try Mas.Open.parse([result.trackId.description])
.run(storeSearch: storeSearch, openCommand: openCommand)
}
.to(beSuccess())
expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments).toNot(beNil())
let url = URL(string: openCommand.arguments!.first!) let url = URL(string: openCommand.arguments!.first!)
expect(url).toNot(beNil()) expect(url).toNot(beNil())
expect(url?.scheme) == "macappstore" expect(url?.scheme) == "macappstore"
} }
it("just opens MAS if no app specified") { it("just opens MAS if no app specified") {
let cmdResult = cmd.run(OpenCommand.Options(appId: "appstore")) expect {
expect(cmdResult).to(beSuccess()) try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand)
}
.to(beSuccess())
expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments).toNot(beNil())
let url = URL(string: openCommand.arguments!.first!) let url = URL(string: openCommand.arguments!.first!)
expect(url).toNot(beNil()) expect(url).toNot(beNil())

View file

@ -1,5 +1,5 @@
// //
// OutdatedCommandSpec.swift // OutdatedSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,17 +11,18 @@ import Quick
@testable import mas @testable import mas
public class OutdatedCommandSpec: QuickSpec { public class OutdatedSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("outdated command") { describe("outdated command") {
it("displays apps with pending updates") { it("displays apps with pending updates") {
let cmd = OutdatedCommand() expect {
let result = cmd.run(OutdatedCommand.Options(verbose: true)) try Mas.Outdated.parse(["--verbose"])
print(result) .run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class PurchaseCommandSpec: QuickSpec { public class PurchaseSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("purchase command") { xdescribe("purchase command") {
it("purchases apps") { xit("purchases apps") {
let cmd = PurchaseCommand() expect {
let result = cmd.run(PurchaseCommand.Options(appIds: [])) try Mas.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock())
expect(result).to(beSuccess()) }
.toNot(throwError())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// ResetCommandSpec.swift // ResetSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class ResetCommandSpec: QuickSpec { public class ResetSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("reset command") { describe("reset command") {
it("resets the App Store state") { it("resets the App Store state") {
let cmd = ResetCommand() expect {
let result = cmd.run(ResetCommand.Options(debug: false)) try Mas.Reset.parse([]).runInternal()
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// SearchCommandSpec.swift // SearchSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,7 +11,7 @@ import Quick
@testable import mas @testable import mas
public class SearchCommandSpec: QuickSpec { public class SearchSpec: QuickSpec {
override public func spec() { override public func spec() {
let result = SearchResult( let result = SearchResult(
trackId: 1111, trackId: 1111,
@ -30,21 +30,20 @@ public class SearchCommandSpec: QuickSpec {
} }
it("can find slack") { it("can find slack") {
storeSearch.apps[result.trackId] = result storeSearch.apps[result.trackId] = result
expect {
let search = SearchCommand(storeSearch: storeSearch) try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch)
let searchOptions = SearchOptions(appName: "slack", price: false) }
let result = search.run(searchOptions) .to(beSuccess())
expect(result).to(beSuccess())
} }
it("fails when searching for nonexistent app") { it("fails when searching for nonexistent app") {
let search = SearchCommand(storeSearch: storeSearch) expect {
let searchOptions = SearchOptions(appName: "nonexistent", price: false) try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch)
let result = search.run(searchOptions) }
expect(result) .to(
.to( beFailure { error in
beFailure { error in expect(error) == .noSearchResultsFound
expect(error) == .noSearchResultsFound }
}) )
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// SignInCommandSpec.swift // SignInSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -12,7 +12,7 @@ import Quick
@testable import mas @testable import mas
// Deprecated test // Deprecated test
public class SignInCommandSpec: QuickSpec { public class SignInSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -20,9 +20,10 @@ public class SignInCommandSpec: QuickSpec {
// account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
xdescribe("signin command") { xdescribe("signin command") {
xit("signs in") { xit("signs in") {
let cmd = SignInCommand() expect {
let result = cmd.run(SignInCommand.Options(username: "", password: "", dialog: false)) try Mas.SignIn.parse(["", ""]).runInternal()
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// SignOutCommandSpec.swift // SignOutSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class SignOutCommandSpec: QuickSpec { public class SignOutSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("signout command") { describe("signout command") {
it("signs out") { it("signs out") {
let cmd = SignOutCommand() expect {
let result = cmd.run(SignOutCommand.Options()) try Mas.SignOut.parse([]).runInternal()
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// UninstallCommandSpec.swift // UninstallSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-27. // Created by Ben Chatelain on 2018-12-27.
@ -12,7 +12,7 @@ import Quick
@testable import mas @testable import mas
public class UninstallCommandSpec: QuickSpec { public class UninstallSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -27,60 +27,69 @@ public class UninstallCommandSpec: QuickSpec {
itemIdentifier: NSNumber(value: appId) itemIdentifier: NSNumber(value: appId)
) )
let mockLibrary = AppLibraryMock() let mockLibrary = AppLibraryMock()
let uninstall = UninstallCommand(appLibrary: mockLibrary)
context("dry run") { context("dry run") {
let options = UninstallCommand.Options(appId: appId, dryRun: true) let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appId)])
beforeEach { beforeEach {
mockLibrary.reset() mockLibrary.reset()
} }
it("can't remove a missing app") { it("can't remove a missing app") {
let result = uninstall.run(options) expect {
expect(result) uninstall.run(appLibrary: mockLibrary)
.to( }
beFailure { error in .to(
expect(error) == .notInstalled beFailure { error in
}) expect(error) == .notInstalled
}
)
} }
it("finds an app") { it("finds an app") {
mockLibrary.installedApps.append(app) mockLibrary.installedApps.append(app)
let result = uninstall.run(options) expect {
expect(result).to(beSuccess()) uninstall.run(appLibrary: mockLibrary)
}
.to(beSuccess())
} }
} }
context("wet run") { context("wet run") {
let options = UninstallCommand.Options(appId: appId, dryRun: false) let uninstall = try! Mas.Uninstall.parse([String(appId)])
beforeEach { beforeEach {
mockLibrary.reset() mockLibrary.reset()
} }
it("can't remove a missing app") { it("can't remove a missing app") {
let result = uninstall.run(options) expect {
expect(result) uninstall.run(appLibrary: mockLibrary)
.to( }
beFailure { error in .to(
expect(error) == .notInstalled beFailure { error in
}) expect(error) == .notInstalled
}
)
} }
it("removes an app") { it("removes an app") {
mockLibrary.installedApps.append(app) mockLibrary.installedApps.append(app)
let result = uninstall.run(options) expect {
expect(result).to(beSuccess()) uninstall.run(appLibrary: mockLibrary)
}
.to(beSuccess())
} }
it("fails if there is a problem with the trash command") { it("fails if there is a problem with the trash command") {
var brokenUninstall = app // make mutable copy var brokenUninstall = app // make mutable copy
brokenUninstall.bundlePath = "/dev/null" brokenUninstall.bundlePath = "/dev/null"
mockLibrary.installedApps.append(brokenUninstall) mockLibrary.installedApps.append(brokenUninstall)
let result = uninstall.run(options) expect {
expect(result) uninstall.run(appLibrary: mockLibrary)
.to( }
beFailure { error in .to(
expect(error) == .uninstallFailed beFailure { error in
}) expect(error) == .uninstallFailed
}
)
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// UpgradeCommandSpec.swift // UpgradeSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class UpgradeCommandSpec: QuickSpec { public class UpgradeSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("upgrade command") { describe("upgrade command") {
it("upgrades stuff") { it("upgrades stuff") {
let cmd = UpgradeCommand() expect {
let result = cmd.run(UpgradeCommand.Options(apps: [""])) try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -1,5 +1,5 @@
// //
// VendorCommandSpec.swift // VendorSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2019-01-03. // Created by Ben Chatelain on 2019-01-03.
@ -11,7 +11,7 @@ import Quick
@testable import mas @testable import mas
public class VendorCommandSpec: QuickSpec { public class VendorSpec: QuickSpec {
override public func spec() { override public func spec() {
let result = SearchResult( let result = SearchResult(
trackId: 1111, trackId: 1111,
@ -20,7 +20,6 @@ public class VendorCommandSpec: QuickSpec {
) )
let storeSearch = StoreSearchMock() let storeSearch = StoreSearchMock()
let openCommand = OpenSystemCommandMock() let openCommand = OpenSystemCommandMock()
let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand)
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
@ -30,26 +29,32 @@ public class VendorCommandSpec: QuickSpec {
storeSearch.reset() storeSearch.reset()
} }
it("fails to open app with invalid ID") { it("fails to open app with invalid ID") {
let result = cmd.run(VendorCommand.Options(appId: -999)) expect {
expect(result) try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .searchFailed beFailure { error in
}) expect(error) == .searchFailed
}
)
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
let result = cmd.run(VendorCommand.Options(appId: 999)) expect {
expect(result) try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
.to( }
beFailure { error in .to(
expect(error) == .noSearchResultsFound beFailure { error in
}) expect(error) == .noSearchResultsFound
}
)
} }
it("opens vendor app page in browser") { it("opens vendor app page in browser") {
storeSearch.apps[result.trackId] = result storeSearch.apps[result.trackId] = result
expect {
let cmdResult = cmd.run(VendorCommand.Options(appId: result.trackId)) try Mas.Vendor.parse([String(result.trackId)])
expect(cmdResult).to(beSuccess()) .run(storeSearch: storeSearch, openCommand: openCommand)
}
.to(beSuccess())
expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments).toNot(beNil())
expect(openCommand.arguments!.first!) == result.sellerUrl expect(openCommand.arguments!.first!) == result.sellerUrl
} }

View file

@ -1,5 +1,5 @@
// //
// VersionCommandSpec.swift // VersionSpec.swift
// masTests // masTests
// //
// Created by Ben Chatelain on 2018-12-28. // Created by Ben Chatelain on 2018-12-28.
@ -11,16 +11,17 @@ import Quick
@testable import mas @testable import mas
public class VersionCommandSpec: QuickSpec { public class VersionSpec: QuickSpec {
override public func spec() { override public func spec() {
beforeSuite { beforeSuite {
Mas.initialize() Mas.initialize()
} }
describe("version command") { describe("version command") {
it("displays the current version") { it("displays the current version") {
let cmd = VersionCommand() expect {
let result = cmd.run(VersionCommand.Options()) try Mas.Version.parse([]).runInternal()
expect(result).to(beSuccess()) }
.to(beSuccess())
} }
} }
} }

View file

@ -19,22 +19,20 @@ public class OutputListenerSpec: QuickSpec {
describe("output listener") { describe("output listener") {
it("can intercept a single line written stdout") { it("can intercept a single line written stdout") {
let output = OutputListener() let output = OutputListener()
let expectedOutput = "hi there"
print("hi there", terminator: "") print("hi there", terminator: "")
expect(output.contents) == expectedOutput expect(output.contents) == "hi there"
} }
it("can intercept multiple lines written stdout") { it("can intercept multiple lines written stdout") {
let output = OutputListener() let output = OutputListener()
let expectedOutput = """
hi there
"""
print("hi there") print("hi there")
expect(output.contents) == expectedOutput expect(output.contents) == """
hi there
"""
} }
} }
} }