Skip to content

feat(scripts): add interactive Homebrew bundle installer - #17

Merged
jackboberg merged 15 commits into
latestfrom
feat/interactive-brew-bundle
Sep 1, 2026
Merged

jackboberg merged 15 commits into
latestfrom
feat/interactive-brew-bundle

Conversation

@jackboberg

@jackboberg jackboberg commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

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-interactive

  • Prompts for each missing Homebrew dependency with y/n/Y/N/q options
  • Uses brew bundle check --global to check against ~/.Brewfile
  • Installs selected entries via temporary Brewfile, avoiding persistent lock files
  • Falls back to non-interactive mode when stdout isn't a tty or BREW_BUNDLE_NONINTERACTIVE=1 is set
  • Compatible with macOS bash 3.2 (no associative arrays)
  • Uses portable mktemp templates for macOS/BSD compatibility

Refactored: Shared script infrastructure

  • Extracted messaging functions (msg, msg_info, msg_warn, etc.) to script/lib/messaging.sh
  • Replaced echo -e with printf '%b\n' for portable output
  • Standardized SCRIPT_DIR pattern using ${BASH_SOURCE[0]} across all scripts
  • All scripts now source dependencies at the top with shellcheck comments
  • Used local variables and cleanup function for temp files in brew-bundle-interactive

Homebrew workflow

  • Brewfile is symlinked to ~/.Brewfile during bootstrap
  • Handles broken symlinks gracefully before creating new ones
  • Uses brew bundle --global for checks and installs
  • Lock files are transient (written to temp directory, cleaned up on exit)
  • Removed tracked Brewfile.lock.json from repo
  • Removed Brewfile.lock.json from dotfile gitignore (no longer needed)
  • Tracks config/homebrew/trust.json to preserve tap trust decisions

Cleanup

  • Fixed pre-existing bug in script/setup where >> was used instead of | for tee

Usage

# Interactive (default when running in a terminal)
script/bootstrap

# Non-interactive (CI or when you want everything installed)
BREW_BUNDLE_NONINTERACTIVE=1 script/bootstrap

# Regenerate ~/.Brewfile from installed packages
brew bundle dump --global

Prompt options

Key Action
Enter or y Yes, install this entry
n No, skip this entry
Y Yes to this and all remaining entries
N No to this and all remaining entries
q Stop selecting and install accepted entries

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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-interactive for per-entry interactive selection with non-interactive fallback.
  • Extracts messaging utilities (msg_*, confirm/affirm, colors) into script/lib/messaging.sh and updates scripts to source shared bootstrap/messaging earlier.
  • Removes the tracked Brewfile.lock.json and 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.

Comment thread script/brew-bundle-interactive Outdated
Comment thread script/lib/messaging.sh
Comment thread gitignore Outdated
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 -.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread script/brew-bundle-interactive Outdated
Comment thread script/bootstrap
- 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)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread script/brew-bundle-interactive
Comment thread script/brew-bundle-interactive
Comment thread script/brew-bundle-interactive
…-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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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/bootstrap runs brew bundle --global to install dependencies, but the implementation calls script/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

Comment thread script/brew-bundle-interactive

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 cleanup after brew_bundle_interactive returns, but tmp/missing are local to the function; at shell exit they are out of scope, so the temp files won’t be removed and ${tmp:-}.lock.json can 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

Comment thread README.md
Comment on lines +30 to +34
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
@jackboberg
jackboberg merged commit 799f5de into latest Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants