feat(scripts): add interactive Homebrew bundle installer - #17
Conversation
Create script/lib/messaging.sh with shared messaging functions (bootstrap_colors, msg, msg_header, msg_info, msg_warn, msg_error, confirm, affirm) to enable consistent styling across scripts. Update script/bootstrap to source the shared library instead of defining these functions inline.
Add script/brew-bundle-interactive which prompts for each missing Homebrew dependency with y/n/Y/N/q options. Uses brew bundle check to skip already-installed packages and only prompts for missing ones. Update script/bootstrap to use the interactive installer instead of running brew bundle directly.
With interactive per-machine installs, the lock file won't accurately reflect the full Brewfile. Add it to .gitignore to prevent tracking.
- Use SCRIPT_DIR (uppercase) consistently across all scripts - Use BASH_SOURCE[0] pattern for robust path resolution - Source dependencies at the top of each file with shellcheck comments - Simplify execution blocks to only contain entry point - Fix pre-existing bug in setup where >> was used instead of | for tee
There was a problem hiding this comment.
🟡 Changes recommended
The ignore rule for Brewfile.lock.json is added to gitignore instead of the repo’s .gitignore, and the interactive installer’s temp-file cleanup uses global variables/trap behavior that should be tightened to avoid leaking state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an interactive Homebrew Bundle installer to allow selectively installing missing Brewfile entries, and refactors shared messaging helpers to a common library used across scripts.
Changes:
- Introduces
script/brew-bundle-interactivefor per-entry interactive selection with non-interactive fallback. - Extracts messaging utilities (
msg_*,confirm/affirm, colors) intoscript/lib/messaging.shand updates scripts to source shared bootstrap/messaging earlier. - Removes the tracked
Brewfile.lock.jsonand attempts to ignore it going forward.
File summaries
| File | Description |
|---|---|
| script/update | Sources bootstrap up-front and standardizes SCRIPT_DIR resolution. |
| script/setup | Standardizes repo root resolution via SCRIPT_DIR/DOT_DIR and fixes tee piping bug. |
| script/lib/messaging.sh | New shared messaging/color/confirm helpers extracted from script/bootstrap. |
| script/brew-bundle-interactive | New interactive installer that filters Brewfile entries and installs only selected missing items. |
| script/bootstrap | Switches Homebrew dependency install step to the new interactive installer; sources messaging library. |
| gitignore | Adds Brewfile.lock.json to ignore list (note: not the repo’s .gitignore). |
| Brewfile.lock.json | Removes the tracked lock file from the repository. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Make tmp/missing local to brew_bundle_interactive() and use a named cleanup function for the EXIT trap. This prevents state leakage and ensures the trap captures resolved paths at set time.
Replace echo -e with printf '%b\n' in msg() for consistent behavior across environments. echo -e has inconsistent handling of backslashes and inputs starting with -.
There was a problem hiding this comment.
🟢 Approval recommended
The changes are cohesive, the refactor keeps existing call ordering safe, and no verified functional issues were found in the updated scripts.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Lite
The lock file should live in HOME_DIR alongside the symlinked ~/.Brewfile, not in the repo. No value in ignoring it in the dotfile gitignore.
Check commands now use --global to target ~/.Brewfile, while the install step still uses --file=<tmp> to support interactive partial installs. This ensures the lock file is written to HOME_DIR, not the repo. Remove the brewfile argument since checks always target ~/.Brewfile.
During clean installs, rcup hasn't run yet when bootstrap installs Homebrew deps. This ensures ~/.Brewfile exists before running brew-bundle-interactive, so --global checks work correctly.
Document that Brewfile is symlinked to ~/.Brewfile and managed via brew bundle --global. Explain the interactive installer and how to regenerate the Brewfile.
Use SCRIPT_DIR instead of PWD to compute the Brewfile path, making the symlink logic work regardless of working directory or how the script is invoked.
There was a problem hiding this comment.
🟡 Changes recommended
The interactive installer currently uses non-portable mktemp invocation (breaking on macOS/BSD), and the ~/.Brewfile symlink creation can fail on broken symlinks due to -e behavior under set -e.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Lite
- Add template argument to mktemp for macOS/BSD compatibility - Remove broken ~/.Brewfile symlinks before creating new ones
- Commit config/homebrew/trust.json to preserve tap trust decisions - Ignore trust.json.lock (generated by Homebrew)
fa7c1e4 to
14c1217
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new interactive installer has a few confirmed failure modes (missing ~/.Brewfile/brew, sed pass-through, and EOF handling under set -e) that should be fixed to make bootstrapping reliable.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 8/9 changed files
- Comments generated: 3
- Review effort level: Lite
…-bundle-interactive - Add explicit checks for brew and ~/.Brewfile existence before running - Fix sed to use -n flag with explicit p to only emit matched patterns - Handle EOF (Ctrl-D) gracefully in interactive prompts - Fix unbound variable error in cleanup trap when exiting early
There was a problem hiding this comment.
🟡 Changes recommended
The interactive installer’s temp-file cleanup is broken due to EXIT trap scope with local variables, and the README currently misstates the install command behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
README.md:34
- README claims
script/bootstraprunsbrew bundle --globalto install dependencies, but the implementation callsscript/brew-bundle-interactive(which may install via a temporary Brewfile using--file). This is misleading for users trying to understand which command is actually executed.
The `Brewfile` is symlinked to `~/.Brewfile` and managed via `brew bundle --global`.
- `script/bootstrap` runs `brew bundle --global` to install dependencies
- Use `brew bundle dump --global` to regenerate `~/.Brewfile` from installed packages
- The interactive installer prompts for each missing dependency
- Files reviewed: 8/9 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The new interactive installer’s EXIT-trap cleanup currently won’t reliably delete its temp files and can accidentally delete an unrelated .lock.json in the current directory.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
script/brew-bundle-interactive:31
- The EXIT trap calls
cleanupafterbrew_bundle_interactivereturns, buttmp/missingare local to the function; at shell exit they are out of scope, so the temp files won’t be removed and${tmp:-}.lock.jsoncan expand to.lock.json, potentially deleting an unrelated file in the current directory. Capture the filenames in the trap command (or make the variables global) so cleanup is reliable and safe.
trap "rm -f '$tmp' '$missing' '$tmp.lock.json'" EXIT
if [[ ! -t 1 ]] || [[ -n "${BREW_BUNDLE_NONINTERACTIVE:-}" ]]; then
msg_info "==> Installing Homebrew dependencies (non-interactive)"
- Files reviewed: 8/9 changed files
- Comments generated: 1
- Review effort level: Lite
| The `Brewfile` is symlinked to `~/.Brewfile` and managed via `brew bundle --global`. | ||
|
|
||
| - `script/bootstrap` runs `brew bundle --global` to install dependencies | ||
| - Use `brew bundle dump --global` to regenerate `~/.Brewfile` from installed packages | ||
| - The interactive installer prompts for each missing dependency |
Summary
Adds an interactive Homebrew bundle installer that prompts for each missing dependency, allowing selective installation (e.g., skip personal apps on work machines).
Changes
New:
script/brew-bundle-interactivey/n/Y/N/qoptionsbrew bundle check --globalto check against~/.BrewfileBREW_BUNDLE_NONINTERACTIVE=1is setmktemptemplates for macOS/BSD compatibilityRefactored: Shared script infrastructure
msg,msg_info,msg_warn, etc.) toscript/lib/messaging.shecho -ewithprintf '%b\n'for portable outputSCRIPT_DIRpattern using${BASH_SOURCE[0]}across all scriptsbrew-bundle-interactiveHomebrew workflow
Brewfileis symlinked to~/.Brewfileduring bootstrapbrew bundle --globalfor checks and installsBrewfile.lock.jsonfrom repoBrewfile.lock.jsonfrom dotfilegitignore(no longer needed)config/homebrew/trust.jsonto preserve tap trust decisionsCleanup
script/setupwhere>>was used instead of|for teeUsage
Prompt options
EnterorynYNq