Non-admin · new Mac (Apple Silicon)
You have no admin rights, but Homebrew works here when installed into your home
directory (~/brew). Set up your terminal + font first, then let brew handle
the rest — GUI apps (casks → ~/Applications, no admin) and the non-problematic
formulae — and manual-tools.sh install the manual-install list (coursier,
direnv, bat, awscli, tinytex) as prebuilt binaries. (Those would otherwise build from
source under ~/brew’s custom prefix — slow or failing — which is why they’re manual.)
Goal:
maximum no-admin, no password prompts.
1. Install Homebrew (no admin)
git clone https://github.com/Homebrew/brew ~/brew
Add to ~/.zprofile, then restart your terminal:
eval "$("$HOME"/brew/bin/brew shellenv)"
export HOMEBREW_CASK_OPTS="--appdir=$HOME/Applications" # casks -> ~/Applications, no admin
HOMEBREW_CASK_OPTS=--appdir is the key: it makes brew install --cask … drop apps
into ~/Applications instead of /Applications, so no app install asks for an admin
password.
2. Terminal + font first
Install iTerm2 and the FiraCode Nerd Font before anything else, then configure iTerm and do the rest of the setup from inside it:
brew install --cask iterm2 font-fira-code-nerd-font
iTerm2 configuration
(Once iTerm2 is installed — by brew cask on the admin path, or into ~/Applications
by manual-tools.sh on the non-admin paths — configure it as below.)
Global options (non-admin friendly)


3. The rest via brew
curl -L -o Brewfile https://raw.githubusercontent.com/teeckoo/mac-setup/main/Brewfile
brew bundle --verbose # casks (GUI + font) + the non-problematic formulae
rm Brewfile
brew bundle installs the GUI apps as casks (VS Code, IntelliJ, Chrome, Firefox,
Bruno, Slack — into ~/Applications; re-lists iTerm2 + font, already-installed casks
are skipped) and the rest of the CLI tools (gkit, rg, gum, jq, node, sdk,
starship, fzf, antidote, plus containers/cloud/git tools…). All no admin.
Why not everything via brew? Your
~/brewis a custom prefix, so Homebrew can’t use bottles and builds formulae from source. Casks are unaffected (prebuilt downloads), and the “rest” formulae build fast enough — but a handful are slow or fail to build from source. Those are the manual-install list, handled next.
4. The “install manually” list (prebuilt, not brew)
curl -fsSL https://raw.githubusercontent.com/teeckoo/mac-setup/main/manual-tools.sh | bash
manual-tools.sh installs the manual-install list as prebuilt binaries
(so they don’t source-build under ~/brew): coursier (cs), direnv, bat,
awscli, tinytex (no brew formula), R (the CRAN build relocated into
~/.local — no admin, since the r cask prompts for one), mdbook (a Rust
build that ~/brew would compile from source), and container
dev — colima + lima +
the docker CLI (colima start --vm-type vz, no admin — colima is the Docker
engine). It prints guidance for eza (no macOS binary — ls/cargo install eza),
wget (use curl), and gnupg (SSH commit signing).
Shell config (~/.zprofile + ~/.zshrc)
Homebrew is on PATH already (its shellenv), so you only need the interactive block.
~/.zshrc:
# ---- System ----
ulimit -n 4096 # prevent "too many open files"
setopt AUTO_CD # cd by typing a folder name
chpwd() { ls -C; } # listing after each cd
export DIRENV_LOG_FORMAT=""
# ---- Completions (before plugins/SDKMAN, which call compdef) ----
autoload -Uz compinit
compinit -i
# ---- Zsh plugins (antidote) ----
source "$(brew --prefix)/opt/antidote/share/antidote/antidote.zsh"
antidote load < ~/.zsh_plugins.txt
# ---- Prompt (starship) ----
eval "$(starship init zsh)"
# ---- fzf ----
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# ---- direnv ----
command -v direnv >/dev/null 2>&1 && eval "$(direnv hook zsh)"
# ---- SDKMAN ----
[ -d ~/.sdkman ] && export SDKMAN_DIR="$HOME/.sdkman"
[ -d ~/.sdkman ] || export SDKMAN_DIR="$(brew --prefix sdkman-cli)/libexec"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"
Open a new shell to auto-install the plugins.
starship prompt
Create ~/.config/starship.toml:
format = """
${env_var.OPTIONAL_PROMPT_HEADER}\
$directory $git_branch$git_status$all${env_var.OPTIONAL_PROMPT_FOOTER}
$character"""
[line_break]
disabled = false
[directory]
truncation_length = 0
truncate_to_repo = false
[env_var.OPTIONAL_PROMPT_HEADER]
variable = "OPTIONAL_PROMPT_HEADER"
# The \n is placed inside the string format so it only triggers if text exists
format = "[$env_value]($style)\n"
style = "bold blue"
[env_var.OPTIONAL_PROMPT_FOOTER]
variable = "OPTIONAL_PROMPT_FOOTER"
format = "[$env_value]($style)\n" # Newline at the START
style = "bold yellow"
(starship reads this automatically; the ~/.zshrc block below runs starship init zsh.)
Zsh plugins (antidote)
Create ~/.zsh_plugins.txt:
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
antidote loads this on every interactive shell (see the ~/.zshrc block). Add more
plugins here as you like.
fzf (fuzzy finder)
When the fzf installer prompts, answer:
| Prompt | Answer |
|---|---|
| fuzzy auto-completion | yes |
| key bindings | yes |
| update shell config files | no |
(“no” on the last one because the ~/.zshrc block already sources ~/.fzf.zsh.)
git log pager
Keep git log on one screen (git ships with the Xcode Command Line Tools):
git config --global --replace-all core.pager "less -F -X"
Troubleshooting
Gatekeeper “cannot be opened”. macOS may quarantine a downloaded binary on first run. Clear it:
xattr -dr com.apple.quarantine ~/.local/bin
compinit prompts every shell / command not found: compdef. A leftover Homebrew
site-functions dir owned by the old admin makes a bare compinit refuse to run. The
~/.zshrc block uses compinit -i (ignore insecure dirs) before the plugin/SDKMAN
lines — keep that order.
npm i -g permission denied. Global installs are redirected to ~/.local
(npm config set prefix ~/.local), so packages land in ~/.local/bin (already on
PATH). After a major Node LTS bump, packages with native bindings may need a
npm i -g reinstall to match the new ABI.
Re-running is safe (idempotent)
Both perfect-bottles.sh and manual-tools.sh re-run safely — to finish a partial
install or refresh to latest. CLI binaries and vendor installers overwrite in place
(no duplicates); Node wipes/reinstalls latest LTS (global packages survive); GUI apps
are replaced in place (a failed download leaves the old .app); antidote/fzf are
left alone if present; an unreachable source keeps its existing copy and is logged.
There’s no local cache, so a refresh re-downloads (~200 MB Node, ~2 GB GUI apps).
Admin · new Mac (Homebrew)
You have admin rights on this Mac, so Homebrew does everything — CLI tools, GUI
apps (casks), and fonts — from one Brewfile. Set up your terminal + font first, then
install the rest from inside iTerm.
1. Terminal + font first
brew install --cask iterm2 font-fira-code-nerd-font
iTerm2 configuration
(Once iTerm2 is installed — by brew cask on the admin path, or into ~/Applications
by manual-tools.sh on the non-admin paths — configure it as below.)
Global options (non-admin friendly)


2. Install everything — all brew
base=https://raw.githubusercontent.com/teeckoo/mac-setup/main
curl -L -o Brewfile "$base/Brewfile"
curl -L -o Brewfile.admin "$base/Brewfile.admin"
brew bundle --verbose # the Brewfile ("the rest"): casks + formulae
brew bundle --verbose --file=Brewfile.admin # extras: coursier, direnv, wget, awscli, bat,
# gnupg, eza, mdbook, colima, docker, MacTeX
rm Brewfile Brewfile.admin
100% brew, no manual steps. The
Brewfile is “the rest”;
Brewfile.admin
holds the packages kept out of it for the non-admin path (they’d source-build under a
non-admin ~/brew, but bottle fine here). Container dev: colima start --vm-type vz,
then docker …. LaTeX comes from the mactex-no-gui cask (TinyTeX has no Homebrew
formula). The no-admin helper scripts (perfect-bottles.sh, manual-tools.sh) are
not needed on this path.
Shell config (~/.zprofile + ~/.zshrc)
Homebrew is on PATH already (its shellenv), so you only need the interactive block.
~/.zshrc:
# ---- System ----
ulimit -n 4096 # prevent "too many open files"
setopt AUTO_CD # cd by typing a folder name
chpwd() { ls -C; } # listing after each cd
export DIRENV_LOG_FORMAT=""
# ---- Completions (before plugins/SDKMAN, which call compdef) ----
autoload -Uz compinit
compinit -i
# ---- Zsh plugins (antidote) ----
source "$(brew --prefix)/opt/antidote/share/antidote/antidote.zsh"
antidote load < ~/.zsh_plugins.txt
# ---- Prompt (starship) ----
eval "$(starship init zsh)"
# ---- fzf ----
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# ---- direnv ----
command -v direnv >/dev/null 2>&1 && eval "$(direnv hook zsh)"
# ---- SDKMAN ----
[ -d ~/.sdkman ] && export SDKMAN_DIR="$HOME/.sdkman"
[ -d ~/.sdkman ] || export SDKMAN_DIR="$(brew --prefix sdkman-cli)/libexec"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"
Open a new shell to auto-install the plugins.
starship prompt
Create ~/.config/starship.toml:
format = """
${env_var.OPTIONAL_PROMPT_HEADER}\
$directory $git_branch$git_status$all${env_var.OPTIONAL_PROMPT_FOOTER}
$character"""
[line_break]
disabled = false
[directory]
truncation_length = 0
truncate_to_repo = false
[env_var.OPTIONAL_PROMPT_HEADER]
variable = "OPTIONAL_PROMPT_HEADER"
# The \n is placed inside the string format so it only triggers if text exists
format = "[$env_value]($style)\n"
style = "bold blue"
[env_var.OPTIONAL_PROMPT_FOOTER]
variable = "OPTIONAL_PROMPT_FOOTER"
format = "[$env_value]($style)\n" # Newline at the START
style = "bold yellow"
(starship reads this automatically; the ~/.zshrc block below runs starship init zsh.)
Zsh plugins (antidote)
Create ~/.zsh_plugins.txt:
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
antidote loads this on every interactive shell (see the ~/.zshrc block). Add more
plugins here as you like.
fzf (fuzzy finder)
When the fzf installer prompts, answer:
| Prompt | Answer |
|---|---|
| fuzzy auto-completion | yes |
| key bindings | yes |
| update shell config files | no |
(“no” on the last one because the ~/.zshrc block already sources ~/.fzf.zsh.)
git log pager
Keep git log on one screen (git ships with the Xcode Command Line Tools):
git config --global --replace-all core.pager "less -F -X"
Troubleshooting
Gatekeeper “cannot be opened”. macOS may quarantine a downloaded binary on first run. Clear it:
xattr -dr com.apple.quarantine ~/.local/bin
compinit prompts every shell / command not found: compdef. A leftover Homebrew
site-functions dir owned by the old admin makes a bare compinit refuse to run. The
~/.zshrc block uses compinit -i (ignore insecure dirs) before the plugin/SDKMAN
lines — keep that order.
npm i -g permission denied. Global installs are redirected to ~/.local
(npm config set prefix ~/.local), so packages land in ~/.local/bin (already on
PATH). After a major Node LTS bump, packages with native bindings may need a
npm i -g reinstall to match the new ABI.
Re-running is safe (idempotent)
Both perfect-bottles.sh and manual-tools.sh re-run safely — to finish a partial
install or refresh to latest. CLI binaries and vendor installers overwrite in place
(no duplicates); Node wipes/reinstalls latest LTS (global packages survive); GUI apps
are replaced in place (a failed download leaves the old .app); antidote/fzf are
left alone if present; an unreachable source keeps its existing copy and is logged.
There’s no local cache, so a refresh re-downloads (~200 MB Node, ~2 GB GUI apps).
Non-admin · old Intel (no Homebrew)
An Intel (x86_64) Mac with no admin rights, where Homebrew is impractical (it
compiles from source). Everything installs into your home directory — no brew, no
admin — via two scripts: perfect-bottles.sh (CLI tools as prebuilt binaries) and
manual-tools.sh (GUI apps + a few extras).
1. Terminal + font first (no admin)
Get iTerm2 and the FiraCode Nerd Font into your home directory, configure iTerm, and
switch to it — then install everything else from inside it. (No /tmp on this box, so
downloads go under ~/.cache.)
mkdir -p ~/Applications ~/Library/Fonts ~/.cache/dl
# iTerm2 -> ~/Applications (no admin)
curl -fsSL https://iterm2.com/downloads/stable/latest -o ~/.cache/dl/iterm.zip
ditto -x -k ~/.cache/dl/iterm.zip ~/Applications/
xattr -dr com.apple.quarantine ~/Applications/iTerm.app
# FiraCode Nerd Font -> ~/Library/Fonts
curl -fsSL https://github.com/ryanoasis/nerd-fonts/releases/latest/download/FiraCode.zip -o ~/.cache/dl/FiraCode.zip
unzip -qo ~/.cache/dl/FiraCode.zip -d ~/.cache/dl/fc && cp ~/.cache/dl/fc/*.ttf ~/Library/Fonts/
iTerm2 configuration
(Once iTerm2 is installed — by brew cask on the admin path, or into ~/Applications
by manual-tools.sh on the non-admin paths — configure it as below.)
Global options (non-admin friendly)


2. Install everything else
From your configured iTerm:
base=https://raw.githubusercontent.com/teeckoo/mac-setup/main
curl -fsSL "$base/perfect-bottles.sh" | bash # CLI tools (+ refreshes the font) -> $HOME
curl -fsSL "$base/manual-tools.sh" | bash # GUI apps + extras (+ refreshes iTerm2)
perfect-bottles.sh installs gkit, rg, gum, glow, pandoc, mdbook, jq, git-filter-repo, uv, starship, claude, node+npm/npx, sdk (SDKMAN), antidote,
fzf (and the FiraCode font). It auto-loads the shared lib-no-brew.sh (local copy if present, else
fetched), so the one-liner works with no clone. The scripts re-install iTerm2 + the
font from step 1 in place — harmless.
GUI apps (no admin password)
manual-tools.sh installs these into ~/Applications by copying the .app out of
the vendor .dmg/.zip and clearing the Gatekeeper quarantine flag — no sudo,
no admin prompt (unlike brew casks, which install to /Applications and ask for an
admin password):
| App | Source | Arch |
|---|---|---|
| iTerm2 | iterm2.com/downloads/stable/latest | universal |
| VS Code | update.code.visualstudio.com/latest/<darwin|darwin-arm64>/stable | per-arch |
| IntelliJ IDEA | download.jetbrains.com/product?code=IIU&latest&distribution=<mac|macM1> | per-arch |
| Google Chrome | dl.google.com/.../googlechrome.dmg | universal |
| Firefox | download.mozilla.org/?product=firefox-latest-ssl&os=osx | universal |
| Bruno | latest GitHub release, *_<x64_mac|arm64_mac>.dmg | per-arch |
| Slack | slack.com/ssb/download-osx | universal |
A failed download is logged and skipped — re-run to retry; existing apps are replaced in place. (Opera and Chromium aren’t automated — no clean signed download; grab them by hand if needed.)
On Intel,
manual-tools.shalso installscoursier(cs),direnv,batas prebuilt binaries,awsclivia the no-admin user pkg,R(the CRAN build — its.pkgis expanded andR.frameworkrelocated into~/.local, withR/Rscripton PATH; no admin), and container dev (colima+lima+ thedockerCLI —colima start --vm-type vz, no admin). It prints guidance fortinytex(its installer prompts for admin on Ventura),eza(no macOS binary — usels),wget(usecurl), andgnupg(no no-admin path — consider SSH commit signing).
3. Shell config (~/.zprofile + ~/.zshrc)
perfect-bottles.sh appends both blocks for you (marker-guarded; your own content is
left untouched). Shown here for reference / manual paste:
~/.zprofile:
# ---- no-brew base config (perfect-bottles.sh) ----
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
export DIRENV_LOG_FORMAT=""
~/.zshrc:
. "$HOME/.cargo/env"
. "$HOME/.local/bin/env"
# ---- no-brew base config (perfect-bottles.sh) ----
# ---- System ----
ulimit -n 4096 # prevent "too many open files"
setopt AUTO_CD # cd by typing a folder name
# ---- Unified Directory Change Hooks ----
autoload -U add-zsh-hook
# Global path tracking marker (safely preserved)
typeset -g _LAST_LISTED_DIR=""
# 1. Your implicit local environment script processor
_chpwd_trigger_lenvrc() {
if [[ -f ".dir.session" ]]; then
source ".dir.session"
fi
}
add-zsh-hook chpwd _chpwd_trigger_lenvrc
# ---- Completions (must run before SDKMAN/plugins, which call compdef) ----
autoload -Uz compinit
compinit -i # -i: ignore "insecure" fpath dirs instead of prompting
# ---- Zsh plugins (antidote, cloned to ~/.antidote) ----
source "$HOME/.antidote/antidote.zsh"
antidote load < ~/.zsh_plugins.txt
# ---- Prompt (starship) ----
eval "$(starship init zsh)"
# ---- fzf (installed to ~/.fzf) ----
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# ---- direnv (installed by manual-tools.sh; guarded so this is a no-op if absent) ----
command -v direnv >/dev/null 2>&1 && eval "$(direnv hook zsh)"
#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!!
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
SDKMAN.
perfect-bottles.shinstalls SDKMAN into~/.sdkmanby running its official installer under zsh — SDKMAN’s bash-4 gate only fires under bash, and stock Intel macOS has only bash 3.2. SDKMAN’s installer appends its ownsdkman-init.shblock to the end of~/.zshrc(aftercompinit -i, where it must be) — don’t add one yourself. Just need a JDK?cs java --setup(coursier).
starship prompt
Create ~/.config/starship.toml:
format = """
${env_var.OPTIONAL_PROMPT_HEADER}\
$directory $git_branch$git_status$all${env_var.OPTIONAL_PROMPT_FOOTER}
$character"""
[line_break]
disabled = false
[directory]
truncation_length = 0
truncate_to_repo = false
[env_var.OPTIONAL_PROMPT_HEADER]
variable = "OPTIONAL_PROMPT_HEADER"
# The \n is placed inside the string format so it only triggers if text exists
format = "[$env_value]($style)\n"
style = "bold blue"
[env_var.OPTIONAL_PROMPT_FOOTER]
variable = "OPTIONAL_PROMPT_FOOTER"
format = "[$env_value]($style)\n" # Newline at the START
style = "bold yellow"
(starship reads this automatically; the ~/.zshrc block below runs starship init zsh.)
global direnv to add support for source_up_sdkmanrc
Create ~/.config/direnv/direnvrc:
# ==============================================================================
# 1. Fully Generic SDKMAN! Environment Loader (Local Folder Scope)
# ==============================================================================
layout_sdkman() {
: "${SDKMAN_DIR:=$HOME/.sdkman}"
if [[ -f ".sdkmanrc" && -f "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then
local zsh_safeguards='export ZSH_DISABLE_COMPFIX="true"; autoload -U compinit; compinit -u;'
direnv_load "$SHELL" -c "$zsh_safeguards source '$SDKMAN_DIR/bin/sdkman-init.sh' && sdk env >/dev/null 2>&1 && direnv dump"
fi
}
# ==============================================================================
# 2. Agnostic Ancestor Discovery Hook (Strict Explicit Execution)
# ==============================================================================
source_up_sdkmanrc() {
: "${SDKMAN_DIR:=$HOME/.sdkman}"
# Start hunting from the parent folder upward
local current_dir
current_dir=$(dirname "$PWD")
# Climb all the way to the filesystem root (/) until a file is found
while [[ "$current_dir" != "/" ]]; do
if [[ -f "$current_dir/.sdkmanrc" ]]; then
if [[ -f "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then
# Explicit Logging: Always print because this line is explicitly uncommented here
echo "direnv: loading $current_dir/.sdkmanrc" >&2
# Force evaluation inside that specific directory layer using an isolated subshell
local zsh_safeguards='export ZSH_DISABLE_COMPFIX="true"; autoload -U compinit; compinit -u;'
direnv_load "$SHELL" -c "cd '$current_dir' && $zsh_safeguards source '$SDKMAN_DIR/bin/sdkman-init.sh' && sdk env >/dev/null 2>&1 && direnv dump"
# Mark that the local context explicitly requested and applied an SDK toolchain
_LOCAL_SDKMAN_APPLIED="true"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
# ==============================================================================
# 3. Safe Global Overwrite: Protect Child Contexts During Tree Traversal
# ==============================================================================
source_up() {
# 1. Take a clean snapshot of all environment variables BEFORE climbing the tree
local env_snapshot
env_snapshot=$(env | grep -v '^DIRENV_' | grep -v '^direnv_')
# 2. Find and execute the parent configuration natively without relying on 'source_up' function hooks
local parent_envrc
parent_envrc=$(find_up .envrc)
if [[ -n "$parent_envrc" && "$parent_envrc" != "$PWD/.envrc" ]]; then
source_env "$parent_envrc"
fi
# 3. THE USER PERSPECTIVE ISOLATION ENFORCEMENT:
# If 'source_up_sdkmanrc' is NOT explicitly uncommented and executed in this folder,
# we restore the toolchain variables back to your original baseline shell state.
if [[ "$_LOCAL_SDKMAN_APPLIED" != "true" && ! -f ".sdkmanrc" ]]; then
while read -r line; do
if [[ "$line" == *=* ]]; then
local key="${line%%=*}"
local val="${line#*=}"
# If a variable ends with _HOME or was modified by SDKMAN, revert it
if [[ "$key" == *_HOME ]] || [[ "$key" == sdkman_* ]]; then
export "$key"="$val"
fi
fi
done <<< "$env_snapshot"
# Also wipe out any new software candidates added to the path by parent folders
PATH_rm "$SDKMAN_DIR/candidates/*/*" 2>/dev/null
PATH_rm "$HOME/.sdkman/candidates/*/*" 2>/dev/null
fi
}
# ==============================================================================
# 4. Automated Local Tool Placement Trigger
# ==============================================================================
auto_load_local_tools() {
if [[ -f ".sdkmanrc" ]]; then
layout_sdkman
fi
}
auto_load_local_tools
Zsh plugins (antidote)
Create ~/.zsh_plugins.txt:
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
antidote loads this on every interactive shell (see the ~/.zshrc block). Add more
plugins here as you like.
fzf (fuzzy finder)
When the fzf installer prompts, answer:
| Prompt | Answer |
|---|---|
| fuzzy auto-completion | yes |
| key bindings | yes |
| update shell config files | no |
(“no” on the last one because the ~/.zshrc block already sources ~/.fzf.zsh.)
git log pager
Keep git log on one screen (git ships with the Xcode Command Line Tools):
git config --global --replace-all core.pager "less -F -X"
Troubleshooting
Gatekeeper “cannot be opened”. macOS may quarantine a downloaded binary on first run. Clear it:
xattr -dr com.apple.quarantine ~/.local/bin
compinit prompts every shell / command not found: compdef. A leftover Homebrew
site-functions dir owned by the old admin makes a bare compinit refuse to run. The
~/.zshrc block uses compinit -i (ignore insecure dirs) before the plugin/SDKMAN
lines — keep that order.
npm i -g permission denied. Global installs are redirected to ~/.local
(npm config set prefix ~/.local), so packages land in ~/.local/bin (already on
PATH). After a major Node LTS bump, packages with native bindings may need a
npm i -g reinstall to match the new ABI.
Re-running is safe (idempotent)
Both perfect-bottles.sh and manual-tools.sh re-run safely — to finish a partial
install or refresh to latest. CLI binaries and vendor installers overwrite in place
(no duplicates); Node wipes/reinstalls latest LTS (global packages survive); GUI apps
are replaced in place (a failed download leaves the old .app); antidote/fzf are
left alone if present; an unreachable source keeps its existing copy and is logged.
There’s no local cache, so a refresh re-downloads (~200 MB Node, ~2 GB GUI apps).








