Full Code of alienator88/Pearcleaner for AI

main 3222dc8f305a cached
179 files
15.0 MB
1.1M tokens
26 symbols
1 requests
Download .txt
Showing preview only (4,534K chars total). Download the full file or copy to clipboard to get everything.
Repository: alienator88/Pearcleaner
Branch: main
Commit: 3222dc8f305a
Files: 179
Total size: 15.0 MB

Directory structure:
gitextract_u4mjrmj6/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── project.md
│   └── workflows/
│       └── issues.yml
├── .gitignore
├── Builds/
│   ├── ExportOptions.plist
│   └── changes.md
├── FUNDING.yml
├── FinderOpen/
│   ├── Assets.xcassets/
│   │   ├── Contents.json
│   │   └── Glass.imageset/
│   │       └── Contents.json
│   ├── FinderOpen.entitlements
│   ├── FinderOpen.swift
│   ├── Info.plist
│   └── Localizable.xcstrings
├── LICENSE.md
├── Pear Resources/
│   ├── Glass.icon/
│   │   ├── Assets/
│   │   │   └── pear.heic
│   │   └── icon.json
│   └── Pear.psd
├── Pearcleaner/
│   ├── Logic/
│   │   ├── AppCommands.swift
│   │   ├── AppInfoFetch.swift
│   │   ├── AppPathsFetch.swift
│   │   ├── AppState.swift
│   │   ├── AppsUpdater/
│   │   │   ├── AppStoreReset.swift
│   │   │   ├── AppStoreUpdateChecker.swift
│   │   │   ├── AppStoreUpdater.swift
│   │   │   ├── HomebrewAdoption.swift
│   │   │   ├── HomebrewUpdateChecker.swift
│   │   │   ├── IOSAppInstaller.swift
│   │   │   ├── Models.swift
│   │   │   ├── PrivateFrameworks/
│   │   │   │   ├── CFBundle/
│   │   │   │   │   └── CFBundle_Private.h
│   │   │   │   ├── 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
│   │   │   ├── SSPurchase+Extension.swift
│   │   │   ├── SparkleUpdateChecker.swift
│   │   │   ├── SparkleUpdateDriver.swift
│   │   │   ├── SparkleUpdateOperation.swift
│   │   │   ├── UpdateManager.swift
│   │   │   ├── UpdateQueue.swift
│   │   │   ├── UpdaterDebugLogger.swift
│   │   │   ├── UpdaterSettings.swift
│   │   │   └── VersionComparison.swift
│   │   ├── Brew/
│   │   │   ├── HomebrewAutoUpdateManager.swift
│   │   │   ├── HomebrewController.swift
│   │   │   ├── HomebrewManager.swift
│   │   │   ├── HomebrewPackage.swift
│   │   │   ├── HomebrewTap.swift
│   │   │   └── HomebrewUninstaller.swift
│   │   ├── CLI.swift
│   │   ├── Conditions.swift
│   │   ├── DeepLink.swift
│   │   ├── FileSearch/
│   │   │   ├── FileSearchLogic.swift
│   │   │   └── FileSearchModels.swift
│   │   ├── FuzzySearch.swift
│   │   ├── GlobalConsoleManager.swift
│   │   ├── HelperToolManager.swift
│   │   ├── KeychainPasswordManager.swift
│   │   ├── Lipo.swift
│   │   ├── Locations.swift
│   │   ├── Logic.swift
│   │   ├── PKG/
│   │   │   ├── PKBOM.h
│   │   │   ├── PKBundleComponent.h
│   │   │   ├── PKBundleComponentVersion.h
│   │   │   ├── PKComponent.h
│   │   │   ├── PKGManager.swift
│   │   │   ├── PKInstallHistory.h
│   │   │   ├── PKPackage.h
│   │   │   ├── PKPackageChecker.h
│   │   │   ├── PKPackageInfo.h
│   │   │   ├── PKProductInfo.h
│   │   │   ├── PKReceipt.h
│   │   │   └── Pearcleaner-Bridging-Header.h
│   │   ├── PasswordRequestHandler.swift
│   │   ├── ProcessEnv.swift
│   │   ├── ReversePathsFetch.swift
│   │   ├── TCC/
│   │   │   ├── TCCModels.swift
│   │   │   └── TCCQueryHelper.swift
│   │   ├── UndoHistoryManager.swift
│   │   ├── UndoManager.swift
│   │   └── Utilities.swift
│   ├── PearcleanerApp.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AccentColor.colorset/
│   │   │   │   └── Contents.json
│   │   │   └── Contents.json
│   │   ├── Glass.icon/
│   │   │   ├── Assets/
│   │   │   │   └── pear.heic
│   │   │   └── icon.json
│   │   ├── Info.plist
│   │   ├── Localizable.xcstrings
│   │   ├── Pearcleaner.entitlements
│   │   └── askpass.sh
│   ├── Style/
│   │   ├── ChromeBorder.swift
│   │   ├── CircularProgressView.swift
│   │   ├── ControlGroupChrome.swift
│   │   ├── PearGroupBox.swift
│   │   ├── SparkleProgressBar.swift
│   │   ├── Styles.swift
│   │   └── Theme.swift
│   ├── Views/
│   │   ├── AppsUpdaterView/
│   │   │   ├── AdoptionSheetView.swift
│   │   │   ├── AppsUpdaterView.swift
│   │   │   ├── CaskAdoptionContentView.swift
│   │   │   ├── ExpandableActionButton.swift
│   │   │   ├── UpdateDetailView.swift
│   │   │   ├── UpdateRowViewSidebar.swift
│   │   │   └── UpdaterDetailsSidebar.swift
│   │   ├── AppsView/
│   │   │   ├── AppListItems.swift
│   │   │   ├── AppSearchView.swift
│   │   │   ├── AppsListView.swift
│   │   │   └── GridAppItem.swift
│   │   ├── Brew/
│   │   │   ├── AutoUpdateSection.swift
│   │   │   ├── HomebrewView.swift
│   │   │   ├── LogViewerSheet.swift
│   │   │   ├── MaintenanceSection.swift
│   │   │   ├── PackageDetailsSidebar.swift
│   │   │   ├── SearchInstallSection.swift
│   │   │   └── TapManagementSection.swift
│   │   ├── Components/
│   │   │   ├── BadgeOverlay.swift
│   │   │   ├── GlobalConsoleView.swift
│   │   │   ├── PermissionsSheetView.swift
│   │   │   ├── SidebarDetailView/
│   │   │   │   ├── GenericSidebarListView.swift
│   │   │   │   └── SidebarDetailLayout.swift
│   │   │   ├── StandardSheetView.swift
│   │   │   └── TCCPermissionViewer.swift
│   │   ├── DaemonView.swift
│   │   ├── DeleteHistoryView.swift
│   │   ├── DevelopmentView.swift
│   │   ├── FileSearchView.swift
│   │   ├── FilesView/
│   │   │   ├── FileCategory.swift
│   │   │   ├── FileListView.swift
│   │   │   ├── FilesSidebarView.swift
│   │   │   ├── FilesView.swift
│   │   │   └── TranslationSelectionSheet.swift
│   │   ├── LipoView/
│   │   │   ├── LipoSidebarView.swift
│   │   │   └── LipoView.swift
│   │   ├── MainWindow.swift
│   │   ├── PackageView.swift
│   │   ├── PluginsView.swift
│   │   ├── Settings/
│   │   │   ├── About.swift
│   │   │   ├── Folders.swift
│   │   │   ├── General.swift
│   │   │   ├── Helper.swift
│   │   │   ├── Interface.swift
│   │   │   ├── SettingsWindow.swift
│   │   │   └── Update.swift
│   │   └── ZombieView/
│   │       ├── ZombieSidebarView.swift
│   │       └── ZombieView.swift
│   └── announcements.json
├── Pearcleaner.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           ├── FinderOpen.xcscheme
│           ├── Pearcleaner Debug.xcscheme
│           ├── Pearcleaner Release.xcscheme
│           ├── PearcleanerSentinel Release.xcscheme
│           └── PearcleanerSentinel.xcscheme
├── PearcleanerHelper/
│   ├── CodesignCheck.swift
│   ├── com.alienator88.Pearcleaner.PearcleanerHelper.plist
│   └── main.swift
├── PearcleanerSentinel/
│   ├── FileWatcher.swift
│   ├── com.alienator88.PearcleanerSentinel.plist
│   └── main.swift
├── README.md
├── Shared/
│   └── AppGroupDefaults.swift
└── announcements.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug.md
================================================
---
name: 🐛 Bug Report
about: For submitting new bugs related to the application functionality
title: "[BUG] ENTER ISSUE TITLE HERE"
labels: 'bug'
assignees: ''
---

<!--
WARNING: Issues with the template below removed will be closed!
-->

<!--
Thanks for helping make Pearcleaner better! Before you submit your issue, please make sure you follow the list below and check the appropriate boxes by putting an x inside the [ ]: [x] 
-->

### New Issue Checklist

- [ ] I updated Pearcleaner to the latest version and still observe the issue
- [ ] I searched for [existing GitHub issues](https://github.com/alienator88/pearcleaner/issues)
- [ ] OS Version: [e.g. 13.0]
- [ ] Pearcleaner Version: [e.g. 3.x.x]

---

### Issue:
<!-- A clear and concise description of what the bug is -->

#### Steps:
<!-- Provide step by step directions on how the issue is reproduced -->

#### Screenshots:
<!-- If applicable, add screenshots to show the bug -->


<!-- If your issue has logging that might help, you can grab them via the steps below -->

<!-- 
1. While Pearcleaner is running, push `CMD+D` to open the debug console and show captured logs
2. If relevant logs are seen in the console, copy them below 
-->
#### Debug Console
<pre> [REPLACE THIS TEXT BETWEEN THE PRE TAGS WITH YOUR LOGS] </pre>

---

<!-- 
 1. Open the Terminal app and run the following command: 
    log stream --level debug --style compact --predicate 'subsystem == "com.alienator88.Pearcleaner"'
 2. Launch Pearcleaner to reproduce the startup issue
 3. Copy the logs below from Terminal
  -->
  #### Console Logs
<pre> [REPLACE THIS TEXT BETWEEN THE PRE TAGS WITH YOUR LOGS] </pre>


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: PEARCLEANER IS NOW IN MAINTENANCE MODE
    url: https://github.com/alienator88/Pearcleaner/blob/main/.github/ISSUE_TEMPLATE/project.md
    about: New feature requests will be ignored/closed. Click here for more details.
  - name: ❓ Ask a Question
    url: https://github.com/alienator88/Pearcleaner/discussions/new?category=general
    about: Ask the community for help
  


================================================
FILE: .github/ISSUE_TEMPLATE/feature.md
================================================
<!--
---
name: 🌟 Feature Request
about: For submitting new feature ideas
title: "[FR] ENTER TITLE HERE"
labels: 'enhancement'
assignees: ''
---
-->

<!--
WARNING: Issues with the template below removed will be closed!
-->

<!--
Thanks for helping make Pearcleaner better! Before you submit your issue, please make sure you follow the list below and check the appropriate boxes by putting an x inside the [ ]: [x] 
-->

### New Feature Checklist

- [ ] I searched for existing open/closed feature requests and found none related to this request
- [ ] My request is not design/opinion based <!-- I'm fairly opinionated on the design -->
- [ ] My request will be beneficial to most users <!-- Fringe/edge-case requests will likely not be accepted -->

---

### Request:
<!-- Explain clearly and in detail -->

#### Desired Solution:
<!-- Describe how this should be implemented -->

#### Additional Context:
<!-- Add any other context or screenshots that might help. -->


================================================
FILE: .github/ISSUE_TEMPLATE/project.md
================================================
This project is now in **maintenance mode**.

---

I’m no longer accepting **NEW** feature requests as Pearcleaner has grown to be fairly feature rich, 
and I want to avoid it ballooning further in size or scope. Keeping the project stable, focused, and maintainable is now the priority. Going forward, the focus will be on:

- Bug fixes
- Regressions
- Small improvements to already existing features

Issues related to bugs are still welcome, and reasonable pull requests that focus on fixes or reliability 
improvements will still be reviewed. New feature requests will be ignored/closed if submitted.

Thanks to everyone who has used the project, filed issues, and contributed over time!


================================================
FILE: .github/workflows/issues.yml
================================================
name: Close empty issues and templates
on:
  issues:
    types:
      - reopened
      - opened
      - edited

jobs:
  closeEmptyIssuesAndTemplates:
    name: Close empty issues and templates
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3 # NOTE: Retrieve issue templates.
    - name: Run empty issues closer action
      uses: rickstaa/empty-issues-closer-action@v1
      env:
        github_token: ${{ secrets.GITHUB_TOKEN }}
      with:
        close_comment: Closing this issue because it appears to be empty. Please update the issue for it to be reopened.
        open_comment: Reopening this issue because the author provided more information.
        check_templates: true
        template_close_comment: Closing this issue since the issue template was not filled in. Please provide us with more information to have this issue reopened.
        template_open_comment: Reopening this issue because the author provided more information.


================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
.DS_Store
Builds/Export
.claude
Pearcleaner/Resources/Localizable.xcstrings.bak

================================================
FILE: Builds/ExportOptions.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>method</key>
	<string>mac-application</string>
	<key>teamID</key>
	<string>BK8443AXLU</string>
	<key>signingCertificate</key>
	<string>Developer ID Application: Marius Lupascu (BK8443AXLU)</string>
	<key>signingStyle</key>
	<string>manual</string>
	<key>stripSwiftSymbols</key>
	<true/>
	<key>destination</key>
	<string>export</string>
	<key>uploadSymbols</key>
	<true/>
	<key>manageAppVersionAndBuildNumber</key>
	<true/>
	<key>thinning</key>
	<string>&lt;none&gt;</string>
	<key>embedOnDemandResourcesAssetPacksInBundle</key>
	<true/>
	<key>generateAppStoreInformation</key>
	<false/>
	<key>testFlightInternalTestingOnly</key>
	<false/>
</dict>
</plist>


================================================
FILE: Builds/changes.md
================================================
### What's New

- [x] Ability to hide unused utility pages from global menu in Settings > Interface > Startup View - #476
- [x] "Update All" button now shows progress in toolbar for Updater view
- [x] Add pinyin sorting to homebrew view and updater view - #479
- [x] Warn if updating MAS app with a different source as it can break MAS connection - #484

### Fixes

- [x] Fix pear list-orphaned cli command - #472
- [x] Fix owner of receipt to root:wheel for installd workaround - #475
- [x] Add contentShape around sidebar toggle icons - #485
- [x] Fix computed property not refreshing action buttons in some cases for Updater view
- [x] Reorder some layout components in Updater view
- [x] Translations


================================================
FILE: FUNDING.yml
================================================
github: alienator88


================================================
FILE: FinderOpen/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: FinderOpen/Assets.xcassets/Glass.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "Glass.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: FinderOpen/FinderOpen.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.application-groups</key>
	<array>
		<string>group.com.alienator88.Pearcleaner</string>
	</array>
	<key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
	<array>
		<string>/</string>
	</array>
</dict>
</plist>


================================================
FILE: FinderOpen/FinderOpen.swift
================================================
//
//  FinderSync.swift
//  FinderOpen
//
//  Created by Alin Lupascu on 4/11/24.
//

import Cocoa
import FinderSync

class FinderOpen: FIFinderSync {

    override init() {
        super.init()
        NSLog("FinderSync() launched from %@", Bundle.main.bundlePath as NSString)
        // Set the directory URLs that the Finder Sync extension observes
        FIFinderSyncController.default().directoryURLs = Set([URL(fileURLWithPath: "/")])
    }

    override func menu(for menuKind: FIMenuKind) -> NSMenu {
        let menu = NSMenu(title: "")

        // Ensure we are dealing with the contextual menu for items
        if menuKind == .contextualMenuForItems {
            // Get the selected items
            if let selectedItemURLs = FIFinderSyncController.default().selectedItemURLs(),
               selectedItemURLs.count == 1, selectedItemURLs.first?.pathExtension == "app" {
                // Add menu item if the selected item is a .app file
                let menuItem = NSMenuItem(title: String(localized: "Pearcleaner Uninstall"), action: #selector(openInMyApp), keyEquivalent: "")
                // Add icon if enabled in main app
                if UserDefaults.showAppIconInMenu {
                    if let appIcon = NSApp.applicationIconImage {
                        appIcon.size = NSSize(width: 16, height: 16)
                        menuItem.image = appIcon
                    } else if let fallbackIcon = NSImage(named: "Glass") {
                        fallbackIcon.size = NSSize(width: 16, height: 16)
                        menuItem.image = fallbackIcon
                    }
                }
                menu.addItem(menuItem)

            }
        }

        // Return the menu (which may be empty if the conditions are not met)
        return menu

    }

    @objc func openInMyApp(_ sender: AnyObject?) {
        // Get the selected items (files/folders) in Finder
        guard let selectedItems = FIFinderSyncController.default().selectedItemURLs(), !selectedItems.isEmpty else {
            return
        }

        // Consider only the first selected item
        let firstSelectedItem = selectedItems[0]
        let path = firstSelectedItem.path
        NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(path)")!)

    }

}


================================================
FILE: FinderOpen/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>NSExtension</key>
	<dict>
		<key>NSExtensionAttributes</key>
		<dict/>
		<key>NSExtensionPointIdentifier</key>
		<string>com.apple.FinderSync</string>
		<key>NSExtensionPrincipalClass</key>
		<string>$(PRODUCT_MODULE_NAME).FinderOpen</string>
	</dict>
</dict>
</plist>


================================================
FILE: FinderOpen/Localizable.xcstrings
================================================
{
  "sourceLanguage" : "en",
  "strings" : {
    "Pearcleaner Uninstall" : {
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleaner Deinstallation"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Desinstalar Pearcleaner"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Désinstaller avec Pearcleaner"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Hapus Instalasi Pearlcleaner"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleanerでアンインストール"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Odinstaluj za pomocą Pearcleaner"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Desinstalar o Pearcleaner"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Удалить через Pearcleaner"
          }
        },
        "sk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Odinštalovať Pearcleanerom"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleaner ile Kaldırma"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Видалити через Pearcleaner"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Gỡ cài đặt Pearcleaner"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleaner 卸载"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleaner 解除安裝"
          }
        },
        "zh-HK" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pearcleaner 解除安裝"
          }
        }
      }
    }
  },
  "version" : "1.0"
}

================================================
FILE: LICENSE.md
================================================
“Commons Clause” License Condition v1.0

The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.

Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.

For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.

Software: Pearcleaner

License: Apache 2.0 with Commons Clause

Licensor: alienator88

---

                                Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: Pear Resources/Glass.icon/icon.json
================================================
{
  "fill" : {
    "linear-gradient" : [
      "display-p3:0.93159,0.94081,0.94081,1.00000",
      "display-p3:0.75900,0.75900,0.75900,1.00000"
    ]
  },
  "groups" : [
    {
      "layers" : [
        {
          "blend-mode" : "normal",
          "glass-specializations" : [
            {
              "value" : false
            },
            {
              "appearance" : "dark",
              "value" : true
            },
            {
              "appearance" : "tinted",
              "value" : true
            }
          ],
          "hidden" : false,
          "image-name" : "pear.heic",
          "name" : "pear",
          "position" : {
            "scale" : 1.5,
            "translation-in-points" : [
              11.3046875,
              3.296875
            ]
          }
        }
      ],
      "shadow" : {
        "kind" : "neutral",
        "opacity" : 0.5
      },
      "translucency" : {
        "enabled" : true,
        "value" : 0.5
      }
    }
  ],
  "supported-platforms" : {
    "squares" : [
      "macOS"
    ]
  }
}

================================================
FILE: Pear Resources/Pear.psd
================================================
[File too large to display: 10.7 MB]

================================================
FILE: Pearcleaner/Logic/AppCommands.swift
================================================
//
//  AppCommands.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 10/31/23.
//

import SwiftUI
import AlinFoundation

struct AppCommands: Commands {

    let appState: AppState
    let locations: Locations
    let fsm: FolderSettingsManager
    let updater: Updater
    @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
    @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
    @State private var windowController = WindowManager()
    @ObservedObject private var debugLogger = UpdaterDebugLogger.shared

    init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater) {
        self.appState = appState
        self.locations = locations
        self.fsm = fsm
        self.updater = updater
    }

    var body: some Commands {
        
        // Pearcleaner Menu
        CommandGroup(replacing: .appInfo) {

            Button {
                openAppSettingsWindow(tab: .about, updater: updater)
            } label: {
                Label("About \(Bundle.main.name)", systemImage: "info.circle.fill")
            }

            Divider()

            Button {
                openAppSettingsWindow(updater: updater)
            } label: {
                Label("Settings", systemImage: "gearshape")
            }
            .keyboardShortcut(",", modifiers: .command)

            Button {
                updater.checkForUpdates(sheet: true, force: true)
            } label: {
                Label("Check for Updates", systemImage: "tray.and.arrow.down.fill")
            }
            .keyboardShortcut("u", modifiers: .command)

            Button {
                showCustomAlert(
                    title: "Warning!",
                    message: "Pearcleaner and all of its files will be cleanly removed, are you sure?",
                    okText: "Uninstall",
                    style: .warning,
                    onOk: {
                        uninstallPearcleaner(appState: appState, locations: locations)
                    }
                )
            } label: {
                Label("Uninstall Pearcleaner", systemImage: "trash.fill")
            }

        }

        
        
        // Edit Menu
        CommandGroup(replacing: .undoRedo) {

            Button
            {
                let result = undoTrash()
                if result {
                    if appState.currentPage == .plugins {
                        // For plugins view, post notification to refresh
                        NotificationCenter.default.post(name: NSNotification.Name("PluginsViewShouldRefresh"), object: nil)
                    } else if appState.currentPage == .fileSearch {
                        // For file search view, post notification to undo (has unique cache logic)
                        NotificationCenter.default.post(name: NSNotification.Name("FileSearchViewShouldUndo"), object: nil)
                    } else if appState.currentPage == .orphans {
                        // For orphans view, post notification to refresh
                        NotificationCenter.default.post(name: NSNotification.Name("ZombieViewShouldRefresh"), object: nil)
                    } else if appState.currentPage == .packages {
                        // For packages view, post notification to refresh
                        NotificationCenter.default.post(name: NSNotification.Name("PackagesViewShouldRefresh"), object: nil)
                    } else if appState.currentPage == .development {
                        // For development view, post notification to refresh
                        NotificationCenter.default.post(name: NSNotification.Name("DevelopmentViewShouldRefresh"), object: nil)
                    } else {
                        loadApps(folderPaths: fsm.folderPaths)
                        // After reload, if we're viewing files, refresh the file view
                        if appState.currentView == .files {
                            Task { @MainActor in
                                try? await Task.sleep(nanoseconds: 500_000_000)
                                showAppInFiles(appInfo: appState.appInfo, appState: appState, locations: locations)
                            }
                        }
                    }
                }

            } label: {
                Label("Undo Removal", systemImage: "clear")
            }
            .keyboardShortcut("z", modifiers: .command)
            .disabled(!FileManagerUndo.shared.undoManager.canUndo)

            Divider()

            Button {
                appState.showDeleteHistory = true
            } label: {
                Label("Delete History", systemImage: "clock.arrow.circlepath")
            }
            .keyboardShortcut("y", modifiers: [.command, .shift])
            .disabled(UndoHistoryManager.shared.history.isEmpty)

        }


        // Window Menu
        CommandGroup(after: .sidebar) {

            Menu {
                Button
                {
                    appState.currentPage = .applications

                } label: {
                    Text("Applications")
                }
                .keyboardShortcut("1", modifiers: .command)

                Button
                {
                    appState.currentPage = .development

                } label: {
                    Text("Development")
                }
                .keyboardShortcut("2", modifiers: .command)

                Button
                {
                    appState.currentPage = .fileSearch

                } label: {
                    Text("File Search")
                }
                .keyboardShortcut("3", modifiers: .command)

                Button
                {
                    appState.currentPage = .homebrew

                } label: {
                    Text("Homebrew")
                }
                .keyboardShortcut("4", modifiers: .command)

                Button
                {
                    appState.currentPage = .lipo

                } label: {
                    Text("App Lipo")
                }
                .keyboardShortcut("5", modifiers: .command)

                Button
                {
                    appState.currentPage = .orphans

                } label: {
                    Text("Orphaned Files")
                }
                .keyboardShortcut("6", modifiers: .command)

                Button
                {
                    appState.currentPage = .packages

                } label: {
                    Text("Packages")
                }
                .keyboardShortcut("7", modifiers: .command)

                Button
                {
                    appState.currentPage = .plugins

                } label: {
                    Text("Plugins")
                }
                .keyboardShortcut("8", modifiers: .command)

                Button
                {
                    appState.currentPage = .services

                } label: {
                    Text("Services")
                }
                .keyboardShortcut("9", modifiers: .command)

                Button
                {
                    appState.currentPage = .updater

                } label: {
                    Text("Updater")
                }
                .keyboardShortcut("0", modifiers: .command)

            } label: {
                Label("Navigate To", systemImage: "location.north.fill")
            }

        }


        // Tools Menu
        CommandMenu(Text("Tools", comment: "Tools Menu")) {

            Button {
                Task { @MainActor in
                    switch appState.currentPage {
                    case .applications:
                        if appState.currentView == .files {
                            // User is viewing an app's files - refresh the files list
                            let currentAppInfo = appState.appInfo
                            updateOnMain {
                                appState.selectedItems = []
                            }
                            withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
                                showAppInFiles(appInfo: currentAppInfo, appState: appState, locations: locations)
                            }
                        } else {
                            // User is on empty view or app list - refresh the app list
                            withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
                                // Flush bundle caches before reloading to ensure fresh version info
                                flushBundleCaches(for: appState.sortedApps)
                                loadApps(folderPaths: fsm.folderPaths)
                            }
                        }
                    case .development:
                        NotificationCenter.default.post(name: NSNotification.Name("DevelopmentViewShouldRefresh"), object: nil)
                    case .fileSearch:
                        NotificationCenter.default.post(name: NSNotification.Name("FileSearchViewShouldRefresh"), object: nil)
                    case .homebrew:
                        NotificationCenter.default.post(name: NSNotification.Name("HomebrewViewShouldRefresh"), object: nil)
                    case .lipo:
                        NotificationCenter.default.post(name: NSNotification.Name("LipoViewShouldRefresh"), object: nil)
                    case .orphans:
                        NotificationCenter.default.post(name: NSNotification.Name("ZombieViewShouldRefresh"), object: nil)
                    case .packages:
                        NotificationCenter.default.post(name: NSNotification.Name("PackagesViewShouldRefresh"), object: nil)
                    case .plugins:
                        NotificationCenter.default.post(name: NSNotification.Name("PluginsViewShouldRefresh"), object: nil)
                    case .services:
                        NotificationCenter.default.post(name: NSNotification.Name("DaemonViewShouldRefresh"), object: nil)
                    case .updater:
                        NotificationCenter.default.post(name: NSNotification.Name("UpdaterViewShouldRefresh"), object: nil)
                    }
                }
            } label: {
                Label("Refresh", systemImage: "arrow.counterclockwise.circle")
            }
            .keyboardShortcut("r", modifiers: .command)

            Button
            {
                if !appState.selectedItems.isEmpty {
                    createTarArchive(appState: appState)
                }
            } label: {
                Label("Bundle Files...", systemImage: "archivebox")
            }
            .keyboardShortcut("b", modifiers: .command)
            .disabled(appState.selectedItems.isEmpty)

            Button
            {
                if !appState.appInfo.bundleIdentifier.isEmpty {
                    saveURLsToFile(appState: appState)
                }
            } label: {
                Label("Export File Paths...", systemImage: "square.and.arrow.up")
            }
            .keyboardShortcut("e", modifiers: .command)
            .disabled(appState.selectedItems.isEmpty)

            Button
            {
                if !appState.appInfo.bundleIdentifier.isEmpty {
                    saveURLsToFile(appState: appState, copy: true)
                }
            } label: {
                Label("Copy File Paths", systemImage: "square.and.arrow.up")
            }
            .keyboardShortcut("c", modifiers: [.command, .option])
            .disabled(appState.selectedItems.isEmpty)

        }

        CommandGroup(after: .help) {
            // Debug options
            Button {
                withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
                    windowController.open(with: ConsoleView(), width: 600, height: 400)
                }
            } label: {
                Label("Debug Console", systemImage: "ladybug")
            }
            .keyboardShortcut("d", modifiers: .command)

            Button {
                // Export debug info to file
                exportDebugInfo(appState: appState)
            } label: {
                Label("Export Debug Info...", systemImage: "info.circle")
            }
            .keyboardShortcut("i", modifiers: [.command, .shift])

            // Updater debug log export (only visible on Updater page)
            if appState.currentPage == .updater {
                Button {
                    exportUpdaterDebugInfo()
                } label: {
                    Label("Export Updater Debug Log...", systemImage: "arrow.triangle.2.circlepath.circle")
                }
                .keyboardShortcut("u", modifiers: [.command, .shift])
                .disabled(!debugLogger.hasLogs)
            }

            Divider()

            // GitHub Menu
            Button
            {
                NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner")!)
            } label: {
                Label("View Repository", systemImage: "paperplane")
            }


            Button
            {
                NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
            } label: {
                Label("View Releases", systemImage: "paperplane")
            }


            Button
            {
                NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues")!)
            } label: {
                Label("View Issues", systemImage: "paperplane")
            }


            Divider()


            Button
            {
                NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
            } label: {
                Label("Submit New Issue", systemImage: "paperplane")
            }
        }


    }
}


================================================
FILE: Pearcleaner/Logic/AppInfoFetch.swift
================================================
//
//  AppInfoFetch.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 3/20/24.
//

import Foundation
import SwiftUI
import AlinFoundation

// MARK: - Helper Functions

/// Read Info.plist directly from disk without using Bundle cache
/// This is useful when Bundle(url:) returns nil due to macOS not yet indexing a newly installed app
private func readInfoPlistDirect(at appPath: URL) -> [String: Any]? {
    let infoPlistURL = appPath.appendingPathComponent("Contents/Info.plist")
    return NSDictionary(contentsOf: infoPlistURL) as? [String: Any]
}

// Metadata-based AppInfo Fetcher Class
class MetadataAppInfoFetcher {
    static func getAppInfo(fromMetadata metadata: [String: Any], atPath path: URL) -> AppInfo? {
        // Extract metadata attributes for known fields
        var displayName = metadata["kMDItemDisplayName"] as? String ?? ""
        displayName = displayName.replacingOccurrences(of: ".app", with: "")
        let fsName = metadata["kMDItemFSName"] as? String ?? path.lastPathComponent
        let appName = displayName.isEmpty ? fsName : displayName

        let bundleIdentifier = metadata["kMDItemCFBundleIdentifier"] as? String ?? ""

        // Get version and build number directly from bundle Info.plist instead of metadata (always up-to-date)
        let (version, buildNumber): (String, String?) = {
            // Try Bundle(url:) first
            if let bundle = Bundle(url: path) {
                // Extract marketing version (CFBundleShortVersionString), fallback to CFBundleVersion if missing
                let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
                let buildVer = bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""

                // Use shortVersion if available, otherwise use buildVersion
                let marketingVersion = shortVersion.isEmpty ? buildVer : shortVersion

                // Build number is CFBundleVersion (only set if different from marketing version)
                let build = shortVersion.isEmpty ? nil : buildVer
                return (marketingVersion, build)
            }

            // Fallback: Read Info.plist directly from disk (useful for newly installed apps)
            if let infoDict = readInfoPlistDirect(at: path) {
                let shortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
                let buildVer = infoDict["CFBundleVersion"] as? String ?? ""

                let marketingVersion = shortVersion.isEmpty ? buildVer : shortVersion
                let build = shortVersion.isEmpty ? nil : buildVer
                return (marketingVersion, build)
            }

            return ("", nil)
        }()

        // Size
        let logicalSize = metadata["kMDItemLogicalSize"] as? Int64 ?? 0

        // Extract optional date fields early so we can pass them to fallback if needed
        let creationDate = metadata["kMDItemFSCreationDate"] as? Date
        let contentChangeDate = metadata["kMDItemFSContentChangeDate"] as? Date
        let lastUsedDate = metadata["kMDItemLastUsedDate"] as? Date
        let dateAdded = metadata["kMDItemDateAdded"] as? Date

        // Check if any of the critical fields are missing or invalid
        // Note: Size can be 0 for some apps where Spotlight hasn't indexed size yet, so only require core identifiers
        if appName.isEmpty || bundleIdentifier.isEmpty || version.isEmpty {
            // Fallback to the regular AppInfoFetcher for this app, but preserve dates we extracted from metadata
            return AppInfoFetcher.getAppInfo(atPath: path, dates: (creationDate, contentChangeDate, lastUsedDate, dateAdded))
        }

        // Determine architecture type
        let arch = checkAppBundleArchitecture(at: path.path)

        // Use similar helper functions as `AppInfoFetcher` for attributes not found in metadata
        let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path)
        let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped, md: true)
        let webApp = AppInfoUtils.isWebApp(appPath: path)
        let system = !path.path.contains(NSHomeDirectory())

        // Get cask metadata (includes cask name and auto_updates flag)
        // Pass both display name, path, and bundle ID to handle various matching scenarios
        let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier)
        let cask = caskInfo?.caskName
        let autoUpdates = caskInfo?.autoUpdates

        // Get entitlements for the app
        let entitlements = getEntitlements(for: path.path)
        let teamIdentifier = getTeamIdentifier(for: path.path)

        // Detect update sources (done at load time for performance)
        let bundle = Bundle(url: path)
        let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle?.infoDictionary)
        let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped)

        // Extract App Store adamID from metadata (if available)
        let adamID: UInt64? = {
            if let adamValue = metadata["kMDItemAppStoreAdamID"] {
                // Handle NSNumber conversion
                if let number = adamValue as? NSNumber {
                    return number.uint64Value
                }
                // Handle direct UInt64
                if let uint = adamValue as? UInt64 {
                    return uint
                }
            }
            return nil
        }()

        return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName,
                       appVersion: version, appBuildNumber: buildNumber, appIcon: appIcon, webApp: webApp, wrapped: wrapped, system: system,
                       arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: logicalSize, fileSize: [:],
                       fileIcon: [:], creationDate: creationDate, contentChangeDate: contentChangeDate, lastUsedDate: lastUsedDate, dateAdded: dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier)
    }

    // MARK: - Phase 1: Fast Loading with AppInfoMini

    /// Fast lightweight app info loading for initial display
    /// Skips expensive operations: arch detection, entitlements, cask lookup, team identifier
    /// Always calculates bundleSize (required for sorting)
    static func getAppInfoMini(fromMetadata metadata: [String: Any], atPath path: URL) -> AppInfoMini? {
        // Extract basic info from metadata
        var displayName = metadata["kMDItemDisplayName"] as? String ?? ""
        displayName = displayName.replacingOccurrences(of: ".app", with: "")
        let fsName = metadata["kMDItemFSName"] as? String ?? path.lastPathComponent
        let appName = displayName.isEmpty ? fsName : displayName

        let bundleIdentifier = metadata["kMDItemCFBundleIdentifier"] as? String ?? ""

        // Get version from bundle Info.plist
        let version: String = {
            if let bundle = Bundle(url: path) {
                let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
                let buildVer = bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
                return shortVersion.isEmpty ? buildVer : shortVersion
            }

            if let infoDict = readInfoPlistDirect(at: path) {
                let shortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
                let buildVer = infoDict["CFBundleVersion"] as? String ?? ""
                return shortVersion.isEmpty ? buildVer : shortVersion
            }

            return ""
        }()

        // Require critical fields
        if appName.isEmpty || bundleIdentifier.isEmpty || version.isEmpty {
            return nil
        }

        // Get app icon (required for display, fast enough ~5-10ms)
        let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path)
        let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped, md: true)

        // Determine if system app
        let system = !path.path.contains(NSHomeDirectory())

        // Get bundleSize - ALWAYS calculate, never 0 (required for sorting)
        let bundleSize: Int64 = {
            // Try mdls metadata first (fast)
            if let mdlsSize = metadata["kMDItemLogicalSize"] as? Int64, mdlsSize > 0 {
                return mdlsSize
            }

            // Fallback: Calculate using totalSizeOnDisk (same as AppInfoFetcher)
            return totalSizeOnDisk(for: path)
        }()

        // Extract date fields from metadata
        let creationDate = metadata["kMDItemFSCreationDate"] as? Date
        let contentChangeDate = metadata["kMDItemFSContentChangeDate"] as? Date
        let lastUsedDate = metadata["kMDItemLastUsedDate"] as? Date
        let dateAdded = metadata["kMDItemDateAdded"] as? Date

        return AppInfoMini(
            id: UUID(),
            path: path,
            bundleIdentifier: bundleIdentifier,
            appName: appName,
            appVersion: version,
            appIcon: appIcon,
            system: system,
            bundleSize: bundleSize,  // ✅ Always calculated
            creationDate: creationDate,
            contentChangeDate: contentChangeDate,
            lastUsedDate: lastUsedDate,
            dateAdded: dateAdded
        )
    }

    // MARK: - Phase 2: Upgrade to Full AppInfo

    /// Upgrade AppInfoMini to full AppInfo with all expensive properties
    /// Runs expensive operations: arch, entitlements, cask lookup, team identifier
    static func upgradeToFullAppInfo(mini: AppInfoMini) -> AppInfo {
        let path = mini.path

        // Get bundle for further inspection
        let bundle = Bundle(url: path)
        let infoDict = bundle?.infoDictionary ?? readInfoPlistDirect(at: path)

        // NOW do the expensive operations that were skipped in Phase 1

        // Architecture detection (expensive: reads Mach-O binary headers)
        let arch = checkAppBundleArchitecture(at: path.path)

        // Entitlements scanning (expensive: code signing + recursive bundle scan)
        let entitlements = getEntitlements(for: path.path)

        // Team identifier (expensive: code signing read)
        let teamIdentifier = getTeamIdentifier(for: path.path)

        // Cask lookup (expensive: first-time builds entire lookup table)
        let caskInfo = getCaskInfo(for: mini.appName, appPath: path, bundleId: mini.bundleIdentifier)
        let cask = caskInfo?.caskName
        let autoUpdates = caskInfo?.autoUpdates

        // Detect app properties
        let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path)
        let webApp = AppInfoUtils.isWebApp(appPath: path)

        // Detect update sources
        let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: infoDict)
        let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped)

        // Get build number
        let appBuildNumber: String? = {
            if let dict = infoDict {
                let shortVersion = dict["CFBundleShortVersionString"] as? String ?? ""
                let buildVer = dict["CFBundleVersion"] as? String ?? ""
                return shortVersion.isEmpty ? nil : buildVer
            }
            return nil
        }()

        // Steam detection (not implemented in fast path, defaults to false)
        let steam = false

        // Adam ID (App Store ID) - would need to query mdls again, skip for now
        let adamID: UInt64? = nil

        return AppInfo(
            id: mini.id,
            path: mini.path,
            bundleIdentifier: mini.bundleIdentifier,
            appName: mini.appName,
            appVersion: mini.appVersion,
            appBuildNumber: appBuildNumber,
            appIcon: mini.appIcon,
            webApp: webApp,
            wrapped: wrapped,
            system: mini.system,
            arch: arch,                      // ✅ Phase 2 populated
            cask: cask,                      // ✅ Phase 2 populated
            steam: steam,
            hasSparkle: hasSparkle,          // ✅ Phase 2 populated
            isAppStore: isAppStore,          // ✅ Phase 2 populated
            adamID: adamID,
            autoUpdates: autoUpdates,
            bundleSize: mini.bundleSize,     // Keep from Phase 1
            lipoSavings: nil,
            fileSize: [:],                   // Populated when user selects app
            fileIcon: [:],                   // Populated when user selects app
            creationDate: mini.creationDate,
            contentChangeDate: mini.contentChangeDate,
            lastUsedDate: mini.lastUsedDate,
            dateAdded: mini.dateAdded,
            entitlements: entitlements,      // ✅ Phase 2 populated
            teamIdentifier: teamIdentifier   // ✅ Phase 2 populated
        )
    }
}


// MARK: - Update Source Detection Helpers

class AppCategoryDetector {
    /// Check if app has Sparkle update framework
    /// Detects Sparkle by checking for common Info.plist keys
    /// Excludes SetApp apps (they use Sparkle but are managed by SetApp)
    static func checkForSparkle(bundle: Bundle?, infoDict: [String: Any]?) -> Bool {
        guard let dict = infoDict ?? bundle?.infoDictionary else { return false }

        // Check for common Sparkle keys
        let hasSparkleKeys = dict["SUFeedURL"] != nil ||
               dict["SUFeedUrl"] != nil ||
               dict["SUPublicEDKey"] != nil ||
               dict["SUPublicDSAKeyFile"] != nil ||
               dict["SUEnableAutomaticChecks"] != nil

        // Exclude SetApp apps (they use Sparkle but are managed by SetApp)
        if hasSparkleKeys && isSetAppApp(bundle: bundle, infoDict: dict) {
            return false
        }

        return hasSparkleKeys
    }

    /// Check if app is a SetApp-managed app
    /// SetApp requires all apps to use the "-setapp" bundle ID suffix
    static func isSetAppApp(bundle: Bundle?, infoDict: [String: Any]?) -> Bool {
        // Try to get bundle ID from bundle first, then from infoDict
        if let bundleID = bundle?.bundleIdentifier ?? infoDict?["CFBundleIdentifier"] as? String {
            return bundleID.hasSuffix("-setapp")
        }
        return false
    }

    /// Check if app is from App Store
    /// Detects by checking for receipt or iTunes metadata
    static func checkForAppStore(bundle: Bundle?, path: URL, wrapped: Bool) -> Bool {
        // Check for wrapped iPad/iOS app first
        if wrapped {
            // Determine if path is wrapped or not
            // No wrapper: /Applications/App.app
            // With wrapper: /Applications/App.app/Wrapper/App.app
            let wrapperDir = path.appendingPathComponent("Wrapper")
            let isOuterWrapper = FileManager.default.fileExists(atPath: wrapperDir.path)

            // Use path directly if it's the outer wrapper, otherwise go up two levels to find it
            let outerWrapperPath = isOuterWrapper ? path : path.deletingLastPathComponent().deletingLastPathComponent()
            let iTunesMetadataPath = outerWrapperPath.appendingPathComponent("Wrapper/iTunesMetadata.plist").path

            if FileManager.default.fileExists(atPath: iTunesMetadataPath) {
                return true
            }
        }

        // Check for traditional Mac app receipt
        guard let receiptPath = bundle?.appStoreReceiptURL?.path else { return false }
        return FileManager.default.fileExists(atPath: receiptPath)
    }
}


class AppInfoUtils {
    /// Determines if the app is a web application by directly reading its `Info.plist` using the app path.
    static func isWebApp(appPath: URL) -> Bool {
        let infoPlistURL = appPath.appendingPathComponent("Contents/Info.plist")
        guard let infoDict = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else {
            return false
        }
        return (infoDict["LSTemplateApplication"] as? Bool ?? false) ||
        (infoDict["CFBundleExecutable"] as? String == "app_mode_loader")
    }

    /// Determines if the app is a web application based on its bundle.
    static func isWebApp(bundle: Bundle?) -> Bool {
        guard let infoDict = bundle?.infoDictionary else { return false }
        return (infoDict["LSTemplateApplication"] as? Bool ?? false) ||
        (infoDict["CFBundleExecutable"] as? String == "app_mode_loader")
    }

    /// Fetch app icon.
    static func fetchAppIcon(for path: URL, wrapped: Bool, md: Bool = false) -> NSImage? {
        let iconPath = wrapped ? (md ? path : path.deletingLastPathComponent().deletingLastPathComponent()) : path
        guard let appIcon = getIconForFileOrFolderNS(atPath: iconPath) else {
            printOS("App Icon not found for app at path: \(path)")
            return nil
        }

        let targetSize = NSSize(width: 50, height: 50)

        // OPTIMIZATION: Pre-render icon with cached representation using modern API
        // This must run on main thread to avoid deadlock with AppKit initialization
        func createRenderedIcon() -> NSImage {
            return NSImage(size: targetSize, flipped: false) { rect in
                appIcon.draw(in: rect)
                return true
            }
        }

        if Thread.isMainThread {
            return createRenderedIcon()
        } else {
            return DispatchQueue.main.sync {
                createRenderedIcon()
            }
        }
    }
}



func getMDLSMetadata(for paths: [String]) -> [String: [String: Any]]? {
    let kMDItemLogicalSize: CFString = "kMDItemLogicalSize" as CFString
    let kMDItemPhysicalSize: CFString = "kMDItemPhysicalSize" as CFString
    let kMDItemAppStoreAdamID: CFString = "kMDItemAppStoreAdamID" as CFString

    // List of metadata attributes to fetch
    let attributes: [CFString] = [
        kMDItemFSCreationDate,
        kMDItemFSContentChangeDate,
        kMDItemLastUsedDate,
        kMDItemDateAdded,
        kMDItemDisplayName,
        kMDItemCFBundleIdentifier,
        kMDItemFSName,
        kMDItemLogicalSize,
        kMDItemPhysicalSize,
        kMDItemAppStoreAdamID
    ]

    // OPTIMIZATION: Process in parallel chunks
    let chunks = createOptimalChunks(from: paths, minChunkSize: 15, maxChunkSize: 50)
    let queue = DispatchQueue(label: "com.pearcleaner.metadata", qos: .userInitiated, attributes: .concurrent)
    let group = DispatchGroup()

    var allResults: [String: [String: Any]] = [:]
    let resultsQueue = DispatchQueue(label: "com.pearcleaner.metadata.results")

    for chunk in chunks {
        group.enter()
        queue.async {
            autoreleasepool {
                var chunkResults: [String: [String: Any]] = [:]

                // Process each path in this chunk
                for path in chunk {
                    autoreleasepool {
                        guard let mdItem = MDItemCreate(nil, path as CFString) else {
                            return
                        }

                        var itemMetadata = [String: Any]()
                        for attribute in attributes {
                            if let value = MDItemCopyAttribute(mdItem, attribute) {
                                itemMetadata[attribute as String] = value
                            }
                        }
                        chunkResults[path] = itemMetadata
                    }
                }

                // Safely merge results
                resultsQueue.sync {
                    allResults.merge(chunkResults) { _, new in new }
                }
            }
            group.leave()
        }
    }

    group.wait()
    return allResults.isEmpty ? nil : allResults
}

// Add this helper extension if you don't have it yet:
extension Array {
    func chunked(into size: Int) -> [[Element]] {
        return stride(from: 0, to: count, by: size).map {
            Array(self[$0..<Swift.min($0 + size, count)])
        }
    }
}




//MARK: Fallback legacy function in case metadata doesn't contain the needed information
class AppInfoFetcher {
    static let fileManager = FileManager.default

    public static func getAppInfo(atPath path: URL, wrapped: Bool = false, dates: (creation: Date?, contentChange: Date?, lastUsed: Date?, dateAdded: Date?)? = nil) -> AppInfo? {
        if isDirectoryWrapped(path: path) {
            return handleWrappedDirectory(atPath: path, dates: dates)
        } else {
            return createAppInfoFromBundle(atPath: path, wrapped: wrapped, dates: dates)
        }
    }

    public static func isDirectoryWrapped(path: URL) -> Bool {
        let wrapperURL = path.appendingPathComponent("Wrapper")
        return fileManager.fileExists(atPath: wrapperURL.path)
    }

    private static func handleWrappedDirectory(atPath path: URL, dates: (creation: Date?, contentChange: Date?, lastUsed: Date?, dateAdded: Date?)? = nil) -> AppInfo? {
        let wrapperURL = path.appendingPathComponent("Wrapper")
        do {
            let contents = try fileManager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil)
            guard let firstAppFile = contents.first(where: { $0.pathExtension == "app" }) else {
                printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)")
                return nil
            }
            let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent)
            return getAppInfo(atPath: fullPath, wrapped: true, dates: dates)
        } catch {
            printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)")
            return nil
        }
    }

    private static func createAppInfoFromBundle(atPath path: URL, wrapped: Bool, dates: (creation: Date?, contentChange: Date?, lastUsed: Date?, dateAdded: Date?)? = nil) -> AppInfo? {
        // Try Bundle(url:) first
        if let bundle = Bundle(url: path), let bundleIdentifier = bundle.bundleIdentifier {
            let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent : path.localizedName()

            // Extract marketing version (CFBundleShortVersionString) - no fallback to build number
            let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true
                ? ""
                : bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

            // Extract build number (CFBundleVersion) separately
            let appBuildNumber = bundle.infoDictionary?["CFBundleVersion"] as? String

            let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped)
            let webApp = AppInfoUtils.isWebApp(bundle: bundle)

            let system = !path.path.contains(NSHomeDirectory())

            // Get cask metadata (includes cask name and auto_updates flag)
            // Pass both display name, path, and bundle ID to handle various matching scenarios
            let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier)
            let cask = caskInfo?.caskName
            let autoUpdates = caskInfo?.autoUpdates

            let arch = checkAppBundleArchitecture(at: path.path)

            // Get entitlements for the app
            let entitlements = getEntitlements(for: path.path)
            let teamIdentifier = getTeamIdentifier(for: path.path)

            // Detect update sources (done at load time for performance)
            let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle.infoDictionary)
            let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped)

            // adamID not available in fallback path (no mdls metadata)
            let adamID: UInt64? = nil

            return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon,
                           webApp: webApp, wrapped: wrapped, system: system, arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: dates?.creation, contentChangeDate: dates?.contentChange, lastUsedDate: dates?.lastUsed, dateAdded: dates?.dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier)
        }

        // Fallback: Read Info.plist directly from disk (useful for newly installed apps where Bundle cache isn't ready)
        if let infoDict = readInfoPlistDirect(at: path), let bundleIdentifier = infoDict["CFBundleIdentifier"] as? String {
            let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent : path.localizedName()

            // Extract marketing version (CFBundleShortVersionString) - no fallback to build number
            let appVersion = (infoDict["CFBundleShortVersionString"] as? String)?.isEmpty ?? true
                ? ""
                : infoDict["CFBundleShortVersionString"] as? String ?? ""

            // Extract build number (CFBundleVersion) separately
            let appBuildNumber = infoDict["CFBundleVersion"] as? String

            let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped)
            let webApp = AppInfoUtils.isWebApp(appPath: path)  // Use path-based version since we don't have bundle

            let system = !path.path.contains(NSHomeDirectory())

            // Get cask metadata (includes cask name and auto_updates flag)
            // Pass both display name, path, and bundle ID to handle various matching scenarios
            let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier)
            let cask = caskInfo?.caskName
            let autoUpdates = caskInfo?.autoUpdates

            let arch = checkAppBundleArchitecture(at: path.path)

            // Get entitlements for the app
            let entitlements = getEntitlements(for: path.path)
            let teamIdentifier = getTeamIdentifier(for: path.path)

            // Detect update sources (done at load time for performance)
            let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: nil, infoDict: infoDict)
            let isAppStore = AppCategoryDetector.checkForAppStore(bundle: nil, path: path, wrapped: wrapped)

            // adamID not available in fallback path (no mdls metadata)
            let adamID: UInt64? = nil

            return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon,
                           webApp: webApp, wrapped: wrapped, system: system, arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: dates?.creation, contentChangeDate: dates?.contentChange, lastUsedDate: dates?.lastUsed, dateAdded: dates?.dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier)
        }

        // If both Bundle and direct reading failed, check if this might be a Steam game
        if let steamAppInfo = SteamAppInfoFetcher.checkForSteamGame(launcherPath: path) {
            return steamAppInfo
        }

        printOS("Bundle not found or missing bundle identifier at path: \(path)")
        return nil
    }

}

//MARK: Steam Games Support
class SteamAppInfoFetcher {
    static let fileManager = FileManager.default
    
    /// Check if a failed app bundle is actually a Steam game launcher and find the real bundle
    static func checkForSteamGame(launcherPath: URL) -> AppInfo? {
        // Extract the app name from the launcher path (e.g., "Helltaker" from "Helltaker.app")
        let appName = launcherPath.deletingPathExtension().lastPathComponent
        
        // Check if this app exists in the Steam common directory
        let steamCommonPath = "\(NSHomeDirectory())/Library/Application Support/Steam/steamapps/common"
        let steamGamePath = steamCommonPath + "/" + appName
        
        guard fileManager.fileExists(atPath: steamGamePath) else {
            return nil
        }
        
        // Look for the actual .app bundle within the Steam game directory
        guard let actualAppBundle = findAppBundle(in: steamGamePath) else {
            return nil
        }
        
        // Create AppInfo using the actual Steam game bundle but keep the launcher path
        return createSteamAppInfo(launcherPath: launcherPath, actualBundlePath: actualAppBundle, gameFolderName: appName)
    }
    
    /// Find the .app bundle within a Steam game directory
    private static func findAppBundle(in directory: String) -> URL? {
        do {
            let contents = try fileManager.contentsOfDirectory(atPath: directory)
            
            // Look for .app files
            for item in contents {
                if item.hasSuffix(".app") {
                    let appPath = directory + "/" + item
                    let appURL = URL(fileURLWithPath: appPath)
                    
                    // Verify it has an Info.plist
                    let infoPlistPath = appPath + "/Contents/Info.plist"
                    if fileManager.fileExists(atPath: infoPlistPath) {
                        return appURL
                    }
                }
            }
        } catch {
            printOS("Error searching for app bundle in \(directory): \(error)")
        }
        
        return nil
    }
    
    /// Create AppInfo for Steam games using the launcher path but actual bundle info
    private static func createSteamAppInfo(launcherPath: URL, actualBundlePath: URL, gameFolderName: String) -> AppInfo? {
        guard let bundle = Bundle(url: actualBundlePath) else {
            printOS("Steam game bundle not found at path: \(actualBundlePath)")
            return nil
        }
        
        // Handle missing bundle identifier by providing a fallback
        let bundleIdentifier = bundle.bundleIdentifier?.isEmpty == false ? bundle.bundleIdentifier! : "com.no.bundleid"
        
        // Use the game folder name as the app name
        let appName = gameFolderName.capitalizingFirstLetter()

        // Extract marketing version (CFBundleShortVersionString) - no fallback to build number
        let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true
            ? ""
            : bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

        // Extract build number (CFBundleVersion) separately
        let appBuildNumber = bundle.infoDictionary?["CFBundleVersion"] as? String

        // Use the actual bundle path for the icon (proper game icon) instead of launcher
        let appIcon = AppInfoUtils.fetchAppIcon(for: actualBundlePath, wrapped: false)
        let webApp = false
        let system = false // Steam games are never system apps
        let arch = checkAppBundleArchitecture(at: actualBundlePath.path)
        
        // Use the launcher path as the main path (so users see the ~/Applications version)
        // but store the actual bundle path info
        // Get entitlements for the Steam app
        let entitlements = getEntitlements(for: actualBundlePath.path)
        let teamIdentifier = getTeamIdentifier(for: actualBundlePath.path)

        // Steam games: typically no Sparkle or App Store (distributed via Steam)
        let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle.infoDictionary)
        let isAppStore = false  // Steam games are never from App Store
        let adamID: UInt64? = nil  // Steam games don't have App Store adamID
        let autoUpdates: Bool? = nil  // Steam games don't use Homebrew

        return AppInfo(id: UUID(), path: launcherPath, bundleIdentifier: bundleIdentifier, appName: appName,
                       appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon, webApp: webApp, wrapped: false,
                       system: system, arch: arch, cask: nil, steam: true, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:],
                       fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: entitlements, teamIdentifier: teamIdentifier)
    }
}



private func getEntitlements(for appPath: String) -> [String]? {
    return autoreleasepool { () -> [String]? in
        let appURL = URL(fileURLWithPath: appPath) as CFURL
        var staticCode: SecStaticCode?

        // Create a static code object for the app
        guard SecStaticCodeCreateWithPath(appURL, [], &staticCode) == errSecSuccess,
              let code = staticCode else {
            return nil
        }

        // 1 << 2 is the bitmask for entitlements (kSecCSEntitlements)
        var info: CFDictionary?
        if SecCodeCopySigningInformation(code,
                                         SecCSFlags(rawValue: 1 << 2),
                                         &info) == errSecSuccess,
           let dict = info as? [String: Any],
           let entitlements = dict[kSecCodeInfoEntitlementsDict as String] as? [String: Any] {

            var results: [String] = []

            // com.apple.security.application-groups
            if let appGroups = entitlements["com.apple.security.application-groups"] as? [String] {
                results.append(contentsOf: appGroups)
            }

            // com.apple.developer.icloud-container-identifiers
            if let icloudContainers = entitlements["com.apple.developer.icloud-container-identifiers"] as? [String] {
                results.append(contentsOf: icloudContainers)
            }

            // Note: Path-based entitlements (like temporary-exception.files paths) are not extracted
            // as they cause false positives by matching generic folder names like "Desktop", "Documents"

            // Skip generic binary names that could cause false positives
            let excludedNames = ["crashhandler", "crash handler", "electron"]

            // Scan Contents/MacOS folder for binary names
            // App binaries often leave behind files/folders matching their names
            let macosPath = URL(fileURLWithPath: appPath).appendingPathComponent("Contents/MacOS")
            if FileManager.default.fileExists(atPath: macosPath.path) {
                do {
                    let files = try FileManager.default.contentsOfDirectory(atPath: macosPath.path)
                    for file in files where !file.hasPrefix(".") {
                        let fileNameLower = file.lowercased()
                        // Add binary name if not already present, length >= 5, and not excluded
                        if !results.contains(file) && file.count >= 5 && !excludedNames.contains(fileNameLower) {
                            results.append(file)
                        }
                    }
                } catch {
                    // Silently ignore errors (e.g., permission denied, folder doesn't exist)
                }
            }

            // Scan nested bundles one level deep in Contents/*/ for helper apps
            // Example: Contents/SharedSupport/wpscloudsvr.app
            let contentsPath = URL(fileURLWithPath: appPath).appendingPathComponent("Contents")
            if FileManager.default.fileExists(atPath: contentsPath.path) {
                do {
                    let subdirs = try FileManager.default.contentsOfDirectory(at: contentsPath, includingPropertiesForKeys: nil)
                    for subdir in subdirs where subdir.hasDirectoryPath {
                        // Check for .app bundles in this subdirectory
                        if let bundles = try? FileManager.default.contentsOfDirectory(at: subdir, includingPropertiesForKeys: nil) {
                            for bundle in bundles where bundle.pathExtension == "app" {
                                // Add bundle name (without .app extension)
                                let bundleName = bundle.deletingPathExtension().lastPathComponent
                                let bundleNameLower = bundleName.lowercased()
                                if !results.contains(bundleName) && bundleName.count >= 5 && !excludedNames.contains(bundleNameLower) {
                                    results.append(bundleName)
                                }

                                // Scan nested bundle's MacOS folder for binary names
                                let nestedMacOS = bundle.appendingPathComponent("Contents/MacOS")
                                if let binaries = try? FileManager.default.contentsOfDirectory(atPath: nestedMacOS.path) {
                                    for binary in binaries where !binary.hasPrefix(".") && binary.count >= 5 {
                                        let binaryNameLower = binary.lowercased()
                                        if !results.contains(binary) && !excludedNames.contains(binaryNameLower) {
                                            results.append(binary)
                                        }
                                    }
                                }
                            }
                        }
                    }
                } catch {
                    // Silently ignore errors (e.g., permission denied, folder doesn't exist)
                }
            }

            return results.isEmpty ? nil : results
        }

        return nil
    }
}

private func getTeamIdentifier(for appPath: String) -> String? {
    return autoreleasepool {
        let appURL = URL(fileURLWithPath: appPath) as CFURL
        var staticCode: SecStaticCode?

        // Create a static code object for the app
        guard SecStaticCodeCreateWithPath(appURL, [], &staticCode) == errSecSuccess,
              let code = staticCode else {
            return nil
        }

        // Get signing information with team identifier
        var info: CFDictionary?
        if SecCodeCopySigningInformation(code,
                                         SecCSFlags(rawValue: kSecCSSigningInformation),
                                         &info) == errSecSuccess,
           let dict = info as? [String: Any],
           let teamIdentifier = dict[kSecCodeInfoTeamIdentifier as String] as? String {
            return teamIdentifier
        }

        return nil
    }
}




================================================
FILE: Pearcleaner/Logic/AppPathsFetch.swift
================================================
//
//  AppPathsFetch.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 2/6/25.
//

import Foundation
import AppKit
import SwiftUI
import AlinFoundation

extension String {
    /// Strips trailing version numbers and digits from app names
    /// "Bartender 6" → "Bartender"
    /// "Firefox 120.0" → "Firefox"
    func strippingTrailingDigits() -> String {
        return self.replacingOccurrences(
            of: #"\s+\d+(\.\d+)*\s*$"#,
            with: "",
            options: .regularExpression
        ).trimmingCharacters(in: .whitespaces)
    }
}

class AppPathFinder {
    // Shared properties
    private var appInfo: AppInfo
    private var locations: Locations
    private var containerCollection: [URL] = []
    private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
    @AppStorage("settings.general.searchSensitivity") private var sensitivityLevel: SearchSensitivityLevel = .strict
    @AppStorage("settings.general.searchTextContent") private var searchTextContent: Bool = false

    // Optional override sensitivity level for per-app settings
    private var overrideSensitivityLevel: SearchSensitivityLevel?

    // GUI-specific properties (can be nil for CLI)
    private var appState: AppState?
    private var undo: Bool = false
    private var completion: (() -> Void)?

    // Use a Set for fast membership testing
    private var collectionSet: Set<URL> = []

    // Precompiled UUID regex
    private static let uuidRegex: NSRegularExpression = {
        return try! NSRegularExpression(
            pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
            options: .caseInsensitive
        )
    }()

    // Change from lazy var to regular property initialized in init
    private let cachedIdentifiers: (formattedBundleId: String, bundleLastTwoComponents: String, formattedAppName: String, formattedAppNameStripped: String?, appNameLettersOnly: String, pathComponentName: String, useBundleIdentifier: Bool, formattedCompanyName: String?, formattedEntitlements: [String], formattedTeamIdentifier: String?, formattedBaseBundleId: String?)

    // Exclusion list for app file search (computed property to always get current list)
    private var formattedAppExclusionList: [String] {
        return FolderSettingsManager.shared.fileFolderPathsApps.map { $0.pearFormat() }
    }

    // Computed property to get the effective sensitivity level
    private var effectiveSensitivityLevel: SearchSensitivityLevel {
        return overrideSensitivityLevel ?? sensitivityLevel
    }

    // Initializer for both CLI and GUI
    init(appInfo: AppInfo, locations: Locations, appState: AppState? = nil, undo: Bool = false, sensitivityOverride: SearchSensitivityLevel? = nil, completion: (() -> Void)? = nil) {
        self.appInfo = appInfo
        self.locations = locations
        self.appState = appState
        self.undo = undo
        self.overrideSensitivityLevel = sensitivityOverride
        self.completion = completion

        // Initialize cachedIdentifiers eagerly and thread-safely
        let formattedBundleId = appInfo.bundleIdentifier.pearFormat()
        let bundleComponents = appInfo.bundleIdentifier
            .components(separatedBy: ".")
            .compactMap { $0 != "-" ? $0.lowercased() : nil }
        let bundleLastTwoComponents = bundleComponents.suffix(2).joined()
        let formattedAppName = appInfo.appName.pearFormat()
        let appNameLettersOnly = formattedAppName.filter { $0.isLetter }
        let pathComponentName = appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
        let useBundleIdentifier = AppPathFinder.isValidBundleIdentifier(appInfo.bundleIdentifier)

        // Extract company/dev name from 3-component bundle IDs (e.g., "com.knollsoft.Rectangle" -> "knollsoft")
        let formattedCompanyName: String?
        let rawComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
        if rawComponents.count == 3 {
            formattedCompanyName = rawComponents[1].pearFormat()
        } else {
            formattedCompanyName = nil
        }

        // Pre-format entitlements once to avoid repeated formatting in the hot path
        let formattedEntitlements: [String] = appInfo.entitlements?.compactMap { entitlement in
            let formatted = entitlement.pearFormat()
            return formatted.isEmpty ? nil : formatted
        } ?? []

        // Pre-format team identifier once
        let formattedTeamIdentifier = appInfo.teamIdentifier?.pearFormat()

        // Create base bundle ID by stripping common suffixes (for matching launch daemons/agents)
        // e.g., "com.objective-see.blockblock.helper" -> "com.objective-see.blockblock"
        let formattedBaseBundleId: String?
        let commonSuffixes = ["helper", "agent", "daemon", "service", "xpc", "launcher", "updater", "installer", "uninstaller", "login", "extension", "plugin"]
        if rawComponents.count >= 4 {
            let lastComponent = rawComponents.last?.lowercased() ?? ""
            if commonSuffixes.contains(lastComponent) {
                // Remove last component and format
                let baseBundleId = rawComponents.dropLast().joined(separator: ".")
                formattedBaseBundleId = baseBundleId.pearFormat()
            } else {
                formattedBaseBundleId = nil
            }
        } else {
            formattedBaseBundleId = nil
        }

        // Strip trailing digits from app name for Enhanced/Deep mode matching
        // "Bartender 6" → "bartender6" (regular) + "bartender" (stripped)
        let appNameStripped = appInfo.appName.strippingTrailingDigits()
        let formattedAppNameStripped: String? = {
            let stripped = appNameStripped.pearFormat()
            // Only use if different from regular formatted name and not empty
            return (stripped != formattedAppName && !stripped.isEmpty) ? stripped : nil
        }()

        self.cachedIdentifiers = (formattedBundleId, bundleLastTwoComponents, formattedAppName, formattedAppNameStripped, appNameLettersOnly, pathComponentName, useBundleIdentifier, formattedCompanyName, formattedEntitlements, formattedTeamIdentifier, formattedBaseBundleId)
    }

    // Process the initial URL
    private func initialURLProcessing() {
        if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
            let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
            collectionSet.insert(modifiedUrl)
        }
    }

    // Get all container URLs
    private func getAllContainers(bundleURL: URL) -> [URL] {
        var containers: [URL] = []
        let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier

        guard let containerBundleIdentifier = bundleIdentifier else {
            return containers
        }

        if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
            if FileManager.default.fileExists(atPath: groupContainer.path) {
                containers.append(groupContainer)
                Task { @MainActor in
                    GlobalConsoleManager.shared.appendOutput("Found group container: \(groupContainer.lastPathComponent)\n", source: CurrentPage.applications.title)
                }
            }
        }

        if let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers") {
            do {
                let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
                for directory in containerDirectories {
                    let directoryName = directory.lastPathComponent
                    if AppPathFinder.uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
                        let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
                        if let metadataDict = NSDictionary(contentsOf: metadataPlistURL),
                           let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
                            if applicationBundleID == self.appInfo.bundleIdentifier {
                                containers.append(directory)
                                Task { @MainActor in
                                    GlobalConsoleManager.shared.appendOutput("Found app container: \(directoryName)\n", source: CurrentPage.applications.title)
                                }
                            }
                        }
                    }
                }
            } catch {
                printOS("Error accessing Containers directory: \(error)")
            }
        }
        return containers
    }

    // Combined processing for directories and files
    private func processLocation(_ location: String) {
        processLocation(location, currentDepth: 0, maxDepth: 1, isLibraryRootSearch: false)
    }

    private func processLocation(_ location: String, currentDepth: Int, maxDepth: Int, isLibraryRootSearch: Bool = false) {
        if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
            var localResults: [URL] = []
            var subdirectoriesToSearch: [URL] = []

            for scannedItem in contents {
                let scannedItemURL = URL(fileURLWithPath: location).appendingPathComponent(scannedItem)
                let normalizedItemName: String
                if scannedItemURL.hasDirectoryPath || scannedItemURL.pathExtension.isEmpty {
                    // It's a directory or has no extension - don't remove anything
                    normalizedItemName = scannedItem.pearFormat()
                } else {
                    // It's a file with an extension - remove the extension
                    normalizedItemName = (scannedItem as NSString).deletingPathExtension.pearFormat()
                }
                var isDirectory: ObjCBool = false
                if FileManager.default.fileExists(atPath: scannedItemURL.path, isDirectory: &isDirectory) {
                    if shouldSkipItem(normalizedItemName, at: scannedItemURL) { continue }

                    if specificCondition(normalizedItemName: normalizedItemName, scannedItemURL: scannedItemURL) {
                        // Determine what to add: matched item or its parent (for vendor folders)
                        let itemToAdd: URL

                        // For depth=2 matches in Library root searches, check if parent is a vendor folder
                        if isLibraryRootSearch && currentDepth == 2 {
                            let parentURL = scannedItemURL.deletingLastPathComponent()
                            let parentName = parentURL.lastPathComponent

                            // Add parent directory if it's NOT a standard macOS directory
                            // This captures vendor folders like /Library/Objective-See/ instead of /Library/Objective-See/LuLu/
                            if !Locations.standardLibrarySubdirectories.contains(parentName) {
                                itemToAdd = parentURL
                            } else {
                                itemToAdd = scannedItemURL
                            }
                        } else {
                            itemToAdd = scannedItemURL
                        }

                        localResults.append(itemToAdd)
                    }

                    // If this is a directory and we haven't reached max depth, mark for recursive search
                    // Skip directories in the exclusion list when doing Library root deep searches
                    if isDirectory.boolValue && currentDepth < maxDepth {
                        // For Library root searches at depth 0, check if directory should be excluded from deep search
                        if isLibraryRootSearch && currentDepth == 0 {
                            let dirName = scannedItemURL.lastPathComponent
                            if !skipDeepSearch.contains(dirName) {
                                subdirectoriesToSearch.append(scannedItemURL)
                            }
                        } else {
                            subdirectoriesToSearch.append(scannedItemURL)
                        }
                    }
                }
            }

            collectionAccessQueue.sync {
                collectionSet.formUnion(localResults)
            }

            // Recursively search subdirectories if we haven't reached max depth
            if currentDepth < maxDepth {
                for subdirectory in subdirectoriesToSearch {
                    processLocation(subdirectory.path, currentDepth: currentDepth + 1, maxDepth: maxDepth, isLibraryRootSearch: isLibraryRootSearch)
                }
            }
        }
    }

    // Asynchronous collection for GUI usage
    private func collectLocations() {
        let dispatchGroup = DispatchGroup()
        for location in self.locations.apps.paths {
            dispatchGroup.enter()
            DispatchQueue.global(qos: .userInitiated).async {
                // Use depth=2 for Library directories to find files in vendor subdirectories
                // Example: /Library/Objective-See/LuLu, ~/Library/Microsoft/Edge
                let isLibRoot = self.isLibraryDirectory(location)
                let maxDepth = isLibRoot ? 2 : 1
                self.processLocation(location, currentDepth: 0, maxDepth: maxDepth, isLibraryRootSearch: isLibRoot)
                dispatchGroup.leave()
            }
        }
        dispatchGroup.wait()
    }

    // Synchronous collection for CLI usage
    private func collectLocationsCLI() {
        for location in self.locations.apps.paths {
            // Use depth=2 for Library directories to find files in vendor subdirectories
            let isLibRoot = isLibraryDirectory(location)
            let maxDepth = isLibRoot ? 2 : 1
            processLocation(location, currentDepth: 0, maxDepth: maxDepth, isLibraryRootSearch: isLibRoot)
        }
    }

    // Helper to determine if a location is a Library directory
    private func isLibraryDirectory(_ location: String) -> Bool {
        let home = FileManager.default.homeDirectoryForCurrentUser.path
        return location == "\(home)/Library" || location == "/Library"
    }

    // Skip items based on conditions and membership in collectionSet
    private func shouldSkipItem(_ normalizedItemName: String, at scannedItemURL: URL) -> Bool {
        var containsItem = false
        collectionAccessQueue.sync {
            containsItem = self.collectionSet.contains(scannedItemURL)
        }
        if containsItem {
            return true
        }
        for skipCondition in skipConditions {
            // Check path-based exclusions first
            for skipPath in skipCondition.skipPaths {
                if scannedItemURL.path.hasPrefix(skipPath) {
                    return true
                }
            }

            // Check prefix-based exclusions
            if skipCondition.skipPrefix.contains(where: normalizedItemName.hasPrefix) {
                let isAllowed = skipCondition.allowPrefixes.contains(where: normalizedItemName.hasPrefix)
                if !isAllowed {
                    return true
                }
            }
        }
        return false
    }

    // Check if an item meets specific conditions using cached identifiers
    private func specificCondition(normalizedItemName: String, scannedItemURL: URL) -> Bool {
        let cached = self.cachedIdentifiers

        // Special handling for Steam games: also check Desktop for shortcuts
        if scannedItemURL.path.contains("/Desktop/") && scannedItemURL.pathExtension == "app" {
            let desktopAppName = scannedItemURL.deletingPathExtension().lastPathComponent.pearFormat()
            if desktopAppName == cached.formattedAppName || desktopAppName == cached.appNameLettersOnly {
                return true
            }
        }

        // Special handling for Steam game main folder
        if self.appInfo.steam && scannedItemURL.path.contains("/Library/Application Support/Steam/steamapps/common/") {
            let folderName = scannedItemURL.lastPathComponent.pearFormat()
            // Check if this folder matches the game name
            if folderName == cached.formattedAppName || folderName == cached.appNameLettersOnly {
                return true
            }
        }

        // Special handling for Steam game manifest files
        if self.appInfo.steam && scannedItemURL.path.contains("/Library/Application Support/Steam/steamapps/") &&
           scannedItemURL.lastPathComponent.hasPrefix("appmanifest_") && scannedItemURL.pathExtension == "acf" {

            // Extract the game ID from the filename (e.g., "appmanifest_1289310.acf" -> "1289310")
            let filename = scannedItemURL.lastPathComponent
            if let gameIdFromFile = extractGameId(from: filename) {
                // Get the game ID from the Steam launcher's run.sh file
                if let gameIdFromLauncher = getSteamGameId(from: self.appInfo.path) {
                    return gameIdFromFile == gameIdFromLauncher
                }
            }
        }

        // Check entitlements-based matching (using pre-formatted entitlements)
        // Strict: exact match only, Enhanced/Deep: contains match
        for entitlementFormatted in cached.formattedEntitlements {
            let isMatch = effectiveSensitivityLevel == .strict
                ? normalizedItemName == entitlementFormatted
                : normalizedItemName.contains(entitlementFormatted)

            if isMatch {
                return true
            }
        }

        for condition in conditions {
            if cached.useBundleIdentifier && cached.formattedBundleId.contains(condition.bundle_id) {
                if condition.exclude.contains(where: { normalizedItemName.contains($0) }) {
                    return false
                }
                if condition.include.contains(where: { normalizedItemName.contains($0) }) {
                    return true
                }
            }
        }
        if self.appInfo.webApp {
            return normalizedItemName.contains(cached.formattedBundleId)
        }

        let fullBundleMatch = normalizedItemName.contains(cached.formattedBundleId)
        let sensitivity = effectiveSensitivityLevel == .strict

        // Prevent false matches when cached values are empty strings
        let appNameMatch = !cached.formattedAppName.isEmpty && (sensitivity ? normalizedItemName == cached.formattedAppName : normalizedItemName.contains(cached.formattedAppName))
        let pathNameMatch = !cached.pathComponentName.isEmpty && (sensitivity ? normalizedItemName == cached.pathComponentName : normalizedItemName.contains(cached.pathComponentName))
        let appNameLettersMatch = !cached.appNameLettersOnly.isEmpty && (sensitivity ? normalizedItemName == cached.appNameLettersOnly : normalizedItemName.contains(cached.appNameLettersOnly))

        // Bundle ID component matching (Enhanced/Deep levels only)
        let twoComponentMatch: Bool
        if effectiveSensitivityLevel != .strict {
            twoComponentMatch = normalizedItemName.contains(cached.bundleLastTwoComponents)
        } else {
            twoComponentMatch = false
        }

        // Company name matching (Deep level only)
        let companyMatch: Bool
        if effectiveSensitivityLevel == .deep, let company = cached.formattedCompanyName, !company.isEmpty {
            companyMatch = normalizedItemName.contains(company)
        } else {
            companyMatch = false
        }

        // Team identifier matching (Deep level only)
        let teamIdMatch: Bool
        if effectiveSensitivityLevel == .deep, let teamId = cached.formattedTeamIdentifier, !teamId.isEmpty {
            teamIdMatch = normalizedItemName.contains(teamId)
        } else {
            teamIdMatch = false
        }

        // Base bundle ID matching (for apps with .helper/.agent/etc. suffixes)
        // Matches launch daemons/agents that use base bundle ID without suffix
        let baseBundleIdMatch: Bool
        if let baseBundleId = cached.formattedBaseBundleId, !baseBundleId.isEmpty {
            baseBundleIdMatch = normalizedItemName.contains(baseBundleId)
        } else {
            baseBundleIdMatch = false
        }

        // Stripped app name matching (Enhanced/Deep only)
        // Matches files with version-stripped names: "Bartender 6" → also matches "bartender" files
        let strippedAppNameMatch: Bool
        if effectiveSensitivityLevel != .strict, let stripped = cached.formattedAppNameStripped, !stripped.isEmpty {
            strippedAppNameMatch = normalizedItemName.contains(stripped)
        } else {
            strippedAppNameMatch = false
        }

        return (cached.useBundleIdentifier && fullBundleMatch) || (appNameMatch || pathNameMatch || appNameLettersMatch) || twoComponentMatch || companyMatch || teamIdMatch || baseBundleIdMatch || strippedAppNameMatch
    }
    
    // Helper function to extract game ID from manifest filename
    private func extractGameId(from filename: String) -> String? {
        // Extract from "appmanifest_1289310.acf" -> "1289310"
        let components = filename.components(separatedBy: "_")
        if components.count >= 2 {
            let gameIdWithExtension = components[1]
            return gameIdWithExtension.components(separatedBy: ".").first
        }
        return nil
    }
    
    // Helper function to get Steam game ID from the launcher's run.sh file
    private func getSteamGameId(from appPath: URL) -> String? {
        let runShPath = appPath.appendingPathComponent("Contents/MacOS/run.sh")
        
        guard FileManager.default.fileExists(atPath: runShPath.path) else {
            return nil
        }
        
        do {
            let content = try String(contentsOf: runShPath, encoding: .utf8)
            // Look for "steam://run/" pattern and extract the number after it
            if let range = content.range(of: "steam://run/") {
                let afterRun = String(content[range.upperBound...])
                // Extract the number (game ID) which should be at the beginning
                let gameId = afterRun.components(separatedBy: CharacterSet.decimalDigits.inverted).first
                return gameId?.isEmpty == false ? gameId : nil
            }
        } catch {
            printOS("Error reading run.sh file: \(error)")
        }
        
        return nil
    }

    // Check for associated zombie files
    private func fetchAssociatedZombieFiles() -> [URL] {
        let storedFiles = ZombieFileStorage.shared.getAssociatedFiles(for: self.appInfo.path)

        // Only return files that actually exist on disk
        // Keep associations in storage so they can be restored if file comes back
        return storedFiles.filter { FileManager.default.fileExists(atPath: $0.path) }
    }

    // Helper method to check bundle identifier validity - now static
    private static func isValidBundleIdentifier(_ bundleIdentifier: String) -> Bool {
        let components = bundleIdentifier.components(separatedBy: ".")
        if components.count == 1 {
            return bundleIdentifier.count >= 5
        }
        return true
    }

    // Check spotlight index for leftovers missed by manual search
    private func spotlightSupplementalPaths() -> [URL] {
        // Spotlight enabled for all levels with sensitivity-appropriate matching
        // Strict: Exact matches only (via ==[cd] predicate and post-filter)
        // Enhanced/Deep: Contains matches (via CONTAINS[cd] predicate)
        updateOnMain {
            self.appState?.progressStep = 1
        }
        var results: [URL] = []
        let query = NSMetadataQuery()

        let appName = self.appInfo.appName
        let bundleID = self.appInfo.bundleIdentifier

        // Build predicate based on sensitivity level
        switch self.effectiveSensitivityLevel {
        case .strict:
            // Strict: Only exact filename matches
            query.predicate = NSPredicate(format:
                "kMDItemDisplayName ==[cd] %@ OR kMDItemDisplayName ==[cd] %@",
                appName, bundleID)

        case .enhanced:
            // Enhanced: Partial matching in name and path
            query.predicate = NSPredicate(format:
                "kMDItemDisplayName CONTAINS[cd] %@ OR kMDItemPath CONTAINS[cd] %@",
                appName, bundleID)

        case .deep:
            // Deep: Fuzzy search with metadata and AND logic for multi-word names
            var subpredicates: [NSPredicate] = [
                // DisplayName: appName OR bundleID
                NSCompoundPredicate(orPredicateWithSubpredicates: [
                    NSPredicate(format: "kMDItemDisplayName CONTAINS[cd] %@", appName),
                    NSPredicate(format: "kMDItemDisplayName CONTAINS[cd] %@", bundleID)
                ]),
                // Path: appName OR bundleID
                NSCompoundPredicate(orPredicateWithSubpredicates: [
                    NSPredicate(format: "kMDItemPath CONTAINS[cd] %@", appName),
                    NSPredicate(format: "kMDItemPath CONTAINS[cd] %@", bundleID)
                ]),
                // Comment: appName OR bundleID
                NSCompoundPredicate(orPredicateWithSubpredicates: [
                    NSPredicate(format: "kMDItemComment CONTAINS[cd] %@", appName),
                    NSPredicate(format: "kMDItemComment CONTAINS[cd] %@", bundleID)
                ]),
                // Creator: appName OR bundleID
                NSCompoundPredicate(orPredicateWithSubpredicates: [
                    NSPredicate(format: "kMDItemCreator ==[cd] %@", appName),
                    NSPredicate(format: "kMDItemCreator ==[cd] %@", bundleID)
                ]),
                // Copyright: appName OR bundleID (often contains developer/company info)
                NSCompoundPredicate(orPredicateWithSubpredicates: [
                    NSPredicate(format: "kMDItemCopyright CONTAINS[cd] %@", appName),
                    NSPredicate(format: "kMDItemCopyright CONTAINS[cd] %@", bundleID)
                ]),
                // EncodingApplications: appName only (array of apps that processed the file)
                NSPredicate(format: "kMDItemEncodingApplications CONTAINS[cd] %@", appName)
            ]

            // TextContent: appName OR bundleID (optional, controlled by user setting)
            if searchTextContent {
                subpredicates.append(
                    NSCompoundPredicate(orPredicateWithSubpredicates: [
                        NSPredicate(format: "kMDItemTextContent CONTAINS[cd] %@", appName),
                        NSPredicate(format: "kMDItemTextContent CONTAINS[cd] %@", bundleID)
                    ])
                )
            }

            // Add wildcard predicate: ALL name parts must be present (AND logic)
            let nameParts = appName.split(separator: " ")
            if nameParts.count > 1 {
                // For multi-word names: each part must appear in display name OR path
                let partPredicates = nameParts.map { part in
                    NSCompoundPredicate(orPredicateWithSubpredicates: [
                        NSPredicate(format: "kMDItemDisplayName LIKE[cd] %@", "*\(part)*"),
                        NSPredicate(format: "kMDItemPath LIKE[cd] %@", "*\(part)*")
                    ])
                }
                // All parts must be present (AND)
                let allPartsPresent = NSCompoundPredicate(andPredicateWithSubpredicates: partPredicates)
                subpredicates.append(allPartsPresent)
            } else {
                // Single word: just add LIKE for that word
                subpredicates.append(NSPredicate(format: "kMDItemDisplayName LIKE[cd] %@", "*\(appName)*"))
            }

            // Combine all conditions with OR (any match wins)
            query.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: subpredicates)
        }

        query.searchScopes = [NSMetadataQueryUserHomeScope]

        let currentRunLoop = CFRunLoopGetCurrent()

        let finishedNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil) { _ in
            query.disableUpdates()
            query.stop()
            results = query.results.compactMap {
                ($0 as? NSMetadataItem)?.value(forAttribute: kMDItemPath as String)
            }.compactMap {
                URL(fileURLWithPath: $0 as! String)
            }

            // Post-filter: Only for Strict level
            if self.effectiveSensitivityLevel == .strict {
                let nameFormatted = appName.pearFormat()
                let bundleFormatted = bundleID.pearFormat()
                results = results.filter { url in
                    let pathFormatted = url.lastPathComponent.pearFormat()
                    return pathFormatted == nameFormatted || pathFormatted == bundleFormatted
                }
            }

            CFRunLoopStop(currentRunLoop)
        }

        query.start()

        // Timeout after 5 seconds
        DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
            CFRunLoopStop(currentRunLoop)
        }

        CFRunLoopRun()

        // Limit results to prevent excessive post-processing delays
        if results.count > 500 {
            results = Array(results.prefix(500))
        }

        NotificationCenter.default.removeObserver(finishedNotification)
        return results
    }

    // Finalize the collection for GUI usage
    private func finalizeCollection() {
        DispatchQueue.global(qos: .userInitiated).async {
            let outliers = self.handleOutliers()
            let outliersEx = self.handleOutliers(include: false)
            var tempCollection: [URL] = []
            self.collectionAccessQueue.sync {
                tempCollection = Array(self.collectionSet)
            }

            Task { @MainActor in
                GlobalConsoleManager.shared.appendOutput("Found \(tempCollection.count) files from manual search\n", source: CurrentPage.applications.title)
            }

            tempCollection.append(contentsOf: self.containerCollection)
            tempCollection.append(contentsOf: outliers)

            // Insert spotlight results before sorting and filtering
            Task { @MainActor in
                GlobalConsoleManager.shared.appendOutput("Running Spotlight supplemental search...\n", source: CurrentPage.applications.title)
            }
            let spotlightResults = self.spotlightSupplementalPaths()
            let spotlightOnly = spotlightResults.filter { !self.collectionSet.contains($0) }

            if spotlightOnly.count > 0 {
                Task { @MainActor in
                    GlobalConsoleManager.shared.appendOutput("Spotlight found \(spotlightOnly.count) additional files\n", source: CurrentPage.applications.title)
                }
            }

            tempCollection.append(contentsOf: spotlightOnly)

            let excludePaths = outliersEx.map { $0.path }
            tempCollection.removeAll { url in
                excludePaths.contains(url.path)
            }

            // Apply app exclusion filter to ALL discovered files (from all code paths)
            tempCollection.removeAll { fileURL in
                let normalizedPath = fileURL.standardizedFileURL.path.pearFormat()
                return self.formattedAppExclusionList.contains(normalizedPath) ||
                self.formattedAppExclusionList.contains(where: { normalizedPath.contains($0) })
            }

            let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
            var filteredCollection: [URL] = []
            for url in sortedCollection {
                // Remove any existing child paths of the current URL
                filteredCollection.removeAll { $0.path.hasPrefix(url.path + "/") }

                // Only add if it's not already a subpath of an existing item
                if !filteredCollection.contains(where: { url.path.hasPrefix($0.path + "/") }) {
                    filteredCollection.append(url)
                }
            }

            Task { @MainActor in
                GlobalConsoleManager.shared.appendOutput("Calculating file sizes for \(filteredCollection.count) items...\n", source: CurrentPage.applications.title)
            }

            self.handlePostProcessing(sortedCollection: filteredCollection)
        }
    }

    // Finalize the collection for CLI usage
    private func finalizeCollectionCLI() -> Set<URL> {
        let outliers = handleOutliers()
        let outliersEx = handleOutliers(include: false)
        var tempCollection: [URL] = []
        self.collectionAccessQueue.sync {
            tempCollection = Array(self.collectionSet)
        }
        tempCollection.append(contentsOf: self.containerCollection)
        tempCollection.append(contentsOf: outliers)
        // Insert spotlight results before sorting and filtering
        let spotlightResults = self.spotlightSupplementalPaths()
        let spotlightOnly = spotlightResults.filter { !self.collectionSet.contains($0) }
        //        printOS("Spotlight index found: \(spotlightOnly.count)")
        tempCollection.append(contentsOf: spotlightOnly)

        let excludePaths = outliersEx.map { $0.path }
        tempCollection.removeAll { url in
            excludePaths.contains(url.path)
        }

        // Apply app exclusion filter to ALL discovered files (from all code paths)
        tempCollection.removeAll { fileURL in
            let normalizedPath = fileURL.standardizedFileURL.path.pearFormat()
            return formattedAppExclusionList.contains(normalizedPath) ||
                   formattedAppExclusionList.contains(where: { normalizedPath.contains($0) })
        }

        let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
        var filteredCollection: [URL] = []
        var previousUrl: URL?
        for url in sortedCollection {
            if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
                continue
            }
            filteredCollection.append(url)
            previousUrl = url
        }
        if filteredCollection.count == 1, let firstURL = filteredCollection.first, firstURL.path.contains(".Trash") {
            filteredCollection.removeAll()
        }
        return Set(filteredCollection)
    }

    // Handle outlier paths based on conditions
    private func handleOutliers(include: Bool = true) -> [URL] {
        var outliers: [URL] = []
        let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
        let matchingConditions = conditions.filter { condition in
            bundleIdentifier.contains(condition.bundle_id)
        }
        for condition in matchingConditions {
            if include {
                if let forceIncludes = condition.includeForce {
                    for path in forceIncludes {
                        outliers.append(path)
                    }
                }
            } else {
                if let excludeForce = condition.excludeForce {
                    for path in excludeForce {
                        outliers.append(path)
                    }
                }
            }
        }
        return outliers
    }

    // Post-processing: calculate file details, update state, and call completion
    private func handlePostProcessing(sortedCollection: [URL]) {
        // Fetch associated zombie files and add them to the collection
        var tempCollection = sortedCollection
        let associatedFiles = fetchAssociatedZombieFiles()
        for file in associatedFiles {
            if !tempCollection.contains(file) {
                tempCollection.append(file) // Now it's properly included
            }
        }

        var fileSize: [URL: Int64] = [:]
        var fileIcon: [URL: NSImage?] = [:]
        let chunks = createOptimalChunks(from: tempCollection)
        let queue = DispatchQueue(label: "size-calculation", qos: .userInitiated, attributes: .concurrent)
        let group = DispatchGroup()

        for chunk in chunks {
            group.enter()
            queue.async {
                var localFileSize: [URL: Int64] = [:]
                var localFileIcon: [URL: NSImage?] = [:]

                for path in chunk {
                    let size = spotlightSizeForURL(path)
                    localFileSize[path] = size
                    localFileIcon[path] = self.getSmartIcon(for: path)
                }

                // Merge results safely
                DispatchQueue.main.sync {
                    fileSize.merge(localFileSize) { $1 }
                    fileIcon.merge(localFileIcon) { $1 }
                }
                group.leave()
            }
        }
        group.wait()
        let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
        var updatedCollection = tempCollection
        if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
            updatedCollection.removeAll()
        }

        DispatchQueue.main.async {
            self.appInfo.fileSize = fileSize
            self.appInfo.fileIcon = fileIcon
            self.appInfo.arch = arch
            self.appState?.appInfo = self.appInfo
            if !self.undo {
                self.appState?.selectedItems = Set(updatedCollection)
            }
            self.appState?.progressStep = 0
            self.appState?.showProgress = false

            let totalSize = fileSize.values.reduce(0, +)
            let sizeString = ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
            GlobalConsoleManager.shared.appendOutput("✓ Found \(updatedCollection.count) items (\(sizeString))\n", source: CurrentPage.applications.title)

            self.completion?()
        }
    }

    // Public method for GUI
    func findPaths() {
        Task(priority: .background) {
            await GlobalConsoleManager.shared.appendOutput("Searching for files related to \(self.appInfo.appName)...\n", source: CurrentPage.applications.title)

            if self.appInfo.webApp {
                await GlobalConsoleManager.shared.appendOutput("Detected web app, scanning containers...\n", source: CurrentPage.applications.title)
                self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
                self.initialURLProcessing()
                self.finalizeCollection()
            } else {
                await GlobalConsoleManager.shared.appendOutput("Scanning \(self.locations.apps.paths.count) system locations...\n", source: CurrentPage.applications.title)
                self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
                self.initialURLProcessing()
                self.collectLocations()
                self.finalizeCollection()
            }
        }
    }

    // Public method for CLI
    func findPathsCLI() -> Set<URL> {
        if self.appInfo.webApp {
            self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
            self.initialURLProcessing()
            return finalizeCollectionCLI()
        } else {
            self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
            self.initialURLProcessing()
            self.collectLocationsCLI()
            return finalizeCollectionCLI()
        }
    }

    // Custom icon function that handles .app folders intelligently
    private func getSmartIcon(for path: URL) -> NSImage? {
        // For wrapped apps, check if this is the container path
        if self.appInfo.wrapped {
            // Get container path from inner app path
            let containerPath = self.appInfo.path
                .deletingLastPathComponent()  // Remove ActualApp.app
                .deletingLastPathComponent()  // Remove Wrapper -> get Container.app

            // If the current path matches the container, use the app's icon
            if path.absoluteString == containerPath.absoluteString {
                return self.appInfo.appIcon
            }
        }

        // Regular logic for all other files
        if path.pathExtension == "app" {
            let contentsPath = path.appendingPathComponent("Contents")
            var isDirectory: ObjCBool = false

            if FileManager.default.fileExists(atPath: contentsPath.path, isDirectory: &isDirectory) && isDirectory.boolValue {
                // It's a real app bundle, get the app icon
                return getIconForFileOrFolderNS(atPath: path)
            } else {
                // It's just a folder that ends with .app, get the folder icon
                return NSWorkspace.shared.icon(for: .folder)
            }
        } else {
            // For all other files/folders, use the standard function
            return getIconForFileOrFolderNS(atPath: path)
        }
    }
}

// Get size using Spotlight metadata, fallback to manual calculation if needed
private func spotlightSizeForURL(_ url: URL) -> Int64 {
    // Check if this is a directory (not a bundle like .app)
    var isDirectory: ObjCBool = false
    FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)

    // For plain directories, Spotlight metadata is unreliable (reports wrong size)
    // Skip to manual calculation for directories (Containers, regular folders, etc.)
    if isDirectory.boolValue {
        return totalSizeOnDisk(for: url)
    }

    // For files and bundles (.app, .framework), use Spotlight metadata
    let metadataItem = NSMetadataItem(url: url)
    if let logical = metadataItem?.value(forAttribute: "kMDItemLogicalSize") as? Int64 {
        return logical
    }

    // Fallback to manual calculation if Spotlight has no data
    return totalSizeOnDisk(for: url)
}


================================================
FILE: Pearcleaner/Logic/AppState.swift
================================================
//
//  AppState.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 10/31/23.
//

import AlinFoundation
import FinderSync
import Foundation
import SwiftUI

let home = FileManager.default.homeDirectoryForCurrentUser.path

struct AutoSlimStats: Codable {
    var originalSize: Int64
    var currentSize: Int64
    var lastRunVersion: String
}

class AppState: ObservableObject {
    // MARK: - Singleton Instance
    static let shared = AppState()

    @Published var appInfo: AppInfo
    @Published var zombieFile: ZombieFile
    @Published var sortedApps: [AppInfo] = []
    @Published var selectedItems = Set<URL>()
    @Published var currentView = CurrentDetailsView.empty
    @Published var currentPage: CurrentPage  // Initialized from stored preference in init()
    @Published var showAlert: Bool = false
    @Published var showDeleteHistory: Bool = false
    @Published var sidebar: Bool = true
    @Published var showProgress: Bool = false
    @Published var isBrewCleanupInProgress: Bool = false
    @Published var progressStep: Int = 0
    @Published var leftoverProgress: (String, Double) = ("", 0.0)
    @Published var finderExtensionEnabled: Bool = false
    @Published var externalMode: Bool = false
    @Published var multiMode: Bool = false
    @Published var externalPaths: [URL] = []  // for handling multiple app from drops or deeplinks
    @Published var selectedEnvironment: PathEnv?  // for handling dev environments
    @Published var trashError: Bool = false
    @Published var isGridMode: Bool = false

    // Volume information
    @Published var volumeInfos: [VolumeInfo] = []
    @Published var volumeAnimationShown: Bool = false

    // Per-app sensitivity level (session-only, not persisted)
    @Published var perAppSensitivity: [String: SearchSensitivityLevel] = [:]

    func getBundleSize(for appInfo: AppInfo, updateState: @escaping (Int64) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            // Step 1: Check if the size is available and not 0 in the sortedApps cache
            if let existingAppInfo = self.sortedApps.first(where: { $0.path == appInfo.path }) {
                if existingAppInfo.bundleSize > 0 {
                    // Cached size is available, update the state immediately
                    DispatchQueue.main.async {
                        updateState(existingAppInfo.bundleSize)
                    }
                    return
                }
            }

            // Step 2: If we reach here, we need to calculate the size
            let calculatedSize = totalSizeOnDisk(for: appInfo.path)
            DispatchQueue.main.async {
                // Update the state and the array
                updateState(calculatedSize)

                if let index = self.sortedApps.firstIndex(where: { $0.path == appInfo.path }) {
                    var updatedAppInfo = self.sortedApps[index]
                    updatedAppInfo.bundleSize = calculatedSize
                    updatedAppInfo.arch = isOSArm() ? .arm : .intel
                    self.sortedApps[index] = updatedAppInfo
                }
            }
        }
    }

    init() {
        // Initialize currentPage from stored startup view preference
        let storedStartupView = UserDefaults.standard.integer(forKey: "settings.interface.startupView")

        // Load hidden pages
        let hiddenPages = AppState.loadHiddenPages()

        // Validate: If startup page is hidden, default to .applications
        if hiddenPages.contains(storedStartupView) {
            self.currentPage = .applications
            UserDefaults.standard.set(CurrentPage.applications.rawValue, forKey: "settings.interface.startupView")
        } else {
            self.currentPage = CurrentPage(rawValue: storedStartupView) ?? .applications
        }

        self.appInfo = AppInfo(
            id: UUID(),
            path: URL(fileURLWithPath: ""),
            bundleIdentifier: "",
            appName: "",
            appVersion: "",
            appBuildNumber: nil,
            appIcon: nil,
            webApp: false,
            wrapped: false,
            system: false,
            arch: .empty,
            cask: nil,
            steam: false,
            hasSparkle: false,
            isAppStore: false,
            adamID: nil,
            autoUpdates: nil,
            bundleSize: 0,
            fileSize: [:],
            fileIcon: [:],
            creationDate: nil,
            contentChangeDate: nil,
            lastUsedDate: nil,
            dateAdded: nil,
            entitlements: nil,
            teamIdentifier: nil
        )

        self.zombieFile = ZombieFile(
            id: UUID(),
            fileSize: [:],
            fileIcon: [:]
        )

        updateExtensionStatus()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(updateExtensionStatus),
            name: NSApplication.didBecomeActiveNotification,
            object: nil
        )

    }

    @objc func updateExtensionStatus() {
        let extensionStatus = FIFinderSyncController.isExtensionEnabled
        DispatchQueue.main.async {
            self.finderExtensionEnabled = extensionStatus
        }
    }

    // MARK: - Hidden Pages Management

    static func loadHiddenPages() -> Set<Int> {
        guard let data = UserDefaults.standard.data(forKey: "settings.interface.hiddenPages"),
              let decoded = try? JSONDecoder().decode(Set<Int>.self, from: data) else {
            return []
        }
        return decoded
    }

    static func saveHiddenPages(_ pages: Set<Int>) {
        if let encoded = try? JSONEncoder().encode(pages) {
            UserDefaults.standard.set(encoded, forKey: "settings.interface.hiddenPages")
        }
    }

    // Add this method to restore zombie file associations
    func restoreZombieAssociations() {
        let zombieStorage = ZombieFileStorage.shared

        // Clean up invalid associations first
        let validAppPaths = sortedApps.map { $0.path }
        zombieStorage.cleanupInvalidAssociations(validAppPaths: validAppPaths)

        // For each app that has associations, add the zombie file URLs to their fileSize dictionary
        // The actual sizes will be calculated later during the scan
        for appInfo in sortedApps {
            let associatedFiles = zombieStorage.getAssociatedFiles(for: appInfo.path)

            // Add zombie files to this app's file tracking
            // We'll add them with size 0 - the real sizes will be calculated during scan
            for zombieFile in associatedFiles {
                // Only add if the file still exists
                if FileManager.default.fileExists(atPath: zombieFile.path) {
                    // Add to the current appInfo if it matches, or find and update the correct one
                    if let appIndex = sortedApps.firstIndex(where: { $0.path == appInfo.path }) {
                        sortedApps[appIndex].fileSize[zombieFile] = 0  // Placeholder size
                        // Icon will be fetched during normal scan process
                    }
                }
            }
        }
    }

    func loadVolumeInfo() {
        DispatchQueue.global(qos: .userInitiated).async {
            var volumes: [VolumeInfo] = []

            // First, add root volume (/)
            if let rootVolume = self.getVolumeInfo(for: URL(fileURLWithPath: "/")) {
                volumes.append(rootVolume)

                #if DEBUG
                    // Duplicate for testing
                    let duplicateRoot = VolumeInfo(
                        name: "\(rootVolume.name) Debug",
                        path: rootVolume.path,
                        icon: rootVolume.icon,
                        totalSpace: rootVolume.totalSpace,
                        usedSpace: rootVolume.usedSpace,
                        realAvailableSpace: rootVolume.realAvailableSpace,
                        purgeableSpace: rootVolume.purgeableSpace,
                        isExternal: false
                    )
                    volumes.append(duplicateRoot)
                #endif
            }

            // Then enumerate all mounted volumes in /Volumes
            let volumesPath = "/Volumes"
            if let volumeContents = try? FileManager.default.contentsOfDirectory(
                atPath: volumesPath)
            {
                for volumeName in volumeContents {
                    // Skip dot folders (hidden folders like .timemachine)
                    if volumeName.hasPrefix(".") {
                        continue
                    }

                    let volumePath = "\(volumesPath)/\(volumeName)"
                    let volumeURL = URL(fileURLWithPath: volumePath)

                    // Resolve symlinks
                    let resolvedURL = volumeURL.resolvingSymlinksInPath()

                    // Skip if it's the same as root (to avoid duplicates)
                    if resolvedURL.path == "/" {
                        continue
                    }

                    // Skip Time Machine volumes
                    if !self.isTimeMachineVolume(url: resolvedURL) {
                        if let volumeInfo = self.getVolumeInfo(
                            for: resolvedURL, displayName: volumeName)
                        {
                            volumes.append(volumeInfo)
                        }
                    }
                }
            }

            DispatchQueue.main.async {
                // Preserve hasAnimated state from existing volumes
                for i in 0..<volumes.count {
                    if let existingVolume = self.volumeInfos.first(where: {
                        $0.path == volumes[i].path
                    }) {
                        volumes[i].hasAnimated = existingVolume.hasAnimated
                    }
                }
                self.volumeInfos = volumes
            }
        }
    }

    private func getVolumeInfo(for url: URL, displayName: String? = nil) -> VolumeInfo? {
        let keys: [URLResourceKey] = [
            .volumeNameKey,
            .volumeAvailableCapacityKey,
            .volumeAvailableCapacityForImportantUsageKey,
            .volumeTotalCapacityKey,
            .volumeIsRemovableKey,
            .volumeIsEjectableKey,
        ]

        guard let resource = try? url.resourceValues(forKeys: Set(keys)),
            let total = resource.volumeTotalCapacity,
            let availableWithPurgeable = resource.volumeAvailableCapacity,
            let realAvailable = resource.volumeAvailableCapacityForImportantUsage
        else {
            return nil
        }

        // Use regular available capacity if important usage capacity is 0 (common for DMGs)
        let effectiveAvailable = realAvailable > 0 ? Int(realAvailable) : availableWithPurgeable
        let finderTotalAvailable = Int64(effectiveAvailable)
        let realAvailableSpace = Int64(availableWithPurgeable)
        let purgeableSpace = max(0, finderTotalAvailable - realAvailableSpace)
        let realUsedSpace = Int64(total) - finderTotalAvailable
        let name = displayName ?? resource.volumeName ?? url.lastPathComponent
        let icon = NSWorkspace.shared.icon(forFile: url.path)
        icon.size = NSSize(width: 32, height: 32)

        // Debug prints
        //        print("=== Volume: \(name) ===")
        //        print("Total: \(ByteCountFormatter.string(fromByteCount: Int64(total), countStyle: .file))")
        //        print("Available (with purgeable): \(ByteCountFormatter.string(fromByteCount: Int64(availableWithPurgeable), countStyle: .file))")
        //        print("Available (important usage): \(ByteCountFormatter.string(fromByteCount: Int64(realAvailable), countStyle: .file))")
        //        print("Calculated used: \(ByteCountFormatter.string(fromByteCount: realUsedSpace, countStyle: .file))")
        //        print("Calculated purgeable: \(ByteCountFormatter.string(fromByteCount: purgeableSpace, countStyle: .file))")
        //        print("========================")

        // Check if volume is external (removable or ejectable)
        let isRemovable = resource.volumeIsRemovable ?? false
        let isEjectable = resource.volumeIsEjectable ?? false
        let isExternal = isRemovable || isEjectable

        return VolumeInfo(
            name: name,
            path: url.path,
            icon: Image(nsImage: icon),
            totalSpace: Int64(total),
            usedSpace: realUsedSpace,
            realAvailableSpace: realAvailableSpace,
            purgeableSpace: purgeableSpace,
            isExternal: isExternal
        )
    }

    private func isTimeMachineVolume(url: URL) -> Bool {
        let backupsPath = url.appendingPathComponent("Backups.backupdb")
        let tmDirectoryPath = url.appendingPathComponent(".com.apple.timemachine")

        // Check for common Time Machine indicators
        let hasBackupsDB = FileManager.default.fileExists(atPath: backupsPath.path)
        let hasTMDirectory = FileManager.default.fileExists(atPath: tmDirectoryPath.path)

        // Check if volume name contains "TimeMachine"
        let volumeName = url.lastPathComponent.lowercased()
        let isNamedTimeMachine =
            volumeName.contains("timemachine") || volumeName.contains("time machine")
            || volumeName.contains("time_machine")

        return hasBackupsDB || hasTMDirectory || isNamedTimeMachine
    }

}

struct VolumeInfo: Identifiable, Equatable {
    let id = UUID()
    let name: String
    let path: String
    let icon: Image
    let totalSpace: Int64
    let usedSpace: Int64
    let realAvailableSpace: Int64
    let purgeableSpace: Int64
    let isExternal: Bool
    var hasAnimated: Bool = false

    static func == (lhs: VolumeInfo, rhs: VolumeInfo) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name && lhs.path == rhs.path
            && lhs.totalSpace == rhs.totalSpace && lhs.usedSpace == rhs.usedSpace
            && lhs.realAvailableSpace == rhs.realAvailableSpace
            && lhs.purgeableSpace == rhs.purgeableSpace && lhs.isExternal == rhs.isExternal
            && lhs.hasAnimated == rhs.hasAnimated
    }
}

struct AppInfo: Identifiable, Equatable, Hashable {
    let id: UUID
    let path: URL
    let bundleIdentifier: String
    let appName: String
    let appVersion: String
    let appBuildNumber: String?
    let appIcon: NSImage?
    let webApp: Bool
    let wrapped: Bool
    let system: Bool
    var arch: Arch
    let cask: String?
    let steam: Bool  // New property to mark Steam games
    let hasSparkle: Bool  // Has Sparkle framework or Sparkle keys in Info.plist (detected at load time)
    let isAppStore: Bool  // Has App Store receipt or iTunes metadata (detected at load time)
    let adamID: UInt64?  // App Store adamID from mdls metadata (nil if not App Store or not indexed)
    let autoUpdates: Bool?  // Homebrew cask auto_updates flag from cask JSON (nil if not Homebrew or unknown)
    var bundleSize: Int64  // Only used in the app list view
    var lipoSavings: Int64?  // Cached lipo savings (nil=not calculated, 0=no savings, >0=savings available)
    var fileSize: [URL: Int64]  // Logical file sizes (matches Finder)
    var fileIcon: [URL: NSImage?]
    let creationDate: Date?
    let contentChangeDate: Date?
    let lastUsedDate: Date?
    let dateAdded: Date?
    let entitlements: [String]?
    let teamIdentifier: String?

    var totalSize: Int64 {
        return fileSize.values.reduce(0, +)
    }

    var brew: Bool {
        return cask != nil
    }

    var executableURL: URL? {
        let infoPlistURL = path.appendingPathComponent("Contents/Info.plist")
        guard let info = NSDictionary(contentsOf: infoPlistURL) as? [String: Any],
            let execName = info["CFBundleExecutable"] as? String
        else {
            return nil
        }
        return path.appendingPathComponent("Contents/MacOS").appendingPathComponent(execName)
    }

    var averageColor: Color? {
        Color(appIcon?.averageColor ?? .clear)
    }

    var isEmpty: Bool {
        return path == URL(fileURLWithPath: "./") && bundleIdentifier.isEmpty && appName.isEmpty
    }

    static let empty = AppInfo(
        id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "",
        appVersion: "", appBuildNumber: nil, appIcon: nil, webApp: false, wrapped: false, system: false, arch: .empty,
        cask: nil, steam: false, hasSparkle: false, isAppStore: false, adamID: nil, autoUpdates: nil, bundleSize: 0, lipoSavings: 0, fileSize: [:], fileIcon: [:],
        creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: nil, teamIdentifier: nil)

}

// MARK: - AppInfoMini (Phase 1 Fast Loading)

/// Lightweight version of AppInfo for fast initial app list display
/// Contains only essential properties needed for list rendering and sorting
/// Converts to full AppInfo with placeholder values for deferred properties
struct AppInfoMini {
    let id: UUID
    let path: URL
    let bundleIdentifier: String
    let appName: String
    let appVersion: String
    let appIcon: NSImage?
    let system: Bool
    let bundleSize: Int64           // Always calculated (mdls or totalSizeOnDisk)
    let creationDate: Date?
    let contentChangeDate: Date?
    let lastUsedDate: Date?
    let dateAdded: Date?

    /// Convert AppInfoMini to full AppInfo with placeholder values for expensive properties
    /// Phase 2 will populate these expensive properties in background
    func toAppInfo() -> AppInfo {
        return AppInfo(
            id: self.id,
            path: self.path,
            bundleIdentifier: self.bundleIdentifier,
            appName: self.appName,
            appVersion: self.appVersion,
            appBuildNumber: nil,                // Phase 2
            appIcon: self.appIcon,
            webApp: false,                      // Phase 2
            wrapped: false,                     // Phase 2
            system: self.system,
            arch: .empty,                       // Phase 2 (expensive)
            cask: nil,                          // Phase 2 (expensive)
            steam: false,                       // Phase 2
            hasSparkle: false,                  // Phase 2 (expensive)
            isAppStore: false,                  // Phase 2 (expensive)
            adamID: nil,                        // Phase 2
            autoUpdates: nil,                   // Phase 2
            bundleSize: self.bundleSize,        // ✅ Already calculated
            lipoSavings: nil,                   // Phase 2
            fileSize: [:],                      // Populated when user selects app
            fileIcon: [:],                      // Populated when user selects app
            creationDate: self.creationDate,
            contentChangeDate: self.contentChangeDate,
            lastUsedDate: self.lastUsedDate,
            dateAdded: self.dateAdded,
            entitlements: nil,                  // Phase 2 (expensive)
            teamIdentifier: nil                 // Phase 2 (expensive)
        )
    }
}

extension AppInfo {
    /// Convert full AppInfo to lightweight AppInfoMini (for fallback case when no mdls metadata)
    func toMini() -> AppInfoMini {
        return AppInfoMini(
            id: self.id,
            path: self.path,
            bundleIdentifier: self.bundleIdentifier,
            appName: self.appName,
            appVersion: self.appVersion,
            appIcon: self.appIcon,
            system: self.system,
            bundleSize: self.bundleSize,
            creationDate: self.creationDate,
            contentChangeDate: self.contentChangeDate,
            lastUsedDate: self.lastUsedDate,
            dateAdded: self.dateAdded
        )
    }

    /// Generate debug string that excludes NSImage properties for cleaner output
    func getDebugString() -> String {
        // Format file paths with sizes
        let filePathsFormatted = fileSize
            .sorted { $0.key.path < $1.key.path }
            .map { "  • \($0.key.path) (\(formatBytes($0.value)))" }
            .joined(separator: "\n")

        return """
        ====================================
        AppInfo Debug Output
        ====================================
        ID: \(id)
        App Name: \(appName)
        Bundle ID: \(bundleIdentifier)
        Path: \(path.path)
        Version: \(appVersion)
        Build Number: \(appBuildNumber ?? "nil")
        ====================================
        Architecture: \(arch)
        Web App: \(webApp)
        Wrapped: \(wrapped)
        System App: \(system)
        Steam Game: \(steam)
        Cask: \(cask ?? "nil")
          Auto Updates: \(autoUpdates ?? false)
        ====================================
        Bundle Size: \(formatBytes(bundleSize))
        Total Size: \(formatBytes(totalSize))
        Lipo Savings: \(lipoSavings.map { formatBytes($0) } ?? "nil")
        ====================================
        Creation Date: \(creationDate?.description ?? "nil")
        Content Change: \(contentChangeDate?.description ?? "nil")
        Last Used: \(lastUsedDate?.description ?? "nil")
        Date Added: \(dateAdded?.description ?? "nil")
        ====================================
        Entitlements/Binaries:
        \(entitlements?.joined(separator: "\n") ?? "nil")
        ====================================
        Team ID: \(teamIdentifier ?? "nil")
        ====================================
        Update Sources:
          App Store: \(isAppStore)
          Homebrew: \(brew)
          Sparkle: \(hasSparkle)
        ====================================
        Files (\(fileSize.count)):
        \(filePathsFormatted.isEmpty ? "  (none)" : filePathsFormatted)
        ====================================
        """
    }
}

// MARK: - Fuzzy Search
extension AppInfo: FuzzySearchable {
    var searchableString: String {
        return appName
    }
}

struct ZombieFile: Identifiable, Equatable, Hashable {
    let id: UUID
    var fileSize: [URL: Int64]  // Logical file sizes (matches Finder)
    var fileIcon: [URL: NSImage?]
    var totalSize: Int64 {
        return fileSize.values.reduce(0, +)
    }

    static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])

}

struct AssociatedZombieFile: Codable {
    let appPath: URL  // Unique identifier for the app
    let filePath: URL  // The zombie file to be processed
}

class ZombieFileStorage {
    static let shared = ZombieFileStorage()
    var associatedFiles: [URL: [URL]] = [:]  // Key: App Path, Value: Zombie File URLs

    // UserDefaults key for persistence
    private let associationsKey = "settings.general.zombie.associations"

    private init() {
        loadAssociations()
    }

    // Load associations from UserDefaults
    private func loadAssociations() {
        if let data = UserDefaults.standard.data(forKey: associationsKey),
            let storedAssociations = try? JSONDecoder().decode([String: [String]].self, from: data)
        {

            associatedFiles = storedAssociations.reduce(into: [URL: [URL]]()) { result, pair in
                let appURL = URL(fileURLWithPath: pair.key)
                let zombieURLs = pair.value.map { URL(fileURLWithPath: $0) }
                result[appURL] = zombieURLs
            }
        }
    }

    // Save associations to UserDefaults
    private func saveAssociations() {
        let storableAssociations = associatedFiles.reduce(into: [String: [String]]()) {
            result, pair in
            result[pair.key.path] = pair.value.map { $0.path }
        }

        if let encoded = try? JSONEncoder().encode(storableAssociations) {
            UserDefaults.standard.set(encoded, forKey: associationsKey)
        }
    }

    func addAssociation(appPath: URL, zombieFilePath: URL) {
        if associatedFiles[appPath] == nil {
            associatedFiles[appPath] = []
        }
        if !associatedFiles[appPath]!.contains(zombieFilePath) {
            associatedFiles[appPath]?.append(zombieFilePath)
            saveAssociations()
        }
    }

    func getAssociatedFiles(for appPath: URL) -> [URL] {
        return associatedFiles[appPath] ?? []
    }

    func isPathAssociated(_ path: URL) -> Bool {
        return associatedFiles.values.contains { $0.contains(path) }
    }

    func clearAssociations(for appPath: URL) {
        associatedFiles[appPath] = nil
        saveAssociations()
    }

    func removeAssociation(appPath: URL, zombieFilePath: URL) {
        guard var associatedFilesList = associatedFiles[appPath] else { return }
        associatedFilesList.removeAll { $0 == zombieFilePath }

        if associatedFilesList.isEmpty {
            associatedFiles.removeValue(forKey: appPath)  // Remove key if no files are left
        } else {
            associatedFiles[appPath] = associatedFilesList
        }
        saveAssociations()
    }

    // Clean up associations for apps that no longer exist
    func cleanupInvalidAssociations(validAppPaths: [URL]) {
        let currentAppPaths = Set(associatedFiles.keys)
        let validAppPathsSet = Set(validAppPaths)
        let invalidPaths = currentAppPaths.subtracting(validAppPathsSet)

        for invalidPath in invalidPaths {
            associatedFiles.removeValue(forKey: invalidPath)
        }

        if !invalidPaths.isEmpty {
            saveAssociations()
        }
    }
}

enum Arch {
    case arm
    case intel
    case universal
    case empty

    var type: String {
        switch self {
        case .arm:
            return "arm"
        case .intel:
            return "intel"
        case .universal:
            return String(localized: "universal")
        case .empty:
            return ""
        }
    }
}

enum CurrentPage: Int, CaseIterable, Identifiable {
    case applications
    case development
    case fileSearch
    case homebrew
    case lipo
    case orphans
    case packages
    case plugins
    case services
    case updater

    var id: Int { rawValue }

    /// Pages that are hidden in release builds (only visible in DEBUG mode)
    static var debugOnlyPages: [CurrentPage] {
        return []
    }

    /// Returns all pages filtered based on build configuration and user visibility settings
    static var availablePages: [CurrentPage] {
        let hiddenPages = AppState.loadHiddenPages()

        #if DEBUG
        return CurrentPage.allCases.filter { !hiddenPages.contains($0.rawValue) }
        #else
        return CurrentPage.allCases
            .filter { !debugOnlyPages.contains($0) }
            .filter { !hiddenPages.contains($0.rawValue) }
        #endif
    }

    var details: (title: String, icon: String) {
        switch self {
        case .applications:
            return (String(localized: "Apps"), "macwindow")
        case .development:
            return (String(localized: "Developer"), "hammer.circle")
        case .fileSearch:
            return (String(localized: "File Search"), "magnifyingglass")
        case .homebrew:
            return (String(localized: "Homebrew"), "mug")
        case .lipo:
            return (String(localized: "Lipo"), "scissors")
        case .orphans:
            return (String(localized: "Orphans"), "doc.text.magnifyingglass")
        case .packages:
            return (String(localized: "Packages"), "shippingbox")
        case .plugins:
            return (String(localized: "Plugins"), "puzzlepiece")
        case .services:
            return (String(localized: "Services"), "gearshape.2")
        case .updater:
            return (String(localized: "Updater"), "arrow.down.circle")
        }
    }

    var title: String { details.title }
    var icon: String { details.icon }
}

//MARK: Sorting for sidebar apps list
enum SortOption: Int, CaseIterable, Identifiable {
    case alphabetical
    case size
    case creationDate
    case dateAdded
    case contentChangeDate
    case lastUsedDate

    var id: Int { rawValue }

    var title: String {
        let titles: [String] = [
            String(localized: "App Name"),
            String(localized: "App Size"),
            String(localized: "Date Created"),
            String(localized: "Date Added"),
            String(localized: "Modified Date"),
            String(localized: "Last Used Date"),
        ]
        return titles[rawValue]
    }
}

//MARK: Sorting for file list view
enum SortOptionList: String, CaseIterable {
    case name = "name"
    case path = "path"
    case size = "size"

    var title: String {
        switch self {
        case .name: return String(localized: "Name")
        case .path: return String(localized: "Path")
        case .size: return String(localized: "Size")
        }
    }

    var systemImage: String {
        switch self {
        case .name: return "textformat"
        case .path: return "folder"
        case .size: return "number"
        }
    }
}

enum CurrentTabView: Int, CaseIterable {
    case general
    case interface
    case folders
    case update
    case helper
    case about

    var title: String {
        switch self {
        case .general: return String(localized: "General")
        case .interface: return String(localized: "Interface")
        case .folders: return String(localized: "Folders")
        case .update: return String(localized: "Update")
        case .helper: return String(localized: "Helper")
        case .about: return String(localized: "About")
        }
    }
}

enum CurrentDetailsView: Int {
    case empty
    case files
//    case zombie
}

extension AppState {
    // Call this when switching to view an app to ensure associated files are loaded
    func loadAssociatedFilesForCurrentApp() {
        guard !appInfo.isEmpty else { return }

        let associatedFiles = ZombieFileStorage.shared.getAssociatedFiles(for: appInfo.path)

        for zombieFile in associatedFiles {
            if FileManager.default.fileExists(atPath: zombieFile.path) {
                // Add to current app's tracking if not already present
                if appInfo.fileSize[zombieFile] == nil {
                    appInfo.fileSize[zombieFile] = 0  // Will be calculated during scan
                }
            }
        }
    }
}


================================================
FILE: Pearcleaner/Logic/AppsUpdater/AppStoreReset.swift
================================================
//
//  AppStoreReset.swift
//  Pearcleaner
//
//  Based on mas-cli's reset command
//  Copyright © 2018 mas-cli. All rights reserved.
//  License: MIT
//

import Foundation
import AppKit
import StoreFoundation
import CommerceKit

/// Manages App Store service reset operations
/// Based on mas-cli's Reset.swift implementation
/// Use this when App Store downloads are stuck or broken
class AppStoreReset {

    /// Result of a reset operation
    enum ResetResult {
        case success
        case failure(String)
    }

    /// Resets the App Store by:
    /// 1. Terminating Dock and storeuid processes (handles App Store UI)
    /// 2. Killing Mac App Store system daemons
    /// 3. Deleting download cache directory
    ///
    /// - Returns: ResetResult indicating success or failure with error message
    static func reset() async -> ResetResult {
        // Step 1: Terminate App Store UI processes
        let terminatedApps = terminateAppStoreApps()

        // Step 2: Kill system daemons
        let killedDaemons = await killAppStoreDaemons()

        // Step 3: Delete download cache
        let deletedCache = deleteDownloadCache()

        // Check if all steps succeeded
        if terminatedApps && killedDaemons && deletedCache {
            return .success
        } else {
            var errors: [String] = []
            if !terminatedApps { errors.append("Failed to terminate App Store apps") }
            if !killedDaemons { errors.append("Failed to kill system daemons") }
            if !deletedCache { errors.append("Failed to delete download cache") }
            return .failure(errors.joined(separator: ", "))
        }
    }

    // MARK: - Private Implementation

    /// Terminates Dock and storeuid processes (handles App Store UI)
    private static func terminateAppStoreApps() -> Bool {
        let appNames = ["Dock", "storeuid"]
        var allTerminated = true

        for appName in appNames {
            // Find running instance
            guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.\(appName)").first else {
                // App not running, skip (not an error)
                continue
            }

            // Terminate app
            if !app.terminate() {
                allTerminated = false
            }
        }

        return allTerminated
    }

    /// Kills Mac App Store system daemons using sysctl + proc_pidpath
    /// This is the low-level approach mas-cli uses (not using NSRunningApplication)
    private static func killAppStoreDaemons() async -> Bool {
        let daemonPaths = [
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storekitagent",
            "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoreagent",
            "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstored",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storekitagent",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storekitagent",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeaccountd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storeaccountd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storeaccountd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeassetd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storeassetd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storeassetd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storedownloadd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storedownloadd",
            "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storedownloadd",
            "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Versions/A/Support/appstorecomponentsd",
            "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstorecomponentsd"
        ]

        var allKilled = true

        for daemonPath in daemonPaths {
            // Find PID for daemon path using sysctl
            if let pid = findPID(forExecutablePath: daemonPath) {
                // Kill process
                let result = kill(pid, SIGTERM)
                if result != 0 {
                    allKilled = false
                }
            }
            // If daemon not running, that's fine (not an error)
        }

        return allKilled
    }

    /// Finds the PID for a given executable path using sysctl
    /// This is how mas-cli does it (low-level process enumeration)
    private static func findPID(forExecutablePath targetPath: String) -> pid_t? {
        // Get all process PIDs using sysctl
        var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]
        var length: size_t = 0

        // Get required buffer size
        if sysctl(&name, u_int(name.count), nil, &length, nil, 0) == -1 {
            return nil
        }

        // Allocate buffer
        let count = length / MemoryLayout<kinfo_proc>.stride
        var procs = [kinfo_proc](repeating: kinfo_proc(), count: count)

        // Get process list
        if sysctl(&name, u_int(name.count), &procs, &length, nil, 0) == -1 {
            return nil
        }

        // Iterate through processes
        let actualCount = length / MemoryLayout<kinfo_proc>.stride
        for i in 0..<actualCount {
            let pid = procs[i].kp_proc.p_pid

            // Get executable path for this PID using proc_pidpath
            var pathBuffer = [Int8](repeating: 0, count: Int(MAXPATHLEN))
            let pathLength = proc_pidpath(pid, &pathBuffer, UInt32(MAXPATHLEN))

            if pathLength > 0 {
                let executablePath = String(cString: pathBuffer)
                if executablePath == targetPath {
                    return pid
                }
            }
        }

        return nil
    }

    /// Deletes the App Store download cache using CommerceKit's CKDownloadDirectory()
    private static func deleteDownloadCache() -> Bool {
        // Get download directory from CommerceKit (mas-cli approach)
        let downloadDirPath = CKDownloadDirectory(nil)
        let fileManager = FileManager.default

        // Check if directory exists
        guard fileManager.fileExists(atPath: downloadDirPath) else {
            // Directory doesn't exist, nothing to delete (not an error)
            return true
        }

        // Delete directory
        do {
            try fileManager.removeItem(atPath: downloadDirPath)
            return true
        } catch {
            return false
        }
    }
}


================================================
FILE: Pearcleaner/Logic/AppsUpdater/AppStoreUpdateChecker.swift
================================================
//
//  AppStoreUpdateChecker.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 10/13/25.
//

import Foundation
import CommerceKit
import StoreFoundation
import AlinFoundation

class AppStoreUpdateChecker {
    private static let logger = UpdaterDebugLogger.shared

    /// Fallback regions to check if app not found in primary region
    /// Ordered by usage: CN, US, HK, JP, KR, SG
    private static let fallbackRegions = ["CN", "US", "HK", "JP", "KR", "SG"]

    /// Check if an app is a wrapped iPad/iOS app
    /// app.wrapped is already set correctly during app launch
    private static func isIOSApp(_ app: AppInfo) -> Bool {
        return app.wrapped
    }

    static func checkForUpdates(apps: [AppInfo]) async -> [UpdateableApp] {
        guard !apps.isEmpty else { return [] }

        logger.log(.appStore, "Starting App Store update check for \(apps.count) apps")
        await GlobalConsoleManager.shared.appendOutput("Checking for App Store updates (\(apps.count) apps)...\n", source: CurrentPage.updater.title)

        // Create optimal chunks based on CPU cores (smaller chunks for App Store API calls)
        let chunks = createOptimalChunks(from: apps, minChunkSize: 3, maxChunkSize: 10)

        // Process chunks concurrently using TaskGroup
        return await withTaskGroup(of: [UpdateableApp].self) { group in
            for chunk in chunks {
                group.addTask {
                    await checkChunk(chunk: chunk)
                }
            }

            // Collect results from all chunks
            var allUpdates: [UpdateableApp] = []
            for await chunkUpdates in group {
                // Check for cancellation between chunks
                if Task.isCancelled {
                    break
                }
                allUpdates.append(contentsOf: chunkUpdates)
            }

            logger.log(.appStore, "Found \(allUpdates.count) available App Store updates")
            await GlobalConsoleManager.shared.appendOutput("Found \(allUpdates.count) App Store update(s)\n", source: CurrentPage.updater.title)
            return allUpdates
        }
    }

    /// Check a chunk of apps for updates concurrently
    private static func checkChunk(chunk: [AppInfo]) async -> [UpdateableApp] {
        await withTaskGroup(of: UpdateableApp?.self) { group in
            for app in chunk {
                group.addTask {
                    await checkSingleApp(app: app)
                }
            }

            // Collect non-nil results
            var updates: [UpdateableApp] = []
            for await update in group {
                if let update = update {
                    updates.append(update)
                }
            }

            return updates
        }
    }

    /// Check a single app for updates
    private static func checkSingleApp(app: AppInfo) async -> UpdateableApp? {
        logger.log(.appStore, "Checking: \(app.appName) (\(app.bundleIdentifier))")

        // OPTIMIZATION: Try direct adamID lookup first if available (much faster than bundleID search)
        let result: (AppStoreInfo, String)?
        let isWrappedApp = isIOSApp(app)

        if let adamID = app.adamID {
            logger.log(.appStore, "  🚀 Fast path - using cached adamID: \(adamID)")
            let primaryRegion = await getAppStoreRegion()

            // Use entity filtering based on app type (prevents iOS version pollution for Mac apps)
            let entity = isWrappedApp ? "macSoftware" : "desktopSoftware"

            if let appStoreInfo = await fetchAppStoreInfoByAdamID(adamID: adamID, region: primaryRegion, entity: entity) {
                result = (appStoreInfo, primaryRegion)
                logger.log(.appStore, "  ✅ Found via adamID lookup")
            } else {
                // Fallback to bundleID search if adamID lookup fails (app might have been transferred)
                logger.log(.appStore, "  ⚠️ adamID lookup failed, falling back to bundleID search")
                result = await getAppStoreInfo(bundleID: app.bundleIdentifier, isWrappedIOSApp: isWrappedApp)
            }
        } else {
            // No cached adamID - use standard bundleID search
            logger.log(.appStore, "  📍 Standard path - using bundleID lookup")
            result = await getAppStoreInfo(bundleID: app.bundleIdentifier, isWrappedIOSApp: isWrappedApp)
        }

        guard let (appStoreInfo, foundRegion) = result else {
            logger.log(.appStore, "  ❌ API lookup failed - not found in App Store")
            return nil
        }

        logger.log(.appStore, "  ✅ Found in App Store: v\(appStoreInfo.version) (adamID: \(appStoreInfo.adamID)) in region: \(foundRegion)")

        // Use Version for robust comparison (handles 1, 2, 3+ component versions)
        let installedVer = Version(versionNumber: app.appVersion, buildNumber: nil)
        let availableVer = Version(versionNumber: appStoreInfo.version, buildNumber: nil)

        // Skip if versions are empty/invalid
        guard !installedVer.isEmpty && !availableVer.isEmpty else {
            logger.log(.appStore, "  ⚠️ Skipped - empty/invalid version (Installed: \(app.appVersion), Available: \(appStoreInfo.version))")
            return nil
        }

        logger.log(.appStore, "  Comparing versions - Installed: \(app.appVersion), Available: \(appStoreInfo.version)")

        // Detect if this is a wrapped iOS app
        let isIOSApp = Self.isIOSApp(app)

        // Only add if App Store version is GREATER than installed version
        if availableVer > installedVer {
            logger.log(.appStore, "  📦 UPDATE AVAILABLE: \(app.appVersion) → \(appStoreInfo.version)\(isIOSApp ? " (iOS app)" : "")")
            return UpdateableApp(
                appInfo: app,
                availableVersion: appStoreInfo.version,
                availableBuildNumber: nil,  // App Store doesn't provide separate build numbers
                source: .appStore,
                adamID: appStoreInfo.adamID,
                appStoreURL: appStoreInfo.appStoreURL,
                status: .idle,
                progress: 0.0,
                isSelectedForUpdate: true,
                releaseTitle: nil,
                releaseDescription: appStoreInfo.releaseNotes,
                releaseNotesLink: nil,
                releaseDate: appStoreInfo.releaseDate,
                isPreRelease: false,  // App Store updates are not pre-releases
                isIOSApp: isIOSApp,
                foundInRegion: foundRegion,
                fetchedReleaseNotes: nil,  // App Store has inline release notes
                appcastItem: nil  // App Store doesn't use Sparkle
            )
        }

        logger.log(.appStore, "  ✓ Up to date")
        return nil
    }

    private struct AppStoreInfo {
        let adamID: UInt64
        let version: String
        let appStoreURL: String
        let releaseNotes: String?
        let releaseDate: String?
    }

    private static func getAppStoreInfo(bundleID: String, isWrappedIOSApp: Bool) async -> (info: AppStoreInfo, region: String)? {
        // Get user's primary region
        let primaryRegion = await getAppStoreRegion()
        logger.log(.appStore, "    Primary region: \(primaryRegion)")

        // Try primary region first with all entity types
        if let info = await tryAllEntities(bundleID: bundleID, region: primaryRegion, isWrappedIOSApp: isWrappedIOSApp) {
            return (info, primaryRegion)
        }

        // If not found, try fallback regions
        logger.log(.appStore, "    Not found in primary region, trying fallback regions...")
        for region in fallbackRegions where region != primaryRegion {
            logger.log(.appStore, "    Trying region: \(region)")
            if let info = await tryAllEntities(bundleID: bundleID, region: region, isWrappedIOSApp: isWrappedIOSApp) {
                logger.log(.appStore, "    ✓ Found in region: \(region)")
                return (info, region)
            }
        }

        logger.log(.appStore, "    ❌ Not found in any region")
        return nil
    }

    /// Try all entity types for a given region based on app type
    private static func tryAllEntities(bundleID: String, region: String, isWrappedIOSApp: Bool) async -> AppStoreInfo? {
        if isWrappedIOSApp {
            // Wrapped iOS apps: Only try macSoftware (covers "Designed for iPad" apps)
            logger.log(.appStore, "      Trying entity: macSoftware (iOS app)")
            if let info = await fetchAppStoreInfo(bundleID: bundleID, region: region, entity: "macSoftware") {
                logger.log(.appStore, "      ✓ Found with macSoftware")
                return info
            }
        } else {
            // Regular Mac apps: Only try desktopSoftware (prevents iOS version pollution)
            logger.log(.appStore, "      Trying entity: desktopSoftware (Mac app)")
            if let info = await fetchAppStoreInfo(bundleID: bundleID, region: region, entity: "desktopSoftware") {
                logger.log(.appStore, "      ✓ Found with desktopSoftware")
                return info
            }
        }

        return nil
    }

    private static func fetchAppStoreInfo(bundleID: String, region: String, entity: String?) async -> AppStoreInfo? {
        // Query iTunes Search API using bundle ID
        guard let endpoint = URL(string: "https://itunes.apple.com/lookup") else {
            return nil
        }

        var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)

        var queryItems = [
            URLQueryItem(name: "bundleId", value: bundleID),
            URLQueryItem(name: "country", value: region),
            URLQueryItem(name: "limit", value: "1")
        ]

        // Add entity parameter if provided (desktopSoftware or macSoftware)
        if let entity = entity {
            queryItems.append(URLQueryItem(name: "entity", value: entity))
        }

        components?.queryItems = queryItems

        guard let url = components?.url else {
            return nil
        }

        do {
            let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
            let (data, _) = try await URLSession.shared.data(for: request)

            if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
               let resultCount = json["resultCount"] as? Int,
               resultCount > 0,
               let results = json["results"] as? [[String: Any]],
               let firstResult = results.first,
               let trackId = firstResult["trackId"] as? UInt64,
               let version = firstResult["version"] as? String,
               let trackViewUrl = firstResult["trackViewUrl"] as? String {

                // Extract optional metadata
                let releaseNotes = firstResult["releaseNotes"] as? String
                let releaseDate = firstResult["currentVersionReleaseDate"] as? String

                return AppStoreInfo(
                    adamID: trackId,
                    version: version,
                    appStoreURL: trackViewUrl,
                    releaseNotes: releaseNotes,
                    releaseDate: releaseDate
                )
            }
        } catch {
            // Error querying iTunes API - silently fail
        }

        return nil
    }

    /// Fetch App Store info using adamID (faster than bundleID lookup)
    /// Uses direct adamID lookup, avoiding multi-region/entity fallback overhead
    private static func fetchAppStoreInfoByAdamID(adamID: UInt64, region: String, entity: String?) async -> AppStoreInfo? {
        // Query iTunes Search API using adamID
        guard let endpoint = URL(string: "https://itunes.apple.com/lookup") else {
            return nil
        }

        var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)

        var queryItems = [
            URLQueryItem(name: "id", value: String(adamID)),
            URLQueryItem(name: "country", value: region),
            URLQueryItem(name: "limit", value: "1")
        ]

        // Add entity parameter if provided (desktopSoftware for Mac apps, macSoftware for iOS apps)
        if let entity = entity {
            queryItems.append(URLQueryItem(name: "entity", value: entity))
        }

        components?.queryItems = queryItems

        guard let url = components?.url else {
            return nil
        }

        do {
            let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
            let (data, _) = try await URLSession.shared.data(for: request)

            if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
               let resultCount = json["resultCount"] as? Int,
               resultCount > 0,
               let results = json["results"] as? [[String: Any]],
               let firstResult = results.first,
               let trackId = firstResult["trackId"] as? UInt64,
               let version = firstResult["version"] as? String,
               let trackViewUrl = firstResult["trackViewUrl"] as? String {

                // Extract optional metadata
                let releaseNotes = firstResult["releaseNotes"] as? String
                let releaseDate = firstResult["currentVersionReleaseDate"] as? String

                return AppStoreInfo(
                    adamID: trackId,
                    version: version,
                    appStoreURL: trackViewUrl,
                    releaseNotes: releaseNotes,
                    releaseDate: releaseDate
                )
            }
        } catch {
            // Error querying iTunes API - silently fail
        }

        return nil
    }

    // MARK: - Private Helpers

    /// Get the user's App Store region (2-letter ISO 3166-1 alpha-2 code)
    /// Locale.region.identifier already returns alpha-2 codes (e.g., "US", "GB", "FR")
    private static func getAppStoreRegion() async -> String {
        return Locale.autoupdatingCurrent.region?.identifier ?? "US"
    }
}


================================================
FILE: Pearcleaner/Logic/AppsUpdater/AppStoreUpdater.swift
================================================
//
//  AppStoreUpdater.swift
//  Pearcleaner
//
//  Created by Alin Lupascu on 10/13/25.
//

import Foundation
import CommerceKit
import StoreFoundation
import AlinFoundation

// MARK: - Error Types

enum AppStoreUpdateError: Error {
    case noDownloads
    case downloadFailed(String)
    case downloadCancelled
    case networkError(Error)
}

// MARK: - AppStoreUpdater

class AppStoreUpdater {
    static let shared = AppStoreUpdater()

    private init() {}

    /// Check if running macOS version affected by installd bug
    /// Affected: 14.8.2 (Darwin 24.6.2), 15.7.2 (Darwin 25.7.2), and 26.1+ (Darwin 26.1+)
    private func needsInstalldWorkaround() -> Bool {
        let version = ProcessInfo.processInfo.operatingSystemVersion

        // macOS 26.1+ (all versions)
        if version.majorVersion >= 26 && version.minorVersion >= 1 {
            return true
        }

        // macOS 15.7.2 (Darwin 25.7.2)
        if version.majorVersion == 25 && version.minorVersion == 7 && version.patchVersion >= 2 {
            return true
        }

        // macOS 14.8.2 (Darwin 24.6.2)
        if version.majorVersion == 24 && version.minorVersion == 6 && version.patchVersion >= 2 {
            return true
        }

        return false
    }

    /// Update an app from the App Store with progress tracking
    /// - Parameters:
    ///   - adamID: The App Store ID of the app
    ///   - appPath: Path to the installed app (for receipt injection)
    ///   - progress: Progress callback (percent: 0.0-1.0, status message)
    ///   - attemptCount: Number of retry attempts for network errors (default: 3)
    func updateApp(
        adamID: UInt64,
        appPath: URL,
        isIOSApp: Bool = false,
        progress: @escaping @Sendable (Double, String) -> Void,
        attemptCount: UInt32 = 3
    ) async throws {
        await GlobalConsoleManager.shared.appendOutput("Initiating App Store download for adamID \(adamID)...\n", source: CurrentPage.updater.title)

        do {
            // Create SSPurchase for downloading (purchasing: false = update existing app)
            // NOTE: For iOS apps, all entity types ("software", "macSoftware", "desktopSoftware")
            // download the same universal variant, which is NOT Mac-compatible.
            // iOS app updates are currently non-functional due to Apple's CDN serving wrong variant.
            let purchase = await SSPurchase(adamID: adamID, purchasing: false, kind: "software")

            // Mark as update to indicate this is a redownload of owned app
            purchase.isUpdate = true

            // iOS apps need special handling on ALL macOS versions (flag passed from caller)
            // Check if workaround is needed for macOS apps on affected OS versions
            let needsWorkaround = needsInstalldWorkaround()

            if isIOSApp {
                await GlobalConsoleManager.shared.appendOutput("Detected iOS app - using custom installer\n", source: CurrentPage.updater.title)
            } else if needsWorkaround {
                await GlobalConsoleManager.shared.appendOutput("Detected macOS version with installer bug - using custom installer\n", source: CurrentPage.updater.title)
            }

            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
                // NOTE: iOS apps will show "Current Version Not Compatible" dialog
                // User must click "Download Last Compatible" to proceed with download
                CKPurchaseController.shared().perform(purchase, withOptions: 0) { _, _, error, response in
                    if let error = error {
                        Task {
                            await GlobalConsoleManager.shared.appendOutput("✗ App Store purchase failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title)
                        }
                        continuation.resume(throwing: error)
                    } else if response?.downloads?.isEmpty == false {
                        // Download started - create observer to track it
                        Task {
                            await GlobalConsoleManager.shared.appendOutput("Download started, monitoring progress...\n", source: CurrentPage.updater.title)
                            do {
                                if isIOSApp {
                                    // iOS apps: Use dedicated iOS observer (all macOS versions)
                                    printOS("⚠️ Detected iOS app - using custom installer")
                                    let observer = IOSDownloadObserver(adamID: adamID, appPath: appPath, progress: progress)
                                    try await observer.observeDownloadQueue()
                                } else if needsWorkaround {
                                    // macOS apps on affected OS versions: Use workaround observer
                                    printOS("⚠️ Detected macOS version with private frameworks installer bug - using custom installer")
                                    let observer = MacOSDownloadObserverWithWorkaround(adamID: adamID, appPath: appPath, progress: progress)
                                    try await observer.observeDownloadQueue()
                                } else {
                                    // macOS apps on unaffected OS versions: Use standard observer
                                    let observer = AppStoreDownloadObserver(adamID: adamID, progress: progress)
                                    try await observer.observeDownloadQueue()
                                }
                                await GlobalConsoleManager.shared.appendOutput("Download and installation completed successfully\n", source: CurrentPage.updater.title)
                                continuation.resume()
                            } catch {
                                await GlobalConsoleManager.shared.appendOutput("✗ Download/installation failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title)
                                continuation.resume(throwing: error)
                            }
                        }
                    } else {
                        // No downloads means already up to date
                        Task {
                            await GlobalConsoleManager.shared.appendOutput("App is already up to date\n", source: CurrentPage.updater.title)
                        }
                        progress(1.0, "Already up to date")
                        continuation.resume()
                    }
                }
            }
        } catch {
            // Retry logic for network errors (like mas does)
            guard attemptCount > 1 else {
                await GlobalConsoleManager.shared.appendOutput("✗ Update failed after retries: \(error.localizedDescription)\n", source: CurrentPage.updater.title)
                throw error
            }

            // Only retry network errors
            guard (error as NSError).domain == NSURLErrorDomain else {
                await GlobalConsoleManager.shared.appendOutput("✗ Non-network error, not retrying: \(error.localizedDescription)\n", source: CurrentPage.updater.title)
                throw error
            }

            let remainingAttempts = attemptCount - 1
            await GlobalConsoleManager.shared.appendOutput("Network error, retrying... (\(remainingAttempts) attempts remaining)\n", source: CurrentPage.updater.title)
            try await updateApp(adamID: adamID, appPath: appPath, isIOSApp: isIOSApp, progress: progress, attemptCount: remainingAttempts)
        }
    }
}

// MARK: - AppStoreDownloadObserver

/// Per-download observer that tracks a single App Store download/update
/// This matches the architecture used by mas CLI tool
private final class AppStoreDownloadObserver: NSObject, CKDownloadQueueObserver {
    private let adamID: UInt64
    private let progressCallback: @Sendable (Double, String) -> Void
    private var completionHandler: (() -> Void)?
    private var errorHandler: ((Error) -> Void)?

    init(adamID: UInt64, progress: @escaping @Sendable (Double, String) -> Void) {
        self.adamID = adamID
        self.progressCallback = progress
        super.init()
    }

    /// Observe the download queue until this download completes
    /// Uses defer to ensure observer is always removed when done
    func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws {
        let observerID = queue.add(self)
        defer {
            queue.removeObserver(observerID)
        }

        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            completionHandler = { [weak self] in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume()
            }
            errorHandler = { [weak self] error in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume(throwing: error)
            }
        }
    }

    // MARK: - CKDownloadQueueObserver Delegate Methods

    func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) {
        // Download was added to queue - no action needed
    }

    func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status else {
            return
        }

        // This is the official completion signal from CommerceKit
        if status.isFailed {
            let error = status.error ?? AppStoreUpdateError.downloadFailed("Download failed")
            Task {
                await GlobalConsoleManager.shared.appendOutput("✗ App Store download failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title)
            }
            errorHandler?(error)
        } else if status.isCancelled {
            Task {
                await GlobalConsoleManager.shared.appendOutput("Download cancelled by user\n", source: CurrentPage.updater.title)
            }
            errorHandler?(AppStoreUpdateError.downloadCancelled)
        } else {
            // Success!
            Task {
                await GlobalConsoleManager.shared.appendOutput("App Store download completed\n", source: CurrentPage.updater.title)
            }
            progressCallback(1.0, "Completed")
            completionHandler?()
        }
    }

    func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status,
              let activePhase = status.activePhase else {
            return
        }

        let phaseType = activePhase.phaseType
        let percentComplete = status.percentComplete  // Float: 0.0 to 1.0
        let progress = max(0.0, min(1.0, Double(percentComplete)))

        // Report progress based on phase
        // Special case: at 100%, always show "Installing..." (CommerceKit sometimes resets to phase 0 at completion)
        if progress >= 1.0 {
            progressCallback(progress, "Installing...")
        } else {
            switch phaseType {
            case 0: // Downloading
                progressCallback(progress, "Downloading...")

            case 1: // Installing
                progressCallback(progress, "Installing...")

            case 4: // Initial/Preparing
                progressCallback(progress, "Preparing...")

            case 5: // Downloaded (not complete yet - wait for changedWithRemoval)
                progressCallback(progress, "Installing...")

            default:
                progressCallback(progress, "Processing...")
            }
        }
    }
}

// MARK: - IOSDownloadObserver

/// Observer for iOS/iPad app downloads (IPA files)
/// Preserves IPA to /tmp for installation
/// Used for all iOS apps regardless of macOS version
private final class IOSDownloadObserver: NSObject, CKDownloadQueueObserver {
    private let adamID: UInt64
    private let appPath: URL
    private let progressCallback: @Sendable (Double, String) -> Void
    private var completionHandler: (() -> Void)?
    private var errorHandler: ((Error) -> Void)?
    private var iosFilesPreserved = false  // Track if IPA was already preserved
    private var hardLinkedIPAPath: String?  // Path to hard-linked IPA in /tmp
    private var isManuallyInstalling = false

    init(adamID: UInt64, appPath: URL, progress: @escaping @Sendable (Double, String) -> Void) {
        self.adamID = adamID
        self.appPath = appPath
        self.progressCallback = progress
        super.init()
    }

    func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws {
        let observerID = queue.add(self)
        defer {
            queue.removeObserver(observerID)
        }

        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            completionHandler = { [weak self] in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume()
            }
            errorHandler = { [weak self] error in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume(throwing: error)
            }
        }
    }

    // MARK: - CKDownloadQueueObserver Delegate Methods

    func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) {
        // Download was added to queue - no action needed
    }

    func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status else {
            return
        }

        // iOS app download completed - always perform manual installation if IPA was preserved
        // (CommerceKit cannot install iOS apps, so we handle it ourselves regardless of reported status)
        if let ipaPath = hardLinkedIPAPath {
            Task {
                await performManualInstallation(ipaPath: ipaPath)
            }
        } else {
            // No IPA preserved - this shouldn't happen, but handle gracefully
            if status.isFailed || status.isCancelled {
                errorHandler?(AppStoreUpdateError.downloadFailed("Failed to preserve IPA file"))
            } else {
                // Unexpected: CommerceKit claims success but we don't have an IPA
                printOS("⚠️ Download completed but no IPA was preserved")
                progressCallback(1.0, "Completed")
                completionHandler?()
            }
        }
    }

    func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status,
              let activePhase = status.activePhase else {
            return
        }

        let phaseType = activePhase.phaseType
        let percentComplete = status.percentComplete
        let progress = max(0.0, min(1.0, Double(percentComplete)))

        // At 80% progress, preserve IPA file before CommerceKit potentially cleans it up
        if progress >= 0.80 && progress < 1.0 && !iosFilesPreserved {
            preserveIPAFile()
        }

        // Report progress
        if isManuallyInstalling {
            progressCallback(progress, "Installing...")
        } else if progress >= 1.0 {
            progressCallback(progress, "Downloading...")
        } else {
            switch phaseType {
            case 0: // Downloading
                progressCallback(progress, "Downloading...")
            case 4: // Initial/Preparing
                progressCallback(progress, "Preparing...")
            default:
                progressCallback(progress, "Downloading...")
            }
        }
    }

    // MARK: - Helper Methods

    private func performManualInstallation(ipaPath: String) async {
        isManuallyInstalling = true

        // 80%: Preparing installation
        progressCallback(0.80, "Preparing installation...")
        try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 sec

        do {
            // 80-100%: IOSAppInstaller handles the rest (extraction, metadata, installation, cleanup)
            try await IOSAppInstaller.installIOSApp(
                ipaPath: ipaPath,
                adamID: adamID,
                existingAppPath: appPath,
                progress: progressCallback
            )

            completionHandler?()
        } catch {
            errorHandler?(error)
        }
    }

    private func preserveIPAFile() {
        guard !iosFilesPreserved else { return }

        let downloadDir = "\(CKDownloadDirectory(nil))/\(adamID)"
        let tempDir = "/tmp/pearcleaner-ios-\(adamID)"

        do {
            // Create temp directory
            try FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: true)

            let contents = try FileManager.default.contentsOfDirectory(atPath: downloadDir)

            if let ipaFile = contents.first(where: { $0.hasSuffix(".ipa") }) {
                // Hard link IPA to /tmp (same pattern as PKG files)
                let ipaSource = "\(downloadDir)/\(ipaFile)"
                let ipaDest = "\(tempDir)/app.ipa"

                try FileManager.default.linkItem(atPath: ipaSource, toPath: ipaDest)
                hardLinkedIPAPath = ipaDest

                iosFilesPreserved = true
            } else {
                printOS("⚠️ No IPA file found in \(downloadDir)")
            }
        } catch {
            printOS("❌ Failed to preserve IPA: \(error.localizedDescription)")
        }
    }
}

// MARK: - MacOSDownloadObserverWithWorkaround

/// Special observer for macOS versions affected by installd bug
/// Downloads PKG, hard links it, then manually installs via HelperToolManager
/// Used only for macOS apps on affected OS versions
private final class MacOSDownloadObserverWithWorkaround: NSObject, CKDownloadQueueObserver {
    private let adamID: UInt64
    private let appPath: URL
    private let progressCallback: @Sendable (Double, String) -> Void
    private var completionHandler: (() -> Void)?
    private var errorHandler: ((Error) -> Void)?
    private var hardLinkedPkgPath: String?
    private var hardLinkedReceiptPath: String?
    private var isManuallyInstalling = false

    init(adamID: UInt64, appPath: URL, progress: @escaping @Sendable (Double, String) -> Void) {
        self.adamID = adamID
        self.appPath = appPath
        self.progressCallback = progress
        super.init()
    }

    func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws {
        let observerID = queue.add(self)
        defer {
            queue.removeObserver(observerID)
        }

        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            completionHandler = { [weak self] in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume()
            }
            errorHandler = { [weak self] error in
                self?.completionHandler = nil
                self?.errorHandler = nil
                continuation.resume(throwing: error)
            }
        }
    }

    // MARK: - CKDownloadQueueObserver Delegate Methods

    func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) {
        // Download was added to queue - no action needed
    }

    func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status else {
            return
        }

        // Download completed (installd will fail, but we have the PKG hard linked)
        if status.isFailed || status.isCancelled {
            // Expected failure on affected macOS versions - proceed with manual installation
            if let pkgPath = hardLinkedPkgPath {
                Task {
                    await performManualInstallation(pkgPath: pkgPath)
                }
            } else {
                errorHandler?(AppStoreUpdateError.downloadFailed("Failed to preserve PKG file"))
            }
        } else {
            // Unexpected success (installd worked somehow)
            if let pkgPath = hardLinkedPkgPath {
                // Clean up temp directory since installd succeeded
                let tempDir = (pkgPath as NSString).deletingLastPathComponent
                try? FileManager.default.removeItem(atPath: tempDir)
                hardLinkedPkgPath = nil
                hardLinkedReceiptPath = nil
            }
            progressCallback(1.0, "Completed")
            completionHandler?()
        }
    }

    func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
        guard let metadata = download.metadata,
              metadata.itemIdentifier == adamID,
              let status = download.status,
              let activePhase = status.activePhase else {
            return
        }

        let phaseType = activePhase.phaseType
        let percentComplete = status.percentComplete
        let progress = max(0.0, min(1.0, Double(percentComplete)))

        // At 80% progress, create hard link to preserve PKG before installd fails
        if progress >= 0.80 && progress < 1.0 && hardLinkedPkgPath ==
Download .txt
gitextract_u4mjrmj6/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── project.md
│   └── workflows/
│       └── issues.yml
├── .gitignore
├── Builds/
│   ├── ExportOptions.plist
│   └── changes.md
├── FUNDING.yml
├── FinderOpen/
│   ├── Assets.xcassets/
│   │   ├── Contents.json
│   │   └── Glass.imageset/
│   │       └── Contents.json
│   ├── FinderOpen.entitlements
│   ├── FinderOpen.swift
│   ├── Info.plist
│   └── Localizable.xcstrings
├── LICENSE.md
├── Pear Resources/
│   ├── Glass.icon/
│   │   ├── Assets/
│   │   │   └── pear.heic
│   │   └── icon.json
│   └── Pear.psd
├── Pearcleaner/
│   ├── Logic/
│   │   ├── AppCommands.swift
│   │   ├── AppInfoFetch.swift
│   │   ├── AppPathsFetch.swift
│   │   ├── AppState.swift
│   │   ├── AppsUpdater/
│   │   │   ├── AppStoreReset.swift
│   │   │   ├── AppStoreUpdateChecker.swift
│   │   │   ├── AppStoreUpdater.swift
│   │   │   ├── HomebrewAdoption.swift
│   │   │   ├── HomebrewUpdateChecker.swift
│   │   │   ├── IOSAppInstaller.swift
│   │   │   ├── Models.swift
│   │   │   ├── PrivateFrameworks/
│   │   │   │   ├── CFBundle/
│   │   │   │   │   └── CFBundle_Private.h
│   │   │   │   ├── 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
│   │   │   ├── SSPurchase+Extension.swift
│   │   │   ├── SparkleUpdateChecker.swift
│   │   │   ├── SparkleUpdateDriver.swift
│   │   │   ├── SparkleUpdateOperation.swift
│   │   │   ├── UpdateManager.swift
│   │   │   ├── UpdateQueue.swift
│   │   │   ├── UpdaterDebugLogger.swift
│   │   │   ├── UpdaterSettings.swift
│   │   │   └── VersionComparison.swift
│   │   ├── Brew/
│   │   │   ├── HomebrewAutoUpdateManager.swift
│   │   │   ├── HomebrewController.swift
│   │   │   ├── HomebrewManager.swift
│   │   │   ├── HomebrewPackage.swift
│   │   │   ├── HomebrewTap.swift
│   │   │   └── HomebrewUninstaller.swift
│   │   ├── CLI.swift
│   │   ├── Conditions.swift
│   │   ├── DeepLink.swift
│   │   ├── FileSearch/
│   │   │   ├── FileSearchLogic.swift
│   │   │   └── FileSearchModels.swift
│   │   ├── FuzzySearch.swift
│   │   ├── GlobalConsoleManager.swift
│   │   ├── HelperToolManager.swift
│   │   ├── KeychainPasswordManager.swift
│   │   ├── Lipo.swift
│   │   ├── Locations.swift
│   │   ├── Logic.swift
│   │   ├── PKG/
│   │   │   ├── PKBOM.h
│   │   │   ├── PKBundleComponent.h
│   │   │   ├── PKBundleComponentVersion.h
│   │   │   ├── PKComponent.h
│   │   │   ├── PKGManager.swift
│   │   │   ├── PKInstallHistory.h
│   │   │   ├── PKPackage.h
│   │   │   ├── PKPackageChecker.h
│   │   │   ├── PKPackageInfo.h
│   │   │   ├── PKProductInfo.h
│   │   │   ├── PKReceipt.h
│   │   │   └── Pearcleaner-Bridging-Header.h
│   │   ├── PasswordRequestHandler.swift
│   │   ├── ProcessEnv.swift
│   │   ├── ReversePathsFetch.swift
│   │   ├── TCC/
│   │   │   ├── TCCModels.swift
│   │   │   └── TCCQueryHelper.swift
│   │   ├── UndoHistoryManager.swift
│   │   ├── UndoManager.swift
│   │   └── Utilities.swift
│   ├── PearcleanerApp.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AccentColor.colorset/
│   │   │   │   └── Contents.json
│   │   │   └── Contents.json
│   │   ├── Glass.icon/
│   │   │   ├── Assets/
│   │   │   │   └── pear.heic
│   │   │   └── icon.json
│   │   ├── Info.plist
│   │   ├── Localizable.xcstrings
│   │   ├── Pearcleaner.entitlements
│   │   └── askpass.sh
│   ├── Style/
│   │   ├── ChromeBorder.swift
│   │   ├── CircularProgressView.swift
│   │   ├── ControlGroupChrome.swift
│   │   ├── PearGroupBox.swift
│   │   ├── SparkleProgressBar.swift
│   │   ├── Styles.swift
│   │   └── Theme.swift
│   ├── Views/
│   │   ├── AppsUpdaterView/
│   │   │   ├── AdoptionSheetView.swift
│   │   │   ├── AppsUpdaterView.swift
│   │   │   ├── CaskAdoptionContentView.swift
│   │   │   ├── ExpandableActionButton.swift
│   │   │   ├── UpdateDetailView.swift
│   │   │   ├── UpdateRowViewSidebar.swift
│   │   │   └── UpdaterDetailsSidebar.swift
│   │   ├── AppsView/
│   │   │   ├── AppListItems.swift
│   │   │   ├── AppSearchView.swift
│   │   │   ├── AppsListView.swift
│   │   │   └── GridAppItem.swift
│   │   ├── Brew/
│   │   │   ├── AutoUpdateSection.swift
│   │   │   ├── HomebrewView.swift
│   │   │   ├── LogViewerSheet.swift
│   │   │   ├── MaintenanceSection.swift
│   │   │   ├── PackageDetailsSidebar.swift
│   │   │   ├── SearchInstallSection.swift
│   │   │   └── TapManagementSection.swift
│   │   ├── Components/
│   │   │   ├── BadgeOverlay.swift
│   │   │   ├── GlobalConsoleView.swift
│   │   │   ├── PermissionsSheetView.swift
│   │   │   ├── SidebarDetailView/
│   │   │   │   ├── GenericSidebarListView.swift
│   │   │   │   └── SidebarDetailLayout.swift
│   │   │   ├── StandardSheetView.swift
│   │   │   └── TCCPermissionViewer.swift
│   │   ├── DaemonView.swift
│   │   ├── DeleteHistoryView.swift
│   │   ├── DevelopmentView.swift
│   │   ├── FileSearchView.swift
│   │   ├── FilesView/
│   │   │   ├── FileCategory.swift
│   │   │   ├── FileListView.swift
│   │   │   ├── FilesSidebarView.swift
│   │   │   ├── FilesView.swift
│   │   │   └── TranslationSelectionSheet.swift
│   │   ├── LipoView/
│   │   │   ├── LipoSidebarView.swift
│   │   │   └── LipoView.swift
│   │   ├── MainWindow.swift
│   │   ├── PackageView.swift
│   │   ├── PluginsView.swift
│   │   ├── Settings/
│   │   │   ├── About.swift
│   │   │   ├── Folders.swift
│   │   │   ├── General.swift
│   │   │   ├── Helper.swift
│   │   │   ├── Interface.swift
│   │   │   ├── SettingsWindow.swift
│   │   │   └── Update.swift
│   │   └── ZombieView/
│   │       ├── ZombieSidebarView.swift
│   │       └── ZombieView.swift
│   └── announcements.json
├── Pearcleaner.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           ├── FinderOpen.xcscheme
│           ├── Pearcleaner Debug.xcscheme
│           ├── Pearcleaner Release.xcscheme
│           ├── PearcleanerSentinel Release.xcscheme
│           └── PearcleanerSentinel.xcscheme
├── PearcleanerHelper/
│   ├── CodesignCheck.swift
│   ├── com.alienator88.Pearcleaner.PearcleanerHelper.plist
│   └── main.swift
├── PearcleanerSentinel/
│   ├── FileWatcher.swift
│   ├── com.alienator88.PearcleanerSentinel.plist
│   └── main.swift
├── README.md
├── Shared/
│   └── AppGroupDefaults.swift
└── announcements.json
Download .txt
SYMBOL INDEX (26 symbols across 19 files)

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadQueue.h
  function interface (line 8) | interface CKDownloadQueue : CKServiceInterface {

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKPurchaseController.h
  function interface (line 10) | interface CKPurchaseController : CKServiceInterface {

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISStoreAccount.h
  function interface (line 8) | interface ISStoreAccount : NSObject <NSSecureCoding> {

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownload.h
  function interface (line 8) | interface SSDownload : NSObject <NSSecureCoding> {

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadMetadata.h
  type _NSZone (line 61) | struct _NSZone

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadPhase.h
  type _NSZone (line 20) | struct _NSZone

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadStatus.h
  type _NSZone (line 23) | struct _NSZone

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchase.h
  type _NSZone (line 40) | struct _NSZone

FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchaseResponse.h
  function interface (line 8) | interface SSPurchaseResponse : NSObject <NSSecureCoding> {

FILE: Pearcleaner/Logic/PKG/PKBOM.h
  type _BOMBom (line 9) | struct _BOMBom
  type _BOMFSObject (line 10) | struct _BOMFSObject
  function interface (line 12) | interface PKBOM : NSObject
  type _BOMFSObject (line 18) | struct _BOMFSObject
  type _BOMFSObject (line 19) | struct _BOMFSObject
  type _BOMBom (line 26) | struct _BOMBom

FILE: Pearcleaner/Logic/PKG/PKBundleComponent.h
  function interface (line 11) | interface PKBundleComponent : PKComponent

FILE: Pearcleaner/Logic/PKG/PKBundleComponentVersion.h
  function interface (line 11) | interface PKBundleComponentVersion : NSObject
  type __CFBundle (line 24) | struct __CFBundle
  type __CFBundle (line 26) | struct __CFBundle

FILE: Pearcleaner/Logic/PKG/PKComponent.h
  function interface (line 11) | interface PKComponent : NSObject

FILE: Pearcleaner/Logic/PKG/PKInstallHistory.h
  function interface (line 9) | interface PKInstallHistory : NSObject

FILE: Pearcleaner/Logic/PKG/PKPackage.h
  type _NSZone (line 48) | struct _NSZone

FILE: Pearcleaner/Logic/PKG/PKPackageChecker.h
  function interface (line 11) | interface PKPackageChecker : NSObject

FILE: Pearcleaner/Logic/PKG/PKPackageInfo.h
  function interface (line 11) | interface PKPackageInfo : NSObject

FILE: Pearcleaner/Logic/PKG/PKProductInfo.h
  type _NSZone (line 19) | struct _NSZone

FILE: Pearcleaner/Logic/PKG/PKReceipt.h
  function interface (line 9) | interface PKReceipt : NSObject
Condensed preview — 179 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,861K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "chars": 1650,
    "preview": "---\nname: 🐛 Bug Report\nabout: For submitting new bugs related to the application functionality\ntitle: \"[BUG] ENTER ISSUE"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 426,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: PEARCLEANER IS NOW IN MAINTENANCE MODE\n    url: https://github.com/"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.md",
    "chars": 968,
    "preview": "<!--\n---\nname: 🌟 Feature Request\nabout: For submitting new feature ideas\ntitle: \"[FR] ENTER TITLE HERE\"\nlabels: 'enhance"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/project.md",
    "chars": 692,
    "preview": "This project is now in **maintenance mode**.\n\n---\n\nI’m no longer accepting **NEW** feature requests as Pearcleaner has g"
  },
  {
    "path": ".github/workflows/issues.yml",
    "chars": 966,
    "preview": "name: Close empty issues and templates\non:\n  issues:\n    types:\n      - reopened\n      - opened\n      - edited\n\njobs:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 2250,
    "preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
  },
  {
    "path": "Builds/ExportOptions.plist",
    "chars": 833,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Builds/changes.md",
    "chars": 705,
    "preview": "### What's New\n\n- [x] Ability to hide unused utility pages from global menu in Settings > Interface > Startup View - #47"
  },
  {
    "path": "FUNDING.yml",
    "chars": 20,
    "preview": "github: alienator88\n"
  },
  {
    "path": "FinderOpen/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "FinderOpen/Assets.xcassets/Glass.imageset/Contents.json",
    "chars": 303,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Glass.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n  "
  },
  {
    "path": "FinderOpen/FinderOpen.entitlements",
    "chars": 483,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "FinderOpen/FinderOpen.swift",
    "chars": 2306,
    "preview": "//\n//  FinderSync.swift\n//  FinderOpen\n//\n//  Created by Alin Lupascu on 4/11/24.\n//\n\nimport Cocoa\nimport FinderSync\n\ncl"
  },
  {
    "path": "FinderOpen/Info.plist",
    "chars": 446,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "FinderOpen/Localizable.xcstrings",
    "chars": 2430,
    "preview": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"Pearcleaner Uninstall\" : {\n      \"localizations\" : {\n        \"de\" : {\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 12301,
    "preview": "“Commons Clause” License Condition v1.0\n\nThe Software is provided to you by the Licensor under the License, as defined b"
  },
  {
    "path": "Pear Resources/Glass.icon/icon.json",
    "chars": 1063,
    "preview": "{\n  \"fill\" : {\n    \"linear-gradient\" : [\n      \"display-p3:0.93159,0.94081,0.94081,1.00000\",\n      \"display-p3:0.75900,0"
  },
  {
    "path": "Pearcleaner/Logic/AppCommands.swift",
    "chars": 14033,
    "preview": "//\n//  AppCommands.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport SwiftUI\nimport AlinFound"
  },
  {
    "path": "Pearcleaner/Logic/AppInfoFetch.swift",
    "chars": 38765,
    "preview": "//\n//  AppInfoFetch.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/20/24.\n//\n\nimport Foundation\nimport SwiftU"
  },
  {
    "path": "Pearcleaner/Logic/AppPathsFetch.swift",
    "chars": 42543,
    "preview": "//\n//  AppPathsFetch.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 2/6/25.\n//\n\nimport Foundation\nimport AppKit"
  },
  {
    "path": "Pearcleaner/Logic/AppState.swift",
    "chars": 30314,
    "preview": "//\n//  AppState.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport AlinFoundation\nimport Finde"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/AppStoreReset.swift",
    "chars": 6928,
    "preview": "//\n//  AppStoreReset.swift\n//  Pearcleaner\n//\n//  Based on mas-cli's reset command\n//  Copyright © 2018 mas-cli. All rig"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/AppStoreUpdateChecker.swift",
    "chars": 14134,
    "preview": "//\n//  AppStoreUpdateChecker.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport Foundation\nimp"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/AppStoreUpdater.swift",
    "chars": 26753,
    "preview": "//\n//  AppStoreUpdater.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport Foundation\nimport Co"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/HomebrewAdoption.swift",
    "chars": 7126,
    "preview": "//\n//  HomebrewAdoption.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/18/25.\n//\n\nimport Foundation\n\n// MARK"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/HomebrewUpdateChecker.swift",
    "chars": 12356,
    "preview": "//\n//  HomebrewUpdateChecker.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport Foundation\n\ncl"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/IOSAppInstaller.swift",
    "chars": 16414,
    "preview": "//\n//  IOSAppInstaller.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/12/25.\n//\n\nimport Foundation\nimport Ap"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/Models.swift",
    "chars": 3277,
    "preview": "//\n//  Models.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport Foundation\nimport SwiftUI\nimp"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CFBundle/CFBundle_Private.h",
    "chars": 465,
    "preview": "//\n//  CFBundle_Private.h\n//  Pearcleaner\n//\n//  Private API for flushing bundle caches\n//\n\n#import <Foundation/Foundati"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadDirectory.h",
    "chars": 162,
    "preview": "//\n// CKDownloadDirectory.h\n// mas\n//\n// Copyright © 2018 mas-cli. All rights reserved.\n//\n\nNSString * _Nonnull CKDownlo"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadQueue.h",
    "chars": 2574,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/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": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKPurchaseController.h",
    "chars": 1970,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKServiceInterface.h",
    "chars": 269,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CommerceKit.h",
    "chars": 488,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/module.modulemap",
    "chars": 173,
    "preview": "module CommerceKit [system] [no_undeclared_includes] {\n\trequires macos, objc\n\tuse StoreFoundation\n\tlink framework \"Comme"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISAccountService-Protocol.h",
    "chars": 6278,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISServiceProxy.h",
    "chars": 2899,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISStoreAccount.h",
    "chars": 1706,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownload.h",
    "chars": 1940,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadMetadata.h",
    "chars": 3720,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadPhase.h",
    "chars": 1040,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadStatus.h",
    "chars": 1153,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchase.h",
    "chars": 2222,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchaseResponse.h",
    "chars": 982,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/StoreFoundation.h",
    "chars": 814,
    "preview": "//\n// Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew)\n//\n// - LC_BUILD_VERSION: "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/module.modulemap",
    "chars": 180,
    "preview": "module StoreFoundation [system] [no_undeclared_includes] {\n\trequires macos, objc\n\tuse Foundation\n\tlink framework \"StoreF"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/SSPurchase+Extension.swift",
    "chars": 1412,
    "preview": "//\n//  SSPurchase+Extension.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//  Based on mas-cli imple"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/SparkleUpdateChecker.swift",
    "chars": 28885,
    "preview": "//\n//  SparkleUpdateChecker.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/28/25.\n//\n//  Simplified Sparkle "
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/SparkleUpdateDriver.swift",
    "chars": 12169,
    "preview": "//\n//  SparkleUpdateDriver.swift\n//  Pearcleaner\n//\n//  Custom SPUUserDriver for programmatically controlling Sparkle up"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/SparkleUpdateOperation.swift",
    "chars": 2458,
    "preview": "//\n//  SparkleUpdateOperation.swift\n//  Pearcleaner\n//\n//  Operation subclass that blocks until Sparkle update completes"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/UpdateManager.swift",
    "chars": 45785,
    "preview": "//\n//  UpdateManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport Foundation\nimport Swif"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/UpdateQueue.swift",
    "chars": 1156,
    "preview": "//\n//  UpdateQueue.swift\n//  Pearcleaner\n//\n//  Manages concurrent Sparkle update operations with proper queuing to prev"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/UpdaterDebugLogger.swift",
    "chars": 4020,
    "preview": "//\n//  UpdaterDebugLogger.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/25/25.\n//\n\nimport Foundation\nimport"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/UpdaterSettings.swift",
    "chars": 1643,
    "preview": "//\n//  UpdaterSettings.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/21/25.\n//\n\nimport Foundation\n\n// MARK:"
  },
  {
    "path": "Pearcleaner/Logic/AppsUpdater/VersionComparison.swift",
    "chars": 12579,
    "preview": "//\n//  VersionComparison.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/22/25.\n//\n//  Based on Sparkle Frame"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewAutoUpdateManager.swift",
    "chars": 23505,
    "preview": "//\n//  HomebrewAutoUpdateManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/25/25.\n//\n\nimport Foundation"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewController.swift",
    "chars": 103481,
    "preview": "//\n//  HomebrewController.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport Foundation\nimport"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewManager.swift",
    "chars": 24117,
    "preview": "//\n//  HomebrewManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport Foundation\nimport Al"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewPackage.swift",
    "chars": 9075,
    "preview": "//\n//  HomebrewPackage.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport Foundation\n\n// MARK:"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewTap.swift",
    "chars": 489,
    "preview": "//\n//  HomebrewTap.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport Foundation\n\nstruct Homeb"
  },
  {
    "path": "Pearcleaner/Logic/Brew/HomebrewUninstaller.swift",
    "chars": 23776,
    "preview": "//\n//  HomebrewUninstaller.swift\n//  Pearcleaner\n//\n//  Created by Pearcleaner on 2025-10-03.\n//\n\nimport Foundation\nimpo"
  },
  {
    "path": "Pearcleaner/Logic/CLI.swift",
    "chars": 16840,
    "preview": "import AlinFoundation\nimport ArgumentParser\nimport Foundation\nimport ServiceManagement\nimport SwiftUI\nimport UniformType"
  },
  {
    "path": "Pearcleaner/Logic/Conditions.swift",
    "chars": 11657,
    "preview": "//\n//  Conditions.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 4/15/24.\n//\n\nimport Foundation\n\nstruct Conditi"
  },
  {
    "path": "Pearcleaner/Logic/DeepLink.swift",
    "chars": 12410,
    "preview": "//\n//  DeepLink.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/9/23.\n//\n\nimport Foundation\nimport SwiftUI\nim"
  },
  {
    "path": "Pearcleaner/Logic/FileSearch/FileSearchLogic.swift",
    "chars": 19951,
    "preview": "//\n//  FileSearchLogic.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 09/29/25.\n//\n\nimport Foundation\nimport Ap"
  },
  {
    "path": "Pearcleaner/Logic/FileSearch/FileSearchModels.swift",
    "chars": 6957,
    "preview": "//\n//  FileSearchModels.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 09/29/25.\n//\n\nimport Foundation\nimport A"
  },
  {
    "path": "Pearcleaner/Logic/FuzzySearch.swift",
    "chars": 6306,
    "preview": "import Foundation\n\n/// FuzzySearchCharacters is used to normalise strings\nstruct FuzzySearchCharacter {\n    let content:"
  },
  {
    "path": "Pearcleaner/Logic/GlobalConsoleManager.swift",
    "chars": 2279,
    "preview": "//\n//  GlobalConsoleManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/13/24.\n//\n\nimport Foundation\nimpo"
  },
  {
    "path": "Pearcleaner/Logic/HelperToolManager.swift",
    "chars": 14430,
    "preview": "//\n//  HelperToolManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/14/25.\n//\n\nimport ServiceManagement\ni"
  },
  {
    "path": "Pearcleaner/Logic/KeychainPasswordManager.swift",
    "chars": 4617,
    "preview": "//\n//  KeychainPasswordManager.swift\n//  Pearcleaner\n//\n//  Manages sudo password caching in macOS Keychain with time-ba"
  },
  {
    "path": "Pearcleaner/Logic/Lipo.swift",
    "chars": 18663,
    "preview": "//\n//  Lipo.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 4/10/25.\n//\n\nimport Foundation\n\n\n// Helper structs f"
  },
  {
    "path": "Pearcleaner/Logic/Locations.swift",
    "chars": 9581,
    "preview": "//\n//  Locations.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/10/23.\n//\n\nimport Foundation\n\n\nclass Locatio"
  },
  {
    "path": "Pearcleaner/Logic/Logic.swift",
    "chars": 60497,
    "preview": "//\n//  Logic.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport AlinFoundation\nimport Foundati"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKBOM.h",
    "chars": 751,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKBundleComponent.h",
    "chars": 1232,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKBundleComponentVersion.h",
    "chars": 1501,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKComponent.h",
    "chars": 2371,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKGManager.swift",
    "chars": 6331,
    "preview": "//\n//  PKGManager.swift\n//  Pearcleaner\n//\n//  Wrapper for Apple's private PackageKit framework APIs\n//\n\nimport Foundati"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKInstallHistory.h",
    "chars": 391,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKPackage.h",
    "chars": 1906,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKPackageChecker.h",
    "chars": 1138,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKPackageInfo.h",
    "chars": 4958,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKProductInfo.h",
    "chars": 1165,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/PKReceipt.h",
    "chars": 2638,
    "preview": "//\n//     Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).\n//\n//  Copyright (C) 1997-"
  },
  {
    "path": "Pearcleaner/Logic/PKG/Pearcleaner-Bridging-Header.h",
    "chars": 511,
    "preview": "//\n//  Pearcleaner-Bridging-Header.h\n//  Pearcleaner\n//\n//  Use this file to import Objective-C headers for Swift.\n//\n\n#"
  },
  {
    "path": "Pearcleaner/Logic/PasswordRequestHandler.swift",
    "chars": 2373,
    "preview": "//\n//  PasswordRequestHandler.swift\n//  Pearcleaner\n//\n//  Handles password requests from CLI via distributed notificati"
  },
  {
    "path": "Pearcleaner/Logic/ProcessEnv.swift",
    "chars": 5425,
    "preview": "//\n//  ProcessEnv.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/6/25.\n//\n\nimport Foundation\n\npublic extensi"
  },
  {
    "path": "Pearcleaner/Logic/ReversePathsFetch.swift",
    "chars": 12485,
    "preview": "//\n//  ReversePathsFetch.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/20/24.\n//\n\nimport Foundation\nimport A"
  },
  {
    "path": "Pearcleaner/Logic/TCC/TCCModels.swift",
    "chars": 4812,
    "preview": "//\n//  TCCModels.swift\n//  Pearcleaner\n//\n//  TCC (Transparency, Consent, and Control) data models for privacy permissio"
  },
  {
    "path": "Pearcleaner/Logic/TCC/TCCQueryHelper.swift",
    "chars": 5402,
    "preview": "//\n//  TCCQueryHelper.swift\n//  Pearcleaner\n//\n//  SQLite3 query helper for TCC (Transparency, Consent, and Control) dat"
  },
  {
    "path": "Pearcleaner/Logic/UndoHistoryManager.swift",
    "chars": 6815,
    "preview": "//\n//  UndoHistoryManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/10/25.\n//\n\nimport Foundation\nimport"
  },
  {
    "path": "Pearcleaner/Logic/UndoManager.swift",
    "chars": 10776,
    "preview": "//\n//  UndoManager.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 2/24/25.\n//\n\nimport Foundation\nimport SwiftUI"
  },
  {
    "path": "Pearcleaner/Logic/Utilities.swift",
    "chars": 32542,
    "preview": "//\n//  Utilities.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/3/23.\n//\n\nimport Foundation\nimport SwiftUI\ni"
  },
  {
    "path": "Pearcleaner/PearcleanerApp.swift",
    "chars": 4004,
    "preview": "//\n//  PearcleanerApp.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport SwiftUI\nimport AppKit"
  },
  {
    "path": "Pearcleaner/Resources/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 701,
    "preview": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"display-p3\",\n        \"components\" : {\n          \"alpha"
  },
  {
    "path": "Pearcleaner/Resources/Assets.xcassets/Contents.json",
    "chars": 137,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"compression-type\" : \"gpu-optimized"
  },
  {
    "path": "Pearcleaner/Resources/Glass.icon/icon.json",
    "chars": 1744,
    "preview": "{\n  \"fill\" : {\n    \"linear-gradient\" : [\n      \"display-p3:0.93159,0.94081,0.94081,1.00000\",\n      \"display-p3:0.75900,0"
  },
  {
    "path": "Pearcleaner/Resources/Info.plist",
    "chars": 1277,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Pearcleaner/Resources/Localizable.xcstrings",
    "chars": 2421255,
    "preview": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"\" : {\n      \"comment\" : \"Do not translate\",\n      \"shouldTranslate\" : "
  },
  {
    "path": "Pearcleaner/Resources/Pearcleaner.entitlements",
    "chars": 310,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Pearcleaner/Resources/askpass.sh",
    "chars": 128,
    "preview": "#!/bin/bash\nSCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nexec \"$SCRIPT_DIR/../MacOS/Pearcleaner\" ask-p"
  },
  {
    "path": "Pearcleaner/Style/ChromeBorder.swift",
    "chars": 3485,
    "preview": "//\n//  ChromeBorder.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/8/25.\n//\n\nimport SwiftUI\n\nextension View {"
  },
  {
    "path": "Pearcleaner/Style/CircularProgressView.swift",
    "chars": 1731,
    "preview": "//\n//  CircularProgressView.swift\n//  Pearcleaner\n//\n//  Circular progress indicator with thick stroke that fills clockw"
  },
  {
    "path": "Pearcleaner/Style/ControlGroupChrome.swift",
    "chars": 2623,
    "preview": "//\n//  ControlGroupChrome.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/8/25.\n//\n\nimport SwiftUI\n\npublic enu"
  },
  {
    "path": "Pearcleaner/Style/PearGroupBox.swift",
    "chars": 3581,
    "preview": "//\n//  IceGroupBox.swift\n//  Pearcleaner\n//\n//  Created by Jordan Baird (Ice), slightly altered for Pearcleaner usage by"
  },
  {
    "path": "Pearcleaner/Style/SparkleProgressBar.swift",
    "chars": 4467,
    "preview": "//\n//  SparkleProgressBar.swift\n//  Pearcleaner\n//\n//  Animated sparkle progress bar with twinkle effects\n//  Created by"
  },
  {
    "path": "Pearcleaner/Style/Styles.swift",
    "chars": 38293,
    "preview": "//\n//  Styles.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport Foundation\nimport SwiftUI\nimp"
  },
  {
    "path": "Pearcleaner/Style/Theme.swift",
    "chars": 22641,
    "preview": "import SwiftUI\nimport AlinFoundation\n\n// MARK: - Observable Theme Manager\nclass ThemeManager: ObservableObject {\n    sta"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/AdoptionSheetView.swift",
    "chars": 13796,
    "preview": "//\n//  AdoptionSheetView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/18/25.\n//\n\nimport SwiftUI\n\nenum Adop"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/AppsUpdaterView.swift",
    "chars": 12337,
    "preview": "//\n//  AppsUpdaterView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/13/25.\n//\n\nimport SwiftUI\nimport AlinF"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/CaskAdoptionContentView.swift",
    "chars": 5072,
    "preview": "//\n//  CaskAdoptionContentView.swift\n//  Pearcleaner\n//\n//  Reusable adoption UI content shared between AdoptionSheetVie"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/ExpandableActionButton.swift",
    "chars": 6451,
    "preview": "//\n//  ExpandableActionButton.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/25/25.\n//\n\nimport SwiftUI\nimpor"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/UpdateDetailView.swift",
    "chars": 49658,
    "preview": "//\n//  UpdateDetailView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/19/25.\n//\n\nimport SwiftUI\nimport Alin"
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/UpdateRowViewSidebar.swift",
    "chars": 7326,
    "preview": "//\n//  UpdateRowViewSidebar.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/19/25.\n//\n\nimport SwiftUI\nimport "
  },
  {
    "path": "Pearcleaner/Views/AppsUpdaterView/UpdaterDetailsSidebar.swift",
    "chars": 21895,
    "preview": "//\n//  UpdaterHiddenSidebar.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/16/25.\n//\n\nimport Foundation\nimpo"
  },
  {
    "path": "Pearcleaner/Views/AppsView/AppListItems.swift",
    "chars": 8763,
    "preview": "//\n//  AppListDetails.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/10/23.\n//\n\nimport AlinFoundation\nimport"
  },
  {
    "path": "Pearcleaner/Views/AppsView/AppSearchView.swift",
    "chars": 7405,
    "preview": "//\n//  Searchbar.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 4/26/24.\n//\n\nimport AlinFoundation\nimport Swift"
  },
  {
    "path": "Pearcleaner/Views/AppsView/AppsListView.swift",
    "chars": 6399,
    "preview": "//\n//  AppsListView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/4/24.\n//\n\nimport Foundation\nimport SwiftUI"
  },
  {
    "path": "Pearcleaner/Views/AppsView/GridAppItem.swift",
    "chars": 7461,
    "preview": "//\n//  GridAppItem.swift\n//  Pearcleaner\n//\n//  Created for grid layout mode\n//\n\nimport AlinFoundation\nimport Foundation"
  },
  {
    "path": "Pearcleaner/Views/Brew/AutoUpdateSection.swift",
    "chars": 24906,
    "preview": "//\n//  AutoUpdateSection.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/25/25.\n//\n\nimport SwiftUI\nimport Ali"
  },
  {
    "path": "Pearcleaner/Views/Brew/HomebrewView.swift",
    "chars": 11503,
    "preview": "//\n//  HomebrewView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport SwiftUI\nimport AlinFoun"
  },
  {
    "path": "Pearcleaner/Views/Brew/LogViewerSheet.swift",
    "chars": 951,
    "preview": "//\n//  LogViewerSheet.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/26/25.\n//\n\nimport SwiftUI\n\nstruct LogVi"
  },
  {
    "path": "Pearcleaner/Views/Brew/MaintenanceSection.swift",
    "chars": 24223,
    "preview": "//\n//  MaintenanceSection.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport SwiftUI\nimport Al"
  },
  {
    "path": "Pearcleaner/Views/Brew/PackageDetailsSidebar.swift",
    "chars": 897,
    "preview": "//\n//  PackageDetailsSidebar.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/07/25.\n//\n\nimport SwiftUI\nimport"
  },
  {
    "path": "Pearcleaner/Views/Brew/SearchInstallSection.swift",
    "chars": 131360,
    "preview": "//\n//  SearchInstallSection.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport SwiftUI\nimport "
  },
  {
    "path": "Pearcleaner/Views/Brew/TapManagementSection.swift",
    "chars": 24053,
    "preview": "//\n//  TapManagementSection.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/01/25.\n//\n\nimport SwiftUI\nimport "
  },
  {
    "path": "Pearcleaner/Views/Components/BadgeOverlay.swift",
    "chars": 19626,
    "preview": "//\n//  BadgeOverlay.swift\n//  Pearcleaner\n//\n//  Unified overlay component for all toolbar badge notifications\n//  Creat"
  },
  {
    "path": "Pearcleaner/Views/Components/GlobalConsoleView.swift",
    "chars": 6844,
    "preview": "//\n//  GlobalConsoleView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/13/24.\n//\n\nimport SwiftUI\n\nstruct Gl"
  },
  {
    "path": "Pearcleaner/Views/Components/PermissionsSheetView.swift",
    "chars": 6963,
    "preview": "//\n//  PermissionsSheetView.swift\n//  Pearcleaner\n//\n//  Custom permissions view with ThemeColors integration\n//\n\nimport"
  },
  {
    "path": "Pearcleaner/Views/Components/SidebarDetailView/GenericSidebarListView.swift",
    "chars": 11602,
    "preview": "//\n//  GenericSidebarListView.swift\n//  Pearcleaner\n//\n//  Created as a reusable component extracted from AppSearchView\n"
  },
  {
    "path": "Pearcleaner/Views/Components/SidebarDetailView/SidebarDetailLayout.swift",
    "chars": 769,
    "preview": "//\n//  SidebarDetailLayout.swift\n//  Pearcleaner\n//\n//  Created as a reusable layout component extracted from MainWindow"
  },
  {
    "path": "Pearcleaner/Views/Components/StandardSheetView.swift",
    "chars": 2671,
    "preview": "//\n//  StandardSheetView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/26/25.\n//\n\nimport SwiftUI\n\nstruct St"
  },
  {
    "path": "Pearcleaner/Views/Components/TCCPermissionViewer.swift",
    "chars": 5809,
    "preview": "//\n//  TCCPermissionViewer.swift\n//  Pearcleaner\n//\n//  Sheet view for displaying TCC (privacy) permissions for an appli"
  },
  {
    "path": "Pearcleaner/Views/DaemonView.swift",
    "chars": 44124,
    "preview": "//\n//  DaemonView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/10/25.\n//\n\nimport SwiftUI\nimport AlinFoundat"
  },
  {
    "path": "Pearcleaner/Views/DeleteHistoryView.swift",
    "chars": 10553,
    "preview": "//\n//  DeleteHistoryView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/10/25.\n//\n\nimport SwiftUI\nimport Ali"
  },
  {
    "path": "Pearcleaner/Views/DevelopmentView.swift",
    "chars": 76053,
    "preview": "//\n//  DevelopmentView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/15/24.\n//\nimport SwiftUI\nimport AlinFo"
  },
  {
    "path": "Pearcleaner/Views/FileSearchView.swift",
    "chars": 49514,
    "preview": "//\n//  FileSearchView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 09/29/25.\n//\n\nimport SwiftUI\nimport AlinFo"
  },
  {
    "path": "Pearcleaner/Views/FilesView/FileCategory.swift",
    "chars": 11436,
    "preview": "//\n//  FileCategory.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/18/25.\n//\n\nimport Foundation\nimport Swift"
  },
  {
    "path": "Pearcleaner/Views/FilesView/FileListView.swift",
    "chars": 16683,
    "preview": "//\n//  FileListView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 7/31/25.\n//\n\nimport AlinFoundation\nimport Fo"
  },
  {
    "path": "Pearcleaner/Views/FilesView/FilesSidebarView.swift",
    "chars": 17077,
    "preview": "//\n//  SidebarView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 7/31/25.\n//\n\nimport Foundation\nimport SwiftUI"
  },
  {
    "path": "Pearcleaner/Views/FilesView/FilesView.swift",
    "chars": 28479,
    "preview": "//\n//  FilesView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/1/23.\n//\n\nimport AlinFoundation\nimport Found"
  },
  {
    "path": "Pearcleaner/Views/FilesView/TranslationSelectionSheet.swift",
    "chars": 8933,
    "preview": "//\n//  TranslationSelectionSheet.swift\n//  Pearcleaner\n//\n//  Created by Claude on 10/20/25.\n//\n\nimport SwiftUI\nimport A"
  },
  {
    "path": "Pearcleaner/Views/LipoView/LipoSidebarView.swift",
    "chars": 8723,
    "preview": "//\n//  LipoSidebarView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/9/25.\n//\n\nimport AlinFoundation\nimport "
  },
  {
    "path": "Pearcleaner/Views/LipoView/LipoView.swift",
    "chars": 29669,
    "preview": "//\n//  LipoView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/20/25.\n//\n\nimport AlinFoundation\nimport SwiftU"
  },
  {
    "path": "Pearcleaner/Views/MainWindow.swift",
    "chars": 38089,
    "preview": "//\n//  AppListH.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/5/23.\n//\n\nimport AlinFoundation\nimport Finder"
  },
  {
    "path": "Pearcleaner/Views/PackageView.swift",
    "chars": 66154,
    "preview": "//\n//  PackageView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/10/25.\n//\n\nimport SwiftUI\nimport AlinFounda"
  },
  {
    "path": "Pearcleaner/Views/PluginsView.swift",
    "chars": 62928,
    "preview": "//\n//  PluginsView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 09/29/25.\n//\n\nimport SwiftUI\nimport AlinFound"
  },
  {
    "path": "Pearcleaner/Views/Settings/About.swift",
    "chars": 7070,
    "preview": "//\n//  About.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/5/23.\n//\n\nimport SwiftUI\nimport AlinFoundation\n\n"
  },
  {
    "path": "Pearcleaner/Views/Settings/Folders.swift",
    "chars": 24835,
    "preview": "//\n//  Folders.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/20/24.\n//\n\n\nimport Foundation\nimport SwiftUI\nim"
  },
  {
    "path": "Pearcleaner/Views/Settings/General.swift",
    "chars": 25929,
    "preview": "//\n//  General.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/5/23.\n//\n\nimport Foundation\nimport SwiftUI\nimp"
  },
  {
    "path": "Pearcleaner/Views/Settings/Helper.swift",
    "chars": 8829,
    "preview": "//\n//  Helper.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/14/25.\n//\n\nimport SwiftUI\nimport Foundation\nimpo"
  },
  {
    "path": "Pearcleaner/Views/Settings/Interface.swift",
    "chars": 15586,
    "preview": "//\n//  Interface.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 3/18/24.\n//\n\n\nimport Foundation\nimport SwiftUI\n"
  },
  {
    "path": "Pearcleaner/Views/Settings/SettingsWindow.swift",
    "chars": 10576,
    "preview": "//\n//  SettingsWindow.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 10/31/23.\n//\n\nimport SwiftUI\nimport AlinFo"
  },
  {
    "path": "Pearcleaner/Views/Settings/Update.swift",
    "chars": 2766,
    "preview": "//\n//  Update.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 11/5/23.\n//\n\n\nimport SwiftUI\nimport Foundation\nimp"
  },
  {
    "path": "Pearcleaner/Views/ZombieView/ZombieSidebarView.swift",
    "chars": 8247,
    "preview": "//\n//  ZombieSidebarView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 8/9/25.\n//\n\nimport Foundation\nimport Sw"
  },
  {
    "path": "Pearcleaner/Views/ZombieView/ZombieView.swift",
    "chars": 37338,
    "preview": "//\n//  ZombieView.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 2/26/24.\n//\n\nimport Foundation\nimport SwiftUI\n"
  },
  {
    "path": "Pearcleaner/announcements.json",
    "chars": 2736,
    "preview": "{\n    \"4.4.0\": \"- Privileged Helper Service: Pearcleaner now supports using a privileged helper service to perform opera"
  },
  {
    "path": "Pearcleaner.xcodeproj/project.pbxproj",
    "chars": 60847,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 73;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "Pearcleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
    "chars": 947,
    "preview": "{\n  \"originHash\" : \"878acce7c95e9774ee43b058d63510f4ad7a7a3a673f5600c415bcc68aad7c3f\",\n  \"pins\" : [\n    {\n      \"identit"
  },
  {
    "path": "Pearcleaner.xcodeproj/xcshareddata/xcschemes/FinderOpen.xcscheme",
    "chars": 3982,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   wasCreatedForAppExtension = \"YES\"\n   ve"
  },
  {
    "path": "Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Debug.xcscheme",
    "chars": 3276,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   version = \"1.7\">\n   <BuildAction\n      "
  },
  {
    "path": "Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Release.xcscheme",
    "chars": 2884,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   version = \"1.7\">\n   <BuildAction\n      "
  },
  {
    "path": "Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel Release.xcscheme",
    "chars": 2917,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   version = \"1.7\">\n   <BuildAction\n      "
  },
  {
    "path": "Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel.xcscheme",
    "chars": 2948,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   version = \"1.7\">\n   <BuildAction\n      "
  },
  {
    "path": "PearcleanerHelper/CodesignCheck.swift",
    "chars": 4569,
    "preview": "//\n//  CodesignCheck.swift\n//\n//  Created by Erik Berglund on 2018-10-01.\n//  Copyright © 2018 Erik Berglund. All rights"
  },
  {
    "path": "PearcleanerHelper/com.alienator88.Pearcleaner.PearcleanerHelper.plist",
    "chars": 564,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PearcleanerHelper/main.swift",
    "chars": 3896,
    "preview": "//\n//  main.swift\n//  PearcleanerHelper\n//\n//  Created by Alin Lupascu on 3/14/25.\n//\n\nimport Foundation\nimport Objectiv"
  },
  {
    "path": "PearcleanerSentinel/FileWatcher.swift",
    "chars": 7122,
    "preview": "//\n//  FileWatcher.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 9/2/25.\n//\n\nimport Cocoa\nimport Foundation\n\np"
  },
  {
    "path": "PearcleanerSentinel/com.alienator88.PearcleanerSentinel.plist",
    "chars": 507,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PearcleanerSentinel/main.swift",
    "chars": 2186,
    "preview": "//\n//  main.swift\n//  PearcleanerSentinel\n//\n//  Created by Alin Lupascu on 11/9/23.\n//\n\n\nimport AppKit\n\nmain()\n\nvar glo"
  },
  {
    "path": "README.md",
    "chars": 6243,
    "preview": "# Pearcleaner\n\n### Project Status: \n> As you may have noticed, there haven't been recent updates made to the app and I w"
  },
  {
    "path": "Shared/AppGroupDefaults.swift",
    "chars": 591,
    "preview": "//\n//  AppGroupDefaults.swift\n//  Pearcleaner\n//\n//  Created by Alin Lupascu on 9/30/25.\n//\n\nimport Foundation\n\nextensio"
  },
  {
    "path": "announcements.json",
    "chars": 3243,
    "preview": "{\n  \"4.4.0\": {\n    \"features\": [\n      \"Privileged Helper Service: Pearcleaner now supports using a privileged helper se"
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the alienator88/Pearcleaner GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 179 files (15.0 MB), approximately 1.1M tokens, and a symbol index with 26 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.

Copied to clipboard!