diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 3af6297e4..b3dd62624 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -44,7 +44,7 @@ jobs: run: Scripts/setup_workflow_repo - name: 🔩 Initialize CodeQL - uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: languages: ${{matrix.language}} build-mode: ${{matrix.build-mode}} @@ -60,6 +60,6 @@ jobs: Scripts/build codeql -c release - name: 🔍 Perform CodeQL analysis - uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: category: /language:${{matrix.language}} diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index 26b43b6b2..b7a399a32 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -50,6 +50,6 @@ jobs: retention-days: 5 - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: sarif_file: results.sarif diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 54b79147f..33e48f3a3 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -6,7 +6,7 @@ # .markdownlint-cli2.yaml # mas # -# markdownlint-cli2 0.23.1 / markdownlint 0.41.1 +# markdownlint-cli2 0.23.2 / markdownlint 0.41.1 # --- gitignore: true @@ -22,7 +22,7 @@ config: emphasis-style: style: underscore fenced-code-language: - allowed_languages: [console, shell, swift] + allowed_languages: [console, shell] language_only: true heading-style: style: atx diff --git a/AGENTS.md b/AGENTS.md index 134e87767..01399ae35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,8 @@ Read it before making repository changes. - `main` is the trunk - Branch topics from `main` -- Before committing (to preserve tokens, agents should skip steps 2 & 3): +- Before committing (to preserve tokens, agents should skip all of the following + steps unless explicitly directed to perform them): 1. Add or edit tests for non-trivial changes 2. Repeatedly run `Scripts/format` until no modifications are made 3. Repeatedly run `Scripts/lint` & fix all violations until no violations are @@ -36,11 +37,35 @@ Read it before making repository changes. ## Content Formatting -- **Newlines:** UNIX (i.e. `\n`) -- **Indentation:** Tabs (width: 2) -- **Max line length:** 120 characters (tabs count as 2 characters) +- **Newlines:** Unix (i.e. `\n`) +- **Indentation:** Tabs (2 characters wide) for all files unless otherwise + specified; 2 spaces for YAML; 1 space for Markdown +- **Max line length:** 120 characters for all files unless otherwise specified + (tabs count as 2 characters); 80 for Markdown; unlimited for header, JSON & + swiftformat - **Unnecessary trailing whitespace:** Remove - **File ends:** Single newline +- **Quoting:** Quote strings only when necessary, preferring the most literal + format that works over more interpreted formats; if multiple quote syntaxes + are functionally equivalent, prefer the visually lightest, e.g., prefer single + quotes over double quotes if they are functionally equivalent +- **Text**: In documentation, comments, strings, etc.: + - **Commas:** Use Oxford commas for lists + - **Ampersands:** Prefer `&` to `and` (omit Oxford comma before `&`) + - **Exceptions:** `and` should be used in `and/or` & after a comma that + separates distinct clauses (but not in a list) + - **Quotes**: The enclosing quotations marks of a quote at the end of a + sentence (iff the whole sentence isn't a quote) should not enclose the + terminal punctuation mark of the encompassing sentence + - **Iff**: Use `iff` as `if & only if` + +### Markdown Guidelines + +- **Style:** GitHub-Flavored Markdown (GFM), ATX headings, backtick-fenced code + blocks with language identifier, underscore emphasis, asterisk strong & hyphen + bullets +- **HTML:** Limit to HTML supported by GFM that doesn't have a native GFM + equivalent ## Refactoring Rules @@ -129,10 +154,11 @@ Commands follow a consistent structure: ### Style Essentials - Name most function parameters -- Capitalize acronym & initialism characters consistently (e.g., `HTTPRequest`, - not `HttpRequest`) +- Capitalize acronym & initialism characters consistently (e.g., `ADAM`, `API`, + `HTTPRequest`, `JSON`) - Shadow variables if the respective original will no longer be used - Strongify weak references instead of evaluating them multiple times +- Group computed properties below stored properties ### Code Preference Hierarchies @@ -279,7 +305,8 @@ Within this section & all subsections, `X` is a placeholder for any type name. ### Testing Requirements -- Add tests for all non-trivial changes +- Add tests for all non-trivial changes (to preserve tokens, agents should not + add tests unless explicitly directed to do so) - Implement in [Swift Testing](https://github.com/swiftlang/swift-testing) - Derive test file paths from source file paths: - replace the `Sources/mas` source path folder prefix with `Tests/MASTests` diff --git a/Brewfile b/Brewfile index 8fa4cf994..e3c873f83 100644 --- a/Brewfile +++ b/Brewfile @@ -1,9 +1,9 @@ brew "actionlint" # 1.7.12 brew "editorconfig-checker" # 3.8.0 -brew "gh" # 2.96.0 +brew "gh" # 2.97.0 brew "git" # 2.55.0 -brew "ipsw" # 3.1.705 -brew "markdownlint-cli2" # 0.23.1 +brew "ipsw" # 3.1.707 +brew "markdownlint-cli2" # 0.23.2 brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.8.0 brew "shellcheck" # 0.11.0 brew "swiftformat" # 0.62.1 diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 8890b6d57..000000000 --- a/GEMINI.md +++ /dev/null @@ -1,106 +0,0 @@ -# Gemini Guidelines for `mas` - -You are an expert Swift & Zsh developer assisting with the maintenance & -development of `mas`, a command-line interface for the Mac App Store. - -## Project Overview - -- **Name:** `mas` -- **Description:** A CLI for the Mac App Store, designed for scripting & - automation. -- **Language:** Swift 6.3 (using Swift Argument Parser) -- **Target OS:** macOS 13+ -- **Project Type:** SwiftPM project - -## Technical Stack - -- **Swift:** 6.3+ (Check [.swift-version](.swift-version)) -- **Xcode:** 26.4+ (Check [.xcode-version](.xcode-version)) -- **macOS:** 13+ (Check [Package.swift](Package.swift)) -- **Private Frameworks:** Uses `CommerceKit` & `StoreFoundation` for App Store - integration only where public APIs are insufficient. - -## Development Workflows - -### Bootstrap - -```shell -Scripts/bootstrap -``` - -### Build - -- Debug: `Scripts/build` -- Release: `Scripts/build '' -c release` - -### Lint & Format - -- Quick Lint: `Scripts/lint -AP` -- Full Lint: `Scripts/lint` -- Format: `Scripts/format` (Run repeatedly until no changes occur) - -### Test - -```shell -Scripts/test -``` - -## Engineering Standards - -Refer to [AGENTS.md](AGENTS.md) for comprehensive guidelines. Key highlights: - -### Content Guidelines - -- **Newlines:** UNIX (LF) -- **Indentation:** Tabs (width: 2) for Swift/Zsh; Spaces (2) for YAML; Spaces - (1) for Markdown. -- **Max line length:** 120 characters (80 for Markdown). -- **Preservation:** Do not reformat, rename, or reorder code unless necessary - for functionality. - -### Markdown Guidelines - -- **Style:** GitHub-Flavored Markdown (GFM), ATX headings, backtick-fenced code - blocks with language identifier, underscore emphasis, asterisk strong & hyphen - bullets. -- **HTML:** Limited to elements supported by GFM that don't have an equivalent - in GFM for the use case. - -### YAML Guidelines - -- **Style:** 2-space indentation, unix newlines, quoted strings only when - necessary, single quotes for strings. -- **Rules:** Forbid non-empty braces, require document start (`---`). - -### Zsh Scripting - -- **Shebang:** `#!/bin/zsh -Ndefgku` -- **Setup:** Start scripts with `. "${0:A:h}/_setup_script"` -- **Preference:** Use zsh builtins over external commands. -- **Commands:** Use `cp -c` & `trash` (not `rm`). - -### Swift Development - -- **Structure:** Organized by `Commands/`, `Models/`, `Utilities/`. -- **Force Unwrapping:** Avoid in `Sources/mas/`. -- **Naming:** Capitalize acronyms consistently (e.g., `ADAM`, `API`, `JSON`). -- **Organization:** Group computed properties below stored properties. -- **Error Handling:** Prefer typed throws (`throws(ErrorType)`), - then `rethrows`, then untyped `throws`. - -### Git Workflow - -- **Trunk:** `main` -- **Commits:** Follow [conventional commit style]( - https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html - ). -- **Pre-commit:** Always run `Scripts/format` & `Scripts/lint` before - committing. - -## Testing Requirements - -- **Framework:** [Swift Testing](https://github.com/swiftlang/swift-testing). -- **Location:** `Tests/MASTests/` -- **Naming Convention:** `Sources/mas/Path/To/File.swift` -> - `Tests/MASTests/Path/To/MASTests+File.swift`. -- **Assertions:** Use force unwrapping (`!`) in tests if appropriate. diff --git a/Package.resolved b/Package.resolved index 11437fd5c..4a1411a47 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "15b8eb806c3bfec0acb2b7b38dce2a04d5492e25c6d436feab71a0e63d3bbaad", + "originHash" : "5ac6d2319b06a3b484da0e9e0392cc900cbef6f756a89cdfc5b1b740fdb56f6e", "pins" : [ { "identity" : "bigint", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/KittyMac/Sextant", "state" : { - "revision" : "61b2f49b249353a3e83c2df9b56b6b9542e8a2bb", - "version" : "0.4.40" + "revision" : "960ac266012fecd98f93dcf64b0dd4fdb3753f26", + "version" : "0.4.41" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/KittyMac/Spanker", "state" : { - "revision" : "13789974bcb73c72995c9927a217c0c07817d42c", - "version" : "0.2.55" + "revision" : "7177446066ac3b106e16184420ff2a1aa1f6df96", + "version" : "0.2.60" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-subprocess", "state" : { - "revision" : "11633673a41f509f8945f23c96c7acd4adafd679", - "version" : "0.5.0" + "revision" : "b3937ab85dd32f6e9435914599c1519074769c1a", + "version" : "1.0.0" } }, { diff --git a/Package.swift b/Package.swift index 9717426e0..962dc9279 100644 --- a/Package.swift +++ b/Package.swift @@ -19,14 +19,14 @@ _ = Package( platforms: [.macOS(.v13)], products: [.executable(name: "mas", targets: ["mas"])], dependencies: [ - .package(url: "https://github.com/KittyMac/Sextant", from: "0.4.40"), + .package(url: "https://github.com/KittyMac/Sextant", from: "0.4.41"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.2"), .package(url: "https://github.com/apple/swift-atomics", from: "1.3.1"), .package(url: "https://github.com/apple/swift-collections", from: "1.6.0"), .package(url: "https://github.com/attaswift/BigInt", from: "6.0.0"), .package(url: "https://github.com/rarestype/swift-json", from: "3.5.0"), .package(url: "https://github.com/scinfu/SwiftSoup", from: "2.13.7"), - .package(url: "https://github.com/swiftlang/swift-subprocess", from: "0.5.0"), + .package(url: "https://github.com/swiftlang/swift-subprocess", from: "1.0.0"), ], targets: [ .plugin(name: "MASBuildToolPlugin", capability: .buildTool()), diff --git a/README.md b/README.md index f74de4161..691afc83b 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,23 @@ automation. ## Installation - + | Provider | Method | mas | macOS | |:------------------------------------------------------------------------------|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------| | [Homebrew](https://brew.sh) [Core](https://github.com/Homebrew/homebrew-core) | `brew install mas` | [![Homebrew Core](https://repology.org/badge/version-for-repo/homebrew/mas-mac-app-store.svg?header=)](https://formulae.brew.sh/formula/mas) | 14+ (recommended) | | [Homebrew](https://brew.sh) [Tap](https://github.com/mas-cli/homebrew-tap) | `brew install mas-cli/tap/mas` | [![Homebrew Tap](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fmas-cli%2Fhomebrew-tap%2Freleases%2Flatest&query=%24.name&label=&color=4c1)](https://github.com/mas-cli/homebrew-tap/releases/latest) | 13+ | | [MacPorts](https://www.macports.org/install.php) | `sudo port install mas` | [![MacPorts](https://repology.org/badge/version-for-repo/macports/mas-mac-app-store.svg?header=)](https://ports.macports.org/port/mas/details/) | 13+ | | [GitHub Releases](https://github.com/mas-cli/mas/releases) | Installers & source archives | All | Release-dependent | - + ## Commands Detailed documentation is available via `man mas` & `mas --help`. - + | Command | Functionality | Notes | Aliases | |:------------------------------|:----------------------------------------------|:------------------------------------------------------------------------------------------------------------|:-----------| | `search …` | Search for App Store apps | [json](#json-app-output) | | @@ -58,24 +58,24 @@ Detailed documentation is available via `man mas` & `mas --help`. | `reset` | Reset App Store processes | | | | `config` | Output config | [json](#json-config-output) | | | `version` | Output version | | | - + ## Integrations - + | Tool | Functionality | |:-----------------------------------------------------------------|:----------------------------------------------------------------------------| | [Homebrew Bundle](https://docs.brew.sh/Brew-Bundle-and-Brewfile) | Include installed apps in `Brewfile`; get, install & update `Brewfile` apps | | [Topgrade](https://github.com/topgrade-rs/topgrade) | Update apps | - + ## Known Issues - + | Issue | Solution | |:-------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Manage system software (macOS, Safari…) | Use [`softwareupdate`](https://www.unix.com/man-page/osx/8/softwareupdate) | @@ -86,21 +86,21 @@ Detailed documentation is available via `man mas` & `mas --help`. | Undetected installed apps | [Index apps in Spotlight](#spotlight) | | `This redownload is not available for this Apple Account…` error | Sign in the correct Apple Account to the App Store, or uninstall the app & get it with the current Apple Account | | Other bugs | [Subscribe to an existing](https://github.com/mas-cli/mas/issues), or [open a new](https://github.com/mas-cli/mas/issues/new?template=01-bug-report.yaml), bug report | - + ## Development - + | Action | Command | |:------------------------------------------------------------------------|:-------------------------------| | Build | `Scripts/build` or Xcode 26.4+ | | Set up zsh wrapper | `Scripts/setup_libexec` | | Run zsh wrapper | `Scripts/mas` | | Test ([Swift Testing](https://developer.apple.com/xcode/swift-testing)) | `Scripts/test` | - + ## App IDs @@ -196,8 +196,8 @@ mdfind 'kMDItemAppStoreAdamID = ' If any App Store apps are not properly indexed, index via: - + ```shell # Individual app (if the omitted apps are known). e.g., for Xcode: mdimport /Applications/Xcode.app @@ -209,8 +209,8 @@ mdimport /Applications ${vol:+"/Volumes/${vol}/Applications"} # All volumes: sudo mdutil -Eai on ``` - + ## Root Privileges @@ -293,8 +293,8 @@ available from the App Store. The 2 outdated-app-detection modes are selectable via mutually exclusive flags: - + | Feature | `--inaccurate` (default) | `--accurate` | |:-----------------|:---------------------------------------------------------------------------------|:---------------------------------------------| | **Method** | Query the [iTunes Search API](https://performance-partners.apple.com/search-api) | Initiate App Store download to read metadata | @@ -303,8 +303,8 @@ The 2 outdated-app-detection modes are selectable via mutually exclusive flags: | **Requirements** | [iTunes Search API](https://performance-partners.apple.com/search-api) | Apple Account signed in to the App Store | | **Dialogs** | Only if `--no-check-min-os` | Various potential dialogs | | **Hangs** | None | If checking 100+ apps in quick succession | - + #### `--inaccurate` (default) diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h b/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h index fa138e9e9..4027aad43 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKDownloadQueue.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h b/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h index 22a3251a8..f346df6d8 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKPurchaseController.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h b/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h index c80ea4142..467e987b7 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CKServiceInterface.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h b/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h index 589aa9167..77e24b50f 100644 --- a/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h +++ b/Sources/PrivateFrameworks/include/CommerceKit/CommerceKit.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h index 8e89f0ceb..8d51325ee 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISAccountService-Protocol.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h index f661c9beb..ccf9583b8 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISServiceProxy.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h b/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h index 16fb7b25c..c9c77bda8 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/ISStoreAccount.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h index 5d520020e..bb78353d3 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownload.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h index dd26b02f5..f150f7ac0 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadMetadata.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h index 386743d3d..48139b796 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadPhase.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h index f7f4e53b0..d2401d022 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSDownloadStatus.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h index 72a9a98f3..3452d7a25 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchase.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h index 19871d705..caa888ef0 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/SSPurchaseResponse.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h b/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h index 1060aad7b..45d85ae3c 100644 --- a/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h +++ b/Sources/PrivateFrameworks/include/StoreFoundation/StoreFoundation.h @@ -1,5 +1,5 @@ // -// Generated by https://github.com/blacktop/ipsw (Version: 3.1.705, BuildCommit: Homebrew) +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.707, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26.5, SDK: 26.5, Tool: ld (1267) // - LC_SOURCE_VERSION: 716.4.2.0.0 diff --git a/Sources/mas/Commands/MAS.Home.swift b/Sources/mas/Commands/MAS.Home.swift index 7c293390e..2d7c3e990 100644 --- a/Sources/mas/Commands/MAS.Home.swift +++ b/Sources/mas/Commands/MAS.Home.swift @@ -27,9 +27,9 @@ extension MAS { } func run(catalogApps: [CatalogApp]) async { - await catalogApps.map(\.appStorePageURLString).forEach(attemptTo: "open") { appStorePageURLString in - guard let url = URL(string: appStorePageURLString) else { - throw MASError.invalidURL(appStorePageURLString) + await catalogApps.forEach(attemptTo: "open") { catalogApp in + guard let url = URL(string: catalogApp.appStorePageURLString) else { + throw MASError.invalidURL(catalogApp.appStorePageURLString) } _ = try await url.open() diff --git a/Sources/mas/Commands/MAS.Reset.swift b/Sources/mas/Commands/MAS.Reset.swift index e8ed3e738..28885e949 100644 --- a/Sources/mas/Commands/MAS.Reset.swift +++ b/Sources/mas/Commands/MAS.Reset.swift @@ -30,9 +30,9 @@ extension MAS { 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) + printer.warning("Failed to terminate app with bundle ID", bundleID) if !app.forceTerminate() { - printer.error("Failed to force terminate app with bundle ID:", bundleID) + printer.error("Failed to force terminate app with bundle ID", bundleID) } } } diff --git a/Sources/mas/Commands/MAS.Seller.swift b/Sources/mas/Commands/MAS.Seller.swift index 1c9d93db8..5b213b3fc 100644 --- a/Sources/mas/Commands/MAS.Seller.swift +++ b/Sources/mas/Commands/MAS.Seller.swift @@ -28,15 +28,11 @@ extension MAS { } func run(catalogApps: [CatalogApp]) async { - await catalogApps.compactMap { catalogApp in + await catalogApps.forEach(attemptTo: "open") { catalogApp in guard let sellerURLString = catalogApp.sellerURLString else { printer.error("Failed to find seller app web page for ADAM ID", catalogApp.adamID) - return String?.none + return } - - return sellerURLString - } - .forEach(attemptTo: "open") { sellerURLString in guard let url = URL(string: sellerURLString) else { throw MASError.invalidURL(sellerURLString) } diff --git a/Sources/mas/Commands/MAS.Uninstall.swift b/Sources/mas/Commands/MAS.Uninstall.swift index bb10e66f6..b4949ddda 100644 --- a/Sources/mas/Commands/MAS.Uninstall.swift +++ b/Sources/mas/Commands/MAS.Uninstall.swift @@ -65,24 +65,22 @@ extension MAS { appropriateFor: appURL, create: true, ) - var destinationPath = trashURL.appending(path: appURL.lastPathComponent, directoryHint: .isDirectory).filePath - if fileManager.fileExists(atPath: destinationPath) { - let pathExtension = appURL.pathExtension - destinationPath = trashURL.appending( - path: """ - \(appURL.deletingPathExtension().lastPathComponent) \ - \(Date().formatted(trashCollisionDateFormatStyle))\ - \(pathExtension.isEmpty ? "" : ".\(pathExtension)") - """, - directoryHint: .isDirectory, - ) - .filePath - } + let destinationPath = trashURL.appending(path: appURL.lastPathComponent, directoryHint: .isDirectory).filePath _ = try await mas::run( .path("/usr/bin/sudo"), "/bin/mv", appPath, - destinationPath, + fileManager.fileExists(atPath: destinationPath) + ? trashURL.appending( + path: """ + \(appURL.deletingPathExtension().lastPathComponent) \ + \(Date().formatted(trashCollisionDateFormatStyle))\ + \(appURL.pathExtension.ifNotEmptyPrepend(".")) + """, + directoryHint: .isDirectory, + ) + .filePath + : destinationPath, errorMessage: "Failed to trash \(appPath.quoted) to \(destinationPath.quoted)", ) printer.info("Uninstalled", appPath.quoted, "to", destinationPath.quoted) diff --git a/Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift b/Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift index 5f6ff45c6..3ede4f004 100644 --- a/Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift +++ b/Sources/mas/Commands/OptionGroups/ForceBundleIDOptionGroup.swift @@ -9,5 +9,5 @@ private import ArgumentParser struct ForceBundleIDOptionGroup: ParsableArguments { @Flag(name: .customLong("bundle"), help: "Process all app IDs as bundle IDs") - var forceBundleID = false + private(set) var forceBundleID = false } diff --git a/Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift b/Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift index 6373ee5c0..86777ad77 100644 --- a/Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift +++ b/Sources/mas/Commands/OptionGroups/ForceOptionGroup.swift @@ -9,5 +9,5 @@ private import ArgumentParser struct ForceOptionGroup: ParsableArguments { @Flag(help: "Force reinstall") - var force = false + private(set) var force = false } diff --git a/Sources/mas/Commands/OptionGroups/InstalledAppsOptionGroup.swift b/Sources/mas/Commands/OptionGroups/InstalledAppsOptionGroup.swift index 5ad0d061d..f83aad2e6 100644 --- a/Sources/mas/Commands/OptionGroups/InstalledAppsOptionGroup.swift +++ b/Sources/mas/Commands/OptionGroups/InstalledAppsOptionGroup.swift @@ -11,7 +11,7 @@ struct InstalledAppsOptionGroup: ParsableArguments { @OptionGroup private var forceBundleIDOptionGroup: ForceBundleIDOptionGroup // swiftformat:disable:this organizeDeclarations @Argument(help: .init("App ID", valueName: "app-id")) - var appIDStrings = [String]() + private(set) var appIDStrings = [String]() var appIDs: [AppID] { appIDStrings.map { .init(from: $0, forceBundleID: forceBundleIDOptionGroup.forceBundleID) } diff --git a/Sources/mas/Commands/OptionGroups/OutputFormatOptionGroup.swift b/Sources/mas/Commands/OptionGroups/OutputFormatOptionGroup.swift index 9806ffcd4..80e54bc9e 100644 --- a/Sources/mas/Commands/OptionGroups/OutputFormatOptionGroup.swift +++ b/Sources/mas/Commands/OptionGroups/OutputFormatOptionGroup.swift @@ -11,7 +11,7 @@ private import Foundation struct OutputFormatOptionGroup: ParsableArguments { @Flag(name: .customLong("json"), help: "Output JSON") - var shouldOutputJSON = false + private(set) var shouldOutputJSON = false func info(_ items: Any..., separator: String = " ", terminator: String = "\n") { var stat = stat() diff --git a/Sources/mas/Models/AppStoreAction.swift b/Sources/mas/Models/AppStoreAction.swift index 54d9a8955..1cdcd88e6 100644 --- a/Sources/mas/Models/AppStoreAction.swift +++ b/Sources/mas/Models/AppStoreAction.swift @@ -68,9 +68,8 @@ enum AppStoreAction: String { return } - await OrderedSet(adamIDs).forEach(attemptTo: "\(self) app for ADAM ID") { adamID in - try await app(withADAMID: adamID) { _, _ in false } - } + await OrderedSet(adamIDs) + .forEach(attemptTo: "\(self) app for ADAM ID") { try await app(withADAMID: $0) { _, _ in false } } } func app(withADAMID adamID: ADAMID, shouldCancel: @escaping @Sendable (String?, Bool) -> Bool) async throws { diff --git a/Sources/mas/Models/CatalogApp.swift b/Sources/mas/Models/CatalogApp.swift index ff35e8519..4ec115546 100644 --- a/Sources/mas/Models/CatalogApp.swift +++ b/Sources/mas/Models/CatalogApp.swift @@ -92,8 +92,7 @@ extension CatalogApp: JSONDecodable { } var object = object - let jsonMinimumOSVersion = try object[minimumOSVersionKey]?.decode() ?? "" - if jsonMinimumOSVersion != minimumOSVersion { + if try object[minimumOSVersionKey]?.decode() != minimumOSVersion { if let index = object.fields.firstIndex(where: { $0.key == minimumOSVersionKey }) { object.fields[index] = (minimumOSVersionKey, .string(minimumOSVersion)) } else { diff --git a/Sources/mas/Models/MASError.swift b/Sources/mas/Models/MASError.swift index 1daf23ee4..098259661 100644 --- a/Sources/mas/Models/MASError.swift +++ b/Sources/mas/Models/MASError.swift @@ -33,13 +33,13 @@ extension MASError: CustomStringConvertible { case let .error(message, cause, separatorWhenCause, separatorWhenNoCause): "\(message)\(cause.map { "\(separatorWhenCause)\($0)" } ?? separatorWhenNoCause)" case let .invalidJSON(string): - "Invalid JSON:\n\(string)" + "Failed to parse JSON:\n\(string)" case let .invalidURL(string): - "Invalid URL: \(string)" + "Failed to parse URL: \(string)" case let .noCatalogAppsFound(searchTerm): - "No apps found in the App Store for search term: \(searchTerm)" + "Failed to find apps in the App Store for search term: \(searchTerm)" case let .unknownAppID(appID): - "No apps found in the App Store for \(appID)" + "Failed to find app in the App Store with \(appID)" } } } diff --git a/Sources/mas/Models/OutdatedApp.swift b/Sources/mas/Models/OutdatedApp.swift index c10f8ea9f..395b8bc17 100644 --- a/Sources/mas/Models/OutdatedApp.swift +++ b/Sources/mas/Models/OutdatedApp.swift @@ -16,15 +16,20 @@ struct OutdatedApp { init(installedApp: InstalledApp, newVersion: String) { self.installedApp = installedApp self.newVersion = newVersion - var jsonObjectInstalled = installedApp.jsonObject - jsonObjectInstalled.fields.insert( - (newVersionKey, .string(newVersion)), - at: jsonObjectInstalled.fields - .map(\.key.rawValue) - .lowerBound(of: newVersionKey.rawValue, using: NumericStringComparator.forward), + lazyJSON = .init( + .init( + describing: { + var jsonObject = installedApp.jsonObject + jsonObject.fields.insert( + (newVersionKey, .string(newVersion)), + at: jsonObject.fields + .map(\.key.rawValue) + .lowerBound(of: newVersionKey.rawValue, using: NumericStringComparator.forward), + ) + return jsonObject + }(), + ), ) - let jsonObject = jsonObjectInstalled - lazyJSON = .init(.init(describing: jsonObject)) } } diff --git a/Sources/mas/Utilities/Processes/Subprocess.swift b/Sources/mas/Utilities/Processes/Subprocess.swift index 96c3f39ef..723a87a02 100644 --- a/Sources/mas/Utilities/Processes/Subprocess.swift +++ b/Sources/mas/Utilities/Processes/Subprocess.swift @@ -8,12 +8,6 @@ internal import Foundation internal import Subprocess -private extension String { - func ifNotEmptyPrepend(_ prefix: String) -> Self { - isEmpty ? self : prefix + self - } -} - func run( _ executable: Executable, _ args: String..., @@ -29,8 +23,8 @@ func run( output: .string(limit: maxCaptureByteCount, encoding: encoding), error: .string(limit: maxCaptureByteCount, encoding: encoding), ) - let outString = executionResult.standardOutput?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let errString = executionResult.standardError?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let outString = executionResult.standardOutput.trimmingCharacters(in: .whitespacesAndNewlines) + let errString = executionResult.standardError.trimmingCharacters(in: .whitespacesAndNewlines) guard executionResult.terminationStatus.isSuccess else { throw MASError.error( """ diff --git a/Sources/mas/Utilities/Resources/Environment.swift b/Sources/mas/Utilities/Resources/Environment.swift index 1d8af4764..28b4aac20 100644 --- a/Sources/mas/Utilities/Resources/Environment.swift +++ b/Sources/mas/Utilities/Resources/Environment.swift @@ -20,8 +20,8 @@ struct Environment { let searchForAppsMatchingSearchTerm: @Sendable (String) async throws -> [CatalogApp] init( - dataFrom: @escaping @Sendable (URL) async throws -> (Data, URLResponse) - = URLSession(configuration: .ephemeral).data, + dataFrom: // swiftformat:disable:next indent + @escaping @Sendable (URL) async throws -> (Data, URLResponse) = URLSession(configuration: .ephemeral).data, lookupAppFromAppID: @escaping @Sendable (AppID) async throws -> CatalogApp = lookup, searchForAppsMatchingSearchTerm: @escaping @Sendable (String) async throws -> [CatalogApp] = search, ) { diff --git a/Sources/mas/Utilities/Swift/Array.swift b/Sources/mas/Utilities/Swift/Array.swift new file mode 100644 index 000000000..f5aa66df4 --- /dev/null +++ b/Sources/mas/Utilities/Swift/Array.swift @@ -0,0 +1,13 @@ +// +// Array.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +extension Array { + init(reservedCapacity: Int) { + self.init() + reserveCapacity(reservedCapacity) + } +} diff --git a/Sources/mas/Utilities/Swift/String.swift b/Sources/mas/Utilities/Swift/String.swift index 6d441a01c..391de5650 100644 --- a/Sources/mas/Utilities/Swift/String.swift +++ b/Sources/mas/Utilities/Swift/String.swift @@ -16,68 +16,99 @@ extension String { "'\(replacing("'", with: "\\'"))'" } + func ifNotEmptyPrepend(_ prefix: String) -> Self { + isEmpty ? self : prefix + self + } + func removingSuffix(_ suffix: Self) -> Self { hasSuffix(suffix) ? .init(dropLast(suffix.count)) : self } - func similarity(to other: Self) -> Double { - let this = Array(precomposedStringWithCanonicalMapping) - let that = Array(other.precomposedStringWithCanonicalMapping) - let thisLength = this.count - let thatLength = that.count - guard thisLength > 0 || thatLength > 0 else { - return 1.0 - } - guard thisLength != 0, thatLength != 0 else { - return 0.0 + func similarity(to that: Self) -> Double { + func score(_ string: Self) -> ([ScoredCharacter], Double) { + string.precomposedStringWithCanonicalMapping + .reduce(into: (.init(reservedCapacity: string.count), 0)) { result, character in + let scoredCharacter = ScoredCharacter(character) + result.0.append(scoredCharacter) + result.1 += scoredCharacter.cost + } } - // 2D matrix for Damerau-Levenshtein to track transpositions - var matrix = Array(repeating: Array(repeating: 0.0, count: thatLength + 1), count: thisLength + 1) - - // Initialize base costs (deletions/insertions) - for index in 0...thisLength { - matrix[index][0] = .init(index) + let (thisChars, thisCost) = score(self) + let (thatChars, thatCost) = score(that) + let thisLength = thisChars.count + let thatLength = thatChars.count + guard thisLength > 0 else { + return thatLength == 0 ? 1 : 0 } - for index in 0...thatLength { - matrix[0][index] = .init(index) + guard thatLength > 0 else { + return 0 } + let columnCount = thatLength + 1 + var rowTwoPrevious = [Double](repeating: 0, count: columnCount) + var rowPrevious = (0...thatLength).map(Double.init) + var rowCurrent = [Double](repeating: 0, count: columnCount) for i in 1...thisLength { // swiftlint:disable:this identifier_name - for j in 1...thatLength { // swiftlint:disable:this identifier_name - let thisChar = this[i - 1] - let thatChar = that[j - 1] - if thisChar == thatChar { - matrix[i][j] = matrix[i - 1][j - 1] - } else { - // Standard edit costs - let cost = Swift::min( - matrix[i - 1][j] + thisChar.structuralCost, // Deletion - matrix[i][j - 1] + thatChar.structuralCost, // Insertion - matrix[i - 1][j - 1] + thisChar.substitutionCost(for: thatChar), - ) + let thisChar = thisChars[i - 1] + func cost(of thatChar: ScoredCharacter, at thatIndex: Int) -> Double { + Swift::min( + rowPrevious[thatIndex] + thisChar.cost, // Deletion + rowCurrent[thatIndex - 1] + thatChar.cost, // Insertion + rowPrevious[thatIndex - 1] + thisChar.cost(substitutingFor: thatChar), + ) + } + + rowCurrent[0] = .init(i) + if i > 1 { + let previousThisChar = thisChars[i - 2] + for j in 1...thatLength { // swiftlint:disable:this identifier_name + let thatChar = thatChars[j - 1] + guard thisChar != thatChar else { + rowCurrent[j] = rowPrevious[j - 1] + continue + } - matrix[i][j] = i > 1 && j > 1 && this[i - 1] == that[j - 2] && this[i - 2] == that[j - 1] // Transposition - ? min(cost, matrix[i - 2][j - 2] + 0.4) + let cost = cost(of: thatChar, at: j) + // Damerau-Levenshtein transposition check + rowCurrent[j] = j > 1 && thisChar == thatChars[j - 2] && previousThisChar == thatChar + ? min(cost, rowTwoPrevious[j - 2] + 0.4) : cost } + } else { + for j in 1...thatLength { // swiftlint:disable:this identifier_name + let thatChar = thatChars[j - 1] + rowCurrent[j] = thisChar == thatChar ? rowPrevious[j - 1] : cost(of: thatChar, at: j) + } } + swap(&rowTwoPrevious, &rowPrevious) + swap(&rowPrevious, &rowCurrent) } - return max(0, 1.0 - (matrix[thisLength][thatLength] / .init(max(thisLength, thatLength)))) + let maxCost = max(thisCost, thatCost) + return maxCost == 0 ? 1 : max(0, 1 - rowPrevious[thatLength] / maxCost) } } -private extension Character { - var structuralCost: Double { - isWhitespace || isPunctuation || isSymbol ? 0.25 : 1.0 +private struct ScoredCharacter: Equatable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.character == rhs.character } - private var folded: String { - .init(self).folding(options: .diacriticInsensitive, locale: .current) + let character: Character + let lowercased: String + let folded: String + let cost: Double + + init(_ character: Character) { + self.character = character + let string = String(character) + lowercased = string.lowercased() + folded = character.isASCII ? string : string.folding(options: .diacriticInsensitive, locale: .current) + cost = character.isWhitespace || character.isPunctuation || character.isSymbol ? 0.25 : 1 } - func substitutionCost(for that: Self) -> Double { - folded == that.folded ? 0.1 : lowercased() == that.lowercased() ? 0.2 : 1.0 + func cost(substitutingFor that: Self) -> Double { + folded == that.folded ? 0.1 : lowercased == that.lowercased ? 0.2 : 1 } }