Convert machine setup to declarative mise bootstrap - #21
Merged
Conversation
Port bootstrap.sh and defaults.sh to mise's declarative bootstrap: packages, repos, dotfiles, macOS defaults and hooks in mise.toml, with the four genuinely imperative steps left as `bootstrap:*` tasks. Verified against this machine with `mise bootstrap -n`: all 37 macOS defaults keys from defaults.sh are covered (the two that aren't expressible — the -currentHost tap behavior and the $HOME-relative screenshot location — moved to hooks), and 23 of 25 dotfile entries already report `applied`. Two fixes fall out of the port: - ~/.tmux is declared as one directory link, matching what this machine actually has, so .tmux.conf's bindings for agenda.sh, btop.sh, goto-window.sh, move-window.sh and session-jump.sh resolve on a fresh install; bootstrap.sh linked only four of the nine helpers. - The dead ~/.ssh/config entry is dropped — the repo has no .ssh/, so bootstrap.sh would either prompt over a real config or leave a broken symlink. mise.toml is un-ignored here because the global gitignore excludes per-project mise configs; this repo's copy is the setup itself. bootstrap.sh and defaults.sh stay until this is proven on a fresh machine.
mise.toml now covers everything both scripts did, so delete them and point the README and CI at `mise bootstrap`. CI drives the real documented path — install mise from mise.run, then `mise bootstrap --force-dotfiles --skip tools,task` — and asserts convergence with `mise bootstrap dotfiles status --missing` instead of hand-listing every expected symlink. [tools] and the bootstrap task are skipped for runtime (bootstrap:nvim compiles every treesitter parser); bootstrap:linux-shims still runs on its own since it creates the fd/bat shims the tool check asserts on. Submodules are no longer checked out by actions/checkout so the pre-dotfiles hook is exercised instead. Also harden the workflow while it's open: default-deny permissions with per-job grants, actions/checkout pinned to a SHA and bumped to v7.0.1, and persist-credentials disabled. zizmor reports no findings. shellcheck now covers .tmux/*.sh, which is what's left of the repo's shell scripts; .claude/scripts/statusline.sh has pre-existing warnings and is left for its own change. Drop the iTerm2 Catppuccin import task and the iTerm2 pointer in .tmux.conf — Ghostty is the terminal now.
code-editor opened iTerm, and claude-man/monitor_tmux_pane passed -sender com.googlecode.iterm2 to terminal-notifier so click-through focused an app that is no longer installed.
.gitmodules declared .vim/pack/minpac/opt/minpac but .vim/ has no gitlink in the index (and no longer exists in the repo), so the entry was inert config. The four .vim* .gitignore lines guarded paths that are likewise gone.
Homebrew runs on Linux, so the brew: prefix only filters in one direction — the Ubuntu CI job installed the entire macOS list alongside the apt one and then died building terminal-notifier from source, which needs xcodebuild. Every package entry now carries an explicit os filter. The filter needs mise 2026.8.4, so declare min_version; without it an older mise reports only 'invalid type: map, expected a string'.
Tasks run in a non-login shell, so .zprofile's path_helper line never fires and /opt/homebrew/bin is absent. On a fresh Mac that meant the bootstrap task could not see the tools bootstrap had just installed: bootstrap:nvim hit its 'command -v nvim || exit 0' guard and skipped every plugin and treesitter parser without printing anything, and bootstrap:tpm ran install_plugins without tmux and failed the run. Both guards now say why they skipped instead of exiting silently, and a missing tmux no longer fails the whole bootstrap.
.zshrc ended with `[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local`. When that file is absent the test returns 1, and since it was the last statement in .zshrc the shell reached its first prompt with $? = 1, so powerlevel10k drew the OK caret in the ERROR color. Any machine without a .zshrc.local opened every new terminal with a red prompt. Verified in a clean VM: the caret emits SGR 31 without the file and 32 with it; the `if` form returns 0 either way.
bootstrap:nvim synced plugins and treesitter parsers but never loaded mason, so its ensure_installed tools were still downloaded on the first real nvim launch — the thing this task exists to avoid. Load mason headless and block until its install queue drains. lazygit was missing outright: LazyVim binds <leader>gg to it and flags it in checkhealth. macOS only; it is not in the Ubuntu LTS apt repos.
`[env] _.path` prepends, so the previous fix put /opt/homebrew/bin ahead of mise's own installs and let brew's python/ruby/node shadow the [tools] versions inside this repo. Move the append into the two tasks that actually need brew binaries.
Two silent half-failures, both of which left work for the first real launch — the thing this task exists to prevent: - treesitter: ~300 parsers is enough back-to-back GitHub downloads to draw 429s, and install() resolves regardless, so one pass exited 0 with 167 of ~300 installed. Retry until a pass adds nothing, clearing the stale extract dirs that made retries die on ENOTEMPTY. - mason: the wait sampled "nothing is installing" once, which is true early on while downloads are still queued, so nvim exited mid-install. Require 15 consecutive idle seconds instead. Also print the resulting counts, since exit 0 clearly wasn't evidence of much.
The retry loop capped at 4 passes and broke out mid-climb (292 -> 309 parsers on the last pass), reporting nothing about having stopped early. Loop until a pass adds nothing, and say so out loud if 8 passes isn't enough. Mason failures were invisible too: erb-formatter and erb-lint are gems and die with "gem failed with exit code 1" on a machine whose ruby didn't install, but that only reaches mason.log. Print the names.
LazyVim's terraform extra hardcodes terraform_fmt and terraform_validate, so
opening a .tf file threw "Error running terraform: ENOENT" — the binary was
never installed.
It lands in [tools] rather than [bootstrap.packages] because homebrew-core
dropped the formula over the BUSL relicense, and mise's brew backend rejects
the hashicorp tap outright ("the tap must publish API metadata ... mise will
not proxy to the brew CLI"), erroring out the entire packages step rather than
skipping the one entry.
This reverts the [tools] entry from c9386fc. ~/.config/mise/config.toml is symlinked into this repo, so the entry went live on every machine using these dotfiles the moment it was committed — no bootstrap required. With GitHub's releases API down, mise cannot resolve terraform@latest and warns on every shell init, and that console output trips Powerlevel10k's instant-prompt guard. Re-add once the API is healthy, pinned rather than "latest" and installed at the same time, so an unresolvable entry can never sit in the live config.
# Conflicts: # .tmux.conf
The final hook said on-demand parser installs were expected. That was true before bootstrap:nvim installed all of them; now the opposite holds, and the old note talks you out of noticing the exact symptom that means the warm silently half-finished.
The LazyVim terraform extra shells out to the binary on every .tf buffer and errored with ENOENT without it. Pinned rather than "latest": this file is symlinked into ~/.config, so a version mise cannot resolve warns on every shell init, and that console output trips Powerlevel10k's instant-prompt guard — which is exactly what a GitHub releases-API outage produced last week.
# Conflicts: # bin/vm
mise ships a rubygems plugin that shells out to `mise reshim` after installing any gem with executables, and `mise exec` does not put mise itself on PATH. On a machine that installed mise the way the README says — curl mise.run, which lands in ~/.local/bin — that hook died with ENOENT, gem exited 1, and mason reported erb-formatter and erb_lint as failed even though the gem files installed fine. It never reproduced on my Mac because mise came from brew there, and the task already appended /opt/homebrew/bin. A pristine VM run following the documented install path failed both packages every time. Verified on that VM: 8 mason packages before, 10 after, no failure warning.
.zsh/path.zsh puts ~/bin at the front of $PATH and the README documents a dozen scripts that live there, but nothing ever created the directory. On this Mac it has been a hand-made symlink since June, so the gap was invisible; on a pristine VM every one of those scripts was missing and 'command -v claude-man' came back empty. Verified both ways: mise reports the existing hand-made link as already applied here, and creates it from scratch on the VM, after which claude-man and vm both resolve.
mise bootstraps itself so it can't be a [bootstrap.packages] entry — whichever copy you install by hand wins, and nothing in the repo enforces it. Homebrew's build disables self-update and lagged upstream by two releases, but the reason it matters is that it lands in /opt/homebrew/bin and so hid the mason gem bug fixed in ec943ed: on a mise.run machine the reshim hook needs ~/.local/bin on PATH, and a brew install accidentally satisfied it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the imperative
bootstrap.sh(329 lines) anddefaults.sh(79 lines)with a declarative
mise.toml. Setup is nowmise bootstrap— convergent,dry-runnable, and diffable.
What moved where
brew install/apt-get installloops[bootstrap.packages](brew:,brew-cask:,apt:— the prefix is the platform filter)ln -sloop over a hardcoded file list[dotfiles], 25 entriesgit cloneof gcd[bootstrap.repos]defaults write× 37[bootstrap.macos.*]curated + raw[bootstrap.macos.defaults.<domain>]brew-cask:font-inconsolata-nerd-font[tasks.bootstrap]+ threebootstrap:*subtasksBugs this surfaced and fixes
.tmux.confbinds 9 helper scripts butbootstrap.shsymlinked only 4 —dead bindings on a fresh install. The TOML links
~/.tmuxas a wholedirectory, which is what the live machine actually has.
bootstrap.shlinked.ssh/config, but the repo has no.ssh/— it wouldprompt over a real
~/.ssh/configor leave a broken symlink. Dropped.~/.config/lsd/colors.yamlwas a broken symlink to a file nothing creates.The new config repairs it.
Ordering notes
TPM is a task, not a
[bootstrap.repos]entry: repos run at step 7 anddotfiles at step 8, so cloning into
~/.tmux/plugins/would materialize areal
~/.tmuxand block the whole-directory symlink. Submodules init from apre-dotfileshook for the same reason.Two
defaultskeys live in hooks rather than[bootstrap.macos.defaults]because template expressions do not render in defaults values (verified
against a junk domain):
com.apple.screencapture locationneeds$HOME, andNSGlobalDomain com.apple.mouse.tapBehaviorneeds-currentHost.Parity verification
domain keypairs fromdefaults.shand frommise bootstrap macos defaults status, diffed withcomm. 37 keys covered;the only two in the old script but not the TOML are the two moved to hooks.
Nothing extra.
mise bootstrap dotfiles status→ 25 entries, 23applied,2
differs(one real file byte-identical to the repo copy, one brokensymlink) — both resolved by
--force-dotfiles.mise bootstrap -nprints the bootstrap task withoutexecuting it.
CI
.github/workflows/bootstrap.ymldrives the real thing on Ubuntu instead ofasserting symlinks by hand:
mise bootstrap -y --force-dotfiles --skip tools,task, thenmise bootstrap dotfiles status --missing(exit 1 ondrift), a CLI-tool presence check, and a re-run no-op check. Hardened per the
gh-actions-security checklist — SHA-pinned checkout,
permissions: {}default-deny,
persist-credentials: false.Also
iTerm2 references swapped to Ghostty (
bin/code-editor, twoterminal-notifier -senderbundle IDs, a.tmux.confcomment), and the deadminpac submodule plus four stale
.vim*ignore rules removed.Not done here
mise bootstraphas not been run for real on the live machine yet — only-nand the per-step status commands. The old scripts are already deleted, sothis branch is the only path once merged.