Summary
Add a one-command "update everything" script to the hyperi-developer installer,
so a freshly-provisioned machine has a single command (and an optional
clickable app) to update everything the installer set up.
There is no single package manager on macOS, so "update the lot" means running
several tools in sequence -- the macOS analogue of the Linux
apt update && apt upgrade && flatpak update habit.
Why
- The installer lays down Homebrew formulae+casks, uv tools, rustup toolchains,
and the Claude Code CLI -- each updates via a different command. Users
currently have to remember and run all of them.
- A maintained script that tracks the installer's own tool set keeps "update"
in lockstep with "install".
Proposed behaviour
- One command updates: Homebrew (formulae + casks, incl.
--cask --greedy,
autoremove, cleanup), uv tools, rustup toolchains, Claude Code CLI, and
macOS system/security updates (softwareupdate, last, may need a restart).
- Self-guarding: every section is
command -v-guarded, so a missing tool is
skipped (printed, not fatal). Safe to run where only some tools are present.
- Unattended-friendly:
HOMEBREW_NO_ENV_HINTS=1, softwareupdate --agree-to-license. The only interactive step is the single sudo password
for macOS updates.
- Per-section failures are recorded and reported in a summary; one failing
section never aborts the rest.
- Optional
--install: builds a double-clickable launcher app (via
osacompile, no deps) that runs the script in a Terminal window.
Notes for merging into the installer
- Keep the covered tool list in sync with whatever the installer provisions
(e.g. if Node/JFrog/Linear CLI land in a profile, add their updaters).
gcloud is the brew cask gcloud-cli, so brew upgrade --cask already
covers it -- do NOT also run gcloud components update (that forks the
version brew tracks from the one on disk).
- The Homebrew section sets
HOMEBREW_CASK_OPTS=--no-quarantine to avoid the
Gatekeeper "downloaded from the internet" prompt on freshly-updated casks.
- Still requires
zsh itself (used for the coloured output and arrays).
Reference implementation
Working script in use today on macOS -- a drop-in starting point. Adjust the
tool list to match the installer's profiles before merging:
#!/usr/bin/env zsh
#
# update-mac.sh — update everything on this machine in one command:
# - macOS system + security updates (softwareupdate)
# - Homebrew formulae and casks (apps, CLIs, subsystems — incl. aws, gh,
# az, kubectl, helm, terraform, vault, gcloud-cli, ...)
# - uv tools (ansible, hyperi-ci), rustup toolchains
# - Claude Code CLI (self-installed under ~/.local)
#
# Each section is independent and self-guarding: any tool that isn't
# installed is skipped (not an error), and if a step fails the script
# keeps going and reports it in the summary at the end. Safe to run on a
# machine that has only some of these tools. Run with: ~/update-mac.sh
# (will prompt once for your password for the macOS system updates).
#
# Run `~/update-mac.sh --install` once to drop a double-clickable
# "Update Mac" app into /Applications.
set -u
emulate -L zsh
# --- pretty output --------------------------------------------------------
autoload -Uz colors && colors
typeset -a FAILED
section() { print -P "\n%F{cyan}==> $1%f"; }
ok() { print -P "%F{green} ✓ $1%f"; }
warn() { print -P "%F{yellow} ! $1%f"; }
# run "<label>" cmd args... — runs a step, records failure but never aborts
run() {
local label=$1; shift
if "$@"; then ok "$label"; else warn "$label FAILED"; FAILED+=("$label"); fi
}
have() { command -v "$1" >/dev/null 2>&1; }
# --- self-resolve: absolute path to this script (symlinks resolved) -------
SELF=${${(%):-%x}:A}
usage() {
cat <<EOF
update-mac.sh — update Homebrew, uv, rustup, Claude Code and macOS in one go.
Usage:
update-mac.sh Run all updates now.
update-mac.sh --install Create a clickable "Update Mac" app in /Applications.
update-mac.sh --help Show this help.
EOF
}
# Build a double-clickable .app that opens Terminal and runs this script.
# Uses osacompile (built into macOS) — no dependencies. The app tells Terminal
# to `exec` this script, so when the run finishes the shell exits and (if your
# Terminal profile is set to "close if the shell exited cleanly") the window
# closes by itself; a failed run leaves it open to read.
install_app() {
if ! have osacompile; then
warn "osacompile not found (not macOS?) — cannot build the app"
return 1
fi
local app="/Applications/Update Mac.app"
local icon_store="$HOME/.local/share/update-mac/AppIcon.icns"
local legacy_icon="/Applications/Update All.app/Contents/Resources/AppIcon.icns"
section "Installing clickable app"
# Keep the custom icon outside any app bundle so re-installs (or deleting
# the old "Update All.app") never lose it. Migrate it from the legacy app
# the first time, if we don't already have a stored copy.
if [[ ! -f "$icon_store" && -f "$legacy_icon" ]]; then
mkdir -p "${icon_store:h}"
cp "$legacy_icon" "$icon_store" && ok "kept custom icon at $icon_store"
fi
rm -rf "$app"
local tmp; tmp=$(mktemp -d)
cat > "$tmp/run.applescript" <<EOF
tell application "Terminal"
activate
do script "exec " & quoted form of "$SELF"
end tell
EOF
if ! osacompile -o "$app" "$tmp/run.applescript"; then
warn "failed to build the app (no write access to /Applications?)"
rm -rf "$tmp"
return 1
fi
rm -rf "$tmp"
# Apply the custom icon. osacompile apps read Contents/Resources/applet.icns;
# remove the bundled Assets.car so it can't override our icns.
if [[ -f "$icon_store" ]]; then
cp "$icon_store" "$app/Contents/Resources/applet.icns"
rm -f "$app/Contents/Resources/Assets.car"
fi
# Editing the bundle invalidates osacompile's signature: re-sign ad-hoc and
# nudge the icon cache so Finder/Dock pick up the new icon.
have codesign && codesign --force --deep --sign - "$app" >/dev/null 2>&1
touch "$app"
ok "created $app"
print -P "%F{green} Launch it from Spotlight/Launchpad as 'Update Mac', or drag it to the Dock.%f"
print -P "%F{yellow} First launch asks permission to control Terminal — allow it once.%f"
}
# --- argument handling ----------------------------------------------------
case "${1:-}" in
--install) install_app; exit $? ;;
-h|--help) usage; exit 0 ;;
"") ;; # no args: fall through and run all updates
*) print -u2 "update-mac.sh: unknown option '$1'"; usage; exit 2 ;;
esac
print -P "%F{magenta}╔════════════════════════════════════════╗%f"
print -P "%F{magenta}║ Updating everything on $(scutil --get ComputerName 2>/dev/null || hostname)%f"
print -P "%F{magenta}╚════════════════════════════════════════╝%f"
# --- Homebrew: formulae + casks (the bulk of your apps & subsystems) ------
if have brew; then
section "Homebrew"
# Don't quarantine freshly-downloaded casks. Without this, every updated
# app triggers the macOS Gatekeeper prompt ("<App> is an app downloaded
# from the Internet, are you sure you want to open it?") on first launch.
export HOMEBREW_CASK_OPTS="--no-quarantine"
# Run unattended: suppress brew's env-var hints so output stays clean.
export HOMEBREW_NO_ENV_HINTS=1
run "brew update" brew update
run "brew upgrade" brew upgrade # formulae + outdated casks
# Casks that self-update report no version to brew; --greedy catches them too.
run "brew upgrade --cask --greedy" brew upgrade --cask --greedy
run "brew autoremove" brew autoremove # drop now-unused dependencies
run "brew cleanup" brew cleanup --prune=all # delete old downloads/versions
else
warn "Homebrew not found — skipping"
fi
# --- uv: CLI tools installed via `uv tool install` ------------------------
if have uv; then
section "uv tools"
run "uv tool upgrade --all" uv tool upgrade --all
else
warn "uv not found — skipping"
fi
# --- rustup: Rust toolchains (rustup itself is updated by brew) -----------
if have rustup; then
section "rustup toolchains"
run "rustup update" rustup update
else
warn "rustup not found — skipping"
fi
# --- Claude Code CLI: self-installed under ~/.local, not brew-managed ------
if have claude; then
section "Claude Code CLI"
run "claude update" claude update
else
warn "claude not found — skipping"
fi
# NOTE: gcloud is intentionally NOT updated here. It's installed as the brew
# cask 'gcloud-cli', so `brew upgrade --cask` above already updates it.
# Running `gcloud components update` as well would fork the version brew
# tracks from the one on disk.
# --- macOS: Apple system + security updates (last; may need a restart) ----
if have softwareupdate; then
section "macOS system updates (Apple)"
warn "may prompt for your password and could require a restart"
run "softwareupdate" sudo softwareupdate --install --all --agree-to-license
else
warn "softwareupdate not found (not macOS?) — skipping"
fi
# --- summary --------------------------------------------------------------
print -P "\n%F{magenta}──────────── summary ────────────%f"
if (( ${#FAILED} == 0 )); then
print -P "%F{green}All updates completed successfully.%f"
else
print -P "%F{yellow}Completed with ${#FAILED} issue(s):%f"
for f in $FAILED; do print -P "%F{yellow} - $f%f"; done
exit 1
fi
Filed from a working copy at ~/update-mac.sh. The companion doc
(~/update-mac.md) explains the per-section design rationale.
Summary
Add a one-command "update everything" script to the hyperi-developer installer,
so a freshly-provisioned machine has a single command (and an optional
clickable app) to update everything the installer set up.
There is no single package manager on macOS, so "update the lot" means running
several tools in sequence -- the macOS analogue of the Linux
apt update && apt upgrade && flatpak updatehabit.Why
and the Claude Code CLI -- each updates via a different command. Users
currently have to remember and run all of them.
in lockstep with "install".
Proposed behaviour
--cask --greedy,autoremove,cleanup), uv tools, rustup toolchains, Claude Code CLI, andmacOS system/security updates (
softwareupdate, last, may need a restart).command -v-guarded, so a missing tool isskipped (printed, not fatal). Safe to run where only some tools are present.
HOMEBREW_NO_ENV_HINTS=1,softwareupdate --agree-to-license. The only interactive step is the single sudo passwordfor macOS updates.
section never aborts the rest.
--install: builds a double-clickable launcher app (viaosacompile, no deps) that runs the script in a Terminal window.Notes for merging into the installer
(e.g. if Node/JFrog/Linear CLI land in a profile, add their updaters).
gcloudis the brew caskgcloud-cli, sobrew upgrade --caskalreadycovers it -- do NOT also run
gcloud components update(that forks theversion brew tracks from the one on disk).
HOMEBREW_CASK_OPTS=--no-quarantineto avoid theGatekeeper "downloaded from the internet" prompt on freshly-updated casks.
zshitself (used for the coloured output and arrays).Reference implementation
Working script in use today on macOS -- a drop-in starting point. Adjust the
tool list to match the installer's profiles before merging:
Filed from a working copy at
~/update-mac.sh. The companion doc(
~/update-mac.md) explains the per-section design rationale.