Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [2.0.1] - 2026-06-17

### Fixed

- Retry global npm upgrades with `--legacy-peer-deps` only when npm fails with `ERESOLVE`, allowing packages with upstream optional peer metadata conflicts to update while keeping normal npm failures strict.

## [2.0.0] - 2026-06-08

### Added
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A small, modular CLI to update common macOS, Linux, WSL, and Windows tooling.

`v2.0.0` is the current release contract: Bash remains the entrypoint on macOS/Linux/WSL, while native Windows support uses `pwsh` via `updates.cmd` and `updates.ps1`.
`v2.0.x` is the current release contract: Bash remains the entrypoint on macOS/Linux/WSL, while native Windows support uses `pwsh` via `updates.cmd` and `updates.ps1`.

This script can be disruptive (it updates global environments). Use `--dry-run` and scope with `--only` / `--skip`.

Expand All @@ -29,10 +29,10 @@ sudo mkdir -p /usr/local/bin
sudo install -m 0755 ./updates /usr/local/bin/updates
```

Native Windows (`v2.0.0`, PowerShell 7):
Native Windows (`v2.0.1`, PowerShell 7):

```powershell
$version = '2.0.0'
$version = '2.0.1'
Comment thread
amanthanvi marked this conversation as resolved.
$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
Expand Down Expand Up @@ -101,7 +101,7 @@ Modules are auto-detected: if the underlying command isn’t installed, the modu
- `go`: update Go binaries from `GO_BINARIES` in `~/.updatesrc` (entries default to `@latest`)
- `macos`: list available macOS software updates via `softwareupdate -l` (disabled by default; enable with `--macos-updates` or `--full`)

Native Windows `v2.0.0` default-on modules: `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `go`.
Native Windows `v2.0.x` default-on modules: `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `go`.
On native Windows, `--full` selects every supported Windows module even if `SKIP_MODULES` in config would otherwise omit one; explicit `--skip` still wins.

## Configuration (`~/.updatesrc`)
Expand Down Expand Up @@ -154,9 +154,9 @@ Install what you actually use:

- This script updates _global_ environments (`npm -g`, `pip`), which can be disruptive.
- Use `--dry-run` first, and consider `--only`/`--skip` to control scope.
- `updates` itself is distributed through GitHub Releases only. No third-party package manager channel is supported for `updates` in `v2.0.0`.
- Self-update is fixed to the canonical GitHub repo `amanthanvi/updates`; `UPDATES_SELF_UPDATE_REPO` is removed in `v2.0.0` and setting it is an error.
- Official self-update artifacts for `v2.0.0` are `updates`, `updates-windows.zip`, `updates-release.json`, and `SHA256SUMS`.
- Since `v2.0.0`, `updates` itself 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 repo `amanthanvi/updates`; `UPDATES_SELF_UPDATE_REPO` is removed and setting it is an error.
- Official self-update artifacts for `v2.0.1` are `updates`, `updates-windows.zip`, `updates-release.json`, and `SHA256SUMS`.
- 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-update` forces a live check.
- Native Windows self-update works only for official standalone installs rooted at `%LOCALAPPDATA%\\Programs\\updates` with a valid `install-source.json` receipt. Manual file copies warn and skip instead of being overwritten.
- On macOS, Homebrew casks are disabled by default; enable with `--brew-mode casks` or `--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`.
Expand Down
2 changes: 1 addition & 1 deletion install-windows.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#requires -Version 7.0
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$Version = '2.0.0',
[string]$Version = '2.0.1',
[string]$InstallRoot,
[string]$SourceZip,
[string]$SourceRoot,
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/windows_test_lib.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ function Install-RepoWindowsRuntime {
[string]$RepoRoot,
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[string]$Version = '2.0.0',
[string]$Version = '2.0.1',
Comment thread
amanthanvi marked this conversation as resolved.
[switch]$WithReceipt
)

Expand Down
81 changes: 67 additions & 14 deletions tests/test_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export PATH="${stub_bin}:${BASE_PATH}"

# Self-update hits the network by default; disable for deterministic tests.
export UPDATES_SELF_UPDATE=0
SELF_UPDATE_CURRENT_TEST_VERSION="2.0.1"
SELF_UPDATE_NEXT_TEST_VERSION="2.0.2"

write_stub_to_dir() {
local dir="$1"
Expand Down Expand Up @@ -745,8 +747,8 @@ write_stub_to_dir "$self_update_cache_bin" uname 'echo Darwin'
# shellcheck disable=SC2016
write_stub_to_dir "$self_update_cache_bin" brew 'echo "brew $*" >>"$CALL_LOG"'
write_self_update_curl_stub "$self_update_cache_bin"
create_self_update_fixture "$self_update_cache_fixture" '2.0.1' 'unsupported-digest'
write_self_update_cache_with_metadata "${self_update_cache_xdg}/updates/self-update-amanthanvi_updates.cache" "$(date +%s)" 'v2.0.1' "$self_update_cache_fixture" 'md5:deadbeef'
create_self_update_fixture "$self_update_cache_fixture" "$SELF_UPDATE_NEXT_TEST_VERSION" 'unsupported-digest'
write_self_update_cache_with_metadata "${self_update_cache_xdg}/updates/self-update-amanthanvi_updates.cache" "$(date +%s)" "v${SELF_UPDATE_NEXT_TEST_VERSION}" "$self_update_cache_fixture" 'md5:deadbeef'
: >"$self_update_cache_http_log"
: >"$CALL_LOG"
out="$(CI='' UPDATES_SELF_UPDATE=1 XDG_CACHE_HOME="$self_update_cache_xdg" SELF_UPDATE_FIXTURE_DIR="$self_update_cache_fixture" SELF_UPDATE_CALL_LOG="$self_update_cache_http_log" PATH="${self_update_cache_bin}:${BASE_PATH}" "$self_update_cache_script" --only brew --no-emoji --no-color 2>&1)"
Expand All @@ -757,7 +759,7 @@ if grep -q '^curl https://api.github.com/repos/amanthanvi/updates/releases/lates
fi
grep -q '^curl https://example.invalid/updates-release.json$' "$self_update_cache_http_log"
echo "$out" | grep -q 'self-update manifest digest missing or unsupported; continuing'
if [ "$("$self_update_cache_script" --version)" != "2.0.0" ]; then
if [ "$("$self_update_cache_script" --version)" != "$SELF_UPDATE_CURRENT_TEST_VERSION" ]; then
echo "Expected cached unsupported digest metadata to leave installed version unchanged" >&2
exit 1
fi
Expand All @@ -775,15 +777,15 @@ write_stub_to_dir "$self_update_digest_bin" uname 'echo Darwin'
# shellcheck disable=SC2016
write_stub_to_dir "$self_update_digest_bin" brew 'echo "brew $*" >>"$CALL_LOG"'
write_self_update_curl_stub "$self_update_digest_bin"
create_self_update_fixture "$self_update_digest_fixture" '2.0.1' 'unsupported-digest'
printf 'checked_at=%s\nlatest_tag=%s\n' "$(date +%s)" 'v2.0.1' >"${self_update_digest_xdg}/updates/self-update-amanthanvi_updates.cache"
create_self_update_fixture "$self_update_digest_fixture" "$SELF_UPDATE_NEXT_TEST_VERSION" 'unsupported-digest'
printf 'checked_at=%s\nlatest_tag=%s\n' "$(date +%s)" "v${SELF_UPDATE_NEXT_TEST_VERSION}" >"${self_update_digest_xdg}/updates/self-update-amanthanvi_updates.cache"
: >"$self_update_digest_http_log"
: >"$CALL_LOG"
out="$(CI='' UPDATES_SELF_UPDATE=1 XDG_CACHE_HOME="$self_update_digest_xdg" SELF_UPDATE_FIXTURE_DIR="$self_update_digest_fixture" SELF_UPDATE_CALL_LOG="$self_update_digest_http_log" PATH="${self_update_digest_bin}:${BASE_PATH}" "$self_update_digest_script" --only brew --no-emoji --no-color 2>&1)"
echo "$out" | grep -q 'self-update manifest digest missing or unsupported; continuing'
grep -q '^curl https://api.github.com/repos/amanthanvi/updates/releases/latest$' "$self_update_digest_http_log"
grep -q '^curl https://example.invalid/updates-release.json$' "$self_update_digest_http_log"
if [ "$("$self_update_digest_script" --version)" != "2.0.0" ]; then
if [ "$("$self_update_digest_script" --version)" != "$SELF_UPDATE_CURRENT_TEST_VERSION" ]; then
echo "Expected unsupported digest metadata to leave installed version unchanged" >&2
exit 1
fi
Expand All @@ -800,13 +802,13 @@ write_stub_to_dir "$self_update_manifest_bin" uname 'echo Darwin'
# shellcheck disable=SC2016
write_stub_to_dir "$self_update_manifest_bin" brew 'echo "brew $*" >>"$CALL_LOG"'
write_self_update_curl_stub "$self_update_manifest_bin"
create_self_update_fixture "$self_update_manifest_fixture" '2.0.1' 'invalid-manifest'
create_self_update_fixture "$self_update_manifest_fixture" "$SELF_UPDATE_NEXT_TEST_VERSION" 'invalid-manifest'
: >"$self_update_manifest_http_log"
: >"$CALL_LOG"
out="$(CI='' UPDATES_SELF_UPDATE=1 XDG_CACHE_HOME="$self_update_manifest_xdg" SELF_UPDATE_FIXTURE_DIR="$self_update_manifest_fixture" SELF_UPDATE_CALL_LOG="$self_update_manifest_http_log" PATH="${self_update_manifest_bin}:${BASE_PATH}" "$self_update_manifest_script" --only brew --no-emoji --no-color 2>&1)"
echo "$out" | grep -q 'self-update manifest is invalid; continuing'
grep -q '^curl https://example.invalid/updates$' "$self_update_manifest_http_log"
if [ "$("$self_update_manifest_script" --version)" != "2.0.0" ]; then
if [ "$("$self_update_manifest_script" --version)" != "$SELF_UPDATE_CURRENT_TEST_VERSION" ]; then
echo "Expected invalid manifest to leave installed version unchanged" >&2
exit 1
fi
Expand All @@ -826,15 +828,15 @@ write_stub_to_dir "$self_update_fallback_bin" python 'exit 127'
write_stub_to_dir "$self_update_fallback_bin" python3 'exit 127'
write_stub_to_dir "$self_update_fallback_bin" node 'exit 127'
write_self_update_curl_stub "$self_update_fallback_bin"
create_self_update_fixture "$self_update_fallback_fixture" '2.0.1'
create_self_update_fixture "$self_update_fallback_fixture" "$SELF_UPDATE_NEXT_TEST_VERSION"
: >"$self_update_fallback_http_log"
: >"$CALL_LOG"
out="$(CI='' UPDATES_SELF_UPDATE=1 XDG_CACHE_HOME="$self_update_fallback_xdg" SELF_UPDATE_FIXTURE_DIR="$self_update_fallback_fixture" SELF_UPDATE_CALL_LOG="$self_update_fallback_http_log" PATH="${self_update_fallback_bin}:${BASE_PATH}" "$self_update_fallback_script" --only brew --no-emoji --no-color 2>&1)"
grep -q '^curl https://api.github.com/repos/amanthanvi/updates/releases/latest$' "$self_update_fallback_http_log"
grep -q '^curl https://example.invalid/updates-release.json$' "$self_update_fallback_http_log"
grep -q '^curl https://example.invalid/updates$' "$self_update_fallback_http_log"
if [ "$("$self_update_fallback_script" --version)" != "2.0.1" ]; then
echo "Expected shell-only self-update fallback to install version 2.0.1" >&2
if [ "$("$self_update_fallback_script" --version)" != "$SELF_UPDATE_NEXT_TEST_VERSION" ]; then
echo "Expected shell-only self-update fallback to install version ${SELF_UPDATE_NEXT_TEST_VERSION}" >&2
echo "$out" >&2
exit 1
fi
Expand All @@ -854,12 +856,12 @@ if [ -n "$SYSTEM_NODE" ]; then
write_stub_to_dir "$self_update_node_bin" brew 'echo "brew $*" >>"$CALL_LOG"'
write_stub_to_dir "$self_update_node_bin" node "exec \"$SYSTEM_NODE\" \"\$@\""
write_self_update_curl_stub "$self_update_node_bin"
create_self_update_fixture "$self_update_node_fixture" '2.0.1'
create_self_update_fixture "$self_update_node_fixture" "$SELF_UPDATE_NEXT_TEST_VERSION"
: >"$self_update_node_http_log"
: >"$CALL_LOG"
out="$(CI='' UPDATES_SELF_UPDATE=1 XDG_CACHE_HOME="$self_update_node_xdg" SELF_UPDATE_FIXTURE_DIR="$self_update_node_fixture" SELF_UPDATE_CALL_LOG="$self_update_node_http_log" PATH="${self_update_node_bin}:${BASE_PATH}" "$self_update_node_script" --only brew --no-emoji --no-color 2>&1)"
if [ "$("$self_update_node_script" --version)" != "2.0.1" ]; then
echo "Expected node-only self-update parsing to preserve bootstrap_min=0 and install version 2.0.1" >&2
if [ "$("$self_update_node_script" --version)" != "$SELF_UPDATE_NEXT_TEST_VERSION" ]; then
echo "Expected node-only self-update parsing to preserve bootstrap_min=0 and install version ${SELF_UPDATE_NEXT_TEST_VERSION}" >&2
echo "$out" >&2
exit 1
fi
Expand Down Expand Up @@ -933,6 +935,57 @@ out="$("$SCRIPT" --only node --no-emoji --no-color)"
echo "$out" | grep -q 'All global npm packages are up-to-date'
write_stub ncu 'echo "{\"npm\":\"11.7.0\"}"'

echo "Test: node retries npm ERESOLVE with legacy peer deps"
write_stub ncu 'echo "{\"@tarquinen/opencode-dcp\":\"3.1.13\"}"'
# shellcheck disable=SC2016
write_stub npm '
echo "npm $*" >>"$CALL_LOG"
case " $* " in
*" --legacy-peer-deps "*)
;;
*" @tarquinen/opencode-dcp@3.1.13 "*)
echo "npm error code ERESOLVE" >&2
exit 1
;;
esac
'
: >"$CALL_LOG"
npm_eresolve_stderr="${tmp_dir}/npm-eresolve-stderr.log"
out="$("$SCRIPT" --only node --no-emoji --no-color 2>"$npm_eresolve_stderr")"
echo "$out" | grep -q '^==> node END (OK)'
grep -q '^npm install -g -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q '^npm install -g --legacy-peer-deps -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q 'npm error code ERESOLVE' "$npm_eresolve_stderr"
grep -q 'retrying with --legacy-peer-deps' "$npm_eresolve_stderr"

echo "Test: node fails when npm ERESOLVE retry fails"
write_stub ncu 'echo "{\"@tarquinen/opencode-dcp\":\"3.1.13\"}"'
# shellcheck disable=SC2016
write_stub npm '
echo "npm $*" >>"$CALL_LOG"
echo "npm error code ERESOLVE" >&2
exit 1
'
: >"$CALL_LOG"
npm_eresolve_retry_stderr="${tmp_dir}/npm-eresolve-retry-stderr.log"
set +e
out="$("$SCRIPT" --only node --no-emoji --no-color 2>"$npm_eresolve_retry_stderr")"
rc=$?
set -e
if [ "$rc" -eq 0 ]; then
echo "Expected node to fail when the legacy peer deps retry also fails" >&2
exit 1
fi
echo "$out" | grep -q '^==> node END (FAIL)'
grep -q '^npm install -g -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q '^npm install -g --legacy-peer-deps -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q 'npm error code ERESOLVE' "$npm_eresolve_retry_stderr"
grep -q 'retrying with --legacy-peer-deps' "$npm_eresolve_retry_stderr"

write_stub ncu 'echo "{\"npm\":\"11.7.0\"}"'
# shellcheck disable=SC2016
write_stub npm 'echo "npm $*" >>"$CALL_LOG"'

echo "Test: node falls back to npx npm-check-updates"
rm -f "${stub_bin}/ncu"
# shellcheck disable=SC2016
Expand Down
Loading