Last active
August 7, 2026 19:58
-
-
Save mrflory/da0c82c9ee3e84da4b4549a4a829cd38 to your computer and use it in GitHub Desktop.
Script to update all packages on macOS (brew, node.js via nvm, npm, python via uv, uv tools)
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| confirm() { | |
| local prompt="$1" | |
| read -r -p "$prompt [y/N]: " answer | |
| case "$answer" in | |
| [Yy]|[Yy][Ee][Ss]) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| run_step() { | |
| local title="$1" | |
| shift | |
| echo | |
| echo "==> $title" | |
| if confirm "Continue with: $title?"; then | |
| "$@" | |
| echo "Done: $title" | |
| else | |
| echo "Skipped: $title" | |
| fi | |
| } | |
| update_brew() { | |
| if ! command -v brew >/dev/null 2>&1; then | |
| echo "Homebrew not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Currently installed Homebrew packages:" | |
| brew list --versions | |
| brew update | |
| local outdated | |
| outdated="$(brew outdated || true)" | |
| if [ -z "$outdated" ]; then | |
| echo "No outdated Homebrew packages found." | |
| return 0 | |
| fi | |
| echo "Outdated Homebrew packages:" | |
| echo "$outdated" | |
| if confirm "Upgrade the listed Homebrew packages now?"; then | |
| brew upgrade | |
| else | |
| echo "Homebrew upgrade skipped after showing the list." | |
| fi | |
| } | |
| cleanup_brew() { | |
| if ! command -v brew >/dev/null 2>&1; then | |
| echo "Homebrew not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Current Homebrew cache size:" | |
| du -sh "$(brew --cache)" 2>/dev/null || echo "Unable to determine Homebrew cache size." | |
| brew cleanup | |
| if confirm "Prune all Homebrew cached downloads?"; then | |
| brew cleanup --prune=all | |
| else | |
| echo "Kept reusable Homebrew cached downloads." | |
| fi | |
| echo "Homebrew cache size after cleanup:" | |
| du -sh "$(brew --cache)" 2>/dev/null || echo "Unable to determine Homebrew cache size." | |
| } | |
| load_nvm() { | |
| if command -v nvm >/dev/null 2>&1; then | |
| return 0 | |
| fi | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| if [ -s "$NVM_DIR/nvm.sh" ]; then | |
| # shellcheck disable=SC1090 | |
| . "$NVM_DIR/nvm.sh" | |
| command -v nvm >/dev/null 2>&1 | |
| return $? | |
| fi | |
| return 1 | |
| } | |
| update_node_nvm() { | |
| if ! load_nvm; then | |
| echo "nvm not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Currently installed Node versions:" | |
| nvm list | |
| local current | |
| local target_lts | |
| current="$(nvm current 2>/dev/null || true)" | |
| target_lts="$(nvm version-remote --lts 2>/dev/null || true)" | |
| if [ -n "$current" ] \ | |
| && [ "$current" != "none" ] \ | |
| && [ "$current" != "system" ] \ | |
| && [ -n "$target_lts" ] \ | |
| && [ "$target_lts" != "N/A" ] \ | |
| && [ "$current" != "$target_lts" ]; then | |
| nvm install --lts --reinstall-packages-from="$current" | |
| if confirm "Uninstall old Node version $current from nvm?"; then | |
| nvm uninstall "$current" || echo "Failed to uninstall $current; remove it manually if needed." | |
| else | |
| echo "Kept old Node version $current." | |
| fi | |
| else | |
| nvm install --lts | |
| fi | |
| nvm alias default "lts/*" | |
| nvm use --lts >/dev/null | |
| } | |
| update_npm_global() { | |
| if ! command -v npm >/dev/null 2>&1; then | |
| echo "npm not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Currently installed global npm packages:" | |
| npm list -g --depth=0 | |
| npm -g update | |
| } | |
| python_version_gt() { | |
| local lhs="$1" | |
| local rhs="$2" | |
| local lhs_major lhs_minor lhs_patch rhs_major rhs_minor rhs_patch | |
| IFS=. read -r lhs_major lhs_minor lhs_patch <<< "$lhs" | |
| IFS=. read -r rhs_major rhs_minor rhs_patch <<< "$rhs" | |
| if ((lhs_major != rhs_major)); then | |
| ((lhs_major > rhs_major)) | |
| return $? | |
| fi | |
| if ((lhs_minor != rhs_minor)); then | |
| ((lhs_minor > rhs_minor)) | |
| return $? | |
| fi | |
| ((lhs_patch > rhs_patch)) | |
| } | |
| list_installed_uv_cpython_versions() { | |
| uv python list --managed-python --only-installed 2>/dev/null \ | |
| | awk ' | |
| $1 ~ /^cpython-[0-9]+\.[0-9]+\.[0-9]+-/ { | |
| version = $1 | |
| sub(/^cpython-/, "", version) | |
| sub(/-.*/, "", version) | |
| seen[version] = 1 | |
| } | |
| END { | |
| for (version in seen) { | |
| print version | |
| } | |
| } | |
| ' | |
| } | |
| update_uv_python() { | |
| if ! command -v uv >/dev/null 2>&1; then | |
| echo "uv not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Currently installed Python versions via uv:" | |
| uv python list | |
| if uv python upgrade --help >/dev/null 2>&1; then | |
| uv python upgrade | |
| elif uv python install --help 2>/dev/null | grep -q -- "--upgrade"; then | |
| uv python install --upgrade | |
| else | |
| echo "uv does not support Python upgrade commands, step will be skipped." | |
| return 0 | |
| fi | |
| local versions=() | |
| local version | |
| while IFS= read -r version; do | |
| versions+=("$version") | |
| done < <(list_installed_uv_cpython_versions) | |
| if [ "${#versions[@]}" -eq 0 ]; then | |
| echo "No uv-managed CPython versions found to clean up." | |
| return 0 | |
| fi | |
| local newest="${versions[0]}" | |
| for version in "${versions[@]}"; do | |
| if python_version_gt "$version" "$newest"; then | |
| newest="$version" | |
| fi | |
| done | |
| echo "Newest uv-managed CPython version: $newest" | |
| local old_versions=() | |
| for version in "${versions[@]}"; do | |
| if [ "$version" != "$newest" ]; then | |
| old_versions+=("$version") | |
| fi | |
| done | |
| if [ "${#old_versions[@]}" -eq 0 ]; then | |
| echo "No old uv-managed CPython versions found." | |
| return 0 | |
| fi | |
| echo "Old uv-managed CPython versions:" | |
| printf ' %s\n' "${old_versions[@]}" | |
| if confirm "Uninstall old uv-managed CPython versions?"; then | |
| uv python uninstall "${old_versions[@]}" \ | |
| || echo "Failed to uninstall one or more old Python versions; remove them manually if needed." | |
| else | |
| echo "Kept old uv-managed CPython versions." | |
| fi | |
| } | |
| update_uv_global() { | |
| if ! command -v uv >/dev/null 2>&1; then | |
| echo "uv not found, step will be skipped." | |
| return 0 | |
| fi | |
| echo "Currently installed global uv tools:" | |
| uv tool list | |
| uv tool upgrade --all | |
| } | |
| update_global_skills() { | |
| if ! command -v npx >/dev/null 2>&1; then | |
| echo "npx not found, step will be skipped." | |
| return 0 | |
| fi | |
| npx skills update --global | |
| } | |
| update_hermes() { | |
| if ! command -v hermes >/dev/null 2>&1; then | |
| echo "hermes not found, step will be skipped." | |
| return 0 | |
| fi | |
| hermes update | |
| } | |
| echo "Interactive update script started." | |
| run_step "Update Homebrew packages" update_brew | |
| run_step "Clean Homebrew cache" cleanup_brew | |
| run_step "Update Node.js (nvm, LTS)" update_node_nvm | |
| run_step "Update global npm packages" update_npm_global | |
| run_step "Update Python via uv" update_uv_python | |
| run_step "Update global uv tools" update_uv_global | |
| run_step "Update globally installed skills" update_global_skills | |
| run_step "Update Hermes" update_hermes | |
| echo | |
| echo "All steps completed." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment