Skip to content

arena: manifest registry - #15

Closed
drewjs wants to merge 6 commits into
mainfrom
arena/manifest-registry
Closed

drewjs wants to merge 6 commits into
mainfrom
arena/manifest-registry

Conversation

@drewjs

@drewjs drewjs commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Why

A fresh Mac needs one command that installs Homebrew and the right tools, and links their configs. Each machine should get only the tools it needs, and a rerun should change nothing. This PR adds a plain registry, tools.txt, with one line per tool, and an install.sh that reads it. Stow stays the config mechanism.

How it works

  • Each line of tools.txt names a tool, its stow package, its Homebrew formulae and casks (cask: prefix), the tools it needs, and its profiles. Every tool is in full. Tools tagged minimal form minimal.
  • install.sh resolves the selection, adds the tools the selection needs, and saves the request (for example --profile full or --only nvim,skhd) to the gitignored tools.local. A bare rerun reuses that request. Machines on full pick up new tools on their next run.
  • It installs only the packages that brew list does not show. It initializes submodules only inside selected packages. It moves anything in the way of a link to ~/.dotfiles-backup/<time>-<pid>/. Then it runs stow.
  • Before stowing, it creates the top two directory levels of each package as real directories (~/.config, ~/.local/bin, ~/.claude, ~/.config/nvim). Without this, stow folds a missing ~/.local or ~/.claude into one symlink, and nvim plugins or Claude Code state would land in the git checkout.
  • From zero, bash -c "$(curl -fsSL .../install.sh)" installs Homebrew (one password prompt), clones to ~/dotfiles (DOTFILES_DIR overrides), and runs the clone's install.sh.
  • Flags are --profile, --only, --skip, --pick (numbered list, one prompt), --list, and --dry-run. Skipping a tool that a selected tool needs is an error.
  • To add a tool, add one line to tools.txt. CI reads the same file.

Scope

  • Covers Homebrew packages and stow links. It does not start the yabai or skhd services, grant macOS permissions, or install the commercial Ghostty font.
  • Deselecting a tool later does not unlink or uninstall it. Run stow -D <pkg> yourself.
  • It does not add, remove, or edit any package contents, and it does not touch alacritty/ or ghostty/.

Tradeoffs

Rejected Reason
Brewfile and brew bundle A second list beside the stow packages. brew bundle upgrades outdated formulae by default, so a rerun is not a no-op.
Bash associative arrays or one script per tool macOS bash 3.2 has no associative arrays. Per-tool scripts put one tool in several files.
stow --adopt Moves existing files into the repo and overwrites tracked configs.
stow --no-folding New files in nvim/.config/nvim/lua would not appear until a rerun. It would also unfold the links on the current Mac.
Unlink or uninstall on deselect Would remove links made by hand on the current Mac.
Save the resolved tool list Machines on full would never get new tools.

Behavior that removes or overwrites configs you have today

  • Nothing is deleted. A regular file at a path a selected package links, such as a real ~/.zshrc, is moved to ~/.dotfiles-backup/<time>-<pid>/ at the same relative path.
  • A symlink that points outside this checkout is moved the same way, for example ~/.config/nvim pointing at an older clone. The link moves. Its target is not touched.
  • A real directory at a path where the package has a file is moved whole. A regular file at a parent path, such as a file named ~/.config, is moved too.
  • Links that already point into this checkout stay. On a simulated copy of the current hand-stowed Mac, nothing moved. Only the unstowed Ghostty config got linked.
  • A fresh Mac gets a different layout from the current Mac. ~/.config/nvim, ~/.config/herdr, and ~/.config/tmux are real directories with per-file links, so herdr runtime state stays out of the repo. Existing folded links are kept.
  • brew install can upgrade outdated dependencies of a newly installed package. That is Homebrew's default, and it changes no configs.

Verification

  • CI run on free runners only, tested against the merge with current main (which includes the Alacritty removal). lint (macos-15) ran shellcheck, a parse with /bin/bash 3.2, and --list. full install from zero (macos-15) removed the runner's Homebrew, ran the curl bootstrap with --profile full, and passed. subset install (macos-14) ran --only nvim,skhd over the preinstalled Homebrew and passed.
  • .github/scripts/verify-install.sh seeds conflicting files, then checks packages, links, real top-level directories, and intact backups. It checks that no formula, cask, or link outside the selection appeared. A bare rerun must leave Homebrew versions, links, backups, repo status, and mtimes unchanged. Both jobs printed ok: a bare rerun changed nothing and PASS.
  • Locally on Linux with real stow, a stub brew, and bash 3.2.57, four deliberate breaks turned the verifier red: a wrong expected set, a rerun that rewrites state, a backup that deletes, and a removed fold guard.
  • The existing claude-review check fails because the Claude action rejects PRs that a bot opened. This PR does not change that workflow.
Open in Web Open in Cursor 

cursoragent and others added 4 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>
@claude

claude Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review: install.sh + tools.txt

Solid design. One registry, bash 3.2 aware, idempotent, and the verifier checks from outside the installer. I read the diff only and did not run it.

Worth fixing

  1. claim can move a whole parent symlink (install.sh, claim). If ~/.config or ~/.local is a symlink to a synced dir (Dropbox, a bare-repo setup), it is not "into the repo", so backup moves it. make_dirs then creates an empty real dir, and every other app's config vanishes from view. It is reversible, but surprising. Suggest only backing up foreign symlinks at the leaf. For intermediate components, either die with a clear message or leave them and let stow traverse or fail.
  2. Casks installed outside brew fail the run. have_c only sees brew-managed casks. If Ghostty.app or Claude Code (native installer, npm) already exists, brew install --cask errors and set -e aborts the run. Consider checking /Applications/Ghostty.app or command -v claude, or tolerating a per-cask failure with a warning.
  3. State is saved before installs succeed. save_state runs before ensure_brew and install_packages. A failed brew run still persists the selection, so the next bare run silently reuses it. Probably fine, but moving it to the end is safer for --only experiments.

Minor

  • --profile - matches every tool whose profiles column is - (has on the - placeholder). Reject - as a profile name.
  • bootstrap installs Homebrew (sudo prompt) even for --list, which is read-only. Handle --list before ensure_brew, or die with "clone first" as --dry-run does.
  • When run via bash -c "$(curl …)", HERE falls back to the cwd. If the cwd happens to hold some tools.txt, it runs that one. Require the manifest next to a real BASH_SOURCE, or ignore HERE when BASH_SOURCE is empty.
  • make_dirs prunes ! -name .git, but the verifier uses .git*. Use the same filter in both.
  • MOVED/has are space-delimited, so paths with spaces would break dedupe. Unlikely for dotfiles, but worth a comment.

Security

  • curl | bash of main plus the Homebrew HEAD installer is the standard trade-off. Consider recommending a pinned tag or commit in the README. tools.local is parsed through parse_args with set -f, so no injection there.

CI / tests

  • Add timeout-minutes to the full and subset jobs. The from-zero job installs yabai, ghostty and everything else, and could hang for hours.
  • Pin actions to SHAs, or accept tags as is.
  • Not covered: --pick, --dry-run (assert no change), --skip of a needed tool (expect an error), an unknown tool or profile, and a symlink-to-outside conflict. A few cheap negative tests in lint (! ./install.sh --only nope, a --dry-run snapshot diff) would pin these.
  • The subset job hard-codes "nvim skhd yabai", which duplicates the needs resolution. That is fine, since it acts as an independent oracle, but note it must be updated if tools.txt changes.

Docs in README and AGENTS.md match the behavior.

cursoragent and others added 2 commits October 17, 2026 22:24
@drewjs drewjs closed this Oct 17, 2026
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