A small, modular CLI to update common macOS, Linux, WSL, and Windows tooling.
The v2 contract uses Bash on macOS/Linux/WSL and PowerShell 7 via updates.cmd/updates.ps1 on native Windows. The v2.1 release adds offline diagnosis and safer Windows activation without changing existing v2 interfaces.
This script can be disruptive (it updates global environments). Use --dry-run and scope with --only / --skip.
See SPEC.md for the full CLI/module contract, exit codes, and release invariants.
Using the Makefile (macOS/Linux/WSL):
make install
# Recommended (user-writable; enables self-update without sudo):
# make install PREFIX=$HOME/.local
# or: make install PREFIX=/opt/homebrewManual install (macOS/Linux/WSL):
chmod +x ./updates
sudo mkdir -p /usr/local/bin
sudo install -m 0755 ./updates /usr/local/bin/updatesNative Windows (v2.1.2, PowerShell 7):
$version = '2.1.2'
$installer = Join-Path $env:TEMP 'install-updates-windows.ps1'
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/amanthanvi/updates/v$version/install-windows.ps1" -OutFile $installer
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File $installer -Version $version
& "$env:LOCALAPPDATA\Programs\updates\updates.cmd" --versionThe installer downloads and authenticates the official updates-windows.zip GitHub Release asset before laying out updates.cmd, updates.ps1, versioned payload files, current.txt, previous.txt, and install-source.json under %LOCALAPPDATA%\Programs\updates. For a local ZIP, pass -SourceZip .\updates-windows.zip and preferably -SourceZipSha256 <sha256>; v2.1 accepts an unhashed local ZIP with a prominent trust warning as a compatibility bridge. The installer does not modify PATH.
The receipt's installed_version records the newest payload that finished installation and validation. It may temporarily differ from current.txt when activation fails safely and the previously active payload remains runnable.
updates
updates --dry-run
updates --only brew,node --brew-mode formula
updates --only linux -n
updates --only winget,node,bun
updates --full
updates --skip python --log-file ./updates.log
updates --json -n --no-self-update --log-level warn
updates --doctor
updates --doctor --jsonExample output (trimmed):
Starting updates...
==> brew START
Homebrew 🍺
==> brew END (OK) (12s)
==> SUMMARY ok=1 skip=0 fail=0 total=12s
Done in 12s. 🎉
List available modules:
updates --list-modulesShow help:
updates --helpModules are auto-detected: if the underlying command isn’t installed, the module is skipped (unless you used --only, in which case it’s an error).
brew: update/upgrade Homebrew formulae (+ casks when enabled via--brew-mode casks/--brew-mode greedy/--full)shell: update Oh My Zsh and custom git plugins/themes (auto-detected; detached, missing-upstream, or dirty tracked repos warn and skip)repos: update aman dev repos under~/GitRepos(auto-detectedaman-*-setupdirs; only clean tracked branches are pulled)linux: upgrade Linux system packages (auto-detectsapt-get/dnf/yum/pacman/zypper/apk)winget: upgrade installed Windows packages/apps viawinget(Windows only)node: plan global npm upgrades via npm-check-updates, verify each candidate against the active Node runtime with npm's engine-strict dry-run, then install compatible candidates one at a time (sources NVM first when available on macOS/Linux)bun: upgrade Bun global packages everywhere; native Windows only self-updates the Bun CLI when it appears standalone-installedpython: upgrade global/user Python packages via a resolved launcher (py -3,python, thenpython3); externally-managed Bash environments use a guarded user-site pathuv: update uv-managed tools everywhere; native Windows only self-updates uv when it appears standalone-installedmas: upgrade Mac App Store apps viamas(disabled by default; enable with--mas-upgradeor--full)pipx: upgrade pipx-managed apps viapipx upgrade-allrustup: update Rust toolchains viarustup updateclaude: update Claude Code CLI viaclaude updatepi: update pi AI CLI extensions viapi updatemise: update mise and upgrade installed tools (mise self-update,mise upgrade)go: update Go binaries fromGO_BINARIESin~/.updatesrc(entries default to@latest)macos: list available macOS software updates viasoftwareupdate -l(disabled by default; enable with--macos-updatesor--full)
Native Windows v2.1 default-on modules: winget, node, bun, python, uv, pipx, rustup, claude, pi, go.
On native Windows, --full selects every supported Windows module even if SKIP_MODULES in config would otherwise omit one; explicit --skip still wins.
Platform support summary:
| Module | macOS | Linux/WSL | Windows |
|---|---|---|---|
| brew, shell, repos | Yes | Yes | No |
| linux | No | Yes | No |
| winget | No | No | Yes |
| node, bun, python, uv, pipx, rustup, claude, pi, go | Yes | Yes | Yes |
| mas, macos | Yes | No | No |
| mise | Yes | Yes | Deferred |
Unsupported modules skip during default selection; explicitly selecting one with --only is a usage error.
updates --doctor diagnoses only local state. It performs no network access and no repair. Exit 0 means required checks passed (warnings are allowed), exit 1 means at least one integrity check failed, and exit 2 means usage or configuration was invalid. With --json, stdout remains JSONL-only and contains doctor_check plus one doctor_summary event.
updates optionally reads ~/.updatesrc for defaults (CLI flags override; pass --no-config to ignore). The file is parsed as line-oriented KEY=value, tolerates a UTF-8 BOM, and resolves home via HOME or USERPROFILE on Windows.
Native Windows note: PARALLEL remains part of the shared config surface for the Bash implementation, but the PowerShell runtime warns and ignores it; explicit --parallel <N> is rejected on native Windows.
Example:
# ~/.updatesrc
SKIP_MODULES=mas,macos
BREW_MODE=formula
BREW_CLEANUP=1
LOG_LEVEL=info
GO_BINARIES="golang.org/x/tools/gopls,github.com/go-delve/delve/cmd/dlv"
NODE_NPM_INSTALL_FLAGS="--legacy-peer-deps"For GO_BINARIES, entries may be module or module@version. If @version is omitted, it defaults to @latest.
NODE_NPM_INSTALL_FLAGS passes extra whitespace-separated flags to npm install -g in the node module, before the -- package separator. Default is empty (no extra flags). This is useful when peer-dependency conflicts require a scoped flag such as --legacy-peer-deps without setting it globally in ~/.npmrc.
Install what you actually use:
brew(Homebrew)git(for theshellandreposmodules)- NVM-managed
npm/ncuare preferred when$NVM_DIR/nvm.shor~/.nvm/nvm.shexists; directncumust support--enginesNode, otherwiseupdatesfalls back tonpx npm-check-updates. Every returned candidate is also checked with npm's engine-strict dry-run; incompatible candidates warn and skip. pwsh(PowerShell 7) for native Windows supportwingetfor thewingetmodule on Windowsbunfor thebunmoduleuv: https://github.com/astral-sh/uvmas:brew install masmise: https://mise.jdx.devpipx:brew install pipxrustup: from https://rustup.rsclaude(Claude Code CLI) for theclaudemodulepi(npm-installed AI coding CLI) for thepimodulego(for thegomodule)- On Linux: a supported system package manager (
apt-get,dnf,yum,pacman,zypper, orapk) andsudo(if not running as root)
Tests require python3 with either public packaging or pip's vendored packaging module available; lint additionally requires shellcheck and shfmt. tests/test_release.sh exercises release guards in isolated temporary repositories.
./scripts/lint.sh
./scripts/test.sh- This script updates global environments (
npm -g,pip), which can be disruptive. - Use
--dry-runfirst, and consider--only/--skipto control scope. - For npm 11+ global installs,
updatesmay retry once with npm's suggested one-shot--allow-scripts=...list so package postinstall steps can finish without changing persistent npm config. - Node updates are filtered against the active Node runtime and installed per package. An unexpected incompatible or otherwise failed package does not prevent later compatible packages from being attempted, but still fails the node module.
- Git-backed
shell/reposupdates never infer tracking branches or alter local work. Detached HEADs, branches without upstreams, and dirty worktrees warn and skip; diverged histories and failed pulls/post-pull actions fail the module. - These resilience changes preserve the public v2 CLI and JSONL contracts.
- Since
v2.0.0,updatesitself is distributed through GitHub Releases only. No third-party package manager channel is supported. - Since
v2.0.0, self-update is fixed to the canonical GitHub repoamanthanvi/updates;UPDATES_SELF_UPDATE_REPOis removed and setting it is an error. - Official self-update artifacts for
v2.1.2areupdates,updates-windows.zip,updates-release.json, andSHA256SUMS. - Normal runs throttle GitHub release checks to about once every 24 hours using a small local cache under
XDG_CACHE_HOME,~/Library/Caches,~/.cache, or%LOCALAPPDATA%\\updates; explicit--self-updateforces a live check. Cached tags are untrusted hints and never replace live release, digest, checksum, or manifest verification before applying an update. - Native Windows self-update works only for official standalone installs rooted at
%LOCALAPPDATA%\\Programs\\updateswith a validinstall-source.jsonreceipt. Manual file copies warn and skip instead of being overwritten. - On macOS, Homebrew casks are disabled by default; enable with
--brew-mode casksor--brew-mode greedy(or--full). On macOS 26+, cask upgrades may be blocked unless your terminal app is allowed under Privacy & Security → App Management (e.g. Ghostty). If you see a system notification like “<Terminal App> tried modifying your system…”, enable App Management or rerun with--brew-mode formula. - On WSL, updates apply to the Linux distro; native Windows updates require the native Windows entrypoints.
- Output uses ANSI colors when run in a TTY; disable with
--no-colororNO_COLOR=1. When--log-fileis used, colors are disabled to keep logs clean. - If Python is externally-managed (PEP 668), Bash runs a guarded user-site upgrade by default: it lists user packages, dry-runs wheel-only plans, skips packages that would add packages absent from the user site, use source distributions, or violate installed requirements, re-checks the safe subset in one combined dry-run, installs only that set, then runs
pip check. Pip must support--dry-run --reportfor the guarded path; missing support is an error under--onlyand a safe skip otherwise. Remainingpip checkfailures that were already present before install are warned after install instead of failing the guarded run. Guarded pip calls include--break-system-packagesonly when pip supports it.--pip-forceskips the guard and uses pip's system-protection override (dangerous).
See CONTRIBUTING.md.
MIT — see LICENSE.