The main Refresh action already refetches everything; the submenu
button duplicated it. Its only handler, forceRefreshOutdated, is
removed too as nothing else referenced it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runBrew built a command string and ran it through zsh -c, so the shell
interpreted the whole line — any metacharacter in an interpolated
package name (;, $(), backticks) would have been executed. Names come
from brew itself so exploitation was unlikely, but the injection class
is now gone: Process gets an argument array via /usr/bin/env, which
resolves brew on PATH (covering the bare "brew" fallback) and execs it
with no shell in between. Also slightly faster per invocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every brew command appended its full output to logBuffer and nothing
ever trimmed it, so with the hourly refresh timer the buffer grew
forever. Memory aside, each log() call re-renders the entire buffer
into the log window's NSTextView, so logging got slower over time.
Keep only the most recent 100k characters, dropping the oldest content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refreshOutdatedList (the 5-second cache path) was never called by any
menu item or code path, so the cache logic never ran. lastOutdatedFetch
was only written. cachedOutdated stays: the Upgrade All confirmation
dialog uses it to list pending packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brew upgrade also upgrades casks, and replacing a cask quits the running
app (e.g. the browser) with no warning. Upgrade All now shows an NSAlert
listing the outdated packages and warning that running apps may be
closed, with a Cancel option.
NSApp.activate is needed because BrewBar is an accessory app (no Dock
icon) — without it the modal alert can appear behind other windows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brew runs inside a Process with no terminal attached, so any sudo prompt
(cask upgrades, some installers) would hang or fail silently — there was
nowhere to type the password.
Set SUDO_ASKPASS to a small helper script written to Application Support
at launch. When sudo detects no tty, it runs the helper, which shows a
native macOS password dialog via osascript and prints the answer for
sudo to consume. Cancelling the dialog makes sudo fail cleanly instead
of hanging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completions were invoked on a background queue. Most call sites wrapped
their UI work in DispatchQueue.main.async, but upgradeSingle's completion
calls refreshAll(), which sets statusMenuItem.title directly — an AppKit
mutation off the main thread. It also raced on cachedOutdated /
lastOutdatedFetch (written from background, read from main).
runBrew now always dispatches its completion to the main queue, so every
caller can safely touch UI, and the now-redundant inner main.async hops
are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract brew output parsing into BrewParser.swift (pure Swift, no AppKit)
and add BrewBarTests target with 7 passing tests covering parseOutdated
and parseVersion edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>