This document is the source of truth for how the updates CLI behaves: flags, output, exit codes, module contracts, configuration, and release invariants.
If anything here disagrees with other docs, update the other docs (or this spec) so they match.
- Title: updates v2.0.0 specification
- Owner (DRI): Aman Thanvi (@amanthanvi)
- Status: Released
- Last updated: 2026-06-08
- Release date: 2026-06-08
- Links: Repository
A cross-platform CLI that updates common macOS, Linux, WSL, and Windows development tooling with a single command. The Unix implementation remains a single-file Bash entrypoint; native Windows support is delivered through PowerShell 7 entrypoints (updates.cmd, updates.ps1) plus a versioned payload layout.
Developers maintain a growing set of global tools and runtimes that each have their own update workflow. Running 5-10 separate update commands is tedious, easy to forget, and error-prone, and those workflows now span macOS, Linux distros, WSL, and native Windows. updates consolidates this into a single, safe-by-default command with dry-run, scoping, structured output for automation, and a first-party GitHub-only self-update channel.
v2.0 signals the next stable CLI contract: native Windows support is added, the self-update source is fixed to the canonical GitHub repo, and the previous custom self-update repo override is removed. From v2.0.0 onward, flag names, module names, exit codes, output format, and environment variables are again frozen until the next major version.
- Primary KPIs:
- All 17 modules pass lint + stub/contract tests across macOS, Linux, and Windows.
- JSONL output is parseable by
jqfor all event types. - Config file (
~/.updatesrc) correctly sets defaults overridden by CLI flags acrossHOME/USERPROFILElayouts.
- Guardrails:
- No module mutates state in
--dry-runmode. - Self-update never corrupts the installed entrypoint or payload (GitHub asset digest +
SHA256SUMS+ manifest verified). --jsonmode produces valid JSONL on stdout with zero human-readable text mixed in.- Official self-update for
updatesitself only trusts first-party GitHub release artifacts.
- No module mutates state in
- "Done means":
- A user can run
updateson a fresh macOS, Linux, or Windows machine, have all available modules detected and updated, and pipe--jsonoutput to a script for CI integration.
- A user can run
- Not a general-purpose package manager.
- Not an installer for dependencies (
brew,pipx,mas, etc.). - Not an automatic macOS upgrade tool (the
macosmodule lists updates; it does not install them). - No rich TUI, no interactive prompts (beyond what underlying tools produce).
- No telemetry or phone-home beyond self-update checks.
- No plugin/extension system for user-defined modules (modules are hardcoded).
- No third-party package-manager distribution channel for
updatesitself inv2.0.0.
- Solo developer (primary): Uses a Mac, Linux workstation, WSL distro, or Windows machine with Homebrew, Node, Python, Rust, Bun, etc. Wants a single command to keep everything current. Runs manually or via cron.
- CI/automation user: Runs
updates --json -n --no-self-updatein a pipeline to produce structured upgrade reports or keep build images current. - Polyglot developer: Uses mise/asdf for runtime management, uv for Python tooling, Go for CLI tools, and winget/Bun on Windows. Wants all of these covered without remembering individual update commands.
- Flow 1 — Default run:
updates(auto-detects available modules, runs safe defaults, prints human-readable progress and summary). - Flow 2 — Scoped run:
updates --only brew,node --dry-run(preview what would change for specific modules). - Flow 3 — Full upgrade:
updates --full(platform-specific "everything": casks/mas/macos on macOS, every supported module on native Windows). - Flow 4 — CI/scripted:
updates --json -n --no-self-update --log-level warn(structured output, non-interactive, quiet).
- Loading: Module boundary line printed (
==> <module> START); underlying tool output streams through. - Empty: "All packages are up-to-date" message per module when nothing to upgrade.
- Error:
ERROR: ...on stderr; module marked FAIL in summary; exit code 1. - Permission denied:
WARN:message when self-update lacks write access or a valid Windows install receipt; the run skips gracefully instead of elevating on Windows. - Offline/degraded: Self-update silently skips on network failure; individual modules fail based on their own tool's behavior.
- Accessibility: Plain-text output; emoji disabled with
--no-emoji; colors disabled with--no-color/NO_COLOR=1.
The v2.0.0 contract freezes: flag names, module names, exit codes, output format (boundary lines + summary), environment variables, JSONL event types, and the canonical self-update source. Adding new flags/modules in minor versions is allowed; removing or renaming is a breaking change requiring a major version bump.
updates [options]
Native Windows supports updates.cmd as the user-facing launcher and updates.ps1 as the stable bootstrap. macOS, Linux, and WSL continue to use the updates Bash entrypoint.
The CLI takes no positional arguments. Unknown options or unexpected arguments MUST error with exit code 2.
| Code | Meaning |
|---|---|
0 |
Success (including modules skipped due to missing deps) |
1 |
One or more selected modules failed |
2 |
Usage / configuration error (unknown flag, invalid value, unsupported platform, etc.) |
130 |
Interrupted (SIGINT on Unix; Ctrl+C / Ctrl+Break on Windows) |
143 |
Terminated (SIGTERM on Unix; cooperative terminate path on Windows) |
Information:
-h,--help: print help and exit0.--version: print the SemVer version (e.g.2.0.0) and exit0.--list-modules: print the module list and exit0.
Execution control:
--dry-run: MUST NOT execute mutating commands; prints what would run.--only <list>: run only the named modules (CSV or quoted space-separated).--skip <list>: skip the named modules.--strict: stop on the first module failure.-n,--non-interactive: avoid interactive prompts when possible.
Output:
--log-level <level>: set output verbosity. Levels:error,warn,info(default),debug.error: only ERROR messages.warn: WARN + ERROR messages.info: normal progress output (default; equivalent to previous behavior without--quietor--verbose).debug: all output including commands being run (prefixed with+).
--json: emit JSONL events to stdout; human-readable output goes to stderr.--no-emoji: disable emoji in output.--no-color: disable ANSI colors.--log-file <path>: append all output to a log file (colors disabled in file).
Self-update:
--self-update/--no-self-update: enable/disable self-update (default enabled).- Custom self-update repos are not supported in
v2.0.0;UPDATES_SELF_UPDATE_REPOis removed and setting it is an error.
Configuration:
--no-config: ignore~/.updatesrcconfig file.
Brew:
--brew-mode <mode>: Homebrew upgrade scope. Modes:formula(default on macOS): upgrade formulae only.casks: upgrade formulae + casks.greedy: upgrade formulae + casks (greedy).
--brew-cleanup/--no-brew-cleanup: runbrew cleanupafter upgrade (default enabled).
Module presets:
--full: enable the "everything" preset:- sets
--brew-mode greedy - enables
--mas-upgradeand--macos-updates - runs all other auto-detected modules (including
uv,mise, andgo;gostill requiresGO_BINARIESto be configured) - on native Windows, selects every supported Windows module and ignores
SKIP_MODULESfrom config; explicit--skipstill wins
- sets
--mas-upgrade/--no-mas-upgrade: enable themasmodule (default disabled).--macos-updates/--no-macos-updates: enable themacosmodule (default disabled).
Python:
--pip-force: pass--break-system-packagestopip(unsafe; for PEP 668 environments).--parallel <N>: parallelism for Bash pip upgrades (default4, minimum1). Native Windows rejects the CLI flag and warns+ignoresPARALLELin config.
Deprecated flags (accepted with WARN in 0.9.0; removed in 1.0.0):
| Old flag | Replacement |
| -------------------------------- | --------------------- | ---------------------------------------------- |
| --brew-casks | --brew-mode greedy | Matches v0.x default (--brew-greedy enabled) |
| --no-brew-casks | --brew-mode formula | |
| --brew-greedy | --brew-mode greedy | Only meaningful when casks are enabled |
| --no-brew-greedy | --brew-mode casks | Keeps casks but disables greedy |
| -q, --quiet | --log-level warn |
| -v, --verbose | --log-level debug |
| --python-break-system-packages | --pip-force |
If you previously used --brew-casks --no-brew-greedy, the equivalent is --brew-mode casks.
--brew-mode replaces the previous --brew-casks and --brew-greedy boolean flags with a single enum:
| Mode | brew update |
brew upgrade args |
Notes |
|---|---|---|---|
formula |
Yes | --formula |
Default on macOS. Safe; no app bundle changes. |
casks |
Yes | (no flag — upgrades formulae + casks) | Includes cask upgrades. |
greedy |
Yes | --greedy |
Includes greedy cask upgrades. |
On non-macOS platforms, the default is formula (casks are irrelevant on Linux).
--full sets --brew-mode greedy (among other things).
Replaces the previous --quiet / --verbose boolean flags with a single enum:
| Level | Behavior |
|---|---|
error |
Only ERROR: messages on stderr. Module boundaries and summary suppressed. |
warn |
WARN: + ERROR: messages. Module boundaries and summary still print. |
info |
Normal progress output (default). |
debug |
All output, including commands prefixed with +. |
When --json is active, --log-level controls the verbosity of human output on stderr. JSONL on stdout always includes all event types regardless of log level.
Replaces --python-break-system-packages. Passes --break-system-packages to pip install calls. This is dangerous on PEP 668 externally-managed environments and should only be used when the user explicitly wants to override system Python protections.
Module lists are parsed from a single argument:
- CSV is recommended:
--only brew,node - Whitespace inside the argument is supported, but must be quoted:
--only "brew node"
If --only/--skip includes an unknown module, the CLI MUST exit with code 2.
If --only includes a module that is not supported on the current platform, the CLI MUST exit with code 2.
Precedence: --skip overrides --only.
When --json is passed:
- stdout emits one JSON object per line (JSONL). No human-readable text is mixed in.
- stderr receives human-readable output (controlled by
--log-level). - Each JSON line has an
"event"field. Event types:
| Event | Fields | Emitted when |
|---|---|---|
module_start |
event, module, timestamp |
A module begins execution |
module_end |
event, module, status (ok|skip|fail), seconds, timestamp |
A module finishes |
upgrade |
event, module, package, from, to |
A package upgrade is detected (when parseable) |
log |
event, module, message, timestamp |
A normal log line is emitted |
warn |
event, module, message, timestamp |
A warning is emitted |
error |
event, module, message, timestamp |
An error is emitted |
summary |
event, ok, skip, fail, total_seconds, failures, timestamp |
Run completes |
timestamp is ISO 8601 UTC (e.g. 2026-02-07T12:00:00Z).
Modules that cannot parse upgrade details (e.g., brew, rustup) emit module_start/module_end but no upgrade events. The upgrade event is best-effort for modules where output is parseable (e.g., node, python, uv).
~/.updatesrc is an optional, line-oriented KEY=value file. Lines starting with # are comments. Empty lines are ignored. The parser tolerates a UTF-8 BOM and does not shell-source the file.
Home resolution is HOME first; on Windows, USERPROFILE is the fallback when HOME is unset.
Precedence: config file < CLI flags. CLI flags always win. Environment variables (UPDATES_*) are separate and follow existing behavior.
The file is skipped if it does not exist. Pass --no-config to ignore it entirely.
| Key | Type | Maps to | Example |
|---|---|---|---|
SKIP_MODULES |
CSV | --skip |
SKIP_MODULES=python,mas |
BREW_MODE |
enum | --brew-mode |
BREW_MODE=greedy |
BREW_CLEANUP |
0/1 | --[no-]brew-cleanup |
BREW_CLEANUP=0 |
MAS_UPGRADE |
0/1 | --[no-]mas-upgrade |
MAS_UPGRADE=1 |
MACOS_UPDATES |
0/1 | --[no-]macos-updates |
MACOS_UPDATES=1 |
LOG_LEVEL |
enum | --log-level |
LOG_LEVEL=warn |
PARALLEL |
int | --parallel |
PARALLEL=8 |
PIP_FORCE |
0/1 | --pip-force |
PIP_FORCE=1 |
SELF_UPDATE |
0/1 | --[no-]self-update |
SELF_UPDATE=0 |
NO_EMOJI |
0/1 | --no-emoji |
NO_EMOJI=1 |
NO_COLOR |
0/1 | --no-color |
NO_COLOR=1 |
GO_BINARIES |
CSV (module[@version]) | go module binary list | GO_BINARIES="golang.org/x/tools/gopls,github.com/go-delve/delve/cmd/dlv" |
REPOS_DIR |
path | repos module base directory | REPOS_DIR=/home/user/projects |
Unknown keys are silently ignored (forward compatibility).
Native Windows note: PARALLEL is only honored by the Bash implementation. The PowerShell runtime warns and ignores PARALLEL, and explicit --parallel <N> CLI usage exits 2.
When passed, ~/.updatesrc is not read. Useful for CI, testing, and debugging.
Output is intended to be stable and easy to grep.
- Normal progress goes to stdout (or stderr when
--jsonis active). - Warnings and errors go to stderr and are prefixed:
WARN: ...ERROR: ...
--log-level warn(and below) suppress normal output but MUST NOT suppressWARN:/ERROR:messages.- Emoji is enabled by default;
--no-emojiremoves emoji. - ANSI colors are enabled automatically when output is a TTY; set
NO_COLOR=1or pass--no-colorto disable. When--log-fileis used, colors are disabled in the log file.
Standardized boundaries:
- Each selected module prints boundary lines:
==> <module> START==> <module> END (<OK|SKIP|FAIL>) (<Ns>)
- After the run completes, a summary line is printed:
==> SUMMARY ok=<N> skip=<N> fail=<N> total=<Ns> [failures=<csv>]
See Section 3.8 for the full event type table.
- Every JSONL line is a valid JSON object terminated by
\n. - The
eventfield is always present and is one of the defined event types. - Unknown event types MAY be added in minor versions; consumers SHOULD ignore unknown types.
--log-file <path> duplicates output to the given file by teeing both stdout and stderr and appending (tee -a). The log directory is created if missing (mkdir -p). Colors are stripped from the log file.
When --json is active, the log file receives the human-readable stderr output, not the JSONL stream.
- ANSI colors are enabled when stderr/stdout are TTYs and
NO_COLORis not set. --no-colororNO_COLOR=1disables colors globally.--no-emojidisables emoji in output.TERM=dumbdisables colors.
| Variable | Default | Description |
|---|---|---|
UPDATES_ALLOW_NON_DARWIN=1 |
unset | Allow running on unsupported OSes (prints warning) |
UPDATES_SELF_UPDATE=0 |
1 |
Disable self-update |
UPDATES_SELF_UPDATED=1 |
unset | Internal guard: skip a second self-update after successful re-exec/relaunch |
NO_COLOR=1 |
unset | Disable ANSI colors |
CI |
unset | When set, self-update is disabled |
If UPDATES_SELF_UPDATE_REPO is set, the CLI MUST print an error and exit 2. Custom self-update repos were removed in v2.0.0.
- Each module has a canonical public name; the Unix implementation uses Bash functions named
module_<name>(), while the native Windows implementation uses PowerShell functions with the same public module names. - Modules are run sequentially in a fixed order.
- Some modules are opt-in in default runs for safety (
mas,macos). - On macOS,
--brew-modedefaults toformula(safe). - Modules are command-driven and auto-detected.
- Unsupported modules auto-skip in default runs;
--only <module>against an unsupported module MUST exit2.
Execution order: brew, shell, repos, linux, winget, node, bun, python, uv, mas, pipx, rustup, claude, pi, mise, go, macos.
| Module | macOS | Linux | WSL | Windows | Notes |
|---|---|---|---|---|---|
brew |
Yes | Yes | Yes | No | Requires brew |
shell |
Yes | Yes | Yes | No | Requires git; updates Oh My Zsh + custom plugins/themes |
repos |
Yes | Yes | Yes | No | Requires git; updates aman-*-setup repos under REPOS_DIR or ~/GitRepos |
linux |
No | Yes | Yes | No | Requires a supported package manager + optional sudo |
winget |
No | No | No | Yes | Requires winget; default-on on native Windows |
node |
Yes | Yes | Yes | Yes | Requires npm; resolves npm-check-updates per platform |
bun |
Yes | Yes | Yes | Yes | Requires bun; global package upgrades always, Bun CLI upgrade only when standalone-installed |
python |
Yes | Yes | Yes | Yes | Requires a resolved Python launcher with pip |
uv |
Yes | Yes | Yes | Yes | Requires uv; Windows self-update of uv only when standalone-installed |
mas |
Yes | No | No | No | Requires mas (opt-in) |
pipx |
Yes | Yes | Yes | Yes | Requires pipx |
rustup |
Yes | Yes | Yes | Yes | Requires rustup |
claude |
Yes | Yes | Yes | No | Requires claude |
pi |
Yes | Yes | Yes | No | Requires pi |
mise |
Yes | Yes | Yes | No | Requires mise |
go |
Yes | Yes | Yes | Yes | Requires go; binary list from config |
macos |
Yes | No | No | No | Requires softwareupdate (opt-in) |
Native Windows default runs auto-select winget, node, bun, python, uv, pipx, rustup, and go when their backing commands or config are present.
Fixed: brew > shell > repos > linux > winget > node > bun > python > uv > mas > pipx > rustup > claude > pi > mise > go > macos.
Rationale: platform package managers first (brew, linux, winget), then git-backed local repos, then language/runtime tools, then opt-in system modules last.
Internally:
- Return
0: success. - Return
1: failure. - Return
2: skipped (missing dependency in auto-detect mode).
User-visible:
- Skipped modules do not make the overall run fail.
- Failed modules make the run exit
1. - With
--strict, the script stops at the first failure.
Each module's contract includes: required commands, what it runs, and side effects.
Purpose: update and upgrade Homebrew formulae (and optionally casks).
- Requires:
brew - Non-dry-run commands:
brew update--brew-mode formula:brew upgrade --formula--brew-mode casks:brew upgrade--brew-mode greedy:brew upgrade --greedy- If
--brew-cleanup(default):brew cleanup
- Side effects: upgrades Homebrew-managed packages.
Purpose: update common shell customization tooling (Oh My Zsh) and its git-backed custom plugins/themes.
- Requires:
git - Detection:
- Oh My Zsh directory:
$ZSH(if set and exists) or~/.oh-my-zsh - Custom directory:
$ZSH_CUSTOM(if set) or<ZSH>/custom - Plugin/theme repos detected in
<custom>/plugins/*and<custom>/themes/*(directories with.git).
- Oh My Zsh directory:
- Non-dry-run:
git -C <dir> pull --ff-onlyfor each detected repo. - With
-n: setsGIT_TERMINAL_PROMPT=0. - Side effects: updates repos in-place.
Purpose: update development git repos matching aman-*-setup under a base directory.
- Requires:
git - Detection:
- Base directory from
REPOS_DIRconfig key, or defaults to~/GitRepos. - Globs
${base}/aman-*-setupfor directories containing.git. - Skips non-existent or non-git directories silently.
- Base directory from
- Non-dry-run:
git -C <dir> pull --ff-onlyfor each detected repo.- If
./scripts/update.shexists and is executable in the repo, runs it after a successful pull. - Post-pull script failure emits a warning but does not fail the module.
- If
- With
-n: setsGIT_TERMINAL_PROMPT=0. - Side effects: updates repos in-place; may run post-pull scripts.
Purpose: upgrade Linux system packages using the host distro package manager.
- Runs only on Linux (including WSL).
- Requires one of:
apt-get,dnf,yum,pacman,zypper,apk. Requiressudoif not root. - Non-dry-run commands (auto-detected):
apt-get:apt-get update+apt-get upgrade [-y](withDEBIAN_FRONTEND=noninteractivewhen-n)dnf:dnf upgrade [-y]yum:yum update [-y]pacman:pacman -Syu [--noconfirm]zypper:zypper refresh [--non-interactive]+zypper update [--non-interactive]apk:apk update+apk upgrade
- Side effects: upgrades OS-managed packages.
Purpose: upgrade installed Windows packages and applications via Windows Package Manager.
- Runs only on native Windows.
- Requires:
winget - Non-dry-run:
winget upgrade --all --silent --accept-source-agreements --accept-package-agreements --dry-run: prints the command that would run; no package-manager preview parsing is required.- Side effects: upgrades winget-managed packages/apps.
Purpose: upgrade global npm packages using npm-check-updates.
- Requires:
npmplus one ofncu.cmd,ncu, ornpx npm-check-updates. - On Windows, updater resolution order is
ncu.cmd, thenncu, thennpx npm-check-updates. - Non-dry-run: resolved updater command with
-g --jsonUpgradedto detect upgrades, thennpm install -g -- <name@version>.... - Side effects: upgrades global npm packages.
Purpose: upgrade Bun global packages and, when safe, the Bun CLI itself.
- Runs on macOS, Linux, and native Windows in
v2.0.0. - Requires:
bun - Non-dry-run:
bun update -gbun upgradeonly when Bun appears standalone-installed; if Bun appears package-managed or ownership is unclear, CLI self-update is skipped on native Windows
- Side effects: upgrades Bun global packages; may upgrade the Bun CLI when ownership is clearly standalone.
Purpose: upgrade global Python packages with pip.
- Requires: a resolved Python launcher with a working
pipmodule. Resolution order ispy -3, thenpython, thenpython3. - PEP 668 detection: if externally-managed, defaults to
--userscope. --pip-force: passes--break-system-packagesto pip.- Non-dry-run:
- Bash implementation:
<launcher> -m pip list --outdated --format=json [--user], then<launcher> -m pip install -U <pkg>in parallel batches of--parallel <N>. - Native Windows PowerShell implementation: same discovery/install flow, but upgrades run sequentially and
--parallel <N>is rejected.
- Bash implementation:
- With
-n: adds--no-inputto pip calls. - Side effects: upgrades Python packages; does not upgrade the Python interpreter itself.
Purpose: update the uv tool itself and all uv-managed tools.
- Requires:
uv - Non-dry-run commands:
uv self updateon macOS/Linux/WSLuv self updateon Windows only whenuvappears standalone-installeduv tool upgrade --all
- Side effects: updates uv binary and all uv-installed tools.
Purpose: upgrade Mac App Store apps.
- Disabled by default (enable with
--mas-upgrade,--full, or--only mas). - Requires:
mas - Non-dry-run:
mas upgrade - Side effects: upgrades App Store apps.
Purpose: upgrade pipx-managed apps.
- Requires:
pipx - Non-dry-run:
pipx upgrade-all - Side effects: upgrades pipx-installed tools.
Purpose: update Rust toolchains.
- Requires:
rustup - Non-dry-run:
rustup update - Side effects: updates installed Rust toolchains/components.
Purpose: update the Claude Code CLI.
- Requires:
claude - Non-dry-run:
claude update - Side effects: updates the Claude Code CLI.
Purpose: update installed extensions of the pi AI coding CLI (pinned sources are skipped by pi itself).
- Requires:
pi - Non-dry-run:
pi update - Side effects: updates installed
piextensions to their latest versions.
Purpose: update mise itself and upgrade all installed tool versions.
- Requires:
mise - Non-dry-run commands:
mise self-updatemise upgrade
- Side effects: updates mise binary and installed tool versions to latest matching constraints.
Purpose: update Go binaries from a user-specified list.
- Requires:
go - Binary list: read from
GO_BINARIESin~/.updatesrc(CSV ofmoduleormodule@versionentries).- If an entry omits
@version, it defaults to@latest(hands-off).
- If an entry omits
- Non-dry-run:
go install <module>@<version>for each entry. - If
GO_BINARIESis empty or unset:- default runs: skipped (return
2) --only go: error (return1)
- default runs: skipped (return
- Side effects: rebuilds and installs Go binaries to
$GOBINor$GOPATH/bin.
Purpose: list available macOS software updates.
- Disabled by default (enable with
--macos-updates,--full, or--only macos). - Requires:
softwareupdate - Non-dry-run:
softwareupdate -l - Side effects: lists updates only (does not install).
- The canonical self-update repo is fixed to
amanthanvi/updates. - Official distribution/update for
updatesitself is GitHub Releases only. No third-party package-manager channel is supported forupdatesinv2.0.0. - If
UPDATES_SELF_UPDATE_REPOis set, the CLI MUST error with exit code2. - Required official release assets:
updatesupdates-windows.zipupdates-release.jsonSHA256SUMS
updates-release.jsonis the canonical self-update manifest for both Unix and Windows. Required fields:versionsource_repochannelbootstrap_minwindows_assetunix_assetchecksum_asset
- Normal eligible runs throttle the GitHub release metadata check to at most once every 24 hours using a best-effort local cache:
${XDG_CACHE_HOME}/updateswhenXDG_CACHE_HOMEis set${HOME}/Library/Caches/updateson macOS otherwise${HOME}/.cache/updateson Linux/WSL otherwise%LOCALAPPDATA%\\updateson native Windows
- The cache stores only release-check metadata (
checked_at,latest_tag) and is ignored if missing or invalid. - Passing
--self-updateforces a live metadata refresh even when the cache is fresh. - Self-update is skipped when:
--no-self-updateorUPDATES_SELF_UPDATE=0--dry-runmodeCIenvironment variable is set- Running from a git checkout (development)
- Installed as a symlink
- Eligible releases MUST be published (
draft=false,prerelease=false), immutable, and expose GitHub assetdigestvalues for the required assets. - Verification flow:
- download the required assets
- verify each download against the GitHub release-asset
digest - verify the platform artifact against
SHA256SUMS - verify
updates-release.jsonagainst the requested tag, canonical repo, schema, and expected asset names - verify the installed payload version before re-exec/relaunch
- Unix/macOS/Linux/WSL self-update:
- download
updates - replace the installed script in place
- reopen the installed copy and verify its embedded version before re-exec
- re-exec once, guarded by
UPDATES_SELF_UPDATED=1
- download
- Native Windows self-update is supported only for official standalone installs rooted at
%LOCALAPPDATA%\\Programs\\updates. install-windows.ps1creates the official native Windows standalone layout from the publishedupdates-windows.ziprelease asset, or from a local-SourceZip/repository-SourceRootfor verification.- Native Windows standalone layout:
updates.cmdupdates.ps1install-source.jsoncurrent.txtprevious.txtversions/<semver>/manifest.jsonversions/<semver>/updates-main.ps1
install-source.jsonis the canonical Windows authorization record. Required fields:kind=standalonechannel=github-releasesource_repo=amanthanvi/updatesscope=userinstalled_version
- Native Windows self-update additionally requires:
- a valid
install-source.json - a user-writable install root proven by a real create/delete probe
- a non-symlink, non-git-checkout, non-mixed/manual partial install
- a valid
- Native Windows apply flow:
- validate receipt + release metadata
- download and verify
updates-windows.zip,updates-release.json, andSHA256SUMS - extract into a staging directory
- validate payload structure and
bootstrap_min - update
previous.txt, atomically switchcurrent.txt, then relaunch once withUPDATES_SELF_UPDATED=1
- Any receipt, trust, digest, checksum, manifest, extraction, staging, or relaunch failure leaves the current version active and prints a warning. The run stays non-fatal unless another selected module fails.
- Secrets: No secrets are stored or required. Self-update uses unauthenticated GitHub API calls.
- Supply chain: Self-update is fixed to first-party GitHub Releases. Eligible releases must be immutable and publish
updates-release.json,SHA256SUMS, and required assets with GitHub assetdigestvalues. Unix verifies the script before re-exec; Windows verifies receipt, manifest, payload structure, and staging before relaunch. - PII: No user data is collected or transmitted.
- Abuse cases:
- Malicious or tampered GitHub release asset: mitigated by immutable releases, GitHub asset digests,
SHA256SUMS, manifest validation, and HTTPS. - pip parallel upgrades: stderr interleaving is cosmetic, not a security issue.
--pip-forceis explicitly opt-in and documented as unsafe.
- Malicious or tampered GitHub release asset: mitigated by immutable releases, GitHub asset digests,
- Privilege escalation:
sudois only used for Linux system package upgrades. Native Windows self-update never elevates; unknown or non-writable layouts warn and skip.
| Failure | Detection | User impact | System behavior | Recovery | Blast radius |
|---|---|---|---|---|---|
| Module dep missing (auto) | command -v / command probe |
Module skipped | Return 2, continue |
None needed | Single module |
Module dep missing (--only) |
command -v / command probe |
Error message | Return 1, module fails |
Install the dep | Single module |
| Module command fails | Non-zero exit | Module marked FAIL | Continue (or stop if --strict) |
Re-run or fix manually | Single module |
| Self-update download fails | HTTP/tool non-zero | Warning printed | Continues without update | Re-run later | None |
| Self-update digest/checksum mismatch | Digest or SHA256 compare | Warning printed | Continues without update | Report issue | None |
| Windows install receipt missing/invalid | Receipt validation | Warning printed | Continues without update | Reinstall from official zip | None |
| Self-update manifest/payload invalid | Manifest/schema/structure check | Warning printed | Continues without update | Report issue / reinstall | None |
| Network unreachable | Tool-specific timeout | Module fails | Continue | Fix network, re-run | Affected modules |
| Config file parse error | Line parser rejects a value | Warning printed | Continues with defaults | Fix ~/.updatesrc |
All config values |
| Disk full | Write fails | Module fails | Continue | Free space | Affected module |
| SIGINT / Ctrl+C / Ctrl+Break received | Trap / console handler | Interrupted message | Exit 130 |
Re-run | Current module may be partial |
- Self-update: no automatic retries. Metadata checks are throttled with a 24-hour cache; explicit
--self-updatebypasses the cache. - Module commands: no retries. Modules run the underlying tool once; if it fails, the module fails.
- No circuit breakers (modules are independent and run sequentially).
- Logging:
--log-levelcontrols verbosity (error/warn/info/debug).--log-filepersists output. - What not to log: No PII, no environment variable values, no file contents.
- Metrics: JSONL events include per-module timing (
secondsfield) and overalltotal_seconds. - Alerts: Not applicable (CLI tool, not a service). Users can parse JSONL
summaryevents for CI alerting. - Debugging:
--log-level debugprints every command before execution.--jsonprovides structured events for programmatic analysis.- Self-update logs detailed skip reasons at debug level.
Ship all v1.0 features (config file, --json, new modules, --brew-mode, --log-level, --pip-force, -n) with:
- Old flags still accepted.
- Each use of a deprecated flag prints
WARN: --<old-flag> is deprecated; use <new-flag> instead. - No behavior change — deprecated flags map to their replacements internally.
- Remove all deprecated flags. Using them produces
ERROR:and exit code2. - CLI contract is frozen.
| v0.x flag | v1.0 replacement | Notes |
|---|---|---|
--brew-casks |
--brew-mode greedy |
Matches v0.x default (--brew-greedy enabled) |
--no-brew-casks |
--brew-mode formula |
|
--brew-greedy |
--brew-mode greedy |
|
--no-brew-greedy |
--brew-mode casks |
Disables greedy but keeps casks |
-q, --quiet |
--log-level warn |
|
-v, --verbose |
--log-level debug |
|
--python-break-system-packages |
--pip-force |
- Add native Windows support via
updates.cmdandupdates.ps1. - Remove
UPDATES_SELF_UPDATE_REPO; setting it MUST produceERROR:and exit2. - Official install/update for
updatesitself is GitHub Releases only. - Native Windows self-update works only for official standalone installs with a valid
install-source.jsonreceipt.
- Lint:
./scripts/lint.sh(runsbash -n,shellcheck,shfmt -d). - Tests:
./scripts/test.sh(runs./tests/test_cli.sh). - Tests use temporary
PATHstubs to avoid modifying the developer's machine.
- Unit/integration (stub-based):
- Existing tests for brew, node, python, linux, shell, mas, macos, self-update.
- New stubs for: winget, bun, uv, mise, go, repos modules.
- Config file parsing tests (precedence, unknown keys,
--no-config). --brew-modeenum validation tests.--log-leveloutput filtering tests.--jsonJSONL output validation (parse each line as JSON in tests).UPDATES_SELF_UPDATE_REPOerror tests (v2.0.0).- Native Windows contract tests for receipt validation, relaunch guard, and self-update artifact verification.
- Edge cases:
GO_BINARIESempty/unset (go module skips).--json+--log-fileinteraction.--onlywith new module names.- UTF-8 BOM config files.
HOMEunset withUSERPROFILEpresent on Windows.
- Not in scope for the v2.0.0 suite:
- Real-tool integration tests (would require installing all tools).
- Full matrix of third-party tool installs on Windows.
- Given a macOS machine with brew, node, python, uv, mise, go, pipx, rustup, claude, pi installed, when
updatesruns, then all detected modules execute successfully and summary showsfail=0. - Given
--dry-run, when any module runs, then no mutating commands are executed. - Given
--json, whenupdatesruns, then stdout contains only valid JSONL and stderr contains human output. - Given
~/.updatesrcwithSKIP_MODULES=pythonand CLI flag--only python, then CLI flag wins and python module runs. - Given
--brew-mode greedy, when brew module runs, thenbrew upgrade --greedyis called. - Given
GO_BINARIES="golang.org/x/tools/gopls"in config, when go module runs, thengo install golang.org/x/tools/gopls@latestis called. - Given a deprecated flag (e.g.,
--verbose) in 0.9.0, when used, then aWARN:deprecation message is printed and the flag maps to--log-level debug. - Given a deprecated flag in 1.0.0, when used, then
ERROR:is printed and exit code is2. - Given an official native Windows standalone install with a valid receipt, when
updates --self-updatefinds a newer immutable release, then it verifiesupdates-windows.zip,updates-release.json, andSHA256SUMS, flipscurrent.txt, and relaunches once. - Given
UPDATES_SELF_UPDATE_REPOis set, whenupdatesruns, then it printsERROR:and exits2. - Given native Windows receives
Ctrl+CorCtrl+Break, whenupdatesis interrupted, then it exits130.
Versioning:
- SemVer, tagged as
vX.Y.Z. - Script version is
UPDATES_VERSION="<version>"insideupdates. - Windows payload version is
$script:UpdatesVersion = '<version>'insideupdates-main.ps1. - Windows payload manifests and
updates-release.jsonMUST carry the same release version as the Git tag.
Invariants (enforced in CI/release):
- Tag version
vX.Y.ZMUST matchUPDATES_VERSION="X.Y.Z". - Tag version
vX.Y.ZMUST match$script:UpdatesVersion = 'X.Y.Z'. - Tag version
vX.Y.ZMUST matchupdates-release.jsonversion. CHANGELOG.mdMUST contain a header## [X.Y.Z]for the release.- Published GitHub Releases MUST include
updates,updates-windows.zip,updates-release.json, andSHA256SUMS. - Published GitHub Releases MUST be immutable before they are eligible for runtime self-update.
- Required release assets MUST expose GitHub
digestvalues and pass local verification before publish.
Maintainer workflow:
./scripts/release.sh X.Y.Z(validates invariants, runs lint/tests, creates annotated tag).- Create a draft release, upload
updates,updates-windows.zip,updates-release.json, andSHA256SUMS, verify uploaded asset digests and downloaded smoke artifacts, then publish. - Post-publish, run
gh release verify <tag>andgh release verify-asset <tag> <artifact-path>.
The Unix Bash entrypoint uses navigable section markers for organization:
grep '^# SECTION:' updatesSections: globals, output, colors, platform, registry, utilities, self-update, cli, selection, modules, runner.
Native Windows uses:
install-windows.ps1as the official standalone layout installerupdates.cmdas the thin launcherupdates.ps1as the stable bootstrapversions/<semver>/updates-main.ps1as the mutable payloadinstall-source.json,current.txt, andprevious.txtas the standalone update state
config_set_bool <CONFIG_KEY> <value> <VARIABLE>— validates and sets boolean config values. Used byread_config()for 7 boolean keys.
The Unix updates script exceeds the project's 500-line guideline. This is an intentional exception: the Unix single-file distribution model (self-update downloads one file, make install copies one file) makes multi-file splitting impractical there. Native Windows intentionally uses a small bootstrap plus versioned payload directories instead of a single mutable file.
| Date | Decision | Alternatives | Rationale | Consequences |
|---|---|---|---|---|
| 2026-02-07 | Full CLI surface frozen at 1.0 | Partial freeze (flags only) | Users need confidence the contract is stable | Must bump major version for any removal/rename |
| 2026-02-07 | --brew-mode enum replaces 3 booleans |
Keep booleans, add short aliases | Single flag is clearer; reduces flag sprawl | Breaking change; needs 0.9.0 deprecation period |
| 2026-02-07 | --log-level replaces --quiet/--verbose |
Keep both pairs | More granular; standard pattern | Breaking change |
| 2026-02-07 | --pip-force replaces --python-break-system-packages |
Keep long name | Too verbose; confusing for users | Breaking change |
| 2026-02-07 | -n = --non-interactive |
-n = --dry-run (make convention) |
Matches apt/apt-get convention | --dry-run has no short alias |
| 2026-02-07 | JSONL to stdout, human to stderr | JSON replaces human; JSON to file | Allows piping + visual progress simultaneously | --log-file captures stderr (human) only |
| 2026-02-07 | Full JSONL event stream (start/log/upgrade/warn/error/end/summary) | Summary-only JSON | Maximum fidelity for automation | More complex to implement; verbose output |
| 2026-02-07 | ~/.updatesrc as a flat KEY=value config file |
TOML, XDG config dir | Minimal format; easy to parse across Bash and PowerShell | No structured nesting; flat keys only |
| 2026-02-07 | Config < flags (flags always win) | Config < env < flags | Simpler model; env vars are separate concern | Users can't override config via env (use flags) |
| 2026-02-07 | New modules: uv, mise, go | Also docker | Docker pulls are slow/large; poor fit for quick update tool | Can add docker in a minor if demand exists |
| 2026-02-07 | Go module reads module list from config | Auto-detect from binaries | Can't reliably infer module path from binary name | Requires user to maintain GO_BINARIES list (module paths; versions default to @latest) |
| 2026-02-07 | uv: self update + tool upgrade --all | Tool upgrade only | uv's self-update is fast and safe | Touches uv's own binary |
| 2026-02-07 | mise: self-update + upgrade | Also plugins upgrade | Keeps scope minimal; plugins update implicitly | Users with stale plugins must update manually |
| 2026-02-07 | --full runs uv/mise/go too |
Keep --full system-only |
"One command" should mean everything possible | go still needs GO_BINARIES configured |
| 2026-02-07 | SHA256 sufficient for self-update (no cosign/GPG) | Add cosign verification | Adds complexity + dependency; HTTPS+SHA256 is pragmatic | Weaker supply-chain guarantee |
| 2026-02-07 | 0.9.0 deprecation release, then 1.0.0 | Direct to 1.0; incremental 0.9.x | Gives users a migration window for renamed flags | Two releases to manage |
| 2026-02-07 | Test bar: current level + new module stubs | Full split suite + Linux CI | Practical for a single-maintainer project | No Linux CI; limited edge-case coverage |
| 2026-04-26 | Native Windows support ships in v2.0.0 | Keep Windows out of scope | Cross-platform coverage now includes first-class Windows | Requires PowerShell bootstrap + Windows contract suite |
| 2026-04-26 | Self-update source fixed to amanthanvi/updates |
Keep repo override env var | Stronger trust model; less ambiguous support surface | UPDATES_SELF_UPDATE_REPO removal is a breaking change |
| 2026-04-26 | updates distribution remains GitHub Releases only |
Publish to third-party managers | Lowest-friction official channel; simpler trust boundary | No first-party npm/PyPI/Winget/etc. channel for updates itself in v2.0.0 |
| 2026-04-26 | Windows uses receipt-gated standalone self-update | Delegate to package managers | Avoids overwriting unknown installs; first-party ownership | Manual copies and non-official layouts skip self-update |
| 2026-04-26 | Immutable releases + asset digests required for runtime updates | SHA256 only | Stronger supply-chain guarantees without adding local deps | Release pipeline must verify/publish in a stricter order |
- Users install and manage dependencies (brew, ncu, winget, bun, uv, mise, etc.) themselves.
- Bash
/bin/bashis available on macOS/Linux/WSL and PowerShell 7 (pwsh) is available on native Windows. - GitHub Releases remain available and free for self-update checks.
- GitHub release assets continue to expose
digestmetadata through the REST API. uv self update,bun upgrade, andwinget upgraderemain stable subcommands/options.
- None (current behavior: in
--jsonmode,--log-filecaptures the human stderr stream; JSONL remains on stdout).
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Bash 3.2 compatibility issues with new Unix-side features | Medium | High | Test on macOS system Bash explicitly |
Windows console close/terminate paths do not always map cleanly to 143 |
Medium | Medium | Guarantee 130; keep 143 cooperative-only and best-effort on Windows |
uv self update, bun upgrade, or winget upgrade changes behavior |
Low | Medium | Pin to known-good behavior; skip gracefully on failure |
| Release published without required immutability/digests/assets | Low | High | Draft-upload-verify-publish flow; post-publish gh release verify gates |
| JSONL format changes needed post-2.0 | Low | High | Design events to be additive; consumers ignore unknown fields |
| Go module is low-value (few users maintain GO_BINARIES list) | Medium | Low | Module is opt-in by nature; low maintenance cost |