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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,63 +6,47 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
//
import Commandant
import ArgumentParser
import CommerceKit
/// Installs previously purchased apps from the Mac App Store.
public struct InstallCommand: CommandProtocol {
public typealias Options = InstallOptions
public let verb = "install"
public let function = "Install from the Mac App Store"
extension Mas {
/// Installs previously purchased apps from the Mac App Store.
struct Install: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Install from the Mac App Store"
)
private let appLibrary: AppLibrary
@Flag(help: "force reinstall")
var force = false
@Argument(help: "app ID(s) to install")
var appIds: [UInt64]
/// Public initializer.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary())
if case .failure = result {
try result.get()
}
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let appIds = appIds.filter { appId in
if let product = appLibrary.installedApp(forId: appId), !force {
printWarning("\(product.appName) is already installed")
return false
}
/// Runs the command.
public func run(_ options: Options) -> Result<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
}
return true
}
do {
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
do {
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
return .success(())
}
return .success(())
}
}
public struct InstallOptions: OptionsProtocol {
let appIds: [UInt64]
let forceInstall: Bool
public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions {
{ forceInstall in
InstallOptions(appIds: appIds.map { UInt64($0) }, forceInstall: forceInstall)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
/// Command which lists all installed apps.
public struct ListCommand: CommandProtocol {
public typealias Options = NoOptions<MASError>
public let verb = "list"
public let function = "Lists apps from the Mac App Store which are currently installed"
extension Mas {
/// Command which lists all installed apps.
struct List: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Lists apps from the Mac App Store which are currently installed"
)
private let appLibrary: AppLibrary
/// Public initializer.
/// - Parameter appLibrary: AppLibrary manager.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
/// Runs the command.
public func run(_: Options) -> Result<Void, MASError> {
let products = appLibrary.installedApps
if products.isEmpty {
printError("No installed apps found")
return .success(())
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary())
if case .failure = result {
try result.get()
}
}
let output = AppListFormatter.format(products: products)
print(output)
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
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.
//
import Commandant
import ArgumentParser
import CommerceKit
/// Command which installs the first search result. This is handy as many MAS titles
/// can be long with embedded keywords.
public struct LuckyCommand: CommandProtocol {
public typealias Options = LuckyOptions
public let verb = "lucky"
public let function = "Install the first result from the Mac App Store"
extension Mas {
/// Command which installs the first search result. This is handy as many MAS titles
/// can be long with embedded keywords.
struct Lucky: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Install the first result from the Mac App Store"
)
private let appLibrary: AppLibrary
private let storeSearch: StoreSearch
@Flag(help: "force reinstall")
var force = false
@Argument(help: "the app name to install")
var appName: String
public init() {
self.init(storeSearch: MasStoreSearch())
}
/// Designated initializer.
/// - Parameter storeSearch: Search manager.
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch)
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: Search manager.
init(
appLibrary: AppLibrary = MasAppLibrary(),
storeSearch: StoreSearch = MasStoreSearch()
) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<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)
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
if case .failure = result {
try result.get()
}
appId = result.trackId
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
guard let identifier = appId else { fatalError() }
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<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(())
}
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.
//
import Commandant
import ArgumentParser
import Foundation
private let markerValue = "appstore"
private let masScheme = "macappstore"
/// Opens app page in MAS app. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public struct OpenCommand: CommandProtocol {
public typealias Options = OpenOptions
public let verb = "open"
public let function = "Opens app page in AppStore.app"
private let storeSearch: StoreSearch
private var systemOpen: ExternalCommand
public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
extension Mas {
/// Opens app page in MAS app. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
struct Open: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Opens app page in AppStore.app"
)
}
/// Designated initializer.
init(
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
self.storeSearch = storeSearch
systemOpen = openCommand
}
@Argument(help: "the app ID")
var appId: String = markerValue
/// Runs the command.
public func run(_ options: OpenOptions) -> Result<Void, MASError> {
do {
if options.appId == markerValue {
// If no app ID is given, just open the MAS GUI app
try systemOpen.run(arguments: masScheme + "://")
return .success(())
/// Runs the command.
func run() throws {
let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
if case .failure = result {
try result.get()
}
guard let appId = Int(options.appId)
else {
printError("Invalid app ID")
return .failure(.noSearchResultsFound)
}
guard let result = try storeSearch.lookup(app: appId).wait()
else {
return .failure(.noSearchResultsFound)
}
guard var url = URLComponents(string: result.trackViewUrl)
else {
return .failure(.searchFailed)
}
url.scheme = masScheme
do {
try systemOpen.run(arguments: url.string!)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if systemOpen.failed {
let reason = systemOpen.process.terminationReason
printError("Open failed: (\(reason)) \(systemOpen.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
}
public struct OpenOptions: OptionsProtocol {
var appId: String
static func create(_ appId: String) -> OpenOptions {
OpenOptions(appId: appId)
}
public static func evaluate(_ mode: CommandMode) -> Result<OpenOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(defaultValue: markerValue, usage: "the app ID")
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
try openCommand.run(arguments: masScheme + "://")
return .success(())
}
guard let appId = Int(appId)
else {
printError("Invalid app ID")
return .failure(.noSearchResultsFound)
}
guard let result = try storeSearch.lookup(app: appId).wait()
else {
return .failure(.noSearchResultsFound)
}
guard var url = URLComponents(string: result.trackViewUrl)
else {
return .failure(.searchFailed)
}
url.scheme = masScheme
do {
try openCommand.run(arguments: url.string!)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
}
}

View file

@ -6,84 +6,67 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
//
import Commandant
import ArgumentParser
import Foundation
import PromiseKit
import enum Swift.Result
/// Command which displays a list of installed apps which have available updates
/// ready to be installed from the Mac App Store.
public struct OutdatedCommand: CommandProtocol {
public typealias Options = OutdatedOptions
public let verb = "outdated"
public let function = "Lists pending updates from the Mac App Store"
extension Mas {
/// Command which displays a list of installed apps which have available updates
/// ready to be installed from the Mac App Store.
struct Outdated: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Lists pending updates from the Mac App Store"
)
private let appLibrary: AppLibrary
private let storeSearch: StoreSearch
@Flag(help: "Show warnings about apps")
var verbose = false
/// Public initializer.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: StoreSearch manager.
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<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))
""")
}
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
if case .failure = result {
try result.get()
}
}
return firstly {
when(fulfilled: promises)
}.map {
Result<Void, MASError>.success(())
}.recover { error in
// Bubble up MASErrors
.value(Result<Void, MASError>.failure(error as? MASError ?? .searchFailed))
}.wait()
}
}
public struct OutdatedOptions: OptionsProtocol {
public typealias ClientError = MASError
let verbose: Bool
static func create(verbose: Bool) -> OutdatedOptions {
OutdatedOptions(verbose: verbose)
}
public static func evaluate(_ mode: CommandMode) -> Result<OutdatedOptions, CommandantError<MASError>> {
create
<*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps")
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> 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 verbose {
printWarning(
"""
Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.appName).
"""
)
}
return
}
if installedApp.isOutdatedWhenComparedTo(storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
"""
)
}
}
}
return firstly {
when(fulfilled: promises)
}.map {
Result<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.
//
import Commandant
import ArgumentParser
import CommerceKit
public struct PurchaseCommand: CommandProtocol {
public typealias Options = PurchaseOptions
public let verb = "purchase"
public let function = "Purchase and download free apps from the Mac App Store"
extension Mas {
struct Purchase: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Purchase and download free apps from the Mac App Store"
)
private let appLibrary: AppLibrary
@Argument(help: "app ID(s) to install")
var appIds: [UInt64]
/// Public initializer.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary())
if case .failure = result {
try result.get()
}
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let appIds = appIds.filter { appId in
if let product = appLibrary.installedApp(forId: appId) {
printWarning("\(product.appName) has already been purchased.")
return false
}
/// Runs the command.
public func run(_ options: Options) -> Result<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
}
return true
}
do {
try downloadAll(appIds, purchase: true).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
do {
try downloadAll(appIds, purchase: true).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
return .success(())
}
return .success(())
}
}
public struct PurchaseOptions: OptionsProtocol {
let appIds: [UInt64]
public static func create(_ appIds: [Int]) -> PurchaseOptions {
PurchaseOptions(appIds: appIds.map { UInt64($0) })
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
import CommerceKit
/// Kills several macOS processes as a means to reset the app store.
public struct ResetCommand: CommandProtocol {
public typealias Options = ResetOptions
public let verb = "reset"
public let function = "Resets the Mac App Store"
extension Mas {
/// Kills several macOS processes as a means to reset the app store.
struct Reset: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Resets the Mac App Store"
)
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
// The "Reset Application" command in the Mac App Store debug menu performs
// the following steps
//
// - killall Dock
// - killall storeagent (storeagent no longer exists)
// - rm com.apple.appstore download directory
// - clear cookies (appears to be a no-op)
//
// As storeagent no longer exists we will implement a slight variant and kill all
// App Store-associated processes
// - storeaccountd
// - storeassetd
// - storedownloadd
// - storeinstalld
// - storelegacy
@Flag(help: "Enable debug mode")
var debug = false
// Kill processes
let killProcs = [
"Dock",
"storeaccountd",
"storeassetd",
"storedownloadd",
"storeinstalld",
"storelegacy",
]
let kill = Process()
let stdout = Pipe()
let stderr = Pipe()
kill.launchPath = "/usr/bin/killall"
kill.arguments = killProcs
kill.standardOutput = stdout
kill.standardError = stderr
kill.launch()
kill.waitUntilExit()
if kill.terminationStatus != 0, options.debug {
let output = stderr.fileHandleForReading.readDataToEndOfFile()
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
}
// Wipe Download Directory
if let directory = CKDownloadDirectory(nil) {
do {
try FileManager.default.removeItem(atPath: directory)
} catch {
if options.debug {
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
}
/// Runs the command.
func run() throws {
let result = runInternal()
if case .failure = result {
try result.get()
}
}
return .success(())
}
}
public struct ResetOptions: OptionsProtocol {
let debug: Bool
public static func create(debug: Bool) -> ResetOptions {
ResetOptions(debug: debug)
}
public static func evaluate(_ mode: CommandMode) -> Result<ResetOptions, CommandantError<MASError>> {
create
<*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode")
func runInternal() -> Result<Void, MASError> {
// The "Reset Application" command in the Mac App Store debug menu performs
// the following steps
//
// - killall Dock
// - killall storeagent (storeagent no longer exists)
// - rm com.apple.appstore download directory
// - clear cookies (appears to be a no-op)
//
// As storeagent no longer exists we will implement a slight variant and kill all
// App Store-associated processes
// - storeaccountd
// - storeassetd
// - storedownloadd
// - storeinstalld
// - storelegacy
// Kill processes
let killProcs = [
"Dock",
"storeaccountd",
"storeassetd",
"storedownloadd",
"storeinstalld",
"storelegacy",
]
let kill = Process()
let stdout = Pipe()
let stderr = Pipe()
kill.launchPath = "/usr/bin/killall"
kill.arguments = killProcs
kill.standardOutput = stdout
kill.standardError = stderr
kill.launch()
kill.waitUntilExit()
if kill.terminationStatus != 0, debug {
let output = stderr.fileHandleForReading.readDataToEndOfFile()
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
}
// Wipe Download Directory
if let directory = CKDownloadDirectory(nil) {
do {
try FileManager.default.removeItem(atPath: directory)
} catch {
if debug {
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
}
}
}
return .success(())
}
}
}

View file

@ -6,62 +6,46 @@
// Copyright © 2016 Andrew Naylor. All rights reserved.
//
import Commandant
import ArgumentParser
/// Search the Mac App Store using the iTunes Search API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
public struct SearchCommand: CommandProtocol {
public typealias Options = SearchOptions
public let verb = "search"
public let function = "Search for apps from the Mac App Store"
extension Mas {
/// Search the Mac App Store using the iTunes Search API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
struct Search: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Search for apps from the Mac App Store"
)
private let storeSearch: StoreSearch
@Flag(help: "Show price of found apps")
var price = false
@Argument(help: "the app name to search")
var appName: String
public init() {
self.init(storeSearch: MasStoreSearch())
}
/// Designated initializer.
///
/// - Parameter storeSearch: Search manager.
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.storeSearch = storeSearch
}
public func run(_ options: Options) -> Result<Void, MASError> {
do {
let results = try storeSearch.search(for: options.appName).wait()
if results.isEmpty {
return .failure(.noSearchResultsFound)
func run() throws {
let result = run(storeSearch: MasStoreSearch())
if case .failure = result {
try result.get()
}
}
let output = SearchResultFormatter.format(results: results, includePrice: options.price)
print(output)
func run(storeSearch: StoreSearch) -> Result<Void, MASError> {
do {
let results = try storeSearch.search(for: appName).wait()
if results.isEmpty {
return .failure(.noSearchResultsFound)
}
return .success(())
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
let output = SearchResultFormatter.format(results: results, includePrice: price)
print(output)
return .success(())
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .failure(.searchFailed)
}
}
}
public struct SearchOptions: OptionsProtocol {
let appName: String
let price: Bool
public static func create(_ appName: String) -> (_ price: Bool) -> SearchOptions {
{ price in
SearchOptions(appName: appName, price: price)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
import StoreFoundation
public struct SignInCommand: CommandProtocol {
public typealias Options = SignInOptions
extension Mas {
struct SignIn: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signin",
abstract: "Sign in to the Mac App Store"
)
public let verb = "signin"
public let function = "Sign in to the Mac App Store"
@Flag(help: "Complete login with graphical dialog")
var dialog = false
@Argument(help: "Apple ID")
var username: String
@Argument(help: "Password")
var password: String = ""
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
do {
_ = try ISStoreAccount.signIn(
username: options.username,
password: options.password,
systemDialog: options.dialog
)
.wait()
return .success(())
} catch {
return .failure(error as? MASError ?? .signInFailed(error: error as NSError))
/// Runs the command.
func run() throws {
let result = runInternal()
if case .failure = result {
try result.get()
}
}
func runInternal() -> Result<Void, MASError> {
do {
_ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait()
return .success(())
} catch {
return .failure(error as? MASError ?? .signInFailed(error: error as NSError))
}
}
}
}
public struct SignInOptions: OptionsProtocol {
public typealias ClientError = MASError
let username: String
let password: String
let dialog: Bool
static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions {
{ password in { dialog in SignInOptions(username: username, password: password, dialog: dialog) } }
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
import CommerceKit
public struct SignOutCommand: CommandProtocol {
public typealias Options = NoOptions<MASError>
public let verb = "signout"
public let function = "Sign out of the Mac App Store"
extension Mas {
struct SignOut: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signout",
abstract: "Sign out of the Mac App Store"
)
/// Runs the command.
public func run(_: Options) -> Result<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()
/// Runs the command.
func run() throws {
let result = runInternal()
if case .failure = result {
try result.get()
}
}
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.
//
import Commandant
import ArgumentParser
import CommerceKit
import StoreFoundation
/// Command which uninstalls apps managed by the Mac App Store.
public struct UninstallCommand: CommandProtocol {
public typealias Options = UninstallOptions
public let verb = "uninstall"
public let function = "Uninstall app installed from the Mac App Store"
extension Mas {
/// Command which uninstalls apps managed by the Mac App Store.
struct Uninstall: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Uninstall app installed from the Mac App Store"
)
private let appLibrary: AppLibrary
/// Flag indicating that removal shouldn't be performed
@Flag(help: "dry run")
var dryRun = false
@Argument(help: "ID of app to uninstall")
var appId: Int
/// Public initializer.
/// - Parameter appLibrary: AppLibrary manager.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}
/// Runs the uninstall command.
///
/// - Parameter options: UninstallOptions (arguments) for this command
/// - Returns: Success or an error.
public func run(_ options: Options) -> Result<Void, MASError> {
let appId = UInt64(options.appId)
guard let product = appLibrary.installedApp(forId: appId) else {
return .failure(.notInstalled)
/// Runs the uninstall command.
func run() throws {
let result = run(appLibrary: MasAppLibrary())
if case .failure = result {
try result.get()
}
}
if options.dryRun {
printInfo("\(product.appName) \(product.bundlePath)")
printInfo("(not removed, dry run)")
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
let appId = UInt64(appId)
guard let product = appLibrary.installedApp(forId: appId) else {
return .failure(.notInstalled)
}
if dryRun {
printInfo("\(product.appName) \(product.bundlePath)")
printInfo("(not removed, dry run)")
return .success(())
}
do {
try appLibrary.uninstallApp(app: product)
} catch {
return .failure(.uninstallFailed)
}
return .success(())
}
do {
try appLibrary.uninstallApp(app: product)
} catch {
return .failure(.uninstallFailed)
}
return .success(())
}
}
/// Options for the uninstall command.
public struct UninstallOptions: OptionsProtocol {
/// Numeric app ID
let appId: Int
/// Flag indicating that removal shouldn't be performed
let dryRun: Bool
static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions {
{ dryRun in
UninstallOptions(appId: appId, dryRun: dryRun)
}
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
import Foundation
import PromiseKit
import enum Swift.Result
/// Command which upgrades apps with new versions available in the Mac App Store.
public struct UpgradeCommand: CommandProtocol {
public typealias Options = UpgradeOptions
public let verb = "upgrade"
public let function = "Upgrade outdated apps from the Mac App Store"
extension Mas {
/// Command which upgrades apps with new versions available in the Mac App Store.
struct Upgrade: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Upgrade outdated apps from the Mac App Store"
)
private let appLibrary: AppLibrary
private let storeSearch: StoreSearch
@Argument(help: "app(s) to upgrade")
var apps: [String] = []
/// Public initializer.
public init() {
self.init(appLibrary: MasAppLibrary())
}
/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
/// - Parameter storeSearch: StoreSearch manager.
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
do {
apps = try findOutdatedApps(options)
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
/// Runs the command.
func run() throws {
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
if case .failure = result {
try result.get()
}
}
guard apps.count > 0 else {
printWarning("Nothing found to upgrade")
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
do {
apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch)
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
}
guard apps.count > 0 else {
printWarning("Nothing found to upgrade")
return .success(())
}
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
print(
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" }
.joined(separator: "\n"))
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value)
do {
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
return .success(())
}
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
print(
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" }
.joined(separator: "\n"))
private func findOutdatedApps(
appLibrary: AppLibrary,
storeSearch: StoreSearch
) throws -> [(SoftwareProduct, SearchResult)] {
let apps: [SoftwareProduct] =
apps.isEmpty
? appLibrary.installedApps
: apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
}
}
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value)
do {
try downloadAll(appIds).wait()
} catch {
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
}
let promises = apps.map { installedApp in
// only upgrade apps whose local version differs from the store version
firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
}.map { result -> (SoftwareProduct, SearchResult)? in
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
}
return .success(())
}
private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] {
let apps: [SoftwareProduct] =
options.apps.isEmpty
? appLibrary.installedApps
: options.apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
return (installedApp, storeApp)
}
}
let promises = apps.map { installedApp in
// only upgrade apps whose local version differs from the store version
firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
}.map { result -> (SoftwareProduct, SearchResult)? in
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
}
return (installedApp, storeApp)
}
return try when(fulfilled: promises).wait().compactMap { $0 }
}
return try when(fulfilled: promises).wait().compactMap { $0 }
}
}
public struct UpgradeOptions: OptionsProtocol {
let apps: [String]
static func create(_ apps: [String]) -> UpgradeOptions {
UpgradeOptions(apps: apps)
}
public static func evaluate(_ mode: CommandMode) -> Result<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.
//
import Commandant
import ArgumentParser
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
public struct VendorCommand: CommandProtocol {
public typealias Options = VendorOptions
public let verb = "vendor"
public let function = "Opens vendor's app page in a browser"
private let storeSearch: StoreSearch
private var openCommand: ExternalCommand
public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
extension Mas {
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
struct Vendor: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Opens vendor's app page in a browser"
)
}
/// Designated initializer.
init(
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
self.storeSearch = storeSearch
self.openCommand = openCommand
}
@Argument(help: "the app ID to show the vendor's website")
var appId: Int
/// Runs the command.
public func run(_ options: VendorOptions) -> Result<Void, MASError> {
do {
guard let result = try storeSearch.lookup(app: options.appId).wait()
else {
return .failure(.noSearchResultsFound)
/// Runs the command.
func run() throws {
let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
if case .failure = result {
try result.get()
}
guard let vendorWebsite = result.sellerUrl
else { throw MASError.noVendorWebsite }
do {
try openCommand.run(arguments: vendorWebsite)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
}
public struct VendorOptions: OptionsProtocol {
let appId: Int
static func create(_ appId: Int) -> VendorOptions {
VendorOptions(appId: appId)
}
public static func evaluate(_ mode: CommandMode) -> Result<VendorOptions, CommandantError<MASError>> {
create
<*> mode <| Argument(usage: "the app ID to show the vendor's website")
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
do {
guard let result = try storeSearch.lookup(app: appId).wait()
else {
return .failure(.noSearchResultsFound)
}
guard let vendorWebsite = result.sellerUrl
else { throw MASError.noVendorWebsite }
do {
try openCommand.run(arguments: vendorWebsite)
} catch {
printError("Unable to launch open command")
return .failure(.searchFailed)
}
if openCommand.failed {
let reason = openCommand.process.terminationReason
printError("Open failed: (\(reason)) \(openCommand.stderr)")
return .failure(.searchFailed)
}
} catch {
// Bubble up MASErrors
if let error = error as? MASError {
return .failure(error)
}
return .failure(.searchFailed)
}
return .success(())
}
}
}

View file

@ -6,17 +6,26 @@
// Copyright © 2015 Andrew Naylor. All rights reserved.
//
import Commandant
import ArgumentParser
/// Command which displays the version of the mas tool.
public struct VersionCommand: CommandProtocol {
public typealias Options = NoOptions<MASError>
public let verb = "version"
public let function = "Print version number"
extension Mas {
/// Command which displays the version of the mas tool.
struct Version: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print version number"
)
/// Runs the command.
public func run(_: Options) -> Result<Void, MASError> {
print(Package.version)
return .success(())
/// Runs the command.
func run() throws {
let result = runInternal()
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
public enum MASError: Error, Equatable {
enum MASError: Error, Equatable {
case notSupported
case failed(error: NSError?)
@ -36,7 +36,7 @@ public enum MASError: Error, Equatable {
// MARK: - CustomStringConvertible
extension MASError: CustomStringConvertible {
public var description: String {
var description: String {
switch self {
case .notSignedIn:
return "Not signed in"

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import Foundation
import PromiseKit
extension URLSession: NetworkSession {
public func loadData(from url: URL) -> Promise<Data> {
func loadData(from url: URL) -> Promise<Data> {
Promise { seal in
dataTask(with: url) { data, _, error in
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
//
// Created by Ben Chatelain on 2018-12-28.
@ -12,7 +12,7 @@ import Quick
@testable import mas
// Deprecated test
public class AccountCommandSpec: QuickSpec {
public class AccountSpec: QuickSpec {
override public func spec() {
beforeSuite {
Mas.initialize()
@ -20,9 +20,10 @@ public class AccountCommandSpec: QuickSpec {
// account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
xdescribe("Account command") {
xit("displays active account") {
let cmd = AccountCommand()
let result = cmd.run(AccountCommand.Options())
expect(result).to(beSuccess())
expect {
try Mas.Account.parse([]).runInternal()
}
.to(beSuccess())
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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