mirror of
https://github.com/mas-cli/mas
synced 2024-11-21 19:23:01 +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" : [
|
||||
{
|
||||
"identity" : "commandant",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Carthage/Commandant.git",
|
||||
"state" : {
|
||||
"revision" : "a1671cf728db837cf5ec1980a80d276bbba748f6",
|
||||
"version" : "0.18.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cwlcatchexception",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
|
||||
"state" : {
|
||||
"revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea",
|
||||
"version" : "2.1.1"
|
||||
"revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
|
||||
"version" : "2.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -23,8 +14,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
||||
"state" : {
|
||||
"revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
|
||||
"version" : "2.1.0"
|
||||
"revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071",
|
||||
"version" : "2.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -41,8 +32,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mxcl/PromiseKit.git",
|
||||
"state" : {
|
||||
"revision" : "43772616c46a44a9977e41924ae01d0e55f2f9ca",
|
||||
"version" : "6.18.1"
|
||||
"revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
|
||||
"version" : "6.22.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -63,13 +54,22 @@
|
|||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||
"state" : {
|
||||
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "version",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mxcl/Version.git",
|
||||
"state" : {
|
||||
"revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25",
|
||||
"version" : "2.0.1"
|
||||
"revision" : "303a0f916772545e1e8667d3104f83be708a723c",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -17,11 +17,11 @@ let package = Package(
|
|||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
.package(url: "https://github.com/Carthage/Commandant.git", from: "0.18.0"),
|
||||
.package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"),
|
||||
.package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"),
|
||||
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.16.2"),
|
||||
.package(url: "https://github.com/mxcl/Version.git", from: "2.0.1"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
|
||||
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"),
|
||||
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
|
||||
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
|
||||
],
|
||||
targets: [
|
||||
|
@ -30,7 +30,7 @@ let package = Package(
|
|||
.executableTarget(
|
||||
name: "mas",
|
||||
dependencies: [
|
||||
"Commandant",
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
"PromiseKit",
|
||||
"Regex",
|
||||
"Version",
|
||||
|
|
|
@ -6,27 +6,36 @@
|
|||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
public struct AccountCommand: CommandProtocol {
|
||||
public typealias Options = NoOptions<MASError>
|
||||
public let verb = "account"
|
||||
public let function = "Prints the primary account Apple ID"
|
||||
extension Mas {
|
||||
struct Account: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Prints the primary account Apple ID"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
if #available(macOS 12, *) {
|
||||
// Account information is no longer available as of Monterey.
|
||||
// https://github.com/mas-cli/mas/issues/417
|
||||
return .failure(.notSupported)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = runInternal()
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
print(try ISStoreAccount.primaryAccount.wait().identifier)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .failed(error: error as NSError))
|
||||
func runInternal() -> Result<Void, MASError> {
|
||||
if #available(macOS 12, *) {
|
||||
// Account information is no longer available as of Monterey.
|
||||
// https://github.com/mas-cli/mas/issues/417
|
||||
return .failure(.notSupported)
|
||||
}
|
||||
|
||||
do {
|
||||
print(try ISStoreAccount.primaryAccount.wait().identifier)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .failed(error: error as NSError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,74 +6,53 @@
|
|||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
|
||||
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
public struct HomeCommand: CommandProtocol {
|
||||
public typealias Options = HomeOptions
|
||||
|
||||
public let verb = "home"
|
||||
public let function = "Opens MAS Preview app page in a browser"
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
private var openCommand: ExternalCommand
|
||||
|
||||
public init() {
|
||||
self.init(
|
||||
storeSearch: MasStoreSearch(),
|
||||
openCommand: OpenSystemCommand()
|
||||
extension Mas {
|
||||
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
struct Home: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens MAS Preview app page in a browser"
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
self.openCommand = openCommand
|
||||
}
|
||||
@Argument(help: "ID of app to show on MAS Preview")
|
||||
var appId: Int
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: HomeOptions) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: options.appId).wait() else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: result.trackViewUrl)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct HomeOptions: OptionsProtocol {
|
||||
let appId: Int
|
||||
|
||||
static func create(_ appId: Int) -> HomeOptions {
|
||||
HomeOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<HomeOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "ID of app to show on MAS Preview")
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: appId).wait() else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: result.trackViewUrl)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,55 +6,44 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
/// Displays app details. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
public struct InfoCommand: CommandProtocol {
|
||||
public let verb = "info"
|
||||
public let function = "Display app information from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Displays app details. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
struct Info: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Display app information from the Mac App Store"
|
||||
)
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
@Argument(help: "ID of app to show info")
|
||||
var appId: Int
|
||||
|
||||
public init() {
|
||||
self.init(storeSearch: MasStoreSearch())
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: InfoOptions) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: options.appId).wait() else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(storeSearch: MasStoreSearch())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct InfoOptions: OptionsProtocol {
|
||||
let appId: Int
|
||||
|
||||
static func create(_ appId: Int) -> InfoOptions {
|
||||
InfoOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<InfoOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "ID of app to show info")
|
||||
func run(storeSearch: StoreSearch) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: appId).wait() else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,63 +6,47 @@
|
|||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
/// Installs previously purchased apps from the Mac App Store.
|
||||
public struct InstallCommand: CommandProtocol {
|
||||
public typealias Options = InstallOptions
|
||||
public let verb = "install"
|
||||
public let function = "Install from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Installs previously purchased apps from the Mac App Store.
|
||||
struct Install: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Install from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
@Flag(help: "force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "app ID(s) to install")
|
||||
var appIds: [UInt64]
|
||||
|
||||
/// Public initializer.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary()) {
|
||||
self.appLibrary = appLibrary
|
||||
}
|
||||
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIds = appIds.filter { appId in
|
||||
if let product = appLibrary.installedApp(forId: appId), !force {
|
||||
printWarning("\(product.appName) is already installed")
|
||||
return false
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIds = options.appIds.filter { appId in
|
||||
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
|
||||
printWarning("\(product.appName) is already installed")
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
do {
|
||||
try downloadAll(appIds).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIds).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct InstallOptions: OptionsProtocol {
|
||||
let appIds: [UInt64]
|
||||
let forceInstall: Bool
|
||||
|
||||
public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions {
|
||||
{ forceInstall in
|
||||
InstallOptions(appIds: appIds.map { UInt64($0) }, forceInstall: forceInstall)
|
||||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<InstallOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "app ID(s) to install")
|
||||
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,39 +6,34 @@
|
|||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
|
||||
/// Command which lists all installed apps.
|
||||
public struct ListCommand: CommandProtocol {
|
||||
public typealias Options = NoOptions<MASError>
|
||||
public let verb = "list"
|
||||
public let function = "Lists apps from the Mac App Store which are currently installed"
|
||||
extension Mas {
|
||||
/// Command which lists all installed apps.
|
||||
struct List: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Lists apps from the Mac App Store which are currently installed"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
|
||||
/// Public initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary()) {
|
||||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
let products = appLibrary.installedApps
|
||||
if products.isEmpty {
|
||||
printError("No installed apps found")
|
||||
return .success(())
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
let output = AppListFormatter.format(products: products)
|
||||
print(output)
|
||||
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
|
||||
let products = appLibrary.installedApps
|
||||
if products.isEmpty {
|
||||
printError("No installed apps found")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .success(())
|
||||
let output = AppListFormatter.format(products: products)
|
||||
print(output)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,101 +6,74 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
/// Command which installs the first search result. This is handy as many MAS titles
|
||||
/// can be long with embedded keywords.
|
||||
public struct LuckyCommand: CommandProtocol {
|
||||
public typealias Options = LuckyOptions
|
||||
public let verb = "lucky"
|
||||
public let function = "Install the first result from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Command which installs the first search result. This is handy as many MAS titles
|
||||
/// can be long with embedded keywords.
|
||||
struct Lucky: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Install the first result from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
@Flag(help: "force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "the app name to install")
|
||||
var appName: String
|
||||
|
||||
public init() {
|
||||
self.init(storeSearch: MasStoreSearch())
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
/// - Parameter storeSearch: Search manager.
|
||||
init(storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch)
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
/// - Parameter storeSearch: Search manager.
|
||||
init(
|
||||
appLibrary: AppLibrary = MasAppLibrary(),
|
||||
storeSearch: StoreSearch = MasStoreSearch()
|
||||
) {
|
||||
self.appLibrary = appLibrary
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
var appId: Int?
|
||||
|
||||
do {
|
||||
let results = try storeSearch.search(for: options.appName).wait()
|
||||
guard let result = results.first else {
|
||||
printError("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
|
||||
appId = result.trackId
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
guard let identifier = appId else { fatalError() }
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
|
||||
var appId: Int?
|
||||
|
||||
return install(UInt64(identifier), options: options)
|
||||
}
|
||||
do {
|
||||
let results = try storeSearch.search(for: appName).wait()
|
||||
guard let result = results.first else {
|
||||
printError("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
appId = result.trackId
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
guard let identifier = appId else { fatalError() }
|
||||
|
||||
return install(UInt64(identifier), appLibrary: appLibrary)
|
||||
}
|
||||
|
||||
/// Installs an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appId: App identifier
|
||||
/// - appLibrary: Library of installed apps
|
||||
/// - Returns: Result of the operation.
|
||||
fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
if let product = appLibrary.installedApp(forId: appId), !force {
|
||||
printWarning("\(product.appName) is already installed")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll([appId]).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
|
||||
/// Installs an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appId: App identifier
|
||||
/// - options: command options.
|
||||
/// - Returns: Result of the operation.
|
||||
fileprivate func install(_ appId: UInt64, options: Options) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
|
||||
printWarning("\(product.appName) is already installed")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll([appId]).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct LuckyOptions: OptionsProtocol {
|
||||
let appName: String
|
||||
let forceInstall: Bool
|
||||
|
||||
public static func create(_ appName: String) -> (_ forceInstall: Bool) -> LuckyOptions {
|
||||
{ forceInstall in
|
||||
LuckyOptions(appName: appName, forceInstall: forceInstall)
|
||||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<LuckyOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "the app name to install")
|
||||
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,97 +6,76 @@
|
|||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
private let markerValue = "appstore"
|
||||
private let masScheme = "macappstore"
|
||||
|
||||
/// Opens app page in MAS app. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
public struct OpenCommand: CommandProtocol {
|
||||
public typealias Options = OpenOptions
|
||||
|
||||
public let verb = "open"
|
||||
public let function = "Opens app page in AppStore.app"
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
private var systemOpen: ExternalCommand
|
||||
|
||||
public init() {
|
||||
self.init(
|
||||
storeSearch: MasStoreSearch(),
|
||||
openCommand: OpenSystemCommand()
|
||||
extension Mas {
|
||||
/// Opens app page in MAS app. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
struct Open: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens app page in AppStore.app"
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
systemOpen = openCommand
|
||||
}
|
||||
@Argument(help: "the app ID")
|
||||
var appId: String = markerValue
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: OpenOptions) -> Result<Void, MASError> {
|
||||
do {
|
||||
if options.appId == markerValue {
|
||||
// If no app ID is given, just open the MAS GUI app
|
||||
try systemOpen.run(arguments: masScheme + "://")
|
||||
return .success(())
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
|
||||
guard let appId = Int(options.appId)
|
||||
else {
|
||||
printError("Invalid app ID")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard let result = try storeSearch.lookup(app: appId).wait()
|
||||
else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard var url = URLComponents(string: result.trackViewUrl)
|
||||
else {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
url.scheme = masScheme
|
||||
|
||||
do {
|
||||
try systemOpen.run(arguments: url.string!)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if systemOpen.failed {
|
||||
let reason = systemOpen.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(systemOpen.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenOptions: OptionsProtocol {
|
||||
var appId: String
|
||||
|
||||
static func create(_ appId: String) -> OpenOptions {
|
||||
OpenOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<OpenOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(defaultValue: markerValue, usage: "the app ID")
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
|
||||
do {
|
||||
if appId == markerValue {
|
||||
// If no app ID is given, just open the MAS GUI app
|
||||
try openCommand.run(arguments: masScheme + "://")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
guard let appId = Int(appId)
|
||||
else {
|
||||
printError("Invalid app ID")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard let result = try storeSearch.lookup(app: appId).wait()
|
||||
else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard var url = URLComponents(string: result.trackViewUrl)
|
||||
else {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
url.scheme = masScheme
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: url.string!)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,84 +6,67 @@
|
|||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
import enum Swift.Result
|
||||
|
||||
/// Command which displays a list of installed apps which have available updates
|
||||
/// ready to be installed from the Mac App Store.
|
||||
public struct OutdatedCommand: CommandProtocol {
|
||||
public typealias Options = OutdatedOptions
|
||||
public let verb = "outdated"
|
||||
public let function = "Lists pending updates from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Command which displays a list of installed apps which have available updates
|
||||
/// ready to be installed from the Mac App Store.
|
||||
struct Outdated: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Lists pending updates from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
@Flag(help: "Show warnings about apps")
|
||||
var verbose = false
|
||||
|
||||
/// Public initializer.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
/// - Parameter storeSearch: StoreSearch manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.appLibrary = appLibrary
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
let promises = appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
|
||||
}.done { storeApp in
|
||||
guard let storeApp else {
|
||||
if options.verbose {
|
||||
printWarning(
|
||||
"""
|
||||
Identifier \(installedApp.itemIdentifier) not found in store. \
|
||||
Was expected to identify \(installedApp.appName).
|
||||
""")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||
print(
|
||||
"""
|
||||
\(installedApp.itemIdentifier) \(installedApp.appName) \
|
||||
(\(installedApp.bundleVersion) -> \(storeApp.version))
|
||||
""")
|
||||
}
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
return firstly {
|
||||
when(fulfilled: promises)
|
||||
}.map {
|
||||
Result<Void, MASError>.success(())
|
||||
}.recover { error in
|
||||
// Bubble up MASErrors
|
||||
.value(Result<Void, MASError>.failure(error as? MASError ?? .searchFailed))
|
||||
}.wait()
|
||||
}
|
||||
}
|
||||
|
||||
public struct OutdatedOptions: OptionsProtocol {
|
||||
public typealias ClientError = MASError
|
||||
|
||||
let verbose: Bool
|
||||
|
||||
static func create(verbose: Bool) -> OutdatedOptions {
|
||||
OutdatedOptions(verbose: verbose)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<OutdatedOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps")
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
|
||||
let promises = appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
|
||||
}.done { storeApp in
|
||||
guard let storeApp else {
|
||||
if verbose {
|
||||
printWarning(
|
||||
"""
|
||||
Identifier \(installedApp.itemIdentifier) not found in store. \
|
||||
Was expected to identify \(installedApp.appName).
|
||||
"""
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||
print(
|
||||
"""
|
||||
\(installedApp.itemIdentifier) \(installedApp.appName) \
|
||||
(\(installedApp.bundleVersion) -> \(storeApp.version))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return firstly {
|
||||
when(fulfilled: promises)
|
||||
}.map {
|
||||
Result<Void, MASError>.success(())
|
||||
}.recover { error in
|
||||
// Bubble up MASErrors
|
||||
.value(Result<Void, MASError>.failure(error as? MASError ?? .searchFailed))
|
||||
}.wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,58 +6,44 @@
|
|||
// Copyright (c) 2017 Jakob Rieck. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
public struct PurchaseCommand: CommandProtocol {
|
||||
public typealias Options = PurchaseOptions
|
||||
public let verb = "purchase"
|
||||
public let function = "Purchase and download free apps from the Mac App Store"
|
||||
extension Mas {
|
||||
struct Purchase: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Purchase and download free apps from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
@Argument(help: "app ID(s) to install")
|
||||
var appIds: [UInt64]
|
||||
|
||||
/// Public initializer.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary()) {
|
||||
self.appLibrary = appLibrary
|
||||
}
|
||||
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIds = appIds.filter { appId in
|
||||
if let product = appLibrary.installedApp(forId: appId) {
|
||||
printWarning("\(product.appName) has already been purchased.")
|
||||
return false
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIds = options.appIds.filter { appId in
|
||||
if let product = appLibrary.installedApp(forId: appId) {
|
||||
printWarning("\(product.appName) has already been purchased.")
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
do {
|
||||
try downloadAll(appIds, purchase: true).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIds, purchase: true).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct PurchaseOptions: OptionsProtocol {
|
||||
let appIds: [UInt64]
|
||||
|
||||
public static func create(_ appIds: [Int]) -> PurchaseOptions {
|
||||
PurchaseOptions(appIds: appIds.map { UInt64($0) })
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<PurchaseOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "app ID(s) to install")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,84 +6,83 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
/// Kills several macOS processes as a means to reset the app store.
|
||||
public struct ResetCommand: CommandProtocol {
|
||||
public typealias Options = ResetOptions
|
||||
public let verb = "reset"
|
||||
public let function = "Resets the Mac App Store"
|
||||
extension Mas {
|
||||
/// Kills several macOS processes as a means to reset the app store.
|
||||
struct Reset: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Resets the Mac App Store"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
// The "Reset Application" command in the Mac App Store debug menu performs
|
||||
// the following steps
|
||||
//
|
||||
// - killall Dock
|
||||
// - killall storeagent (storeagent no longer exists)
|
||||
// - rm com.apple.appstore download directory
|
||||
// - clear cookies (appears to be a no-op)
|
||||
//
|
||||
// As storeagent no longer exists we will implement a slight variant and kill all
|
||||
// App Store-associated processes
|
||||
// - storeaccountd
|
||||
// - storeassetd
|
||||
// - storedownloadd
|
||||
// - storeinstalld
|
||||
// - storelegacy
|
||||
@Flag(help: "Enable debug mode")
|
||||
var debug = false
|
||||
|
||||
// Kill processes
|
||||
let killProcs = [
|
||||
"Dock",
|
||||
"storeaccountd",
|
||||
"storeassetd",
|
||||
"storedownloadd",
|
||||
"storeinstalld",
|
||||
"storelegacy",
|
||||
]
|
||||
|
||||
let kill = Process()
|
||||
let stdout = Pipe()
|
||||
let stderr = Pipe()
|
||||
|
||||
kill.launchPath = "/usr/bin/killall"
|
||||
kill.arguments = killProcs
|
||||
kill.standardOutput = stdout
|
||||
kill.standardError = stderr
|
||||
|
||||
kill.launch()
|
||||
kill.waitUntilExit()
|
||||
|
||||
if kill.terminationStatus != 0, options.debug {
|
||||
let output = stderr.fileHandleForReading.readDataToEndOfFile()
|
||||
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
|
||||
}
|
||||
|
||||
// Wipe Download Directory
|
||||
if let directory = CKDownloadDirectory(nil) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: directory)
|
||||
} catch {
|
||||
if options.debug {
|
||||
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
|
||||
}
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = runInternal()
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct ResetOptions: OptionsProtocol {
|
||||
let debug: Bool
|
||||
|
||||
public static func create(debug: Bool) -> ResetOptions {
|
||||
ResetOptions(debug: debug)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<ResetOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode")
|
||||
func runInternal() -> Result<Void, MASError> {
|
||||
// The "Reset Application" command in the Mac App Store debug menu performs
|
||||
// the following steps
|
||||
//
|
||||
// - killall Dock
|
||||
// - killall storeagent (storeagent no longer exists)
|
||||
// - rm com.apple.appstore download directory
|
||||
// - clear cookies (appears to be a no-op)
|
||||
//
|
||||
// As storeagent no longer exists we will implement a slight variant and kill all
|
||||
// App Store-associated processes
|
||||
// - storeaccountd
|
||||
// - storeassetd
|
||||
// - storedownloadd
|
||||
// - storeinstalld
|
||||
// - storelegacy
|
||||
|
||||
// Kill processes
|
||||
let killProcs = [
|
||||
"Dock",
|
||||
"storeaccountd",
|
||||
"storeassetd",
|
||||
"storedownloadd",
|
||||
"storeinstalld",
|
||||
"storelegacy",
|
||||
]
|
||||
|
||||
let kill = Process()
|
||||
let stdout = Pipe()
|
||||
let stderr = Pipe()
|
||||
|
||||
kill.launchPath = "/usr/bin/killall"
|
||||
kill.arguments = killProcs
|
||||
kill.standardOutput = stdout
|
||||
kill.standardError = stderr
|
||||
|
||||
kill.launch()
|
||||
kill.waitUntilExit()
|
||||
|
||||
if kill.terminationStatus != 0, debug {
|
||||
let output = stderr.fileHandleForReading.readDataToEndOfFile()
|
||||
printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)")
|
||||
}
|
||||
|
||||
// Wipe Download Directory
|
||||
if let directory = CKDownloadDirectory(nil) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: directory)
|
||||
} catch {
|
||||
if debug {
|
||||
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,62 +6,46 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
|
||||
/// Search the Mac App Store using the iTunes Search API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
|
||||
public struct SearchCommand: CommandProtocol {
|
||||
public typealias Options = SearchOptions
|
||||
public let verb = "search"
|
||||
public let function = "Search for apps from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Search the Mac App Store using the iTunes Search API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
|
||||
struct Search: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Search for apps from the Mac App Store"
|
||||
)
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
@Flag(help: "Show price of found apps")
|
||||
var price = false
|
||||
@Argument(help: "the app name to search")
|
||||
var appName: String
|
||||
|
||||
public init() {
|
||||
self.init(storeSearch: MasStoreSearch())
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// - Parameter storeSearch: Search manager.
|
||||
init(storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
do {
|
||||
let results = try storeSearch.search(for: options.appName).wait()
|
||||
if results.isEmpty {
|
||||
return .failure(.noSearchResultsFound)
|
||||
func run() throws {
|
||||
let result = run(storeSearch: MasStoreSearch())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
let output = SearchResultFormatter.format(results: results, includePrice: options.price)
|
||||
print(output)
|
||||
func run(storeSearch: StoreSearch) -> Result<Void, MASError> {
|
||||
do {
|
||||
let results = try storeSearch.search(for: appName).wait()
|
||||
if results.isEmpty {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
let output = SearchResultFormatter.format(results: results, includePrice: price)
|
||||
print(output)
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchOptions: OptionsProtocol {
|
||||
let appName: String
|
||||
let price: Bool
|
||||
|
||||
public static func create(_ appName: String) -> (_ price: Bool) -> SearchOptions {
|
||||
{ price in
|
||||
SearchOptions(appName: appName, price: price)
|
||||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<SearchOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "the app name to search")
|
||||
<*> mode <| Option(key: "price", defaultValue: false, usage: "Show price of found apps")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,46 +6,38 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
public struct SignInCommand: CommandProtocol {
|
||||
public typealias Options = SignInOptions
|
||||
extension Mas {
|
||||
struct SignIn: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signin",
|
||||
abstract: "Sign in to the Mac App Store"
|
||||
)
|
||||
|
||||
public let verb = "signin"
|
||||
public let function = "Sign in to the Mac App Store"
|
||||
@Flag(help: "Complete login with graphical dialog")
|
||||
var dialog = false
|
||||
@Argument(help: "Apple ID")
|
||||
var username: String
|
||||
@Argument(help: "Password")
|
||||
var password: String = ""
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
do {
|
||||
_ = try ISStoreAccount.signIn(
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
systemDialog: options.dialog
|
||||
)
|
||||
.wait()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .signInFailed(error: error as NSError))
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = runInternal()
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
func runInternal() -> Result<Void, MASError> {
|
||||
do {
|
||||
_ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .signInFailed(error: error as NSError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SignInOptions: OptionsProtocol {
|
||||
public typealias ClientError = MASError
|
||||
|
||||
let username: String
|
||||
let password: String
|
||||
let dialog: Bool
|
||||
|
||||
static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions {
|
||||
{ password in { dialog in SignInOptions(username: username, password: password, dialog: dialog) } }
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<SignInOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "Apple ID")
|
||||
<*> mode <| Argument(defaultValue: "", usage: "Password")
|
||||
<*> mode <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,24 +6,34 @@
|
|||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
public struct SignOutCommand: CommandProtocol {
|
||||
public typealias Options = NoOptions<MASError>
|
||||
public let verb = "signout"
|
||||
public let function = "Sign out of the Mac App Store"
|
||||
extension Mas {
|
||||
struct SignOut: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signout",
|
||||
abstract: "Sign out of the Mac App Store"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
if #available(macOS 10.13, *) {
|
||||
ISServiceProxy.genericShared().accountService.signOut()
|
||||
} else {
|
||||
// Using CKAccountStore to sign out does nothing on High Sierra
|
||||
// https://github.com/mas-cli/mas/issues/129
|
||||
CKAccountStore.shared().signOut()
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = runInternal()
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
return .success(())
|
||||
func runInternal() -> Result<Void, MASError> {
|
||||
if #available(macOS 10.13, *) {
|
||||
ISServiceProxy.genericShared().accountService.signOut()
|
||||
} else {
|
||||
// Using CKAccountStore to sign out does nothing on High Sierra
|
||||
// https://github.com/mas-cli/mas/issues/129
|
||||
CKAccountStore.shared().signOut()
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,75 +6,52 @@
|
|||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Command which uninstalls apps managed by the Mac App Store.
|
||||
public struct UninstallCommand: CommandProtocol {
|
||||
public typealias Options = UninstallOptions
|
||||
public let verb = "uninstall"
|
||||
public let function = "Uninstall app installed from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Command which uninstalls apps managed by the Mac App Store.
|
||||
struct Uninstall: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Uninstall app installed from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
/// Flag indicating that removal shouldn't be performed
|
||||
@Flag(help: "dry run")
|
||||
var dryRun = false
|
||||
@Argument(help: "ID of app to uninstall")
|
||||
var appId: Int
|
||||
|
||||
/// Public initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary()) {
|
||||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the uninstall command.
|
||||
///
|
||||
/// - Parameter options: UninstallOptions (arguments) for this command
|
||||
/// - Returns: Success or an error.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
let appId = UInt64(options.appId)
|
||||
|
||||
guard let product = appLibrary.installedApp(forId: appId) else {
|
||||
return .failure(.notInstalled)
|
||||
/// Runs the uninstall command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
if options.dryRun {
|
||||
printInfo("\(product.appName) \(product.bundlePath)")
|
||||
printInfo("(not removed, dry run)")
|
||||
func run(appLibrary: AppLibrary) -> Result<Void, MASError> {
|
||||
let appId = UInt64(appId)
|
||||
|
||||
guard let product = appLibrary.installedApp(forId: appId) else {
|
||||
return .failure(.notInstalled)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
printInfo("\(product.appName) \(product.bundlePath)")
|
||||
printInfo("(not removed, dry run)")
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
do {
|
||||
try appLibrary.uninstallApp(app: product)
|
||||
} catch {
|
||||
return .failure(.uninstallFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
do {
|
||||
try appLibrary.uninstallApp(app: product)
|
||||
} catch {
|
||||
return .failure(.uninstallFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for the uninstall command.
|
||||
public struct UninstallOptions: OptionsProtocol {
|
||||
/// Numeric app ID
|
||||
let appId: Int
|
||||
|
||||
/// Flag indicating that removal shouldn't be performed
|
||||
let dryRun: Bool
|
||||
|
||||
static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions {
|
||||
{ dryRun in
|
||||
UninstallOptions(appId: appId, dryRun: dryRun)
|
||||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<UninstallOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "ID of app to uninstall")
|
||||
<*> mode <| Switch(flag: nil, key: "dry-run", usage: "dry run")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,104 +6,90 @@
|
|||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
import enum Swift.Result
|
||||
|
||||
/// Command which upgrades apps with new versions available in the Mac App Store.
|
||||
public struct UpgradeCommand: CommandProtocol {
|
||||
public typealias Options = UpgradeOptions
|
||||
public let verb = "upgrade"
|
||||
public let function = "Upgrade outdated apps from the Mac App Store"
|
||||
extension Mas {
|
||||
/// Command which upgrades apps with new versions available in the Mac App Store.
|
||||
struct Upgrade: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Upgrade outdated apps from the Mac App Store"
|
||||
)
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
@Argument(help: "app(s) to upgrade")
|
||||
var apps: [String] = []
|
||||
|
||||
/// Public initializer.
|
||||
public init() {
|
||||
self.init(appLibrary: MasAppLibrary())
|
||||
}
|
||||
|
||||
/// Internal initializer.
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
/// - Parameter storeSearch: StoreSearch manager.
|
||||
init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.appLibrary = appLibrary
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
|
||||
do {
|
||||
apps = try findOutdatedApps(options)
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
return .failure(error as? MASError ?? .searchFailed)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
guard apps.count > 0 else {
|
||||
printWarning("Nothing found to upgrade")
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) -> Result<Void, MASError> {
|
||||
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
|
||||
do {
|
||||
apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch)
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
return .failure(error as? MASError ?? .searchFailed)
|
||||
}
|
||||
|
||||
guard apps.count > 0 else {
|
||||
printWarning("Nothing found to upgrade")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
|
||||
print(
|
||||
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" }
|
||||
.joined(separator: "\n"))
|
||||
|
||||
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value)
|
||||
do {
|
||||
try downloadAll(appIds).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
|
||||
print(
|
||||
apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" }
|
||||
.joined(separator: "\n"))
|
||||
private func findOutdatedApps(
|
||||
appLibrary: AppLibrary,
|
||||
storeSearch: StoreSearch
|
||||
) throws -> [(SoftwareProduct, SearchResult)] {
|
||||
let apps: [SoftwareProduct] =
|
||||
apps.isEmpty
|
||||
? appLibrary.installedApps
|
||||
: apps.compactMap {
|
||||
if let appId = UInt64($0) {
|
||||
// if argument a UInt64, lookup app by id using argument
|
||||
return appLibrary.installedApp(forId: appId)
|
||||
} else {
|
||||
// if argument not a UInt64, lookup app by name using argument
|
||||
return appLibrary.installedApp(named: $0)
|
||||
}
|
||||
}
|
||||
|
||||
let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value)
|
||||
do {
|
||||
try downloadAll(appIds).wait()
|
||||
} catch {
|
||||
return .failure(error as? MASError ?? .downloadFailed(error: error as NSError))
|
||||
}
|
||||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
|
||||
}.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] {
|
||||
let apps: [SoftwareProduct] =
|
||||
options.apps.isEmpty
|
||||
? appLibrary.installedApps
|
||||
: options.apps.compactMap {
|
||||
if let appId = UInt64($0) {
|
||||
// if argument a UInt64, lookup app by id using argument
|
||||
return appLibrary.installedApp(forId: appId)
|
||||
} else {
|
||||
// if argument not a UInt64, lookup app by name using argument
|
||||
return appLibrary.installedApp(named: $0)
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
}
|
||||
|
||||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
storeSearch.lookup(app: installedApp.itemIdentifier.intValue)
|
||||
}.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||
}
|
||||
|
||||
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
public struct UpgradeOptions: OptionsProtocol {
|
||||
let apps: [String]
|
||||
|
||||
static func create(_ apps: [String]) -> UpgradeOptions {
|
||||
UpgradeOptions(apps: apps)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<UpgradeOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(defaultValue: [], usage: "app(s) to upgrade")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,78 +6,57 @@
|
|||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
|
||||
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
public struct VendorCommand: CommandProtocol {
|
||||
public typealias Options = VendorOptions
|
||||
|
||||
public let verb = "vendor"
|
||||
public let function = "Opens vendor's app page in a browser"
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
private var openCommand: ExternalCommand
|
||||
|
||||
public init() {
|
||||
self.init(
|
||||
storeSearch: MasStoreSearch(),
|
||||
openCommand: OpenSystemCommand()
|
||||
extension Mas {
|
||||
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
|
||||
struct Vendor: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens vendor's app page in a browser"
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
self.openCommand = openCommand
|
||||
}
|
||||
@Argument(help: "the app ID to show the vendor's website")
|
||||
var appId: Int
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: VendorOptions) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: options.appId).wait()
|
||||
else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
|
||||
guard let vendorWebsite = result.sellerUrl
|
||||
else { throw MASError.noVendorWebsite }
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: vendorWebsite)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public struct VendorOptions: OptionsProtocol {
|
||||
let appId: Int
|
||||
|
||||
static func create(_ appId: Int) -> VendorOptions {
|
||||
VendorOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<VendorOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Argument(usage: "the app ID to show the vendor's website")
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) -> Result<Void, MASError> {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: appId).wait()
|
||||
else {
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard let vendorWebsite = result.sellerUrl
|
||||
else { throw MASError.noVendorWebsite }
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: vendorWebsite)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,26 @@
|
|||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import ArgumentParser
|
||||
|
||||
/// Command which displays the version of the mas tool.
|
||||
public struct VersionCommand: CommandProtocol {
|
||||
public typealias Options = NoOptions<MASError>
|
||||
public let verb = "version"
|
||||
public let function = "Print version number"
|
||||
extension Mas {
|
||||
/// Command which displays the version of the mas tool.
|
||||
struct Version: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Print version number"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
print(Package.version)
|
||||
return .success(())
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
let result = runInternal()
|
||||
if case .failure = result {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
func runInternal() -> Result<Void, MASError> {
|
||||
print(Package.version)
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum MASError: Error, Equatable {
|
||||
enum MASError: Error, Equatable {
|
||||
case notSupported
|
||||
|
||||
case failed(error: NSError?)
|
||||
|
@ -36,7 +36,7 @@ public enum MASError: Error, Equatable {
|
|||
|
||||
// MARK: - CustomStringConvertible
|
||||
extension MASError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .notSignedIn:
|
||||
return "Not signed in"
|
||||
|
|
|
@ -91,7 +91,7 @@ func printInfo(_ message: String) {
|
|||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Warning:" underlined in yellow.
|
||||
public func printWarning(_ message: String) {
|
||||
func printWarning(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Warning: \(message)", to: &standardError)
|
||||
return
|
||||
|
@ -102,7 +102,7 @@ public func printWarning(_ message: String) {
|
|||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Error:" underlined in red.
|
||||
public func printError(_ message: String) {
|
||||
func printError(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Error: \(message)", to: &standardError)
|
||||
return
|
||||
|
|
|
@ -6,10 +6,39 @@
|
|||
// Copyright © 2021 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import PromiseKit
|
||||
|
||||
public enum Mas {
|
||||
public static func initialize() {
|
||||
@main
|
||||
struct Mas: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Mac App Store command-line interface",
|
||||
subcommands: [
|
||||
Account.self,
|
||||
Home.self,
|
||||
Info.self,
|
||||
Install.self,
|
||||
List.self,
|
||||
Lucky.self,
|
||||
Open.self,
|
||||
Outdated.self,
|
||||
Purchase.self,
|
||||
Reset.self,
|
||||
Search.self,
|
||||
SignIn.self,
|
||||
SignOut.self,
|
||||
Uninstall.self,
|
||||
Upgrade.self,
|
||||
Vendor.self,
|
||||
Version.self,
|
||||
]
|
||||
)
|
||||
|
||||
func validate() throws {
|
||||
Mas.initialize()
|
||||
}
|
||||
|
||||
static func initialize() {
|
||||
PromiseKit.conf.Q.map = .global()
|
||||
PromiseKit.conf.Q.return = .global()
|
||||
PromiseKit.conf.logHandler = { event in
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import PromiseKit
|
||||
|
||||
extension URLSession: NetworkSession {
|
||||
public func loadData(from url: URL) -> Promise<Data> {
|
||||
func loadData(from url: URL) -> Promise<Data> {
|
||||
Promise { seal in
|
||||
dataTask(with: url) { data, _, error in
|
||||
if let data {
|
||||
|
|
|
@ -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
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -12,7 +12,7 @@ import Quick
|
|||
@testable import mas
|
||||
|
||||
// Deprecated test
|
||||
public class AccountCommandSpec: QuickSpec {
|
||||
public class AccountSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -20,9 +20,10 @@ public class AccountCommandSpec: QuickSpec {
|
|||
// account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
|
||||
xdescribe("Account command") {
|
||||
xit("displays active account") {
|
||||
let cmd = AccountCommand()
|
||||
let result = cmd.run(AccountCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Account.parse([]).runInternal()
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// HomeCommandSpec.swift
|
||||
// HomeSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
|
@ -11,7 +11,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class HomeCommandSpec: QuickSpec {
|
||||
public class HomeSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
|
@ -20,7 +20,6 @@ public class HomeCommandSpec: QuickSpec {
|
|||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -30,26 +29,32 @@ public class HomeCommandSpec: QuickSpec {
|
|||
storeSearch.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
let result = cmd.run(HomeCommand.Options(appId: -999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
})
|
||||
expect {
|
||||
try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
}
|
||||
)
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
let result = cmd.run(HomeCommand.Options(appId: 999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
expect {
|
||||
try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
}
|
||||
)
|
||||
}
|
||||
it("opens app on MAS Preview") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
|
||||
let cmdResult = cmd.run(HomeCommand.Options(appId: result.trackId))
|
||||
expect(cmdResult).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Home.parse([String(result.trackId)]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(beSuccess())
|
||||
expect(openCommand.arguments).toNot(beNil())
|
||||
expect(openCommand.arguments!.first!) == result.trackViewUrl
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// InfoCommandSpec.swift
|
||||
// InfoSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,7 +11,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class InfoCommandSpec: QuickSpec {
|
||||
public class InfoSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
currentVersionReleaseDate: "2019-01-07T18:53:13Z",
|
||||
|
@ -25,7 +25,6 @@ public class InfoCommandSpec: QuickSpec {
|
|||
version: "1.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let cmd = InfoCommand(storeSearch: storeSearch)
|
||||
let expectedOutput = """
|
||||
Awesome App 1.0 [2.0]
|
||||
By: Awesome Dev
|
||||
|
@ -44,28 +43,33 @@ public class InfoCommandSpec: QuickSpec {
|
|||
storeSearch.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
let result = cmd.run(InfoCommand.Options(appId: -999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
})
|
||||
expect {
|
||||
try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
}
|
||||
)
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
let result = cmd.run(InfoCommand.Options(appId: 999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
expect {
|
||||
try Mas.Info.parse(["999"]).run(storeSearch: storeSearch)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
}
|
||||
)
|
||||
}
|
||||
it("displays app details") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
let output = OutputListener()
|
||||
|
||||
let result = cmd.run(InfoCommand.Options(appId: result.trackId))
|
||||
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Info.parse([String(result.trackId)]).run(storeSearch: storeSearch)
|
||||
}
|
||||
.to(beSuccess())
|
||||
expect(output.contents) == expectedOutput
|
||||
}
|
||||
}
|
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class InstallCommandSpec: QuickSpec {
|
||||
public class InstallSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("install command") {
|
||||
it("installs apps") {
|
||||
let cmd = InstallCommand()
|
||||
let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false))
|
||||
expect(result).to(beSuccess())
|
||||
xdescribe("install command") {
|
||||
xit("installs apps") {
|
||||
expect {
|
||||
try Mas.Install.parse([]).run(appLibrary: AppLibraryMock())
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// ListCommandSpec.swift
|
||||
// ListSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class ListCommandSpec: QuickSpec {
|
||||
public class ListSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("list command") {
|
||||
it("lists apps") {
|
||||
let list = ListCommand()
|
||||
let result = list.run(ListCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.List.parse([]).run(appLibrary: AppLibraryMock())
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// LuckyCommandSpec.swift
|
||||
// LuckySpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,7 +11,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class LuckyCommandSpec: QuickSpec {
|
||||
public class LuckySpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
|
@ -21,9 +21,10 @@ public class LuckyCommandSpec: QuickSpec {
|
|||
}
|
||||
describe("lucky command") {
|
||||
xit("installs the first app matching a search") {
|
||||
let cmd = LuckyCommand(storeSearch: storeSearch)
|
||||
let result = cmd.run(LuckyCommand.Options(appName: "Slack", forceInstall: false))
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch)
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OpenCommandSpec.swift
|
||||
// OpenSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-03.
|
||||
|
@ -12,7 +12,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class OpenCommandSpec: QuickSpec {
|
||||
public class OpenSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
|
@ -21,7 +21,6 @@ public class OpenCommandSpec: QuickSpec {
|
|||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -31,34 +30,43 @@ public class OpenCommandSpec: QuickSpec {
|
|||
storeSearch.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
let result = cmd.run(OpenCommand.Options(appId: "-999"))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
})
|
||||
expect {
|
||||
try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
}
|
||||
)
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
let result = cmd.run(OpenCommand.Options(appId: "999"))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
expect {
|
||||
try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
}
|
||||
)
|
||||
}
|
||||
it("opens app in MAS") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
|
||||
let cmdResult = cmd.run(OpenCommand.Options(appId: result.trackId.description))
|
||||
expect(cmdResult).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Open.parse([result.trackId.description])
|
||||
.run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(beSuccess())
|
||||
expect(openCommand.arguments).toNot(beNil())
|
||||
let url = URL(string: openCommand.arguments!.first!)
|
||||
expect(url).toNot(beNil())
|
||||
expect(url?.scheme) == "macappstore"
|
||||
}
|
||||
it("just opens MAS if no app specified") {
|
||||
let cmdResult = cmd.run(OpenCommand.Options(appId: "appstore"))
|
||||
expect(cmdResult).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(beSuccess())
|
||||
expect(openCommand.arguments).toNot(beNil())
|
||||
let url = URL(string: openCommand.arguments!.first!)
|
||||
expect(url).toNot(beNil())
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OutdatedCommandSpec.swift
|
||||
// OutdatedSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,17 +11,18 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class OutdatedCommandSpec: QuickSpec {
|
||||
public class OutdatedSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("outdated command") {
|
||||
it("displays apps with pending updates") {
|
||||
let cmd = OutdatedCommand()
|
||||
let result = cmd.run(OutdatedCommand.Options(verbose: true))
|
||||
print(result)
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Outdated.parse(["--verbose"])
|
||||
.run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class PurchaseCommandSpec: QuickSpec {
|
||||
public class PurchaseSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("purchase command") {
|
||||
it("purchases apps") {
|
||||
let cmd = PurchaseCommand()
|
||||
let result = cmd.run(PurchaseCommand.Options(appIds: []))
|
||||
expect(result).to(beSuccess())
|
||||
xdescribe("purchase command") {
|
||||
xit("purchases apps") {
|
||||
expect {
|
||||
try Mas.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// ResetCommandSpec.swift
|
||||
// ResetSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class ResetCommandSpec: QuickSpec {
|
||||
public class ResetSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("reset command") {
|
||||
it("resets the App Store state") {
|
||||
let cmd = ResetCommand()
|
||||
let result = cmd.run(ResetCommand.Options(debug: false))
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Reset.parse([]).runInternal()
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SearchCommandSpec.swift
|
||||
// SearchSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,7 +11,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class SearchCommandSpec: QuickSpec {
|
||||
public class SearchSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
|
@ -30,21 +30,20 @@ public class SearchCommandSpec: QuickSpec {
|
|||
}
|
||||
it("can find slack") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
|
||||
let search = SearchCommand(storeSearch: storeSearch)
|
||||
let searchOptions = SearchOptions(appName: "slack", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch)
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
it("fails when searching for nonexistent app") {
|
||||
let search = SearchCommand(storeSearch: storeSearch)
|
||||
let searchOptions = SearchOptions(appName: "nonexistent", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
expect {
|
||||
try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SignInCommandSpec.swift
|
||||
// SignInSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -12,7 +12,7 @@ import Quick
|
|||
@testable import mas
|
||||
|
||||
// Deprecated test
|
||||
public class SignInCommandSpec: QuickSpec {
|
||||
public class SignInSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -20,9 +20,10 @@ public class SignInCommandSpec: QuickSpec {
|
|||
// account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
|
||||
xdescribe("signin command") {
|
||||
xit("signs in") {
|
||||
let cmd = SignInCommand()
|
||||
let result = cmd.run(SignInCommand.Options(username: "", password: "", dialog: false))
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.SignIn.parse(["", ""]).runInternal()
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SignOutCommandSpec.swift
|
||||
// SignOutSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class SignOutCommandSpec: QuickSpec {
|
||||
public class SignOutSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("signout command") {
|
||||
it("signs out") {
|
||||
let cmd = SignOutCommand()
|
||||
let result = cmd.run(SignOutCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.SignOut.parse([]).runInternal()
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// UninstallCommandSpec.swift
|
||||
// UninstallSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
|
@ -12,7 +12,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class UninstallCommandSpec: QuickSpec {
|
||||
public class UninstallSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -27,60 +27,69 @@ public class UninstallCommandSpec: QuickSpec {
|
|||
itemIdentifier: NSNumber(value: appId)
|
||||
)
|
||||
let mockLibrary = AppLibraryMock()
|
||||
let uninstall = UninstallCommand(appLibrary: mockLibrary)
|
||||
|
||||
context("dry run") {
|
||||
let options = UninstallCommand.Options(appId: appId, dryRun: true)
|
||||
let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appId)])
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
}
|
||||
it("can't remove a missing app") {
|
||||
let result = uninstall.run(options)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
})
|
||||
expect {
|
||||
uninstall.run(appLibrary: mockLibrary)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
}
|
||||
)
|
||||
}
|
||||
it("finds an app") {
|
||||
mockLibrary.installedApps.append(app)
|
||||
|
||||
let result = uninstall.run(options)
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
uninstall.run(appLibrary: mockLibrary)
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
context("wet run") {
|
||||
let options = UninstallCommand.Options(appId: appId, dryRun: false)
|
||||
let uninstall = try! Mas.Uninstall.parse([String(appId)])
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
}
|
||||
it("can't remove a missing app") {
|
||||
let result = uninstall.run(options)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
})
|
||||
expect {
|
||||
uninstall.run(appLibrary: mockLibrary)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
}
|
||||
)
|
||||
}
|
||||
it("removes an app") {
|
||||
mockLibrary.installedApps.append(app)
|
||||
|
||||
let result = uninstall.run(options)
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
uninstall.run(appLibrary: mockLibrary)
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
it("fails if there is a problem with the trash command") {
|
||||
var brokenUninstall = app // make mutable copy
|
||||
brokenUninstall.bundlePath = "/dev/null"
|
||||
mockLibrary.installedApps.append(brokenUninstall)
|
||||
|
||||
let result = uninstall.run(options)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .uninstallFailed
|
||||
})
|
||||
expect {
|
||||
uninstall.run(appLibrary: mockLibrary)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .uninstallFailed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// UpgradeCommandSpec.swift
|
||||
// UpgradeSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class UpgradeCommandSpec: QuickSpec {
|
||||
public class UpgradeSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("upgrade command") {
|
||||
it("upgrades stuff") {
|
||||
let cmd = UpgradeCommand()
|
||||
let result = cmd.run(UpgradeCommand.Options(apps: [""]))
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// VendorCommandSpec.swift
|
||||
// VendorSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-03.
|
||||
|
@ -11,7 +11,7 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class VendorCommandSpec: QuickSpec {
|
||||
public class VendorSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
|
@ -20,7 +20,6 @@ public class VendorCommandSpec: QuickSpec {
|
|||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
|
@ -30,26 +29,32 @@ public class VendorCommandSpec: QuickSpec {
|
|||
storeSearch.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
let result = cmd.run(VendorCommand.Options(appId: -999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
})
|
||||
expect {
|
||||
try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
}
|
||||
)
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
let result = cmd.run(VendorCommand.Options(appId: 999))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
expect {
|
||||
try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
}
|
||||
)
|
||||
}
|
||||
it("opens vendor app page in browser") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
|
||||
let cmdResult = cmd.run(VendorCommand.Options(appId: result.trackId))
|
||||
expect(cmdResult).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Vendor.parse([String(result.trackId)])
|
||||
.run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
}
|
||||
.to(beSuccess())
|
||||
expect(openCommand.arguments).toNot(beNil())
|
||||
expect(openCommand.arguments!.first!) == result.sellerUrl
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// VersionCommandSpec.swift
|
||||
// VersionSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
|
@ -11,16 +11,17 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class VersionCommandSpec: QuickSpec {
|
||||
public class VersionSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
}
|
||||
describe("version command") {
|
||||
it("displays the current version") {
|
||||
let cmd = VersionCommand()
|
||||
let result = cmd.run(VersionCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
expect {
|
||||
try Mas.Version.parse([]).runInternal()
|
||||
}
|
||||
.to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,22 +19,20 @@ public class OutputListenerSpec: QuickSpec {
|
|||
describe("output listener") {
|
||||
it("can intercept a single line written stdout") {
|
||||
let output = OutputListener()
|
||||
let expectedOutput = "hi there"
|
||||
|
||||
print("hi there", terminator: "")
|
||||
|
||||
expect(output.contents) == expectedOutput
|
||||
expect(output.contents) == "hi there"
|
||||
}
|
||||
it("can intercept multiple lines written stdout") {
|
||||
let output = OutputListener()
|
||||
let expectedOutput = """
|
||||
hi there
|
||||
|
||||
"""
|
||||
|
||||
print("hi there")
|
||||
|
||||
expect(output.contents) == expectedOutput
|
||||
expect(output.contents) == """
|
||||
hi there
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue