Architecture
Brewwery 1.1.1 is a native macOS application written in Swift 6 with SwiftUI, AppKit, and Foundation. There is no Electron, Node.js, Rust, web view, or JavaScript bridge. The native 1.x app replaces the 0.9.x Electron app.
Overview
┌───────────────────────────────────────────────┐
│ Brewwery (app target) │
│ SwiftUI views · @Observable feature models │
│ AppKit: menu bar item, Dock badge, windows │
│ Sparkle: application updates │
├───────────────────────────────────────────────┤
│ BrewweryCore (library, no UI) │
│ HomebrewClient actor │
│ HomebrewCommand typed, validated argv │
│ HomebrewRunner actor, streaming │
│ OperationCoordinator one mutation at a time │
│ Parsers · Models · Persistence · Errors │
└───────────────────────────────────────────────┘
│ posix_spawn, no shell
Homebrew (brew)Project structure
Sources/
├── BrewweryCore/ # no UI — testable on its own
│ ├── Homebrew/ # detection, typed commands, validation, runner, coordinator
│ ├── Models/ # Formula, Cask, OutdatedPackage, BrewService, …
│ ├── Parsers/ # Homebrew JSON and text output, fixture-tested
│ ├── Persistence/ # settings, history, favorites
│ └── Errors/ Logging/ Utilities/
└── Brewwery/ # the app
├── App/ # entry point, delegate, environment, menus, operations
├── DesignSystem/ # palette, metrics, components, tables, state panels
├── Features/ # Shell (sidebar, status bar, search) and one folder per page
├── Models/ # @Observable feature models
└── Catalogs/ # bundled Discover, command, and Brewfile data
Tests/ # core, app, live, and shared test-support targets
Packaging/ # Info.plist, entitlements, logo sources
Scripts/ # app bundle, release, and screenshot scriptsThe project is a Swift Package; open Package.swift in Xcode or use swift build.
Layers
Views and feature models
Each page is a SwiftUI view backed by an @Observable, @MainActor model — PackageLibrary, UpdatesModel, ServicesModel, and so on — owned by a single AppEnvironment. Models load data through HomebrewClient and publish loading, error, and empty states that the views render.
HomebrewClient
An actor that exposes one async method per Homebrew operation (search(_:), outdated(), services(), doctor(), …). It builds a HomebrewCommand, runs it, and turns the output into models with the parsers. Homebrew output that changed between versions is handled here — for example, falling back to brew list --versions --json when --json=v2 is rejected.
HomebrewCommand
A closed enum with one case per supported command. Each case validates its dynamic values with HomebrewIdentifier and returns a fixed argument list. Nothing else in the app can produce arguments for brew. See Security for the complete list and the validation rules.
HomebrewDetector
Finds brew in this order: a saved custom path, /opt/homebrew/bin/brew, /usr/local/bin/brew, then brew on PATH. A custom path is only used after it passes validation.
HomebrewRunner and ChildProcess
Runs brew with posix_spawn in its own process group, with the environment Homebrew needs plus HOMEBREW_NO_AUTO_UPDATE=1 and HOMEBREW_NO_ANALYTICS=1. Short commands capture their output; long operations stream standard output and standard error as an AsyncStream. Cancelling sends SIGTERM to the whole process group and SIGKILL five seconds later if needed, so child processes such as curl and git stop too.
Buffered commands are cancellable and enforce a 16 MB limit per pipe, returning an explicit error rather than truncated JSON. Reads have a 60-second timeout, short mutations five minutes, and custom-path probes ten seconds. Streaming pipes and pending UI output are bounded. The runner independently refuses overlapping mutations and waits for the child to exit before releasing its slot.
OperationCoordinator
Owns installs, uninstalls, upgrades, service actions, cleanup, metadata updates, tap changes, and pinning. It reserves a mutation slot before Homebrew detection starts, enforces operation timeouts, collects bounded output, and records results in History. Lists refresh after failures and cancellations as well as success. The guard is process-local: it does not serialize commands started from Terminal or another app.
Persistence
| Store | Backing |
|---|---|
SettingsStore | UserDefaults (com.brewwery.app) |
HistoryStore | ~/Library/Application Support/Brewwery/history.json, newest 100 entries |
FavoritesStore | ~/Library/Application Support/Brewwery/favorites.json |
JSONPersistence coordinates snapshots on the main actor. JSON encoding, recovery copies, and atomic file writes execute on one serial utility queue. Each store keeps at most one in-flight write and the latest pending snapshot; a completed older write cannot erase a newer pending change.
Normal quit waits for pending saves using AppKit’s deferred termination reply. On failure, changes stay in memory and the UI offers Retry; quitting then requires a choice to discard them. Unreadable JSON is preserved as an .unreadable-<UUID>.bak file before replacement. Force-quit and power loss can still lose unsaved changes. Initial JSON loading and manual History export remain synchronous.
AppKit integration
AppDelegate installs the menu bar item, refreshes the Dock badge from the shared outdated list, and brings the main window forward from the menu. Application updates go through AppUpdateController. Sparkle requires a configured app bundle with a feed URL and public key; isolated QA profiles disable it. Distributed updates carry both Developer ID and Sparkle EdDSA signatures.
Errors
Every failure is a BrewweryError with a stable code, a user-facing message, and the raw output when there is one. Common codes:
| Code | Meaning |
|---|---|
HOMEBREW_NOT_FOUND | No working Homebrew executable was found |
BREW_COMMAND_FAILED | Homebrew exited with an error |
BREW_JSON_PARSE_FAILED | Homebrew’s JSON output could not be read |
INVALID_PACKAGE_NAME, INVALID_CASK_TOKEN, INVALID_TAP_NAME, INVALID_SERVICE_NAME, INVALID_FILE_PATH | Input failed validation and was never sent to Homebrew |
PACKAGE_INSTALL_FAILED, PACKAGE_UNINSTALL_FAILED, SERVICE_COMMAND_FAILED, CLEANUP_RUN_FAILED, … | A specific operation failed |
OPERATION_IN_PROGRESS | Another operation that changes Homebrew is still running |
OPERATION_CANCELLED | You cancelled the operation |
OPERATION_TIMEOUT | The operation exceeded its safety timeout |
BLOCKED_EXTERNAL_URL | A link was outside the allowlist |
Tech stack
| Area | Technology |
|---|---|
| Language | Swift 6 with strict concurrency |
| UI | SwiftUI, with AppKit for the menu bar, Dock, and windows |
| State | Observation (@Observable) |
| Concurrency | Swift actors and AsyncStream |
| Processes | posix_spawn with process groups |
| Updates | Sparkle 2 |
| Logging | os.Logger, subsystem com.brewwery.app |
| Tests | XCTest |
| Build | Swift Package Manager; universal arm64 + x86_64 |
| Minimum OS | macOS 14 Sonoma |