Skip to content

me-cedric/BrewMatch

Repository files navigation


BrewMatch

Safety-first macOS app scanner for Homebrew Cask migration.

CI Status MIT License Swift 6.3 macOS 14+ Homebrew Cask


BrewMatch is a SwiftPM macOS CLI that scans installed .app bundles and reports which manually installed apps may have a Homebrew Cask replacement. It can also export a suggested Brewfile for review.

It is designed for local-first inventory and migration planning. It is dry-run by default; the only opt-in mutation path is the guarded Homebrew adopt command.

Status

BrewMatch is early pre-1.0 software. Current version: 0.8.0.

The scanner, report output, ignore file, and Brewfile suggestion export are functional and covered by unit tests. Matching remains heuristic and should be reviewed before running brew bundle.

Area Current state
App scanning Reads /Applications and ~/Applications, one level deep.
Metadata Parses app Info.plist, MAS receipts, and local Homebrew Cask metadata.
Matching Uses bundle identifiers, cask artifact names, normalized names, prefix matches, and fuzzy fallback.
Reports Text and JSON scan reports with summary counts and warnings.
Brewfile export Suggestion-only output for high-confidence matches by default.
App changes Dry-run by default. Guarded adoption command exists for one explicit Homebrew adopt command only.

Safety

  • Dry-run by default.
  • No direct app delete, move, or modify actions.
  • The only mutation path is an explicit brew install --cask --adopt <token> call after all adopt safety gates pass.
  • No credentials.
  • No telemetry.
  • No network calls except local brew commands.
  • Missing Homebrew is OK: BrewMatch still scans apps and reports matching as unavailable.

Features

  • Scans installed macOS .app bundles.
  • Detects likely system apps and App Store apps.
  • Detects Homebrew-managed casks when Homebrew is available.
  • Finds possible Homebrew Cask replacements for manually installed apps.
  • Produces plain text and JSON reports.
  • Supports ignore files by bundle identifier, app name, or absolute path.
  • Exports suggested Brewfile content for review.
  • Keeps ambiguous suggestions commented, never active.
  • Checks local readiness with brewmatch doctor.

Install

Build from source:

git clone https://github.com/me-cedric/BrewMatch.git
cd BrewMatch
swift build -c release

Install the release binary locally with the helper script:

./scripts/install-release.sh

Preview install without copying:

./scripts/install-release.sh --dry-run

Install into a custom prefix or bin directory:

./scripts/install-release.sh --prefix /usr/local
./scripts/install-release.sh --prefix "$HOME/.local"
./scripts/install-release.sh --bin-dir "$HOME/bin"

The installer builds with SwiftPM, copies only the brewmatch binary, refuses to overwrite unless --force is passed, and never uses sudo automatically. If the destination is not writable, it prints a manual copy command for you to review.

Manual local install:

cp .build/release/brewmatch /usr/local/bin/brewmatch

Uninstall the local binary:

./scripts/uninstall-local.sh
./scripts/uninstall-local.sh --dry-run

The uninstall script removes only a file named exactly brewmatch at the configured path and never removes directories or uses sudo automatically.

Generate a local checksum:

./scripts/checksums.sh
cat dist/*.sha256

Package local release artifacts:

./scripts/package-release.sh
(cd dist && shasum -a 256 -c brewmatch_Darwin_arm64.sha256)

Safer local test before installing:

.build/release/brewmatch scan

Run from SwiftPM:

swift run brewmatch scan

Use built binary:

.build/release/brewmatch scan

No official Homebrew cask or tap exists yet. See docs/homebrew-tap-prep.md for draft tap preparation notes.

Homebrew Tap Status

No official Homebrew tap is published yet. Use GitHub Release artifacts or the local install script until a tap is published.

Maintainer-only formula, tap scaffold, and manual publish plan tooling exists for preparation:

./scripts/package-release.sh
SHA256="$(./scripts/extract-release-sha.sh --file dist/brewmatch_Darwin_arm64.sha256)"
./scripts/generate-homebrew-formula.sh --version 0.8.0 --sha256 "$SHA256" --output dist/homebrew/brewmatch.rb --force
./scripts/validate-homebrew-formula.sh --formula dist/homebrew/brewmatch.rb --version 0.8.0 --sha256 "$SHA256"
./scripts/scaffold-homebrew-tap.sh --version 0.8.0 --sha256 "$SHA256" --output-dir dist/homebrew-tap --force
./scripts/validate-homebrew-tap.sh --tap-dir dist/homebrew-tap --version 0.8.0 --sha256 "$SHA256"
./scripts/plan-homebrew-tap-publish.sh --version 0.8.0 --tap-repo me-cedric/homebrew-brewmatch --sha256 "$SHA256" --output dist/homebrew-tap-publish-plan.txt --force
./scripts/validate-homebrew-tap-publish-plan.sh --plan dist/homebrew-tap-publish-plan.txt --version 0.8.0 --tap-repo me-cedric/homebrew-brewmatch --sha256 "$SHA256"

v0.9 adds publish plan generation only. These scripts do not run brew, publish a tap, initialize git, or push to a tap repository. See docs/homebrew-formula.md, docs/homebrew-tap-scaffold.md, and docs/homebrew-tap-publish-plan.md.

Commands

Command Purpose
brewmatch doctor Check local readiness without modifying apps.
brewmatch doctor --json Print machine-readable doctor checks.
brewmatch scan Scan apps and print a text report.
brewmatch scan --json Scan apps and print JSON.
brewmatch scan --include-system Include /System/Applications in scan roots; system apps remain skipped.
brewmatch scan --output report.json Export JSON report based on .json extension.
brewmatch report --output report.txt --force Export text report and overwrite existing file.
brewmatch report --explain Include confidence reasoning and candidate match reasons.
brewmatch brewfile Print suggested Brewfile content.
brewmatch brewfile --with-comments --output Brewfile Export commented Brewfile suggestions.
brewmatch suggestions Alias for brewmatch brewfile --with-comments.
brewmatch plan Print a dry-run migration plan. No actions are executed.
brewmatch plan --strict Include only low-risk proposed entries; move other candidates to review.
brewmatch plan --explain Include detailed reasoning, source classification, and risk notes.
brewmatch plan --with-commands Include proposed commands as dry-run text only.
brewmatch adopt Dry-run adopt planning. No actions are executed by default.
brewmatch adopt --cask firefox Show the selected dry-run adoption candidate.
brewmatch adopt --cask firefox --dry-run Explicit dry-run; same as default.
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system Execute only if every safety gate and preflight check passes.

suggestions is an alias for brewfile --with-comments.

Report export:

brewmatch scan --output report.json
brewmatch report --output report.txt --force

JSON output includes raw app fields, match reasons, warnings, and summary counts:

brewmatch scan --json

Doctor

brewmatch doctor checks local readiness without modifying applications.

brewmatch doctor
brewmatch doctor --json
brewmatch doctor --json --output doctor.json --force

Doctor checks macOS version, architecture, Homebrew path/version, cask metadata support, app directory readability, ignore file validity, temporary output writing, and whether an app scan can run. It is read-only for applications and does not install, adopt, delete, move, or modify apps. It may create and remove one temporary file in the system temp directory for the write check.

Doctor exits 0 when checks pass with warnings or better, and non-zero when a check fails. See docs/doctor-json-schema.md for JSON fields.

Scan And Report

brewmatch report --explain adds confidence details:

brewmatch report --explain

Explain output names the match source: bundle identifier, cask artifact app name, cask token, normalized name, or fuzzy fallback.

brewmatch scan --include-system adds /System/Applications to scan roots. System apps are still classified as skipped system apps and are never adopt candidates.

Brewfile Export

By default, Brewfile export includes only high-confidence, non-ambiguous cask matches:

# BrewMatch suggested Brewfile
# Generated from local macOS application scan
# Review before running brew bundle
# Active casks are high-confidence non-ambiguous suggestions by default

cask "firefox"

Options:

  • --include-medium
  • --include-low
  • --include-ambiguous
  • --with-comments
  • --no-header
  • --output <path>
  • --force
  • --ignore-file <path>

Ambiguous matches are commented when included:

# Ambiguous: Cursor.app
# candidate: cursor confidence: medium reason: normalized app name match
# candidate: cursor-cli confidence: medium reason: token prefix match
# cask "cursor"

Migration Plan

brewmatch plan prepares a dry-run migration plan for apps that may be movable under Homebrew management later.

It does not run brew install --cask --adopt, install casks, delete apps, move apps, or modify apps.

brewmatch plan
brewmatch plan --json
brewmatch plan --strict
brewmatch plan --strict --with-commands
brewmatch plan --explain
brewmatch plan --with-commands
brewmatch plan --output plan.json --json --force
brewmatch plan --json --output plan.json --force

Every plan includes:

No actions will be executed.

Risk levels:

  • low: high confidence from an exact bundle identifier match.
  • medium: high confidence from app name, artifact name, or token matching.
  • review-required: medium or low confidence, ambiguous candidates, App Store apps, and system apps.

Plan statuses:

  • proposed: low/medium risk high-confidence candidates, unless --strict excludes medium risk.
  • reviewRequired: candidates that should be manually checked.
  • skipped: system, App Store, ignored, already managed, no-match, or threshold-excluded apps.

--strict keeps only low-risk entries as proposed and marks medium-risk candidates as review-required with excluded by strict mode.

Exact adopt commands are shown only when --with-commands is passed. Proposed entries render active command text. Review-required entries render commented command text:

# review required: brew install --cask --adopt cursor

These commands are never executed by BrewMatch. See docs/plan-json-schema.md for machine-readable plan fields.

When --with-commands is passed, plan output also includes Copyable commands with only low-risk proposed commands. Review-required commands stay commented and out of that section.

Adopt

brewmatch adopt is a guarded foundation for future Homebrew Cask adoption. Default behavior is dry-run.

brewmatch adopt
brewmatch adopt --cask firefox
brewmatch adopt --cask firefox --dry-run
brewmatch adopt --app Firefox.app
brewmatch adopt --json --output adopt.json --force
brewmatch adopt --cask firefox --audit-log adopt-audit.json
brewmatch adopt --cask firefox --require-clean-plan --explain
brewmatch adopt --cask firefox --with-commands
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system --require-clean-plan --audit-log ./adopt-audit.json

Dry-run output may show the exact command that would be used:

brew install --cask --adopt firefox

BrewMatch never deletes or moves apps directly. Real execution only shells out to:

brew install --cask --adopt <token>

BrewMatch does not run that command unless all safety gates pass:

  • --execute is present.
  • --dry-run is not present.
  • Exactly one cask token or app selector is provided.
  • Selected entry is proposed.
  • Selected entry is low risk.
  • Selected match confidence is high.
  • Selected app is not ignored, not App Store, not system, and not ambiguous.
  • Confirmation phrase exactly matches --confirm "adopt <token>".
  • User also passes --i-understand-this-may-change-my-system.
  • If stdin is interactive, user types exact final prompt response ADOPT.
  • Command arguments are exactly ["install", "--cask", "--adopt", "<token>"].

Before execution, BrewMatch also runs preflight checks:

  • Homebrew is available.
  • The selected cask token resolves through Homebrew metadata or cask search.
  • The app still exists at the scanned path.
  • The app is not already Homebrew-managed.
  • The bundle identifier still matches the scanned app, when available.

--require-clean-plan adds stricter execution gates. It blocks execution when the current scan has review-required entries, warnings, or the selected app has alternative candidates. In dry-run mode, --explain shows this gate without executing.

--audit-log <path> writes a JSON audit object for dry-run, blocked, and executed runs. It refuses to overwrite existing files unless --force is passed.

--with-commands adds Copyable commands for low-risk proposed dry-run commands only. Review-required commands are excluded.

Blocked or dry-run output always includes:

No actions were executed.

Ignore File

Default path:

~/.config/brewmatch/ignore.json

Supported formats:

[
  "com.example.CustomApp",
  "Custom App.app",
  "/Applications/Custom App.app"
]

Or grouped:

{
  "bundleIdentifiers": ["com.example.CustomApp"],
  "names": ["Custom App.app"],
  "paths": ["/Applications/Custom App.app"]
}

Ignored apps stay visible in reports under Ignored.

Architecture

BrewMatch/
├── Sources/BrewMatch/
│   ├── AppScanner.swift          # read-only .app discovery and Info.plist parsing
│   ├── BrewClient.swift          # local brew command wrapper and cask metadata parsing
│   ├── Matcher.swift             # cask confidence scoring
│   ├── Reporter.swift            # scan report assembly and text/JSON rendering
│   ├── BrewfileRenderer.swift    # suggestion-only Brewfile output
│   ├── MigrationPlan.swift       # dry-run migration planning and plan JSON
│   └── main.swift                # CLI entry point
└── Tests/BrewMatchTests/

Core boundaries:

  • AppScanner only reads filesystem metadata.
  • BrewClient only calls local brew commands and returns parsed data.
  • Matcher is pure matching logic.
  • Reporter and BrewfileRenderer turn scan results into output.
  • Tests use mocked Homebrew clients and temporary directories.

Testing

swift package describe
swift test
swift build

Unit tests do not require Homebrew to be installed.

Shortcut targets:

./scripts/validate.sh
./scripts/smoke-release.sh
make validate
make smoke
make build
make test
make clean

Release Builds

The Release Build GitHub Actions workflow builds release artifacts on tag pushes matching v*.*.* and on manual runs. It uploads:

  • brewmatch
  • brewmatch_Darwin_arm64.tar.gz
  • brewmatch_Darwin_arm64.sha256

Tag pushes may create or update a GitHub Release object with the tarball and checksum. Manual workflow runs do not create a GitHub Release unless create_release is explicitly set to true. The workflow does not publish a Homebrew tap and does not require custom secrets.

Local distribution helpers:

./scripts/package-release.sh
./scripts/verify-release-version.sh --version 0.8.0 --binary dist/brewmatch
./scripts/install-release.sh --dry-run
./scripts/uninstall-local.sh --dry-run

Checksum verification:

cd dist
shasum -a 256 -c brewmatch_Darwin_arm64.sha256

Draft release notes live in docs/release-notes. Artifact details live in docs/release-artifacts.md. Generated dist/ artifacts are ignored by git.

Current Limitations

  • Matching is heuristic.
  • Homebrew metadata depends on local brew info --json=v2 --cask.
  • Scanner only searches /Applications and ~/Applications, one level deep.
  • Brewfile output is suggestion-only. Review before running brew bundle.

Roadmap

  • More cask metadata fields.
  • Better duplicate app grouping.
  • Optional config file.
  • GUI later, after CLI behavior stabilizes.

Contributing

Contributions are welcome. Read CONTRIBUTING.md before opening a PR.

Keep changes aligned with the read-only safety model:

  • no app modification,
  • no telemetry,
  • no secrets,
  • no Homebrew dependency in tests.

Security

BrewMatch reports may include app names, bundle identifiers, versions, and local paths. Treat JSON reports as potentially sensitive and sanitize them before sharing.

See SECURITY.md for reporting guidance.

Keywords

macOS, Swift, SwiftPM, command line, CLI, Homebrew, Homebrew Cask, Brewfile, brew bundle, application inventory, app migration, local-first, privacy, read-only.

License

MIT. See LICENSE.