mirror of
https://github.com/mas-cli/mas
synced 2024-11-24 20:43:10 +00:00
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:
parent
6793a91e03
commit
2535e3da42
42 changed files with 952 additions and 1108 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue