Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
/.idea/
/.swiftpm/
/.vscode/
/libexec/bin/mas
.DS_Store
*~
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ type_contents_order:
- type_method
- view_life_cycle_method
- ib_action
- other_method
- subscript
- other_method
unneeded_override:
affect_initializers: true
unused_import:
Expand Down
19 changes: 18 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ private let swiftSettings = [
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.strictMemorySafety(),
.treatAllWarnings(as: .error),
]

Expand All @@ -23,6 +24,7 @@ _ = Package(
.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.1"),
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0"),
.package(url: "https://github.com/mas-cli/swift-json.git", revision: "564110e7f56a573aafed49da379368a65faf0eb7"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.13.2"),
],
targets: [
Expand All @@ -33,6 +35,7 @@ _ = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "JSON", package: "swift-json"),
.product(name: "OrderedCollections", package: "swift-collections"),
"BigInt",
"PrivateFrameworks",
Expand Down
117 changes: 117 additions & 0 deletions Scripts/mas
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/bin/zsh -Ndefgku

# Copyright © 2026 mas-cli. All rights reserved.

setopt pipefail

path_or_simple_command() {
# shellcheck disable=SC2139
[[ -x "${1}" ]] && printf %s "${1}" || printf %s "${1:t}"
}

# shellcheck disable=SC2311
mas="$(path_or_simple_command "${0:A:h}/../libexec/bin/mas")"
readonly mas
# shellcheck disable=SC2311
column="$(path_or_simple_command /usr/bin/column)"
readonly column
# shellcheck disable=SC2311
jq="$(path_or_simple_command /usr/bin/jq)"
readonly jq

case "${1:-}" in
outdated)
# shellcheck disable=SC1056,SC1072,SC1073
{
# shellcheck disable=SC1083
{ exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -sr '
try (
(map(.adamID | tostring | length) | max) as $max_adam_id_length |
(map(.version // "" | length) | max) as $max_version_length |
.[] |
(.adamID | tostring) as $adam_id |
(.version // "") as $version |
[
" " * ($max_adam_id_length - ($adam_id | length)) + $adam_id,
.name,
"(" + $version + " " * ($max_version_length - ($version | length)) + " -> " + .newVersion + ")"
] |
join("\u001f")
) catch ("\u001B[4;31mError:\u001B[0m Invalid data from mas: \(.)\n" | halt_error(1))
' | "${column}" -ts $'\u001f'
} 4>&1 5>&2
;;
list|search)
{
{ exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -sr '
try (
(map(.adamID | tostring | length) | max) as $max_adam_id_length |
.[] |
(.adamID | tostring) as $adam_id |
[
" " * ($max_adam_id_length - ($adam_id | length)) + $adam_id,
.name,
"(" + (.version // "") + ")"
] |
join("\u001f")
) catch ("\u001B[4;31mError:\u001B[0m Invalid data from mas: \(.)\n" | halt_error(1))
' | "${column}" -ts $'\u001f'
} 4>&1 5>&2
;;
lookup|info)
{
{ exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -sr '
def numberCommas($n):
($n | abs | round | tostring) as $s |
if $n < 0 then "-" else "" end + ([$s | while(length > 0; .[:-3]) | .[-3:]] | reverse | join(","))
;
try (
{
"name": "App",
"version": "Version",
"formattedPrice": "Price",
"sellerName": "By",
"currentVersionReleaseDate": "Released",
"minimumOSVersion": "Minimum OS",
"fileSizeBytes": "Size",
"appStorePageURL": "From"
} as $key_map |
($key_map | values | map(length) | max) as $max_key_length |
[
.[]? as $in |
[
($key_map | keys_unsorted)[] |
select($in[.] != null) as $k |
"\($key_map[$k]) \("▁" * ($max_key_length - ($key_map[$k] | length) + 1)) \(
$in[$k] |
if $k == "fileSizeBytes" then
numberCommas(tonumber? / 1e6 // 0) + " MB"
elif $k == "currentVersionReleaseDate" then
(fromdateiso8601? | strflocaltime("%Y-%m-%d")) // .
else
.
end
)"
] |
join("\n")
] |
map(select(length > 0)) | if length > 0 then join("\n\n") else empty end
) catch ("\u001B[4;31mError:\u001B[0m Invalid data from mas: \(.)\n" | halt_error(1))
'
} 4>&1 5>&2
;;
config)
{
{ exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -r '
try (
(keys_unsorted | map(length) | max) as $max_key_length |
to_entries[] |
"\(.key) \("▁" * ($max_key_length - (.key | length) + 1)) \(.value)"
) catch ("\u001B[4;31mError:\u001B[0m Invalid data from mas: \(.)\n" | halt_error(1))
'
} 4>&1 5>&2
;;
*)
exec "${mas}" "${@}"
;;
esac
14 changes: 8 additions & 6 deletions Scripts/package
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ swift package generate-manual

mkdir -p "${installation_staging_folder}/bin"
mkdir -p "${installation_staging_folder}/etc/bash_completion.d"
mkdir -p "${installation_staging_folder}/libexec/bin"
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"
cp -c "$(swift build -c release --show-bin-path "${@:2}")/mas" "${installation_staging_folder}/libexec/bin/mas"
cp -c .build/plugins/GenerateManual/outputs/mas/mas.1 "${installation_staging_folder}/share/man/man1/mas.1"
cp -c LICENSE README.md "${installation_staging_folder}"
cp -c Scripts/mas "${installation_staging_folder}/bin/mas"
cp -c contrib/completion/mas-completion.bash "${installation_staging_folder}/etc/bash_completion.d/mas"
cp -c contrib/completion/mas.fish "${installation_staging_folder}/share/fish/vendor_completions.d/mas.fish"

ln -fs "${installation_folder}/bin/mas" "${usr_local_bin_staging_folder}/mas"

archs=("${(s: :n)$(lipo -archs "${installation_staging_folder}/bin/mas")}")
archs=("${(s: :n)$(lipo -archs "${installation_staging_folder}/libexec/bin/mas")}")
# shellcheck disable=SC2034
readonly -a archs

Expand Down
15 changes: 15 additions & 0 deletions Scripts/setup_libexec
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/zsh -Ndefgku
#
# Scripts/setup_libexec
# mas
#
# Copyright © 2026 mas-cli. All rights reserved.
#
# Copies executable to libexec/bin/mas.
#

. "${0:A:h}/_setup_script"

mkdir -p libexec/bin
# shellcheck disable=SC1036,SC2086,SC2225
cp -c .build/${1:-(debug|release)}/mas(om[1]) libexec/bin/mas
49 changes: 30 additions & 19 deletions Sources/mas/Commands/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
internal import ArgumentParser
private import Darwin
private import Foundation
private import JSON
private import JSONAST
private import JSONEncoding

extension MAS {
/// Outputs mas config & related system info.
Expand All @@ -16,24 +19,30 @@ extension MAS {
abstract: "Output mas config & related system info",
)

@OptionGroup
private var outputFormatOptionGroup: OutputFormatOptionGroup

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"))
""",
outputFormatOptionGroup.info(
JSON.encode(
KeyValuePairs(
dictionaryLiteral:
("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")),
),
),
)
}
}
Expand Down Expand Up @@ -78,8 +87,10 @@ private var supportedSliceArchitectures: [String] {
?? [] // swiftformat:disable:this indent
}

private var macOSVersion: Substring {
ProcessInfo.processInfo.operatingSystemVersionString.dropFirst(8).replacing("Build ", with: "", maxReplacements: 1)
private var macOSVersion: String {
String(
ProcessInfo.processInfo.operatingSystemVersionString.dropFirst(8).replacing("Build ", with: "", maxReplacements: 1),
)
}

private func configStringValue(_ name: String) -> String {
Expand Down
20 changes: 4 additions & 16 deletions Sources/mas/Commands/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ extension MAS {
abstract: "List apps installed from the App Store",
)

@OptionGroup
private var outputFormatOptionGroup: OutputFormatOptionGroup
@OptionGroup
private var installedAppIDsOptionGroup: InstalledAppIDsOptionGroup

Expand All @@ -24,10 +26,7 @@ extension MAS {

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 {
guard !installedApps.isEmpty else {
printer.warning(
"""
No installed apps found
Expand All @@ -48,18 +47,7 @@ extension MAS {
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"),
)
outputFormatOptionGroup.info(installedApps.map(String.init(describing:)).joined(separator: "\n"))
}
}
}
Loading
Loading