mirror of
https://github.com/mas-cli/mas
synced 2024-11-21 11:13:06 +00:00
Merge branch 'main' into upstream/releases/release-1.8.7
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> # Manual Merge Cleanup: # .actrc # .github/workflows/pr-checks.yml # .github/workflows/release.yml # Brewfile # script/test # script/version_bump # Conflicts: # .github/workflows/build-test.yml # .gitignore # .swiftlint.yml # Brewfile.lock.json # Package.resolved # Sources/mas/Package.swift # Tests/masTests/.swiftlint.yml # script/bootstrap # script/build # script/format # script/lint # script/uninstall # script/version
This commit is contained in:
commit
e9fcf2b254
173 changed files with 5215 additions and 6973 deletions
2
.actrc
2
.actrc
|
@ -2,7 +2,7 @@
|
|||
--eventpath .github/event.json
|
||||
--container-architecture linux/amd64
|
||||
--log-prefix-job-id
|
||||
--platform macos-14=-self-hosted
|
||||
--platform macos-15=-self-hosted
|
||||
--pull=false
|
||||
--reuse
|
||||
--secret-file .secrets
|
||||
|
|
2
.github/workflows/pr-checks.yml
vendored
2
.github/workflows/pr-checks.yml
vendored
|
@ -14,7 +14,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-test:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
defaults:
|
||||
run:
|
||||
# Prefixes all `run` commands with the following command to force them to run outside Rosetta.
|
||||
|
|
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
|
@ -12,7 +12,7 @@ on:
|
|||
types: [published]
|
||||
jobs:
|
||||
start:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
outputs:
|
||||
dry_run: ${{ steps.dry_run.outputs.dry_run }}
|
||||
mas_version: ${{ steps.mas_version.outputs.mas_version }}
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
echo "RELEASE_BRANCH=releases/release-${{ github.event.release.tag_name }}" >>"$GITHUB_OUTPUT"
|
||||
|
||||
prepare-release:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
needs: [start]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -98,7 +98,7 @@ jobs:
|
|||
--body "This PR contains the changes from releasing version [${MAS_VERSION}](https://github.com/mas-cli/mas/releases/tag/${MAS_VERSION})."
|
||||
|
||||
pkg-installer:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
needs: [start, prepare-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -132,7 +132,7 @@ jobs:
|
|||
.build/mas.pkg
|
||||
|
||||
homebrew-tap:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
needs: [start, prepare-release]
|
||||
steps:
|
||||
- name: 📺 Checkout mas repo
|
||||
|
@ -223,7 +223,7 @@ jobs:
|
|||
.build/bottles/mas-*.bottle.tar.gz
|
||||
|
||||
homebrew-core:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
needs: [start, prepare-release, homebrew-tap]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -244,4 +244,3 @@ jobs:
|
|||
run: |
|
||||
DRY_RUN=${DRY_RUN} \
|
||||
script/brew_core_update ${MAS_VERSION}
|
||||
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -25,6 +25,7 @@
|
|||
.build/
|
||||
.envrc
|
||||
.fseventsd
|
||||
.idea/
|
||||
.rubygems/
|
||||
.secrets
|
||||
.swiftpm/
|
||||
|
@ -32,6 +33,8 @@
|
|||
Carthage/
|
||||
DerivedData
|
||||
Pods/
|
||||
Sources/mas/Package.swift
|
||||
Sources/MasKit/Package.swift
|
||||
Temporary Items
|
||||
bin/
|
||||
build/
|
||||
|
|
|
@ -1,41 +1,62 @@
|
|||
{
|
||||
"indentation" : {
|
||||
"spaces" : 4
|
||||
"indentConditionalCompilationBlocks": false,
|
||||
"indentation": {
|
||||
"spaces": 4
|
||||
},
|
||||
"lineLength" : 120,
|
||||
"rules" : {
|
||||
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||
"AlwaysUseLowerCamelCase" : true,
|
||||
"AmbiguousTrailingClosureOverload" : true,
|
||||
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||
"DoNotUseSemicolons" : true,
|
||||
"DontRepeatTypeInStaticProperties" : true,
|
||||
"FileScopedDeclarationPrivacy" : true,
|
||||
"FullyIndirectEnum" : true,
|
||||
"GroupNumericLiterals" : true,
|
||||
"IdentifiersMustBeASCII" : true,
|
||||
"NeverForceUnwrap" : false,
|
||||
"NeverUseForceTry" : false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||
"NoAccessLevelOnExtensionDeclaration" : false,
|
||||
"NoBlockComments" : true,
|
||||
"NoCasesWithOnlyFallthrough" : true,
|
||||
"NoEmptyTrailingClosureParentheses" : true,
|
||||
"NoLabelsInCasePatterns" : true,
|
||||
"NoLeadingUnderscores" : false,
|
||||
"NoParensAroundConditions" : true,
|
||||
"NoVoidReturnOnFunctionSignature" : true,
|
||||
"OneCasePerLine" : true,
|
||||
"OneVariableDeclarationPerLine" : true,
|
||||
"OnlyOneTrailingClosureArgument" : true,
|
||||
"OrderedImports" : true,
|
||||
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||
"UseLetInEveryBoundCaseVariable" : true,
|
||||
"UseShorthandTypeNames" : true,
|
||||
"UseSingleLinePropertyGetter" : true,
|
||||
"UseSynthesizedInitializer" : true,
|
||||
"UseTripleSlashForDocumentationComments" : true,
|
||||
"ValidateDocumentationComments" : false
|
||||
"lineBreakAroundMultilineExpressionChainComponents": true,
|
||||
"lineBreakBeforeControlFlowKeywords": false,
|
||||
"lineBreakBeforeEachArgument": true,
|
||||
"lineBreakBeforeEachGenericRequirement": true,
|
||||
"lineBreakBetweenDeclarationAttributes": true,
|
||||
"lineLength": 120,
|
||||
"maximumBlankLines": 1,
|
||||
"multiElementCollectionTrailingCommas": true,
|
||||
"prioritizeKeepingFunctionOutputTogether": true,
|
||||
"respectsExistingLineBreaks": true,
|
||||
"rules": {
|
||||
"AllPublicDeclarationsHaveDocumentation": true,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit": true,
|
||||
"AlwaysUseLowerCamelCase": true,
|
||||
"AmbiguousTrailingClosureOverload": true,
|
||||
"BeginDocumentationCommentWithOneLineSummary": true,
|
||||
"DoNotUseSemicolons": true,
|
||||
"DontRepeatTypeInStaticProperties": true,
|
||||
"FileScopedDeclarationPrivacy": true,
|
||||
"FullyIndirectEnum": true,
|
||||
"GroupNumericLiterals": true,
|
||||
"IdentifiersMustBeASCII": true,
|
||||
"NeverForceUnwrap": true,
|
||||
"NeverUseForceTry": true,
|
||||
"NeverUseImplicitlyUnwrappedOptionals": true,
|
||||
"NoAccessLevelOnExtensionDeclaration": true,
|
||||
"NoAssignmentInExpressions": true,
|
||||
"NoBlockComments": true,
|
||||
"NoCasesWithOnlyFallthrough": true,
|
||||
"NoEmptyTrailingClosureParentheses": true,
|
||||
"NoLabelsInCasePatterns": true,
|
||||
"NoLeadingUnderscores": true,
|
||||
"NoParensAroundConditions": true,
|
||||
"NoPlaygroundLiterals": true,
|
||||
"NoVoidReturnOnFunctionSignature": true,
|
||||
"OmitExplicitReturns": true,
|
||||
"OneCasePerLine": true,
|
||||
"OneVariableDeclarationPerLine": true,
|
||||
"OnlyOneTrailingClosureArgument": true,
|
||||
"OrderedImports": true,
|
||||
"ReplaceForEachWithForLoop": true,
|
||||
"ReturnVoidInsteadOfEmptyTuple": true,
|
||||
"TypeNamesShouldBeCapitalized": true,
|
||||
"UseEarlyExits": true,
|
||||
"UseLetInEveryBoundCaseVariable": true,
|
||||
"UseShorthandTypeNames": true,
|
||||
"UseSingleLinePropertyGetter": true,
|
||||
"UseSynthesizedInitializer": true,
|
||||
"UseTripleSlashForDocumentationComments": true,
|
||||
"UseWhereClausesInForLoops": true,
|
||||
"ValidateDocumentationComments": true
|
||||
},
|
||||
"version" : 1
|
||||
"spacesAroundRangeFormationOperators": false,
|
||||
"spacesBeforeEndOfLineComments": 1,
|
||||
"TrailingComma": false,
|
||||
"version": 1
|
||||
}
|
||||
|
|
24
.swiftformat
24
.swiftformat
|
@ -5,20 +5,34 @@
|
|||
# https://github.com/nicklockwood/SwiftFormat#config-file
|
||||
#
|
||||
|
||||
--exclude docs/
|
||||
|
||||
# Disabled rules
|
||||
--disable blankLinesAroundMark
|
||||
--disable consecutiveSpaces
|
||||
--disable hoistAwait
|
||||
--disable hoistPatternLet
|
||||
--disable hoistTry
|
||||
|
||||
# Enable later
|
||||
--disable indent
|
||||
--disable trailingCommas
|
||||
|
||||
# Enabled rules (disabled by default)
|
||||
--enable trailingClosures
|
||||
#--enable acronyms
|
||||
#--enable blankLinesBetweenImports
|
||||
--enable blockComments
|
||||
--enable docComments
|
||||
--enable isEmpty
|
||||
--enable noExplicitOwnership
|
||||
#--enable organizeDeclarations
|
||||
--enable redundantProperty
|
||||
--enable sortSwitchCases
|
||||
--enable wrapConditionalBodies
|
||||
--enable wrapEnumCases
|
||||
--enable wrapMultilineConditionalAssignment
|
||||
--enable wrapSwitchCases
|
||||
|
||||
# Rule options
|
||||
--commas always
|
||||
--extensionacl on-declarations
|
||||
--hexliteralcase lowercase
|
||||
--importgrouping testable-last
|
||||
--lineaftermarks false
|
||||
--ranges no-space
|
||||
|
|
|
@ -5,10 +5,40 @@
|
|||
# https://github.com/realm/SwiftLint#configuration
|
||||
#
|
||||
---
|
||||
opt_in_rules:
|
||||
- all
|
||||
disabled_rules:
|
||||
- non_optional_string_data_conversion
|
||||
- balanced_xctest_lifecycle
|
||||
- closure_body_length
|
||||
- contrasted_opening_brace
|
||||
- explicit_acl
|
||||
- explicit_enum_raw_value
|
||||
- explicit_top_level_acl
|
||||
- explicit_type_interface
|
||||
- file_header
|
||||
- file_name
|
||||
- final_test_case
|
||||
- force_unwrapping
|
||||
- function_body_length
|
||||
- inert_defer
|
||||
- legacy_objc_type
|
||||
- no_grouping_extension
|
||||
- number_separator
|
||||
- one_declaration_per_file
|
||||
- prefer_nimble
|
||||
- prefixed_toplevel_constant
|
||||
- quick_discouraged_call
|
||||
- quick_discouraged_pending_test
|
||||
- required_deinit
|
||||
- sorted_enum_cases
|
||||
- trailing_comma
|
||||
excluded:
|
||||
- docs
|
||||
opening_brace:
|
||||
allow_multiline_func: true
|
||||
- unused_capture_list
|
||||
- vertical_whitespace_between_cases
|
||||
file_types_order:
|
||||
order: [
|
||||
[main_type],
|
||||
[supporting_type],
|
||||
[extension],
|
||||
[preview_provider],
|
||||
[library_content_provider]
|
||||
]
|
||||
|
|
13
Brewfile
13
Brewfile
|
@ -3,10 +3,13 @@ brew "mise"
|
|||
brew "sd"
|
||||
brew "shellcheck"
|
||||
brew "shfmt"
|
||||
brew "swift-format"
|
||||
brew "swiftformat"
|
||||
brew "trash"
|
||||
brew "yamllint"
|
||||
|
||||
# Already installed on GitHub Actions runner.
|
||||
# brew "swiftlint"
|
||||
|
||||
tap "peripheryapp/periphery"
|
||||
cask "periphery"
|
||||
if OS.mac? && MacOS.version >= :ventura
|
||||
brew "swiftlint"
|
||||
tap "peripheryapp/periphery"
|
||||
cask "periphery"
|
||||
end
|
||||
|
|
|
@ -2,89 +2,79 @@
|
|||
"entries": {
|
||||
"brew": {
|
||||
"markdownlint-cli": {
|
||||
"version": "0.39.0",
|
||||
"version": "0.42.0",
|
||||
"bottle": {
|
||||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e",
|
||||
"sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f",
|
||||
"sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e",
|
||||
"sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f",
|
||||
"sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f"
|
||||
},
|
||||
"arm64_monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f",
|
||||
"sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e",
|
||||
"sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f",
|
||||
"sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5",
|
||||
"sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f",
|
||||
"sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f"
|
||||
},
|
||||
"monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f",
|
||||
"sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5",
|
||||
"sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f",
|
||||
"sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e",
|
||||
"sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mise": {
|
||||
"version": "2024.3.11",
|
||||
"version": "2024.10.11",
|
||||
"bottle": {
|
||||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:3d1ce9664736b5039466ceeb8286f87150a220d76cf62e5c5538ed4c42c01ff0",
|
||||
"sha256": "3d1ce9664736b5039466ceeb8286f87150a220d76cf62e5c5538ed4c42c01ff0"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:2ccc2946ac8d9af9fdde8ce0ef8d9271010eee24f8824086134533480b95b69f",
|
||||
"sha256": "2ccc2946ac8d9af9fdde8ce0ef8d9271010eee24f8824086134533480b95b69f"
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:d28afbc2017aa2d5904da9ca14e1fc9d01d341bfd0adc5168ace961e326b5b1c",
|
||||
"sha256": "d28afbc2017aa2d5904da9ca14e1fc9d01d341bfd0adc5168ace961e326b5b1c"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:8e87fe7f49fa24545c99bcefc1fd1a22edfa6426b8c59e349bd445ced8544dd2",
|
||||
"sha256": "8e87fe7f49fa24545c99bcefc1fd1a22edfa6426b8c59e349bd445ced8544dd2"
|
||||
},
|
||||
"arm64_monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:2c0deb90ea8214e22f4f10fa53ef8d90b66c769f4784807ce99614c6c3a97b43",
|
||||
"sha256": "2c0deb90ea8214e22f4f10fa53ef8d90b66c769f4784807ce99614c6c3a97b43"
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:c4005d63d073861cef944841aa88925b1b929bc150d94bebe65322a4c7a6886f",
|
||||
"sha256": "c4005d63d073861cef944841aa88925b1b929bc150d94bebe65322a4c7a6886f"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:458438873c3a621d77849261f7ef57173670bdf0a64e018592e1682685be28ce",
|
||||
"sha256": "458438873c3a621d77849261f7ef57173670bdf0a64e018592e1682685be28ce"
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:d392eae34949295556f9c3598ce97bf44f5392b996b02ea31956169332a41f1c",
|
||||
"sha256": "d392eae34949295556f9c3598ce97bf44f5392b996b02ea31956169332a41f1c"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:55196d84f5fc7d1b56dd03943dd8744a23e064b78928ebc4de85d8659a71c9ca",
|
||||
"sha256": "55196d84f5fc7d1b56dd03943dd8744a23e064b78928ebc4de85d8659a71c9ca"
|
||||
},
|
||||
"monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:31c0c0ca68d7b2d3d12492c97a7e36547c88cddc8c6e195a3a4fb7b443a74f31",
|
||||
"sha256": "31c0c0ca68d7b2d3d12492c97a7e36547c88cddc8c6e195a3a4fb7b443a74f31"
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:fd1afe999715d971c78f4bfa90b74fae7b7f8688477b07ea89c67ee36ea8463a",
|
||||
"sha256": "fd1afe999715d971c78f4bfa90b74fae7b7f8688477b07ea89c67ee36ea8463a"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:97e003f7841847029e38986685ec809b9a6eab9b66409e6baad4e568860a23aa",
|
||||
"sha256": "97e003f7841847029e38986685ec809b9a6eab9b66409e6baad4e568860a23aa"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:22601cf4faf8764cf29d6cea72ef0e0789b10da581d899a229aace4e0069b2c4",
|
||||
"sha256": "22601cf4faf8764cf29d6cea72ef0e0789b10da581d899a229aace4e0069b2c4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +85,11 @@
|
|||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/sd/blobs/sha256:3cf7ab4495f622a4f245bb1c7c30225ef881dc390ee5edc59a1d3c4381cecca1",
|
||||
"sha256": "3cf7ab4495f622a4f245bb1c7c30225ef881dc390ee5edc59a1d3c4381cecca1"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/sd/blobs/sha256:6bc773a70934364157591cd888e617601a42ed1f615fda8f77364fa45631d08d",
|
||||
|
@ -139,6 +134,11 @@
|
|||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364",
|
||||
"sha256": "5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:ef742b6992cfcdcd7289718ac64b27174e421d29ce3ad9b81e1856349059b117",
|
||||
|
@ -178,117 +178,241 @@
|
|||
}
|
||||
},
|
||||
"shfmt": {
|
||||
"version": "3.8.0",
|
||||
"version": "3.10.0",
|
||||
"bottle": {
|
||||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863",
|
||||
"sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34",
|
||||
"sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863",
|
||||
"sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34",
|
||||
"sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34"
|
||||
},
|
||||
"arm64_monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34",
|
||||
"sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863",
|
||||
"sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62",
|
||||
"sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989",
|
||||
"sha256": "788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62",
|
||||
"sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62"
|
||||
},
|
||||
"monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62",
|
||||
"sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989",
|
||||
"sha256": "788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:772a5dfe3e281fc51f6200313fb62b454314bf4978a8fe70ba2026a4fe5af5c4",
|
||||
"sha256": "772a5dfe3e281fc51f6200313fb62b454314bf4978a8fe70ba2026a4fe5af5c4"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0b15af30edec238edf607c38a95bd45249cdd6f48f30ab33bdd0a9c2ae2da956",
|
||||
"sha256": "0b15af30edec238edf607c38a95bd45249cdd6f48f30ab33bdd0a9c2ae2da956"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"swift-format": {
|
||||
"version": "510.1.0",
|
||||
"bottle": {
|
||||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:4bee0106201ba2a3036576610e61832b97fb65292c194f52fc15d62e1bdb2243",
|
||||
"sha256": "4bee0106201ba2a3036576610e61832b97fb65292c194f52fc15d62e1bdb2243"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:ac50e5269ecc0bffb70a6c5077f97954e2e51c9158a3bfa36b86d89f9d6c5e43",
|
||||
"sha256": "ac50e5269ecc0bffb70a6c5077f97954e2e51c9158a3bfa36b86d89f9d6c5e43"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:7fb047e8f80a72e5d8d7ae50c496d0cf59dd3ab654ce6048e4b7fa7b85afe69a",
|
||||
"sha256": "7fb047e8f80a72e5d8d7ae50c496d0cf59dd3ab654ce6048e4b7fa7b85afe69a"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:a99a19c9fc177a57b2577e3c1b30feb70f13388fc9c4e4ea7968f783058e09a0",
|
||||
"sha256": "a99a19c9fc177a57b2577e3c1b30feb70f13388fc9c4e4ea7968f783058e09a0"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:a652f68cc4bed9c3186b66c8ee68e79b7387d37943aaff0a0c2d4197367b73fe",
|
||||
"sha256": "a652f68cc4bed9c3186b66c8ee68e79b7387d37943aaff0a0c2d4197367b73fe"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:b1949fcb7f1b943fa5b0216bc6f18e12dc369c0538b093786332f851f22b0b03",
|
||||
"sha256": "b1949fcb7f1b943fa5b0216bc6f18e12dc369c0538b093786332f851f22b0b03"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"swiftformat": {
|
||||
"version": "0.53.5",
|
||||
"version": "0.54.6_1",
|
||||
"bottle": {
|
||||
"rebuild": 0,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:03eb08eb7de0e697e574b5d5c94104a88c9548ee880b942f1916536fe7ff897a",
|
||||
"sha256": "03eb08eb7de0e697e574b5d5c94104a88c9548ee880b942f1916536fe7ff897a"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:04e089d4b1ae1217dd6c8133b3c661add56d7c4f4f24ee67becd3cf8f54e6e80",
|
||||
"sha256": "04e089d4b1ae1217dd6c8133b3c661add56d7c4f4f24ee67becd3cf8f54e6e80"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:0ff9c3c154fea61303bd060da1aecebb025a3a33460b24910cf55e6ae366574e",
|
||||
"sha256": "0ff9c3c154fea61303bd060da1aecebb025a3a33460b24910cf55e6ae366574e"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:19a6ce102e7df1cdee150dee619025aa3b2a4980070bee4f8cdd6976c0936d46",
|
||||
"sha256": "19a6ce102e7df1cdee150dee619025aa3b2a4980070bee4f8cdd6976c0936d46"
|
||||
},
|
||||
"arm64_monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:745ba037da0e1fe62f2f22faa45a17655b89d8870bacd9db32597ce1fd779509",
|
||||
"sha256": "745ba037da0e1fe62f2f22faa45a17655b89d8870bacd9db32597ce1fd779509"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:52200577da57cebd27e7d4b6a9ed84f6d3475b7f91e28ec4f5947fc2992cd943",
|
||||
"sha256": "52200577da57cebd27e7d4b6a9ed84f6d3475b7f91e28ec4f5947fc2992cd943"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:6830f0bd5d06dca19d2bcd614e6d0c87e7a3d703d33bce90d0448a83310dddcc",
|
||||
"sha256": "6830f0bd5d06dca19d2bcd614e6d0c87e7a3d703d33bce90d0448a83310dddcc"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:9f79e28a0a5c7172be8bfcf23fca47de08f8bc03a3ddcdfbf52704445b9d8b18",
|
||||
"sha256": "9f79e28a0a5c7172be8bfcf23fca47de08f8bc03a3ddcdfbf52704445b9d8b18"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:dacbfeca6cbe99fc73448f08c0289f135e807bc220ac1dcb61952410f1b43535",
|
||||
"sha256": "dacbfeca6cbe99fc73448f08c0289f135e807bc220ac1dcb61952410f1b43535"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:416528899d45dc25edc2f14c857239a2c922b4be548345423857f140c6b90f0f",
|
||||
"sha256": "416528899d45dc25edc2f14c857239a2c922b4be548345423857f140c6b90f0f"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:86c47e1a74da98ff5646c8d510ea5e6de45e9dc97bc59f151bd2a8848b5bc9f8",
|
||||
"sha256": "86c47e1a74da98ff5646c8d510ea5e6de45e9dc97bc59f151bd2a8848b5bc9f8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trash": {
|
||||
"version": "0.9.2",
|
||||
"bottle": {
|
||||
"rebuild": 1,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f",
|
||||
"sha256": "f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76",
|
||||
"sha256": "90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099",
|
||||
"sha256": "60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099"
|
||||
},
|
||||
"arm64_monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0",
|
||||
"sha256": "541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0"
|
||||
},
|
||||
"arm64_big_sur": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481",
|
||||
"sha256": "11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923",
|
||||
"sha256": "c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e",
|
||||
"sha256": "539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e"
|
||||
},
|
||||
"monterey": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:a5e30f5378aca201ca8bc7a350ebac28b3202366be1b37cf254f77c27761753a",
|
||||
"sha256": "a5e30f5378aca201ca8bc7a350ebac28b3202366be1b37cf254f77c27761753a"
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6",
|
||||
"sha256": "09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6"
|
||||
},
|
||||
"big_sur": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094",
|
||||
"sha256": "403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094"
|
||||
},
|
||||
"catalina": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6",
|
||||
"sha256": "b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6"
|
||||
},
|
||||
"mojave": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551",
|
||||
"sha256": "d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551"
|
||||
},
|
||||
"high_sierra": {
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a",
|
||||
"sha256": "0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yamllint": {
|
||||
"version": "1.35.1",
|
||||
"bottle": {
|
||||
"rebuild": 2,
|
||||
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||
"files": {
|
||||
"arm64_sequoia": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:cb74d6cc51501733531acd25b26fd474557d19374b98eb7de16271ff2c257860",
|
||||
"sha256": "cb74d6cc51501733531acd25b26fd474557d19374b98eb7de16271ff2c257860"
|
||||
},
|
||||
"arm64_sonoma": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:e36b43d6b87028fe2005878cb15c78edec6ddb898e9a86ff7b901fe093cf9c0e",
|
||||
"sha256": "e36b43d6b87028fe2005878cb15c78edec6ddb898e9a86ff7b901fe093cf9c0e"
|
||||
},
|
||||
"arm64_ventura": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:3591f98aaaebba5e9360926f5ca756dbd85c6a46de0554042376ac83548c7fb3",
|
||||
"sha256": "3591f98aaaebba5e9360926f5ca756dbd85c6a46de0554042376ac83548c7fb3"
|
||||
},
|
||||
"sonoma": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:3889369233f5f342b73cc70625748a52d72117603b92f352af00a9ebd27cb1c4",
|
||||
"sha256": "3889369233f5f342b73cc70625748a52d72117603b92f352af00a9ebd27cb1c4"
|
||||
},
|
||||
"ventura": {
|
||||
"cellar": ":any",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:209883378df0edf4a0691fd2dbf6f2e8da7776bd9c0de0fc70fa04dd0fc51c8d",
|
||||
"sha256": "209883378df0edf4a0691fd2dbf6f2e8da7776bd9c0de0fc70fa04dd0fc51c8d"
|
||||
},
|
||||
"x86_64_linux": {
|
||||
"cellar": "/home/linuxbrew/.linuxbrew/Cellar",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:909ae79dbe735c9377355e202d07a58aeff1af1707ba7a3c843cf7c3b10f68a9",
|
||||
"sha256": "909ae79dbe735c9377355e202d07a58aeff1af1707ba7a3c843cf7c3b10f68a9"
|
||||
"cellar": ":any_skip_relocation",
|
||||
"url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:993514320174f1147d538719552131a73d34cf66dc9f82c38f6ed28b16cea287",
|
||||
"sha256": "993514320174f1147d538719552131a73d34cf66dc9f82c38f6ed28b16cea287"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tap": {
|
||||
"peripheryapp/periphery": {
|
||||
"revision": "4f73aefe6e01ba2543b9ee50f7653d866784fd61"
|
||||
}
|
||||
},
|
||||
"cask": {
|
||||
"periphery": {
|
||||
"version": "2.18.0",
|
||||
"options": {
|
||||
"full_name": "periphery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"macos": {
|
||||
"sonoma": {
|
||||
"HOMEBREW_VERSION": "4.2.15-75-g221fde4",
|
||||
"HOMEBREW_PREFIX": "/opt/homebrew",
|
||||
"monterey": {
|
||||
"HOMEBREW_VERSION": "4.4.2-62-g59d56f8",
|
||||
"HOMEBREW_PREFIX": "/usr/local",
|
||||
"Homebrew/homebrew-core": "api",
|
||||
"CLT": "15.3.0.0.1.1708646388",
|
||||
"Xcode": "15.3",
|
||||
"macOS": "14.4.1"
|
||||
"CLT": "14.2.0.0.1.1668646533",
|
||||
"Xcode": "14.2",
|
||||
"macOS": "12.7.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ We love pull requests from everyone. By participating in this project, you agree
|
|||
- [Open an issue](https://github.com/mas-cli/mas/issues/new) to simply ask a question or request a new feature.
|
||||
- Search for similar issues with the
|
||||
[ERROR MESSAGE](https://github.com/mas-cli/mas/issues?utf8=%E2%9C%93&q=is%3Aopen+ERROR+MESSAGE)
|
||||
you are exeriencing.
|
||||
you are experiencing.
|
||||
- If one doesn't exist, [open a new issue](https://github.com/mas-cli/mas/issues/new)
|
||||
- Clearly describe the issue including steps to reproduce when it is a bug.
|
||||
- Include the earliest version of `mas` that you know has the issue.
|
||||
|
|
131
Package.resolved
131
Package.resolved
|
@ -1,106 +1,77 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
"pins" : [
|
||||
{
|
||||
"package": "Commandant",
|
||||
"repositoryURL": "https://github.com/Carthage/Commandant.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "a1671cf728db837cf5ec1980a80d276bbba748f6",
|
||||
"version": "0.18.0"
|
||||
"identity" : "cwlcatchexception",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
|
||||
"state" : {
|
||||
"revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
|
||||
"version" : "2.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CwlCatchException",
|
||||
"repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3b123999de19bf04905bc1dfdb76f817b0f2cc00",
|
||||
"version": "2.1.2"
|
||||
"identity" : "cwlpreconditiontesting",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
||||
"state" : {
|
||||
"revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071",
|
||||
"version" : "2.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CwlPreconditionTesting",
|
||||
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dc9af4781f2afdd1e68e90f80b8603be73ea7abc",
|
||||
"version": "2.2.0"
|
||||
"identity" : "nimble",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Quick/Nimble.git",
|
||||
"state" : {
|
||||
"revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc",
|
||||
"version" : "10.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nimble",
|
||||
"repositoryURL": "https://github.com/Quick/Nimble.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc",
|
||||
"version": "10.0.0"
|
||||
"identity" : "promisekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mxcl/PromiseKit.git",
|
||||
"state" : {
|
||||
"revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
|
||||
"version" : "6.22.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PromiseKit",
|
||||
"repositoryURL": "https://github.com/mxcl/PromiseKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8a98e31a47854d3180882c8068cc4d9381bf382d",
|
||||
"version": "6.22.1"
|
||||
"identity" : "quick",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Quick/Quick.git",
|
||||
"state" : {
|
||||
"revision" : "f9d519828bb03dfc8125467d8f7b93131951124c",
|
||||
"version" : "5.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Quick",
|
||||
"repositoryURL": "https://github.com/Quick/Quick.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f9d519828bb03dfc8125467d8f7b93131951124c",
|
||||
"version": "5.0.1"
|
||||
"identity" : "regex",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sharplet/Regex.git",
|
||||
"state" : {
|
||||
"revision" : "76c2b73d4281d77fc3118391877efd1bf972f515",
|
||||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Regex",
|
||||
"repositoryURL": "https://github.com/sharplet/Regex.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "76c2b73d4281d77fc3118391877efd1bf972f515",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-argument-parser",
|
||||
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41",
|
||||
"version": "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-format",
|
||||
"repositoryURL": "https://github.com/apple/swift-format",
|
||||
"state": {
|
||||
"branch": "release/5.9",
|
||||
"revision": "1323e87eced56bdcfed1bb78af1f16f39274d032",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-syntax",
|
||||
"repositoryURL": "https://github.com/apple/swift-syntax.git",
|
||||
"state": {
|
||||
"branch": "release/5.9",
|
||||
"revision": "9a101b70eee2a9dec04f92d2d47b22ebe57a1aae",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Version",
|
||||
"repositoryURL": "https://github.com/mxcl/Version.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "1fe824b80d89201652e7eca7c9252269a1d85e25",
|
||||
"version": "2.0.1"
|
||||
"identity" : "version",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mxcl/Version.git",
|
||||
"state" : {
|
||||
"revision" : "303a0f916772545e1e8667d3104f83be708a723c",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.6.1
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
@ -13,38 +13,24 @@ let package = Package(
|
|||
.executable(
|
||||
name: "mas",
|
||||
targets: ["mas"]
|
||||
),
|
||||
.library(
|
||||
name: "MasKit",
|
||||
targets: ["MasKit"]
|
||||
),
|
||||
)
|
||||
],
|
||||
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/Quick/Quick.git", from: "5.0.1"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
|
||||
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"),
|
||||
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
|
||||
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "mas",
|
||||
dependencies: ["MasKit"],
|
||||
swiftSettings: [
|
||||
.unsafeFlags([
|
||||
"-I", "Sources/PrivateFrameworks/CommerceKit",
|
||||
"-I", "Sources/PrivateFrameworks/StoreFoundation",
|
||||
])
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "MasKit",
|
||||
dependencies: [
|
||||
"Commandant",
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
"PromiseKit",
|
||||
"Regex",
|
||||
"Version",
|
||||
|
@ -62,8 +48,8 @@ let package = Package(
|
|||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "MasKitTests",
|
||||
dependencies: ["MasKit", "Nimble", "Quick"],
|
||||
name: "masTests",
|
||||
dependencies: ["mas", "Nimble", "Quick"],
|
||||
resources: [.copy("JSON")],
|
||||
swiftSettings: [
|
||||
.unsafeFlags([
|
||||
|
@ -75,30 +61,3 @@ let package = Package(
|
|||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
|
||||
// https://github.com/apple/swift-format#matching-swift-format-to-your-swift-version-swift-57-and-earlier
|
||||
#if compiler(>=5.8)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("release/5.9"))
|
||||
]
|
||||
#elseif compiler(>=5.7)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("release/5.7"))
|
||||
]
|
||||
#elseif compiler(>=5.6)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("release/5.6"))
|
||||
]
|
||||
#elseif compiler(>=5.5)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("swift-5.5-branch"))
|
||||
]
|
||||
#elseif compiler(>=5.4)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("swift-5.4-branch"))
|
||||
]
|
||||
#elseif compiler(>=5.3)
|
||||
package.dependencies += [
|
||||
.package(url: "https://github.com/apple/swift-format", .branch("swift-5.3-branch"))
|
||||
]
|
||||
#endif
|
||||
|
|
312
README.md
312
README.md
|
@ -1,16 +1,20 @@
|
|||
<h1 align="center"><img src="mas-cli.png" alt="mas-cli" width="450" height="auto"></h1>
|
||||
<h1 align="center"><img src="mas-cli.png" alt="mas-cli" width="450" height="138"></h1>
|
||||
|
||||
# mas-cli
|
||||
# mas
|
||||
|
||||
A simple command line interface for the Mac App Store. Designed for scripting and automation.
|
||||
A command-line interface for the Mac App Store. Designed for scripting and automation.
|
||||
|
||||
[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/mas-cli/mas/blob/main/LICENSE)
|
||||
[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org)
|
||||
[![GitHub Release](https://img.shields.io/github/release/mas-cli/mas.svg)](https://github.com/mas-cli/mas/releases)
|
||||
[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](
|
||||
https://github.com/mas-cli/mas/blob/main/LICENSE
|
||||
)
|
||||
[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org)
|
||||
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
|
||||
[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain)
|
||||
[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)](
|
||||
https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain
|
||||
)
|
||||
|
||||
## 📲 Install
|
||||
## 📲 Installation
|
||||
|
||||
### 🍺 Homebrew
|
||||
|
||||
|
@ -20,20 +24,24 @@ A simple command line interface for the Mac App Store. Designed for scripting an
|
|||
brew install mas
|
||||
```
|
||||
|
||||
### MacPorts
|
||||
⚠️ macOS 10.15 (Catalina) or newer is required to install mas from the Homebrew core formula.
|
||||
|
||||
[MacPorts](https://www.macports.org/install.php) works as well:
|
||||
### 🔌 MacPorts
|
||||
|
||||
[MacPorts](https://www.macports.org/install.php) is an alternative way to install:
|
||||
|
||||
```bash
|
||||
sudo port install mas
|
||||
```
|
||||
|
||||
⚠️ Note that macOS 10.15 (Catalina) is required to install mas from MacPorts or the core Homebrew formula.
|
||||
⚠️ macOS 10.15 (Catalina) or newer is required to install mas from MacPorts.
|
||||
|
||||
### ☎️ Older macOS Versions
|
||||
|
||||
#### 🍻 Custom Homebrew tap
|
||||
|
||||
We provide a [custom Homebrew tap](https://github.com/mas-cli/homebrew-tap) with pre-built bottles
|
||||
for all macOS versions since 10.11.
|
||||
for all macOS versions since 10.11 (El Capitan).
|
||||
|
||||
To install mas from our tap:
|
||||
|
||||
|
@ -41,38 +49,48 @@ To install mas from our tap:
|
|||
brew install mas-cli/tap/mas
|
||||
```
|
||||
|
||||
#### Swift 5 Runtime Support
|
||||
#### 🐙 GitHub Releases
|
||||
|
||||
mas requires Swift 5 runtime support. macOS 10.14.4 and later include it, but earlier releases did not.
|
||||
Without it, running `mas` may report an error similar to this:
|
||||
Alternatively, binaries and sources are available from the [GitHub Releases](https://github.com/mas-cli/mas/releases).
|
||||
|
||||
#### 🕊 Swift 5 Runtime Support
|
||||
|
||||
mas requires Swift 5 runtime support. macOS 10.14.4 (Mojave) and newer include it, but earlier releases do not.
|
||||
Without it, running mas might report errors similar to:
|
||||
> dyld: Symbol not found: _$s11SubSequenceSlTl
|
||||
|
||||
To get Swift 5 support, you have a few options:
|
||||
To get Swift 5 support on macOS versions older than 10.14.4 (Mojave), you can:
|
||||
|
||||
- Upgrade to macOS 10.14.4 (Mojave) or newer.
|
||||
- Install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998).
|
||||
- Update to macOS 10.14.4 or later.
|
||||
- Install Xcode 10.2 or later to `/Applications/Xcode.app`.
|
||||
|
||||
### 🐙 GitHub Releases
|
||||
|
||||
Alternatively, binaries are available in the [GitHub Releases](https://github.com/mas-cli/mas/releases).
|
||||
- Install Xcode 10.2 or newer to `/Applications/Xcode.app`.
|
||||
|
||||
## 🤳🏻 Usage
|
||||
|
||||
Each application in the Mac App Store has a product identifier which is also
|
||||
used for mas-cli commands. Using `mas list` will show all installed
|
||||
applications and their product identifiers.
|
||||
### 🪪 App IDs
|
||||
|
||||
```bash
|
||||
$ mas list
|
||||
446107677 Screens
|
||||
407963104 Pixelmator
|
||||
497799835 Xcode
|
||||
```
|
||||
Each application in the Mac App Store has an integer app identifier (app ID).
|
||||
mas commands accept app IDs as arguments and output App IDs to uniquely identify apps.
|
||||
|
||||
It is possible to search for applications by name using `mas search` which
|
||||
will search the Mac App Store and return matching identifiers.
|
||||
Include the `--price` flag to include prices in the result.
|
||||
`mas search` and `mas list` can be used to find the app IDs of relevant apps.
|
||||
|
||||
Alternatively, to find an app's app ID:
|
||||
|
||||
1. Find the app in the Mac App Store
|
||||
2. Select `Share` > `Copy Link`
|
||||
3. Extract the app ID from the URL. e.g., the Mac App Store URL for Xcode,
|
||||
[https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12),
|
||||
has app ID `497799835`
|
||||
|
||||
### 🛍 Info from the Mac App Store
|
||||
|
||||
None of the commands in this section require you to be logged into an Apple ID,
|
||||
neither for your macOS user, nor in the Mac App Store.
|
||||
|
||||
#### `mas search`
|
||||
|
||||
`mas search <search-term>` searches by name for applications available from the Mac App Store.
|
||||
Providing the `--price` flag includes each app's price in the output.
|
||||
|
||||
```bash
|
||||
$ mas search Xcode
|
||||
|
@ -82,151 +100,200 @@ $ mas search Xcode
|
|||
[...]
|
||||
```
|
||||
|
||||
Another way to find the identifier for an app is to
|
||||
#### `mas info`
|
||||
|
||||
1. Find the app in the Mac App Store
|
||||
2. Select `Share` > `Copy Link`
|
||||
3. Grab the identifier from the string, e.g. for Xcode,
|
||||
[https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12)
|
||||
has identifier `497799835`
|
||||
|
||||
To install or update an application simply run `mas install` with an
|
||||
application identifier:
|
||||
`mas info <app-id>` displays more detailed information about an application available from the Mac App Store.
|
||||
|
||||
```bash
|
||||
$ mas install 808809998
|
||||
==> Downloading PaintCode 2
|
||||
==> Installed PaintCode 2
|
||||
$ mas info 497799835
|
||||
Xcode 16.0 [0.0]
|
||||
By: Apple Inc.
|
||||
Released: 2024-09-16
|
||||
Minimum OS: 14.5
|
||||
Size: 2.98 GB
|
||||
From: https://apps.apple.com/us/app/xcode/id497799835?mt=12&uo=4
|
||||
```
|
||||
|
||||
If you want to install the first result that the `search` command returns, use the `lucky` command.
|
||||
### 📚 Info from Your Local App Library
|
||||
|
||||
All the commands in this section require you to be logged into an Apple ID for your macOS user.
|
||||
|
||||
#### `mas list`
|
||||
|
||||
`mas list` displays all the applications on your Mac that were installed from the Mac App Store.
|
||||
|
||||
```bash
|
||||
$ mas lucky twitter
|
||||
==> Downloading Twitter
|
||||
==> Installed Twitter
|
||||
$ mas list
|
||||
497799835 Xcode (15.4)
|
||||
640199958 Developer (10.6.5)
|
||||
899247664 TestFlight (3.5.2)
|
||||
```
|
||||
|
||||
> Please note that this command will not allow you to install (or even purchase) an app for the first time:
|
||||
use the `purchase` command in that case.
|
||||
> ⛔ The `purchase` command is not supported as of macOS 10.15 Catalina. Please see [Known Issues](#%EF%B8%8F-known-issues).
|
||||
#### `mas outdated`
|
||||
|
||||
```bash
|
||||
$ mas purchase 768053424
|
||||
==> Downloading Gapplin
|
||||
==> Installed Gapplin
|
||||
```
|
||||
|
||||
> Please note that you may have to re-authenticate yourself in the App Store to complete the purchase.
|
||||
This is the case if the application is not free or if you configured your account not to remember the
|
||||
credentials for free purchases.
|
||||
|
||||
Use `mas outdated` to list all applications with pending updates.
|
||||
`mas outdated` displays all applications installed from the Mac App Store on your computer that have pending upgrades.
|
||||
|
||||
```bash
|
||||
$ mas outdated
|
||||
497799835 Xcode (7.0)
|
||||
446107677 Screens VNC - Access Your Computer From Anywhere (3.6.7)
|
||||
497799835 Xcode (15.4 -> 16.0)
|
||||
640199958 Developer (10.6.5 -> 10.6.6)
|
||||
```
|
||||
|
||||
> `mas` is only able to install/update applications that are listed in the Mac App Store itself.
|
||||
Use [`softwareupdate(8)`] utility for downloading system updates (e.g. Xcode Command Line Tools)
|
||||
Run [`mas upgrade`](#mas-upgrade) to install pending upgrades.
|
||||
|
||||
To install all pending updates run `mas upgrade`.
|
||||
### ⬇️ Installing Apps
|
||||
|
||||
All the commands in this section require you to be logged into an Apple ID in the Mac App Store.
|
||||
|
||||
> Depending on your Apple ID settings, you might need to re-authenticate yourself in the Mac App Store to perform a
|
||||
> purchase, install, lucky, or upgrade, even if you are already signed in to an Apple ID in the Mac App Store.
|
||||
|
||||
#### `mas purchase`
|
||||
|
||||
`mas purchase <app-id>…` installs free applications that you haven't yet gotten/"purchased" from the Mac App Store.
|
||||
|
||||
> `purchase` is currently a misnomer, because it currently can only "purchase" free
|
||||
> apps. To purchase apps that cost money, please purchase them directly in the Mac App Store.
|
||||
|
||||
```bash
|
||||
$ mas purchase 497799835
|
||||
==> Downloading Xcode
|
||||
==> Installed Xcode
|
||||
```
|
||||
|
||||
#### `mas install`
|
||||
|
||||
`mas install <app-id>…` installs apps that you have already gotten/"purchased" from the Mac App Store.
|
||||
Providing the `--force` flag re-installs the app even if it is already installed on your computer.
|
||||
|
||||
```bash
|
||||
$ mas install 497799835
|
||||
==> Downloading Xcode
|
||||
==> Installed Xcode
|
||||
```
|
||||
|
||||
#### `mas lucky`
|
||||
|
||||
`mas lucky <search-term>` installs the first result that would be returned by `mas search <search-term>`.
|
||||
Like `mas install`, `mas lucky` can only install apps that have previously been gotten/"purchased".
|
||||
|
||||
```bash
|
||||
$ mas lucky Xcode
|
||||
==> Downloading Xcode
|
||||
==> Installed Xcode
|
||||
```
|
||||
|
||||
### 🆕 Upgrading Apps
|
||||
|
||||
All the commands in this section require you to be logged into an Apple ID in the Mac App Store.
|
||||
|
||||
> mas only installs/upgrades applications from the Mac App Store.
|
||||
Use [`softwareupdate(8)`] to install system updates (e.g., Xcode Command Line Tools, Safari, etc.)
|
||||
|
||||
#### `mas upgrade`
|
||||
|
||||
`mas upgrade` upgrades outdated apps installed from the Mac App Store. Without any arguments, it upgrades all such apps.
|
||||
|
||||
```bash
|
||||
$ mas upgrade
|
||||
Upgrading 2 outdated applications:
|
||||
Xcode (7.0), Screens VNC - Access Your Computer From Anywhere (3.6.7)
|
||||
Xcode (15.4) -> (16.0)
|
||||
Developer (10.6.5) -> (10.6.6)
|
||||
==> Downloading Xcode
|
||||
==> Installed Xcode
|
||||
==> Downloading iFlicks
|
||||
==> Installed iFlicks
|
||||
==> Downloading Developer
|
||||
==> Installed Developer
|
||||
```
|
||||
|
||||
Updates can be performed selectively by providing the app identifier(s) to
|
||||
`mas upgrade`
|
||||
Upgrades can be performed selectively by providing app IDs to `mas upgrade`.
|
||||
|
||||
```bash
|
||||
$ mas upgrade 715768417
|
||||
Upgrading 1 outdated application:
|
||||
Xcode (8.0)
|
||||
Xcode (15.4) -> (16.0)
|
||||
==> Downloading Xcode
|
||||
==> Installed Xcode
|
||||
```
|
||||
|
||||
### 🚏📥 Sign-in
|
||||
### Mac App Store Account Management
|
||||
|
||||
> ⛔ The `signin` command is not supported as of macOS 10.13 High Sierra. Please see [Known Issues](#%EF%B8%8F-known-issues).
|
||||
All the commands in this section interact with the Apple ID for which you are signed in in the Mac App Store.
|
||||
These commands do not interact with the Apple ID for which your macOS user is signed in.
|
||||
|
||||
To sign into the Mac App Store for the first time run `mas signin`.
|
||||
#### `mas signin`
|
||||
|
||||
> ⛔ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. On those macOS versions, please
|
||||
> sign in via the Mac App Store instead. Please see [Known Issues](#known-issues).
|
||||
|
||||
On macOS 10.12 (Sierra) or older, `mas signin <apple-id>` signs in to the specified Apple ID in the Mac App Store.
|
||||
|
||||
```bash
|
||||
$ mas signin mas@example.com
|
||||
==> Signing in to Apple ID: mas@example.com
|
||||
Password:
|
||||
```
|
||||
|
||||
If you experience issues signing in this way, you can ask to sign in using a graphical dialog
|
||||
(provided by Mac App Store application):
|
||||
Providing the `--dialog` flag signs in using a graphical dialog provided by Mac App Store.
|
||||
|
||||
```bash
|
||||
$ mas signin --dialog mas@example.com
|
||||
==> Signing in to Apple ID: mas@example.com
|
||||
mas signin --dialog mas@example.com
|
||||
```
|
||||
|
||||
You can also embed your password in the command.
|
||||
|
||||
```bash
|
||||
$ mas signin mas@example.com 'ZdkM4f$gzF;gX3ABXNLf8KcCt.x.np'
|
||||
==> Signing in to Apple ID: mas@example.com
|
||||
mas signin mas@example.com MyPassword
|
||||
```
|
||||
|
||||
Use `mas signout` to sign out from the Mac App Store.
|
||||
#### `mas signout`
|
||||
|
||||
`mas signout` signs out from the current Apple ID in the Mac App Store.
|
||||
|
||||
## 🍺 Homebrew integration
|
||||
|
||||
`mas` is integrated with [homebrew-bundle]. If `mas` is installed, and you run `brew bundle dump`,
|
||||
then your Mac App Store apps will be included in the Brewfile created. See the [homebrew-bundle]
|
||||
mas integrates with [homebrew-bundle]. If mas is installed, when you run `brew bundle dump`,
|
||||
your Mac App Store apps will be included in the created Brewfile. See the [homebrew-bundle]
|
||||
docs for more details.
|
||||
|
||||
## ⚠️ Known Issues
|
||||
<!-- markdownlint-disable-next-line MD033 -->
|
||||
## <a name="known-issues"></a> ⚠️ Known Issues
|
||||
|
||||
Over time, Apple has changed the APIs used by `mas` to manage App Store apps, limiting its capabilities. Please sign in
|
||||
or purchase apps using the App Store app instead. Subsequent redownloads can be performed with `mas install`.
|
||||
### 💥 Changed Apple Private Frameworks
|
||||
|
||||
- ⛔️ The `signin` command is not supported as of macOS 10.13 High Sierra. [#164](https://github.com/mas-cli/mas/issues/164)
|
||||
- ⛔️ The `purchase` command is not supported as of macOS 10.15 Catalina. [#289](https://github.com/mas-cli/mas/issues/289)
|
||||
- ⛔️ The `account` command is not supported as of macOS 12 Monterey. [#417](https://github.com/mas-cli/mas/issues/417)
|
||||
mas uses multiple undocumented Apple private frameworks to implement much of its functionality.
|
||||
Over time, Apple has silently changed these frameworks, breaking some functionality. Known issues include:
|
||||
|
||||
The versions `mas` sees from the app bundles on your Mac don't always match the versions reported by the App Store for
|
||||
the same app bundles. This leads to some confusion when the `outdated` and `upgrade` commands differ in behavior from
|
||||
what is shown as outdated in the App Store app. Further confusing matters, there is often some delay due to CDN
|
||||
propagation and caching between the time a new app version is released to the App Store, and the time it appears
|
||||
available in the App Store app or via the `mas` command. These issues cause symptoms like
|
||||
- ⛔️ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. [#164](
|
||||
https://github.com/mas-cli/mas/issues/164
|
||||
)
|
||||
- ⛔️ The `account` command is not supported on macOS 12 (Monterey) or newer. [#417](
|
||||
https://github.com/mas-cli/mas/issues/417
|
||||
)
|
||||
|
||||
### 👀 Version Consistency
|
||||
|
||||
mas might be using suboptimal app version sources to compare local app versions with Mac App Store app versions.
|
||||
That current sources are frequently consistent with the Mac App Store, but are infrequently inconsistent.
|
||||
This might cause symptoms like [#384](https://github.com/mas-cli/mas/issues/384) and
|
||||
[#387](https://github.com/mas-cli/mas/issues/387). mas will be updated soon to fix any such problems, if possible.
|
||||
|
||||
### ⏳ Eventual Consistency
|
||||
|
||||
The Mac App Store operates on eventual consistency, so the versions seen by various parts of mas or the Mac App Store
|
||||
might be inconsistent for some period of time. This might cause symptoms like
|
||||
[#384](https://github.com/mas-cli/mas/issues/384) and [#387](https://github.com/mas-cli/mas/issues/387).
|
||||
|
||||
Macs with Apple silicon can install and run iOS and iPadOS apps from the App Store. `mas` is not yet aware of these
|
||||
apps, and is not yet able to install or update them. [#321](https://github.com/mas-cli/mas/issues/321)
|
||||
### 📱 iOS and iPadOS Apps
|
||||
|
||||
## 💥 When something doesn't work
|
||||
Macs with Apple Silicon can install and run iOS and iPadOS apps from the Mac App Store. mas is not yet aware of these
|
||||
apps, and is not yet able to install or upgrade them. [#321](https://github.com/mas-cli/mas/issues/321)
|
||||
|
||||
If you see this error, it's probably because you haven't installed the app through the App Store yet.
|
||||
See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233).
|
||||
> This redownload is not available for this Apple ID either because it was bought by a different user or the
|
||||
> item was refunded or cancelled.
|
||||
### 📺 Using `tmux`
|
||||
|
||||
If `mas` doesn't work for you as expected (e.g. you can't update/download apps), run `mas reset` and try again.
|
||||
If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new).
|
||||
All your feedback is much appreciated! ✨
|
||||
|
||||
## 📺 Using `tmux`
|
||||
|
||||
`mas` operates via the same system services as the Mac App Store. These exist as
|
||||
separate processes with communication through XPC. As a result of this, `mas`
|
||||
mas operates via the same system services as the Mac App Store. These exist as
|
||||
separate processes with communication through XPC. As a result of this, mas
|
||||
experiences similar problems as the pasteboard when running inside `tmux`. A
|
||||
[wrapper tool exists](https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard) to
|
||||
fix pasteboard behaviour which also works for `mas`.
|
||||
fix pasteboard behaviour which also works for mas.
|
||||
|
||||
You should consider configuring `tmux` to use the wrapper but if you do not wish
|
||||
to do this it can be used on a one-off basis as follows:
|
||||
|
@ -236,9 +303,20 @@ brew install reattach-to-user-namespace
|
|||
reattach-to-user-namespace mas install
|
||||
```
|
||||
|
||||
## 🚫 When something doesn't work
|
||||
|
||||
If you see the following error, it's probably because you haven't yet "purchased" the app through the Mac App Store.
|
||||
See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233).
|
||||
> This redownload is not available for this Apple ID either because it was bought by a different user or the
|
||||
> item was refunded or cancelled.
|
||||
|
||||
If mas doesn't work for you as expected (e.g. you can't install/upgrade apps), run `mas reset`, then try again.
|
||||
If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new).
|
||||
All feedback is much appreciated! ✨
|
||||
|
||||
## ℹ️ Build from source
|
||||
|
||||
You can build from Xcode by opening the root `mas` directory, or from the Terminal:
|
||||
You can build from Xcode by opening the root mas directory, or from the Terminal:
|
||||
|
||||
```bash
|
||||
script/bootstrap
|
||||
|
@ -251,7 +329,7 @@ Build output can be found in the `.build/` directory within the project.
|
|||
|
||||
The tests in this project are a recent work-in-progress.
|
||||
Since Xcode does not officially support tests for command-line tool targets,
|
||||
all logic is part of the MasKit target with tests in MasKitTests.
|
||||
all logic is part of the mas target with tests in masTests.
|
||||
Tests are written using [Quick].
|
||||
|
||||
```bash
|
||||
|
@ -260,7 +338,7 @@ script/test
|
|||
|
||||
## 📄 License
|
||||
|
||||
mas-cli was created by [@argon](https://github.com/argon).
|
||||
mas was created by [@argon](https://github.com/argon).
|
||||
Code is under the [MIT license](LICENSE).
|
||||
|
||||
[homebrew-bundle]: https://github.com/Homebrew/homebrew-bundle
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
//
|
||||
// Downloader.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Downloads a list of apps, one after the other, printing progress to the console.
|
||||
///
|
||||
/// - Parameter appIDs: The IDs of the apps to be downloaded
|
||||
/// - Parameter purchase: Flag indicating whether the apps needs to be purchased.
|
||||
/// Only works for free apps. Defaults to false.
|
||||
/// - Returns: A promise that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise<Void> {
|
||||
var firstError: Error?
|
||||
return appIDs.reduce(Guarantee<Void>.value(())) { previous, appID in
|
||||
previous.then {
|
||||
downloadWithRetries(appID, purchase: purchase).recover { error in
|
||||
if firstError == nil {
|
||||
firstError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}.done {
|
||||
if let error = firstError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadWithRetries(
|
||||
_ appID: UInt64, purchase: Bool = false, attempts: Int = 3
|
||||
) -> Promise<Void> {
|
||||
download(appID, purchase: purchase).recover { error -> Promise<Void> in
|
||||
guard attempts > 1 else {
|
||||
throw error
|
||||
}
|
||||
|
||||
// If the download failed due to network issues, try again. Otherwise, fail immediately.
|
||||
guard case MASError.downloadFailed(let downloadError) = error,
|
||||
case NSURLErrorDomain = downloadError?.domain
|
||||
else {
|
||||
throw error
|
||||
}
|
||||
|
||||
let attempts = attempts - 1
|
||||
printWarning((downloadError ?? error).localizedDescription)
|
||||
printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
|
||||
return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads an app, printing progress to the console.
|
||||
///
|
||||
/// - Parameter appID: The ID of the app to be downloaded
|
||||
/// - Parameter purchase: Flag indicating whether the app needs to be purchased.
|
||||
/// Only works for free apps. Defaults to false.
|
||||
/// - Returns: A promise the completes when the download is complete.
|
||||
private func download(_ appID: UInt64, purchase: Bool = false) -> Promise<Void> {
|
||||
var storeAccount: ISStoreAccount?
|
||||
if #unavailable(macOS 12) {
|
||||
// Monterey obscured the user's account information, but still allows
|
||||
// redownloads without passing it to SSPurchase.
|
||||
// https://github.com/mas-cli/mas/issues/417
|
||||
guard let account = ISStoreAccount.primaryAccount else {
|
||||
return Promise(error: MASError.notSignedIn)
|
||||
}
|
||||
|
||||
storeAccount = account as? ISStoreAccount
|
||||
guard storeAccount != nil else {
|
||||
fatalError("Unable to cast StoreAccount to ISStoreAccount")
|
||||
}
|
||||
}
|
||||
|
||||
return Promise<SSPurchase> { seal in
|
||||
let purchase = SSPurchase(adamId: appID, account: storeAccount, purchase: purchase)
|
||||
purchase.perform { purchase, _, error, response in
|
||||
if let error {
|
||||
seal.reject(MASError.purchaseFailed(error: error as NSError?))
|
||||
return
|
||||
}
|
||||
|
||||
guard response?.downloads.isEmpty == false, let purchase else {
|
||||
print("No downloads")
|
||||
seal.reject(MASError.noDownloads)
|
||||
return
|
||||
}
|
||||
|
||||
seal.fulfill(purchase)
|
||||
}
|
||||
}.then { purchase -> Promise<Void> in
|
||||
let observer = PurchaseDownloadObserver(purchase: purchase)
|
||||
let download = Promise<Void> { seal in
|
||||
observer.errorHandler = seal.reject
|
||||
observer.completionHandler = seal.fulfill_
|
||||
}
|
||||
|
||||
let downloadQueue = CKDownloadQueue.shared()
|
||||
let observerID = downloadQueue.add(observer)
|
||||
return download.ensure {
|
||||
downloadQueue.remove(observerID)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
//
|
||||
// ISStoreAccount.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 22/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
extension ISStoreAccount: StoreAccount {
|
||||
static var primaryAccount: StoreAccount? {
|
||||
var account: ISStoreAccount?
|
||||
|
||||
if #available(macOS 10.13, *) {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
|
||||
let accountService: ISAccountService = ISServiceProxy.genericShared().accountService
|
||||
accountService.primaryAccount { (storeAccount: ISStoreAccount) in
|
||||
account = storeAccount
|
||||
group.leave()
|
||||
}
|
||||
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
} else {
|
||||
// macOS 10.9-10.12
|
||||
let accountStore = CKAccountStore.shared()
|
||||
account = accountStore.primaryAccount
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> StoreAccount {
|
||||
var storeAccount: ISStoreAccount?
|
||||
var maserror: MASError?
|
||||
|
||||
let accountService: ISAccountService = ISServiceProxy.genericShared().accountService
|
||||
let client = ISStoreClient(storeClientType: 0)
|
||||
accountService.setStoreClient(client)
|
||||
|
||||
let context = ISAuthenticationContext(accountID: 0)
|
||||
context.appleIDOverride = username
|
||||
|
||||
if systemDialog {
|
||||
context.appleIDOverride = username
|
||||
} else {
|
||||
context.demoMode = true
|
||||
context.demoAccountName = username
|
||||
context.demoAccountPassword = password
|
||||
context.demoAutologinMode = true
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
|
||||
// Only works on macOS Sierra and below
|
||||
accountService.signIn(with: context) { success, account, error in
|
||||
if success {
|
||||
storeAccount = account
|
||||
} else {
|
||||
maserror = .signInFailed(error: error as NSError?)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
||||
if systemDialog {
|
||||
group.wait()
|
||||
} else {
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
}
|
||||
|
||||
if let account = storeAccount {
|
||||
return account
|
||||
}
|
||||
|
||||
throw maserror ?? MASError.signInFailed(error: nil)
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// SSPurchase.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 25/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
typealias SSPurchaseCompletion =
|
||||
(_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void
|
||||
|
||||
extension SSPurchase {
|
||||
convenience init(adamId: UInt64, account: ISStoreAccount?, purchase: Bool = false) {
|
||||
self.init()
|
||||
|
||||
var parameters: [String: Any] = [
|
||||
"productType": "C",
|
||||
"price": 0,
|
||||
"salableAdamId": adamId,
|
||||
"pg": "default",
|
||||
"appExtVrsId": 0,
|
||||
]
|
||||
|
||||
if purchase {
|
||||
parameters["macappinstalledconfirmed"] = 1
|
||||
parameters["pricingParameters"] = "STDQ"
|
||||
|
||||
} else {
|
||||
// is redownload, use existing functionality
|
||||
parameters["pricingParameters"] = "STDRDL"
|
||||
}
|
||||
|
||||
buyParameters =
|
||||
parameters.map { key, value in
|
||||
"\(key)=\(value)"
|
||||
}
|
||||
.joined(separator: "&")
|
||||
|
||||
itemIdentifier = adamId
|
||||
|
||||
if let account {
|
||||
accountIdentifier = account.dsID
|
||||
appleID = account.identifier
|
||||
}
|
||||
|
||||
// Not sure if this is needed, but lets use it here.
|
||||
if purchase {
|
||||
isRedownload = false
|
||||
}
|
||||
|
||||
let downloadMetadata = SSDownloadMetadata()
|
||||
downloadMetadata.kind = "software"
|
||||
downloadMetadata.itemIdentifier = adamId
|
||||
|
||||
self.downloadMetadata = downloadMetadata
|
||||
}
|
||||
|
||||
func perform(_ completion: @escaping SSPurchaseCompletion) {
|
||||
CKPurchaseController.shared().perform(self, withOptions: 0, completionHandler: completion)
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// Account.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import StoreFoundation
|
||||
|
||||
public struct AccountCommand: CommandProtocol {
|
||||
public typealias Options = NoOptions<MASError>
|
||||
public let verb = "account"
|
||||
public let function = "Prints the primary account Apple ID"
|
||||
|
||||
public init() {}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
if let account = ISStoreAccount.primaryAccount {
|
||||
print(String(describing: account.identifier))
|
||||
} else {
|
||||
printError("Not signed in")
|
||||
return .failure(.notSignedIn)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
//
|
||||
// Home.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
|
||||
/// 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()
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
self.openCommand = openCommand
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
//
|
||||
// Info.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Denis Lebedev on 21/10/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// Install.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
|
||||
/// Public initializer.
|
||||
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: 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))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// List.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
|
||||
/// 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"
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
let output = AppListFormatter.format(products: products)
|
||||
print(output)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
//
|
||||
// Lucky.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Pablo Varela on 05/11/17.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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), options: options)
|
||||
}
|
||||
|
||||
/// Installs an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appId: App identifier
|
||||
/// - options: command opetions.
|
||||
/// - 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")
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
//
|
||||
// Open.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
systemOpen = openCommand
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
//
|
||||
// Outdated.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
/// 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))
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// Purchase.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Jakob Rieck on 24/10/2017.
|
||||
// Copyright (c) 2017 Jakob Rieck. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
|
||||
/// Public initializer.
|
||||
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: Options) -> Result<Void, MASError> {
|
||||
if #available(macOS 10.15, *) {
|
||||
// Purchases are no longer possible as of Catalina.
|
||||
// https://github.com/mas-cli/mas/issues/289
|
||||
return .failure(.notSupported)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
//
|
||||
// Reset.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 14/09/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
public init() {}
|
||||
|
||||
/// 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
|
||||
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// Search.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Michael Schneider on 4/14/16.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
|
||||
/// 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"
|
||||
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let output = SearchResultFormatter.format(results: results, includePrice: options.price)
|
||||
print(output)
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// SignIn.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 14/02/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import StoreFoundation
|
||||
|
||||
public struct SignInCommand: CommandProtocol {
|
||||
public typealias Options = SignInOptions
|
||||
|
||||
public let verb = "signin"
|
||||
public let function = "Sign in to the Mac App Store"
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<Void, MASError> {
|
||||
if #available(macOS 10.13, *) {
|
||||
// Signing in is no longer possible as of High Sierra.
|
||||
// https://github.com/mas-cli/mas/issues/164
|
||||
return .failure(.notSupported)
|
||||
}
|
||||
|
||||
guard ISStoreAccount.primaryAccount == nil else {
|
||||
return .failure(.alreadySignedIn)
|
||||
}
|
||||
|
||||
do {
|
||||
printInfo("Signing in to Apple ID: \(options.username)")
|
||||
|
||||
let password: String = {
|
||||
if options.password.isEmpty, !options.dialog {
|
||||
return String(validatingUTF8: getpass("Password: "))!
|
||||
}
|
||||
return options.password
|
||||
}()
|
||||
|
||||
_ = try ISStoreAccount.signIn(username: options.username, password: password, systemDialog: options.dialog)
|
||||
} catch let error as NSError {
|
||||
return .failure(.signInFailed(error: error))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// SignOut.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 14/02/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
if #available(macOS 10.13, *) {
|
||||
let accountService: ISAccountService = ISServiceProxy.genericShared().accountService
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
//
|
||||
// Uninstall.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
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 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 {
|
||||
printInfo("\(product.appName) \(product.bundlePath)")
|
||||
printInfo("(not removed, dry run)")
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
//
|
||||
// Upgrade.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 30/12/2015.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
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"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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
|
||||
// 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")
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
//
|
||||
// Vendor.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
|
||||
/// 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()
|
||||
)
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
init(
|
||||
storeSearch: StoreSearch = MasStoreSearch(),
|
||||
openCommand: ExternalCommand = OpenSystemCommand()
|
||||
) {
|
||||
self.storeSearch = storeSearch
|
||||
self.openCommand = openCommand
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// Version.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 20/09/2015.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
|
||||
/// 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"
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_: Options) -> Result<Void, MASError> {
|
||||
print(Package.version)
|
||||
return .success(())
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
//
|
||||
// AppLibrary.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Utility for managing installed apps.
|
||||
protocol AppLibrary {
|
||||
/// Entire set of installed apps.
|
||||
var installedApps: [SoftwareProduct] { get }
|
||||
|
||||
/// Finds an app by ID.
|
||||
///
|
||||
/// - Parameter forId: MAS ID for app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
func installedApp(forId: UInt64) -> SoftwareProduct?
|
||||
|
||||
/// Uninstalls an app.
|
||||
///
|
||||
/// - Parameter app: App to be removed.
|
||||
/// - Throws: Error if there is a problem.
|
||||
func uninstallApp(app: SoftwareProduct) throws
|
||||
}
|
||||
|
||||
/// Common logic
|
||||
extension AppLibrary {
|
||||
/// Finds an app by name.
|
||||
///
|
||||
/// - Parameter id: MAS ID for app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
func installedApp(forId identifier: UInt64) -> SoftwareProduct? {
|
||||
let appId = NSNumber(value: identifier)
|
||||
return installedApps.first { $0.itemIdentifier == appId }
|
||||
}
|
||||
|
||||
/// Finds an app by name.
|
||||
///
|
||||
/// - Parameter appName: Full title of an app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
func installedApp(named appName: String) -> SoftwareProduct? {
|
||||
installedApps.first { $0.appName == appName }
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// MasAppLibrary.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
|
||||
/// Utility for managing installed apps.
|
||||
class MasAppLibrary: AppLibrary {
|
||||
/// CommerceKit's singleton manager of installed software.
|
||||
private let softwareMap: SoftwareMap
|
||||
|
||||
/// Array of installed software products.
|
||||
lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts().filter { product in
|
||||
product.bundlePath.starts(with: "/Applications/")
|
||||
}
|
||||
|
||||
/// Internal initializer for providing a mock software map.
|
||||
/// - Parameter softwareMap: SoftwareMap to use
|
||||
init(softwareMap: SoftwareMap = CKSoftwareMap.shared()) {
|
||||
self.softwareMap = softwareMap
|
||||
}
|
||||
|
||||
/// Finds an app using a bundle identifier.
|
||||
///
|
||||
/// - Parameter bundleId: Bundle identifier of app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
func installedApp(forBundleId bundleId: String) -> SoftwareProduct? {
|
||||
softwareMap.product(for: bundleId)
|
||||
}
|
||||
|
||||
/// Uninstalls an app.
|
||||
///
|
||||
/// - Parameter app: App to be removed.
|
||||
/// - Throws: Error if there is a problem.
|
||||
func uninstallApp(app: SoftwareProduct) throws {
|
||||
if !userIsRoot() {
|
||||
printWarning("Apps installed from the Mac App Store require root permission to remove.")
|
||||
}
|
||||
|
||||
let appUrl = URL(fileURLWithPath: app.bundlePath)
|
||||
do {
|
||||
// Move item to trash
|
||||
var trashUrl: NSURL?
|
||||
try FileManager().trashItem(at: appUrl, resultingItemURL: &trashUrl)
|
||||
if let path = trashUrl?.path {
|
||||
printInfo("App moved to trash: \(path)")
|
||||
}
|
||||
} catch {
|
||||
printError("Unable to move app to trash.")
|
||||
throw MASError.uninstallFailed
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects whether the current user is root.
|
||||
///
|
||||
/// - Returns: true if the current user is root; false otherwise
|
||||
private func userIsRoot() -> Bool {
|
||||
NSUserName() == "root"
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// StoreSearch.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
/// Protocol for searching the MAS catalog.
|
||||
protocol StoreSearch {
|
||||
func lookup(app appId: Int) -> Promise<SearchResult?>
|
||||
func search(for appName: String) -> Promise<[SearchResult]>
|
||||
}
|
||||
|
||||
enum Entity: String {
|
||||
case macSoftware
|
||||
case iPadSoftware
|
||||
case iPhoneSoftware = "software"
|
||||
}
|
||||
|
||||
// MARK: - Common methods
|
||||
extension StoreSearch {
|
||||
/// Builds the search URL for an app.
|
||||
///
|
||||
/// - Parameter appName: MAS app identifier.
|
||||
/// - Returns: URL for the search service or nil if appName can't be encoded.
|
||||
func searchURL(for appName: String, inCountry country: String?, ofEntity entity: Entity = .macSoftware) -> URL? {
|
||||
guard var components = URLComponents(string: "https://itunes.apple.com/search") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "media", value: "software"),
|
||||
URLQueryItem(name: "entity", value: entity.rawValue),
|
||||
URLQueryItem(name: "term", value: appName),
|
||||
]
|
||||
|
||||
if let country {
|
||||
components.queryItems!.append(URLQueryItem(name: "country", value: country))
|
||||
}
|
||||
|
||||
return components.url
|
||||
}
|
||||
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: URL for the lookup service or nil if appId can't be encoded.
|
||||
func lookupURL(forApp appId: Int, inCountry country: String?) -> URL? {
|
||||
guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "id", value: "\(appId)"),
|
||||
URLQueryItem(name: "entity", value: "desktopSoftware"),
|
||||
]
|
||||
|
||||
if let country {
|
||||
components.queryItems!.append(URLQueryItem(name: "country", value: country))
|
||||
}
|
||||
|
||||
return components.url
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
//
|
||||
// Utilities.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 14/09/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A collection of output formatting helpers
|
||||
|
||||
/// Terminal Control Sequence Indicator
|
||||
let csi = "\u{001B}["
|
||||
|
||||
#if DEBUG
|
||||
|
||||
var printObserver: ((String) -> Void)?
|
||||
|
||||
// Override global print for testability.
|
||||
// See MasKitTests/OutputListener.swift.
|
||||
func print(
|
||||
_ items: Any...,
|
||||
separator: String = " ",
|
||||
terminator: String = "\n"
|
||||
) {
|
||||
if let observer = printObserver {
|
||||
let output =
|
||||
items
|
||||
.map { "\($0)" }
|
||||
.joined(separator: separator)
|
||||
.appending(terminator)
|
||||
observer(output)
|
||||
}
|
||||
|
||||
var prefix = ""
|
||||
for item in items {
|
||||
Swift.print(prefix, terminator: "")
|
||||
Swift.print(item, terminator: "")
|
||||
prefix = separator
|
||||
}
|
||||
|
||||
Swift.print(terminator, terminator: "")
|
||||
}
|
||||
|
||||
func print(
|
||||
_ items: Any...,
|
||||
separator: String = " ",
|
||||
terminator: String = "\n",
|
||||
to output: inout some TextOutputStream
|
||||
) {
|
||||
if let observer = printObserver {
|
||||
let output =
|
||||
items
|
||||
.map { "\($0)" }
|
||||
.joined(separator: separator)
|
||||
.appending(terminator)
|
||||
observer(output)
|
||||
}
|
||||
|
||||
var prefix = ""
|
||||
for item in items {
|
||||
Swift.print(prefix, terminator: "", to: &output)
|
||||
Swift.print(item, terminator: "", to: &output)
|
||||
prefix = separator
|
||||
}
|
||||
|
||||
Swift.print(terminator, terminator: "", to: &output)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private var standardError = FileHandle.standardError
|
||||
|
||||
extension FileHandle: TextOutputStream {
|
||||
public func write(_ string: String) {
|
||||
guard let data = string.data(using: .utf8) else { return }
|
||||
write(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a message to stdout prefixed with a blue arrow.
|
||||
func printInfo(_ message: String) {
|
||||
guard isatty(fileno(stdout)) != 0 else {
|
||||
print("==> \(message)")
|
||||
return
|
||||
}
|
||||
|
||||
// Blue bold arrow, Bold text
|
||||
print("\(csi)1;34m==>\(csi)0m \(csi)1m\(message)\(csi)0m")
|
||||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Warning:" underlined in yellow.
|
||||
public func printWarning(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Warning: \(message)", to: &standardError)
|
||||
return
|
||||
}
|
||||
|
||||
// Yellow, underlined "Warning:" prefix
|
||||
print("\(csi)4;33mWarning:\(csi)0m \(message)", to: &standardError)
|
||||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Error:" underlined in red.
|
||||
public func printError(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Error: \(message)", to: &standardError)
|
||||
return
|
||||
}
|
||||
|
||||
// Red, underlined "Error:" prefix
|
||||
print("\(csi)4;31mError:\(csi)0m \(message)", to: &standardError)
|
||||
}
|
||||
|
||||
/// Flushes stdout.
|
||||
func clearLine() {
|
||||
guard isatty(fileno(stdout)) != 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
print("\(csi)2K\(csi)0G", terminator: "")
|
||||
fflush(stdout)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// MasKit.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Chris Araman on 4/22/21.
|
||||
// Copyright © 2021 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import PromiseKit
|
||||
|
||||
public enum MasKit {
|
||||
public static func initialize() {
|
||||
PromiseKit.conf.Q.map = .global()
|
||||
PromiseKit.conf.Q.return = .global()
|
||||
PromiseKit.conf.logHandler = { event in
|
||||
switch event {
|
||||
case .waitOnMainThread:
|
||||
// Ignored. This is a console app that waits on the main thread for
|
||||
// promises to be processed on the global DispatchQueue.
|
||||
break
|
||||
default:
|
||||
// Other events indicate a programming error.
|
||||
fatalError("PromiseKit event: \(event)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
// Generated by: script/version
|
||||
enum Package {
|
||||
static let version = "1.8.6"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// CKSoftwareMap+SoftwareMap.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// CKSoftwareProduct+SoftwareProduct.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
62
Sources/mas/AppStore/Downloader.swift
Normal file
62
Sources/mas/AppStore/Downloader.swift
Normal file
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Downloader.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Downloads a list of apps, one after the other, printing progress to the console.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appIDs: The IDs of the apps to be downloaded
|
||||
/// - purchase: Flag indicating whether the apps needs to be purchased.
|
||||
/// Only works for free apps. Defaults to false.
|
||||
/// - Returns: A promise that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
|
||||
var firstError: Error?
|
||||
return
|
||||
appIDs
|
||||
.reduce(Guarantee.value(())) { previous, appID in
|
||||
previous.then {
|
||||
downloadWithRetries(appID, purchase: purchase)
|
||||
.recover { error in
|
||||
if firstError == nil {
|
||||
firstError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.done {
|
||||
if let error = firstError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> {
|
||||
SSPurchase().perform(appID: appID, purchase: purchase)
|
||||
.recover { error in
|
||||
guard attempts > 1 else {
|
||||
throw error
|
||||
}
|
||||
|
||||
// If the download failed due to network issues, try again. Otherwise, fail immediately.
|
||||
guard
|
||||
case MASError.downloadFailed(let downloadError) = error,
|
||||
case NSURLErrorDomain = downloadError?.domain
|
||||
else {
|
||||
throw error
|
||||
}
|
||||
|
||||
let attempts = attempts - 1
|
||||
printWarning((downloadError ?? error).localizedDescription)
|
||||
printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
|
||||
return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
|
||||
}
|
||||
}
|
94
Sources/mas/AppStore/ISStoreAccount.swift
Normal file
94
Sources/mas/AppStore/ISStoreAccount.swift
Normal file
|
@ -0,0 +1,94 @@
|
|||
//
|
||||
// ISStoreAccount.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 22/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
private let timeout = 30.0
|
||||
|
||||
extension ISStoreAccount: StoreAccount {
|
||||
static var primaryAccount: Promise<ISStoreAccount> {
|
||||
if #available(macOS 10.13, *) {
|
||||
return race(
|
||||
Promise { seal in
|
||||
ISServiceProxy.genericShared().accountService
|
||||
.primaryAccount { storeAccount in
|
||||
seal.fulfill(storeAccount)
|
||||
}
|
||||
},
|
||||
after(seconds: timeout)
|
||||
.then {
|
||||
Promise(error: MASError.notSignedIn)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return .value(CKAccountStore.shared().primaryAccount)
|
||||
}
|
||||
|
||||
static func signIn(appleID: String, password: String, systemDialog: Bool) -> Promise<ISStoreAccount> {
|
||||
// swift-format-ignore: UseEarlyExits
|
||||
if #available(macOS 10.13, *) {
|
||||
// Signing in is no longer possible as of High Sierra.
|
||||
// https://github.com/mas-cli/mas/issues/164
|
||||
return Promise(error: MASError.notSupported)
|
||||
// swiftlint:disable:next superfluous_else
|
||||
} else {
|
||||
return
|
||||
primaryAccount
|
||||
.then { account -> Promise<ISStoreAccount> in
|
||||
if account.isSignedIn {
|
||||
return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier))
|
||||
}
|
||||
|
||||
let password =
|
||||
password.isEmpty && !systemDialog
|
||||
? String(validatingUTF8: getpass("Password: ")) ?? ""
|
||||
: password
|
||||
|
||||
guard !password.isEmpty || systemDialog else {
|
||||
return Promise(error: MASError.noPasswordProvided)
|
||||
}
|
||||
|
||||
let context = ISAuthenticationContext(accountID: 0)
|
||||
context.appleIDOverride = appleID
|
||||
|
||||
let signInPromise =
|
||||
Promise<ISStoreAccount> { seal in
|
||||
let accountService = ISServiceProxy.genericShared().accountService
|
||||
accountService.setStoreClient(ISStoreClient(storeClientType: 0))
|
||||
accountService.signIn(with: context) { success, storeAccount, error in
|
||||
if success, let storeAccount {
|
||||
seal.fulfill(storeAccount)
|
||||
} else {
|
||||
seal.reject(MASError.signInFailed(error: error as NSError?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if systemDialog {
|
||||
return signInPromise
|
||||
}
|
||||
|
||||
context.demoMode = true
|
||||
context.demoAccountName = appleID
|
||||
context.demoAccountPassword = password
|
||||
context.demoAutologinMode = true
|
||||
|
||||
return race(
|
||||
signInPromise,
|
||||
after(seconds: timeout)
|
||||
.then {
|
||||
Promise(error: MASError.signInFailed(error: nil))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// PurchaseDownloadObserver.swift
|
||||
// mas-cli
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
|
@ -9,7 +9,8 @@
|
|||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
@objc class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
|
||||
@objc
|
||||
class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
|
||||
let purchase: SSPurchase
|
||||
var completionHandler: (() -> Void)?
|
||||
var errorHandler: ((MASError) -> Void)?
|
||||
|
@ -19,7 +20,8 @@ import StoreFoundation
|
|||
}
|
||||
|
||||
func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
guard
|
||||
download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status
|
||||
else {
|
||||
return
|
||||
|
@ -41,7 +43,8 @@ import StoreFoundation
|
|||
}
|
||||
|
||||
func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
guard
|
||||
download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status
|
||||
else {
|
||||
return
|
||||
|
@ -64,6 +67,7 @@ struct ProgressState {
|
|||
let phase: String
|
||||
|
||||
var percentage: String {
|
||||
// swiftlint:disable:next no_magic_numbers
|
||||
String(format: "%.1f%%", floor(percentComplete * 1000) / 10)
|
||||
}
|
||||
}
|
94
Sources/mas/AppStore/SSPurchase.swift
Normal file
94
Sources/mas/AppStore/SSPurchase.swift
Normal file
|
@ -0,0 +1,94 @@
|
|||
//
|
||||
// SSPurchase.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 25/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
extension SSPurchase {
|
||||
func perform(appID: AppID, purchase: Bool) -> Promise<Void> {
|
||||
var parameters: [String: Any] = [
|
||||
"productType": "C",
|
||||
"price": 0,
|
||||
"salableAdamId": appID,
|
||||
"pg": "default",
|
||||
"appExtVrsId": 0,
|
||||
]
|
||||
|
||||
if purchase {
|
||||
parameters["macappinstalledconfirmed"] = 1
|
||||
parameters["pricingParameters"] = "STDQ"
|
||||
} else {
|
||||
parameters["pricingParameters"] = "STDRDL"
|
||||
}
|
||||
|
||||
buyParameters =
|
||||
parameters.map { key, value in
|
||||
"\(key)=\(value)"
|
||||
}
|
||||
.joined(separator: "&")
|
||||
|
||||
itemIdentifier = appID
|
||||
|
||||
// Not sure if this is needed…
|
||||
if purchase {
|
||||
isRedownload = false
|
||||
}
|
||||
|
||||
downloadMetadata = SSDownloadMetadata()
|
||||
downloadMetadata.kind = "software"
|
||||
downloadMetadata.itemIdentifier = appID
|
||||
|
||||
// Monterey obscures the user's App Store account, but allows
|
||||
// redownloads without passing any account IDs to SSPurchase.
|
||||
// https://github.com/mas-cli/mas/issues/417
|
||||
if #available(macOS 12, *) {
|
||||
return perform()
|
||||
}
|
||||
|
||||
return
|
||||
ISStoreAccount.primaryAccount
|
||||
.then { storeAccount in
|
||||
self.accountIdentifier = storeAccount.dsID
|
||||
self.appleID = storeAccount.identifier
|
||||
return self.perform()
|
||||
}
|
||||
}
|
||||
|
||||
private func perform() -> Promise<Void> {
|
||||
Promise<SSPurchase> { seal in
|
||||
CKPurchaseController.shared()
|
||||
.perform(self, withOptions: 0) { purchase, _, error, response in
|
||||
if let error {
|
||||
seal.reject(MASError.purchaseFailed(error: error as NSError?))
|
||||
return
|
||||
}
|
||||
|
||||
guard response?.downloads.isEmpty == false, let purchase else {
|
||||
seal.reject(MASError.noDownloads)
|
||||
return
|
||||
}
|
||||
|
||||
seal.fulfill(purchase)
|
||||
}
|
||||
}
|
||||
.then { purchase in
|
||||
let observer = PurchaseDownloadObserver(purchase: purchase)
|
||||
let downloadQueue = CKDownloadQueue.shared()
|
||||
let observerID = downloadQueue.add(observer)
|
||||
|
||||
return Promise<Void> { seal in
|
||||
observer.errorHandler = seal.reject
|
||||
observer.completionHandler = seal.fulfill_
|
||||
}
|
||||
.ensure {
|
||||
downloadQueue.remove(observerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
//
|
||||
// StoreAccount.swift
|
||||
// mas-cli
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 4/3/18.
|
||||
// Copyright © 2018 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// periphery:ignore - save for future use in testing
|
||||
protocol StoreAccount {
|
||||
var identifier: String { get set }
|
||||
var dsID: NSNumber { get set }
|
||||
}
|
33
Sources/mas/Commands/Account.swift
Normal file
33
Sources/mas/Commands/Account.swift
Normal file
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Account.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
extension MAS {
|
||||
struct Account: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Prints the primary account Apple ID"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
if #available(macOS 12, *) {
|
||||
// Account information is no longer available as of Monterey.
|
||||
// https://github.com/mas-cli/mas/issues/417
|
||||
throw MASError.notSupported
|
||||
}
|
||||
|
||||
do {
|
||||
print(try ISStoreAccount.primaryAccount.wait().identifier)
|
||||
} catch {
|
||||
throw error as? MASError ?? MASError.failed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
Sources/mas/Commands/Home.swift
Normal file
49
Sources/mas/Commands/Home.swift
Normal file
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// Home.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
extension MAS {
|
||||
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Home: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens MAS Preview app page in a browser"
|
||||
)
|
||||
|
||||
@Argument(help: "ID of app to show on MAS Preview")
|
||||
var appID: AppID
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: result.trackViewUrl)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
Sources/mas/Commands/Info.swift
Normal file
40
Sources/mas/Commands/Info.swift
Normal file
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Info.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Denis Lebedev on 21/10/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
extension MAS {
|
||||
/// Displays app details. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Info: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Display app information from the Mac App Store"
|
||||
)
|
||||
|
||||
@Argument(help: "ID of app to show info")
|
||||
var appID: AppID
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
Sources/mas/Commands/Install.swift
Normal file
47
Sources/mas/Commands/Install.swift
Normal file
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Install.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@Flag(help: "force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "app ID(s) to install")
|
||||
var appIDs: [AppID]
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
||||
printWarning("\(appName) is already installed")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIDs).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Sources/mas/Commands/List.swift
Normal file
32
Sources/mas/Commands/List.swift
Normal file
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// List.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
let products = appLibrary.installedApps
|
||||
if products.isEmpty {
|
||||
printError("No installed apps found")
|
||||
} else {
|
||||
print(AppListFormatter.format(products: products))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
Sources/mas/Commands/Lucky.swift
Normal file
72
Sources/mas/Commands/Lucky.swift
Normal file
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// Lucky.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Pablo Varela on 05/11/17.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@Flag(help: "force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "the app name to install")
|
||||
var searchTerm: String
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
var appID: AppID?
|
||||
|
||||
do {
|
||||
let results = try searcher.search(for: searchTerm).wait()
|
||||
guard let result = results.first else {
|
||||
printError("No results found")
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
appID = result.trackId
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
|
||||
guard let appID else {
|
||||
fatalError("app ID returned from Apple is null")
|
||||
}
|
||||
|
||||
try install(appID: appID, appLibrary: appLibrary)
|
||||
}
|
||||
|
||||
/// Installs an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appID: App identifier
|
||||
/// - appLibrary: Library of installed apps
|
||||
/// - Throws: Any error that occurs while attempting to install the app.
|
||||
private func install(appID: AppID, appLibrary: AppLibrary) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
||||
printWarning("\(appName) is already installed")
|
||||
} else {
|
||||
do {
|
||||
try downloadAll([appID]).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
Sources/mas/Commands/Open.swift
Normal file
66
Sources/mas/Commands/Open.swift
Normal file
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// Open.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
private let masScheme = "macappstore"
|
||||
|
||||
extension MAS {
|
||||
/// Opens app page in MAS app. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Open: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens app page in 'App Store.app'"
|
||||
)
|
||||
|
||||
@Argument(help: "the app ID")
|
||||
var appID: AppID?
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let appID else {
|
||||
// If no app ID is given, just open the MAS GUI app
|
||||
try openCommand.run(arguments: masScheme + "://")
|
||||
return
|
||||
}
|
||||
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
guard var url = URLComponents(string: result.trackViewUrl) else {
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
url.scheme = masScheme
|
||||
|
||||
guard let urlString = url.string else {
|
||||
printError("Unable to construct URL")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
do {
|
||||
try openCommand.run(arguments: urlString)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
if openCommand.failed {
|
||||
printError("Open failed: (\(openCommand.process.terminationReason)) \(openCommand.stderr)")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
Sources/mas/Commands/Outdated.swift
Normal file
63
Sources/mas/Commands/Outdated.swift
Normal file
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// Outdated.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@Flag(help: "Show warnings about apps")
|
||||
var verbose = false
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
_ = try when(
|
||||
fulfilled:
|
||||
appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.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))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.wait()
|
||||
}
|
||||
}
|
||||
}
|
44
Sources/mas/Commands/Purchase.swift
Normal file
44
Sources/mas/Commands/Purchase.swift
Normal file
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Purchase.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Jakob Rieck on 24/10/2017.
|
||||
// Copyright (c) 2017 Jakob Rieck. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension MAS {
|
||||
struct Purchase: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Purchase and download free apps from the Mac App Store"
|
||||
)
|
||||
|
||||
@Argument(help: "app ID(s) to install")
|
||||
var appIDs: [AppID]
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
|
||||
printWarning("\(appName) has already been purchased.")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIDs, purchase: true).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
Sources/mas/Commands/Reset.swift
Normal file
79
Sources/mas/Commands/Reset.swift
Normal file
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// Reset.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 14/09/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@Flag(help: "Enable debug mode")
|
||||
var debug = false
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
// 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()
|
||||
printError("killall failed:\n\(String(data: output, encoding: .utf8) ?? "Error info not available")")
|
||||
}
|
||||
|
||||
// Wipe Download Directory
|
||||
if let directory = CKDownloadDirectory(nil) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: directory)
|
||||
} catch {
|
||||
if debug {
|
||||
printError("removeItemAtPath:\"\(directory)\" failed, \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Sources/mas/Commands/Search.swift
Normal file
43
Sources/mas/Commands/Search.swift
Normal file
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// Search.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Michael Schneider on 4/14/16.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
extension MAS {
|
||||
/// Search the Mac App Store using the iTunes Search API.
|
||||
///
|
||||
/// See - https://performance-partners.apple.com/search-api
|
||||
struct Search: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Search for apps from the Mac App Store"
|
||||
)
|
||||
|
||||
@Flag(help: "Show price of found apps")
|
||||
var price = false
|
||||
@Argument(help: "the app name to search")
|
||||
var searchTerm: String
|
||||
|
||||
func run() throws {
|
||||
try run(searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
let results = try searcher.search(for: searchTerm).wait()
|
||||
if results.isEmpty {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
let output = SearchResultFormatter.format(results: results, includePrice: price)
|
||||
print(output)
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
Sources/mas/Commands/SignIn.swift
Normal file
35
Sources/mas/Commands/SignIn.swift
Normal file
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// SignIn.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 14/02/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
extension MAS {
|
||||
struct SignIn: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signin",
|
||||
abstract: "Sign in to the Mac App Store"
|
||||
)
|
||||
|
||||
@Flag(help: "Complete login with graphical dialog")
|
||||
var dialog = false
|
||||
@Argument(help: "Apple ID")
|
||||
var appleID: String
|
||||
@Argument(help: "Password")
|
||||
var password: String = ""
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
do {
|
||||
_ = try ISStoreAccount.signIn(appleID: appleID, password: password, systemDialog: dialog).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? MASError.signInFailed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
Sources/mas/Commands/SignOut.swift
Normal file
30
Sources/mas/Commands/SignOut.swift
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// SignOut.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 14/02/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension MAS {
|
||||
struct SignOut: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signout",
|
||||
abstract: "Sign out of the Mac App Store"
|
||||
)
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
if #available(macOS 10.13, *) {
|
||||
ISServiceProxy.genericShared().accountService.signOut()
|
||||
} else {
|
||||
// Using CKAccountStore to sign out does nothing on High Sierra
|
||||
// https://github.com/mas-cli/mas/issues/129
|
||||
CKAccountStore.shared().signOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
Sources/mas/Commands/Uninstall.swift
Normal file
65
Sources/mas/Commands/Uninstall.swift
Normal file
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Uninstall.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
/// Flag indicating that removal shouldn't be performed.
|
||||
@Flag(help: "dry run")
|
||||
var dryRun = false
|
||||
@Argument(help: "ID of app to uninstall")
|
||||
var appID: AppID
|
||||
|
||||
/// Runs the uninstall command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
guard NSUserName() == "root" else {
|
||||
throw MASError.macOSUserMustBeRoot
|
||||
}
|
||||
|
||||
guard let username = getSudoUsername() else {
|
||||
throw MASError.runtimeError("Could not determine the original username")
|
||||
}
|
||||
|
||||
guard
|
||||
let uid = getSudoUID(),
|
||||
seteuid(uid) == 0
|
||||
else {
|
||||
throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'")
|
||||
}
|
||||
|
||||
let installedApps = appLibrary.installedApps(withAppID: appID)
|
||||
guard !installedApps.isEmpty else {
|
||||
throw MASError.notInstalled(appID: appID)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
for installedApp in installedApps {
|
||||
printInfo("'\(installedApp.appName)' '\(installedApp.bundlePath)'")
|
||||
}
|
||||
printInfo("(not removed, dry run)")
|
||||
} else {
|
||||
guard seteuid(0) == 0 else {
|
||||
throw MASError.runtimeError("Failed to revert effective user from '\(username)' back to 'root'")
|
||||
}
|
||||
|
||||
try appLibrary.uninstallApps(atPaths: installedApps.map(\.bundlePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
Sources/mas/Commands/Upgrade.swift
Normal file
88
Sources/mas/Commands/Upgrade.swift
Normal file
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// Upgrade.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 30/12/2015.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@Argument(help: "app(s) to upgrade")
|
||||
var appIDs: [String] = []
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
|
||||
do {
|
||||
apps = try findOutdatedApps(appLibrary: appLibrary, searcher: searcher)
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
|
||||
guard !apps.isEmpty else {
|
||||
printWarning("Nothing found to upgrade")
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
do {
|
||||
try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
}
|
||||
|
||||
private func findOutdatedApps(
|
||||
appLibrary: AppLibrary,
|
||||
searcher: AppStoreSearcher
|
||||
) throws -> [(SoftwareProduct, SearchResult)] {
|
||||
let apps =
|
||||
appIDs.isEmpty
|
||||
? appLibrary.installedApps
|
||||
: appIDs.flatMap { appID in
|
||||
if let appID = AppID(appID) {
|
||||
// argument is an AppID, lookup apps by id using argument
|
||||
return appLibrary.installedApps(withAppID: appID)
|
||||
}
|
||||
|
||||
// argument is not an AppID, lookup apps by name using argument
|
||||
return appLibrary.installedApps(named: appID)
|
||||
}
|
||||
|
||||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
}
|
||||
|
||||
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||
}
|
||||
}
|
||||
}
|
53
Sources/mas/Commands/Vendor.swift
Normal file
53
Sources/mas/Commands/Vendor.swift
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// Vendor.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2016 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
extension MAS {
|
||||
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Vendor: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens vendor's app page in a browser"
|
||||
)
|
||||
|
||||
@Argument(help: "the app ID to show the vendor's website")
|
||||
var appID: AppID
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
guard let vendorWebsite = result.sellerUrl else {
|
||||
throw MASError.noVendorWebsite
|
||||
}
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: vendorWebsite)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
if openCommand.failed {
|
||||
let reason = openCommand.process.terminationReason
|
||||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
Sources/mas/Commands/Version.swift
Normal file
23
Sources/mas/Commands/Version.swift
Normal file
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Version.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 20/09/2015.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
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.
|
||||
func run() throws {
|
||||
print(Package.version)
|
||||
}
|
||||
}
|
||||
}
|
41
Sources/mas/Controllers/AppLibrary.swift
Normal file
41
Sources/mas/Controllers/AppLibrary.swift
Normal file
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// AppLibrary.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Utility for managing installed apps.
|
||||
protocol AppLibrary {
|
||||
/// Entire set of installed apps.
|
||||
var installedApps: [SoftwareProduct] { get }
|
||||
|
||||
/// Uninstalls all apps located at any of the elements of `appPaths`.
|
||||
///
|
||||
/// - Parameter appPaths: Paths to apps to be uninstalled.
|
||||
/// - Throws: Error if any problem occurs.
|
||||
func uninstallApps(atPaths appPaths: [String]) throws
|
||||
}
|
||||
|
||||
/// Common logic
|
||||
extension AppLibrary {
|
||||
/// Finds all installed instances of apps whose app ID is `appID`.
|
||||
///
|
||||
/// - Parameter appID: app ID for app(s).
|
||||
/// - Returns: [SoftwareProduct] of matching apps.
|
||||
func installedApps(withAppID appID: AppID) -> [SoftwareProduct] {
|
||||
let appID = NSNumber(value: appID)
|
||||
return installedApps.filter { $0.itemIdentifier == appID }
|
||||
}
|
||||
|
||||
/// Finds all installed instances of apps whose name is `appName`.
|
||||
///
|
||||
/// - Parameter appName: Full name of app(s).
|
||||
/// - Returns: [SoftwareProduct] of matching apps.
|
||||
func installedApps(named appName: String) -> [SoftwareProduct] {
|
||||
installedApps.filter { $0.appName == appName }
|
||||
}
|
||||
}
|
96
Sources/mas/Controllers/AppStoreSearcher.swift
Normal file
96
Sources/mas/Controllers/AppStoreSearcher.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// AppStoreSearcher.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
/// Protocol for searching the MAS catalog.
|
||||
protocol AppStoreSearcher {
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?>
|
||||
func search(for searchTerm: String) -> Promise<[SearchResult]>
|
||||
}
|
||||
|
||||
enum Entity: String {
|
||||
case desktopSoftware
|
||||
case macSoftware
|
||||
case iPadSoftware
|
||||
case iPhoneSoftware = "software"
|
||||
}
|
||||
|
||||
private enum URLAction {
|
||||
case lookup
|
||||
case search
|
||||
|
||||
var queryItemName: String {
|
||||
switch self {
|
||||
case .lookup:
|
||||
return "id"
|
||||
case .search:
|
||||
return "term"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Common methods
|
||||
extension AppStoreSearcher {
|
||||
/// Builds the search URL for an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - searchTerm: term for which to search in MAS.
|
||||
/// - country: 2-letter ISO region code of the MAS in which to search.
|
||||
/// - entity: OS platform of apps for which to search.
|
||||
/// - Returns: URL for the search service or nil if searchTerm can't be encoded.
|
||||
func searchURL(
|
||||
for searchTerm: String,
|
||||
inCountry country: String?,
|
||||
ofEntity entity: Entity = .desktopSoftware
|
||||
) -> URL? {
|
||||
url(.search, searchTerm, inCountry: country, ofEntity: entity)
|
||||
}
|
||||
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appID: MAS app identifier.
|
||||
/// - country: 2-letter ISO region code of the MAS in which to search.
|
||||
/// - entity: OS platform of apps for which to search.
|
||||
/// - Returns: URL for the lookup service or nil if appID can't be encoded.
|
||||
func lookupURL(
|
||||
forAppID appID: AppID,
|
||||
inCountry country: String?,
|
||||
ofEntity entity: Entity = .desktopSoftware
|
||||
) -> URL? {
|
||||
url(.lookup, String(appID), inCountry: country, ofEntity: entity)
|
||||
}
|
||||
|
||||
private func url(
|
||||
_ action: URLAction,
|
||||
_ queryItemValue: String,
|
||||
inCountry country: String?,
|
||||
ofEntity entity: Entity = .desktopSoftware
|
||||
) -> URL? {
|
||||
guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var queryItems = [
|
||||
URLQueryItem(name: "media", value: "software"),
|
||||
URLQueryItem(name: "entity", value: entity.rawValue),
|
||||
]
|
||||
|
||||
if let country {
|
||||
queryItems.append(URLQueryItem(name: "country", value: country))
|
||||
}
|
||||
|
||||
queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue))
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
return components.url
|
||||
}
|
||||
}
|
733
Sources/mas/Controllers/Finder.swift
Normal file
733
Sources/mas/Controllers/Finder.swift
Normal file
|
@ -0,0 +1,733 @@
|
|||
// swift-format-ignore-file
|
||||
// swiftlint:disable:next blanket_disable_command
|
||||
// swiftlint:disable attributes discouraged_none_name file_length file_types_order identifier_name
|
||||
// swiftlint:disable:next blanket_disable_command
|
||||
// swiftlint:disable implicitly_unwrapped_optional line_length missing_docs
|
||||
import AppKit
|
||||
import ScriptingBridge
|
||||
|
||||
// MARK: FinderEdfm
|
||||
@objc
|
||||
public enum FinderEdfm: AEKeyword {
|
||||
case macOSFormat = 0x6466_6866 // 'dfhf'
|
||||
case macOSExtendedFormat = 0x6466_682b // 'dfh+'
|
||||
case ufsFormat = 0x6466_7566 // 'dfuf'
|
||||
case nfsFormat = 0x6466_6e66 // 'dfnf'
|
||||
case audioFormat = 0x6466_6175 // 'dfau'
|
||||
case proDOSFormat = 0x6466_7072 // 'dfpr'
|
||||
case msdosFormat = 0x6466_6d73 // 'dfms'
|
||||
case ntfsFormat = 0x6466_6e74 // 'dfnt'
|
||||
case iso9660Format = 0x6466_3936 // 'df96'
|
||||
case highSierraFormat = 0x6466_6873 // 'dfhs'
|
||||
case quickTakeFormat = 0x6466_7174 // 'dfqt'
|
||||
case applePhotoFormat = 0x6466_7068 // 'dfph'
|
||||
case appleShareFormat = 0x6466_6173 // 'dfas'
|
||||
case udfFormat = 0x6466_7564 // 'dfud'
|
||||
case webDAVFormat = 0x6466_7764 // 'dfwd'
|
||||
case ftpFormat = 0x6466_6674 // 'dfft'
|
||||
case packetWrittenUDFFormat = 0x6466_7075 // 'dfpu'
|
||||
case xsanFormat = 0x6466_6163 // 'dfac'
|
||||
case apfsFormat = 0x6466_6170 // 'dfap'
|
||||
case exFATFormat = 0x6466_7866 // 'dfxf'
|
||||
case smbFormat = 0x6466_736d // 'dfsm'
|
||||
case unknownFormat = 0x6466_3f3f // 'df??'
|
||||
}
|
||||
|
||||
// MARK: FinderIpnl
|
||||
@objc
|
||||
public enum FinderIpnl: AEKeyword {
|
||||
case generalInformationPanel = 0x6770_6e6c // 'gpnl'
|
||||
case sharingPanel = 0x7370_6e6c // 'spnl'
|
||||
case memoryPanel = 0x6d70_6e6c // 'mpnl'
|
||||
case previewPanel = 0x7670_6e6c // 'vpnl'
|
||||
case applicationPanel = 0x6170_6e6c // 'apnl'
|
||||
case languagesPanel = 0x706b_6c67 // 'pklg'
|
||||
case pluginsPanel = 0x706b_7067 // 'pkpg'
|
||||
case nameExtensionPanel = 0x6e70_6e6c // 'npnl'
|
||||
case commentsPanel = 0x6370_6e6c // 'cpnl'
|
||||
case contentIndexPanel = 0x6369_6e6c // 'cinl'
|
||||
case burningPanel = 0x6270_6e6c // 'bpnl'
|
||||
case moreInfoPanel = 0x6d69_6e6c // 'minl'
|
||||
case simpleHeaderPanel = 0x7368_6e6c // 'shnl'
|
||||
}
|
||||
|
||||
// MARK: FinderPple
|
||||
@objc
|
||||
public enum FinderPple: AEKeyword {
|
||||
case generalPreferencesPanel = 0x7067_6e70 // 'pgnp'
|
||||
case labelPreferencesPanel = 0x706c_6270 // 'plbp'
|
||||
case sidebarPreferencesPanel = 0x7073_6964 // 'psid'
|
||||
case advancedPreferencesPanel = 0x7061_6476 // 'padv'
|
||||
}
|
||||
|
||||
// MARK: FinderPriv
|
||||
@objc
|
||||
public enum FinderPriv: AEKeyword {
|
||||
case readOnly = 0x7265_6164 // 'read'
|
||||
case readWrite = 0x7264_7772 // 'rdwr'
|
||||
case writeOnly = 0x7772_6974 // 'writ'
|
||||
case none = 0x6e6f_6e65 // 'none'
|
||||
}
|
||||
|
||||
// MARK: FinderEcvw
|
||||
@objc
|
||||
public enum FinderEcvw: AEKeyword {
|
||||
case iconView = 0x6963_6e76 // 'icnv'
|
||||
case listView = 0x6c73_7677 // 'lsvw'
|
||||
case columnView = 0x636c_7677 // 'clvw'
|
||||
case groupView = 0x6772_7677 // 'grvw'
|
||||
case flowView = 0x666c_7677 // 'flvw'
|
||||
}
|
||||
|
||||
// MARK: FinderEarr
|
||||
@objc
|
||||
public enum FinderEarr: AEKeyword {
|
||||
case notArranged = 0x6e61_7272 // 'narr'
|
||||
case snapToGrid = 0x6772_6461 // 'grda'
|
||||
case arrangedByName = 0x6e61_6d61 // 'nama'
|
||||
case arrangedByModificationDate = 0x6d64_7461 // 'mdta'
|
||||
case arrangedByCreationDate = 0x6364_7461 // 'cdta'
|
||||
case arrangedBySize = 0x7369_7a61 // 'siza'
|
||||
case arrangedByKind = 0x6b69_6e61 // 'kina'
|
||||
case arrangedByLabel = 0x6c61_6261 // 'laba'
|
||||
}
|
||||
|
||||
// MARK: FinderEpos
|
||||
@objc
|
||||
public enum FinderEpos: AEKeyword {
|
||||
case right = 0x6c72_6774 // 'lrgt'
|
||||
case bottom = 0x6c62_6f74 // 'lbot'
|
||||
}
|
||||
|
||||
// MARK: FinderSodr
|
||||
@objc
|
||||
public enum FinderSodr: AEKeyword {
|
||||
case normal = 0x736e_726d // 'snrm'
|
||||
case reversed = 0x7372_7673 // 'srvs'
|
||||
}
|
||||
|
||||
// MARK: FinderElsv
|
||||
@objc
|
||||
public enum FinderElsv: AEKeyword {
|
||||
case nameColumn = 0x656c_736e // 'elsn'
|
||||
case modificationDateColumn = 0x656c_736d // 'elsm'
|
||||
case creationDateColumn = 0x656c_7363 // 'elsc'
|
||||
case sizeColumn = 0x656c_7373 // 'elss'
|
||||
case kindColumn = 0x656c_736b // 'elsk'
|
||||
case labelColumn = 0x656c_736c // 'elsl'
|
||||
case versionColumn = 0x656c_7376 // 'elsv'
|
||||
case commentColumn = 0x656c_7343 // 'elsC'
|
||||
}
|
||||
|
||||
// MARK: FinderLvic
|
||||
@objc
|
||||
public enum FinderLvic: AEKeyword {
|
||||
case smallIcon = 0x736d_6963 // 'smic'
|
||||
case largeIcon = 0x6c67_6963 // 'lgic'
|
||||
}
|
||||
|
||||
@objc
|
||||
public protocol SBObjectProtocol: NSObjectProtocol {
|
||||
func get() -> Any!
|
||||
}
|
||||
|
||||
@objc
|
||||
public protocol SBApplicationProtocol: SBObjectProtocol {
|
||||
var delegate: SBApplicationDelegate! { get set }
|
||||
var isRunning: Bool { get }
|
||||
|
||||
func activate()
|
||||
}
|
||||
|
||||
// MARK: FinderGenericMethods
|
||||
@objc
|
||||
public protocol FinderGenericMethods {
|
||||
@objc optional func openUsing(_ using_: SBObject!, withProperties: [AnyHashable: Any]!) // Open the specified object(s)
|
||||
@objc optional func printWithProperties(_ withProperties: [AnyHashable: Any]!) // Print the specified object(s)
|
||||
@objc optional func activate() // Activate the specified window (or the Finder)
|
||||
@objc optional func close() // Close an object
|
||||
@objc optional func dataSizeAs(_ as: NSNumber!) -> Int // Return the size in bytes of an object
|
||||
@objc optional func delete() -> SBObject // Move an item from its container to the trash
|
||||
@objc optional func duplicateTo(_ to: SBObject!, replacing: Bool, routingSuppressed: Bool, exactCopy: Bool) -> SBObject // Duplicate one or more object(s)
|
||||
@objc optional func exists() -> Bool // Verify if an object exists
|
||||
@objc optional func moveTo(_ to: SBObject!, replacing: Bool, positionedAt: [Any]!, routingSuppressed: Bool) -> SBObject // Move object(s) to a new location
|
||||
@objc optional func select() // Select the specified object(s)
|
||||
@objc optional func sortBy(_ by: Selector) -> SBObject // Return the specified object(s) in a sorted list
|
||||
@objc optional func cleanUpBy(_ by: Selector) // Arrange items in window nicely (only applies to open windows in icon view that are not kept arranged)
|
||||
@objc optional func eject() // Eject the specified disk(s)
|
||||
@objc optional func emptySecurity(_ security: Bool) // Empty the trash
|
||||
@objc optional func erase() // (NOT AVAILABLE) Erase the specified disk(s)
|
||||
@objc optional func reveal() // Bring the specified object(s) into view
|
||||
@objc optional func updateNecessity(_ necessity: Bool, registeringApplications: Bool) // Update the display of the specified object(s) to match their on-disk representation
|
||||
}
|
||||
|
||||
// MARK: FinderApplication
|
||||
@objc
|
||||
public protocol FinderApplication: SBApplicationProtocol {
|
||||
@objc optional var clipboard: SBObject { get } // (NOT AVAILABLE YET) the Finder’s clipboard window (copy)
|
||||
@objc optional var name: String { get } // the Finder’s name (copy)
|
||||
@objc optional var visible: Bool { get } // Is the Finder’s layer visible?
|
||||
@objc optional var frontmost: Bool { get } // Is the Finder the frontmost process?
|
||||
@objc optional var selection: SBObject { get } // the selection in the frontmost Finder window (copy)
|
||||
@objc optional var insertionLocation: SBObject { get } // the container in which a new folder would appear if “New Folder” was selected (copy)
|
||||
@objc optional var productVersion: String { get } // the version of the System software running on this computer (copy)
|
||||
@objc optional var version: String { get } // the version of the Finder (copy)
|
||||
@objc optional var startupDisk: FinderDisk { get } // the startup disk (copy)
|
||||
@objc optional var desktop: FinderDesktopObject { get } // the desktop (copy)
|
||||
@objc optional var trash: FinderTrashObject { get } // the trash (copy)
|
||||
@objc optional var home: FinderFolder { get } // the home directory (copy)
|
||||
@objc optional var computerContainer: FinderComputerObject { get } // the computer location (as in Go > Computer) (copy)
|
||||
@objc optional var FinderPreferences: FinderPreferences { get } // Various preferences that apply to the Finder as a whole (copy)
|
||||
|
||||
@objc optional var desktopPicture: FinderFile { get } // the desktop picture of the main monitor
|
||||
|
||||
@objc optional func setDesktopPicture(_ desktopPicture: FinderFile!) // the desktop picture of the main monitor
|
||||
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func disks() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
@objc optional func windows() -> SBElementArray
|
||||
@objc optional func FinderWindows() -> SBElementArray
|
||||
@objc optional func clippingWindows() -> SBElementArray
|
||||
|
||||
@objc optional func quit() // Quit the Finder
|
||||
@objc optional func activate() // Activate the specified window (or the Finder)
|
||||
@objc optional func copy() // (NOT AVAILABLE YET) Copy the selected items to the clipboard (the Finder must be the front application)
|
||||
@objc optional func eject() // Eject the specified disk(s)
|
||||
@objc optional func emptySecurity(_ security: Bool) // Empty the trash
|
||||
@objc optional func restart() // Restart the computer
|
||||
@objc optional func shutDown() // Shut Down the computer
|
||||
@objc optional func sleep() // Put the computer to sleep
|
||||
@objc optional func setVisible(_ visible: Bool) // Is the Finder’s layer visible?
|
||||
@objc optional func setFrontmost(_ frontmost: Bool) // Is the Finder the frontmost process?
|
||||
@objc optional func setSelection(_ selection: SBObject!) // the selection in the frontmost Finder window
|
||||
}
|
||||
|
||||
extension SBApplication: FinderApplication {}
|
||||
|
||||
// MARK: FinderItem
|
||||
@objc
|
||||
public protocol FinderItem: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var name: String { get } // the name of the item (copy)
|
||||
@objc optional var displayedName: String { get } // the user-visible name of the item (copy)
|
||||
@objc optional var nameExtension: String { get } // the name extension of the item (such as “txt”) (copy)
|
||||
@objc optional var extensionHidden: Bool { get } // Is the item's extension hidden from the user?
|
||||
@objc optional var index: Int { get } // the index in the front-to-back ordering within its container
|
||||
@objc optional var container: SBObject { get } // the container of the item (copy)
|
||||
@objc optional var disk: SBObject { get } // the disk on which the item is stored (copy)
|
||||
@objc optional var position: NSPoint { get } // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons)
|
||||
@objc optional var desktopPosition: NSPoint { get } // the position of the item on the desktop
|
||||
@objc optional var bounds: NSRect { get } // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons)
|
||||
@objc optional var labelIndex: Int { get } // the label of the item
|
||||
@objc optional var locked: Bool { get } // Is the file locked?
|
||||
@objc optional var kind: String { get } // the kind of the item (copy)
|
||||
@objc optional var objectDescription: String { get } // a description of the item (copy)
|
||||
@objc optional var comment: String { get } // the comment of the item, displayed in the “Get Info” window (copy)
|
||||
@objc optional var size: Int64 { get } // the logical size of the item
|
||||
@objc optional var physicalSize: Int64 { get } // the actual space used by the item on disk
|
||||
@objc optional var creationDate: Date { get } // the date on which the item was created (copy)
|
||||
@objc optional var modificationDate: Date { get } // the date on which the item was last modified (copy)
|
||||
@objc optional var icon: FinderIconFamily { get } // the icon bitmap of the item (copy)
|
||||
@objc optional var URL: String { get } // the URL of the item (copy)
|
||||
@objc optional var owner: String { get } // the user that owns the container (copy)
|
||||
@objc optional var group: String { get } // the user or group that has special access to the container (copy)
|
||||
@objc optional var ownerPrivileges: FinderPriv { get }
|
||||
@objc optional var groupPrivileges: FinderPriv { get }
|
||||
@objc optional var everyonesPrivileges: FinderPriv { get }
|
||||
@objc optional var informationWindow: SBObject { get } // the information window for the item (copy)
|
||||
@objc optional var properties: [AnyHashable: Any] { get } // every property of an item (copy)
|
||||
|
||||
@objc optional func setName(_ name: String!) // the name of the item
|
||||
@objc optional func setNameExtension(_ nameExtension: String!) // the name extension of the item (such as “txt”)
|
||||
@objc optional func setExtensionHidden(_ extensionHidden: Bool) // Is the item's extension hidden from the user?
|
||||
@objc optional func setPosition(_ position: NSPoint) // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons)
|
||||
@objc optional func setDesktopPosition(_ desktopPosition: NSPoint) // the position of the item on the desktop
|
||||
@objc optional func setBounds(_ bounds: NSRect) // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons)
|
||||
@objc optional func setLabelIndex(_ labelIndex: Int) // the label of the item
|
||||
@objc optional func setLocked(_ locked: Bool) // Is the file locked?
|
||||
@objc optional func setComment(_ comment: String!) // the comment of the item, displayed in the “Get Info” window
|
||||
@objc optional func setModificationDate(_ modificationDate: Date!) // the date on which the item was last modified
|
||||
@objc optional func setIcon(_ icon: FinderIconFamily!) // the icon bitmap of the item
|
||||
@objc optional func setOwner(_ owner: String!) // the user that owns the container
|
||||
@objc optional func setGroup(_ group: String!) // the user or group that has special access to the container
|
||||
@objc optional func setOwnerPrivileges(_ ownerPrivileges: FinderPriv)
|
||||
@objc optional func setGroupPrivileges(_ groupPrivileges: FinderPriv)
|
||||
@objc optional func setEveryonesPrivileges(_ everyonesPrivileges: FinderPriv)
|
||||
@objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of an item
|
||||
}
|
||||
|
||||
extension SBObject: FinderItem {}
|
||||
|
||||
// MARK: FinderContainer
|
||||
@objc
|
||||
public protocol FinderContainer: FinderItem {
|
||||
@objc optional var entireContents: SBObject { get } // the entire contents of the container, including the contents of its children (copy)
|
||||
@objc optional var expandable: Bool { get } // (NOT AVAILABLE YET) Is the container capable of being expanded as an outline?
|
||||
@objc optional var expanded: Bool { get } // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists)
|
||||
@objc optional var completelyExpanded: Bool { get } // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists)
|
||||
@objc optional var containerWindow: SBObject { get } // the container window for this folder (copy)
|
||||
|
||||
@objc optional func setExpanded(_ expanded: Bool) // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists)
|
||||
@objc optional func setCompletelyExpanded(_ completelyExpanded: Bool) // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists)
|
||||
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderContainer {}
|
||||
|
||||
// MARK: FinderComputerObject
|
||||
@objc
|
||||
public protocol FinderComputerObject: FinderItem {}
|
||||
|
||||
extension SBObject: FinderComputerObject {}
|
||||
|
||||
// MARK: FinderDisk
|
||||
@objc
|
||||
public protocol FinderDisk: FinderContainer {
|
||||
@objc optional var capacity: Int64 { get } // the total number of bytes (free or used) on the disk
|
||||
@objc optional var freeSpace: Int64 { get } // the number of free bytes left on the disk
|
||||
@objc optional var ejectable: Bool { get } // Can the media be ejected (floppies, CDs, and so on)?
|
||||
@objc optional var localVolume: Bool { get } // Is the media a local volume (as opposed to a file server)?
|
||||
@objc optional var startup: Bool { get } // Is this disk the boot disk?
|
||||
@objc optional var format: FinderEdfm { get } // the filesystem format of this disk
|
||||
@objc optional var journalingEnabled: Bool { get } // Does this disk do file system journaling?
|
||||
@objc optional var ignorePrivileges: Bool { get } // Ignore permissions on this disk?
|
||||
|
||||
@objc optional func setIgnorePrivileges(_ ignorePrivileges: Bool) // Ignore permissions on this disk?
|
||||
|
||||
@objc optional func id() -> Int // the unique id for this disk (unchanged while disk remains connected and Finder remains running)
|
||||
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderDisk {}
|
||||
|
||||
// MARK: FinderFolder
|
||||
@objc
|
||||
public protocol FinderFolder: FinderContainer {
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderFolder {}
|
||||
|
||||
// MARK: FinderDesktopObject
|
||||
@objc
|
||||
public protocol FinderDesktopObject: FinderContainer {
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func disks() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderDesktopObject {}
|
||||
|
||||
// MARK: FinderTrashObject
|
||||
@objc
|
||||
public protocol FinderTrashObject: FinderContainer {
|
||||
@objc optional var warnsBeforeEmptying: Bool { get } // Display a dialog when emptying the trash?
|
||||
|
||||
@objc optional func setWarnsBeforeEmptying(_ warnsBeforeEmptying: Bool) // Display a dialog when emptying the trash?
|
||||
|
||||
@objc optional func items() -> SBElementArray
|
||||
@objc optional func containers() -> SBElementArray
|
||||
@objc optional func folders() -> SBElementArray
|
||||
@objc optional func files() -> SBElementArray
|
||||
@objc optional func aliasFiles() -> SBElementArray
|
||||
@objc optional func applicationFiles() -> SBElementArray
|
||||
@objc optional func documentFiles() -> SBElementArray
|
||||
@objc optional func internetLocationFiles() -> SBElementArray
|
||||
@objc optional func clippings() -> SBElementArray
|
||||
@objc optional func packages() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderTrashObject {}
|
||||
|
||||
// MARK: FinderFile
|
||||
@objc
|
||||
public protocol FinderFile: FinderItem {
|
||||
@objc optional var fileType: NSNumber { get } // the OSType identifying the type of data contained in the item (copy)
|
||||
@objc optional var creatorType: NSNumber { get } // the OSType identifying the application that created the item (copy)
|
||||
@objc optional var stationery: Bool { get } // Is the file a stationery pad?
|
||||
@objc optional var productVersion: String { get } // the version of the product (visible at the top of the “Get Info” window) (copy)
|
||||
@objc optional var version: String { get } // the version of the file (visible at the bottom of the “Get Info” window) (copy)
|
||||
|
||||
@objc optional func setFileType(_ fileType: NSNumber!) // the OSType identifying the type of data contained in the item
|
||||
@objc optional func setCreatorType(_ creatorType: NSNumber!) // the OSType identifying the application that created the item
|
||||
@objc optional func setStationery(_ stationery: Bool) // Is the file a stationery pad?
|
||||
}
|
||||
|
||||
extension SBObject: FinderFile {}
|
||||
|
||||
// MARK: FinderAliasFile
|
||||
@objc
|
||||
public protocol FinderAliasFile: FinderFile {
|
||||
@objc optional var originalItem: SBObject { get } // the original item pointed to by the alias (copy)
|
||||
|
||||
@objc optional func setOriginalItem(_ originalItem: SBObject!) // the original item pointed to by the alias
|
||||
}
|
||||
|
||||
extension SBObject: FinderAliasFile {}
|
||||
|
||||
// MARK: FinderApplicationFile
|
||||
@objc
|
||||
public protocol FinderApplicationFile: FinderFile {
|
||||
@objc optional var suggestedSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the developer recommends the application be launched
|
||||
@objc optional var minimumSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched
|
||||
@objc optional var preferredSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched
|
||||
@objc optional var acceptsHighLevelEvents: Bool { get } // Is the application high-level event aware? (OBSOLETE: always returns true)
|
||||
@objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted?
|
||||
@objc optional var opensInClassic: Bool { get } // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment?
|
||||
|
||||
@objc optional func setMinimumSize(_ minimumSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched
|
||||
@objc optional func setPreferredSize(_ preferredSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched
|
||||
@objc optional func setOpensInClassic(_ opensInClassic: Bool) // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment?
|
||||
|
||||
@objc optional func id() -> String // the bundle identifier or creator type of the application
|
||||
}
|
||||
|
||||
extension SBObject: FinderApplicationFile {}
|
||||
|
||||
// MARK: FinderDocumentFile
|
||||
@objc
|
||||
public protocol FinderDocumentFile: FinderFile {}
|
||||
|
||||
extension SBObject: FinderDocumentFile {}
|
||||
|
||||
// MARK: FinderInternetLocationFile
|
||||
@objc
|
||||
public protocol FinderInternetLocationFile: FinderFile {
|
||||
@objc optional var location: String { get } // the internet location (copy)
|
||||
}
|
||||
|
||||
extension SBObject: FinderInternetLocationFile {}
|
||||
|
||||
// MARK: FinderClipping
|
||||
@objc
|
||||
public protocol FinderClipping: FinderFile {
|
||||
@objc optional var clippingWindow: SBObject { get } // (NOT AVAILABLE YET) the clipping window for this clipping (copy)
|
||||
}
|
||||
|
||||
extension SBObject: FinderClipping {}
|
||||
|
||||
// MARK: FinderPackage
|
||||
@objc
|
||||
public protocol FinderPackage: FinderItem {}
|
||||
|
||||
extension SBObject: FinderPackage {}
|
||||
|
||||
// MARK: FinderWindow
|
||||
@objc
|
||||
public protocol FinderWindow: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var position: NSPoint { get } // the upper left position of the window
|
||||
@objc optional var bounds: NSRect { get } // the boundary rectangle for the window
|
||||
@objc optional var titled: Bool { get } // Does the window have a title bar?
|
||||
@objc optional var name: String { get } // the name of the window (copy)
|
||||
@objc optional var index: Int { get } // the number of the window in the front-to-back layer ordering
|
||||
@objc optional var closeable: Bool { get } // Does the window have a close box?
|
||||
@objc optional var floating: Bool { get } // Does the window have a title bar?
|
||||
@objc optional var modal: Bool { get } // Is the window modal?
|
||||
@objc optional var resizable: Bool { get } // Is the window resizable?
|
||||
@objc optional var zoomable: Bool { get } // Is the window zoomable?
|
||||
@objc optional var zoomed: Bool { get } // Is the window zoomed?
|
||||
@objc optional var visible: Bool { get } // Is the window visible (always true for open Finder windows)?
|
||||
@objc optional var collapsed: Bool { get } // Is the window collapsed
|
||||
@objc optional var properties: [AnyHashable: Any] { get } // every property of a window (copy)
|
||||
|
||||
@objc optional func setPosition(_ position: NSPoint) // the upper left position of the window
|
||||
@objc optional func setBounds(_ bounds: NSRect) // the boundary rectangle for the window
|
||||
@objc optional func setIndex(_ index: Int) // the number of the window in the front-to-back layer ordering
|
||||
@objc optional func setZoomed(_ zoomed: Bool) // Is the window zoomed?
|
||||
@objc optional func setCollapsed(_ collapsed: Bool) // Is the window collapsed
|
||||
@objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of a window
|
||||
|
||||
@objc optional func id() -> Int // the unique id for this window
|
||||
}
|
||||
|
||||
extension SBObject: FinderWindow {}
|
||||
|
||||
// MARK: FinderFinderWindow
|
||||
@objc
|
||||
public protocol FinderFinderWindow: FinderWindow {
|
||||
@objc optional var target: SBObject { get } // the container at which this file viewer is targeted (copy)
|
||||
@objc optional var currentView: FinderEcvw { get } // the current view for the container window
|
||||
@objc optional var iconViewOptions: FinderIconViewOptions { get } // the icon view options for the container window (copy)
|
||||
@objc optional var listViewOptions: FinderListViewOptions { get } // the list view options for the container window (copy)
|
||||
@objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for the container window (copy)
|
||||
@objc optional var toolbarVisible: Bool { get } // Is the window's toolbar visible?
|
||||
@objc optional var statusbarVisible: Bool { get } // Is the window's status bar visible?
|
||||
@objc optional var sidebarWidth: Int { get } // the width of the sidebar for the container window
|
||||
|
||||
@objc optional func setTarget(_ target: SBObject!) // the container at which this file viewer is targeted
|
||||
@objc optional func setCurrentView(_ currentView: FinderEcvw) // the current view for the container window
|
||||
@objc optional func setToolbarVisible(_ toolbarVisible: Bool) // Is the window's toolbar visible?
|
||||
@objc optional func setStatusbarVisible(_ statusbarVisible: Bool) // Is the window's status bar visible?
|
||||
@objc optional func setSidebarWidth(_ sidebarWidth: Int) // the width of the sidebar for the container window
|
||||
}
|
||||
|
||||
extension SBObject: FinderFinderWindow {}
|
||||
|
||||
// MARK: FinderDesktopWindow
|
||||
@objc
|
||||
public protocol FinderDesktopWindow: FinderFinderWindow {}
|
||||
|
||||
extension SBObject: FinderDesktopWindow {}
|
||||
|
||||
// MARK: FinderInformationWindow
|
||||
@objc
|
||||
public protocol FinderInformationWindow: FinderWindow {
|
||||
@objc optional var item: SBObject { get } // the item from which this window was opened (copy)
|
||||
@objc optional var currentPanel: FinderIpnl { get } // the current panel in the information window
|
||||
|
||||
@objc optional func setCurrentPanel(_ currentPanel: FinderIpnl) // the current panel in the information window
|
||||
}
|
||||
|
||||
extension SBObject: FinderInformationWindow {}
|
||||
|
||||
// MARK: FinderPreferencesWindow
|
||||
@objc
|
||||
public protocol FinderPreferencesWindow: FinderWindow {
|
||||
@objc optional var currentPanel: FinderPple { get } // The current panel in the Finder preferences window
|
||||
|
||||
@objc optional func setCurrentPanel(_ currentPanel: FinderPple) // The current panel in the Finder preferences window
|
||||
}
|
||||
|
||||
extension SBObject: FinderPreferencesWindow {}
|
||||
|
||||
// MARK: FinderClippingWindow
|
||||
@objc
|
||||
public protocol FinderClippingWindow: FinderWindow {}
|
||||
|
||||
extension SBObject: FinderClippingWindow {}
|
||||
|
||||
// MARK: FinderProcess
|
||||
@objc
|
||||
public protocol FinderProcess: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var name: String { get } // the name of the process (copy)
|
||||
@objc optional var visible: Bool { get } // Is the process' layer visible?
|
||||
@objc optional var frontmost: Bool { get } // Is the process the frontmost process?
|
||||
@objc optional var file: SBObject { get } // the file from which the process was launched (copy)
|
||||
@objc optional var fileType: NSNumber { get } // the OSType of the file type of the process (copy)
|
||||
@objc optional var creatorType: NSNumber { get } // the OSType of the creator of the process (the signature) (copy)
|
||||
@objc optional var acceptsHighLevelEvents: Bool { get } // Is the process high-level event aware (accepts open application, open document, print document, and quit)?
|
||||
@objc optional var acceptsRemoteEvents: Bool { get } // Does the process accept remote events?
|
||||
@objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted?
|
||||
@objc optional var totalPartitionSize: Int { get } // the size of the partition with which the process was launched
|
||||
@objc optional var partitionSpaceUsed: Int { get } // the number of bytes currently used in the process' partition
|
||||
|
||||
@objc optional func setVisible(_ visible: Bool) // Is the process' layer visible?
|
||||
@objc optional func setFrontmost(_ frontmost: Bool) // Is the process the frontmost process?
|
||||
}
|
||||
|
||||
extension SBObject: FinderProcess {}
|
||||
|
||||
// MARK: FinderApplicationProcess
|
||||
@objc
|
||||
public protocol FinderApplicationProcess: FinderProcess {
|
||||
@objc optional var applicationFile: FinderApplicationFile { get } // the application file from which this process was launched (copy)
|
||||
}
|
||||
|
||||
extension SBObject: FinderApplicationProcess {}
|
||||
|
||||
// MARK: FinderDeskAccessoryProcess
|
||||
@objc
|
||||
public protocol FinderDeskAccessoryProcess: FinderProcess {
|
||||
@objc optional var deskAccessoryFile: SBObject { get } // the desk accessory file from which this process was launched (copy)
|
||||
}
|
||||
|
||||
extension SBObject: FinderDeskAccessoryProcess {}
|
||||
|
||||
// MARK: FinderPreferences
|
||||
@objc
|
||||
public protocol FinderPreferences: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var window: FinderPreferencesWindow { get } // the window that would open if Finder preferences was opened (copy)
|
||||
@objc optional var iconViewOptions: FinderIconViewOptions { get } // the default icon view options (copy)
|
||||
@objc optional var listViewOptions: FinderListViewOptions { get } // the default list view options (copy)
|
||||
@objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for all windows (copy)
|
||||
@objc optional var foldersSpringOpen: Bool { get } // Spring open folders after the specified delay?
|
||||
@objc optional var delayBeforeSpringing: Double { get } // the delay before springing open a container in seconds (from 0.167 to 1.169)
|
||||
@objc optional var desktopShowsHardDisks: Bool { get } // Hard disks appear on the desktop?
|
||||
@objc optional var desktopShowsExternalHardDisks: Bool { get } // External hard disks appear on the desktop?
|
||||
@objc optional var desktopShowsRemovableMedia: Bool { get } // CDs, DVDs, and iPods appear on the desktop?
|
||||
@objc optional var desktopShowsConnectedServers: Bool { get } // Connected servers appear on the desktop?
|
||||
@objc optional var newWindowTarget: SBObject { get } // target location for a newly-opened Finder window (copy)
|
||||
@objc optional var foldersOpenInNewWindows: Bool { get } // Folders open into new windows?
|
||||
@objc optional var foldersOpenInNewTabs: Bool { get } // Folders open into new tabs?
|
||||
@objc optional var newWindowsOpenInColumnView: Bool { get } // Open new windows in column view?
|
||||
@objc optional var allNameExtensionsShowing: Bool { get } // Show name extensions, even for items whose “extension hidden” is true?
|
||||
|
||||
@objc optional func setFoldersSpringOpen(_ foldersSpringOpen: Bool) // Spring open folders after the specified delay?
|
||||
@objc optional func setDelayBeforeSpringing(_ delayBeforeSpringing: Double) // the delay before springing open a container in seconds (from 0.167 to 1.169)
|
||||
@objc optional func setDesktopShowsHardDisks(_ desktopShowsHardDisks: Bool) // Hard disks appear on the desktop?
|
||||
@objc optional func setDesktopShowsExternalHardDisks(_ desktopShowsExternalHardDisks: Bool) // External hard disks appear on the desktop?
|
||||
@objc optional func setDesktopShowsRemovableMedia(_ desktopShowsRemovableMedia: Bool) // CDs, DVDs, and iPods appear on the desktop?
|
||||
@objc optional func setDesktopShowsConnectedServers(_ desktopShowsConnectedServers: Bool) // Connected servers appear on the desktop?
|
||||
@objc optional func setNewWindowTarget(_ newWindowTarget: SBObject!) // target location for a newly-opened Finder window
|
||||
@objc optional func setFoldersOpenInNewWindows(_ foldersOpenInNewWindows: Bool) // Folders open into new windows?
|
||||
@objc optional func setFoldersOpenInNewTabs(_ foldersOpenInNewTabs: Bool) // Folders open into new tabs?
|
||||
@objc optional func setNewWindowsOpenInColumnView(_ newWindowsOpenInColumnView: Bool) // Open new windows in column view?
|
||||
@objc optional func setAllNameExtensionsShowing(_ allNameExtensionsShowing: Bool) // Show name extensions, even for items whose “extension hidden” is true?
|
||||
}
|
||||
|
||||
extension SBObject: FinderPreferences {}
|
||||
|
||||
// MARK: FinderLabel
|
||||
@objc
|
||||
public protocol FinderLabel: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var name: String { get } // the name associated with the label (copy)
|
||||
@objc optional var index: Int { get } // the index in the front-to-back ordering within its container
|
||||
@objc optional var color: NSColor { get } // the color associated with the label (copy)
|
||||
|
||||
@objc optional func setName(_ name: String!) // the name associated with the label
|
||||
@objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container
|
||||
@objc optional func setColor(_ color: NSColor!) // the color associated with the label
|
||||
}
|
||||
|
||||
extension SBObject: FinderLabel {}
|
||||
|
||||
// MARK: FinderIconFamily
|
||||
@objc
|
||||
public protocol FinderIconFamily: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var largeMonochromeIconAndMask: Any { get } // the large black-and-white icon and the mask for large icons (copy)
|
||||
@objc optional var large8BitMask: Any { get } // the large 8-bit mask for large 32-bit icons (copy)
|
||||
@objc optional var large32BitIcon: Any { get } // the large 32-bit color icon (copy)
|
||||
@objc optional var large8BitIcon: Any { get } // the large 8-bit color icon (copy)
|
||||
@objc optional var large4BitIcon: Any { get } // the large 4-bit color icon (copy)
|
||||
@objc optional var smallMonochromeIconAndMask: Any { get } // the small black-and-white icon and the mask for small icons (copy)
|
||||
@objc optional var small8BitMask: Any { get } // the small 8-bit mask for small 32-bit icons (copy)
|
||||
@objc optional var small32BitIcon: Any { get } // the small 32-bit color icon (copy)
|
||||
@objc optional var small8BitIcon: Any { get } // the small 8-bit color icon (copy)
|
||||
@objc optional var small4BitIcon: Any { get } // the small 4-bit color icon (copy)
|
||||
}
|
||||
|
||||
extension SBObject: FinderIconFamily {}
|
||||
|
||||
// MARK: FinderIconViewOptions
|
||||
@objc
|
||||
public protocol FinderIconViewOptions: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var arrangement: FinderEarr { get } // the property by which to keep icons arranged
|
||||
@objc optional var iconSize: Int { get } // the size of icons displayed in the icon view
|
||||
@objc optional var showsItemInfo: Bool { get } // additional info about an item displayed in icon view
|
||||
@objc optional var showsIconPreview: Bool { get } // displays a preview of the item in icon view
|
||||
@objc optional var textSize: Int { get } // the size of the text displayed in the icon view
|
||||
@objc optional var labelPosition: FinderEpos { get } // the location of the label in reference to the icon
|
||||
@objc optional var backgroundPicture: FinderFile { get } // the background picture of the icon view (copy)
|
||||
@objc optional var backgroundColor: NSColor { get } // the background color of the icon view (copy)
|
||||
|
||||
@objc optional func setArrangement(_ arrangement: FinderEarr) // the property by which to keep icons arranged
|
||||
@objc optional func setIconSize(_ iconSize: Int) // the size of icons displayed in the icon view
|
||||
@objc optional func setShowsItemInfo(_ showsItemInfo: Bool) // additional info about an item displayed in icon view
|
||||
@objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in icon view
|
||||
@objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the icon view
|
||||
@objc optional func setLabelPosition(_ labelPosition: FinderEpos) // the location of the label in reference to the icon
|
||||
@objc optional func setBackgroundPicture(_ backgroundPicture: FinderFile!) // the background picture of the icon view
|
||||
@objc optional func setBackgroundColor(_ backgroundColor: NSColor!) // the background color of the icon view
|
||||
}
|
||||
|
||||
extension SBObject: FinderIconViewOptions {}
|
||||
|
||||
// MARK: FinderColumnViewOptions
|
||||
@objc
|
||||
public protocol FinderColumnViewOptions: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var textSize: Int { get } // the size of the text displayed in the column view
|
||||
@objc optional var showsIcon: Bool { get } // displays an icon next to the label in column view
|
||||
@objc optional var showsIconPreview: Bool { get } // displays a preview of the item in column view
|
||||
@objc optional var showsPreviewColumn: Bool { get } // displays the preview column in column view
|
||||
@objc optional var disclosesPreviewPane: Bool { get } // discloses the preview pane of the preview column in column view
|
||||
|
||||
@objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the column view
|
||||
@objc optional func setShowsIcon(_ showsIcon: Bool) // displays an icon next to the label in column view
|
||||
@objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in column view
|
||||
@objc optional func setShowsPreviewColumn(_ showsPreviewColumn: Bool) // displays the preview column in column view
|
||||
@objc optional func setDisclosesPreviewPane(_ disclosesPreviewPane: Bool) // discloses the preview pane of the preview column in column view
|
||||
}
|
||||
|
||||
extension SBObject: FinderColumnViewOptions {}
|
||||
|
||||
// MARK: FinderListViewOptions
|
||||
@objc
|
||||
public protocol FinderListViewOptions: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var calculatesFolderSizes: Bool { get } // Are folder sizes calculated and displayed in the window?
|
||||
@objc optional var showsIconPreview: Bool { get } // displays a preview of the item in list view
|
||||
@objc optional var iconSize: FinderLvic { get } // the size of icons displayed in the list view
|
||||
@objc optional var textSize: Int { get } // the size of the text displayed in the list view
|
||||
@objc optional var sortColumn: FinderColumn { get } // the column that the list view is sorted on (copy)
|
||||
@objc optional var usesRelativeDates: Bool { get } // Are relative dates (e.g., today, yesterday) shown in the list view?
|
||||
|
||||
@objc optional func setCalculatesFolderSizes(_ calculatesFolderSizes: Bool) // Are folder sizes calculated and displayed in the window?
|
||||
@objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in list view
|
||||
@objc optional func setIconSize(_ iconSize: FinderLvic) // the size of icons displayed in the list view
|
||||
@objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the list view
|
||||
@objc optional func setSortColumn(_ sortColumn: FinderColumn!) // the column that the list view is sorted on
|
||||
@objc optional func setUsesRelativeDates(_ usesRelativeDates: Bool) // Are relative dates (e.g., today, yesterday) shown in the list view?
|
||||
|
||||
@objc optional func columns() -> SBElementArray
|
||||
}
|
||||
|
||||
extension SBObject: FinderListViewOptions {}
|
||||
|
||||
// MARK: FinderColumn
|
||||
@objc
|
||||
public protocol FinderColumn: SBObjectProtocol, FinderGenericMethods {
|
||||
@objc optional var index: Int { get } // the index in the front-to-back ordering within its container
|
||||
@objc optional var name: FinderElsv { get } // the column name
|
||||
@objc optional var sortDirection: FinderSodr { get } // The direction in which the window is sorted
|
||||
@objc optional var width: Int { get } // the width of this column
|
||||
@objc optional var minimumWidth: Int { get } // the minimum allowed width of this column
|
||||
@objc optional var maximumWidth: Int { get } // the maximum allowed width of this column
|
||||
@objc optional var visible: Bool { get } // is this column visible
|
||||
|
||||
@objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container
|
||||
@objc optional func setSortDirection(_ sortDirection: FinderSodr) // The direction in which the window is sorted
|
||||
@objc optional func setWidth(_ width: Int) // the width of this column
|
||||
@objc optional func setVisible(_ visible: Bool) // is this column visible
|
||||
}
|
||||
|
||||
extension SBObject: FinderColumn {}
|
||||
|
||||
// MARK: FinderAliasList
|
||||
@objc
|
||||
public protocol FinderAliasList: SBObjectProtocol, FinderGenericMethods {}
|
||||
|
||||
extension SBObject: FinderAliasList {}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// MasStoreSearch.swift
|
||||
// MasKit
|
||||
// ITunesSearchAppStoreSearcher.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
|
@ -12,14 +12,14 @@ import Regex
|
|||
import Version
|
||||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
class MasStoreSearch: StoreSearch {
|
||||
class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||
private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#)
|
||||
|
||||
// CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed
|
||||
// into the App Store. Instead, we'll make an educated guess that it matches the currently
|
||||
// selected locale in macOS. This obviously isn't always going to match, but it's probably
|
||||
// better than passing no "country" at all to the iTunes Search API.
|
||||
// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
|
||||
// https://performance-partners.apple.com/search-api
|
||||
private let country: String?
|
||||
private let networkManager: NetworkManager
|
||||
|
||||
|
@ -34,56 +34,59 @@ class MasStoreSearch: StoreSearch {
|
|||
|
||||
/// Searches for an app.
|
||||
///
|
||||
/// - Parameter appName: MAS ID of app
|
||||
/// - Parameter completion: A closure that receives the search results or an Error if there is a
|
||||
/// problem with the network request. Results array will be empty if there were no matches.
|
||||
func search(for appName: String) -> Promise<[SearchResult]> {
|
||||
/// - Parameter searchTerm: a search term matched against app names
|
||||
/// - Returns: A Promise of an Array of SearchResults matching searchTerm
|
||||
func search(for searchTerm: String) -> Promise<[SearchResult]> {
|
||||
// Search for apps for compatible platforms, in order of preference.
|
||||
// Macs with Apple Silicon can run iPad and iPhone apps.
|
||||
var entities = [Entity.macSoftware]
|
||||
var entities = [Entity.desktopSoftware]
|
||||
if SysCtlSystemCommand.isAppleSilicon {
|
||||
entities += [.iPadSoftware, .iPhoneSoftware]
|
||||
}
|
||||
|
||||
let results = entities.map { entity -> Promise<[SearchResult]> in
|
||||
guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else {
|
||||
fatalError("Failed to build URL for \(appName)")
|
||||
let results = entities.map { entity in
|
||||
guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else {
|
||||
fatalError("Failed to build URL for \(searchTerm)")
|
||||
}
|
||||
return loadSearchResults(url)
|
||||
}
|
||||
|
||||
// Combine the results, removing any duplicates.
|
||||
var seenAppIDs = Set<Int>()
|
||||
return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in
|
||||
var seenAppIDs = Set<AppID>()
|
||||
return when(fulfilled: results)
|
||||
.flatMapValues { $0 }
|
||||
.filterValues { result in
|
||||
seenAppIDs.insert(result.trackId).inserted
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appId: MAS ID of app
|
||||
/// - Parameter appID: MAS ID of app
|
||||
/// - Returns: A Promise for the search result record of app, or nil if no apps match the ID,
|
||||
/// or an Error if there is a problem with the network request.
|
||||
func lookup(app appId: Int) -> Promise<SearchResult?> {
|
||||
guard let url = lookupURL(forApp: appId, inCountry: country) else {
|
||||
fatalError("Failed to build URL for \(appId)")
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
||||
fatalError("Failed to build URL for \(appID)")
|
||||
}
|
||||
return firstly {
|
||||
loadSearchResults(url)
|
||||
}.then { results -> Guarantee<SearchResult?> in
|
||||
}
|
||||
.then { results -> Guarantee<SearchResult?> in
|
||||
guard let result = results.first else {
|
||||
return .value(nil)
|
||||
}
|
||||
|
||||
guard let pageUrl = URL(string: result.trackViewUrl)
|
||||
else {
|
||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||
return .value(result)
|
||||
}
|
||||
|
||||
return firstly {
|
||||
self.scrapeAppStoreVersion(pageUrl)
|
||||
}.map { pageVersion in
|
||||
guard let pageVersion,
|
||||
self.scrapeAppStoreVersion(pageURL)
|
||||
}
|
||||
.map { pageVersion in
|
||||
guard
|
||||
let pageVersion,
|
||||
let searchVersion = Version(tolerant: result.version),
|
||||
pageVersion > searchVersion
|
||||
else {
|
||||
|
@ -94,7 +97,8 @@ class MasStoreSearch: StoreSearch {
|
|||
var result = result
|
||||
result.version = pageVersion.description
|
||||
return result
|
||||
}.recover { _ in
|
||||
}
|
||||
.recover { _ in
|
||||
// If we were unable to scrape the App Store page, assume compatibility.
|
||||
.value(result)
|
||||
}
|
||||
|
@ -104,26 +108,27 @@ class MasStoreSearch: StoreSearch {
|
|||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||
firstly {
|
||||
networkManager.loadData(from: url)
|
||||
}.map { data -> [SearchResult] in
|
||||
}
|
||||
.map { data in
|
||||
do {
|
||||
return try JSONDecoder().decode(SearchResultList.self, from: data).results
|
||||
} catch {
|
||||
throw MASError.jsonParsing(error: error as NSError)
|
||||
throw MASError.jsonParsing(data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// App Store pages indicate:
|
||||
// - compatibility with Macs with Apple Silicon
|
||||
// - (often) a version that is newer than what is listed in search results
|
||||
//
|
||||
// We attempt to scrape this information here.
|
||||
private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise<Version?> {
|
||||
/// Scrape the app version from the App Store webpage at the given URL.
|
||||
///
|
||||
/// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API.
|
||||
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageUrl)
|
||||
}.map { data in
|
||||
let html = String(decoding: data, as: UTF8.self)
|
||||
guard let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
networkManager.loadData(from: pageURL)
|
||||
}
|
||||
.map { data in
|
||||
guard
|
||||
let html = String(data: data, encoding: .utf8),
|
||||
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
let version = Version(tolerant: capture)
|
||||
else {
|
||||
return nil
|
|
@ -1,12 +1,12 @@
|
|||
//
|
||||
// SoftwareMap.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 3/1/20.
|
||||
// Copyright © 2020 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
/// Somewhat analygous to CKSoftwareMap
|
||||
/// Somewhat analogous to CKSoftwareMap.
|
||||
protocol SoftwareMap {
|
||||
func allSoftwareProducts() -> [SoftwareProduct]
|
||||
func product(for bundleIdentifier: String) -> SoftwareProduct?
|
159
Sources/mas/Controllers/SoftwareMapAppLibrary.swift
Normal file
159
Sources/mas/Controllers/SoftwareMapAppLibrary.swift
Normal file
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// SoftwareMapAppLibrary.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import CommerceKit
|
||||
import ScriptingBridge
|
||||
|
||||
/// Utility for managing installed apps.
|
||||
class SoftwareMapAppLibrary: AppLibrary {
|
||||
/// CommerceKit's singleton manager of installed software.
|
||||
private let softwareMap: SoftwareMap
|
||||
|
||||
/// Array of installed software products.
|
||||
lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts()
|
||||
.filter { product in
|
||||
product.bundlePath.starts(with: "/Applications/")
|
||||
}
|
||||
|
||||
/// Internal initializer for providing a mock software map.
|
||||
/// - Parameter softwareMap: SoftwareMap to use
|
||||
init(softwareMap: SoftwareMap = CKSoftwareMap.shared()) {
|
||||
self.softwareMap = softwareMap
|
||||
}
|
||||
|
||||
/// Finds an app using a bundle identifier.
|
||||
///
|
||||
/// - Parameter bundleID: Bundle identifier of app.
|
||||
/// - Returns: `SoftwareProduct` for app if found; `nil` otherwise.
|
||||
func installedApp(forBundleID bundleID: String) -> SoftwareProduct? {
|
||||
softwareMap.product(for: bundleID)
|
||||
}
|
||||
|
||||
/// Uninstalls all apps located at any of the elements of `appPaths`.
|
||||
///
|
||||
/// - Parameter appPaths: Paths to apps to be uninstalled.
|
||||
/// - Throws: Error if any problem occurs.
|
||||
func uninstallApps(atPaths appPaths: [String]) throws {
|
||||
try delete(pathsFromOwnerIDsByPath: try chown(paths: appPaths))
|
||||
}
|
||||
}
|
||||
|
||||
func getSudoUsername() -> String? {
|
||||
ProcessInfo.processInfo.environment["SUDO_USER"]
|
||||
}
|
||||
|
||||
func getSudoUID() -> uid_t? {
|
||||
guard let uid = ProcessInfo.processInfo.environment["SUDO_UID"] else {
|
||||
return nil
|
||||
}
|
||||
return uid_t(uid)
|
||||
}
|
||||
|
||||
func getSudoGID() -> gid_t? {
|
||||
guard let gid = ProcessInfo.processInfo.environment["SUDO_GID"] else {
|
||||
return nil
|
||||
}
|
||||
return gid_t(gid)
|
||||
}
|
||||
|
||||
private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t) {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
||||
guard
|
||||
let uid = attributes[.ownerAccountID] as? uid_t,
|
||||
let gid = attributes[.groupOwnerAccountID] as? gid_t
|
||||
else {
|
||||
throw MASError.runtimeError("Failed to determine running user's uid & gid")
|
||||
}
|
||||
return (uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] {
|
||||
guard let sudoUID = getSudoUID() else {
|
||||
throw MASError.runtimeError("Failed to get original uid")
|
||||
}
|
||||
|
||||
guard let sudoGID = getSudoGID() else {
|
||||
throw MASError.runtimeError("Failed to get original gid")
|
||||
}
|
||||
|
||||
let ownerIDsByPath = try paths.reduce(into: [String: (uid_t, gid_t)]()) { dict, path in
|
||||
dict[path] = try getOwnerAndGroupOfItem(atPath: path)
|
||||
}
|
||||
|
||||
var chownedIDsByPath: [String: (uid_t, gid_t)] = [:]
|
||||
for (path, ownerIDs) in ownerIDsByPath {
|
||||
guard chown(path, sudoUID, sudoGID) == 0 else {
|
||||
for (chownedPath, chownedIDs) in chownedIDsByPath
|
||||
where chown(chownedPath, chownedIDs.0, chownedIDs.1) != 0 {
|
||||
printError("Failed to revert ownership of '\(path)' back to uid \(chownedIDs.0) & gid \(chownedIDs.1)")
|
||||
}
|
||||
throw MASError.runtimeError("Failed to change ownership of '\(path)' to uid \(sudoUID) & gid \(sudoGID)")
|
||||
}
|
||||
|
||||
chownedIDsByPath[path] = ownerIDs
|
||||
}
|
||||
|
||||
return ownerIDsByPath
|
||||
}
|
||||
|
||||
private func delete(pathsFromOwnerIDsByPath ownerIDsByPath: [String: (uid_t, gid_t)]) throws {
|
||||
guard let finder: FinderApplication = SBApplication(bundleIdentifier: "com.apple.finder") else {
|
||||
throw MASError.runtimeError("Failed to obtain Finder access: com.apple.finder does not exist")
|
||||
}
|
||||
|
||||
guard let items = finder.items else {
|
||||
throw MASError.runtimeError("Failed to obtain Finder access: finder.items does not exist")
|
||||
}
|
||||
|
||||
for (path, ownerIDs) in ownerIDsByPath {
|
||||
let object = items().object(atLocation: URL(fileURLWithPath: path))
|
||||
|
||||
guard let item = object as? FinderItem else {
|
||||
throw MASError.runtimeError(
|
||||
"""
|
||||
Failed to obtain Finder access: finder.items().object(atLocation: URL(fileURLWithPath: \
|
||||
\"\(path)\") is a '\(type(of: object))' that does not conform to 'FinderItem'
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
guard let delete = item.delete else {
|
||||
throw MASError.runtimeError("Failed to obtain Finder access: FinderItem.delete does not exist")
|
||||
}
|
||||
|
||||
let uid = ownerIDs.0
|
||||
let gid = ownerIDs.1
|
||||
guard let deletedURLString = (delete() as FinderItem).URL else {
|
||||
throw MASError.runtimeError(
|
||||
"""
|
||||
Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \
|
||||
delete result did not have a URL
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
guard let deletedURL = URL(string: deletedURLString) else {
|
||||
throw MASError.runtimeError(
|
||||
"""
|
||||
Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \
|
||||
delete result URL is invalid: \(deletedURLString)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
let deletedPath = deletedURL.path
|
||||
print("Deleted '\(path)' to '\(deletedPath)'")
|
||||
guard chown(deletedPath, uid, gid) == 0 else {
|
||||
throw MASError.runtimeError(
|
||||
"Failed to revert ownership of deleted '\(deletedPath)' back to uid \(uid) & gid \(gid)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// MASError.swift
|
||||
// mas-cli
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 21/08/2015.
|
||||
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
|
||||
|
@ -8,12 +8,17 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum MASError: Error, Equatable {
|
||||
enum MASError: Error, Equatable {
|
||||
case notSupported
|
||||
|
||||
case failed(error: NSError?)
|
||||
|
||||
case runtimeError(String)
|
||||
|
||||
case notSignedIn
|
||||
case noPasswordProvided
|
||||
case signInFailed(error: NSError?)
|
||||
case alreadySignedIn
|
||||
case alreadySignedIn(asAppleID: String)
|
||||
|
||||
case purchaseFailed(error: NSError?)
|
||||
case downloadFailed(error: NSError?)
|
||||
|
@ -24,77 +29,81 @@ public enum MASError: Error, Equatable {
|
|||
case noSearchResultsFound
|
||||
case noVendorWebsite
|
||||
|
||||
case notInstalled
|
||||
case uninstallFailed
|
||||
case notInstalled(appID: AppID)
|
||||
case uninstallFailed(error: NSError?)
|
||||
case macOSUserMustBeRoot
|
||||
|
||||
case noData
|
||||
case jsonParsing(error: NSError?)
|
||||
case jsonParsing(data: Data?)
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
extension MASError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .notSignedIn:
|
||||
return "Not signed in"
|
||||
|
||||
case .noPasswordProvided:
|
||||
return "No password provided"
|
||||
case .notSupported:
|
||||
return """
|
||||
This command is not supported on this macOS version due to changes in macOS. \
|
||||
For more information see: \
|
||||
https://github.com/mas-cli/mas#%EF%B8%8F-known-issues
|
||||
https://github.com/mas-cli/mas#known-issues
|
||||
"""
|
||||
|
||||
case .failed(let error):
|
||||
if let error {
|
||||
return "Failed: \(error.localizedDescription)"
|
||||
}
|
||||
return "Failed"
|
||||
case .runtimeError(let message):
|
||||
return "Runtime Error: \(message)"
|
||||
case .signInFailed(let error):
|
||||
if let error {
|
||||
return "Sign in failed: \(error.localizedDescription)"
|
||||
} else {
|
||||
return "Sign in failed"
|
||||
}
|
||||
|
||||
case .alreadySignedIn:
|
||||
return "Already signed in"
|
||||
|
||||
return "Sign in failed"
|
||||
case .alreadySignedIn(let appleID):
|
||||
return "Already signed in as \(appleID)"
|
||||
case .purchaseFailed(let error):
|
||||
if let error {
|
||||
return "Download request failed: \(error.localizedDescription)"
|
||||
} else {
|
||||
return "Download request failed"
|
||||
}
|
||||
|
||||
return "Download request failed"
|
||||
case .downloadFailed(let error):
|
||||
if let error {
|
||||
return "Download failed: \(error.localizedDescription)"
|
||||
} else {
|
||||
return "Download failed"
|
||||
}
|
||||
|
||||
return "Download failed"
|
||||
case .noDownloads:
|
||||
return "No downloads began"
|
||||
|
||||
case .cancelled:
|
||||
return "Download cancelled"
|
||||
|
||||
case .searchFailed:
|
||||
return "Search failed"
|
||||
|
||||
case .noSearchResultsFound:
|
||||
return "No results found"
|
||||
|
||||
case .noVendorWebsite:
|
||||
return "App does not have a vendor website"
|
||||
|
||||
case .notInstalled:
|
||||
return "Not installed"
|
||||
|
||||
case .uninstallFailed:
|
||||
case .notInstalled(let appID):
|
||||
return "No apps installed with app ID \(appID)"
|
||||
case .uninstallFailed(let error):
|
||||
if let error {
|
||||
return "Uninstall failed: \(error.localizedDescription)"
|
||||
}
|
||||
return "Uninstall failed"
|
||||
|
||||
case .macOSUserMustBeRoot:
|
||||
return "Apps installed from the Mac App Store require root permission to remove."
|
||||
case .noData:
|
||||
return "Service did not return data"
|
||||
|
||||
case .jsonParsing:
|
||||
return "Unable to parse response JSON"
|
||||
case .jsonParsing(let data):
|
||||
if let data {
|
||||
if let unparsable = String(data: data, encoding: .utf8) {
|
||||
return "Unable to parse response as JSON: \n\(unparsable)"
|
||||
}
|
||||
return "Received defective response"
|
||||
}
|
||||
return "Received empty response"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// ExternalCommand.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/1/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// CLI command
|
||||
/// Represents a CLI command.
|
||||
protocol ExternalCommand {
|
||||
var binaryPath: String { get set }
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// OpenSystemCommand.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/2/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// Wrapper for the external open system command.
|
||||
/// https://ss64.com/osx/open.html
|
||||
/// Wrapper for the external 'open' system command (https://ss64.com/osx/open.html).
|
||||
struct OpenSystemCommand: ExternalCommand {
|
||||
var binaryPath: String
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// SysCtlSystemCommand.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Chris Araman on 6/3/21.
|
||||
// Copyright © 2021 mas-cli. All rights reserved.
|
||||
|
@ -8,22 +8,12 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// Wrapper for the external sysctl system command.
|
||||
/// https://ss64.com/osx/sysctl.html
|
||||
/// Wrapper for the external 'sysctl' system command.
|
||||
///
|
||||
/// See - https://ss64.com/osx/sysctl.html
|
||||
struct SysCtlSystemCommand: ExternalCommand {
|
||||
var binaryPath: String
|
||||
|
||||
let process = Process()
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
let stderrPipe = Pipe()
|
||||
|
||||
init(binaryPath: String = "/usr/sbin/sysctl") {
|
||||
self.binaryPath = binaryPath
|
||||
}
|
||||
|
||||
static var isAppleSilicon: Bool = {
|
||||
let sysctl = SysCtlSystemCommand()
|
||||
let sysctl = Self()
|
||||
do {
|
||||
// Returns 1 on Apple Silicon even when run in an Intel context in Rosetta.
|
||||
try sysctl.run(arguments: "-in", "hw.optional.arm64")
|
||||
|
@ -37,4 +27,15 @@ struct SysCtlSystemCommand: ExternalCommand {
|
|||
|
||||
return sysctl.stdout.trimmingCharacters(in: .newlines) == "1"
|
||||
}()
|
||||
|
||||
let process = Process()
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
let stderrPipe = Pipe()
|
||||
|
||||
var binaryPath: String
|
||||
|
||||
init(binaryPath: String = "/usr/sbin/sysctl") {
|
||||
self.binaryPath = binaryPath
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// AppInfoFormatter.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/7/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -18,7 +18,7 @@ enum AppInfoFormatter {
|
|||
let headline = [
|
||||
"\(app.trackName)",
|
||||
"\(app.version)",
|
||||
"[\(app.price ?? 0)]",
|
||||
"[\(app.formattedPrice)]",
|
||||
]
|
||||
.joined(separator: " ")
|
||||
|
||||
|
@ -27,7 +27,7 @@ enum AppInfoFormatter {
|
|||
"By: \(app.sellerName)",
|
||||
"Released: \(humanReadableDate(app.currentVersionReleaseDate))",
|
||||
"Minimum OS: \(app.minimumOsVersion)",
|
||||
"Size: \(humanReadableSize(app.fileSizeBytes ?? "0"))",
|
||||
"Size: \(humanReadableSize(app.fileSizeBytes))",
|
||||
"From: \(app.trackViewUrl)",
|
||||
]
|
||||
.joined(separator: "\n")
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// AppListFormatter.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 6/7/20.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -15,8 +15,8 @@ enum AppListFormatter {
|
|||
|
||||
/// Formats text output with list results.
|
||||
///
|
||||
/// - Parameter products: List of sortware products app data.
|
||||
/// - Returns: Multiliune text outoutp.
|
||||
/// - Parameter products: List of software products app data.
|
||||
/// - Returns: Multiline text output.
|
||||
static func format(products: [SoftwareProduct]) -> String {
|
||||
// find longest appName for formatting, default 50
|
||||
let maxLength = products.map(\.appNameOrBundleIdentifier.count).max() ?? nameColumnMinWidth
|
||||
|
@ -24,12 +24,12 @@ enum AppListFormatter {
|
|||
var output = ""
|
||||
|
||||
for product in products {
|
||||
let appId = product.itemIdentifier.stringValue
|
||||
let appID = product.itemIdentifier.stringValue
|
||||
.padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0)
|
||||
let appName = product.appNameOrBundleIdentifier.padding(toLength: maxLength, withPad: " ", startingAt: 0)
|
||||
let version = product.bundleVersion
|
||||
|
||||
output += "\(appId) \(appName) (\(version))\n"
|
||||
output += "\(appID) \(appName) (\(version))\n"
|
||||
}
|
||||
|
||||
return output.trimmingCharacters(in: .newlines)
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// SearchResultFormatter.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/11/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -10,25 +10,28 @@ import Foundation
|
|||
|
||||
/// Formats text output for the search command.
|
||||
enum SearchResultFormatter {
|
||||
/// Formats text output with search results.
|
||||
/// Formats search results as text.
|
||||
///
|
||||
/// - Parameter results: Search results with app data
|
||||
/// - Returns: Multiliune text outoutp.
|
||||
/// - Parameters:
|
||||
/// - results: Search results containing app data
|
||||
/// - includePrice: Indicates whether to include prices in the output
|
||||
/// - Returns: Multiline text output.
|
||||
static func format(results: [SearchResult], includePrice: Bool = false) -> String {
|
||||
// find longest appName for formatting, default 50
|
||||
let maxLength = results.map(\.trackName.count).max() ?? 50
|
||||
guard let maxLength = results.map(\.trackName.count).max() else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var output = ""
|
||||
|
||||
for result in results {
|
||||
let appId = result.trackId
|
||||
let appID = result.trackId
|
||||
let appName = result.trackName.padding(toLength: maxLength, withPad: " ", startingAt: 0)
|
||||
let version = result.version
|
||||
let price = result.price ?? 0.0
|
||||
|
||||
if includePrice {
|
||||
output += String(format: "%12d %@ $%5.2f (%@)\n", appId, appName, price, version)
|
||||
output += String(format: "%12lu %@ (%@) %@\n", appID, appName, version, result.formattedPrice)
|
||||
} else {
|
||||
output += String(format: "%12d %@ (%@)\n", appId, appName, version)
|
||||
output += String(format: "%12lu %@ (%@)\n", appID, appName, version)
|
||||
}
|
||||
}
|
||||
|
96
Sources/mas/Formatters/Utilities.swift
Normal file
96
Sources/mas/Formatters/Utilities.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// Utilities.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Andrew Naylor on 14/09/2016.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// A collection of output formatting helpers
|
||||
|
||||
/// Terminal Control Sequence Indicator.
|
||||
let csi = "\u{001B}["
|
||||
|
||||
private var standardError = FileHandle.standardError
|
||||
|
||||
extension FileHandle: TextOutputStream {
|
||||
/// Appends the given string to the stream.
|
||||
public func write(_ string: String) {
|
||||
guard let data = string.data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
write(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a message to stdout prefixed with a blue arrow.
|
||||
func printInfo(_ message: String) {
|
||||
guard isatty(fileno(stdout)) != 0 else {
|
||||
print("==> \(message)")
|
||||
return
|
||||
}
|
||||
|
||||
// Blue bold arrow, Bold text
|
||||
print("\(csi)1;34m==>\(csi)0m \(csi)1m\(message)\(csi)0m")
|
||||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Warning:" underlined in yellow.
|
||||
func printWarning(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Warning: \(message)", to: &standardError)
|
||||
return
|
||||
}
|
||||
|
||||
// Yellow, underlined "Warning:" prefix
|
||||
print("\(csi)4;33mWarning:\(csi)0m \(message)", to: &standardError)
|
||||
}
|
||||
|
||||
/// Prints a message to stderr prefixed with "Error:" underlined in red.
|
||||
func printError(_ message: String) {
|
||||
guard isatty(fileno(stderr)) != 0 else {
|
||||
print("Error: \(message)", to: &standardError)
|
||||
return
|
||||
}
|
||||
|
||||
// Red, underlined "Error:" prefix
|
||||
print("\(csi)4;31mError:\(csi)0m \(message)", to: &standardError)
|
||||
}
|
||||
|
||||
/// Flushes stdout.
|
||||
func clearLine() {
|
||||
guard isatty(fileno(stdout)) != 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
print("\(csi)2K\(csi)0G", terminator: "")
|
||||
fflush(stdout)
|
||||
}
|
||||
|
||||
func captureStream(
|
||||
_ stream: UnsafeMutablePointer<FILE>,
|
||||
encoding: String.Encoding = .utf8,
|
||||
_ block: @escaping () throws -> Void
|
||||
) rethrows -> String {
|
||||
let originalFd = fileno(stream)
|
||||
let duplicateFd = dup(originalFd)
|
||||
defer {
|
||||
close(duplicateFd)
|
||||
}
|
||||
|
||||
let pipe = Pipe()
|
||||
dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd)
|
||||
|
||||
do {
|
||||
defer {
|
||||
fflush(stream)
|
||||
dup2(duplicateFd, originalFd)
|
||||
pipe.fileHandleForWriting.closeFile()
|
||||
}
|
||||
|
||||
try block()
|
||||
}
|
||||
|
||||
return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? ""
|
||||
}
|
65
Sources/mas/MAS.swift
Normal file
65
Sources/mas/MAS.swift
Normal file
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// MAS.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Chris Araman on 4/22/21.
|
||||
// Copyright © 2021 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
@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,
|
||||
]
|
||||
)
|
||||
|
||||
static func initialize() {
|
||||
PromiseKit.conf.Q.map = .global()
|
||||
PromiseKit.conf.Q.return = .global()
|
||||
PromiseKit.conf.logHandler = { event in
|
||||
switch event {
|
||||
case .waitOnMainThread:
|
||||
// Ignored. This is a console app that waits on the main thread for
|
||||
// promises to be processed on the global DispatchQueue.
|
||||
break
|
||||
default:
|
||||
// Other events indicate a programming error.
|
||||
fatalError("PromiseKit event: \(event)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validate() throws {
|
||||
Self.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
typealias AppID = UInt64
|
||||
|
||||
extension NSNumber {
|
||||
var appIDValue: AppID {
|
||||
uint64Value
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// SearchResult.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
|
@ -9,13 +9,13 @@
|
|||
struct SearchResult: Decodable {
|
||||
var bundleId: String
|
||||
var currentVersionReleaseDate: String
|
||||
var fileSizeBytes: String?
|
||||
var kind: String
|
||||
var fileSizeBytes: String
|
||||
var formattedPrice: String
|
||||
var minimumOsVersion: String
|
||||
var price: Double?
|
||||
var price: Double
|
||||
var sellerName: String
|
||||
var sellerUrl: String?
|
||||
var trackId: Int
|
||||
var trackId: AppID
|
||||
var trackName: String
|
||||
var trackViewUrl: String
|
||||
var version: String
|
||||
|
@ -24,12 +24,12 @@ struct SearchResult: Decodable {
|
|||
bundleId: String = "",
|
||||
currentVersionReleaseDate: String = "",
|
||||
fileSizeBytes: String = "0",
|
||||
kind: String = "",
|
||||
formattedPrice: String = "0",
|
||||
minimumOsVersion: String = "",
|
||||
price: Double = 0.0,
|
||||
sellerName: String = "",
|
||||
sellerUrl: String = "",
|
||||
trackId: Int = 0,
|
||||
trackId: AppID = 0,
|
||||
trackName: String = "",
|
||||
trackViewUrl: String = "",
|
||||
version: String = ""
|
||||
|
@ -37,7 +37,7 @@ struct SearchResult: Decodable {
|
|||
self.bundleId = bundleId
|
||||
self.currentVersionReleaseDate = currentVersionReleaseDate
|
||||
self.fileSizeBytes = fileSizeBytes
|
||||
self.kind = kind
|
||||
self.formattedPrice = formattedPrice
|
||||
self.minimumOsVersion = minimumOsVersion
|
||||
self.price = price
|
||||
self.sellerName = sellerName
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// SearchResultList.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// SoftwareProduct.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import Version
|
||||
|
||||
/// Protocol describing the members of CKSoftwareProduct used throughout MasKit.
|
||||
/// Protocol describing the members of CKSoftwareProduct used throughout mas.
|
||||
protocol SoftwareProduct {
|
||||
var appName: String { get }
|
||||
var bundleIdentifier: String { get set }
|
||||
|
@ -19,24 +19,25 @@ protocol SoftwareProduct {
|
|||
}
|
||||
|
||||
extension SoftwareProduct {
|
||||
/// Returns bundleIdentifier if appName is empty string.
|
||||
/// - Returns: bundleIdentifier if appName is empty string.
|
||||
var appNameOrBundleIdentifier: String {
|
||||
appName.isEmpty ? bundleIdentifier : appName
|
||||
}
|
||||
|
||||
/// Determines whether the app is considered outdated. Updates that require a higher OS version are excluded.
|
||||
/// Determines whether the app is considered outdated.
|
||||
///
|
||||
/// Updates that require a higher OS version are excluded.
|
||||
///
|
||||
/// - Parameter storeApp: App from search result.
|
||||
/// - Returns: true if the app is outdated; false otherwise.
|
||||
func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool {
|
||||
// Only look at min OS version if we have one, also only consider macOS apps
|
||||
// Replace string literal with MasStoreSearch.Entity once `search` branch is merged.
|
||||
if let osVersion = Version(tolerant: storeApp.minimumOsVersion), storeApp.kind == "mac-software" {
|
||||
// If storeApp requires a version of macOS newer than the running version, do not consider self outdated.
|
||||
if let osVersion = Version(tolerant: storeApp.minimumOsVersion) {
|
||||
let requiredVersion = OperatingSystemVersion(
|
||||
majorVersion: osVersion.major,
|
||||
minorVersion: osVersion.minor,
|
||||
patchVersion: osVersion.patch
|
||||
)
|
||||
// Don't consider an app outdated if the version in the app store requires a higher OS version.
|
||||
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion) else {
|
||||
return false
|
||||
}
|
||||
|
@ -44,7 +45,8 @@ extension SoftwareProduct {
|
|||
|
||||
// The App Store does not enforce semantic versioning, but we assume most apps follow versioning
|
||||
// schemes that increase numerically over time.
|
||||
guard let semanticBundleVersion = Version(tolerant: bundleVersion),
|
||||
guard
|
||||
let semanticBundleVersion = Version(tolerant: bundleVersion),
|
||||
let semanticAppStoreVersion = Version(tolerant: storeApp.version)
|
||||
else {
|
||||
// If a version string can't be parsed as a Semantic Version, our best effort is to check for
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// NetworkManager.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -9,11 +9,11 @@
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
/// Network abstraction
|
||||
/// Network abstraction.
|
||||
class NetworkManager {
|
||||
private let session: NetworkSession
|
||||
|
||||
/// Designated initializer
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// - Parameter session: A networking session.
|
||||
init(session: NetworkSession = URLSession(configuration: .ephemeral)) {
|
||||
|
@ -23,13 +23,14 @@ class NetworkManager {
|
|||
do {
|
||||
let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Caches/com.mphys.mas-cli")
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads data asynchronously.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL to load data from.
|
||||
/// - Parameter url: URL from which to load data.
|
||||
/// - Returns: A Promise for the Data of the response.
|
||||
func loadData(from url: URL) -> Promise<Data> {
|
||||
session.loadData(from: url)
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// NetworkSession.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// URLSession+NetworkSession.swift
|
||||
// MasKit
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
|
@ -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,38 +0,0 @@
|
|||
//
|
||||
// main.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Andrew Naylor on 11/07/2015.
|
||||
// Copyright © 2015 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
import Commandant
|
||||
import MasKit
|
||||
|
||||
MasKit.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,29 +0,0 @@
|
|||
//
|
||||
// AccountCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
// Deprecated test
|
||||
public class AccountCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
//
|
||||
// HomeCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-29.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class HomeCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("home command") {
|
||||
beforeEach {
|
||||
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
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
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(openCommand.arguments).toNot(beNil())
|
||||
expect(openCommand.arguments!.first!) == result.trackViewUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
//
|
||||
// InfoCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class InfoCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
currentVersionReleaseDate: "2019-01-07T18:53:13Z",
|
||||
fileSizeBytes: "1024",
|
||||
minimumOsVersion: "10.14",
|
||||
price: 2.0,
|
||||
sellerName: "Awesome Dev",
|
||||
trackId: 1111,
|
||||
trackName: "Awesome App",
|
||||
trackViewUrl: "https://awesome.app",
|
||||
version: "1.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let cmd = InfoCommand(storeSearch: storeSearch)
|
||||
let expectedOutput = """
|
||||
Awesome App 1.0 [2.0]
|
||||
By: Awesome Dev
|
||||
Released: 2019-01-07
|
||||
Minimum OS: 10.14
|
||||
Size: 1 KB
|
||||
From: https://awesome.app
|
||||
|
||||
"""
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("Info command") {
|
||||
beforeEach {
|
||||
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
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
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(output.contents) == expectedOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// InstallCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class InstallCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("install command") {
|
||||
it("installs apps") {
|
||||
let cmd = InstallCommand()
|
||||
let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false))
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// ListCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class ListCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("list command") {
|
||||
it("lists apps") {
|
||||
let list = ListCommand()
|
||||
let result = list.run(ListCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// LuckyCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class LuckyCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// OpenCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-03.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class OpenCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
trackViewUrl: "fakescheme://some/url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("open command") {
|
||||
beforeEach {
|
||||
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
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
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(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(openCommand.arguments).toNot(beNil())
|
||||
let url = URL(string: openCommand.arguments!.first!)
|
||||
expect(url).toNot(beNil())
|
||||
expect(url) == URL(string: "macappstore://")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
//
|
||||
// OutdatedCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class OutdatedCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
//
|
||||
// PurchaseCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Maximilian Blochberger on 2020-03-21.
|
||||
// Copyright © 2020 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class PurchaseCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("purchase command") {
|
||||
it("purchases apps") {
|
||||
let cmd = PurchaseCommand()
|
||||
let result = cmd.run(PurchaseCommand.Options(appIds: []))
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notSupported
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// SearchCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class SearchCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
trackName: "slack",
|
||||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("search command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
}
|
||||
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())
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
// SignInCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
// Deprecated test
|
||||
public class SignInCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
//
|
||||
// UninstallCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-27.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class UninstallCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("uninstall command") {
|
||||
let appId = 12345
|
||||
let app = SoftwareProductMock(
|
||||
appName: "Some App",
|
||||
bundleIdentifier: "com.some.app",
|
||||
bundlePath: "/tmp/Some.app",
|
||||
bundleVersion: "1.0",
|
||||
itemIdentifier: NSNumber(value: appId)
|
||||
)
|
||||
let mockLibrary = AppLibraryMock()
|
||||
let uninstall = UninstallCommand(appLibrary: mockLibrary)
|
||||
|
||||
context("dry run") {
|
||||
let options = UninstallCommand.Options(appId: appId, dryRun: true)
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
}
|
||||
it("can't remove a missing app") {
|
||||
let result = uninstall.run(options)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
})
|
||||
}
|
||||
it("finds an app") {
|
||||
mockLibrary.installedApps.append(app)
|
||||
|
||||
let result = uninstall.run(options)
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
context("wet run") {
|
||||
let options = UninstallCommand.Options(appId: appId, dryRun: false)
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
}
|
||||
it("can't remove a missing app") {
|
||||
let result = uninstall.run(options)
|
||||
expect(result)
|
||||
.to(
|
||||
beFailure { error in
|
||||
expect(error) == .notInstalled
|
||||
})
|
||||
}
|
||||
it("removes an app") {
|
||||
mockLibrary.installedApps.append(app)
|
||||
|
||||
let result = uninstall.run(options)
|
||||
expect(result).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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// UpgradeCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class UpgradeCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("upgrade command") {
|
||||
it("upgrades stuff") {
|
||||
let cmd = UpgradeCommand()
|
||||
let result = cmd.run(UpgradeCommand.Options(apps: [""]))
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
//
|
||||
// VendorCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-03.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class VendorCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
trackViewUrl: "https://awesome.app",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("vendor command") {
|
||||
beforeEach {
|
||||
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
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
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(openCommand.arguments).toNot(beNil())
|
||||
expect(openCommand.arguments!.first!) == result.sellerUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// VersionCommandSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2018-12-28.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
public class VersionCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
MasKit.initialize()
|
||||
}
|
||||
describe("version command") {
|
||||
it("displays the current version") {
|
||||
let cmd = VersionCommand()
|
||||
let result = cmd.run(VersionCommand.Options())
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue