Resolved by combining both sides in upgradeSingle (two-param runBrew
closure from fix/offline-refresh + cleanup hook from feature/cleanup)
and adapting the cleanup branch's runBrew calls to the new signature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Cleanup menu item runs brew cleanup; holding ⌥ swaps it for a purge
variant (cleanup --prune=all, which also drops cached downloads of
current versions). A new opt-out-by-default setting interposes a plain
cleanup between the end of any upgrade path and its closing refetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runBrew now reports the process exit status to its completion handler.
refreshAll and fetchOutdated stop on failure and show "Check failed —
see Logs": NWPathMonitor cannot see a network that is up but broken
(captive portal, dead DNS, git host down), while the exit code of the
brew command itself catches every failure mode. Upgrade commands keep
ignoring the flag on purpose — a failed upgrade leaves the package in
the outdated list at the next fetch, so the final state stays correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brew outdated never touches the network: offline, a refresh silently
succeeded on stale local tap data and reported "all up to date". An
NWPathMonitor now gates refreshAll — offline shows a dedicated status
instead, and every offline→online transition triggers a refresh, which
covers the launch refresh, scheduled refreshes skipped during an
outage, and clearing the offline status when the connection returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the premature 1.0 with 0.1 to signal beta status. Document the
0.x version scheme and the brew-never-downgrades caveat in RELEASING.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recent Homebrew removed the --no-quarantine option entirely, so the
install docs and cask now instruct clearing the quarantine attribute
with xattr after install/upgrade. The cask prints it as a caveat at
install time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15.6 was just the Xcode default from project creation, not a real
requirement — the newest API in use is SMAppService (macOS 13). The
compiler enforces the audit: everything builds and tests pass at 13.0.
Release zip rebuilt; cask gets the new sha256 and a :ventura floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The repo doubles as a Homebrew tap: brew tap accepts an explicit git
URL, so Casks/brewbar.rb here is enough — no separate homebrew-* repo.
The cask downloads the zipped Release build from the matching Forgejo
release (v<version>) and verifies it against the pinned sha256. dist/
(local release artifacts) is gitignored.
Install:
brew tap maxsoch/brewbar https://forgejo.socheleau.fr/maxsoch/BrewBar.git
brew install --cask --no-quarantine maxsoch/brewbar/brewbar
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
requestAuthorization only shows the system prompt once ever; when
permission is already denied it silently returns false and the checkbox
just snapped off with no explanation. toggleNotify now checks the
authorization status first: already-authorized enables directly,
not-determined triggers the system prompt (errors surfaced in an
alert), and denied shows an alert with an Open System Settings button
pointing at the Notifications pane.
Also adds a UNUserNotificationCenterDelegate so banners are shown even
while BrewBar is the active app — otherwise macOS suppresses them,
which reads as "notifications don't work" right after enabling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runBrew dispatched onto the global concurrent queue, so overlapping
operations (refresh timer firing mid-upgrade, quick manual actions)
ran multiple brew processes at once — each able to summon its own sudo
password dialog, hence duplicate password windows. A private serial
queue makes every brew command wait for the previous one, so a second
dialog cannot appear while one is open. Log output no longer
interleaves either.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monospaced font and semantic colors (dark-mode correct), visible
vertical scroller (was missing), text inset, centered window with a
proper title. Autoscroll now only follows the tail when already at the
bottom instead of yanking the scroll position on every log line.
Also sets isReleasedWhenClosed = false, fixing a latent crash on
close-then-reopen (the settings window already had this guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Accessory apps never appear in the Cmd-Tab switcher and macOS does not
activate them when they open windows — the log window opened behind
other apps (settings only worked via a manual activate call).
presentWindow(of:) now switches the activation policy to .regular
while a BrewBar window is open, giving a Dock icon and a Cmd-Tab entry;
a willClose observer drops back to .accessory once the last window is
gone. Used by both showLogs and showSettings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batched cask upgrades give no per-package hook, so the password dialog
could only echo the whole command. upgradeCasksSequentially chains one
"upgrade --cask <name>" per outdated cask (from the typed cache), each
with an askpass message naming that cask; the status line follows along
("Upgrading tailscale-app..."). Used by both Upgrade All and
auto-upgrade; formulae stay batched since they don't need sudo.
Explicitly named casks upgrade even when self-updating, so the
per-cask commands need no --greedy. Casks that turn outdated only
after the preceding brew update surface in the final refetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runBrew gains an optional askpassMessage; the dialog reads "BrewBar
needs your administrator password to <message>." and defaults to naming
the brew command. Groundwork for per-cask upgrade prompts that name the
app instead of the command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The askpass script is now rewritten before each brew invocation with
the current command baked into the dialog text ("...to run: brew
upgrade tailscale-app") instead of a generic message written once at
launch. The command text is whitelisted to characters that cannot
break out of the shell/AppleScript quoting it is embedded in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetchOutdated returned before refreshing the UI when auto-upgrade
triggered, so the menu showed stale state for the whole upgrade. Now
the fetched list lands in the menu, status line and menu bar count
first; runAutoUpgrade then overwrites the status with its own message.
Notification stays after the post-upgrade refetch — leftovers only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetchOutdated now uses --json and the typed parser, so cachedOutdated
knows formulae from casks. The auto-upgrade trigger checks each kind
against its own toggle: an outdated cask no longer starts a pointless
"upgrade --formula" run (previously any outdated package triggered
whichever auto-upgrades were enabled). runAutoUpgrade takes explicit
formulae/casks flags instead of re-reading settings.
Bonus from the version info now available: outdated menu items and the
Upgrade All confirmation show "name installed → current" instead of
bare names. The legacy line parser and its tests are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseOutdatedJSON decodes brew outdated --json into OutdatedPackages,
which separates formulae from casks — the plain-text output is just
names with no type information. Codable structs mirror the JSON shape,
with convertFromSnakeCase mapping installed_versions to Swift naming.
Undecodable input (brew error text, empty output) parses as .none.
The legacy line parser stays until callers switch over next commit.
Covered by 4 new unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a note under the greedy checkbox spelling out what self-updating
casks are and that the setting affects both the outdated listing and
upgrades (Upgrade All and auto-upgrade). The indented-gray-note layout
is extracted into a noteRow helper, now shared with the cask warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--greedy exists on both brew commands: outdated --greedy lists
self-updating casks, upgrade --greedy actually upgrades them (plain
upgrade skips them, per brew's own help). The listing and auto-upgrade
paths had the flag but manual Upgrade All didn't, so greedy-listed
casks would survive Upgrade All and reappear as outdated forever.
Single-package upgrades don't need it: brew upgrades an explicitly
named cask even when it self-updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New "Ask for confirmation before Upgrade All" checkbox; when disabled,
Upgrade All runs immediately. Auto-upgrade is unaffected either way —
it never prompts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off by default. The status item button shows both image and title
(imagePosition = .imageLeft), so the icon stays visible with the count
beside it. Cleared when up to date or when the setting is off; toggling
applies immediately from the cached list without refetching.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off by default. Enabling the checkbox requests notification permission
(UNUserNotificationCenter); if denied, the checkbox reverts — the auth
callback arrives on a background queue, so the UI update hops to main.
notifyIfNeeded remembers the last notified set and stays silent while
the outdated list is unchanged, so an hourly refresh doesn't re-ping
about the same packages. The set resets as packages get upgraded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two independent toggles, both off by default. When a scheduled fetch
finds outdated packages, runAutoUpgrade runs "upgrade --formula" and/or
"upgrade --cask" (with --greedy if that setting is on), then refetches.
The refetch passes allowAutoUpgrade: false — packages can legitimately
stay outdated after an upgrade (pinned formulae, greedy casks brew
declines), and without the guard that would trigger upgrade → fetch →
upgrade forever. Manual Upgrade All's final fetch gets the same guard.
The casks checkbox carries a warning note in the settings window: a
cask upgrade can close and replace the running app without warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brew outdated normally skips casks that auto-update themselves; with
includeGreedyCasks enabled (the default) BrewBar passes --greedy so they
show up too. Toggling the checkbox refetches immediately since the list
semantics change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkbox registers/unregisters SMAppService.mainApp (macOS 13+ API, no
helper app needed). Deliberately not mirrored in UserDefaults: the
service status is the single source of truth, and syncUI reads it live
so changes made in System Settings > Login Items stay in sync. Failures
revert the checkbox and surface the error in an alert.
Note: registration points at the app bundle's current path, so it only
behaves properly once BrewBar runs from a stable location like
/Applications — debug builds may register the build folder copy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SettingsWindowController (programmatic NSWindow, same pattern as
LogWindowController) with a preset popup and a custom-minutes field for
the refresh frequency. Changes apply immediately, per macOS convention.
The ⏱ Refresh Interval submenu, its custom text-field-in-menu view and
the two backing actions are removed in favor of "⚙️ Settings…" (Cmd+,).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings enum holds all upcoming settings-window preferences. Booleans
default to false (disabled by default); includeGreedyCasks and
confirmBeforeUpgradeAll default to true via register(defaults:), which
provides fallbacks without writing to disk. Launch-at-login is not
stored here — SMAppService will be its source of truth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same treatment as the Outdated item: upgradeAllItem becomes a property,
starts hidden, and updateOutdatedMenu shows it only when the fetch
returns outdated packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote outdatedItem to a property and toggle isHidden from
updateOutdatedMenu: hidden at launch and whenever a fetch returns no
outdated packages, visible otherwise. The "All up to date" placeholder
inside the submenu is dropped — the status line already says it, and
the item is invisible when the list is empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>