Skip to content

Fresh-Mac installer with tool profiles - #16

Merged
drewjs merged 7 commits into
mainfrom
fresh-mac-installer
Oct 17, 2026
Merged

drewjs merged 7 commits into
mainfrom
fresh-mac-installer

Conversation

@drewjs

@drewjs drewjs commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Before: packages are stowed by hand, and there is no installer.

After: one command installs the chosen tools and stows only those configs.

Base is the manifest registry. Grafts are the external selection file, --audit, and HOMEBREW_NO_AUTO_UPDATE=1. Chezmoi and per-tool cards were rejected.

On a Mac that already has configs, a conflicting file or a symlink that does not already point into this checkout is moved aside and that path is replaced by the repo link (or by a real directory, when a directory symlink was in the way). Nothing is deleted. The previous file or symlink is kept under ~/.dotfiles-backup/<YYYYmmdd-HHMMSS>-<pid>/. Links that already point into this checkout are left alone. Deselecting a tool does not unlink or uninstall it.

Open in Web Open in Cursor 

cursoragent and others added 7 commits October 17, 2026 22:17
tools.txt lists each tool's Homebrew packages, stow package, needs, and
profiles. install.sh resolves a selection from it (--profile, --only,
--skip, --pick), remembers it per machine in the gitignored tools.local,
installs missing packages, backs up anything in the way, and stows.

Co-authored-by: drewjsdev <drewjsdev@pm.me>
The full job removes the runner's Homebrew and installs everything
through the curl bootstrap. The subset job installs --only nvim,skhd
over the preinstalled Homebrew. verify-install.sh seeds conflicting
files, then checks packages, links, backups, that nothing outside the
selection is installed or linked, and that a bare rerun changes
nothing by before/after snapshot.

Co-authored-by: drewjsdev <drewjsdev@pm.me>
Co-authored-by: drewjsdev <drewjsdev@pm.me>
Co-authored-by: drewjsdev <drewjsdev@pm.me>
Save --profile, --only, and --skip at ~/Library/Application Support/dotfiles/selection
so a bare rerun and a re-clone repeat that request. Add --audit, and export
HOMEBREW_NO_AUTO_UPDATE=1 so a brew install does not refresh Homebrew.

Co-authored-by: drewjsdev <drewjsdev@pm.me>
@claude

claude Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review

Solid work. The manifest design, bash 3.2 discipline, backup-not-delete behavior, and the black-box CI verifier are all good. I read the diff only and did not run it. Findings below, most important first.

Issues

  1. --list on a bare curl run has side effects. main hits bootstrap whenever no tools.txt sits beside the script. That installs Homebrew and clones the repo before --list ever runs. --dry-run and --audit are guarded, but --list and --help-like read-only flags should be too. Add [ "$LIST" = 0 ] || die "--list needs a checkout".
  2. HERE resolves to the cwd under bash -c. BASH_SOURCE[0] is empty and $0 is install.sh, so dirname gives .. If the user runs the curl one-liner from any directory that happens to contain a tools.txt, the script treats that directory as the repo. Prefer requiring [ -f "$BASH_SOURCE" ], or checking for install.sh beside tools.txt, before trusting HERE.
  3. Selection is saved before the install succeeds (save_state runs ahead of ensure_brew and install_packages). If brew fails, the next bare run silently repeats a request that never completed. Consider saving at the end, or accept it and document it.
  4. Symlinked parent directories get moved whole. If ~/.config is a symlink outside the repo (Dropbox, another dotfiles setup), claim backs up the entire symlink and replaces it with a real dir. That's documented and non-destructive, but it is surprising for a top-level dir. Consider a prompt or a louder dry-run message when the moved path is .config, .local, or .claude.
  5. load_state passes the saved file through parse_args. A hand-edited or corrupt file can inject --audit, --dry-run, --pick, or --list. Whitelist --profile|--only|--skip when loading.

CI / cost

  • The workflow runs 3 macOS jobs on every PR. Add a paths: filter (install.sh, tools.txt, .github/**, stow dirs) and a concurrency group with cancel-in-progress.
  • Pin actions to a SHA, or accept tag pinning for actions/checkout@v4.
  • The full job runs sudo rm -rf /opt/homebrew on the runner. That's fine on an ephemeral runner, but note that it must never be run locally.
  • The curl ... Homebrew/install/HEAD/install.sh fetch is unpinned. That's the standard Homebrew path, but it makes CI non-hermetic.

Minor

  • install_packages calls changed in dry-run. This is harmless but inconsistent with backup.
  • audit_stray only scans ~, ~/.config, ~/.local, and ~/.claude. That matches the current packages, but .skhdrc-style new dotdirs would be missed. A comment saying so would help.
  • I could not verify that herdr and claude-code exist in Homebrew core/cask. The CI full job should catch it.

Tests

Good coverage of full and subset installs, rerun no-op, backups, --audit read-only, and the selection outside the checkout. Not covered: --skip, --pick, the needs closure error path, a corrupt state file, and the symlink-parent case from #4. A couple of cheap --dry-run/--list assertions in the lint job would cover most of those.

🤖 Generated with Claude Code

@drewjs
drewjs marked this pull request as ready for review October 17, 2026 23:06
@drewjs
drewjs merged commit 7a3aca3 into main Oct 17, 2026
4 checks 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