feat(installed): Save the environment as a Brewfile - #248
ronanrodrigo wants to merge 2 commits into
Conversation
|
A brewfile is a representation of what's installed. I don't think it makes sense to have buttons for it in the Configuration page. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved findings remain around command display escaping, preview dependency injection, and verifying the action is enabled in UI tests.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Adds a Configuration action to export the Homebrew environment as a Brewfile via brew bundle dump.
Changes:
- Adds bundle-dump command plumbing and operation tracking.
- Adds save-panel UI, result reporting, localization, and accessibility identifiers.
- Adds unit and UI coverage.
| File | Change |
|---|---|
Tests/BrewFeatureConfigTests/ConfigViewModelTests.swift |
Tests Brewfile save behavior and failures. |
Tests/BrewCoreTests/BrewOperationIDTests.swift |
Tests operation classification. |
Tests/BrewCoreTests/BrewCommandsTests.swift |
Tests bundle-dump command construction. |
Tests/BrewAccessibilityIDTests/AXIDTests.swift |
Tests the new accessibility identifier. |
Sources/BrewRepositoryInterfaces/Fakes/Stubs.swift |
Updates command fakes. |
Sources/BrewRepositoryInterfaces/CommandJob.swift |
Handles bundle-dump jobs. |
Sources/BrewFeatureConfig/Views/ConfigView.swift |
Adds the save action and result UI. |
Sources/BrewFeatureConfig/Views/ConfigColumnsRoot.swift |
Provides command dependencies. |
Sources/BrewFeatureConfig/Views/BrewfileSavePanel.swift |
Provides destination selection. |
Sources/BrewFeatureConfig/ViewModels/ConfigViewModel+Sections.swift |
Adds save availability state. |
Sources/BrewFeatureConfig/ViewModels/ConfigViewModel.swift |
Runs exports and tracks outcomes. |
Sources/BrewFeatureConfig/Resources/Localizable.xcstrings |
Adds export-related strings. |
Sources/BrewCore/Operations/BrewOperationModels.swift |
Adds the bundle-dump operation kind. |
Sources/BrewCore/Operations/BrewMutatingCommandFactory.swift |
Extends command construction. |
Sources/BrewCore/Operations/BrewCommands.swift |
Builds bundle-dump arguments. |
Sources/BrewCore/Operations/BrewCommand.swift |
Renders display commands. |
Sources/BrewCLI/LiveBrewMutatingCommandFactory.swift |
Implements live command creation. |
Sources/BrewAppEnvironment/UnimplementedRepositories.swift |
Updates the fallback factory. |
Sources/BrewAccessibilityID/AXID.swift |
Defines the save-button identifier. |
Package.swift |
Adds test dependencies. |
BrewUITests/Tests/ConfigUITests.swift |
Covers the Configuration action. |
BrewUITests/Screens/ConfigScreen.swift |
Adds UI assertion support. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var displayCommand: String { | ||
| "brew " + arguments.joined(separator: " ") | ||
| } |
There was a problem hiding this comment.
Addressing this in the next commit: shell-quote each displayed argument and add coverage for a path containing spaces.
I use it to keep a snapshot of my environment, making it easier to use the same tools across different Macs or set up a new one, especially during the initial setup based on a well-defined/known configuration. |
|
Sure, but it's a snapshot of what's installed. Not of the configuration of your Homebrew install. |
The Installed header gains a Save Brewfile… action that writes the installed formulae, casks and taps to a Brewfile, so the environment can be snapshotted, backed up or shared without leaving the app. The dump runs brew bundle dump --file=… --force through the existing mutating command pipeline as a new .bundleDump operation, so it streams in the console like every other brew invocation, and the header reports the destination or the failure once it finishes.
539458f to
b81ae0c
Compare
|
Fair point — moved. The action is now in the Installed header next to the package count, so it dumps the list you are looking at, and Configuration is back to what it was. Commit, body and screenshots (before/after of that header) updated. |
graeme
left a comment
There was a problem hiding this comment.
@ronanrodrigo : This isn't really a fundamental enough feature to have it front and centre of the main tab. How about sticking it in the File menu? That's where these sort of "export to file" features usually belong on macOS.

What and why
BrewUI reads the environment out of Homebrew but cannot write it back: to snapshot, back up or share
what is installed, a user has to leave the app and run
brew bundle dumpin a terminal. This adds aSave Brewfile… action to the Installed header, where the installed set already lives.
Save Brewfile…button beside "Your packages", disabled until theinventory has loaded — there is nothing to dump before that. It asks for a destination in a save
panel defaulting to
~/Brewfile, then runsbrew bundle dump --file=… --forcethrough the sharedcommand center, so the run streams in the console like every other
brewinvocation. The headerthen reports where the file landed, or the failure, carrying the same message the console holds.
than the Configuration tab — which is what review asked for here, and what feat: export installed packages as a Brewfile #210 did.
--force. The save panel already asked about replacing an existing file, so the dump need notask again. Cancelling the panel is not a failure and leaves the screen untouched.
BrewOperationKind.bundleDumpreusing the existing mutatingpipeline (
BrewMutatingCommandFactory→SerialBrewCommandCenter→ console), so it behaves likeuninstall and upgrade: one operation at a time, cancellable, logged.
Prior art
#210 (closed) added the same feature to the Installed header. This branch was written independently
and first put the action on Configuration; review moved it to the Installed header. Nothing from #210
was copied — the implementation follows the current command pipeline.
Validation
macOS 27.0 (26A428) · Apple M4 (arm64) · Xcode 27.0 (27A266) · Swift 6.4.
swift build --build-testsxcrun swift test --package-path .DiscoverListRowViewModelTestsandDiscoverPackageDetailViewModelTestsexpect12,345/3,500and get12.345/3.500on this machine (AppleLocale = en_BR). Both reproduce on a cleanorigin/mainworktree, so they are not from this change. EveryBrewFeatureInstalled,BrewCoreandBrewAccessibilityIDsuite passes.mint run swiftformat --lintmint run swiftlint lint --strictHomebrew HomebrewUpgradeHelper Sourcesscripts/localize verifyscripts/test-uixcodebuild test -scheme Brew-UIwith a manual ad-hoc identity: on this machinescripts/test-uiaborts before the first test (troubleshooting item 8 in AGENTS.md —CODE_SIGNING_ALLOWED=NOinvalidates the runner's signature).scripts/annotate-flaky-testsTwo notes on the tooling, neither caused by this change:
The catalog keys were added by hand, because
scripts/localize synccannot see package*.stringsdataon this machine (itsfindlooks forTarget.build, while the app build emitsTarget-b.build). The three new keys were inserted in the tool's own format and the result checkedwith
scripts/localize verify.scripts/test-uipassing here took thexcodebuildinvocation above rather than the script.I followed the conventions and workflow, checked for duplicate PRs and kept this change focused.
I added regression coverage for bug fixes or explained why automated coverage is impractical, and reported the relevant validation above.
Screenshots
The "before" image comes from building this branch with the header's button and result note removed,
which leaves the header exactly as
mainrenders it. The "after" image is in the machine's darkappearance; the same header in light appearance is
here.
AI assistance
Written by Hermes Agent (Nous Research) running
deepseek/deepseek-v4.1-flashthroughOpenRouter, under Ronan's direction: the implementation, tests, catalog entries, screenshots and
every command in the table above. The human reviewed the diff and supplied the framing. No manual
accessibility or keyboard pass was performed, as noted in Validation.