Skip to content

[BUG] pear CLI uses a separate UserDefaults domain from the app #590

Open

Description

@TyceHerrman

New Issue Checklist

  • I updated Pearcleaner to the latest version and still observe the issue
  • I searched existing issues, pull requests, and discussions
  • OS Version: macOS 27.0
  • Pearcleaner Version: 5.4.3 (121)

Issue:

/usr/local/bin/pear and the app don't use the same preferences domain.

The app uses:

com.alienator88.Pearcleaner.plist

The symlinked CLI uses:

pear.plist

As a result, pear list-orphaned doesn't see exclusions added through Settings.

Steps:

  1. Add a path to the orphan exception list in Settings.

  2. Confirm it's stored in the app domain:

    defaults read com.alienator88.Pearcleaner settings.folders.zombie
  3. Run both forms of the executable:

    /usr/local/bin/pear list-orphaned
    /Applications/Pearcleaner.app/Contents/MacOS/Pearcleaner list-orphaned

The symlinked CLI includes the excluded path. The direct app executable omits it.

The installed CLI points to that same executable:

$ readlink /usr/local/bin/pear
/Applications/Pearcleaner.app/Contents/MacOS/Pearcleaner

As a control, copying the same settings.folders.zombie array into the pear domain made /usr/local/bin/pear omit the excluded path. I restored the test preference afterward.

Source

FolderSettingsManager loads the folder settings from UserDefaults.standard:

class FolderSettingsManager: ObservableObject {
static let shared = FolderSettingsManager()
@Published var folderPaths: [String] = []
@Published var fileFolderPathsZ: [String] = []
@Published var fileFolderPathsApps: [String] = []
private let appsKey = "settings.folders.apps"
private let zombieKey = "settings.folders.zombie"
private let appsExclusionKey = "settings.folders.appsExclusion"
let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"]
init() {
loadDefaultPathsIfNeeded()
}
// Application folders //////////////////////////////////////////////////////////////////////////////////
private func loadDefaultPathsIfNeeded() {
var appsPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths
let zombiePaths = UserDefaults.standard.stringArray(forKey: zombieKey) ?? []
let appsExclusionPaths = UserDefaults.standard.stringArray(forKey: appsExclusionKey) ?? []
if appsPaths.count < 2 {
appsPaths = defaultPaths
}
UserDefaults.standard.set(appsPaths, forKey: appsKey)
self.folderPaths = appsPaths
self.fileFolderPathsZ = zombiePaths
self.fileFolderPathsApps = appsExclusionPaths

The CLI creates that manager during launch:

// Process CLI // ========================================================================================================
func handleLaunchMode() {
var arguments = CommandLine.arguments
// Filter out arguments that break CLI commands on startup
arguments = arguments.filter {
!["-NSDocumentRevisionsDebugMode", "YES", "-AppleTextDirection", "NO"].contains($0)
}
if let langIndex = arguments.firstIndex(of: "-AppleLanguages"), langIndex + 1 < arguments.count {
arguments.remove(at: langIndex)
arguments.remove(at: langIndex)
}
let termType = ProcessInfo.processInfo.environment["TERM"]
let isRunningInTerminal = termType != nil && termType != "dumb"
// Check if any CLI command arguments are present (even if not in terminal)
// This handles cases like SUDO_ASKPASS where the app is launched without a terminal
let cliCommands = ["uninstall", "list", "search", "help", "ask-password"]
let hasCLICommand = arguments.dropFirst().contains { arg in
cliCommands.contains(arg) || arg.hasPrefix("--")
}
if isRunningInTerminal || hasCLICommand {
let locations = Locations()
let fsm = FolderSettingsManager()
PearCLI.setupDependencies(locations: locations, fsm: fsm)
do {
// Drop the program name as to not interfere with argument parsing
let args = Array(arguments.dropFirst())
var command = try PearCLI.parseAsRoot(args)
// Run the command if no errors in parsing were caught
try command.run()
} catch {
PearCLI.exit(withError: error) // Cli exit
}

The orphan search reads its exclusion list from the manager:

// Cached formatted data to avoid repeated .pearFormat() calls
private lazy var formattedExclusionList: [String] = {
fsm.fileFolderPathsZ.map { $0.pearFormat() }
}()

The same initializer loads settings.folders.apps and settings.folders.appsExclusion. I only directly tested settings.folders.zombie.

Using an explicit shared suite for these settings would avoid the domain changing with the executable's invocation name.

Related

Screenshots:

Not applicable.

Debug Console

Not applicable.

Console Logs

Not applicable.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions