Showing preview only (491K chars total). Download the full file or copy to clipboard to get everything.
Repository: mas-cli/mas
Branch: main
Commit: 535562b304eb
Files: 138
Total size: 454.7 KB
Directory structure:
gitextract_u6q5_k68/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-bug-report.yaml
│ │ └── 02-feature-request.yaml
│ ├── actionlint.yaml
│ ├── dependabot.yaml
│ ├── release.yaml
│ └── workflows/
│ ├── build-test.yaml
│ ├── codeql.yaml
│ ├── release-published.yaml
│ └── tag-pushed.yaml
├── .gitignore
├── .markdownlint-cli2.yaml
├── .periphery.yaml
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── .xcode-version
├── .yamllint.yaml
├── Brewfile
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Documentation/
│ ├── Sample.swift
│ └── style.md
├── LICENSE
├── Package.resolved
├── Package.swift
├── Plugins/
│ └── MASBuildToolPlugin/
│ └── MASBuildToolPlugin.swift
├── README.md
├── Scripts/
│ ├── _setup_script
│ ├── bootstrap
│ ├── build
│ ├── clean
│ ├── format
│ ├── generate_manual
│ ├── generate_token
│ ├── lint
│ ├── package
│ ├── prebuild
│ ├── release_cancel
│ ├── release_start
│ ├── setup_workflow_repo
│ ├── test
│ ├── update_dependencies
│ ├── update_headers
│ └── version
├── Sources/
│ ├── PrivateFrameworks/
│ │ ├── PrivateFrameworks.c
│ │ └── include/
│ │ ├── CommerceKit/
│ │ │ ├── CKDownloadDirectory.h
│ │ │ ├── CKDownloadQueue.h
│ │ │ ├── CKDownloadQueueObserver-Protocol.h
│ │ │ ├── CKPurchaseController.h
│ │ │ ├── CKServiceInterface.h
│ │ │ ├── CommerceKit.h
│ │ │ └── module.modulemap
│ │ └── StoreFoundation/
│ │ ├── ISAccountService-Protocol.h
│ │ ├── ISServiceProxy.h
│ │ ├── ISStoreAccount.h
│ │ ├── SSDownload.h
│ │ ├── SSDownloadMetadata.h
│ │ ├── SSDownloadPhase.h
│ │ ├── SSDownloadStatus.h
│ │ ├── SSPurchase.h
│ │ ├── SSPurchaseResponse.h
│ │ ├── StoreFoundation.h
│ │ └── module.modulemap
│ └── mas/
│ ├── AppStore/
│ │ ├── AppStoreAction+download.swift
│ │ ├── AppStoreAction.swift
│ │ └── Region.swift
│ ├── Commands/
│ │ ├── Config.swift
│ │ ├── Get.swift
│ │ ├── Home.swift
│ │ ├── Install.swift
│ │ ├── List.swift
│ │ ├── Lookup.swift
│ │ ├── Lucky.swift
│ │ ├── MAS.swift
│ │ ├── Open.swift
│ │ ├── OptionGroups/
│ │ │ ├── CatalogAppIDsOptionGroup.swift
│ │ │ ├── ForceBundleIDOptionGroup.swift
│ │ │ ├── ForceOptionGroup.swift
│ │ │ ├── InstalledAppIDsOptionGroup.swift
│ │ │ ├── OutdatedAccuracy.swift
│ │ │ ├── OutdatedAppOptionGroup.swift
│ │ │ ├── SearchTermOptionGroup.swift
│ │ │ └── VerboseOptionGroup.swift
│ │ ├── Outdated.swift
│ │ ├── Reset.swift
│ │ ├── Search.swift
│ │ ├── Seller.swift
│ │ ├── SignOut.swift
│ │ ├── Uninstall.swift
│ │ ├── Update.swift
│ │ └── Version.swift
│ ├── Controllers/
│ │ ├── CatalogApp+ITunesSearch.swift
│ │ └── InstalledApp+Spotlight.swift
│ ├── Errors/
│ │ └── MASError.swift
│ ├── Models/
│ │ ├── AppID.swift
│ │ ├── CatalogApp.swift
│ │ ├── CatalogAppResults.swift
│ │ ├── InstalledApp.swift
│ │ └── OutdatedApp.swift
│ ├── Network/
│ │ └── URL.swift
│ └── Utilities/
│ ├── Collection.swift
│ ├── FileHandle.swift
│ ├── Group.swift
│ ├── KeyPath.swift
│ ├── Optional.swift
│ ├── Pipe.swift
│ ├── Printer.swift
│ ├── Process.swift
│ ├── ProcessInfo.swift
│ ├── RangeReplaceableCollection.swift
│ ├── Sequence.swift
│ ├── String.swift
│ ├── Sudo.swift
│ ├── User.swift
│ ├── UserAndGroup.swift
│ └── Version+SemVer.swift
├── Tests/
│ └── MASTests/
│ ├── Commands/
│ │ ├── MASTests+Home.swift
│ │ ├── MASTests+List.swift
│ │ ├── MASTests+Lookup.swift
│ │ ├── MASTests+Search.swift
│ │ ├── MASTests+Seller.swift
│ │ └── MASTests+Version.swift
│ ├── Controllers/
│ │ └── MASTests+CatalogApp+ITunesSearch.swift
│ ├── Extensions/
│ │ └── Data.swift
│ ├── MASTests.swift
│ ├── Models/
│ │ ├── MASTests+CatalogApp.swift
│ │ └── MASTests+CatalogAppResults.swift
│ ├── Resources/
│ │ ├── bbedit.json
│ │ ├── slack-lookup.json
│ │ ├── slack.json
│ │ ├── things-lookup.json
│ │ └── things.json
│ └── Utilities/
│ └── Consequences.swift
└── contrib/
└── completion/
├── mas-completion.bash
└── mas.fish
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
#
# .editorconfig
# mas
#
# EditorConfig 0.17.2
#
root = true
[*]
charset = utf-8
continuation_indent_size = 0
end_of_line = lf
indent_size = tab
indent_style = tab
insert_final_newline = true
max_line_length = 120
quote_type = single
spelling_language = en_US
tab_width = 2
trim_trailing_whitespace = true
[*.md]
# Trailing spaces have meaning in Markdown
max_line_length = 80
trim_trailing_whitespace = false
[{*.yaml,*.yml}]
max_line_length = 80
[Scripts/*]
max_line_length = 80
================================================
FILE: .gitattributes
================================================
# Do not remove potentially intentional trailing spaces in Markdown.
**/*.md whitespace=-blank-at-eol
================================================
FILE: .github/CODEOWNERS
================================================
#
# .github/CODEOWNERS
#
/.github/ @mas-cli/admins
================================================
FILE: .github/ISSUE_TEMPLATE/01-bug-report.yaml
================================================
---
name: Bug Report
description: Report a bug.
labels: [\U0001F41B bug]
body:
- type: textarea
id: config
attributes:
label: Configuration
description: Output of `mas config`
placeholder: Output of `mas config`
render: text
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug description
placeholder: |
Bug description
Include expected & actual output, plus other pertinent info
Instead of screenshots, prefer pasting copied commands & output into console blocks, formatted as instructed below
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
placeholder: |
Steps to reproduce
Instead of screenshots, prefer pasting copied commands & output into console blocks, formatted as instructed below
validations:
required: true
- type: markdown
attributes:
value: |
## Console command & output formatting instructions
Provide console commands & output as copied, pasted & formatted text, instead of as screenshots.
If long descriptive text or screenshots of dialogs or apps are necessary, provide them between console blocks.
Format commands & output as follows (where `…` is a placeholder):
- Use a console block: start with ```` ```console ````, end with ```` ``` ````, each on its own line
- Prefix each non-console step (or comment) with two hashes & a space: `## …`
- Remove shell prompts; instead, prefix each console command with a dollar sign & a space: `$ …`
- Prefix each output line beginning with `#`, `$`, `%`, or `>` with an additional instance of that <!--
--> character: `##…`, `$$…`, `%%…`, or `>>…`
- Write all other output lines without any prefix: `…`
e.g.:
````text
```console
## In the App Store GUI, click on…
$ mas list
123 App 1 (4.5.6)
124 App 2 (10.2)
$ mas outdated
123 App 1 (4.5.6 -> 4.5.7)
```
````
================================================
FILE: .github/ISSUE_TEMPLATE/02-feature-request.yaml
================================================
---
name: Feature Request
description: Request a feature.
labels: [\U0001F195 feature request]
body:
- type: textarea
id: problems
attributes:
label: Problem(s) addressed
placeholder: |
Problem(s) addressed
Instead of screenshots, prefer pasting copied commands & output into console blocks, formatted as instructed below
validations:
required: true
- type: textarea
id: proposals
attributes:
label: Proposed solution(s)
placeholder: |
Proposed solution(s)
Instead of screenshots, prefer pasting copied commands & output into console blocks, formatted as instructed below
validations:
required: true
- type: markdown
attributes:
value: |
## Console command & output formatting instructions
Provide console commands & output as copied, pasted & formatted text, instead of as screenshots.
If long descriptive text or screenshots of dialogs or apps are necessary, provide them between console blocks.
Format commands & output as follows (where `…` is a placeholder):
- Use a console block: start with ```` ```console ````, end with ```` ``` ````, each on its own line
- Prefix each non-console step (or comment) with two hashes & a space: `## …`
- Remove shell prompts; instead, prefix each console command with a dollar sign & a space: `$ …`
- Prefix each output line beginning with `#`, `$`, `%`, or `>` with an additional instance of that <!--
--> character: `##…`, `$$…`, `%%…`, or `>>…`
- Write all other output lines without any prefix: `…`
e.g.:
````text
```console
## In the App Store GUI, click on…
$ mas list
123 App 1 (4.5.6)
124 App 2 (10.2)
$ mas outdated
123 App 1 (4.5.6 -> 4.5.7)
```
````
================================================
FILE: .github/actionlint.yaml
================================================
#
# .github/actionlint.yaml
# mas
#
# actionlint 1.7.11
#
---
self-hosted-runner:
labels: [macos-26-intel]
================================================
FILE: .github/dependabot.yaml
================================================
---
version: 2
updates:
- package-ecosystem: github-actions
schedule:
interval: daily
directory: /
labels: [📚 dependencies]
commit-message:
prefix: ⬆️
include: scope
- package-ecosystem: swift
schedule:
interval: daily
directory: /
labels: [📚 dependencies]
commit-message:
prefix: ⬆️
include: scope
================================================
FILE: .github/release.yaml
================================================
---
changelog:
categories:
- title: 🚀 Features
labels: [🆕 feature request]
- title: 🐛 Bug Fixes
labels: [🐛 bug]
- title: Changes
labels: ['*']
================================================
FILE: .github/workflows/build-test.yaml
================================================
#
# .github/workflows/build-test.yaml
#
---
name: Build, Test, and Lint
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
permissions: {}
jobs:
build-test:
name: Build, Test, and Lint
strategy:
matrix:
include:
- runner: macos-14
xcode: brew
- runner: macos-15
- runner: macos-15-intel
- runner: macos-26
- runner: macos-26-intel
runs-on: ${{matrix.runner}}
defaults:
run:
# Force all run commands to not use Rosetta 2 on arm64
shell: ${{endsWith(matrix.runner, '-intel') && '/bin/zsh -Negku {0}' || 'arch -arm64 /bin/zsh -Negku {0}'}}
steps:
- name: 🛒 Checkout repo
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: ${{github.event.repository.default_branch}}
uses: actions/checkout@v6
with:
# Include all history & tags for Scripts/version
fetch-depth: 0
- name: 🔧 Setup repo
run: Scripts/setup_workflow_repo
- name: 🛠 Select Xcode version
if: matrix.xcode != 'brew'
run: xcodes select ${{matrix.xcode}}
- name: 👢 Bootstrap
run: Scripts/bootstrap
- name: 🕊 Use Homebrew Core Swift
if: matrix.xcode == 'brew'
run: |
brew install swift
printf $'%s\n' "$(brew --prefix swift)/bin" >> "${GITHUB_PATH}"
- name: 🏗 Build
run: Scripts/build build-test -c release
- name: 🧪 Test
if: matrix.xcode != 'brew'
run: Scripts/test
- name: 🚨 Lint
if: matrix.xcode != 'brew'
run: Scripts/lint
================================================
FILE: .github/workflows/codeql.yaml
================================================
#
# .github/workflows/codeql.yaml
#
---
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: 44 14 * * 4
workflow_dispatch: {}
jobs:
analyze:
name: Analyze ${{matrix.language}}
runs-on: macos-26
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: swift
build-mode: manual
steps:
- name: 🛒 Checkout repo
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: ${{github.event.repository.default_branch}}
uses: actions/checkout@v6
with:
# Include all history & tags for Scripts/version
fetch-depth: 0
- name: 🔧 Setup repo
run: Scripts/setup_workflow_repo
- name: 🔩 Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{matrix.language}}
build-mode: ${{matrix.build-mode}}
queries: ${{matrix.language == 'swift' && '+security-and-quality' || ''}}
- name: 🏗 Build Swift
if: matrix.language == 'swift'
shell: bash
run: |
xcodes select
Scripts/build codeql -c release
- name: 🔍 Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
with:
category: /language:${{matrix.language}}
================================================
FILE: .github/workflows/release-published.yaml
================================================
#
# .github/workflows/release-published.yaml
#
---
name: release-published
on:
release:
types: [published]
permissions:
actions: read
contents: write
pull-requests: write
defaults:
run:
# Force all run commands to not use Rosetta 2
shell: arch -arm64 /bin/zsh -Negku {0}
jobs:
release-published:
if: ${{!github.event.repository.fork}}
runs-on: macos-26
steps:
- name: 🛒 Checkout repo
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: ${{github.event.repository.default_branch}}
uses: actions/checkout@v6
with:
# Include all history & tags for Scripts/version
fetch-depth: 0
- name: 🔧 Setup repo
run: Scripts/setup_workflow_repo
- name: 🚰 Apply pr-pull label to tap formula bump PR
env:
TOKEN_APP_ID: ${{secrets.TOKEN_APP_ID}}
TOKEN_APP_INSTALLATION_ID: ${{secrets.TOKEN_APP_INSTALLATION_ID}}
TOKEN_APP_PRIVATE_KEY: ${{secrets.TOKEN_APP_PRIVATE_KEY}}
run: |
export GH_TOKEN="$(Scripts/generate_token)"
unsetopt errexit
bump_url="$(gh release -R "${GITHUB_REPOSITORY}" download "${GITHUB_REF_NAME}" -p bump.url -O - 2>/dev/null)"
found_bump_url="${?}"
setopt errexit
if [[ "${found_bump_url}" -eq 0 ]]; then
[[ -n "${bump_url}" ]] && gh pr edit "${bump_url}" --add-label pr-pull
gh release -R "${GITHUB_REPOSITORY}" delete-asset "${GITHUB_REF_NAME}" bump.url -y
else
printf $'No tap formula bump PR URL found for tag %s\n' "${GITHUB_REF_NAME}"
fi
================================================
FILE: .github/workflows/tag-pushed.yaml
================================================
#
# .github/workflows/tag-pushed.yaml
#
---
name: tag-pushed
on:
push:
tags: ['**']
permissions:
contents: write
defaults:
run:
# Force all run commands to not use Rosetta 2
shell: arch -arm64 /bin/zsh -Negku {0}
jobs:
tag-pushed:
if: ${{!github.event.repository.fork}}
runs-on: macos-26
steps:
- name: 🛒 Checkout repo
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: ${{github.event.repository.default_branch}}
uses: actions/checkout@v6
with:
# Include all history & tags for Scripts/version
fetch-depth: 0
- name: 🔧 Setup repo
run: Scripts/setup_workflow_repo
- name: 🖋 Delete tag lacking valid signature
run: |
git fetch --force origin "${GITHUB_REF}:${GITHUB_REF}"
if [[\
"$(git cat-file tag "${GITHUB_REF_NAME}")" != *'-----BEGIN SSH SIGNATURE-----'*'-----END SSH SIGNATURE-----'\
]]; then
printf $'Error: Deleting tag %s because it does not have a valid signature\n' "${GITHUB_REF_NAME}" >&2
git push -d origin "${GITHUB_REF_NAME}"
exit 1
fi
- name: 🏷 Exit if not a version tag
run: |
if [[ ! "${GITHUB_REF_NAME}" =~ '^v[[:digit:]]+(\.[[:digit:]]+)*(-(alpha|beta|rc)\.[[:digit:]]+)?$' ]]; then
printf $'Exiting because %s is not a version tag\n' "${GITHUB_REF_NAME}"
exit 2
fi
- name: 🌳 Delete version tag not on default branch
env:
DEFAULT_BRANCH_NAME: ${{github.event.repository.default_branch}}
run: |
git fetch --force origin "${DEFAULT_BRANCH_NAME}:${DEFAULT_BRANCH_NAME}"
if ! git merge-base --is-ancestor "${GITHUB_REF_NAME}" "${DEFAULT_BRANCH_NAME}"; then
printf $'Error: Deleting version tag %s because it is not on the %s branch\n' "${GITHUB_REF_NAME}"\
"${DEFAULT_BRANCH_NAME}" >&2
git push -d origin "${GITHUB_REF_NAME}"
exit 3
fi
- name: 🛠 Select Xcode version
run: xcodes select
- name: 📦 Build Apple & Intel installers
run: |
Scripts/package package --arch arm64
Scripts/package package --arch x86_64
- name: 🚰 Bump tap formula
env:
TOKEN_APP_ID: ${{secrets.TOKEN_APP_ID}}
TOKEN_APP_INSTALLATION_ID: ${{secrets.TOKEN_APP_INSTALLATION_ID}}
TOKEN_APP_PRIVATE_KEY: ${{secrets.TOKEN_APP_PRIVATE_KEY}}
run: |
export HOMEBREW_GITHUB_API_TOKEN="$(Scripts/generate_token)"
brew tap "${GITHUB_REPOSITORY_OWNER}/tap"
unsetopt errexit
bump_output="$(brew bump-formula-pr\
--tag "${GITHUB_REF_NAME}"\
--revision "${GITHUB_SHA}"\
--no-fork\
--no-browse\
--online\
--strict\
--verbose\
"${GITHUB_REPOSITORY_OWNER}/tap/mas"\
2>&1)"
exit_status="${?}"
setopt errexit
printf %s "${bump_output}"
printf %s "${${(f)bump_output}[-1]}" > .build/bump.url
exit "${exit_status}"
- name: 📝 Create draft release
env:
GH_TOKEN: ${{github.token}}
run: |
gh release create\
"${GITHUB_REF_NAME}"\
".build/mas-${GITHUB_REF_NAME#v}-arm64.pkg"\
".build/mas-${GITHUB_REF_NAME#v}-x86_64.pkg"\
.build/bump.url\
-d\
${"${GITHUB_REF_NAME//[^-]}":+-p}\
-t "${GITHUB_REF_NAME}: ${$(git tag -l "${GITHUB_REF_NAME}" --format='%(contents)')%%$'\n'*}"\
--generate-notes
================================================
FILE: .gitignore
================================================
/.build/
/.idea/
/.swiftpm/
/.vscode/
.DS_Store
*~
================================================
FILE: .markdownlint-cli2.yaml
================================================
# yamllint disable-line rule:line-length
# yaml-language-server: $schema=https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/main/schema/markdownlint-cli2-config-schema.json
#
# .markdownlint-cli2.yaml
# mas
#
# markdownlint-cli2 0.21.0 / markdownlint 0.40.0
#
---
gitignore: true
noBanner: true
noProgress: true
config:
blanks-around-fences:
list_items: false
code-block-style:
style: fenced
code-fence-style:
style: backtick
emphasis-style:
style: underscore
fenced-code-language:
allowed_languages: [console, shell]
language_only: true
heading-style:
style: atx
hr-style:
style: ---
line-length:
stern: true
link-image-style:
shortcut: false
url_inline: false
no-hard-tabs:
code_blocks: false
spaces_per_tab: 2
no-inline-html:
allowed_elements: [details, h1, summary]
no-trailing-spaces:
code_blocks: true
strict: true
ol-prefix:
style: ordered
proper-names:
names: [mas]
reference-links-images:
shortcut_syntax: true
strong-style:
style: asterisk
table-column-style:
style: aligned
table-pipe-style:
style: leading_and_trailing
ul-style:
style: dash
================================================
FILE: .periphery.yaml
================================================
#
# .periphery.yaml
# mas
#
# Periphery 3.6.0
#
---
color: always
disable_update_check: true
quiet: true
relative_results: true
strict: true
superfluous_ignore_comments: false
================================================
FILE: .swift-version
================================================
6.2
================================================
FILE: .swiftformat
================================================
#
# .swiftformat
# mas
#
# SwiftFormat 0.60.1
#
# Disabled rules (enabled by default)
--disable hoistAwait
--disable hoistTry
# Disabled rules (disabled by default)
#--enable blankLineAfterSwitchCase
#--enable markTypes
#--enable preferExplicitFalse
#--enable testSuiteAccessControl
# Enabled rules (disabled by default)
--enable acronyms
--enable blankLinesAfterGuardStatements
--enable blockComments
--enable isEmpty
--enable noExplicitOwnership
--enable noGuardInTests
--enable organizeDeclarations
--enable preferFinalClasses
--enable preferSwiftTesting
--enable privateStateVariables
--enable propertyTypes
--enable singlePropertyPerLine
--enable sortSwitchCases
--enable unusedPrivateDeclarations
--enable urlMacro
--enable validateTestCases
--enable wrapConditionalBodies
--enable wrapEnumCases
--enable wrapMultilineConditionalAssignment
--enable wrapMultilineFunctionChains
--enable wrapSwitchCases
# Rule options
--acronyms ADAM,ADI,AE,ANSI,API,CD,CEO,CF,CI,CK,CPU,DAV,DOS,DS,DSID,DVD,EOF,FAQ,FAT,FD,FTP,GB,GID,GUI,GUID,HTML,HTTP,HTTPS,ID,IFS,ISO,JSON,KB,MAS,MD,MDM,MDS,MIB,MIT,NS,OS,OSX,PDF,PR,QL,SB,SDK,SHA,SS,SSH,STDQ,STDRDL,UDF,UI,UID,URL,US,UTF,UUID,VPP,XML,XPC,YAML
--allow-partial-wrapping false
--complex-attributes prev-line
--computed-var-attributes prev-line
--conditional-assignment always
--exponent-grouping enabled
--file-macro "#fileID"
--fraction-grouping enabled
--func-attributes prev-line
--header //\n// {file}\n// mas\n//\n// Copyright © {created.year} mas-cli\. All rights reserved\.\n//
--hex-literal-case lowercase
--ifdef no-indent
--import-grouping alpha
--indent tab
--indent-strings true
--line-after-marks false
--mark-categories false
--max-width 120
--organization-mode type
--organize-types actor,class,enum,extension,struct
--property-types inferred
--ranges no-space
--redundant-async always
--redundant-throws always
--semicolons never
--short-optionals always
--single-line-for-each convert
--stored-var-attributes prev-line
--tab-width 2
--timezone utc
--type-attributes prev-line
--type-body-marks remove
--wrap-arguments before-first
--wrap-collections before-first
--wrap-conditions before-first
--wrap-effects never
--wrap-parameters before-first
--wrap-return-type never
--wrap-ternary before-operators
--wrap-type-aliases before-first
================================================
FILE: .swiftlint.yml
================================================
#
# .swiftlint.yml
# mas
#
# SwiftLint 0.63.2
#
---
excluded:
- .build/
- .idea/
- .swiftpm/
- .vscode/
opt_in_rules:
- all
analyzer_rules:
- all
disabled_rules:
- closure_body_length
- contrasted_opening_brace
- cyclomatic_complexity
- explicit_acl
- explicit_enum_raw_value
- explicit_self
- explicit_top_level_acl
- explicit_type_interface
- file_header
- function_body_length
- large_tuple
- multiple_closures_with_trailing_closure
- no_extension_access_modifier
- no_grouping_extension
- no_magic_numbers
- prefixed_toplevel_constant
- strict_fileprivate
- type_body_length
attributes:
always_on_line_above: ['@Flag', '@MainActor', '@OptionGroup']
deployment_target:
macOS_deployment_target: 13
macOSApplicationExtension_deployment_target: 13
iOS_deployment_target: 99
iOSApplicationExtension_deployment_target: 99
tvOS_deployment_target: 99
tvOSApplicationExtension_deployment_target: 99
watchOS_deployment_target: 99
watchOSApplicationExtension_deployment_target: 99
file_length:
ignore_comment_only_lines: true
warning: 500
file_name:
excluded: [Group.swift, InstalledApp+Spotlight.swift, Process.swift, User.swift]
file_types_order:
order:
- main_type
- supporting_type
- extension
- preview_provider
- library_content_provider
function_parameter_count:
warning: 6
indentation_width:
indentation_width: 2
include_multiline_strings: false
line_length:
ignores_multiline_strings: true
ignores_regex_literals: true
modifier_order:
preferred_modifier_order:
- acl
- setterACL
- override
- isolation
- dynamic
- mutators
- lazy
- final
- required
- convenience
- typeMethods
- owned
multiline_arguments:
first_argument_location: next_line
non_optional_string_data_conversion:
include_variables: true
number_separator:
minimum_length: 6
opening_brace:
ignore_multiline_statement_conditions: true
operator_usage_whitespace:
skip_aligned_constants: false
prefer_key_path:
restrict_to_standard_functions: false
private_over_fileprivate:
validate_extensions: true
redundant_self:
only_in_closures: false
redundant_type_annotation:
consider_default_literal_types_redundant: true
trailing_comma:
mandatory_comma: true
trailing_whitespace:
ignores_comments: false
type_contents_order:
order:
- case
- associated_type
- type_alias
- subtype
- type_property
- instance_property
- ib_inspectable
- ib_outlet
- initializer
- deinitializer
- type_method
- view_life_cycle_method
- ib_action
- other_method
- subscript
unneeded_override:
affect_initializers: true
unused_import:
require_explicit_imports: true
allowed_transitive_imports:
- module: Darwin
allowed_transitive_imports:
- _DarwinFoundation1
- _DarwinFoundation2
- _DarwinFoundation3
- module: Swift
allowed_transitive_imports:
- _Concurrency
- _StringProcessing
vertical_whitespace_between_cases:
separation: never
================================================
FILE: .xcode-version
================================================
26.3
================================================
FILE: .yamllint.yaml
================================================
#
# .yamllint.yaml
# mas
#
# yamllint 1.38.0
#
---
extends: default
locale: en_US.UTF-8
ignore-from-file: .gitignore
rules:
anchors:
forbid-duplicated-anchors: true
forbid-undeclared-aliases: true
forbid-unused-anchors: true
braces:
forbid: non-empty
max-spaces-inside-empty: 0
brackets:
forbid: false
max-spaces-inside: 0
max-spaces-inside-empty: 0
colons:
max-spaces-before: 0
max-spaces-after: 1
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
require-starting-space: true
min-spaces-from-content: 1
comments-indentation: enable
document-end:
present: false
document-start:
present: true
empty-lines:
max: 2
max-start: 0
max-end: 0
empty-values:
forbid-in-block-mappings: true
forbid-in-block-sequences: true
forbid-in-flow-mappings: true
float-values:
require-numeral-before-decimal: true
hyphens:
max-spaces-after: 1
indentation:
spaces: 2
indent-sequences: false
check-multi-line-strings: false
key-duplicates:
forbid-duplicated-merge-keys: true
key-ordering: disable
line-length:
max: 120
allow-non-breakable-inline-mappings: true
new-line-at-end-of-file: enable
new-lines:
type: unix
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
quoted-strings:
check-keys: true
required: only-when-needed
quote-type: single
trailing-spaces: enable
truthy:
check-keys: false
allowed-values: ['true', 'false']
================================================
FILE: Brewfile
================================================
brew "actionlint" # 1.7.11
brew "gh" # 2.87.3
brew "git" # 2.53.0
brew "ipsw" # 3.1.660
brew "markdownlint-cli2" # 0.21.0
brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.6.0
brew "shellcheck" # 0.11.0
brew "swiftformat" # 0.60.1
brew "swiftlint" # 0.63.2
brew "xcodes" # 1.6.2
brew "yamllint" # 1.38.0
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open & welcoming environment, we as contributors
& maintainers pledge to making participation in our project & our community a
harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, sex characteristics, gender identity & expression, level
of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity & orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming & inclusive language
- Being respectful of differing viewpoints & experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery & unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, & personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior & are expected to take appropriate & fair corrective action in response
to any instances of unacceptable behavior.
Project maintainers have the right & responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, & other contributions that are not
aligned to this Code of Conduct, or to temporarily or permanently ban any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces & in public spaces when
an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined & clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at @mas-cli/admins. All complaints will
be reviewed & investigated & will result in a response that is deemed necessary
& appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the
[Contributor Covenant version 1.4](
https://www.contributor-covenant.org/version/1/4/code-of-conduct/
).
For answers to common questions about this Code of Conduct, see
the [Contributor Covenant FAQ](https://www.contributor-covenant.org/faq/).
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Pull requests (PRs) are welcome from everyone.
By participating in this project, you agree to abide by the
[code of conduct](CODE_OF_CONDUCT.md).
## Getting Started
- Ensure you have a [GitHub account](https://github.com/signup)
- [Search for similar issues](https://github.com/mas-cli/mas/issues)
- If one doesn't exist,
[open a new issue](https://github.com/mas-cli/mas/issues/new/choose)
- Select the appropriate issue template
- Follow the instructions in the issue template
## Making Changes
This project uses [trunk-based development](https://trunkbaseddevelopment.com),
where `main` is the trunk.
- [Fork the repository](https://github.com/mas-cli/mas#fork-button) on
GitHub
- Clone your fork: `git clone git@github.com:your-username/mas.git`
- Create a topic branch instead of [working directly on `main`](
https://softwareengineering.stackexchange.com/questions/223400/when-should-i-stop-committing-to-master-on-new-projects
)
- To branch a topic branch from `main` named, e.g., `feature`, run:
`git checkout -b feature main`
- Commit logical units
- Follow the [style guide](Documentation/style.md)
- Run `Scripts/format` before committing
- Run `Scripts/lint` before committing, then fix all lint violations
- Write tests
- If you need help with tests, feel free to open a PR, then ask for help
- Write [good commit messages](
https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
)
- Push your topic branch to your fork, then submit a pull request (PR)
- If your PR is not ready to be merged, create a draft PR
## Releases
- Release commits are tagged in the format of `v1.2.3`
- Releases (including release notes) are published on the
[Releases page](https://github.com/mas-cli/mas/releases)
## Becoming a Contributor
To join the [team](https://github.com/orgs/mas-cli/teams/contributors), once a
few of your PRs have been accepted,
[open an issue](https://github.com/mas-cli/mas/issues/new) titled
"Add Contributor: @YourGitHubUsername".
This project was created by [@argon](https://github.com/argon), who is unable to
continue contributing to this project, but must remain an owner.
By becoming a contributor, you agree to the following terms:
- Do not claim to be the original author
- Retain [@argon](https://github.com/argon)'s name in the license; others' names
may be added to it after they have made substantial contributions
- Retain [@argon](https://github.com/argon)'s name (Andrew Naylor),
[GitHub account](https://github.com/argon) & [X handle](https://x.com/argon)
in the [README](README.md), though they may be repositioned as deemed suitable
## Project Lead Responsibilities
Project leads agree to the following terms:
- [@argon](https://github.com/argon) must continue to be one of the organization
owners
- Project leads have full control, however, over the project's future direction
- If you are the sole project lead, but can no longer lead the project, either:
- Find someone else to assume the project leadership who agrees to adhere to,
& propagate, the existing terms
- If you cannot find a new project lead:
- Add an [unmaintained badge](https://unmaintained.tech) to the
[README](README.md)
- Message [@argon](https://github.com/argon) via [X](https://x.com/argon) or
[email](mailto:argon@mkbot.net)
- Transfer the project back to [@argon](https://github.com/argon)
================================================
FILE: Documentation/Sample.swift
================================================
//
// Sample.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
// MARK: Types & naming
/// The first letter of a type should be uppercase.
///
/// Prefer structs. When a class is necessary, default to making it `final`.
final class Sample {
let name: String
/// If the first letter of an acronym is lowercase, the entire thing should be
/// lowercase.
let json: Any
deinit {
// Clean up resources
}
/// If the first letter of an acronym is uppercase, the entire thing should be
/// uppercase.
static func decode(from json: JSON) -> Self {
.init(json: json)
}
}
/// Use `()` for void arguments & `Void` for void return types.
let closure: () -> Void = {
// Do nothing
}
/// Use `typealias` when closures are referenced in multiple places.
typealias CoolClosure = (Int) -> Bool
/// Use aliased parameter names when function parameters are ambiguous.
func yTown(some: Int, withCallback callback: CoolClosure) -> Bool {
callback(some)
}
/// Use `$` variable references if the closure fits on one line.
let cool = yTown(5) { $0 == 6 }
/// Use explicit variable names if the closure is on multiple lines.
let cool = yTown(5) { foo in
max(foo, 0)
// …
}
// Strongify weak references in async closures
APIClient.getAwesomeness { [weak self] result in
guard let self else {
return
}
stopLoadingSpinner()
show(result)
}
/// Use if-let to check for not `nil` (even if using an implicitly unwrapped
/// variable from an API).
func someUnauditedAPI(thing: String?) {
if let thing {
printer.info(thing)
}
}
/// Infer variable types instead of explicitly declaring them.
let response = Response.success(Data())
func doSomeWork() -> Response {
let data = Data("", .utf8)
return .success(data)
}
switch response {
case let .success(data):
printer.info("The response returned successfully", data)
case let .failure(error):
printer.error("An error occurred:", error: error)
}
// MARK: Organization
/// Group methods into specific extensions for each level of access control.
private extension MyClass {
func doSomethingPrivate() {
// Do something
}
}
// MARK: Breaking up long lines
// If a guard clause requires multiple lines, chop it down, then start the else
// clause on a new line
guard
let oneItem = somethingFailable(),
let secondItem = somethingFailable2()
else {
return
}
================================================
FILE: Documentation/style.md
================================================
# All Files
- Before committing, run `Scripts/lint` to detect linting violations
- Run `Scripts/format` to automatically fix many linting violations
- Remove unnecessary trailing whitespace
- Note that 2 trailing spaces is valid Markdown to create a line break like
`<br>`, so those should _not_ be removed
- End each file with a [single newline character](
https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789
)
## Swift
[Sample](Sample.swift)
- Avoid [force unwrapping optionals](
https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals
) with `!` in production code
- Production code is located under the [`Sources/mas`](
https://github.com/mas-cli/mas/tree/main/Sources/mas
) folder
- However, force unwrapping is **encouraged** in tests for concision; tests
_should_ break when any expected conditions aren't met
- Prefer `struct`s over `class`es wherever possible
- Default to marking classes as `final`
- Prefer composition over protocol conformance over class inheritance
- Break lines at 120 characters
- Use tabs for indentation
- Use `let` whenever possible to make immutable bindings
- Name most parameters in functions & enum cases
- Use trailing closures
- Let the compiler infer the type whenever possible
- Group computed properties below stored properties
- Use a blank line above & below computed properties
- Apply the capitalization of the first letter throughout an acronym or
initialism
- Use `()` for void arguments & `Void` for void return types
- Strongify weak references instead of evaluating them multiple times
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Andrew Naylor, Ross Goldberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Package.resolved
================================================
{
"originHash" : "fe336dc5a91893be96812ec91baba97112f3407b3c398b918ac3eeebc5bc9781",
"pins" : [
{
"identity" : "bigint",
"kind" : "remoteSourceControl",
"location" : "https://github.com/attaswift/BigInt.git",
"state" : {
"revision" : "e07e00fa1fd435143a2dcf8b7eec9a7710b2fdfe",
"version" : "5.7.0"
}
},
{
"identity" : "chronometer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Chronometer.git",
"state" : {
"revision" : "99d4f4137837a28e1e294eb69ac66ad53c0934d3",
"version" : "0.1.14"
}
},
{
"identity" : "hitch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Hitch.git",
"state" : {
"revision" : "d3bfe4b90303653d71f5423c6ec2cbe47b75d7ed",
"version" : "0.4.151"
}
},
{
"identity" : "sextant",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Sextant.git",
"state" : {
"revision" : "d4f794ac57a84dadacd1a0edd5d29717ef0e7b74",
"version" : "0.4.38"
}
},
{
"identity" : "spanker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Spanker.git",
"state" : {
"revision" : "ff05ac41aea633ca7a9fd966d3164373247ee8dd",
"version" : "0.2.53"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615",
"version" : "1.7.0"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
"version" : "1.3.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "8d9834a6189db730f6264db7556a7ffb751e99ee",
"version" : "1.4.0"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "dba183c96b2da4e4b80bb31b1e2e59cb9542b8fc",
"version" : "2.13.0"
}
}
],
"version" : 3
}
================================================
FILE: Package.swift
================================================
// swift-tools-version:6.2
private import PackageDescription
private let swiftSettings = [
SwiftSetting
.enableUpcomingFeature("ExistentialAny"), // swiftformat:disable:this indent
.enableUpcomingFeature("ImmutableWeakCaptures"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.treatAllWarnings(as: .error),
]
_ = Package(
name: "mas",
platforms: [.macOS(.v13)],
products: [.executable(name: "mas", targets: ["mas"])],
dependencies: [
.package(url: "https://github.com/KittyMac/Sextant.git", from: "0.4.38"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.7.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.4.0"),
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.13.0"),
],
targets: [
.plugin(name: "MASBuildToolPlugin", capability: .buildTool()),
.target(name: "PrivateFrameworks"),
.executableTarget(
name: "mas",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "OrderedCollections", package: "swift-collections"),
"BigInt",
"PrivateFrameworks",
"Sextant",
"SwiftSoup",
],
swiftSettings: swiftSettings,
linkerSettings: [.unsafeFlags(["-F", "/System/Library/PrivateFrameworks"])],
plugins: [.plugin(name: "MASBuildToolPlugin")],
),
.testTarget(
name: "MASTests",
dependencies: ["mas"],
resources: [.process("Resources")],
swiftSettings: swiftSettings,
),
],
swiftLanguageModes: [.v6],
)
================================================
FILE: Plugins/MASBuildToolPlugin/MASBuildToolPlugin.swift
================================================
//
// MASBuildToolPlugin.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import Foundation
internal import PackagePlugin
@main
struct MASBuildToolPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target _: any Target) -> [Command] {
[
.prebuildCommand(
displayName: "Prebuild mas",
executable: context.package.directoryURL.appending(path: "Scripts/prebuild", directoryHint: .notDirectory),
arguments: [context.pluginWorkDirectoryURL.path(percentEncoded: false)],
environment: ProcessInfo.processInfo.environment,
outputFilesDirectory: context.pluginWorkDirectoryURL,
),
]
}
}
================================================
FILE: README.md
================================================
<h1 align="center">

</h1>
[](https://github.com/mas-cli/mas/releases)
[](Package.swift)
[](LICENSE)
[](https://www.swift.org)
[](
https://github.com/mas-cli/mas/actions/workflows/build-test.yaml?query=branch%3Amain
)
[](Package.swift)
mas is a command-line interface for the Mac App Store that is designed for
scripting & automation.
<details>
<summary>
## 📲 Installation
</summary>
<details>
<summary>
### 🔮 macOS 13 (Ventura) or newer
</summary>
<details>
<summary>
#### 🍺 Homebrew Core formula
</summary>
[Homebrew](https://brew.sh) is the preferred way to install:
```shell
brew install mas
```
</details>
<details>
<summary>
#### 🔌 MacPorts
</summary>
[MacPorts](https://www.macports.org/install.php) is an alternative way to
install:
```shell
sudo port install mas
```
</details>
</details>
<details>
<summary>
### 🧮 macOS 10.11 (El Capitan) - 12 (Monterey)
</summary>
<details>
<summary>
#### 🍻 Homebrew tap
</summary>
The [mas-cli Homebrew tap](https://github.com/mas-cli/homebrew-tap) provides
pre-built bottles for all macOS versions since 10.11 (El Capitan).
The newest versions of mas, however, are only available for macOS 13+ (Ventura
or newer).
To install mas from the tap:
```shell
brew install mas-cli/tap/mas
```
</details>
<details>
<summary>
#### 🐙 GitHub Releases
</summary>
Alternatively, binaries & sources are available from
[GitHub Releases](https://github.com/mas-cli/mas/releases).
</details>
</details>
</details>
<details>
<summary>
## 🤳 Usage
</summary>
<details>
<summary>
### 🪪 App IDs
</summary>
Each app in the App Store has a unique integer app identifier (ADAM ID) & a
unique text app identifier (bundle ID). mas commands accept either form of app
ID as arguments.
`mas search` & `mas list` can be used to find the ADAM IDs of apps.
Alternatively, to find an app's ADAM ID:
1. Find the app in the App Store
2. Select `Share` > `Copy Link`
3. Extract the ADAM ID from the URL
- e.g., extract ADAM ID `497799835` from the URL for Xcode
(<https://apps.apple.com/us/app/xcode/id497799835?mt=12>)
</details>
<details>
<summary>
### 🛍 Info from the App Store
</summary>
The commands in this section do not require you to be logged into an Apple
Account, neither for your macOS user nor for the App Store.
<details>
<summary>
#### `mas search`
</summary>
`mas search <search-term>` searches by name for apps available from the App
Store.
Providing the `--price` flag includes each app's price in the output.
```console
$ mas search Xcode
497799835 Xcode
688199928 Docs for Xcode
…
```
</details>
<details>
<summary>
#### `mas lookup`
</summary>
`mas lookup <app-id>` outputs more detailed information about an app available
from the App Store.
```console
$ mas lookup 497799835
Xcode 26.1.1 [Free]
By: Apple Inc.
Released: 2025-11-11
Minimum OS: 15.6
Size: 2,913.8 MB
From: https://apps.apple.com/us/app/xcode/id497799835?mt=12&uo=4
```
</details>
</details>
<details>
<summary>
### 📚 Info from your local app library
</summary>
All the commands in this section require you to be logged into an Apple Account
for your macOS user.
<details>
<summary>
#### `mas list`
</summary>
`mas list` outputs all the apps on your Mac that were installed from the App
Store.
```console
$ mas list
497799835 Xcode (15.4)
640199958 Developer (10.6.5)
899247664 TestFlight (3.5.2)
```
</details>
<details>
<summary>
#### `mas outdated`
</summary>
`mas outdated` outputs all apps installed from the App Store on your Mac that
have pending updates.
```console
$ mas outdated
497799835 Xcode (15.4 -> 16.0)
640199958 Developer (10.6.5 -> 10.6.6)
```
Run [`mas update`](#mas-update) to install pending updates.
</details>
</details>
<details>
<summary>
### ⬇️ Installing apps
</summary>
All the commands in this section require you to be logged into an Apple Account
in the App Store.
> Depending on your Apple Account settings, you might need to re-authenticate in
> the App Store to perform a `get`, `install`, `lucky`, or `update`, even if you
> are already signed in to an Apple Account in the App Store.
<details>
<summary>
#### `mas get`
</summary>
`mas get <app-id>…` installs free apps that you haven't yet gotten/"purchased"
from the App Store.
[Requires root privileges to install apps](#-root-privileges).
> The `purchase` alias is currently a misnomer, because it currently can only
> "purchase" free apps. To purchase apps that cost money, purchase them directly
> in the App Store.
```console
$ mas get 497799835
==> Downloading Xcode
==> Installed Xcode
```
</details>
<details>
<summary>
#### `mas install`
</summary>
`mas install <app-id>…` installs apps that you have already gotten or purchased
from the App Store. Providing the `--force` flag re-installs the app even if it
is already installed on your Mac.
[Requires root privileges to install apps](#-root-privileges).
```console
$ mas install 497799835
==> Downloading Xcode
==> Installed Xcode
```
</details>
<details>
<summary>
#### `mas lucky`
</summary>
`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 or purchased.
[Requires root privileges to install apps](#-root-privileges).
```console
$ mas lucky Xcode
==> Downloading Xcode
==> Installed Xcode
```
</details>
</details>
<details>
<summary>
### 🆕 Upgrading apps
</summary>
All the commands in this section require you to be logged into an Apple Account
in the App Store.
> mas only installs/updates apps from the App Store.
>
> Use [`softwareupdate(8)`](https://www.unix.com/man-page/osx/8/softwareupdate)
> to install system updates (e.g., Xcode Command Line Tools, Safari, etc.)
<details>
<summary>
#### `mas update`
</summary>
`mas update` updates outdated apps installed from the App Store. Without any
arguments, it updates all such apps.
[Requires root privileges to update apps](#-root-privileges).
```console
$ mas update
Upgrading 2 outdated applications:
Xcode (15.4) -> (16.0)
Developer (10.6.5) -> (10.6.6)
==> Downloading Xcode
==> Installed Xcode
==> Downloading Developer
==> Installed Developer
```
Updates can be performed selectively by providing app IDs to `mas update`.
```console
$ mas update 715768417
Upgrading 1 outdated application:
Xcode (15.4) -> (16.0)
==> Downloading Xcode
==> Installed Xcode
```
</details>
</details>
<details>
<summary>
### 🪪 App Store account management
</summary>
All the commands in this section interact with the Apple Account for which you
are signed in to the App Store. These commands do not interact with the Apple
Account for which your macOS user is signed in.
<details>
<summary>
#### `mas signout`
</summary>
`mas signout` signs out from the current Apple Account in the App Store.
</details>
</details>
<details>
<summary>
### 🫚 Root privileges
</summary>
Root privileges are now necessary to install/update apps from the App Store,
because Apple secured `installd` on macOS 26.1+, 15.7.2+ & 14.8.2+ to fix
[CVE-2025-43411](https://nvd.nist.gov/vuln/detail/CVE-2025-43411). To simplify
the code, mas 4.0.0+ requires root privileges to install/update apps for all
versions of macOS, even older ones for which `installd` hasn't been secured.
Most users are already, or soon will be, using affected macOS versions.
Root privileges were always necessary to uninstall apps from the App Store,
because such apps are owned by the `root` user on macOS. mas 4.0.0+ will request
root privileges if you run mas without them, so you needn't remember to use
`sudo mas uninstall …` like beforehand.
Root privileges can be granted by running using `sudo mas …` on the command
line, or, if you run `mas` by itself without `sudo`, by entering your macOS
account password when prompted by `mas`. If you choose the latter route, the
supplied password is piped directly from the terminal to an external process
`sudo` call in the `mas` executable; your password is never seen by any mas
code, nor is it stored in any way.
Any sudo credentials used or established by the `mas` executable will remain
valid, pursuant to your user-configured sudo timeout settings.
</details>
</details>
<details>
<summary>
## 🧩 Integrations
</summary>
<details>
<summary>
### 🍻 Homebrew Bundle
</summary>
If mas is installed:
- `brew bundle dump` includes installed App Store apps in the generated
`Brewfile`
- Homebrew Bundle commands will process App Store apps included in a `Brewfile`
See the
[Homebrew Bundle documentation](https://docs.brew.sh/Brew-Bundle-and-Brewfile)
for more details.
</details>
<details>
<summary>
### ⚙️ Topgrade
</summary>
If mas is installed, running [Topgrade](https://github.com/topgrade-rs/topgrade)
updates installed App Store apps.
</details>
</details>
<details>
<summary>
## ⚠️ Known issues
</summary>
<details>
<summary>
### 💥 Broken Apple private frameworks
</summary>
mas uses multiple undocumented Apple private frameworks to implement much of its
functionality.
Over time, Apple has silently changed these frameworks, breaking some
functionality, including:
- [The `account` command is not supported on macOS 12 (Monterey) or newer](
https://github.com/mas-cli/mas/issues/417
)
- [The `signin` command is not supported on macOS 10.13 (High Sierra) or newer](
https://github.com/mas-cli/mas/issues/164
)
</details>
<details>
<summary>
### ⏳ Eventual consistency
</summary>
The App Store operates on eventual consistency.
[The app versions seen by various parts of mas or the App Store might be
inconsistent for days](https://github.com/mas-cli/mas/issues/387).
</details>
<details>
<summary>
### 📱 iOS & iPadOS apps
</summary>
Apple Silicon Macs can install iOS & iPadOS apps from the App Store.
[mas does not yet support iOS or iPadOS apps](
https://github.com/mas-cli/mas/issues/321
).
</details>
<details>
<summary>
### 📺 `tmux`
</summary>
mas depends on the same XPC system services as the App Store.
mas thus experiences similar problems as the pasteboard when running inside
`tmux`.
This [wrapper](https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard) allows
pasteboard & mas to work inside `tmux`.
`tmux` can be configured to always use the wrapper.
Alternatively, the wrapper can be used on a one-off basis:
```shell
brew install reattach-to-user-namespace
reattach-to-user-namespace mas install
```
</details>
<details>
<summary>
### 🤷 Undetected installed apps
</summary>
mas 2.0.0+ sources data for installed App Store apps from macOS's Spotlight
Metadata Server (aka MDS).
You can check if an App Store app is properly indexed in Spotlight:
```console
## General format:
$ mdls -rn kMDItemAppStoreAdamID <path-to-app>
## Outputs the ADAM ID if the app is indexed
## Outputs nothing if the app is not indexed
## Example:
$ mdls -rn kMDItemAppStoreAdamID /Applications/WhatsApp.app
310633997
```
If an app has been indexed in Spotlight, the path to the app can be found:
```shell
mdfind 'kMDItemAppStoreAdamID = <adam-id>'
```
If any App Store apps are not properly indexed, you can reindex:
```shell
# Individual apps (if you know exactly what apps were incorrectly omitted):
mdimport /Applications/Example.app
# All apps (<LargeAppVolume> is the volume optionally selected for large apps):
mdimport /Applications /Volumes/<LargeAppVolume>/Applications
# All file system volumes (if neither aforementioned command solved the issue):
sudo mdutil -Eai on
```
</details>
</details>
<details>
<summary>
## ❗ Troubleshooting
</summary>
<details>
<summary>
### 🚫 Redownload not available
</summary>
If the following error occurs, you probably [haven't yet gotten or purchased the
app from the App Store](#mas-install).
> This redownload is not available for this Apple Account either because it was
> bought by a different user or the item was refunded or canceled.
</details>
<details>
<summary>
### ❓ Other issues
</summary>
If mas doesn't work as expected (e.g., apps can't be installed/updated), run
`mas reset`, then try again.
If the issue persists, please [file a bug](
https://github.com/mas-cli/mas/issues/new?template=01-bug-report.yaml
).
All feedback is much appreciated!
</details>
</details>
<details>
<summary>
## 🏗 Building
</summary>
mas can be built in Xcode or built by the following script:
```shell
Scripts/build
```
Build output can be found in the `.build` folder in the project's root folder.
</details>
<details>
<summary>
## 🧪 Testing
</summary>
Tests are implemented in
[Swift Testing](https://developer.apple.com/xcode/swift-testing).
Tests can be run by the following script:
```shell
Scripts/test
```
</details>
<details>
<summary>
## 📄 License
</summary>
Code is under the [MIT license](LICENSE).
mas was originally created by Andrew Naylor
([@argon on GitHub](https://github.com/argon) /
[@argon on X](https://x.com/argon)).
</details>
================================================
FILE: Scripts/_setup_script
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/_setup_script
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Performs boilerplate setup for scripts.
#
builtin unalias -as
setopt\
autopushd\
combiningchars\
extendedglob\
extendedhistory\
no_globalrcs\
histexpiredupsfirst\
histignorespace\
histverify\
incappendhistorytime\
interactivecomments\
pipefail\
no_rcs\
no_unset
export -r HISTCHARS='!^#'
export -r IFS=$' \t\n\0'
export -r NULLCMD=cat
export -r PAGER=cat
export -r READNULLCMD=cat
export -r TMPDIR="${"${TMPDIR:-/tmp/}"/%(#b)([^\/])/"${match[1]}"/}"
export -r TMPPREFIX="${TMPPREFIX:-"${TMPDIR}"zsh}"
unset CDPATH
unset ENV
unset KEYBOARD_HACK
unset TMPSUFFIX
unset WORDCHARS
readonly mas_folder="${0:A:h:h}"
if ! cd -- "${mas_folder}"; then
printf $'Error: Failed to cd into mas folder: %s\n' "${mas_folder}" >&2
exit 1
fi
print_notice() {
[[ -v MAS_DO_NOT_PRINT_NOTICE ]] && return
if [[ -t 1 ]]; then
local -r prefix=$'\e[1;34m==>\e[0m'
else
local -r prefix='==>'
fi
printf $'%s %s mas %s%s\n' "${prefix}" "${1}" "${MAS_VERSION:-$(Scripts/version)}" "${${*:2}:+ (arguments: ${${(q)@:2}[*]})}"
}
ensure_command_available() {
local -i return_status=0
local command
for command in "${@}"; do
if ! whence "${command}" >/dev/null; then
printf $'error: %s is not installed. Run \'Scripts/bootstrap\' or \'brew install %s\'.\n' "${command}" "${command}" >&2
return_status=1
fi
done
return "${return_status}"
}
================================================
FILE: Scripts/bootstrap
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/bootstrap
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Installs dependencies for Scripts/format & Scripts/lint.
#
# Usage: bootstrap [<brew-bundle-install-argument>...]
#
. "${0:A:h}/_setup_script"
print_notice '👢 Bootstrapping' "${@}"
if ! whence brew >/dev/null; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
brew update -q
installed_formulae=("${(f)$(brew list --formulae)}")
readonly -a installed_formulae
bootstrap_formulae=("${(f)$(brew deps --union "${(f)$(brew bundle list --formulae)}")}")
readonly -a bootstrap_formulae
# shellcheck disable=SC2086
(("${#installed_formulae:*bootstrap_formulae}")) && brew upgrade --overwrite -q ${installed_formulae:*bootstrap_formulae}
brew bundle upgrade -fq "${@}"
brew upgrade --overwrite -q "${bootstrap_formulae[@]}"
================================================
FILE: Scripts/build
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/build
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Builds the Swift package.
#
. "${0:A:h}/_setup_script"
print_notice '🏗 Building' "${@}"
export -r MAS_DISTRIBUTION="${1:-}"
swift build "${@:2}"
================================================
FILE: Scripts/clean
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/clean
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Deletes the build folder & other generated files.
#
. "${0:A:h}/_setup_script"
print_notice '🗑 Cleaning' "${@}"
zmodload zsh/zutil
zparseopts -D -A received_flag D P r x
if ! [[ -v 'received_flag[-P]' ]]; then
swift package "${${received_flag[-D]+clean}:-reset}"
fi
if [[ -v 'received_flag[-r]' && -e Package.resolved ]]; then
trash Package.resolved
fi
if [[ -v 'received_flag[-x]' && -d .swiftpm ]]; then
trash .swiftpm
fi
================================================
FILE: Scripts/format
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/format
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Automatically formats & fixes style violations using various tools.
#
# Please keep in sync with Scripts/lint.
#
. "${0:A:h}/_setup_script"
print_notice '🧹 Formatting' "${@}"
ensure_command_available markdownlint-cli2 swiftformat swiftlint
export -r MAS_DISTRIBUTION=format
printf -- $'--> 🕊 SwiftFormat\n'
script -q /dev/null swiftformat --strict --markdown-files format-strict . |
(grep -vxE '(?:\^D\x08{2})?Running SwiftFormat\.{3}\r|Reading (?:config|swift-version) file at .*|\x1b\[32mSwiftFormat completed in \d+(?:\.\d+)?s\.\x1b\[0m\r|0/\d+ files formatted\.\r' || true)
printf -- $'--> 🦅 SwiftLint\n'
swiftlint --fix --quiet --reporter relative-path
printf -- $'--> 〽️ Markdown\n'
# shellcheck disable=SC1036
markdownlint-cli2 --fix -- ***/*.md(.)
printf -- $'--> 🚷 Non-Executables\n'
readonly -a non_executables=(Scripts/***/*(N.^f+111))
if (("${#non_executables[@]}")); then
chmod -vv a+x "${non_executables[@]}"
exit 1
fi
================================================
FILE: Scripts/generate_manual
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/generate_manual
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Generates man page.
#
. "${0:A:h}/_setup_script"
print_notice '📖 Generating man pages for' "${@}"
export -r MAS_DISTRIBUTION="${1:-}"
swift package generate-manual "${@:2}"
================================================
FILE: Scripts/generate_token
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/generate_token
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Generates a GitHub App installation access token for GitHub Workflows.
#
. "${0:A:h}/_setup_script"
readonly header=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
readonly payload="${${$(printf '{"iss":%s,"iat":%s,"exp":%s}' "${TOKEN_APP_ID}" "$(("$(date +%s)" - 60))"\
"$(("$(date +%s)" + 540))" | base64)//[=$'\n']}//\/+/_-}"
# shellcheck disable=SC1036,SC1072,SC1073
curl\
-sX POST\
-H "Authorization: Bearer ${header}.${payload}.${${$(printf %s "${header}.${payload}" |
openssl dgst -sha256 -sign =(printf %s "${TOKEN_APP_PRIVATE_KEY}") | base64)//[=$'\n']}//\/+/_-}"\
-H 'Accept: application/vnd.github+json'\
"https://api.github.com/app/installations/${TOKEN_APP_INSTALLATION_ID}/access_tokens" |
jq -r .token
================================================
FILE: Scripts/lint
================================================
#!/bin/zsh -Ndfgku
#
# Scripts/lint
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Reports style violations without making any modifications to the code.
#
# Please keep in sync with Scripts/format.
#
# shellcheck disable=SC1036,SC1056,SC1072
. "${0:A:h}/_setup_script"
print_notice '🚨 Linting' "${@}"
ensure_command_available actionlint git markdownlint-cli2 shellcheck swiftformat swiftlint yamllint || exit
[[ "$(/usr/bin/arch)" = arm64 && "${$(sw_vers -productVersion)%%.*}" -ge 15 ]]
integer -r can_use_periphery="$((! ?))"
# shellcheck disable=SC1073,SC1083
((can_use_periphery)) && { ensure_command_available periphery || exit }
zmodload zsh/zutil
zparseopts -D -A received_flag P
export -r MAS_DISTRIBUTION=lint
integer exit_status=0
printf -- $'--> 🕊 SwiftFormat\n'
script -q /dev/null swiftformat --lint --markdown-files format-strict . |
(grep -vxE '(?:\^D\x08{2})?Running SwiftFormat\.{3}\r|\(lint mode - no files will be changed\.\)\r|Reading (?:config|swift-version) file at .*|\x1b\[32mSwiftFormat completed in \d+(?:\.\d+)?s\.\x1b\[0m\r|0/\d+ files require formatting\.\r|Source input did not pass lint check\.\r' || true)
((exit_status |= ${?}))
printf -- $'--> 🦅 SwiftLint\n'
swiftlint --strict --quiet --reporter relative-path
((exit_status |= ${?}))
if ((can_use_periphery)) && ! [[ -v 'received_flag[-P]' ]]; then
printf -- $'--> 🌀 Periphery\n'
periphery scan --exclude-tests |
(grep -vxE '(?:\x1b\[0;1;32m|\^D\x08{2})\* (?:\x1b\[0;0m\x1b\[0;1m)?No unused code detected\.(?:\x1b\[0;0m)?\r?' || true)
((exit_status |= ${?}))
printf -- $'--> 🌀 Periphery Tests\n'
periphery scan |
(grep -vxE '(?:\x1b\[0;1;32m|\^D\x08{2})\* (?:\x1b\[0;0m\x1b\[0;1m)?No unused code detected\.(?:\x1b\[0;0m)?\r?' || true)
((exit_status |= ${?}))
fi
printf -- $'--> 〽️ Markdown\n'
markdownlint-cli2 -- ***/*.md(.)
((exit_status |= ${?}))
printf -- $'--> 📝 YAML\n'
yamllint -s .
((exit_status |= ${?}))
printf -- $'--> 🌳 Git\n'
git diff --check
((exit_status |= ${?}))
printf -- $'--> 💤 Zsh\n'
for script in Scripts/***/*(.); do
/bin/zsh -n "${script}"
((exit_status |= ${?}))
done
printf -- $'--> 🐙 ActionLint\n'
actionlint -shellcheck shellcheck
((exit_status |= ${?}))
printf -- $'--> 🐚 ShellCheck\n'
shellcheck -s bash -o all -e SC1009,SC1088,SC2296,SC2298,SC2299,SC2300,SC2301,SC2312 -a -P SCRIPTDIR Scripts/***/*(.)
((exit_status |= ${?}))
printf -- $'--> 🚷 Non-Executables\n'
readonly -a non_executables=(Scripts/***/*(N.^f+111))
if (("${#non_executables[@]}")); then
printf $'%s\n' "${non_executables[@]}"
((exit_status |= 1))
fi
exit "${exit_status}"
================================================
FILE: Scripts/package
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/package
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Builds .pkg installer.
#
. "${0:A:h}/_setup_script"
print_notice '📦 Packaging installer for' "${@}"
export MAS_DO_NOT_PRINT_NOTICE=
Scripts/build "${1:-}" -c release "${@:2}"
unset MAS_DO_NOT_PRINT_NOTICE
readonly build_folder=.build
readonly destination_folder="${build_folder}/destination"
readonly installation_folder=/usr/local/opt/mas
readonly installation_staging_folder="${destination_folder}${installation_folder}"
readonly usr_local_bin_staging_folder="${destination_folder}/usr/local/bin"
version="$(Scripts/version)"
readonly version
swift package generate-manual
mkdir -p "${installation_staging_folder}/bin"
mkdir -p "${installation_staging_folder}/etc/bash_completion.d"
mkdir -p "${installation_staging_folder}/share/fish/vendor_completions.d"
mkdir -p "${installation_staging_folder}/share/man/man1"
mkdir -p "${usr_local_bin_staging_folder}"
cp LICENSE README.md "${installation_staging_folder}"
cp contrib/completion/mas-completion.bash "${installation_staging_folder}/etc/bash_completion.d/mas"
cp contrib/completion/mas.fish "${installation_staging_folder}/share/fish/vendor_completions.d/mas.fish"
ln -f "$(swift build -c release --show-bin-path "${@:2}")/mas" "${installation_staging_folder}/bin/mas"
ln -f .build/plugins/GenerateManual/outputs/mas/mas.1 "${installation_staging_folder}/share/man/man1/mas.1"
ln -fs "${installation_folder}/bin/mas" "${usr_local_bin_staging_folder}/mas"
archs=("${(s: :n)$(lipo -archs "${installation_staging_folder}/bin/mas")}")
# shellcheck disable=SC2034
readonly -a archs
pkgbuild\
--identifier io.github.mas-cli.mas\
--install-location /\
--version "${version}"\
--root "${destination_folder}"\
"${build_folder}/mas.pkg"
# shellcheck disable=SC1036
productbuild\
--distribution =(<<<\
'<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
<title>mas</title>
<options customize="never" require-scripts="false" hostArchitectures="'"${(j:,:)archs[@]}"'"/>
<volume-check>
<allowed-os-versions>
<os-version min="13"/>
</allowed-os-versions>
</volume-check>
<choices-outline>
<line choice="mas"/>
</choices-outline>
<choice id="mas" title="mas" visible="false">
<pkg-ref id="mas">mas.pkg</pkg-ref>
</choice>
</installer-gui-script>'
)\
--package-path "${build_folder}"\
"${build_folder}/mas-${version//\//_}-${(j:-:)archs[@]}.pkg"
================================================
FILE: Scripts/prebuild
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/prebuild
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Prebuilds the Swift package.
#
. "${0:A:h}/_setup_script"
print_notice '🎬 Prebuilding' "${@}"
# Generate Swift file containing build information.
# shellcheck disable=SC1102
printf '//
// MAS+BuildInformation.swift
// mas
//
// Copyright © %s mas-cli. All rights reserved.
//
extension MAS {
static let version = "%s"
static let distribution = "%s"
static let gitOrigin = "%s"
static let gitRevision = "%s"
static let swiftVersion = "%s"
static let swiftDriverVersion = "%s"
}
'\
"$(date +%Y)"\
"$(Scripts/version)"\
"${MAS_DISTRIBUTION:-unknown}"\
"$(git remote get-url origin)"\
"$(git rev-parse HEAD)"\
"${${${$(swift --version 2>/dev/null)##( |[[:alpha:]])##}%%$'\n'*}:-unknown}"\
"${${(SM)$(swift --version 2>&1 >/dev/null)##[[:digit:]]([[:digit:]]|.)##}:-unknown}"\
>"${1}/MAS+BuildInformation.swift"
================================================
FILE: Scripts/release_cancel
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/release_cancel
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Cancels a GitHub draft release.
#
# Usage: release_cancel <draft-release-tag>
#
. "${0:A:h}/_setup_script"
readonly tag="${1}"
export MAS_VERSION="${tag#v}"
print_notice '❌ Canceling release for' "${@}"
unset MAS_VERSION
bump_url="$(gh release -R https://github.com/mas-cli/mas download "${tag}" -p bump.url -O - 2>/dev/null || true)"
readonly bump_url
if [[ -n "${bump_url}" ]]; then
printf $'\n'
gh pr close "${bump_url}" -d
printf $'\n'
else
printf $'\nNo tap formula bump PR URL found for draft release tag %s\n\n' "${tag}"
fi
gh release -R https://github.com/mas-cli/mas delete "${tag}" --cleanup-tag -y
================================================
FILE: Scripts/release_start
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/release_start
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Starts the release process by creating & pushing a signed annotated version tag to the GitHub mas-cli/mas repo.
#
# Usage: release_start <version-tag-name> <version-title> [<version-ref>]
#
# <version-tag-name> must match the following zsh pattern:
#
# ^v[[:digit:]]+(\.[[:digit:]]+)*(-(alpha|beta|rc)\.[[:digit:]]+)?$
#
# <version-title> must may contain at most 64 characters, which may be only visible characters or spaces
#
# <version-ref> if optional value supplied, must be on the main branch; defaults to HEAD
#
. "${0:A:h}/_setup_script"
readonly tag="${1}"
readonly title="${2}"
readonly ref="${3:-HEAD}"
export MAS_VERSION="${tag#v}"
print_notice '🚀 Starting release for' "${@}"
unset MAS_VERSION
printf $'\n'
if [[ ! "${tag}" =~ '^v[[:digit:]]+(\.[[:digit:]]+)*(-(alpha|beta|rc)\.[[:digit:]]+)?$' ]]; then
printf $'%s is not a valid version tag\n' "${tag}" >&2
exit 1
fi
if (("${#title}" > 64)); then
printf $'\'%s\' is too long for a version title, which may contain at most 64 characters\n' "${(q)title}" >&2
exit 2
fi
if [[ "${title}" =~ [[:cntrl:]$'\t\n\r'] ]]; then
printf $'\'%s\' is not a valid version title, which may contain only visible characters or spaces\n' "${(q)title}" >&2
exit 3
fi
# shellcheck disable=SC1027,SC1036,SC1072,SC1073
if [[ "${title}" = (' '*|*' ') ]]; then
printf $'\'%s\' is not a valid version title, which may not begin or end with a space\n' "${(q)title}" >&2
exit 4
fi
if ! git merge-base --is-ancestor "${ref}" upstream/main; then
printf $'%s is not a valid reference for a version, which must be on the upstream/main branch\n' "${ref}" >&2
exit 5
fi
git tag -s "${tag}" -m "${title}" "${ref}"
printf $'Created version tag %s with title \'%s\' for reference %s\n\n' "${tag}" "${(q)title}" "${ref}"
git push upstream tag "${tag}"
================================================
FILE: Scripts/setup_workflow_repo
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/setup_workflow_repo
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Sets up the repo for use in a GitHub workflow.
#
. "${0:A:h}/_setup_script"
# shellcheck disable=SC2066
for branch in "${(f)"$(git for-each-ref refs/remotes/origin --format='%(if)%(symref)%(then)%(else)%(refname:strip=-1)%(end)')":#}"; do
git branch --track "${branch}" "origin/${branch}" >/dev/null 2>&1 || true
done
================================================
FILE: Scripts/test
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/test
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Runs mas tests.
#
. "${0:A:h}/_setup_script"
print_notice '🧪 Testing' "${@}"
export -r MAS_DISTRIBUTION=test
swift test --disable-xctest -q "${@}"
================================================
FILE: Scripts/update_dependencies
================================================
#!/bin/zsh -GNdefgku
#
# Scripts/update_dependencies
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Update dependencies.
#
. "${0:A:h}/_setup_script"
print_notice '⬆️ Updating dependencies for' "${@}"
printf -- $'--> 🍺 Homebrew\n'
export MAS_DO_NOT_PRINT_NOTICE=
Scripts/bootstrap
unset MAS_DO_NOT_PRINT_NOTICE
printf -- $'--> 🕊 Swift\n'
swift package update
================================================
FILE: Scripts/update_headers
================================================
#!/bin/zsh -GNdefgku
#
# Scripts/update_headers
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Generates headers for Apple private frameworks.
#
. "${0:A:h}/_setup_script"
ensure_command_available ipsw
zmodload zsh/zutil
zparseopts -D -A received_flag C X
readonly -a frameworks_globs=("${@:-*(/)}")
cd Sources/PrivateFrameworks/include
for framework in ${~frameworks_globs[@]}; do
! [[ -v 'received_flag[-X]' ]] && ipsw class-dump --headers --output . /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e -- "${framework}"
# shellcheck disable=SC1046,SC1047,SC1072,SC1073
if ! [[ -v 'received_flag[-C]' ]]; then
cd -- "${framework}"
# shellcheck disable=SC1036
sed -Ei ''\
-e 's!^// -!// -!g'\
-e 's!^// !//!g'\
-e '/^#(define|endif|ifndef).*/d'\
-e '/^ *\/\*.*\*\/$/d'\
-e '/^@import Foundation;$/d'\
-e 's/^ /\t/g'\
-e 's/_Bool/BOOL/g'\
-e 's/^- \(id\)description;$/- \(nonnull NSString \*\)description;/g'\
-e 's/^- \(void\)encodeWithCoder:\(id\)coder;$/- \(void\)encodeWithCoder:\(nullable NSCoder \*\)coder;/g'\
-e 's/^- \(id\)initWithCoder:\(id\)coder;$/- \(nonnull instancetype\)initWithCoder:\(nullable NSCoder \*\)coder;/g'\
-e 's/^- \(BOOL\)isEqual:\(id\)equal;$/- \(BOOL\)isEqual:\(nullable id\)object;/g'\
-e 's/^\+ \(id\)interface;$/\+ \(nonnull NSXPCInterface \*\)interface;/g'\
-e 's/\(copy\)/\(copy, nullable\)/g'\
-e 's/\(copy, nonatomic\)/\(copy, nonatomic, nullable\)/g'\
-e 's/\(id\)client/\(nullable ISStoreClient \*\)client/g'\
-e 's/\(id\)dictionary/\(nullable NSDictionary \*\)dictionary/g'\
-e 's/\(id\)init/\(nonnull instancetype)init/g'\
-e 's/\(retain\)/\(retain, nullable\)/g'\
-e 's/\(retain, nonatomic\)/\(retain, nonatomic, nullable\)/g'\
-e 's/\(id\)dsid/\(nullable NSNumber \*\)dsID/g'\
-e 's/NSArray \*downloads/NSArray\<SSDownload \*\> \*downloads/g'\
-e 's/\(id\)productID/\(nonnull NSNumber \*\)productID/g'\
-e 's/- \(id\)copyWithZone:\(struct _NSZone \*\)zone;/- \(nonnull instancetype\)copyWithZone:\(nullable struct _NSZone \*\)zone;/g'\
-e 's/\(id\)progress/\(nullable SSOperationProgress \*\)progress/g'\
-e 's/\(id\)url/\(nullable NSURL \*\)url/g'\
-e 's/:\(id\)error/:\(nullable NSError \*\)error/g'\
-e 's/NSObject\<OS_dispatch_queue\> \*/dispatch_queue_t /g'\
-e 's/NSObject\<OS_dispatch_source\> \*/dispatch_source_t /g'\
-e 's!id /\* block \*/!UnknownBlock!g'\
***/*.h(.N)
for header in ***/*.h(.N); do
sed -i '' -e ':a' -e '/^\n*$/{$d;N;ba' -e '}' "${header}"
done
cd ..
fi
done
================================================
FILE: Scripts/version
================================================
#!/bin/zsh -Ndefgku
#
# Scripts/version
# mas
#
# Copyright © 2025 mas-cli. All rights reserved.
#
# Outputs the mas version.
#
. "${0:A:h}/_setup_script"
branch="${"$(git rev-parse --abbrev-ref HEAD)":/main}"
if [[ "${branch}" = HEAD ]]; then
if ! git show-ref --verify --quiet refs/heads/main || git merge-base --is-ancestor HEAD main; then
readonly branch=
else
readonly branch="${"${"${(fnO)"$(git branch --contains HEAD --format '%(ahead-behind:HEAD) %(refname:short)')"}"[1]}"##* }"
fi
else
readonly branch
fi
printf $'%s%s%s\n'\
"${"$(git describe --tags 2>/dev/null)"#v}"\
"${branch:+"-${branch}"}"\
"${"$(git diff-index HEAD --;git ls-files --exclude-standard --others)":+"${MAS_DIRTY_INDICATOR-+}"}"
================================================
FILE: Sources/PrivateFrameworks/PrivateFrameworks.c
================================================
// Xcode will not build without this file causing an object file to be compiled.
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKDownloadDirectory.h
================================================
//
// CKDownloadDirectory.h
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
NSString * _Nonnull CKDownloadDirectory(NSString * _Nullable target);
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface CKDownloadQueue : CKServiceInterface {
NSMutableDictionary *_downloadsByItemID;
NSLock *_downloadsLock;
id _observerToken;
NSLock *_tokenLock;
}
+ (nonnull instancetype)sharedDownloadQueue;
@property (retain, nonatomic, nullable) NSMutableDictionary<NSString *, CKDownloadQueueClient *> *downloadQueueObservers; // <NSString * _Nonnull, CKDownloadQueueClient * _Nonnull>
@property (readonly, nonatomic, nullable) NSArray<SSDownload *> *downloads; // Unverified generic type
@property (retain, nonatomic, nullable) CKDownloadQueueClient *sharedObserver;
- (void)addDownload:(nonnull SSDownload *)download;
- (nonnull NSString *)addObserver:(nullable id <CKDownloadQueueObserver>)observer;
- (nonnull NSString *)addObserver:(nullable id <CKDownloadQueueObserver>)observer forDownloadTypes:(long long)downloadTypes;
- (nonnull NSString *)addObserverForDownloadTypes:(long long)downloadTypes withBlock:(nullable UnknownBlock)block;
- (BOOL)cacheReceiptDataForDownload:(nullable SSDownload *)download;
- (void)cancelDownload:(nullable SSDownload *)download promptToConfirm:(BOOL)promptToConfirm askToDelete:(BOOL)askToDelete;
- (void)checkStoreDownloadQueueForAccount:(nullable ISStoreAccount *)account; // Unverified account type
- (void)connectionWasInterrupted;
- (nullable SSDownload *)downloadForItemIdentifier:(unsigned long long)identifier; // Unverified return type
- (void)fetchIconForItemIdentifier:(unsigned long long)identifier atURL:(nullable NSURL *)url replyBlock:(nonnull UnknownBlock)block;
- (nonnull instancetype)initWithStoreClient:(nullable ISStoreClient *)client; // Unverified client type
- (void)lockApplicationsForBundleID:(nullable NSString *)bundleID; // Unverified bundleID type
- (void)lockedApplicationTriedToLaunchAtPath:(nullable NSString *)path; // Unverified path type
- (void)pauseDownloadWithItemIdentifier:(unsigned long long)identifier;
- (void)performedIconAnimationForDownloadWithIdentifier:(unsigned long long)identifier;
- (void)removeDownloadWithItemIdentifier:(unsigned long long)identifier;
- (void)removeObserver:(nullable NSString *)observerUUID; // Unverified observerUUID type
- (void)resumeDownloadWithItemIdentifier:(unsigned long long)identifier;
- (void)unlockApplicationsWithBundleIdentifier:(nullable NSString *)bundleID; // Unverified bundleID type
@end
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueueObserver-Protocol.h
================================================
//
// CKDownloadQueueObserver-Protocol.h
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
@protocol CKDownloadQueueObserver
@required
- (void)downloadQueue:(nonnull CKDownloadQueue *)queue changedWithAddition:(nonnull SSDownload *)download;
- (void)downloadQueue:(nonnull CKDownloadQueue *)queue changedWithRemoval:(nonnull SSDownload *)download;
- (void)downloadQueue:(nonnull CKDownloadQueue *)queue statusChangedForDownload:(nonnull SSDownload *)download;
@optional
@end
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
typedef void (^SSPurchaseCompletion)(SSPurchase * _Nonnull purchase, BOOL completed, NSError * _Nullable error, SSPurchaseResponse * _Nullable response);
@interface CKPurchaseController : CKServiceInterface {
NSArray *_adoptionEligibleItems;
NSNumber *_adoptionErrorNumber;
NSNumber *_adoptionServerStatus;
NSMutableArray *_purchases;
NSMutableArray *_rejectedPurchases;
}
+ (void)setNeedsSilentMachineAuthorization:(BOOL)needsSilentMachineAuthorization;
+ (nonnull instancetype)sharedPurchaseController;
@property (copy, nullable) void (^dialogHandler)(CKDialog * _Nullable); // Unverified type
- (void)_performVPPReceiptRenewal;
- (BOOL)adoptionCompletedForBundleID:(nullable NSString *)bundleID;
- (void)cancelPurchaseWithProductID:(nullable NSNumber *)productID;
- (void)checkServerDownloadQueue;
- (void)performPurchase:(nonnull SSPurchase *)purchase withOptions:(unsigned long long)options completionHandler:(nullable SSPurchaseCompletion)handler;
- (nullable SSPurchase *)purchaseInProgressForProductID:(nullable NSNumber *)productID; // Unverified return type
- (nullable NSArray<SSPurchase *> *)purchasesInProgress; // Unverified return type
- (void)resumeDownloadForPurchasedProductID:(nullable NSNumber *)productID; // Unverified productID type
- (void)startPurchases:(nullable NSArray<SSPurchase *> *)purchases shouldStartDownloads:(BOOL)shouldStartDownloads eventHandler:(nullable void (^)(NSArray<SSPurchase *> * _Nonnull))handler; // Unverified purchases generic type / handler type
- (void)startPurchases:(nullable NSArray<SSPurchase *> *)purchases withOptions:(unsigned long long)options completionHandler:(nullable void (^)(NSArray<SSPurchase *> * _Nonnull))handler; // Unverified purchases type / handler parameter type
@end
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface CKServiceInterface : ISServiceProxy
@end
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@import StoreFoundation;
@class CKDialog, CKDownloadQueueClient;
@protocol CKDownloadQueueObserver;
#import "CKServiceInterface.h"
#import "CKDownloadDirectory.h"
#import "CKDownloadQueue.h"
#import "CKDownloadQueueObserver-Protocol.h"
#import "CKPurchaseController.h"
================================================
FILE: Sources/PrivateFrameworks/include/CommerceKit/module.modulemap
================================================
module CommerceKit [system] [no_undeclared_includes] {
requires macos, objc
use StoreFoundation
link framework "CommerceKit"
umbrella header "CommerceKit.h"
export *
}
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@protocol ISAccountService <ISServiceRemoteObject>
@required
- (void)accountWithAppleID:(nullable NSString *)appleID replyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified appleID type / block parameter types
- (void)accountWithDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified dsID type / block parameter types
- (void)addAccount:(nullable ISStoreAccount *)account; // Unverified account type
- (void)addAccountStoreObserver:(nullable id <ISAccountStoreObserver>)observer; // Unverified observer type
- (void)addURLBagObserver:(nullable id <ISURLBagObserver>)observer;
- (void)authIsExpiredWithReplyBlock:(nonnull void (^)(BOOL))block; // Unverified block parameter types
- (void)dictionaryForDSID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(NSDictionary * _Nullable))block; // Unverified dsID type / block parameter types
- (void)dictionaryWithReplyBlock:(nonnull void (^)(NSDictionary * _Nonnull))block; // Unverified block parameter types
- (void)generateTouchIDHeadersForDSID:(nullable NSNumber *)dsID challenge:(nullable NSString *)challenge caller:(nullable id)caller replyBlock:(nonnull void (^)(NSDictionary * _Nonnull, NSError * _Nullable))block; // Unverified dsID type / challenge type / caller type / block parameter types
- (void)getTouchIDPreferenceWithReplyBlock:(nonnull void (^)(BOOL, ISStoreAccount * _Nullable, NSError * _Nullable))block; // Unverified block parameter types
- (void)httpHeadersForURL:(nullable NSURL *)url forDSID:(nullable NSNumber *)dsID includeADIHeaders:(BOOL)includeADIHeaders withReplyBlock:(nonnull void (^)(NSDictionary * _Nonnull))block; // Unverified url type / dsID type / block parameter types
- (void)iCloudDSIDReplyBlock:(nonnull void (^)(NSString * _Nullable))block; // Unverified block parameter types
- (void)invalidateAllBags;
- (void)isValidWithReplyBlock:(nonnull void (^)(BOOL))block; // Unverified block parameter types
- (void)loadURLBagWithType:(unsigned long long)type replyBlock:(nonnull void (^)(BOOL, BOOL, NSError * _Nullable))block; // Unverified block parameter types
- (void)needsSilentADIActionForURL:(nullable NSURL *)url dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified url type / dsID type / block parameter types
- (void)needsSilentADIActionForURL:(nullable NSURL *)url withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified url type / block parameter types
- (void)primaryAccountWithReplyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified block parameter types
- (void)processURLResponse:(nullable NSURLResponse *)urlResponse forRequest:(nullable NSURLRequest *)request; // Unverified urlResponse type / request type
- (void)processURLResponse:(nullable NSURLResponse *)urlResponse forRequest:(nullable NSURLRequest *)request dsID:(nullable NSNumber *)dsID; // Unverified urlResponse type / request type / dsID type
- (void)regexWithKey:(nullable NSString *)key dsID:(nullable NSNumber *)dsID matchesString:(nullable NSString *)string replyBlock:(nonnull void (^)(BOOL))block; // Unverified key type / dsID type / string type / block parameter types
- (void)regexWithKey:(nullable NSString *)key matchesString:(nullable NSString *)string replyBlock:(nonnull void (^)(BOOL))block; // Unverified key type / string type / block parameter types
- (void)removeAccountStoreObserver:(nullable id <ISAccountStoreObserver>)observer; // Unverified observer type
- (void)removeURLBagObserver:(nullable id <ISURLBagObserver>)observer; // Unverified observer type
- (void)retailStoreDemoModeReplyBlock:(nonnull void (^)(BOOL, NSString * _Nullable, NSString * _Nullable, BOOL))block; // Unverified block parameter types
- (void)setStoreFrontID:(nullable NSString *)storefrontID; // Unverified storefrontID type
- (void)setTouchIDState:(long long)touchIDState forDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(BOOL, NSError * _Nullable))block; // Unverified dsID type / block parameter types
- (void)shouldSendGUIDWithRequestForURL:(nullable NSURL *)url withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified url type / block parameter types
- (void)signOut;
- (void)storeFrontWithReplyBlock:(nonnull void (^)(NSString * _Nonnull))block; // Unverified block parameter types
- (void)updateTouchIDSettingsForDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(BOOL, NSError * _Nullable))block; // Unverified dsID type / block parameter types
- (void)urlIsTrustedByURLBag:(nullable NSURL *)urlBag dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified urlBag type / dsID type / block parameter types
- (void)urlIsTrustedByURLBag:(nullable NSURL *)urlBag withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified urlBag type / block parameter types
- (void)valueForURLBagKey:(nullable NSString *)bagKey dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(NSURL * _Nullable))block; // Unverified bagKey type / dsID type / block parameter types
- (void)valueForURLBagKey:(nullable NSString *)bagKey withReplyBlock:(nonnull void (^)(NSURL * _Nullable))block; // Unverified bagKey type / block parameter types
@optional
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface ISServiceProxy : NSObject
+ (nonnull instancetype)genericSharedProxy;
+ (void)initialize;
@property (readonly, nonatomic, nonnull) id <ISAccountService> accountService;
@property (readonly, nonatomic, nonnull) id <ISAssetService> assetService;
@property (readonly, nonatomic, nonnull) id <ISDownloadService> downloadService;
@property (readonly, nonatomic, weak, nullable) id <ISServiceRemoteObject> exportedObject;
@property (readonly, nonatomic, nullable) Protocol *exportedProtocol;
@property (retain, nonatomic, nullable) ISStoreClient *storeClient;
@property (readonly, nonatomic, nonnull) id <ISTransactionService> transactionService;
@property (readonly, nonatomic, nonnull) id <ISUIService> uiService;
- (void)accountServiceSynchronousBlock:(nonnull UnknownBlock)block;
- (nonnull id <ISAccountService>)accountServiceWithErrorHandler:(nullable UnknownBlock)handler;
- (void)assetServiceSynchronousBlock:(nonnull UnknownBlock)block;
- (nonnull id <ISAssetService>)assetServiceWithErrorHandler:(nullable UnknownBlock)handler;
- (void)connectionWasInterrupted;
- (nonnull NSXPCConnection *)connectionWithServiceName:(nonnull NSString *)serviceName protocol:(nonnull Protocol *)protocol isMachService:(BOOL)isMachService;
- (void)downloadServiceSynchronousBlock:(nonnull UnknownBlock)block;
- (nonnull id <ISDownloadService>)downloadServiceWithErrorHandler:(nullable UnknownBlock)handler;
- (nonnull instancetype)initWithStoreClient:(nullable ISStoreClient *)client; // Unverified client type
- (nonnull id)objectProxyForServiceName:(nonnull NSString *)serviceName protocol:(nonnull id)protocol interfaceClassName:(nullable NSString *)interfaceClassName isMachService:(BOOL)isMachService errorHandler:(nullable UnknownBlock)handler;
- (void)performSynchronousBlock:(nonnull UnknownBlock)block withServiceName:(nonnull NSString *)serviceName protocol:(nonnull Protocol *)protocol isMachService:(BOOL)isMachService interfaceClassName:(nullable NSString *)interfaceClassName;
- (void)registerForInterrptionNotification;
- (void)transactionServiceSynchronousBlock:(nonnull UnknownBlock)block;
- (nonnull id <ISTransactionService>)transactionServiceWithErrorHandler:(nullable UnknownBlock)handler;
- (void)uiServiceSynchronousBlock:(nonnull UnknownBlock)block;
- (nonnull id <ISUIService>)uiServiceWithErrorHandler:(nullable UnknownBlock)handler;
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface ISStoreAccount : NSObject <NSSecureCoding> {
NSTimer *_tokenInvalidTimer;
}
+ (nullable NSNumber *)dsidFromPlistValue:(nullable id)value;
+ (nonnull NSDictionary *)migratePersistedStoreDictionary:(nullable NSDictionary *)dictionary;
+ (BOOL)supportsSecureCoding;
@property long long URLBagType;
@property (readonly, getter=isAuthenticated) BOOL authenticated;
@property (copy, nullable) NSString *creditString;
@property (copy, nullable) NSNumber *dsID;
@property (copy, nullable) NSString *identifier;
@property BOOL isManagedStudent;
@property BOOL isSignedIn;
@property long long kind;
@property (copy, nullable) NSString *password;
@property (readonly, getter=isPrimary) BOOL primary;
@property (retain, nullable) NSString *storeFront;
@property (copy, nullable) NSString *token;
@property (retain, nullable) NSTimer *tokenExpirationTimer;
@property (retain, nullable) NSDate *tokenIssuedDate;
@property long long touchIDState;
- (nonnull NSString *)description;
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (long long)getTouchIDState;
- (BOOL)hasValidStrongToken;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithPersistedStoreDictionary:(nullable NSDictionary *)dictionary;
- (void)mergeValuesFromAuthenticationResponse:(nullable ISAuthenticationResponse *)response;
- (nonnull NSDictionary<NSString *, NSNumber *> *)persistedStoreDictionary;
- (void)resetTouchIDState;
- (double)strongTokenValidForSecond;
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSDownload : NSObject <NSSecureCoding> {
BOOL _needsPreInstallValidation;
}
+ (BOOL)supportsSecureCoding;
@property (copy, nullable) NSNumber *accountDSID;
@property (copy, nonatomic, nullable) NSArray<SSDownloadAsset *> *assets; // Unverified generic type
@property (copy, nullable) NSString *cancelURLString;
@property (copy, nullable) NSString *customDownloadPath;
@property BOOL didAutoUpdate;
@property unsigned long long downloadType;
@property BOOL installAfterLogout;
@property (copy, nullable) NSString *installPath;
@property BOOL isInServerQueue;
@property (copy, nonatomic, nullable) SSDownloadMetadata *metadata;
@property BOOL needsDisplayInDock;
@property (copy, nullable) NSURL *relaunchAppWithBundleURL;
@property BOOL skipAssetDownloadIfNotAlreadyOnDisk;
@property BOOL skipInstallPhase;
@property (retain, nonatomic, nullable) SSDownloadStatus *status;
- (void)cancel;
- (void)cancelWithPrompt:(BOOL)prompt;
- (void)cancelWithPrompt:(BOOL)prompt storeClient:(nullable ISStoreClient *)client;
- (void)cancelWithStoreClient:(nullable ISStoreClient *)client;
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)init;
- (nonnull instancetype)initWithAssets:(nullable NSArray<SSDownloadAsset *> *)assets metadata:(nullable SSDownloadMetadata *)metadata; // Unverified assets type / metadata type
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (BOOL)isEqual:(nullable id)object;
- (void)pause;
- (void)pauseWithStoreClient:(nullable ISStoreClient *)client;
- (nullable SSDownloadAsset *)primaryAsset;
- (void)resume;
- (void)resumeWithStoreClient:(nullable ISStoreClient *)client;
- (void)setUseUniqueDownloadFolder:(BOOL)useUniqueDownloadFolder;
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSDownloadMetadata : NSObject <NSSecureCoding, NSCopying> {
NSLock *_lock;
}
+ (BOOL)supportsSecureCoding;
@property (readonly, nonnull) NSNumber *ageRestriction;
@property BOOL animationExpected;
@property (retain, nullable) NSString *appleID;
@property (readonly, nonnull) NSString *applicationIdentifier;
@property BOOL artworkIsPrerendered;
@property (readonly, nonnull) NSArray<SSDownloadAsset *> *assets; // Unverified generic type
@property (readonly, nullable) NSString *bundleDisplayName;
@property (retain, nullable) NSString *bundleIdentifier;
@property (readonly, nullable) NSString *bundleShortVersionString;
@property (retain, nullable) NSString *bundleVersion;
@property (retain, nullable) NSString *buyParameters;
@property (readonly, nullable) NSNumber *collectionID;
@property (retain, nullable) NSString *collectionName;
@property (retain, nullable) NSDictionary *dictionary;
@property (retain, nullable) NSString *downloadKey;
@property (retain, nullable) NSNumber *durationInMilliseconds;
@property (retain, nullable) NSData *epubRightsData;
@property (readonly) BOOL extractionCanBeStreamed;
@property (retain, nullable) NSString *fileExtension;
@property (retain, nullable) NSString *genre;
@property (readonly, nullable) NSNumber *iapContentSize;
@property (readonly, nullable) NSString *iapContentVersion;
@property (retain, nullable) NSString *iapInstallPath;
@property (retain, nullable) NSData *ipaInstallBookmarkData NS_AVAILABLE_MAC(14);
@property (retain, nullable) NSString *ipaInstallPath;
@property (readonly) BOOL isExplicitContents;
@property BOOL isMDMProvided;
@property unsigned long long itemIdentifier;
@property (retain, nullable) NSString *kind;
@property (retain, nullable) NSString *managedAppUUIDString;
@property (readonly) BOOL needsSoftwareInstallOperation;
@property (retain, nullable) NSURL *preflightPackageURL;
@property (retain, nullable) NSString *productType;
@property (readonly, nullable) NSString *purchaseDate;
@property (getter=isRental) BOOL rental;
@property (readonly, getter=isSample) BOOL sample;
@property (retain, nullable) NSArray<NSMutableDictionary<NSString *, id> *> *sinfs;
@property (readonly, nullable) NSString *sortArtist;
@property (readonly, nullable) NSString *sortName;
@property (retain, nullable) NSString *subtitle;
@property (retain, nullable) NSURL *thumbnailImageURL;
@property (retain, nullable) NSString *title;
@property (retain, nullable) NSString *transactionIdentifier;
@property (readonly, nullable) NSNumber *uncompressedSize;
@property (retain, nullable) NSNumber *version;
- (nullable id)_valueForFirstAvailableKey:(nullable id)key;
- (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone;
- (nullable NSDictionary *)deltaPackages; // Unverified return type
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)init;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithDictionary:(nullable NSDictionary *)dictionary;
- (nonnull instancetype)initWithKind:(nullable NSString *)kind;
- (nullable id)localServerInfo;
- (void)setValue:(nullable id)value forMetadataKey:(nonnull NSString *)key; // Unverified key type
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSDownloadPhase : NSObject <NSSecureCoding, NSCopying>
+ (BOOL)supportsSecureCoding;
@property (readonly) double estimatedSecondsRemaining;
@property (readonly, nullable) SSOperationProgress *operationProgress;
@property (readonly) long long phaseType;
@property (readonly) float progressChangeRate;
@property (readonly) long long progressUnits;
@property (readonly) long long progressValue;
@property (readonly) long long totalProgressValue;
- (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone;
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)init;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; // Unverified coder type
- (nonnull instancetype)initWithOperationProgress:(nullable SSOperationProgress *)progress; // Unverified progress type
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSDownloadStatus : NSObject <NSSecureCoding>
+ (BOOL)supportsSecureCoding;
@property (readonly, nonatomic, nullable) SSDownloadPhase *activePhase;
@property (nonatomic, getter=isCancelled) BOOL cancelled;
@property (retain, nonatomic, nullable) NSError *error;
@property (nonatomic, getter=isFailed) BOOL failed;
@property (readonly, nonatomic, getter=isPausable) BOOL pausable;
@property (nonatomic, getter=isPaused) BOOL paused;
@property (readonly, nonatomic) float percentComplete;
@property (readonly, nonatomic) float phasePercentComplete;
@property (readonly, nonatomic) long long phaseTimeRemaining;
@property BOOL waiting;
- (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone;
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (void)setOperationProgress:(nullable SSOperationProgress *)progress; // Unverified progress type
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSPurchase : NSObject <NSSecureCoding, NSCopying>
+ (nonnull instancetype)purchaseWithBuyParameters:(nullable NSString *)buyParameters; // Unverified buyParameters type
+ (nonnull NSArray<NSArray<SSPurchase *> *> *)purchasesGroupedByAccountIdentifierWithPurchases:(nullable NSArray<SSPurchase *> *)purchases;
+ (BOOL)supportsSecureCoding;
@property (retain, nonatomic, nullable) NSNumber *accountIdentifier;
@property (retain, nonatomic, nullable) NSString *appleID;
@property (copy, nullable) UnknownBlock authFallbackHandler; // Unverified value type
@property (copy, nonatomic, nullable) NSString *buyParameters;
@property BOOL checkPreflightAterPurchase;
@property (copy, nonatomic, nullable) SSDownloadMetadata *downloadMetadata;
@property (retain, nullable) NSDictionary *dsidLessOptions;
@property BOOL isCancelled;
@property BOOL isDSIDLessPurchase;
@property BOOL isRecoveryPurchase;
@property BOOL isRedownload;
@property BOOL isUpdate;
@property BOOL isVPP;
@property unsigned long long itemIdentifier;
@property (retain, nonatomic, nullable) NSString *managedAppUUIDString;
@property (readonly) BOOL needsAuthentication;
@property (retain, nonatomic, nullable) NSString *parentalControls;
@property (weak, nullable) ISOperation *purchaseOperation;
@property (nonatomic) long long purchaseType;
@property (retain, nonatomic, nullable) NSData *receiptData;
@property (copy, nullable) NSDictionary *responseDialog;
@property BOOL shouldBeInstalledAfterLogout;
@property (readonly, nonatomic, nullable) NSString *sortableAccountIdentifier;
@property (readonly, nonatomic, nonnull) NSString *uniqueIdentifier;
- (nullable NSString *)_sortableAccountIdentifier;
- (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone;
- (nonnull NSString *)description;
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (nonnull NSNumber *)productID;
- (BOOL)purchaseDSIDMatchesPrimaryAccount;
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@interface SSPurchaseResponse : NSObject <NSSecureCoding> {
NSDictionary *_rawResponse;
}
+ (BOOL)supportsSecureCoding;
@property (retain, nullable) NSArray<SSDownload *> *downloads;
@property (retain, nullable) NSDictionary<NSString *, id> *metrics;
- (nonnull NSMutableArray<SSDownload *> *)_newDownloadsFromItems:(nullable NSArray<NSDictionary *> *)items withDSID:(nullable NSNumber *)dsID; // Unverified items element generic types / dsID type
- (void)encodeWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder;
- (nonnull instancetype)initWithDictionary:(nullable NSDictionary *)dictionary userIdentifier:(nullable NSString *)userIdentifier; // Unverified dictionary generic types / userIdentifier type
@end
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h
================================================
//
// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)
//
// - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.2, SDK: 26.2, Tool: ld (1230.3)
// - LC_SOURCE_VERSION: 716.2.2.0.0
//
@import Foundation;
@class ISAuthenticationContext, ISAuthenticationResponse, ISOperation, ISStoreClient, SSDownloadAsset, SSOperationProgress;
@protocol ISAccountStoreObserver, ISAssetService, ISDownloadService, ISInAppService, ISServiceRemoteObject, ISTransactionService, ISUIService, ISURLBagObserver;
typedef void (^UnknownBlock)();
#import "ISStoreAccount.h"
#import "ISAccountService-Protocol.h"
#import "ISServiceProxy.h"
#import "SSDownloadMetadata.h"
#import "SSDownloadPhase.h"
#import "SSDownloadStatus.h"
#import "SSDownload.h"
#import "SSPurchase.h"
#import "SSPurchaseResponse.h"
================================================
FILE: Sources/PrivateFrameworks/include/StoreFoundation/module.modulemap
================================================
module StoreFoundation [system] [no_undeclared_includes] {
requires macos, objc
use Foundation
link framework "StoreFoundation"
umbrella header "StoreFoundation.h"
export *
}
================================================
FILE: Sources/mas/AppStore/AppStoreAction+download.swift
================================================
//
// AppStoreAction+download.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
private import CommerceKit
private import CoreServices
private import Foundation
private import ObjectiveC
private import StoreFoundation
extension AppStoreAction { // swiftlint:disable:this file_types_order
@MainActor
func app(withADAMID adamID: ADAMID, shouldCancel: @escaping @Sendable (String?, Bool) -> Bool) async throws {
let purchase = SSPurchase(
buyParameters: """
productType=C&price=0&pg=default&appExtVrsId=0&pricingParameters=\
\(self == .get ? "STDQ&macappinstalledconfirmed=1" : "STDRDL")&salableAdamId=\(adamID)
""",
)
// Possibly unnecessary…
purchase.isRedownload = self != .get
purchase.isUpdate = self == .update
purchase.itemIdentifier = adamID
let downloadMetadata = SSDownloadMetadata(kind: "software")
downloadMetadata.itemIdentifier = adamID
purchase.downloadMetadata = downloadMetadata
let queue = CKDownloadQueue.shared()
let observer = DownloadQueueObserver(for: self, of: adamID, shouldCancel: shouldCancel)
let observerUUID = queue.add(observer)
defer {
queue.removeObserver(observerUUID)
}
try await withCheckedThrowingContinuation { continuation in
observer.set(continuation: continuation)
CKPurchaseController.shared().perform(purchase, withOptions: 0) { _, _, error, response in
if let error {
Task {
await observer.resumeOnce { $0.resume(throwing: error) }
}
} else if response?.downloads?.isEmpty != false {
Task {
await observer.resumeOnce { continuation in
continuation.resume(throwing: MASError.error("No downloads initiated for ADAM ID \(adamID)"))
}
}
}
}
}
}
}
private actor DownloadQueueObserver: CKDownloadQueueObserver {
private let action: AppStoreAction
private let adamID: ADAMID
private let shouldCancel: @Sendable (String?, Bool) -> Bool
private let downloadFolderURL: URL
private nonisolated(unsafe) var continuation = CheckedContinuation<Void, any Error>?.none
private var prevPhaseType = PhaseType.processing
private var pkgHardLinkURL = URL?.none
private var receiptHardLinkURL = URL?.none
private var alreadyResumed = false
init(for action: AppStoreAction, of adamID: ADAMID, shouldCancel: @escaping @Sendable (String?, Bool) -> Bool) {
self.action = action
self.adamID = adamID
self.shouldCancel = shouldCancel
downloadFolderURL = URL(filePath: "\(CKDownloadDirectory(nil))/\(adamID)", directoryHint: .isDirectory)
}
deinit {
resumeOnce(
alreadyResumed: alreadyResumed,
pkgHardLinkURL: pkgHardLinkURL,
receiptHardLinkURL: receiptHardLinkURL,
) { continuation in
continuation // swiftformat:disable:next indent
.resume(throwing: MASError.error("Observer deallocated before download completed for ADAM ID \(adamID)"))
}
}
nonisolated func set(continuation: CheckedContinuation<Void, any Error>) {
unsafe self.continuation = continuation
}
nonisolated func downloadQueue(_: CKDownloadQueue, changedWithAddition _: SSDownload) {
// Do nothing
}
nonisolated func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
guard
let snapshot = DownloadSnapshot(to: action, download),
snapshot.adamID == adamID,
!snapshot.isCancelled,
!snapshot.isFailed
else {
return
}
guard !shouldCancel(snapshot.version, true) else {
queue.cancelDownload(download, promptToConfirm: false, askToDelete: false)
return
}
Task {
await statusChanged(for: snapshot)
}
}
nonisolated func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
guard let snapshot = DownloadSnapshot(to: action, download), snapshot.adamID == adamID else {
return
}
Task {
await removed(snapshot)
}
}
func resumeOnce(performing action: (CheckedContinuation<Void, any Error>) -> Void) {
resumeOnce(
alreadyResumed: alreadyResumed,
pkgHardLinkURL: pkgHardLinkURL,
receiptHardLinkURL: receiptHardLinkURL,
performing: action,
)
alreadyResumed = true
}
private nonisolated func resumeOnce(
alreadyResumed: Bool,
pkgHardLinkURL: URL?,
receiptHardLinkURL: URL?,
performing action: (CheckedContinuation<Void, any Error>) -> Void,
) {
guard !alreadyResumed else {
return
}
guard let continuation = unsafe continuation else {
MAS.printer.error("Failed to obtain download continuation for ADAM ID \(adamID)")
return
}
action(continuation)
deleteTempFolder(containing: pkgHardLinkURL, fileType: "pkg")
deleteTempFolder(containing: receiptHardLinkURL, fileType: "receipt")
}
private func statusChanged(for snapshot: DownloadSnapshot) {
// Refresh hard links to latest artifacts in the download directory
do {
let downloadFolderChildURLs = try FileManager.default.contentsOfDirectory(
at: downloadFolderURL,
includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey],
)
do {
pkgHardLinkURL = try hardLinkURL(
to: try downloadFolderChildURLs.compactMap { url in
guard url.pathExtension == "pkg" else {
return nil as (url: URL, date: Date)?
}
let resourceValues = try url.resourceValues(forKeys: [.contentModificationDateKey, .isRegularFileKey])
return resourceValues.isRegularFile == true ? resourceValues.contentModificationDate.map { (url, $0) } : nil
}
.max { $0.date < $1.date }?
.url,
existing: pkgHardLinkURL,
)
} catch {
MAS.printer.warning("Failed to link pkg for", snapshot.appNameAndVersion, error: error)
}
do {
receiptHardLinkURL = try hardLinkURL(
to: downloadFolderChildURLs.first { $0.lastPathComponent == "receipt" },
existing: receiptHardLinkURL,
)
} catch {
MAS.printer.warning("Failed to link receipt for", snapshot.appNameAndVersion, error: error)
}
} catch {
MAS.printer.warning(
"Failed to read contents of download folder",
downloadFolderURL.filePath.quoted,
"for",
snapshot.appNameAndVersion,
error: error,
)
}
switch snapshot.activePhaseType {
case prevPhaseType:
break
case
.downloading where prevPhaseType == .processing,
.downloaded where prevPhaseType == .downloading,
.performing:
MAS.printer.clearCurrentLine(of: .standardOutput)
MAS.printer.notice(snapshot.activePhaseType, snapshot.appNameAndVersion)
default:
break
}
if
FileHandle.standardOutput.isTerminal,
snapshot.phasePercentComplete != 0 || snapshot.activePhaseType != .processing
{
// Output the progress bar iff connected to a terminal
let totalLength = 60
let completedLength = Int(snapshot.phasePercentComplete * Float(totalLength))
MAS.printer.clearCurrentLine(of: .standardOutput)
MAS.printer.info(
String(repeating: "#", count: completedLength),
String(repeating: "-", count: totalLength - completedLength),
" ",
UInt64((snapshot.phasePercentComplete * 100).rounded()),
"% ",
snapshot.activePhaseType.performed,
separator: "",
terminator: "",
)
}
prevPhaseType = snapshot.activePhaseType
}
private func removed(_ snapshot: DownloadSnapshot) async {
MAS.printer.clearCurrentLine(of: .standardOutput)
do {
let appFolderURL: URL?
if let error = snapshot.error {
guard error is Ignorable else {
throw error
}
MAS.printer.notice(PhaseType.downloaded, snapshot.appNameAndVersion)
MAS.printer.notice(action.performing.capitalizingFirstCharacter, snapshot.appNameAndVersion)
MAS.printer.info(
String(describing: action).capitalizingFirstCharacter,
"progress cannot be displayed",
terminator: "",
)
appFolderURL = try await install(appNameAndVersion: snapshot.appNameAndVersion)
MAS.printer.clearCurrentLine(of: .standardOutput)
} else {
guard !snapshot.isFailed else {
throw MASError.error("Failed to download \(snapshot.appNameAndVersion)")
}
guard !shouldCancel(snapshot.version, false) else {
resumeOnce { $0.resume() }
return
}
guard !snapshot.isCancelled else {
throw MASError.error("Download cancelled for \(snapshot.appNameAndVersion)")
}
appFolderURL = snapshot.appFolderPath.map { .init(filePath: $0, directoryHint: .isDirectory) }
}
MAS.printer.notice(
[action.performed.capitalizingFirstCharacter, snapshot.appNameAndVersion]
+ (appFolderURL.map { ["in", $0.filePath] } ?? []), // swiftformat:disable:this indent
)
if let appFolderURL {
let fileManager = FileManager.default
if
try applicationsFolderURLs.contains(
where: { applicationsFolderURL in
var relationship = FileManager.URLRelationship.other
try unsafe fileManager.getRelationship(
&relationship,
ofDirectoryAt: applicationsFolderURL,
toItemAt: appFolderURL,
)
return relationship == .contains
},
)
{
let appFolderPath = appFolderURL.filePath
let installedApps = try await installedApps(withADAMID: snapshot.adamID).filter { $0.path != appFolderPath }
if !installedApps.isEmpty {
MAS.printer.warning(
"Multiple installations of ",
snapshot.name ?? "unknown app",
" exist in the applications folders\n\n",
action.performed.capitalizingFirstCharacter,
":\n",
appFolderPath,
"\n\nOthers:\n",
installedApps.map(\.path).sorted(using: .localizedStandard).joined(separator: "\n"),
separator: "",
)
}
} else {
MAS.printer.warning(
action.performed.capitalizingFirstCharacter,
snapshot.appNameAndVersion,
"outside of the applications folders, in",
appFolderURL.filePath,
)
}
}
resumeOnce { $0.resume() }
} catch {
resumeOnce { $0.resume(throwing: error) }
}
}
private func hardLinkURL(to url: URL?, existing existingHardLinkURL: URL?) throws -> URL? {
guard let url, try !url.linksToSameInode(as: existingHardLinkURL) else {
return existingHardLinkURL
}
let fileManager = FileManager.default
let hardLinkURL = try fileManager.url(
for: .itemReplacementDirectory,
in: .userDomainMask,
appropriateFor: url,
create: true,
)
.appending(path: "\(adamID)-\(url.lastPathComponent)", directoryHint: .notDirectory)
try fileManager.linkItem(at: url, to: hardLinkURL)
return hardLinkURL
}
private func install(appNameAndVersion: String) async throws -> URL {
guard let pkgHardLinkPath = pkgHardLinkURL?.filePath else {
throw MASError.error("Failed to find pkg to \(action) \(appNameAndVersion)")
}
guard let receiptHardLinkURL else {
throw MASError.error("Failed to find receipt to import for \(appNameAndVersion)")
}
let (_, standardErrorString) = try await run(
"/usr/sbin/installer",
"-dumplog",
"-pkg",
pkgHardLinkPath,
"-target",
"/",
errorMessage: "Failed to \(action) \(appNameAndVersion) from \(pkgHardLinkPath)",
) { process in try run(asEffectiveUID: 0, andEffectiveGID: 0) { try process.run() } }
guard
let appFolderURLSubstring = standardErrorString
.matches(of: unsafe appFolderURLRegex) // swiftformat:disable indent
.compactMap(\.1)
.min(by: { $0.count < $1.count })
else { // swiftformat:enable indent
throw MASError.error(
"Failed to find app folder URL in installer output for \(appNameAndVersion)",
error: standardErrorString,
)
}
guard let appFolderURL = URL(string: .init(appFolderURLSubstring)) else {
throw MASError.error(
"Failed to parse app folder URL for \(appNameAndVersion) from \(appFolderURLSubstring)",
error: standardErrorString,
)
}
let receiptURL = appFolderURL.appending(path: "Contents/_MASReceipt/receipt", directoryHint: .notDirectory)
do {
let fileManager = FileManager.default
try run(asEffectiveUID: 0, andEffectiveGID: 0) {
if fileManager.fileExists(atPath: receiptURL.filePath) {
try fileManager.removeItem(at: receiptURL)
} else {
try fileManager.createDirectory(
at: receiptURL.deletingLastPathComponent(),
withIntermediateDirectories: true,
attributes: [.ownerAccountID: 0, .groupOwnerAccountID: 0, .posixPermissions: 0o755],
)
}
try fileManager.copyItem(at: receiptHardLinkURL, to: receiptURL)
try fileManager.setAttributes(
[.ownerAccountID: 0, .groupOwnerAccountID: 0, .posixPermissions: 0o755],
ofItemAtPath: receiptURL.filePath,
)
}
} catch {
throw MASError.error(
"""
Failed to copy receipt for \(appNameAndVersion) from \(receiptHardLinkURL.filePath.quoted) to\
\(receiptURL.filePath.quoted)
""",
error: error,
)
}
_ = try await run(
"/usr/bin/mdimport",
appFolderURL.filePath,
errorMessage: "Failed to \(action) \(appNameAndVersion) from \(pkgHardLinkPath)",
)
LSRegisterURL(appFolderURL as NSURL, true) // swiftlint:disable:this legacy_objc_type
return appFolderURL
}
}
private struct DownloadSnapshot { // swiftlint:disable:this one_declaration_per_file
let adamID: ADAMID
let version: String?
let name: String?
let appNameAndVersion: String
let activePhaseType: PhaseType
let phasePercentComplete: Float
let appFolderPath: String?
let isCancelled: Bool
let isFailed: Bool
let error: (any Error)?
init?(to action: AppStoreAction, _ download: SSDownload) {
guard let metadata = download.metadata, let status = download.status else {
return nil
}
adamID = metadata.itemIdentifier
name = metadata.title
version = metadata.bundleVersion
appNameAndVersion = "\(metadata.title ?? "unknown app") (\(version ?? "unknown version"))"
activePhaseType = PhaseType(action, rawValue: status.activePhase?.phaseType)
phasePercentComplete = status.phasePercentComplete
appFolderPath = download.installPath
isCancelled = status.isCancelled
isFailed = status.isFailed
error = status.error.map { $0 as NSError }.map { error in
error.domain == "PKInstallErrorDomain" && error.code == 201 ? Ignorable.installerWorkaround : error as any Error
}
}
}
private enum Ignorable: Error { // swiftlint:disable:this one_declaration_per_file
case installerWorkaround
}
private enum PhaseType: Equatable { // swiftlint:disable:this one_declaration_per_file
case processing // swiftlint:disable:this sorted_enum_cases
case downloading
case downloaded // swiftlint:disable:this sorted_enum_cases
case performing(AppStoreAction) // swiftlint:disable:this sorted_enum_cases
var performed: String {
switch self {
case .processing:
"processed"
case // swiftformat:disable:this sortSwitchCases
.downloading,
.downloaded:
"downloaded"
case let .performing(action):
action.performed
}
}
init(_ action: AppStoreAction, rawValue: Int64?) {
self =
switch rawValue {
case 0:
.downloading
case 1:
.performing(action)
case 5:
.downloaded
default:
.processing
}
}
}
extension PhaseType: CustomStringConvertible {
var description: String {
switch self {
case .processing:
"Processing"
case .downloading:
"Downloading"
case .downloaded:
"Downloaded"
case let .performing(action):
action.performing
}
}
}
private extension String {
var capitalizingFirstCharacter: Self {
prefix(1).capitalized + dropFirst()
}
}
private extension URL {
func linksToSameInode(as url: URL?) throws -> Bool {
guard let url, url.isFileURL, isFileURL else {
return false
}
guard let fileID1 = try resourceValues(forKeys: [.fileResourceIdentifierKey]).fileResourceIdentifier else {
throw MASError.error("Failed to get file resource identifier for \(filePath)")
}
guard let fileID2 = try url.resourceValues(forKeys: [.fileResourceIdentifierKey]).fileResourceIdentifier else {
throw MASError.error("Failed to get file resource identifier for \(url.filePath)")
}
return fileID1.isEqual(fileID2)
}
}
private func deleteTempFolder(containing url: URL?, fileType: String) {
url.map { url in
do {
try FileManager.default.removeItem(at: url.deletingLastPathComponent())
} catch {
MAS.printer.warning("Failed to delete temp folder containing", fileType, url.filePath, error: error)
}
}
}
private nonisolated(unsafe) let appFolderURLRegex = /PackageKit: Registered bundle (\S+) for uid 0/
================================================
FILE: Sources/mas/AppStore/AppStoreAction.swift
================================================
//
// AppStoreAction.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
private import ArgumentParser
private import Darwin
private import OrderedCollections
private import StoreFoundation
enum AppStoreAction {
case get
case install
case update
var performed: String {
switch self {
case .get:
"got"
case .install:
"installed"
case .update:
"updated"
}
}
var performing: String {
switch self {
case .get:
"getting"
case .install:
"installing"
case .update:
"updating"
}
}
func apps(
withAppIDs appIDs: [AppID],
force: Bool,
installedApps: [InstalledApp],
lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp,
) async throws {
try await apps(
withADAMIDs: await appIDs.lookupCatalogApps(using: lookupAppFromAppID).map(\.adamID),
force: force,
installedApps: installedApps,
)
}
func apps(withADAMIDs adamIDs: [ADAMID], force: Bool, installedApps: [InstalledApp]) async throws {
try await apps(
withADAMIDs: adamIDs.filter { adamID in
if !force, let installedApp = installedApps.first(where: { $0.adamID == adamID }) {
MAS.printer.warning("Already ", performed, " ", installedApp.name, " (", adamID, ")", separator: "")
return false
}
return true
},
)
}
func apps(withADAMIDs adamIDs: [ADAMID]) async throws {
guard !adamIDs.isEmpty else {
return
}
let adamIDs = OrderedSet(adamIDs)
guard getuid() == 0 else {
try sudo(MAS._commandName, args: [String(describing: self), "--force"] + adamIDs.map(String.init(describing:)))
return
}
await adamIDs.forEach(attemptTo: "\(self) app for ADAM ID") { adamID in
try await app(withADAMID: adamID) { _, _ in false }
}
}
}
typealias AppStore = AppStoreAction
================================================
FILE: Sources/mas/AppStore/Region.swift
================================================
//
// Region.swift
// mas
//
// Copyright © 2024 mas-cli. All rights reserved.
//
private import Foundation
typealias Region = String
private extension Region {
var appStoreRegion: Self {
switch self { // swiftlint:disable switch_case_on_newline
case "AD": "ES" // Andorra > Spain
case "AQ": "NO" // Antarctica > Norway
case "AS": "US" // American Samoa > United States
case "AW": "NL" // Aruba > Netherlands
case "AX": "FI" // Åland Islands > Finland
case "BD": "IN" // Bangladesh > India
case "BI": "KE" // Burundi > Kenya
case "BL": "FR" // St. Barthélemy > France
case "BQ": "NL" // Bonaire, Sint Eustatius and Saba > Netherlands
case "BV": "NO" // Bouvet Island > Norway
case "CC": "AU" // Cocos (Keeling) Islands > Australia
case "CF": "FR" // Central African Republic > France
case "CK": "NZ" // Cook Islands > New Zealand
case "CU": "US" // Cuba > United States
case "CW": "NL" // Curaçao > Netherlands
case "CX": "AU" // Christmas Island > Australia
case "DJ": "FR" // Djibouti > France
case "EH": "MA" // Western Sahara > Morocco
case "ER": "KE" // Eritrea > Kenya
case "ET": "KE" // Ethiopia > Kenya
case "FK": "GB" // Falkland Islands > United Kingdom
case "FO": "DK" // Faroe Islands > Denmark
case "GF": "FR" // French Guiana > France
case "GG": "GB" // Guernsey > United Kingdom
case "GI": "GB" // Gibraltar > United Kingdom
case "GL": "DK" // Greenland > Denmark
case "GN": "FR" // Guinea > France
case "GP": "FR" // Guadeloupe > France
case "GQ": "FR" // Equatorial Guinea > France
case "GS": "GB" // South Georgia and the South Sandwich Islands > United Kingdom
case "GU": "US" // Guam > United States
case "HM": "AU" // Heard Island and McDonald Islands > Australia
case "HT": "US" // Haiti > United States
case "IC": "ES" // Canary Islands > Spain
case "IM": "GB" // Isle of Man > United Kingdom
case "IO": "GB" // British Indian Ocean Territory > United Kingdom
case "IR": "TR" // Iran > Türkiye
case "JE": "GB" // Jersey > United Kingdom
case "KI": "AU" // Kiribati > Australia
case "KM": "FR" // Comoros > France
case "KP": "CN" // Korea, Democratic People's Republic of > China
case "LI": "CH" // Liechtenstein > Switzerland
case "LS": "ZA" // Lesotho > South Africa
case "MC": "FR" // Monaco > France
case "MF": "FR" // St. Martin > France
case "MH": "US" // Marshall Islands > United States
case "MP": "US" // Northern Mariana Islands > United States
case "MQ": "FR" // Martinique > France
case "NC": "FR" // New Caledonia > France
case "NF": "AU" // Norfolk Island > Australia
case "NU": "NZ" // Niue > New Zealand
case "PF": "FR" // French Polynesia > France
case "PM": "FR" // St. Pierre and Miquelon > France
case "PN": "NZ" // Pitcairn Islands > New Zealand
case "PR": "US" // Puerto Rico > United States
case "PS": "IL" // Palestine > Israel
case "RE": "FR" // Réunion > France
case "SD": "EG" // Sudan > Egypt
case "SH": "GB" // St. Helena, Ascension and Tristan da Cunha > United Kingdom
case "SJ": "NO" // Svalbard and Jan Mayen > Norway
case "SM": "IT" // San Marino > Italy
case "SO": "KE" // Somalia > Kenya
case "SS": "KE" // South Sudan > Kenya
case "SX": "NL" // Sint Maarten > Netherlands
case "SY": "TR" // Syria > Türkiye
case "TF": "FR" // French Southern Territories > France
case "TG": "FR" // Togo > France
case "TK": "NZ" // Tokelau > New Zealand
case "TL": "ID" // Timor-Leste > Indonesia
case "TV": "AU" // Tuvalu > Australia
case "UM": "US" // United States Minor Outlying Islands > United States
case "VA": "IT" // Vatican City State > Italy
case "VI": "US" // Virgin Islands of the United States > United States
case "WF": "FR" // Wallis and Futuna > France
case "WS": "AU" // Samoa > Australia
case "YT": "FR" // Mayotte > France
default: self // swiftlint:enable switch_case_on_newline
}
}
}
var appStoreRegion: Region {
macRegion.appStoreRegion
}
var macRegion: Region {
Locale.current.region?.identifier ?? "US"
}
================================================
FILE: Sources/mas/Commands/Config.swift
================================================
//
// Config.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Darwin
private import Foundation
extension MAS {
/// Outputs mas config & related system info.
struct Config: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Output mas config & related system info",
)
func run() {
printer.info(
"""
mas ▁▁▁▁ \(version)
slice ▁▁ \(runningSliceArchitecture)
slices ▁ \(supportedSliceArchitectures.joined(separator: " "))
dist ▁▁▁ \(distribution)
origin ▁ \(gitOrigin)
rev ▁▁▁▁ \(gitRevision)
swift ▁▁ \(swiftVersion)
driver ▁ \(swiftDriverVersion)
store ▁▁ \(appStoreRegion)
region ▁ \(macRegion)
macos ▁▁ \(macOSVersion)
mac ▁▁▁▁ \(configStringValue("hw.product"))
cpu ▁▁▁▁ \(configStringValue("machdep.cpu.brand_string"))
arch ▁▁▁ \(configStringValue("hw.machine"))
""",
)
}
}
}
private var runningSliceArchitecture: String {
var info = utsname()
return unsafe uname(&info) == 0
? withUnsafePointer(to: &info.machine) { pointer in // swiftformat:disable indent
unsafe pointer.withMemoryRebound(
to: CChar.self,
capacity: unsafe MemoryLayout.size(ofValue: unsafe pointer),
unsafe String.init(cString:),
)
}
: unknown
} // swiftformat:enable indent
private var supportedSliceArchitectures: [String] {
Bundle.main.executableArchitectures.map { archIDs in
archIDs.map { archID in
guard let arch = Int(exactly: archID) else {
return "unknown_\(archID)"
}
return switch arch {
case NSBundleExecutableArchitectureARM64:
"arm64"
case NSBundleExecutableArchitectureI386:
"i386"
case NSBundleExecutableArchitecturePPC:
"ppc"
case NSBundleExecutableArchitecturePPC64:
"ppc64"
case NSBundleExecutableArchitectureX86_64:
"x86_64"
default:
"unknown_0x\(String(arch, radix: 16))"
}
}
}
?? [] // swiftformat:disable:this indent
}
private var macOSVersion: Substring {
ProcessInfo.processInfo.operatingSystemVersionString.dropFirst(8).replacing("Build ", with: "", maxReplacements: 1)
}
private func configStringValue(_ name: String) -> String {
var size = 0
guard unsafe sysctlbyname(name, nil, &size, nil, 0) == 0 else {
unsafe perror(sysCtlByName)
return unknown
}
var buffer = [CChar](repeating: 0, count: size)
guard unsafe sysctlbyname(name, &buffer, &size, nil, 0) == 0 else {
unsafe perror(sysCtlByName)
return unknown
}
return unsafe String(cString: &buffer)
}
private let unknown = "unknown"
private let sysCtlByName = "sysctlbyname"
================================================
FILE: Sources/mas/Commands/Get.swift
================================================
//
// Get.swift
// mas
//
// Copyright © 2026 mas-cli. All rights reserved.
//
internal import ArgumentParser
extension MAS {
/// Gets & installs free apps from the App Store.
struct Get: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Get & install free apps from the App Store",
discussion: requiresRootPrivilegesMessage(),
aliases: ["purchase"],
)
@OptionGroup
private var forceOptionGroup: ForceOptionGroup
@OptionGroup
private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup
func run() async throws {
try await AppStore.get.apps(
withAppIDs: catalogAppIDsOptionGroup.appIDs,
force: forceOptionGroup.force,
installedApps: try await installedApps,
lookupAppFromAppID: lookup(appID:),
)
}
}
}
================================================
FILE: Sources/mas/Commands/Home.swift
================================================
//
// Home.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Opens App Store app pages in the default web browser.
///
/// Uses the iTunes Lookup API:
///
/// https://performance-partners.apple.com/search-api
struct Home: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open App Store app pages in the default web browser",
)
@OptionGroup
private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup
func run() async {
await run(lookupAppFromAppID: lookup(appID:))
}
private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async {
await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}
func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations
await run(appStorePageURLStrings: catalogApps.map(\.appStorePageURLString))
}
private func run(appStorePageURLStrings: [String]) async { // swiftformat:disable:this organizeDeclarations
await appStorePageURLStrings.forEach(attemptTo: "open") { appStorePageURLString in
guard let url = URL(string: appStorePageURLString) else {
throw MASError.unparsableURL(appStorePageURLString)
}
try await url.open()
}
}
}
}
================================================
FILE: Sources/mas/Commands/Install.swift
================================================
//
// Install.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
internal import ArgumentParser
extension MAS {
/// Installs previously gotten apps from the App Store.
struct Install: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Install previously gotten apps from the App Store",
discussion: requiresRootPrivilegesMessage(),
)
@OptionGroup
private var forceOptionGroup: ForceOptionGroup
@OptionGroup
private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup
func run() async throws {
try await AppStore.install.apps(
withAppIDs: catalogAppIDsOptionGroup.appIDs,
force: forceOptionGroup.force,
installedApps: try await installedApps,
lookupAppFromAppID: lookup(appID:),
)
}
}
}
================================================
FILE: Sources/mas/Commands/List.swift
================================================
//
// List.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Lists all apps installed from the App Store.
struct List: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "List apps installed from the App Store",
)
@OptionGroup
private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup
func run() async throws {
run(installedApps: try await installedApps)
}
func run(installedApps: [InstalledApp]) {
let installedApps = installedApps.filter(for: installedAppIDsOptionGroup.appIDs)
guard
let maxADAMIDLength = installedApps.map({ String(describing: $0.adamID).count }).max(),
let maxNameLength = installedApps.map(\.name.count).max()
else {
printer.warning(
"""
No installed apps found
If this is unexpected, any of the following command lines should fix things by reindexing apps in Spotlight\
(which might take some time):
# Individual apps (if you know exactly what apps were incorrectly omitted):
mdimport /Applications/Example.app
# All apps (<LargeAppVolume> is the volume optionally selected for large apps):
mdimport /Applications /Volumes/<LargeAppVolume>/Applications
# All file system volumes (if neither aforementioned command solved the issue):
sudo mdutil -Eai on
""",
)
return
}
let format = "%\(maxADAMIDLength)lu %@ (%@)"
printer.info(
installedApps.map { installedApp in
String(
format: format,
installedApp.adamID,
installedApp.name.padding(toLength: maxNameLength, withPad: " ", startingAt: 0),
installedApp.version,
)
}
.joined(separator: "\n"),
)
}
}
}
================================================
FILE: Sources/mas/Commands/Lookup.swift
================================================
//
// Lookup.swift
// mas
//
// Copyright © 2016 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Outputs app information from the App Store.
///
/// Uses the iTunes Lookup API:
///
/// https://performance-partners.apple.com/search-api
struct Lookup: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Output app information from the App Store",
aliases: ["info"],
)
@OptionGroup
private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup
func run() async {
await run(lookupAppFromAppID: lookup(appID:))
}
private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async {
run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}
func run(catalogApps: [CatalogApp]) { // swiftformat:disable:this organizeDeclarations
printer.info(
catalogApps.map { catalogApp in
"""
\(catalogApp.name) \(catalogApp.version) [\(catalogApp.displayPrice)]
By: \(catalogApp.sellerName)
Released: \(catalogApp.releaseDate.isoCalendarDate)
Minimum OS: \(catalogApp.minimumOSVersion)
Size: \(catalogApp.fileSizeBytes.humanReadableSize)
From: \(catalogApp.appStorePageURLString)
"""
}
.joined(separator: "\n"),
terminator: "",
)
}
}
}
private extension String {
var humanReadableSize: Self {
Int64(self).map { $0.formatted(.byteCount(style: .file, allowedUnits: .mb, spellsOutZero: false)) } ?? self
}
var isoCalendarDate: Self {
(try? Date(self, strategy: .iso8601).formatted(Date.ISO8601FormatStyle(timeZone: .current).year().month().day()))
?? self // swiftformat:disable:this indent
}
}
================================================
FILE: Sources/mas/Commands/Lucky.swift
================================================
//
// Lucky.swift
// mas
//
// Copyright © 2017 mas-cli. All rights reserved.
//
internal import ArgumentParser
extension MAS {
/// Installs the first app returned from searching the App Store (app must
/// have been previously gotten).
///
/// Uses the iTunes Search API:
///
/// https://performance-partners.apple.com/search-api
struct Lucky: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Install the first app returned from searching the App Store",
discussion: // swiftformat:disable:next indent
"App will install only if it has already been gotten\n\n\(requiresRootPrivilegesMessage(to: "install"))",
)
@OptionGroup
private var forceOptionGroup: ForceOptionGroup
@OptionGroup
private var searchTermOptionGroup: SearchTermOptionGroup
func run() async throws {
try await run(installedApps: try await installedApps, searchForAppsMatchingSearchTerm: search(for:))
}
private func run(
installedApps: [InstalledApp],
searchForAppsMatchingSearchTerm: (String) async throws -> [CatalogApp],
) async throws {
let searchTerm = searchTermOptionGroup.searchTerm
guard let adamID = try await searchForAppsMatchingSearchTerm(searchTerm).first?.adamID else {
throw MASError.noCatalogAppsFound(for: searchTerm)
}
try await run(installedApps: installedApps, adamID: adamID)
}
private func run(installedApps: [InstalledApp], adamID: ADAMID) async throws {
try await AppStore.install.apps(
withADAMIDs: [adamID],
force: forceOptionGroup.force,
installedApps: installedApps,
)
}
}
}
================================================
FILE: Sources/mas/Commands/MAS.swift
================================================
//
// MAS.swift
// mas
//
// Copyright © 2021 mas-cli. All rights reserved.
//
internal import ArgumentParser
internal import Foundation
@main
struct MAS: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Mac App Store command-line interface",
version: Self.version,
subcommands: [
Config.self,
Get.self,
Home.self,
Install.self,
List.self,
Lookup.self,
Lucky.self,
Open.self,
Outdated.self,
Reset.self,
Search.self,
Seller.self,
SignOut.self,
Uninstall.self,
Update.self,
Version.self,
],
)
static let printer = Printer()
static var _errorPrefix: String { // swiftlint:disable:this identifier_name
"\(errorPrefix.formatted(with: errorFormat, for: FileHandle.standardError)) "
}
private static func main() async { // swiftlint:disable:this unused_declaration
await main(nil)
}
private static func main(_ arguments: [String]?) async { // swiftlint:disable:this discouraged_optional_collection
do {
let command = try parseAsRoot(arguments)
if let command = cast(command, as: (any AsyncParsableCommand & Sendable).self) {
try await main(command)
} else {
try main(command)
}
let errorCount = printer.errorCount
if errorCount > 0 {
throw ExitCode(errorCount >= UInt64(Int32.max) ? .max : .init(errorCount))
}
} catch {
exit(withError: error)
}
}
}
extension MAS {
static func main(_ command: some ParsableCommand) throws {
try main(command) { command in
var command = command
try command.run()
}
}
static func main(_ command: some AsyncParsableCommand & Sendable) async throws {
try await main(command) { command in
var command = command
try await command.run()
}
}
static func main<Command: ParsableCommand>(_ command: Command, _ body: (Command) throws -> Void) throws {
do {
try ProcessInfo.processInfo.runAsSudoEffectiveUserAndSudoEffectiveGroupIfRootEffectiveUser {
try body(command)
}
} catch {
printer.error(error: try error.failure)
}
}
static func main<Command: AsyncParsableCommand>(_ command: Command, _ body: (Command) async throws -> Void)
async throws { // swiftformat:disable:this indent
do {
try await ProcessInfo.processInfo.runAsSudoEffectiveUserAndSudoEffectiveGroupIfRootEffectiveUser {
try await body(command)
}
} catch {
printer.error(error: try error.failure)
}
}
}
private extension Error {
var failure: Self {
get throws {
guard !MAS.exitCode(for: self).isSuccess else {
throw self
}
return self
}
}
}
extension ParsableCommand {
static func requiresRootPrivilegesMessage(to action: String = String(describing: Self.self).lowercased()) -> String {
"Requires root privileges to \(action) apps"
}
}
private func cast<T>(_ instance: Any, as _: T.Type) -> T? {
instance as? T
}
private let applicationsFolderPath = "/Applications"
private let applicationsFolderURL = URL(filePath: applicationsFolderPath, directoryHint: .isDirectory)
let applicationsFolderURLs = UserDefaults(suiteName: "com.apple.appstored")?
.dictionary(forKey: "PreferredVolume")?["name"] // swiftformat:disable indent
.map { [applicationsFolderURL, URL(filePath: "/Volumes/\($0)\(applicationsFolderPath)", directoryHint: .isDirectory)] }
?? [applicationsFolderURL]
// swiftformat:enable indent
================================================
FILE: Sources/mas/Commands/Open.swift
================================================
//
// Open.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
private import AppKit
internal import ArgumentParser
private import Foundation
private import ObjectiveC
extension MAS {
/// Opens app page in 'App Store.app'.
///
/// Uses the iTunes Lookup API:
///
/// https://performance-partners.apple.com/search-api
struct Open: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open app page in 'App Store.app'",
)
@OptionGroup
private var forceBundleIDOptionGroup: ForceBundleIDOptionGroup
@Argument(help: .init("App ID", valueName: "app-id"))
private var appIDString: String?
func run() async throws {
try await run(lookupAppFromAppID: lookup(appID:))
}
private func run(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async throws {
try await run(appStorePageURLString: appStorePageURLString(lookupAppFromAppID: lookupAppFromAppID))
}
private func run(appStorePageURLString: String?) async throws {
guard let appStorePageURLString else {
// If no App Store Page URL was given, just open the MAS GUI app
try await openMacAppStore()
return
}
try await openMacAppStorePage(forAppStorePageURLString: appStorePageURLString)
}
private func appStorePageURLString(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async throws -> String? {
guard let appIDString else {
return nil
}
return try await lookupAppFromAppID(
AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID),
)
.appStorePageURLString
}
}
}
private func openMacAppStore() async throws {
guard let macAppStoreSchemeURL = URL(string: "macappstore:") else {
throw MASError.error("Failed to create URL from macappstore scheme")
}
let workspace = NSWorkspace.shared
guard let appURL = workspace.urlForApplication(toOpen: macAppStoreSchemeURL) else {
throw MASError.error("Failed to find app to open macappstore URLs")
}
try await workspace.openApplication(at: appURL, configuration: .init())
}
private func openMacAppStorePage(forAppStorePageURLString appStorePageURLString: String) async throws {
guard var urlComponents = URLComponents(string: appStorePageURLString) else {
throw MASError.unparsableURL(appStorePageURLString)
}
urlComponents.scheme = masScheme
guard let url = urlComponents.url else {
throw MASError.unparsableURL(String(describing: urlComponents))
}
try await url.open()
}
private let masScheme = "macappstore"
================================================
FILE: Sources/mas/Commands/OptionGroups/CatalogAppIDsOptionGroup.swift
================================================
//
// CatalogAppIDsOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import ArgumentParser
struct CatalogAppIDsOptionGroup: ParsableArguments {
@OptionGroup
private var forceBundleIDOptionGroup: ForceBundleIDOptionGroup
@Argument(help: .init("App ID", valueName: "app-id"))
private var appIDStrings: [String]
var appIDs: [AppID] {
appIDStrings.map { .init(from: $0, forceBundleID: forceBundleIDOptionGroup.forceBundleID) }
}
}
================================================
FILE: Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift
================================================
//
// ForceBundleIDOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import ArgumentParser
struct ForceBundleIDOptionGroup: ParsableArguments {
@Flag(name: .customLong("bundle"), help: "Process all app IDs as bundle IDs")
var forceBundleID = false
}
================================================
FILE: Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift
================================================
//
// ForceOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import ArgumentParser
struct ForceOptionGroup: ParsableArguments {
@Flag(help: "Force reinstall")
var force = false
}
================================================
FILE: Sources/mas/Commands/OptionGroups/InstalledAppIDsOptionGroup.swift
================================================
//
// InstalledAppIDsOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import ArgumentParser
struct InstalledAppIDsOptionGroup: ParsableArguments {
@OptionGroup
private var forceBundleIDOptionGroup: ForceBundleIDOptionGroup
@Argument(help: .init("App ID", valueName: "app-id"))
private var appIDStrings = [String]()
var appIDs: [AppID] {
appIDStrings.map { .init(from: $0, forceBundleID: forceBundleIDOptionGroup.forceBundleID) }
}
}
================================================
FILE: Sources/mas/Commands/OptionGroups/OutdatedAccuracy.swift
================================================
//
// OutdatedAccuracy.swift
// mas
//
// Copyright © 2026 mas-cli. All rights reserved.
//
internal import ArgumentParser
enum OutdatedAccuracy: String, EnumerableFlag {
case accurate
case inaccurate
static func help(for outdatedAccuracy: Self) -> ArgumentHelp? {
switch outdatedAccuracy {
case .accurate:
"""
Use accurate, slower logic that starts then cancels a download for each queried app, which can exceed download\
limits & which will open dialogs for undownloadable apps
"""
case .inaccurate:
"Use inaccurate, faster logic that avoids dialogs"
}
}
}
================================================
FILE: Sources/mas/Commands/OptionGroups/OutdatedAppOptionGroup.swift
================================================
//
// OutdatedAppOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import ArgumentParser
struct OutdatedAppOptionGroup: ParsableArguments {
@Flag
var accuracy = OutdatedAccuracy.inaccurate
@Flag(
name: .customLong("check-min-os"),
inversion: .prefixedNo,
help: "Check that macOS is new enough to install the latest app version",
)
var shouldCheckMinimumOSVersion = true
}
================================================
FILE: Sources/mas/Commands/OptionGroups/SearchTermOptionGroup.swift
================================================
//
// SearchTermOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import ArgumentParser
struct SearchTermOptionGroup: ParsableArguments {
@Argument(help: .init("Search terms are concatenated into a single search", valueName: "search-term"))
private var searchTermElements: [String]
var searchTerm: String {
searchTermElements.joined(separator: " ")
}
}
================================================
FILE: Sources/mas/Commands/OptionGroups/VerboseOptionGroup.swift
================================================
//
// VerboseOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import ArgumentParser
struct VerboseOptionGroup: ParsableArguments {
@Flag(help: "Output warnings about app IDs unknown to the App Store")
var verbose = false
}
================================================
FILE: Sources/mas/Commands/Outdated.swift
================================================
//
// Outdated.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Outputs a list of installed apps which have updates available to be
/// installed from the App Store.
struct Outdated: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "List pending app updates from the App Store",
)
@OptionGroup
private var outdatedAppOptionGroup: OutdatedAppOptionGroup
@OptionGroup
private var verboseOptionGroup: VerboseOptionGroup
@OptionGroup
private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup
func run() async throws {
await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:))
}
private func run(
installedApps: [InstalledApp],
lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp,
) async {
run(
outdatedApps: await installedApps.outdatedApps(
filterFor: installedAppIDsOptionGroup.appIDs,
lookupAppFromAppID: lookupAppFromAppID,
accuracy: outdatedAppOptionGroup.accuracy,
shouldCheckMinimumOSVersion: outdatedAppOptionGroup.shouldCheckMinimumOSVersion,
shouldWarnIfUnknownApp: verboseOptionGroup.verbose,
),
)
}
private func run(outdatedApps: [OutdatedApp]) {
guard
let maxADAMIDLength = outdatedApps.map({ String(describing: $0.installedApp.adamID).count }).max(),
let maxNameLength = outdatedApps.map(\.installedApp.name.count).max(),
let maxVersionLength = outdatedApps.map(\.installedApp.version.count).max()
else {
return
}
let format = "%\(maxADAMIDLength)lu %@ (%@ -> %@)"
printer.info(
outdatedApps.map { installedApp, newVersion in
String(
format: format,
installedApp.adamID,
installedApp.name.padding(toLength: maxNameLength, withPad: " ", startingAt: 0),
installedApp.version.padding(toLength: maxVersionLength, withPad: " ", startingAt: 0),
newVersion,
)
}
.joined(separator: "\n"),
)
}
}
}
================================================
FILE: Sources/mas/Commands/Reset.swift
================================================
//
// Reset.swift
// mas
//
// Copyright © 2016 mas-cli. All rights reserved.
//
private import AppKit
internal import ArgumentParser
private import CommerceKit
private import Darwin
private import Foundation
extension MAS {
/// Mimics the "Reset Application" command in the App Store debug menu, which
/// performs the following steps:
///
/// - `killall Dock`
/// - `killall storeagent` (`storeagent` no longer exists)
/// - deletes the `com.apple.appstore` download folder
/// - clears cookies (appears to be a no-op)
///
/// As `storeagent` no longer exists, terminates all processes known to be
/// associated with the App Store.
struct Reset: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Reset App Store processes & clear cached App Store downloads",
)
func run() {
for bundleID in ["com.apple.dock", "com.apple.storeuid"] {
for app in NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) where !app.terminate() {
printer.warning("Failed to terminate app with bundle ID:", bundleID)
if !app.forceTerminate() {
printer.error("Failed to force terminate app with bundle ID:", bundleID)
}
}
}
let executablePathSet = Set([
"/System/Library/Frameworks/StoreKit.framework/Support/storekitagent",
"/System/Library/PrivateFrameworks/AppStoreComponents.framework/Support/appstorecomponentsd",
"/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoreagent",
"""
/System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/XPCServices/SetStoreUpdateService.xpc/Contents/MacOS/SetStoreUpdateService
""",
"/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeaccountd",
"/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeassetd",
"/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storedownloadd",
"/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeinstalld",
"/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storelegacy",
])
var processListMIB = [CTL_KERN, KERN_PROC, KERN_PROC_ALL]
var length = 0
guard unsafe sysctl(&processListMIB, u_int(processListMIB.count), nil, &length, nil, 0) == 0 else {
printer.error("Failed to get process list length")
return
}
var kinfoProcs = unsafe [kinfo_proc](repeating: kinfo_proc(), count: length / MemoryLayout<kinfo_proc>.stride)
guard unsafe sysctl(&processListMIB, u_int(processListMIB.count), &kinfoProcs, &length, nil, 0) == 0 else {
printer.error("Failed to get process list")
return
}
var executablePathBuffer = [CChar](repeating: 0, count: .init(PATH_MAX))
for pid in unsafe kinfoProcs.map(\.kp_proc.p_pid) {
guard
unsafe proc_pidpath(pid, &executablePathBuffer, UInt32(executablePathBuffer.count)) > 0,
let executablePath = String(cString: executablePathBuffer, encoding: .utf8),
executablePathSet.contains(executablePath)
else {
continue
}
let exitStatus = kill(pid, SIGTERM)
if exitStatus != 0 {
printer.error("Failed to terminate", executablePath, "getting exit status", exitStatus, "for pid", pid)
}
}
let folder = CKDownloadDirectory(nil)
do {
try FileManager.default.removeItem(atPath: folder)
} catch {
printer.error("Failed to delete download folder", folder, error: error)
}
}
}
}
================================================
FILE: Sources/mas/Commands/Search.swift
================================================
//
// Search.swift
// mas
//
// Copyright © 2016 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Searches for apps in the App Store.
///
/// Uses the iTunes Search API:
///
/// https://performance-partners.apple.com/search-api
struct Search: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Search for apps in the App Store",
)
@Flag(help: "Output the price of each app")
private var price = false
@OptionGroup
private var searchTermOptionGroup: SearchTermOptionGroup
func run() async throws {
try await run(searchForAppsMatchingSearchTerm: search(for:))
}
private func run(searchForAppsMatchingSearchTerm: (String) async throws -> [CatalogApp]) async throws {
try run(catalogApps: try await searchForAppsMatchingSearchTerm(searchTermOptionGroup.searchTerm))
}
func run(catalogApps: [CatalogApp]) throws { // swiftformat:disable:this organizeDeclarations
guard
let maxADAMIDLength = catalogApps.map({ String(describing: $0.adamID).count }).max(),
let maxNameLength = catalogApps.map(\.name.count).max()
else {
throw MASError.noCatalogAppsFound(for: searchTermOptionGroup.searchTerm)
}
let format = "%\(maxADAMIDLength)lu %@ (%@)\(price ? " %@" : "")"
printer.info(
catalogApps.map { catalogApp in
String(
format: format,
catalogApp.adamID,
catalogApp.name.padding(toLength: maxNameLength, withPad: " ", startingAt: 0),
catalogApp.version,
catalogApp.displayPrice,
)
}
.joined(separator: "\n"),
)
}
}
}
================================================
FILE: Sources/mas/Commands/Seller.swift
================================================
//
// Seller.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
extension MAS {
/// Opens apps' seller pages in the default web browser.
///
/// Uses the iTunes Lookup API:
///
/// https://performance-partners.apple.com/search-api
struct Seller: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open apps' seller pages in the default web browser",
aliases: ["vendor"],
)
@OptionGroup
private var catalogAppIDsOptionGroup: CatalogAppIDsOptionGroup
func run() async {
await run(lookupAppFromAppID: lookup(appID:))
}
private func run(lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp) async {
await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}
func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations
await run(
sellerURLStrings: catalogApps.compactMap { catalogApp in
guard let sellerURLString = catalogApp.sellerURLString else {
printer.error("No seller website available for ADAM ID", catalogApp.adamID)
return nil
}
return sellerURLString
},
)
}
private func run(sellerURLStrings: [String]) async { // swiftformat:disable:this organizeDeclarations
await sellerURLStrings.forEach(attemptTo: "open") { sellerURLString in
guard let url = URL(string: sellerURLString) else {
throw MASError.unparsableURL(sellerURLString)
}
try await url.open()
}
}
}
}
================================================
FILE: Sources/mas/Commands/SignOut.swift
================================================
//
// SignOut.swift
// mas
//
// Copyright © 2016 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import StoreFoundation
extension MAS {
/// Signs out of the Apple Account currently signed in to the App Store.
struct SignOut: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signout",
abstract: "Sign out of the App Store",
)
func run() {
ISServiceProxy.genericShared().accountService.signOut()
}
}
}
================================================
FILE: Sources/mas/Commands/Uninstall.swift
================================================
//
// Uninstall.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Foundation
private import OrderedCollections
extension MAS {
/// Uninstalls apps installed from the App Store.
struct Uninstall: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Uninstall apps installed from the App Store",
discussion: requiresRootPrivilegesMessage(),
)
/// Flag indicating that uninstall shouldn't be performed.
@Flag(name: .customLong("dry-run"), help: "Perform dry run")
private var isPerformingDryRun = false
@Flag(name: .customLong("all"), help: "Uninstall all App Store apps")
private var isUninstallingAll = false
@OptionGroup
private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup
func validate() throws {
if isUninstallingAll != installedAppIDsOptionGroup.appIDs.isEmpty {
throw ValidationError(
isUninstallingAll
? "Cannot specify both --all & app IDs" // swiftformat:disable:this indent
: "Must specify either --all or at least one app ID",
)
}
}
func run() async throws {
try run(installedApps: try await installedApps)
}
private func run(installedApps: [InstalledApp]) throws {
let uninstallingAppPathSet = (
isUninstallingAll ? installedApps.map { .bundleID($0.bundleID) } : installedAppIDsOptionGroup.appIDs,
)
.reduce(into: OrderedSet<String>()) { uninstallingAppPathSet, appID in
let uninstallingApps = installedApps.filter { $0.matches(appID) }
guard !uninstallingApps.isEmpty else {
printer.error(appID.notInstalledMessage)
return
}
uninstallingAppPathSet.append(contentsOf: uninstallingApps.map(\.path))
}
guard !uninstallingAppPathSet.isEmpty else {
return
}
guard !isPerformingDryRun else {
printer.notice("Dry run. A wet run would uninstall:\n")
for uninstallingAppPath in uninstallingAppPathSet {
printer.info(uninstallingAppPath)
}
return
}
guard getuid() == 0 else {
try sudo(MAS._commandName, args: CommandLine.arguments.dropFirst())
return
}
let processInfo = ProcessInfo.processInfo
let uid = try processInfo.sudoUID
let gid = try processInfo.sudoGID
let fileManager = FileManager.default
for appPath in uninstallingAppPathSet {
let attributes = try fileManager.attributesOfItem(atPath: appPath)
guard let appUID = attributes[.ownerAccountID] as? uid_t else {
printer.error("Failed to get uid of", appPath)
continue
}
guard let appGID = attributes[.groupOwnerAccountID] as? gid_t else {
printer.error("Failed to get gid of", appPath)
continue
}
do {
try mas.run(asEffectiveUID: 0, andEffectiveGID: 0) {
try fileManager.setAttributes([.ownerAccountID: uid, .groupOwnerAccountID: gid], ofItemAtPath: appPath)
}
} catch {
printer.error("Failed to change ownership of", appPath.quoted, "to uid", uid, "& gid", gid, error: error)
continue
}
var chownPath = appPath
defer {
do {
try mas.run(asEffectiveUID: 0, andEffectiveGID: 0) {
try fileManager.setAttributes(
[.ownerAccountID: appUID, .groupOwnerAccountID: appGID],
ofItemAtPath: chownPath,
)
}
} catch {
printer.warning(
"Failed to revert ownership of",
chownPath.quoted,
"back to uid",
appUID,
"& gid",
appGID,
error: error,
)
}
}
var uninstalledAppNSURL = NSURL?.none // swiftlint:disable:this legacy_objc_type
try unsafe fileManager.trashItem(
at: .init(filePath: appPath, directoryHint: .isDirectory),
resultingItemURL: &uninstalledAppNSURL,
)
guard let uninstalledAppPath = uninstalledAppNSURL?.path else {
printer.error(
"""
Failed to revert ownership of uninstalled \(appPath.quoted) back to uid \(appUID) & gid \(appGID):\
failed to obtain uninstalled app URL
""",
)
continue
}
chownPath = uninstalledAppPath
printer.info("Uninstalled", appPath.quoted, "to", chownPath.quoted)
}
}
}
}
================================================
FILE: Sources/mas/Commands/Update.swift
================================================
//
// Update.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import StoreFoundation
extension MAS {
/// Updates outdated apps installed from the App Store.
struct Update: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Update outdated apps installed from the App Store",
discussion: requiresRootPrivilegesMessage(),
aliases: ["upgrade"],
)
@OptionGroup
private var outdatedAppOptionGroup: OutdatedAppOptionGroup
@OptionGroup
private var forceOptionGroup: ForceOptionGroup
@OptionGroup
private var verboseOptionGroup: VerboseOptionGroup
@OptionGroup
private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup
func run() async throws {
try await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:))
}
private func run(
installedApps: [InstalledApp],
lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp,
) async throws {
try await run(
outdatedApps: forceOptionGroup.force // swiftformat:disable:next indent
? installedApps.filter(for: installedAppIDsOptionGroup.appIDs).map { ($0, "") }
: await installedApps.outdatedApps(
filterFor: installedAppIDsOptionGroup.appIDs,
lookupAppFromAppID: lookupAppFromAppID,
accuracy: outdatedAppOptionGroup.accuracy,
shouldCheckMinimumOSVersion: outdatedAppOptionGroup.shouldCheckMinimumOSVersion,
shouldWarnIfUnknownApp: verboseOptionGroup.verbose,
),
)
}
private func run(outdatedApps: [OutdatedApp]) async throws {
try await AppStore.update.apps(withADAMIDs: outdatedApps.map(\.installedApp.adamID))
}
}
}
================================================
FILE: Sources/mas/Commands/Version.swift
================================================
//
// Version.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
internal import ArgumentParser
extension MAS {
/// Outputs the version of mas.
struct Version: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Output version number",
)
func run() {
printer.info(version)
}
}
}
================================================
FILE: Sources/mas/Controllers/CatalogApp+ITunesSearch.swift
================================================
//
// CatalogApp+ITunesSearch.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
internal import Foundation
private import Sextant
private import SwiftSoup
func lookup(appID: AppID) async throws -> CatalogApp {
try await lookup(appID: appID, inRegion: appStoreRegion)
}
/// Look up app details from the App Store catalog via the iTunes Search API.
///
/// https://performance-partners.apple.com/search-api
///
/// - Parameters:
/// - appID: App ID.
/// - region: The ISO 3166-1 alpha-2 region of the storefront in which to
/// lookup apps.
/// - Returns: A `CatalogApp` for the given `appID` if `appID` is valid.
/// - Throws: A `MASError.unknownAppID(appID)` if `appID` is invalid.
/// Some other `Error` if any other problem occurs.
func lookup(
appID: AppID,
inRegion region: Region = appStoreRegion,
dataFrom dataSource: (URL) async throws -> (Data, URLResponse) = urlSession.data(from:),
) async throws -> CatalogApp {
let queryItem =
switch appID {
case let .adamID(adamID):
URLQueryItem(name: "id", value: .init(adamID))
case let .bundleID(bundleID):
URLQueryItem(name: "bundleId", value: bundleID)
}
guard // swiftformat:disable:this wrap wrapArguments
let catalogApp = // swiftformat:disable:next indent
try await getCatalogApps(from: try url("lookup", queryItem, inRegion: region), dataFrom: dataSource).first
else {
guard
let catalogApp = try await getCatalogApps(
from: try url("lookup", queryItem, inRegion: region, additionalQueryItems: []),
dataFrom: dataSource,
)
.first,
catalogApp.supportedDevices?.contains("MacDesktop-MacDesktop") ?? false
else {
throw MASError.unknownAppID(appID)
}
return catalogApp.with(minimumOSVersion: await catalogApp.minimumOSVersion(dataFrom: dataSource))
}
return catalogApp
}
private extension CatalogApp {
func minimumOSVersion(dataFrom: (URL) async throws -> (Data, URLResponse) = urlSession.data(from:)) async -> String {
do {
return try await URL(string: appStorePageURLString)
.flatMap { url in // swiftformat:disable indent
try unsafe SwiftSoup.parse(try await dataFrom(url).0, appStorePageURLString)
.select("#serialized-server-data")
.first()?
.data()
.query(
string:
"$.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text",
)?
.firstMatch(of: minimumOSVersionRegex)?
.version
}
.map(String.init(_:)) ?? minimumOSVersion // swiftformat:enable indent
} catch {
return minimumOSVersion
}
}
}
func search(for searchTerm: String) async throws -> [CatalogApp] {
try await search(for: searchTerm, inRegion: appStoreRegion)
}
/// Search for app details from the App Store catalog via the iTunes Search API.
///
/// https://performance-partners.apple.com/search-api
///
/// - Parameters:
/// - searchTerm: Term for which to search.
/// - region: The ISO 3166-1 alpha-2 region of the storefront in which to
/// search for apps.
/// - Returns: A `[CatalogApp]` matching `searchTerm`.
/// - Throws: An `Error` if any problem occurs.
func search(
for searchTerm: String,
inRegion region: Region = appStoreRegion,
dataFrom dataSource: @escaping @Sendable (URL) async throws -> (Data, URLResponse) = urlSession.data(from:),
) async throws -> [CatalogApp] {
let queryItem = URLQueryItem(name: "term", value: searchTerm)
let catalogApps = try await getCatalogApps(from: try url("search", queryItem, inRegion: region), dataFrom: dataSource)
let adamIDSet = Set(catalogApps.map(\.adamID))
return catalogApps.priorityMerge(
try await getCatalogApps(
from: try url("search", queryItem, inRegion: region, additionalQueryItems: []),
dataFrom: dataSource,
)
.filter { ($0.supportedDevices?.contains("MacDesktop-MacDesktop") ?? false) && !adamIDSet.contains($0.adamID) }
.concurrentMap { $0.with(minimumOSVersion: await $0.minimumOSVersion(dataFrom: dataSource)) },
) { $0.name.similarity(to: searchTerm) }
}
private func url(
_ action: String,
_ queryItem: URLQueryItem,
inRegion region: Region,
additionalQueryItems: [URLQueryItem] = [URLQueryItem(name: "entity", value: "desktopSoftware")],
) throws -> URL {
let urlString = "https://itunes.apple.com/\(action)"
guard let url = URL(string: urlString) else {
throw MASError.unparsableURL(urlString)
}
return url.appending(
queryItems: [URLQueryItem(name: "media", value: "software")]
+ additionalQueryItems // swiftformat:disable indent
+ [
URLQueryItem(name: "country", value: region),
queryItem,
],
) // swiftformat:enable indent
}
private func getCatalogApps(from url: URL, dataFrom: (URL) async throws -> (Data, URLResponse))
async throws -> [CatalogApp] { // swiftformat:disable:this indent
let (data, _) = try await dataFrom(url)
do {
return try JSONDecoder().decode(CatalogAppResults.self, from: data).results
} catch {
throw MASError.error("Failed to parse JSON from response \(url)", error: .init(data: data, encoding: .utf8) ?? "")
}
}
private let urlSession = URLSession(configuration: .ephemeral)
private nonisolated(unsafe) let minimumOSVersionRegex = /macOS\s*(?<version>\S+)/
================================================
FILE: Sources/mas/Controllers/InstalledApp+Spotlight.swift
================================================
//
// InstalledApp+Spotlight.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import Atomics
private import Foundation
private import ObjectiveC
private extension URL {
var installedAppURLs: [URL] {
FileManager.default // swiftformat:disable indent
.enumerator(at: self, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
.map { enumerator in
enumerator.compactMap { item in
guard
let url = item as? URL,
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true,
url.pathExtension == "app"
else {
return nil as URL?
}
enumerator.skipDescendants()
return try? url.appending(path: "Contents/_MASReceipt/receipt", directoryHint: .notDirectory)
.resourceValues(forKeys: [.fileSizeKey])
.fileSize
.flatMap { $0 > 0 ? url : nil }
}
}
?? []
} // swiftformat:enable indent
}
var installedApps: [InstalledApp] {
get async throws {
try await mas.installedApps(matching: "kMDItemAppStoreAdamID LIKE '*'")
}
}
func installedApps(withADAMID adamID: ADAMID) async throws -> [InstalledApp] {
try await installedApps(matching: "kMDItemAppStoreAdamID = \(adamID)")
}
@MainActor
func installedApps(matching metadataQuery: String) async throws -> [InstalledApp] {
var observer = (any NSObjectProtocol)?.none
defer {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: metadataQuery)
query.searchScopes = applicationsFolderURLs
return try await withCheckedThrowingContinuation { continuation in
let alreadyResumed = ManagedAtomic(false)
observer = NotificationCenter.default.addObserver(
forName: .NSMetadataQueryDidFinishGathering,
object: query,
queue: nil,
) { notification in
guard !alreadyResumed.exchange(true, ordering: .acquiringAndReleasing) else {
return
}
guard let query = notification.object as? NSMetadataQuery else {
continuation.resume(
throwing: MASError.error(
"Notification Center returned a \(type(of: notification.object)) instead of a NSMetadataQuery",
),
)
return
}
query.stop()
let installedApps = query.results
.compactMap { result in // swiftformat:disable indent
(result as? NSMetadataItem).map { item in
InstalledApp(
adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0,
bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "",
name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "")
.removingSuffix(".app"),
path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "",
version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "",
)
}
}
.sorted(using: KeyPathComparator(\.name, comparator: .localizedStandard)) // swiftformat:enable indent
if !["1", "true", "yes"].contains(ProcessInfo.processInfo.environment["MAS_NO_AUTO_INDEX"]?.lowercased()) {
let installedAppPathSet = Set(installedApps.map(\.path))
for installedAppURL in applicationsFolderURLs.flatMap(\.installedAppURLs)
where !installedAppPathSet.contains(installedAppURL.filePath) { // swiftformat:disable:this indent
MAS.printer.warning(
"Found a likely App Store app that is not indexed in Spotlight in ",
installedAppURL.filePath,
"""
Indexing now, which will not complete until sometime after mas exits
Disable auto-indexing via: export MAS_NO_AUTO_INDEX=1
""",
separator: "",
)
Task {
do {
_ = try await run(
"/usr/bin/mdimport",
installedAppURL.filePath,
errorMessage: "Failed to index the Spotlight data for \(installedAppURL.filePath)",
)
} catch {
MAS.printer.error(error: error)
}
}
}
}
continuation.resume(returning: installedApps)
}
query.start()
}
}
================================================
FILE: Sources/mas/Errors/MASError.swift
================================================
//
// MASError.swift
// mas
//
// Copyright © 2015 mas-cli. All rights reserved.
//
enum MASError: Error {
case error(String, error: (any Error)? = nil, separator: String = ":\n", separatorAndErrorReplacement: String = "")
case noCatalogAppsFound(for: String)
case unknownAppID(AppID)
case unparsableURL(String)
static func error(
_ message: String,
error: String?,
separator: String = ":\n",
separatorAndErrorReplacement: String = "",
) -> Self {
.error(
message,
error: error.map { Self.error($0) },
separator: separator,
separatorAndErrorReplacement: separatorAndErrorReplacement,
)
}
}
extension MASError: CustomStringConvertible {
var description: String {
switch self {
case let .error(message, error, separator, separatorAndErrorReplacement):
"\(message)\(error.map { "\(separator)\($0)" } ?? separatorAndErrorReplacement)"
case let .noCatalogAppsFound(searchTerm):
"No apps found in the App Store for search term: \(searchTerm)"
case let .unknownAppID(appID):
"No apps found in the App Store for \(appID)"
case let .unparsableURL(string):
"Failed to parse URL from \(string)"
}
}
}
================================================
FILE: Sources/mas/Models/AppID.swift
================================================
//
// AppID.swift
// mas
//
// Copyright © 2024 mas-cli. All rights reserved.
//
enum AppID: CustomStringConvertible {
case adamID(ADAMID)
case bundleID(String)
var description: String {
switch self {
case let .adamID(adamID):
"ADAM ID \(adamID)"
case let .bundleID(bundleID):
"bundle ID \(bundleID)"
}
}
var notInstalledMessage: String {
"No installed apps with \(self)"
}
init(from string: String, forceBundleID: Bool = false) {
guard !forceBundleID, let adamID = ADAMID(string) else {
self = .bundleID(string)
return
}
self = .adamID(adamID)
}
}
extension [AppID] { // swiftlint:disable:this file_types_order
func lookupCatalogApps(using lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp)
async -> [CatalogApp] { // swiftformat:disable:this indent
await concurrentCompactMap(attemptingTo: "lookup app for", lookupAppFromAppID)
}
}
typealias ADAMID = UInt64
================================================
FILE: Sources/mas/Models/CatalogApp.swift
================================================
//
// CatalogApp.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
struct CatalogApp {
let adamID: ADAMID
let appStorePageURLString: String
let bundleID: String
let fileSizeBytes: String
let formattedPrice: String?
let minimumOSVersion: String
let name: String
let releaseDate: String
let sellerName: String
let sellerURLString: String?
let supportedDevices: [String]? // swiftlint:disable:this discouraged_optional_collection
let version: String
var displayPrice: String {
formattedPrice ?? "?"
}
init(
adamID: ADAMID = 0,
appStorePageURLString: String = "",
bundleID: String = "",
fileSizeBytes: String = "?",
formattedPrice: String? = "?",
minimumOSVersion: String = "",
name: String = "",
releaseDate: String = "",
sellerName: String = "",
sellerURLString: String? = nil,
supportedDevices: [String]? = nil, // swiftlint:disable:this discouraged_optional_collection
version: String = "",
) {
self.adamID = adamID
self.appStorePageURLString = appStorePageURLString
self.bundleID = bundleID
self.fileSizeBytes = fileSizeBytes
self.formattedPrice = formattedPrice
self.minimumOSVersion = minimumOSVersion
self.name = name
self.releaseDate = releaseDate
self.sellerName = sellerName
self.sellerURLString = sellerURLString
self.supportedDevices = supportedDevices
self.version = version
}
func with(minimumOSVersion: String) -> Self {
.init(
adamID: adamID,
appStorePageURLString: appStorePageURLString,
bundleID: bundleID,
fileSizeBytes: fileSizeBytes,
formattedPrice: formattedPrice,
minimumOSVersion: minimumOSVersion,
name: name,
releaseDate: releaseDate,
sellerName: sellerName,
sellerURLString: sellerURLString,
supportedDevices: supportedDevices,
version: version,
)
}
}
extension CatalogApp: Decodable {
enum CodingKeys: String, CodingKey {
case adamID = "trackId"
case appStorePageURLString = "trackViewUrl"
case bundleID = "bundleId"
case fileSizeBytes
case formattedPrice
case minimumOSVersion = "minimumOsVersion"
case name = "trackName"
case releaseDate = "currentVersionReleaseDate"
case sellerName
case sellerURLString = "sellerUrl"
case supportedDevices
case version
}
}
================================================
FILE: Sources/mas/Models/CatalogAppResults.swift
================================================
//
// CatalogAppResults.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
struct CatalogAppResults: Decodable { // swiftlint:disable:next unused_declaration
let resultCount: Int // periphery:ignore
let results: [CatalogApp]
}
================================================
FILE: Sources/mas/Models/InstalledApp.swift
================================================
//
// InstalledApp.swift
// mas
//
// Copyright © 2018 mas-cli. All rights reserved.
//
struct InstalledApp {
let adamID: ADAMID
let bundleID: String
let name: String
let path: String
let version: String
var isTestFlight: Bool {
adamID == 0
}
func matches(_ appID: AppID) -> Bool {
switch appID {
case let .adamID(adamID):
self.adamID == adamID
case let .bundleID(bundleID):
self.bundleID == bundleID
}
}
}
extension [InstalledApp] {
func filter(for appIDs: [AppID]) -> [Element] {
appIDs.isEmpty
? self // swiftformat:disable:this indent
: appIDs.flatMap { appID in
let installedApps = filter { $0.matches(appID) }
if installedApps.isEmpty {
MAS.printer.error(appID.notInstalledMessage)
}
return installedApps
}
}
}
================================================
FILE: Sources/mas/Models/OutdatedApp.swift
================================================
//
// OutdatedApp.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import ArgumentParser
private import Atomics
private import Foundation
private import StoreFoundation
typealias OutdatedApp = (
installedApp: InstalledApp,
newVersion: String,
)
extension [InstalledApp] {
func outdatedApps(
filterFor appIDs: [AppID], // swiftlint:disable:next unneeded_escaping
lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp,
accuracy: OutdatedAccuracy,
shouldCheckMinimumOSVersion: Bool,
shouldWarnIfUnknownApp: Bool,
) async -> [OutdatedApp] {
@Sendable
func installableCatalogApp(from installedApp: InstalledApp) async -> CatalogApp? {
do {
let catalogApp = try await lookupAppFromAppID(.bundleID(installedApp.bundleID))
return shouldCheckMinimumOSVersion // swiftformat:disable indent
&& UniversalSemVerInt(from: catalogApp.minimumOSVersion).flatMap { minimumOSVersion in
ProcessInfo.processInfo.isOperatingSystemAtLeast(
OperatingSystemVersion(
majorVersion: minimumOSVersion.majorInteger,
minorVersion: minimumOSVersion.minorInteger,
patchVersion: minimumOSVersion.patchInteger,
),
)
}
== false ? nil : catalogApp
} catch { // swiftformat:enable indent
if let error = error as? MASError, case MASError.unknownAppID = error {
if shouldWarnIfUnknownApp {
MAS.printer.warning(error, "; was expected to identify: ", installedApp.name, separator: "")
}
} else {
MAS.printer.error(error: error)
}
return nil
}
}
return await filter(for: appIDs).concurrentCompactMap(
accuracy == .accurate
? { @Sendable installedApp in // swiftformat:disable indent
if shouldCheckMinimumOSVersion, await installableCatalogApp(from: installedApp) == nil {
return nil
}
return await withCheckedContinuation { continuation in
Task {
let alreadyResumed = ManagedAtomic(false)
do {
try await AppStore.install.app(withADAMID: installedApp.adamID) { appStoreVersion, shouldOutput in
if
shouldOutput,
let appStoreVersion,
installedApp.version != appStoreVersion,
!alreadyResumed.exchange(true, ordering: .acquiringAndReleasing)
{
continuation.resume(returning: OutdatedApp(installedApp, appStoreVersion))
}
return true
}
} catch {
MAS.printer.error(error: error)
}
if !alreadyResumed.load(ordering: .acquiring) {
continuation.resume(returning: nil)
}
}
}
}
: { @Sendable installedApp in
await installableCatalogApp(from: installedApp).flatMap { catalogApp in
UniversalSemVer(from: installedApp.version).compareSemVerAndBuild(to: .init(from: catalogApp.version))
== .orderedAscending ? OutdatedApp(installedApp, catalogApp.version) : nil
}
},
) // swiftformat:enable indent
}
}
================================================
FILE: Sources/mas/Network/URL.swift
================================================
//
// URL.swift
// mas
//
// Copyright © 2024 mas-cli. All rights reserved.
//
internal import AppKit
private import Foundation
private import ObjectiveC
extension URL {
var filePath: String {
.init(path(percentEncoded: false).dropLast { $0 == "/" })
}
func open(configuration: NSWorkspace.OpenConfiguration = NSWorkspace.OpenConfiguration()) async throws {
try await NSWorkspace.shared.open(self, configuration: configuration)
}
}
================================================
FILE: Sources/mas/Utilities/Collection.swift
================================================
//
// Collection.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
extension Collection {
func dropLast(while predicate: (Element) throws -> Bool) rethrows -> SubSequence {
try indices.reversed().first { try !predicate(self[$0]) }.map { self[...$0] } ?? self[endIndex...]
}
}
extension Collection where Element: Sendable {
func concurrentMap<T: Sendable>(
maxConcurrentTaskCount: Int = defaultMaxConcurrentTaskCount,
_ transform: @escaping @Sendable (Element) async -> T,
) async -> [T] { // swiftlint:disable:next force_unwrapping
await concurrentTransform(maxConcurrentTaskCount: maxConcurrentTaskCount, transform).map { $0! }
}
func concurrentMap<T: Sendable>(
maxConcurrentTaskCount: Int = defaultMaxConcurrentTaskCount,
_ transform: @escaping @Sendable (Element) async throws -> T,
) async rethrows -> [T] { // periphery:ignore
try await concurrentTransform(maxConcurrentTaskCount: maxConcurrentTaskCount, transform).map { $0! }
} // swiftlint:disable:previous force_unwrapping
func concurrentCompactMap<T: Sendable>(
maxConcurrentTaskCount: Int = defaultMaxConcurrentTaskCount,
_ transform: @escaping @Sendable (Element) async -> T?,
) async -> [T] {
await concurrentTransform(maxConcurrentTaskCount: maxConcurrentTaskCount, transform).compactMap(\.self)
}
func concurrentCompactMap<T: Sendable>(
maxConcurrentTaskCount: Int = defaultMaxConcurrentTaskCount,
_ transform: @escaping @Sendable (Element) async throws -> T?,
) async rethrows -> [T] { // periphery:ignore
try await concurrentTransform(maxConcurrentTaskCount: maxConcurrentTaskCount, transform).compactMap(\.self)
}
func concurrentCompactMap<T: Sendable, E: Error>(
attemptingTo perform: String,
maxConcurrentTaskCount: Int = defaultMaxConcurrentTaskCount,
_ transform: @escaping @Sendable (Element) async throws(E) -> T?,
) async -> [T] {
await concurrentCompactMap(maxConcurrentTaskCount: maxConcurrentTaskCount) { element in
do {
return try await transform(element)
} catch {
MAS.printer.error(error is MASError ? [] : ["Failed to", perform, element], error: error)
return nil
}
}
}
private func concurrentTransform<T: Sendable>(
maxConcurrentTaskCount: Int,
_ transform: @escaping @Sendable (Element) async throws -> T?,
) async rethrows -> [T?] {
try await withThrowingTaskGroup(of: (index: Int, result: T?).self) { group in
var iterator = enumerated().makeIterator()
func addNextTask() {
if let next = iterator.next() {
group.addTask {
(next.offset, try await transform(next.element))
}
}
}
for _ in 0..<Swift.min(count, maxConcurrentTaskCount) {
addNextTask()
}
return try await group.reduce(into: [T?](repeating: nil, count: count)) { results, indexedResult in
results[indexedResult.index] = indexedResult.result
addNextTask()
}
}
}
}
private let defaultMaxConcurrentTaskCount = 16
================================================
FILE: Sources/mas/Utilities/FileHandle.swift
================================================
//
// FileHandle.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
private import Darwin
internal import Foundation
extension FileHandle {
var isTerminal: Bool {
isatty(fileDescriptor) != 0
}
}
================================================
FILE: Sources/mas/Utilities/Group.swift
================================================
//
// Group.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import Darwin
private extension gid_t {
var nameAndID: String {
"\(unsafe String(cString: unsafe getgrgid(self).pointee.gr_name).quoted) (\(self))"
}
}
func set(effectiveGID gid: gid_t) throws {
guard setegid(gid) == 0 else {
throw MASError.error("Failed to switch effective group from \(getegid().nameAndID) to \(gid.nameAndID)")
}
}
func reset(effectiveGID gid: gid_t) {
do {
try set(effectiveGID: gid)
} catch {
MAS.printer.warning(error: error)
}
}
================================================
FILE: Sources/mas/Utilities/KeyPath.swift
================================================
//
// KeyPath.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
prefix func ! <Root>(keyPath: KeyPath<Root, Bool>) -> (Root) -> Bool { // swiftlint:disable:this static_operator
{ !$0[keyPath: keyPath] }
}
================================================
FILE: Sources/mas/Utilities/Optional.swift
================================================
//
// Optional.swift
// mas
//
// Copyright © 2026 mas-cli. All rights reserved.
//
extension Optional {
// periphery:ignore
func map<E: Error, U: ~Copyable>(_ transform: (Wrapped) async throws(E) -> U) async throws(E) -> U? {
guard let self else { // swiftlint:disable:previous unused_declaration
return nil
}
return try await transform(self)
}
func flatMap<E: Error, U: ~Copyable>(_ transform: (Wrapped) async throws(E) -> U?) async throws(E) -> U? {
guard let self else {
return nil
}
return try await transform(self)
}
}
================================================
FILE: Sources/mas/Utilities/Pipe.swift
================================================
//
// Pipe.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//
internal import Foundation
extension Pipe {
func readToEnd(encoding: String.Encoding = .utf8) throws -> String? {
try fileHandleForReading.readToEnd().flatMap { .init(data: $0, encoding: encoding) }
}
}
================================================
FILE: Sources/mas/Utilities/Printer.swift
================================================
//
// Printer.swift
// mas
//
// Copyright © 2016 mas-cli. All rights reserved.
//
private import ArgumentParser
private import Atomics
internal import Foundation
/// Prints to `stdout` and `stderr` with ANSI color codes when connected to a
/// terminal.
struct Printer {
private let errorCounter = ManagedAtomic<UInt64>(0)
var errorCount: UInt64 {
errorCounter.load(ordering: .acquiring)
}
func resetErrorCount() { // periphery:ignore
errorCounter.store(0, ordering: .releasing) // swiftlint:disable:previous unused_declaration
}
/// Prints to `stdout`.
@_disfavoredOverload
func info(_ items: Any..., separator: String = " ", terminator: String = "\n") {
info(items, separator: separator, terminator: terminator)
}
/// Prints to `stdout`.
func info(_ items: [Any], separator: String = " ", terminator: String = "\n") {
print(items.map(String.init(describing:)), separator: separator, terminator: terminator, to: .standardOutput)
}
/// Prints to `stdout`, prefixed with "==> "; if connected to a terminal, the
/// prefix is blue.
@_disfavoredOverload
func notice(_ items: Any..., separator: String = " ", terminator: String = "\n") {
notice(items, separator: separator, terminator: terminator)
}
/// Prints to `stdout`, prefixed with "==> "; if connected to a terminal, the
/// prefix is blue.
func notice(_ items: [Any], separator: String = " ", terminator: String = "\n") {
print(items, prefix: "==>", format: "1;34", separator: separator, terminator: terminator, to: .standardOutput)
}
/// Prints to `stderr`, prefixed with "Warning: "; if connected to a terminal,
/// the prefix is yellow & underlined.
@_disfavoredOverload
func warning(_ items: Any..., error: (any Error)? = nil, separator: String = " ", terminator: String = "\n") {
warning(items, error: error, separator: separator, terminator: terminator)
}
/// Prints to `stderr`, prefixed with "Warning: "; if connected to a terminal,
/// the prefix is yellow & underlined.
func warning(_ items: [Any], error: (any Error)? = nil, separator: String = " ", terminator: String = "\n") {
problem(items, prefix: "Warning:", format: "4;33", error: error, separator: separator, terminator: terminator)
}
/// Prints to `stderr`, prefixed with "Error: "; if connected to a terminal,
/// the prefix is red & underlined.
@_disfavoredOverload
func error(_ items: Any..., error: (any Error)? = nil, separator: String = " ", terminator: String = "\n") {
self.error(items, error: error, separator: separator, terminator: terminator)
}
/// Prints to `stderr`, prefixed with "Error: "; if connected to a terminal,
/// the prefix is red & underlined.
func error(_ items: [Any], error: (any Error)? = nil, separator: String = " ", terminator: String = "\n") {
errorCounter.wrappingIncrement(ordering: .relaxed)
problem(items, prefix: errorPrefix, format: errorFormat, error: error, separator: separator, terminator: terminator)
}
func clearCurrentLine(of fileHandle: FileHandle) {
if fileHandle.isTerminal {
do {
try fileHandle.write(contentsOf: Data("\(csi)2K\(csi)0G".utf8))
} catch {
// Do nothing
}
}
}
private func problem(
_ items: [Any],
prefix: String,
format: String,
error: (any Error)?,
separator: String,
terminator: String,
) {
guard !items.isEmpty || (error != nil && !(error is ExitCode)) else {
return
}
print(
items,
prefix: prefix,
format: format,
separator: separator,
terminator: error.map { error in
let errorDescription = String(describing: error)
return "\(errorDescription.isEmpty ? "" : items.isEmpty ? " " : "\n")\(errorDescription)\(terminator)"
}
?? terminator, // swiftformat:disable:this indent
to: .standardE
gitextract_u6q5_k68/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-bug-report.yaml
│ │ └── 02-feature-request.yaml
│ ├── actionlint.yaml
│ ├── dependabot.yaml
│ ├── release.yaml
│ └── workflows/
│ ├── build-test.yaml
│ ├── codeql.yaml
│ ├── release-published.yaml
│ └── tag-pushed.yaml
├── .gitignore
├── .markdownlint-cli2.yaml
├── .periphery.yaml
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── .xcode-version
├── .yamllint.yaml
├── Brewfile
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Documentation/
│ ├── Sample.swift
│ └── style.md
├── LICENSE
├── Package.resolved
├── Package.swift
├── Plugins/
│ └── MASBuildToolPlugin/
│ └── MASBuildToolPlugin.swift
├── README.md
├── Scripts/
│ ├── _setup_script
│ ├── bootstrap
│ ├── build
│ ├── clean
│ ├── format
│ ├── generate_manual
│ ├── generate_token
│ ├── lint
│ ├── package
│ ├── prebuild
│ ├── release_cancel
│ ├── release_start
│ ├── setup_workflow_repo
│ ├── test
│ ├── update_dependencies
│ ├── update_headers
│ └── version
├── Sources/
│ ├── PrivateFrameworks/
│ │ ├── PrivateFrameworks.c
│ │ └── include/
│ │ ├── CommerceKit/
│ │ │ ├── CKDownloadDirectory.h
│ │ │ ├── CKDownloadQueue.h
│ │ │ ├── CKDownloadQueueObserver-Protocol.h
│ │ │ ├── CKPurchaseController.h
│ │ │ ├── CKServiceInterface.h
│ │ │ ├── CommerceKit.h
│ │ │ └── module.modulemap
│ │ └── StoreFoundation/
│ │ ├── ISAccountService-Protocol.h
│ │ ├── ISServiceProxy.h
│ │ ├── ISStoreAccount.h
│ │ ├── SSDownload.h
│ │ ├── SSDownloadMetadata.h
│ │ ├── SSDownloadPhase.h
│ │ ├── SSDownloadStatus.h
│ │ ├── SSPurchase.h
│ │ ├── SSPurchaseResponse.h
│ │ ├── StoreFoundation.h
│ │ └── module.modulemap
│ └── mas/
│ ├── AppStore/
│ │ ├── AppStoreAction+download.swift
│ │ ├── AppStoreAction.swift
│ │ └── Region.swift
│ ├── Commands/
│ │ ├── Config.swift
│ │ ├── Get.swift
│ │ ├── Home.swift
│ │ ├── Install.swift
│ │ ├── List.swift
│ │ ├── Lookup.swift
│ │ ├── Lucky.swift
│ │ ├── MAS.swift
│ │ ├── Open.swift
│ │ ├── OptionGroups/
│ │ │ ├── CatalogAppIDsOptionGroup.swift
│ │ │ ├── ForceBundleIDOptionGroup.swift
│ │ │ ├── ForceOptionGroup.swift
│ │ │ ├── InstalledAppIDsOptionGroup.swift
│ │ │ ├── OutdatedAccuracy.swift
│ │ │ ├── OutdatedAppOptionGroup.swift
│ │ │ ├── SearchTermOptionGroup.swift
│ │ │ └── VerboseOptionGroup.swift
│ │ ├── Outdated.swift
│ │ ├── Reset.swift
│ │ ├── Search.swift
│ │ ├── Seller.swift
│ │ ├── SignOut.swift
│ │ ├── Uninstall.swift
│ │ ├── Update.swift
│ │ └── Version.swift
│ ├── Controllers/
│ │ ├── CatalogApp+ITunesSearch.swift
│ │ └── InstalledApp+Spotlight.swift
│ ├── Errors/
│ │ └── MASError.swift
│ ├── Models/
│ │ ├── AppID.swift
│ │ ├── CatalogApp.swift
│ │ ├── CatalogAppResults.swift
│ │ ├── InstalledApp.swift
│ │ └── OutdatedApp.swift
│ ├── Network/
│ │ └── URL.swift
│ └── Utilities/
│ ├── Collection.swift
│ ├── FileHandle.swift
│ ├── Group.swift
│ ├── KeyPath.swift
│ ├── Optional.swift
│ ├── Pipe.swift
│ ├── Printer.swift
│ ├── Process.swift
│ ├── ProcessInfo.swift
│ ├── RangeReplaceableCollection.swift
│ ├── Sequence.swift
│ ├── String.swift
│ ├── Sudo.swift
│ ├── User.swift
│ ├── UserAndGroup.swift
│ └── Version+SemVer.swift
├── Tests/
│ └── MASTests/
│ ├── Commands/
│ │ ├── MASTests+Home.swift
│ │ ├── MASTests+List.swift
│ │ ├── MASTests+Lookup.swift
│ │ ├── MASTests+Search.swift
│ │ ├── MASTests+Seller.swift
│ │ └── MASTests+Version.swift
│ ├── Controllers/
│ │ └── MASTests+CatalogApp+ITunesSearch.swift
│ ├── Extensions/
│ │ └── Data.swift
│ ├── MASTests.swift
│ ├── Models/
│ │ ├── MASTests+CatalogApp.swift
│ │ └── MASTests+CatalogAppResults.swift
│ ├── Resources/
│ │ ├── bbedit.json
│ │ ├── slack-lookup.json
│ │ ├── slack.json
│ │ ├── things-lookup.json
│ │ └── things.json
│ └── Utilities/
│ └── Consequences.swift
└── contrib/
└── completion/
├── mas-completion.bash
└── mas.fish
SYMBOL INDEX (9 symbols across 9 files)
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h
function interface (line 8) | interface CKDownloadQueue : CKServiceInterface {
FILE: Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h
function interface (line 10) | interface CKPurchaseController : CKServiceInterface {
FILE: Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h
function interface (line 8) | interface ISStoreAccount : NSObject <NSSecureCoding> {
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h
function interface (line 8) | interface SSDownload : NSObject <NSSecureCoding> {
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h
type _NSZone (line 61) | struct _NSZone
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h
type _NSZone (line 20) | struct _NSZone
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h
type _NSZone (line 23) | struct _NSZone
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h
type _NSZone (line 40) | struct _NSZone
FILE: Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h
function interface (line 8) | interface SSPurchaseResponse : NSObject <NSSecureCoding> {
Condensed preview — 138 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (519K chars).
[
{
"path": ".editorconfig",
"chars": 487,
"preview": "#\n# .editorconfig\n# mas\n#\n# EditorConfig 0.17.2\n#\n\nroot = true\n\n[*]\ncharset = utf-8\ncontinuation_indent_size = 0\nend_of_"
},
{
"path": ".gitattributes",
"chars": 102,
"preview": "# Do not remove potentially intentional trailing spaces in Markdown.\n**/*.md whitespace=-blank-at-eol\n"
},
{
"path": ".github/CODEOWNERS",
"chars": 52,
"preview": "#\n# .github/CODEOWNERS\n#\n\n/.github/ @mas-cli/admins\n"
},
{
"path": ".github/ISSUE_TEMPLATE/01-bug-report.yaml",
"chars": 2045,
"preview": "---\nname: Bug Report\ndescription: Report a bug.\nlabels: [\\U0001F41B bug]\nbody:\n- type: textarea\n id: config\n attribute"
},
{
"path": ".github/ISSUE_TEMPLATE/02-feature-request.yaml",
"chars": 1808,
"preview": "---\nname: Feature Request\ndescription: Request a feature.\nlabels: [\\U0001F195 feature request]\nbody:\n- type: textarea\n "
},
{
"path": ".github/actionlint.yaml",
"chars": 109,
"preview": "#\n# .github/actionlint.yaml\n# mas\n#\n# actionlint 1.7.11\n#\n---\nself-hosted-runner:\n labels: [macos-26-intel]\n"
},
{
"path": ".github/dependabot.yaml",
"chars": 339,
"preview": "---\nversion: 2\nupdates:\n- package-ecosystem: github-actions\n schedule:\n interval: daily\n directory: /\n labels: [📚 "
},
{
"path": ".github/release.yaml",
"chars": 163,
"preview": "---\nchangelog:\n categories:\n - title: 🚀 Features\n labels: [🆕 feature request]\n - title: 🐛 Bug Fixes\n labels: [🐛"
},
{
"path": ".github/workflows/build-test.yaml",
"chars": 1702,
"preview": "#\n# .github/workflows/build-test.yaml\n#\n---\nname: Build, Test, and Lint\non:\n pull_request:\n branches: [main]\n push:"
},
{
"path": ".github/workflows/codeql.yaml",
"chars": 1419,
"preview": "#\n# .github/workflows/codeql.yaml\n#\n---\nname: CodeQL\non:\n push:\n branches: [main]\n pull_request:\n branches: [mai"
},
{
"path": ".github/workflows/release-published.yaml",
"chars": 1623,
"preview": "#\n# .github/workflows/release-published.yaml\n#\n---\nname: release-published\non:\n release:\n types: [published]\npermiss"
},
{
"path": ".github/workflows/tag-pushed.yaml",
"chars": 3546,
"preview": "#\n# .github/workflows/tag-pushed.yaml\n#\n---\nname: tag-pushed\non:\n push:\n tags: ['**']\npermissions:\n contents: write"
},
{
"path": ".gitignore",
"chars": 51,
"preview": "/.build/\n/.idea/\n/.swiftpm/\n/.vscode/\n.DS_Store\n*~\n"
},
{
"path": ".markdownlint-cli2.yaml",
"chars": 1195,
"preview": "# yamllint disable-line rule:line-length\n# yaml-language-server: $schema=https://raw.githubusercontent.com/DavidAnson/ma"
},
{
"path": ".periphery.yaml",
"chars": 176,
"preview": "#\n# .periphery.yaml\n# mas\n#\n# Periphery 3.6.0\n#\n---\ncolor: always\ndisable_update_check: true\nquiet: true\nrelative_result"
},
{
"path": ".swift-version",
"chars": 4,
"preview": "6.2\n"
},
{
"path": ".swiftformat",
"chars": 2293,
"preview": "#\n# .swiftformat\n# mas\n#\n# SwiftFormat 0.60.1\n#\n\n# Disabled rules (enabled by default)\n--disable hoistAwait\n--disable ho"
},
{
"path": ".swiftlint.yml",
"chars": 2934,
"preview": "#\n# .swiftlint.yml\n# mas\n#\n# SwiftLint 0.63.2\n#\n---\nexcluded:\n- .build/\n- .idea/\n- .swiftpm/\n- .vscode/\nopt_in_rules:\n- "
},
{
"path": ".xcode-version",
"chars": 5,
"preview": "26.3\n"
},
{
"path": ".yamllint.yaml",
"chars": 1558,
"preview": "#\n# .yamllint.yaml\n# mas\n#\n# yamllint 1.38.0\n#\n---\nextends: default\nlocale: en_US.UTF-8\nignore-from-file: .gitignore\nrul"
},
{
"path": "Brewfile",
"chars": 424,
"preview": "brew \"actionlint\" # 1.7.11\nbrew \"gh\" # 2.87.3\nbrew \"git\" # 2.53.0\nbrew \"ipsw\" "
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3272,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open & welcoming environment, we "
},
{
"path": "CONTRIBUTING.md",
"chars": 3426,
"preview": "# Contributing\n\nPull requests (PRs) are welcome from everyone.\n\nBy participating in this project, you agree to abide by "
},
{
"path": "Documentation/Sample.swift",
"chars": 2341,
"preview": "//\n// Sample.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\n// MARK: Types & naming\n\n/// The firs"
},
{
"path": "Documentation/style.md",
"chars": 1658,
"preview": "# All Files\n\n- Before committing, run `Scripts/lint` to detect linting violations\n- Run `Scripts/format` to automaticall"
},
{
"path": "LICENSE",
"chars": 1095,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Andrew Naylor, Ross Goldberg\n\nPermission is hereby granted, free of charge, to"
},
{
"path": "Package.resolved",
"chars": 2295,
"preview": "{\n\t\"originHash\" : \"fe336dc5a91893be96812ec91baba97112f3407b3c398b918ac3eeebc5bc9781\",\n\t\"pins\" : [\n\t\t{\n\t\t\t\"identity\" : \"b"
},
{
"path": "Package.swift",
"chars": 1884,
"preview": "// swift-tools-version:6.2\n\nprivate import PackageDescription\n\nprivate let swiftSettings = [\n\tSwiftSetting\n\t.enableUpcom"
},
{
"path": "Plugins/MASBuildToolPlugin/MASBuildToolPlugin.swift",
"chars": 664,
"preview": "//\n// MASBuildToolPlugin.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Foundation"
},
{
"path": "README.md",
"chars": 13642,
"preview": "<h1 align=\"center\">\n\n\n\n</h1>\n\n[\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueueObserver-Protocol.h",
"chars": 493,
"preview": "//\n// CKDownloadQueueObserver-Protocol.h\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\n@protocol CKDow"
},
{
"path": "Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h",
"chars": 1974,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h",
"chars": 273,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h",
"chars": 492,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/CommerceKit/module.modulemap",
"chars": 173,
"preview": "module CommerceKit [system] [no_undeclared_includes] {\n\trequires macos, objc\n\tuse StoreFoundation\n\tlink framework \"Comme"
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h",
"chars": 5443,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h",
"chars": 2586,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h",
"chars": 1710,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h",
"chars": 1944,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h",
"chars": 3430,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h",
"chars": 1044,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h",
"chars": 1157,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h",
"chars": 2205,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h",
"chars": 986,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h",
"chars": 818,
"preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.660, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
},
{
"path": "Sources/PrivateFrameworks/include/StoreFoundation/module.modulemap",
"chars": 180,
"preview": "module StoreFoundation [system] [no_undeclared_includes] {\n\trequires macos, objc\n\tuse Foundation\n\tlink framework \"StoreF"
},
{
"path": "Sources/mas/AppStore/AppStoreAction+download.swift",
"chars": 16249,
"preview": "//\n// AppStoreAction+download.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\nprivate import Comme"
},
{
"path": "Sources/mas/AppStore/AppStoreAction.swift",
"chars": 1776,
"preview": "//\n// AppStoreAction.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParser"
},
{
"path": "Sources/mas/AppStore/Region.swift",
"chars": 6344,
"preview": "//\n// Region.swift\n// mas\n//\n// Copyright © 2024 mas-cli. All rights reserved.\n//\n\nprivate import Foundation\n\ntypealias "
},
{
"path": "Sources/mas/Commands/Config.swift",
"chars": 2613,
"preview": "//\n// Config.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Commands/Get.swift",
"chars": 791,
"preview": "//\n// Get.swift\n// mas\n//\n// Copyright © 2026 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\n\nextensio"
},
{
"path": "Sources/mas/Commands/Home.swift",
"chars": 1387,
"preview": "//\n// Home.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivate "
},
{
"path": "Sources/mas/Commands/Install.swift",
"chars": 790,
"preview": "//\n// Install.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\n\nexte"
},
{
"path": "Sources/mas/Commands/List.swift",
"chars": 1789,
"preview": "//\n// List.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivate "
},
{
"path": "Sources/mas/Commands/Lookup.swift",
"chars": 1764,
"preview": "//\n// Lookup.swift\n// mas\n//\n// Copyright © 2016 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Commands/Lucky.swift",
"chars": 1599,
"preview": "//\n// Lucky.swift\n// mas\n//\n// Copyright © 2017 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\n\nextens"
},
{
"path": "Sources/mas/Commands/MAS.swift",
"chars": 3333,
"preview": "//\n// MAS.swift\n// mas\n//\n// Copyright © 2021 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\ninternal "
},
{
"path": "Sources/mas/Commands/Open.swift",
"chars": 2501,
"preview": "//\n// Open.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import AppKit\ninternal import A"
},
{
"path": "Sources/mas/Commands/OptionGroups/CatalogAppIDsOptionGroup.swift",
"chars": 478,
"preview": "//\n// CatalogAppIDsOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Arg"
},
{
"path": "Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift",
"chars": 293,
"preview": "//\n// ForceBundleIDOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Argu"
},
{
"path": "Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift",
"chars": 222,
"preview": "//\n// ForceOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentPars"
},
{
"path": "Sources/mas/Commands/OptionGroups/InstalledAppIDsOptionGroup.swift",
"chars": 485,
"preview": "//\n// InstalledAppIDsOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import A"
},
{
"path": "Sources/mas/Commands/OptionGroups/OutdatedAccuracy.swift",
"chars": 590,
"preview": "//\n// OutdatedAccuracy.swift\n// mas\n//\n// Copyright © 2026 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentPar"
},
{
"path": "Sources/mas/Commands/OptionGroups/OutdatedAppOptionGroup.swift",
"chars": 424,
"preview": "//\n// OutdatedAppOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Argume"
},
{
"path": "Sources/mas/Commands/OptionGroups/SearchTermOptionGroup.swift",
"chars": 402,
"preview": "//\n// SearchTermOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Argume"
},
{
"path": "Sources/mas/Commands/OptionGroups/VerboseOptionGroup.swift",
"chars": 267,
"preview": "//\n// VerboseOptionGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentPa"
},
{
"path": "Sources/mas/Commands/Outdated.swift",
"chars": 2094,
"preview": "//\n// Outdated.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\npriv"
},
{
"path": "Sources/mas/Commands/Reset.swift",
"chars": 3492,
"preview": "//\n// Reset.swift\n// mas\n//\n// Copyright © 2016 mas-cli. All rights reserved.\n//\n\nprivate import AppKit\ninternal import "
},
{
"path": "Sources/mas/Commands/Search.swift",
"chars": 1630,
"preview": "//\n// Search.swift\n// mas\n//\n// Copyright © 2016 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Commands/Seller.swift",
"chars": 1593,
"preview": "//\n// Seller.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Commands/SignOut.swift",
"chars": 480,
"preview": "//\n// SignOut.swift\n// mas\n//\n// Copyright © 2016 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\npriva"
},
{
"path": "Sources/mas/Commands/Uninstall.swift",
"chars": 4145,
"preview": "//\n// Uninstall.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\npri"
},
{
"path": "Sources/mas/Commands/Update.swift",
"chars": 1730,
"preview": "//\n// Update.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Commands/Version.swift",
"chars": 343,
"preview": "//\n// Version.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\n\nexte"
},
{
"path": "Sources/mas/Controllers/CatalogApp+ITunesSearch.swift",
"chars": 5161,
"preview": "//\n// CatalogApp+ITunesSearch.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\ninternal import Foun"
},
{
"path": "Sources/mas/Controllers/InstalledApp+Spotlight.swift",
"chars": 3994,
"preview": "//\n// InstalledApp+Spotlight.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Atomic"
},
{
"path": "Sources/mas/Errors/MASError.swift",
"chars": 1150,
"preview": "//\n// MASError.swift\n// mas\n//\n// Copyright © 2015 mas-cli. All rights reserved.\n//\n\nenum MASError: Error {\n\tcase error("
},
{
"path": "Sources/mas/Models/AppID.swift",
"chars": 933,
"preview": "//\n// AppID.swift\n// mas\n//\n// Copyright © 2024 mas-cli. All rights reserved.\n//\n\nenum AppID: CustomStringConvertible {\n"
},
{
"path": "Sources/mas/Models/CatalogApp.swift",
"chars": 2244,
"preview": "//\n// CatalogApp.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nstruct CatalogApp {\n\tlet adamID: "
},
{
"path": "Sources/mas/Models/CatalogAppResults.swift",
"chars": 248,
"preview": "//\n// CatalogAppResults.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nstruct CatalogAppResults: "
},
{
"path": "Sources/mas/Models/InstalledApp.swift",
"chars": 772,
"preview": "//\n// InstalledApp.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nstruct InstalledApp {\n\tlet adam"
},
{
"path": "Sources/mas/Models/OutdatedApp.swift",
"chars": 2936,
"preview": "//\n// OutdatedApp.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import ArgumentParser\np"
},
{
"path": "Sources/mas/Network/URL.swift",
"chars": 443,
"preview": "//\n// URL.swift\n// mas\n//\n// Copyright © 2024 mas-cli. All rights reserved.\n//\n\ninternal import AppKit\nprivate import Fo"
},
{
"path": "Sources/mas/Utilities/Collection.swift",
"chars": 2928,
"preview": "//\n// Collection.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nextension Collection {\n\tfunc drop"
},
{
"path": "Sources/mas/Utilities/FileHandle.swift",
"chars": 219,
"preview": "//\n// FileHandle.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Darwin\ninternal im"
},
{
"path": "Sources/mas/Utilities/Group.swift",
"chars": 562,
"preview": "//\n// Group.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Darwin\n\nprivate extens"
},
{
"path": "Sources/mas/Utilities/KeyPath.swift",
"chars": 226,
"preview": "//\n// KeyPath.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprefix func ! <Root>(keyPath: KeyPat"
},
{
"path": "Sources/mas/Utilities/Optional.swift",
"chars": 552,
"preview": "//\n// Optional.swift\n// mas\n//\n// Copyright © 2026 mas-cli. All rights reserved.\n//\n\nextension Optional {\n\t// periphery:"
},
{
"path": "Sources/mas/Utilities/Pipe.swift",
"chars": 289,
"preview": "//\n// Pipe.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Foundation\n\nextension P"
},
{
"path": "Sources/mas/Utilities/Printer.swift",
"chars": 5409,
"preview": "//\n// Printer.swift\n// mas\n//\n// Copyright © 2016 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParser\nprivat"
},
{
"path": "Sources/mas/Utilities/Process.swift",
"chars": 1687,
"preview": "//\n// Process.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Foundation\nprivate i"
},
{
"path": "Sources/mas/Utilities/ProcessInfo.swift",
"chars": 1269,
"preview": "//\n// ProcessInfo.swift\n// mas\n//\n// Copyright © 2024 mas-cli. All rights reserved.\n//\n\ninternal import Darwin\nprivate i"
},
{
"path": "Sources/mas/Utilities/RangeReplaceableCollection.swift",
"chars": 348,
"preview": "//\n// RangeReplaceableCollection.swift\n// mas\n//\n// Copyright © 2026 mas-cli. All rights reserved.\n//\n\nextension RangeRe"
},
{
"path": "Sources/mas/Utilities/Sequence.swift",
"chars": 2090,
"preview": "//\n// Sequence.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nextension Sequence {\n\tfunc forEach<"
},
{
"path": "Sources/mas/Utilities/String.swift",
"chars": 2350,
"preview": "//\n// String.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Foundation\n\nextension "
},
{
"path": "Sources/mas/Utilities/Sudo.swift",
"chars": 1133,
"preview": "//\n// Sudo.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParser\nprivate i"
},
{
"path": "Sources/mas/Utilities/User.swift",
"chars": 560,
"preview": "//\n// User.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Darwin\n\nprivate extensi"
},
{
"path": "Sources/mas/Utilities/UserAndGroup.swift",
"chars": 1358,
"preview": "//\n// UserAndGroup.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\ninternal import Darwin\n\nfunc ru"
},
{
"path": "Sources/mas/Utilities/Version+SemVer.swift",
"chars": 5455,
"preview": "//\n// Version+SemVer.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import BigInt\ninterna"
},
{
"path": "Tests/MASTests/Commands/MASTests+Home.swift",
"chars": 452,
"preview": "//\n// MASTests+Home.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParser\n"
},
{
"path": "Tests/MASTests/Commands/MASTests+List.swift",
"chars": 1052,
"preview": "//\n// MASTests+List.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParser\n"
},
{
"path": "Tests/MASTests/Commands/MASTests+Lookup.swift",
"chars": 1173,
"preview": "//\n// MASTests+Lookup.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParse"
},
{
"path": "Tests/MASTests/Commands/MASTests+Search.swift",
"chars": 857,
"preview": "//\n// MASTests+Search.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParse"
},
{
"path": "Tests/MASTests/Commands/MASTests+Seller.swift",
"chars": 458,
"preview": "//\n// MASTests+Seller.swift\n// mas\n//\n// Copyright © 2019 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentParse"
},
{
"path": "Tests/MASTests/Commands/MASTests+Version.swift",
"chars": 402,
"preview": "//\n// MASTests+Version.swift\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nprivate import ArgumentPars"
},
{
"path": "Tests/MASTests/Controllers/MASTests+CatalogApp+ITunesSearch.swift",
"chars": 1282,
"preview": "//\n// MASTests+CatalogApp+ITunesSearch.swift\n// mas\n//\n// Copyright © 2019 mas-cli. All rights reserved.\n//\n\nprivate imp"
},
{
"path": "Tests/MASTests/Extensions/Data.swift",
"chars": 1043,
"preview": "//\n// Data.swift\n// mas\n//\n// Copyright © 2019 mas-cli. All rights reserved.\n//\n\nprivate import Foundation\n@testable pri"
},
{
"path": "Tests/MASTests/MASTests.swift",
"chars": 224,
"preview": "//\n// MASTests.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\n@testable private import mas\nintern"
},
{
"path": "Tests/MASTests/Models/MASTests+CatalogApp.swift",
"chars": 495,
"preview": "//\n// MASTests+CatalogApp.swift\n// mas\n//\n// Copyright © 2020 mas-cli. All rights reserved.\n//\n\nprivate import Foundatio"
},
{
"path": "Tests/MASTests/Models/MASTests+CatalogAppResults.swift",
"chars": 738,
"preview": "//\n// MASTests+CatalogAppResults.swift\n// mas\n//\n// Copyright © 2025 mas-cli. All rights reserved.\n//\n\nprivate import Fo"
},
{
"path": "Tests/MASTests/Resources/bbedit.json",
"chars": 7295,
"preview": "{\n\t\"resultCount\": 1,\n\t\"results\": [\n\t\t{\n\t\t\t\"screenshotUrls\": [\n\t\t\t\t\"https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4"
},
{
"path": "Tests/MASTests/Resources/slack-lookup.json",
"chars": 3725,
"preview": "{\n\t\"resultCount\": 1,\n\t\"results\": [\n\t\t{\n\t\t\t\"screenshotUrls\": [\n\t\t\t\t\"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4"
},
{
"path": "Tests/MASTests/Resources/slack.json",
"chars": 161993,
"preview": "{\n\t\"resultCount\": 39,\n\t\"results\": [\n\t\t{\n\t\t\t\"screenshotUrls\": [\n\t\t\t\t\"https://is3-ssl.mzstatic.com/image/thumb/Purple113/v"
},
{
"path": "Tests/MASTests/Resources/things-lookup.json",
"chars": 5386,
"preview": "{\n\t\"screenshotUrls\": [\n\t\t\"https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/36/fe/ff/36feffbc-a07b-e61e-f0e5-88dcc44"
},
{
"path": "Tests/MASTests/Resources/things.json",
"chars": 66766,
"preview": "{\n\t\"resultCount\": 12,\n\t\"results\": [\n\t\t{\n\t\t\t\"screenshotUrls\": [\n\t\t\t\t\"https://is3-ssl.mzstatic.com/image/thumb/Purple123/v"
},
{
"path": "Tests/MASTests/Utilities/Consequences.swift",
"chars": 4189,
"preview": "//\n// Consequences.swift\n// mas\n//\n// Copyright © 2024 mas-cli. All rights reserved.\n//\n\ninternal import Foundation\n@tes"
},
{
"path": "contrib/completion/mas-completion.bash",
"chars": 650,
"preview": "#!/usr/bin/env bash\n\n_mas() {\n\tlocal cur prev words cword\n\tif declare -F _init_completions >/dev/null 2>&1; then\n\t\t_init"
},
{
"path": "contrib/completion/mas.fish",
"chars": 5211,
"preview": "# fish completions for mas\n\nfunction __fish_mas_list_available -d \"Lists applications available to install from the App "
}
]
About this extraction
This page contains the full source code of the mas-cli/mas GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 138 files (454.7 KB), approximately 144.0k tokens, and a symbol index with 9 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.