Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ jobs:
- runner: macos-14
xcode: brew
- runner: macos-15
xcode: 26.1.1
- runner: macos-15-intel
xcode: 26.1.1
- runner: macos-26
runs-on: ${{matrix.runner}}
defaults:
Expand Down
15 changes: 3 additions & 12 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# .swiftformat
# mas
#
# SwiftFormat 0.58.7
# SwiftFormat 0.59.0
#

# Disabled rules (enabled by default)
Expand All @@ -16,30 +16,21 @@
# Enabled rules (disabled by default)
--enable acronyms
--enable blankLinesAfterGuardStatements
--enable blankLinesBetweenImports
--enable blockComments
--enable docComments
--enable emptyExtensions
--enable environmentEntry
--enable isEmpty
--enable noExplicitOwnership
--enable noForceTryInTests
--enable noForceUnwrapInTests
--enable noGuardInTests
--enable organizeDeclarations
--enable preferFinalClasses
--enable preferSwiftTesting
--enable privateStateVariables
--enable propertyTypes
--enable redundantAsync
--enable redundantEquatable
--enable redundantMemberwiseInit
--enable redundantProperty
--enable redundantThrows
--enable singlePropertyPerLine
--enable sortSwitchCases
--enable testSuiteAccessControl
--enable unusedPrivateDeclarations
--enable urlMacro
--enable validateTestCases
--enable wrapConditionalBodies
--enable wrapEnumCases
--enable wrapMultilineConditionalAssignment
Expand Down
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# .swiftlint.yml
# mas
#
# SwiftLint 0.63.0
# SwiftLint 0.63.2
#
---
excluded:
Expand Down
2 changes: 1 addition & 1 deletion .yamllint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# .yamllint.yaml
# mas
#
# yamllint 1.37.1
# yamllint 1.38.0
#
---
extends: default
Expand Down
8 changes: 4 additions & 4 deletions Brewfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
brew "actionlint" # 1.7.10
brew "gh" # 2.83.2
brew "gh" # 2.86.0
brew "git" # 2.52.0
brew "ipsw" # 3.1.648
brew "markdownlint-cli2" # 0.20.0
brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.4.0
brew "shellcheck" # 0.11.0
brew "swiftformat" # 0.58.7
brew "swiftlint" # 0.63.0
brew "swiftformat" # 0.59.0
brew "swiftlint" # 0.63.2
brew "xcodes" # 1.6.2
brew "yamllint" # 1.37.1
brew "yamllint" # 1.38.0
2 changes: 1 addition & 1 deletion Sources/mas/AppStore/AppStoreAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ enum AppStoreAction: Sendable {
lookupAppFromAppID: (AppID) async throws -> CatalogApp,
) async throws {
try await apps(
withADAMIDs: await appIDs.lookupCatalogApps(lookupAppFromAppID: lookupAppFromAppID).map(\.adamID),
withADAMIDs: await appIDs.lookupCatalogApps(using: lookupAppFromAppID).map(\.adamID),
force: force,
installedApps: installedApps,
)
Expand Down
11 changes: 5 additions & 6 deletions Sources/mas/Commands/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ extension MAS {
driver ▁ \(swiftDriverVersion)
store ▁▁ \(appStoreRegion)
region ▁ \(macRegion)
macos ▁▁ \(
ProcessInfo.processInfo
.operatingSystemVersionString // swiftformat:disable:this indent
.dropFirst(8) // swiftformat:disable:this indent
.replacing("Build ", with: "", maxReplacements: 1) // swiftformat:disable:this indent
)
macos ▁▁ \(macOSVersion)
mac ▁▁▁▁ \(configStringValue("hw.product"))
cpu ▁▁▁▁ \(configStringValue("machdep.cpu.brand_string"))
arch ▁▁▁ \(configStringValue("hw.machine"))
Expand Down Expand Up @@ -83,6 +78,10 @@ private var supportedSliceArchitectures: [String] {
?? [] // 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 {
Expand Down
4 changes: 1 addition & 3 deletions Sources/mas/Commands/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ extension MAS {
}

private func run(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async {
await run(
catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(lookupAppFromAppID: lookupAppFromAppID),
)
await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}

func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension MAS {
/// Lists all apps installed from the App Store.
struct List: AsyncParsableCommand, Sendable {
static let configuration = CommandConfiguration(
abstract: "List all apps installed from the App Store",
abstract: "List apps installed from the App Store",
)

@OptionGroup
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Lookup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension MAS {
}

private func run(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async {
run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(lookupAppFromAppID: lookupAppFromAppID))
run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}

func run(catalogApps: [CatalogApp]) { // swiftformat:disable:this organizeDeclarations
Expand Down
13 changes: 13 additions & 0 deletions Sources/mas/Commands/OptionGroups/AccuracyOptionGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// AccuracyOptionGroup.swift
// mas
//
// Copyright © 2025 mas-cli. All rights reserved.
//

private import ArgumentParser

struct AccuracyOptionGroup: ParsableArguments {
@Flag
var accuracy = OutdatedAccuracy.inaccurate
}
45 changes: 0 additions & 45 deletions Sources/mas/Commands/OptionGroups/AccurateOptionGroup.swift

This file was deleted.

28 changes: 28 additions & 0 deletions Sources/mas/Commands/OptionGroups/OutdatedAccuracy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// OutdatedAccuracy.swift
// mas
//
// Copyright © 2026 mas-cli. All rights reserved.
//

internal import ArgumentParser

enum OutdatedAccuracy: String, EnumerableFlag {
case accurate
case accurateIgnoreUnknownApps
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 .accurateIgnoreUnknownApps:
"Use --accurate logic, but ignore apps that are unknown to the App Store"
case .inaccurate:
"Use inaccurate, faster logic that avoids dialogs & that ignores apps that are unknown to the App Store"
}
}
}
15 changes: 9 additions & 6 deletions Sources/mas/Commands/Outdated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension MAS {
)

@OptionGroup
private var accurateOptionGroup: AccurateOptionGroup
private var accuracyOptionGroup: AccuracyOptionGroup
@OptionGroup
private var verboseOptionGroup: VerboseOptionGroup
@OptionGroup
Expand All @@ -27,14 +27,17 @@ extension MAS {
await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:))
}

private func run(installedApps: [InstalledApp], lookupAppFromAppID: (AppID) async throws -> CatalogApp) async {
private func run(
installedApps: [InstalledApp],
lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp,
) async {
run(
outdatedApps: await outdatedApps(
installedApps: installedApps,
from: installedApps,
filterFor: installedAppIDsOptionGroup.appIDs,
lookupAppFromAppID: lookupAppFromAppID,
accurateOptionGroup: accurateOptionGroup,
verboseOptionGroup: verboseOptionGroup,
installedAppIDsOptionGroup: installedAppIDsOptionGroup,
accuracy: accuracyOptionGroup.accuracy,
shouldWarnIfUnknownApp: verboseOptionGroup.verbose,
),
)
}
Expand Down
1 change: 0 additions & 1 deletion Sources/mas/Commands/Reset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ extension MAS {
return
}

// swiftformat:disable:next spaceAroundBrackets
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")
Expand Down
4 changes: 1 addition & 3 deletions Sources/mas/Commands/Seller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ extension MAS {
}

private func run(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async {
await run(
catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(lookupAppFromAppID: lookupAppFromAppID),
)
await run(catalogApps: await catalogAppIDsOptionGroup.appIDs.lookupCatalogApps(using: lookupAppFromAppID))
}

func run(catalogApps: [CatalogApp]) async { // swiftformat:disable:this organizeDeclarations
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/SignOut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension MAS {
struct SignOut: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "signout",
abstract: "Sign out of the Apple Account currently signed in to the App Store",
abstract: "Sign out of the App Store",
)

func run() {
Expand Down
22 changes: 10 additions & 12 deletions Sources/mas/Commands/Uninstall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,40 +40,38 @@ extension MAS {
}

private func run(installedApps: [InstalledApp]) throws {
let uninstallingAppByPath = (
isUninstallingAll ? installedApps.map { AppID.adamID($0.adamID) } : installedAppIDsOptionGroup.appIDs,
) // swiftformat:disable indent
.reduce(into: OrderedDictionary<String, InstalledApp>()) { uninstallingAppByPath, appID in
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
}

for uninstallingApp in uninstallingApps {
uninstallingAppByPath[uninstallingApp.path] = uninstallingApp
}
uninstallingAppPathSet.append(contentsOf: uninstallingApps.map(\.path))
}
guard !uninstallingAppByPath.isEmpty else { // swiftformat:enable indent
guard !uninstallingAppPathSet.isEmpty else {
return
}
guard !isPerformingDryRun else {
printer.notice("Dry run. A wet run would uninstall:\n")
for appPath in uninstallingAppByPath.keys {
printer.info(appPath)
for uninstallingAppPath in uninstallingAppPathSet {
printer.info(uninstallingAppPath)
}
return
}
guard getuid() == 0 else {
try sudo(MAS._commandName, args: [Self._commandName] + uninstallingAppByPath.values.map { String($0.adamID) })
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 uninstallingAppByPath.keys {
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)
Expand Down
12 changes: 6 additions & 6 deletions Sources/mas/Commands/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension MAS {
)

@OptionGroup
private var accurateOptionGroup: AccurateOptionGroup
private var accuracyOptionGroup: AccuracyOptionGroup
@OptionGroup
private var forceOptionGroup: ForceOptionGroup
@OptionGroup
Expand All @@ -30,17 +30,17 @@ extension MAS {
try await run(installedApps: try await installedApps.filter(!\.isTestFlight), lookupAppFromAppID: lookup(appID:))
}

private func run(installedApps: [InstalledApp], lookupAppFromAppID: (AppID) async throws -> CatalogApp)
private func run(installedApps: [InstalledApp], lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp)
async throws { // swiftformat:disable:this indent
try await run(
outdatedApps: forceOptionGroup.force // swiftformat:disable:next indent
? installedApps.filter(for: installedAppIDsOptionGroup.appIDs).map { ($0, "") }
: await outdatedApps(
installedApps: installedApps,
from: installedApps,
filterFor: installedAppIDsOptionGroup.appIDs,
lookupAppFromAppID: lookupAppFromAppID,
accurateOptionGroup: accurateOptionGroup,
verboseOptionGroup: verboseOptionGroup,
installedAppIDsOptionGroup: installedAppIDsOptionGroup,
accuracy: accuracyOptionGroup.accuracy,
shouldWarnIfUnknownApp: verboseOptionGroup.verbose,
),
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Models/AppID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enum AppID: CustomStringConvertible, Sendable {
}

extension [AppID] { // swiftlint:disable:this file_types_order
func lookupCatalogApps(lookupAppFromAppID: (AppID) async throws -> CatalogApp) async -> [CatalogApp] {
func lookupCatalogApps(using lookupAppFromAppID: (AppID) async throws -> CatalogApp) async -> [CatalogApp] {
await compactMap(attemptingTo: "lookup app for", lookupAppFromAppID)
}
}
Expand Down
Loading
Loading