Skip to content

Latest commit

 

History

History
869 lines (647 loc) · 53.6 KB

File metadata and controls

869 lines (647 loc) · 53.6 KB

updates — Specification (v2.0.0)

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.

0) Metadata

  • 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

1) Executive Summary

1.1 What we're building

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.

1.2 Problem statement / why now

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.

1.3 Success metrics

  • Primary KPIs:
    • All 17 modules pass lint + stub/contract tests across macOS, Linux, and Windows.
    • JSONL output is parseable by jq for all event types.
    • Config file (~/.updatesrc) correctly sets defaults overridden by CLI flags across HOME/USERPROFILE layouts.
  • Guardrails:
    • No module mutates state in --dry-run mode.
    • Self-update never corrupts the installed entrypoint or payload (GitHub asset digest + SHA256SUMS + manifest verified).
    • --json mode produces valid JSONL on stdout with zero human-readable text mixed in.
    • Official self-update for updates itself only trusts first-party GitHub release artifacts.
  • "Done means":
    • A user can run updates on a fresh macOS, Linux, or Windows machine, have all available modules detected and updated, and pipe --json output to a script for CI integration.

1.4 Non-goals / out of scope

  • Not a general-purpose package manager.
  • Not an installer for dependencies (brew, pipx, mas, etc.).
  • Not an automatic macOS upgrade tool (the macos module 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 updates itself in v2.0.0.

2) Users & UX

2.1 Personas

  • 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-update in 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.

2.2 Primary flows

  • 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).

2.3 UX states checklist

  • 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.

3) CLI Contract (v2.0 stability guarantee)

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.

3.1 Invocation

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.

3.2 Exit codes

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)

3.3 Options

Information:

  • -h, --help: print help and exit 0.
  • --version: print the SemVer version (e.g. 2.0.0) and exit 0.
  • --list-modules: print the module list and exit 0.

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 --quiet or --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_REPO is removed and setting it is an error.

Configuration:

  • --no-config: ignore ~/.updatesrc config 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: run brew cleanup after upgrade (default enabled).

Module presets:

  • --full: enable the "everything" preset:
    • sets --brew-mode greedy
    • enables --mas-upgrade and --macos-updates
    • runs all other auto-detected modules (including uv, mise, and go; go still requires GO_BINARIES to be configured)
    • on native Windows, selects every supported Windows module and ignores SKIP_MODULES from config; explicit --skip still wins
  • --mas-upgrade / --no-mas-upgrade: enable the mas module (default disabled).
  • --macos-updates / --no-macos-updates: enable the macos module (default disabled).

Python:

  • --pip-force: pass --break-system-packages to pip (unsafe; for PEP 668 environments).
  • --parallel <N>: parallelism for Bash pip upgrades (default 4, minimum 1). Native Windows rejects the CLI flag and warns+ignores PARALLEL in 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.

3.4 --brew-mode details

--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).

3.5 --log-level details

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.

3.6 --pip-force details

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.

3.7 Module lists (--only, --skip)

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.

3.8 --json (JSONL streaming output)

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).

4) Configuration File (~/.updatesrc)

4.1 Format & precedence

~/.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.

4.2 Supported keys

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.

4.3 --no-config flag

When passed, ~/.updatesrc is not read. Useful for CI, testing, and debugging.

5) Output & Logging

5.1 Human output (boundary lines, summary)

Output is intended to be stable and easy to grep.

  • Normal progress goes to stdout (or stderr when --json is active).
  • Warnings and errors go to stderr and are prefixed:
    • WARN: ...
    • ERROR: ...
  • --log-level warn (and below) suppress normal output but MUST NOT suppress WARN:/ERROR: messages.
  • Emoji is enabled by default; --no-emoji removes emoji.
  • ANSI colors are enabled automatically when output is a TTY; set NO_COLOR=1 or pass --no-color to disable. When --log-file is 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>]

5.2 JSONL event stream contract

See Section 3.8 for the full event type table.

  • Every JSONL line is a valid JSON object terminated by \n.
  • The event field 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.

5.3 --log-file behavior

--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.

5.4 Color / emoji

  • ANSI colors are enabled when stderr/stdout are TTYs and NO_COLOR is not set.
  • --no-color or NO_COLOR=1 disables colors globally.
  • --no-emoji disables emoji in output.
  • TERM=dumb disables colors.

6) Environment Variables

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.

7) Module System

7.1 Principles

  • 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-mode defaults to formula (safe).
  • Modules are command-driven and auto-detected.
  • Unsupported modules auto-skip in default runs; --only <module> against an unsupported module MUST exit 2.

7.2 Module list & platform matrix

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.

7.3 Module execution order

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.

7.4 Skip vs failure semantics

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.

8) Module Specifications

Each module's contract includes: required commands, what it runs, and side effects.

8.1 brew

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.

8.2 shell

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).
  • Non-dry-run: git -C <dir> pull --ff-only for each detected repo.
  • With -n: sets GIT_TERMINAL_PROMPT=0.
  • Side effects: updates repos in-place.

8.3 repos

Purpose: update development git repos matching aman-*-setup under a base directory.

  • Requires: git
  • Detection:
    • Base directory from REPOS_DIR config key, or defaults to ~/GitRepos.
    • Globs ${base}/aman-*-setup for directories containing .git.
    • Skips non-existent or non-git directories silently.
  • Non-dry-run: git -C <dir> pull --ff-only for each detected repo.
    • If ./scripts/update.sh exists 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.
  • With -n: sets GIT_TERMINAL_PROMPT=0.
  • Side effects: updates repos in-place; may run post-pull scripts.

8.4 linux

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. Requires sudo if not root.
  • Non-dry-run commands (auto-detected):
    • apt-get: apt-get update + apt-get upgrade [-y] (with DEBIAN_FRONTEND=noninteractive when -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.

8.5 winget

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.

8.6 node

Purpose: upgrade global npm packages using npm-check-updates.

  • Requires: npm plus one of ncu.cmd, ncu, or npx npm-check-updates.
  • On Windows, updater resolution order is ncu.cmd, then ncu, then npx npm-check-updates.
  • Non-dry-run: resolved updater command with -g --jsonUpgraded to detect upgrades, then npm install -g -- <name@version>....
  • Side effects: upgrades global npm packages.

8.7 bun

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 -g
    • bun upgrade only 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.

8.8 python

Purpose: upgrade global Python packages with pip.

  • Requires: a resolved Python launcher with a working pip module. Resolution order is py -3, then python, then python3.
  • PEP 668 detection: if externally-managed, defaults to --user scope.
  • --pip-force: passes --break-system-packages to 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.
  • With -n: adds --no-input to pip calls.
  • Side effects: upgrades Python packages; does not upgrade the Python interpreter itself.

8.9 uv

Purpose: update the uv tool itself and all uv-managed tools.

  • Requires: uv
  • Non-dry-run commands:
    • uv self update on macOS/Linux/WSL
    • uv self update on Windows only when uv appears standalone-installed
    • uv tool upgrade --all
  • Side effects: updates uv binary and all uv-installed tools.

8.10 mas

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.

8.11 pipx

Purpose: upgrade pipx-managed apps.

  • Requires: pipx
  • Non-dry-run: pipx upgrade-all
  • Side effects: upgrades pipx-installed tools.

8.12 rustup

Purpose: update Rust toolchains.

  • Requires: rustup
  • Non-dry-run: rustup update
  • Side effects: updates installed Rust toolchains/components.

8.13 claude

Purpose: update the Claude Code CLI.

  • Requires: claude
  • Non-dry-run: claude update
  • Side effects: updates the Claude Code CLI.

8.14 pi

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 pi extensions to their latest versions.

8.15 mise

Purpose: update mise itself and upgrade all installed tool versions.

  • Requires: mise
  • Non-dry-run commands:
    • mise self-update
    • mise upgrade
  • Side effects: updates mise binary and installed tool versions to latest matching constraints.

8.16 go

Purpose: update Go binaries from a user-specified list.

  • Requires: go
  • Binary list: read from GO_BINARIES in ~/.updatesrc (CSV of module or module@version entries).
    • If an entry omits @version, it defaults to @latest (hands-off).
  • Non-dry-run: go install <module>@<version> for each entry.
  • If GO_BINARIES is empty or unset:
    • default runs: skipped (return 2)
    • --only go: error (return 1)
  • Side effects: rebuilds and installs Go binaries to $GOBIN or $GOPATH/bin.

8.17 macos

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).

9) Self-Update

  • The canonical self-update repo is fixed to amanthanvi/updates.
  • Official distribution/update for updates itself is GitHub Releases only. No third-party package-manager channel is supported for updates in v2.0.0.
  • If UPDATES_SELF_UPDATE_REPO is set, the CLI MUST error with exit code 2.
  • Required official release assets:
    • updates
    • updates-windows.zip
    • updates-release.json
    • SHA256SUMS
  • updates-release.json is the canonical self-update manifest for both Unix and Windows. Required fields:
    • version
    • source_repo
    • channel
    • bootstrap_min
    • windows_asset
    • unix_asset
    • checksum_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}/updates when XDG_CACHE_HOME is set
    • ${HOME}/Library/Caches/updates on macOS otherwise
    • ${HOME}/.cache/updates on Linux/WSL otherwise
    • %LOCALAPPDATA%\\updates on native Windows
  • The cache stores only release-check metadata (checked_at, latest_tag) and is ignored if missing or invalid.
  • Passing --self-update forces a live metadata refresh even when the cache is fresh.
  • Self-update is skipped when:
    • --no-self-update or UPDATES_SELF_UPDATE=0
    • --dry-run mode
    • CI environment 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 asset digest values 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.json against 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
  • Native Windows self-update is supported only for official standalone installs rooted at %LOCALAPPDATA%\\Programs\\updates.
  • install-windows.ps1 creates the official native Windows standalone layout from the published updates-windows.zip release asset, or from a local -SourceZip/repository -SourceRoot for verification.
  • Native Windows standalone layout:
    • updates.cmd
    • updates.ps1
    • install-source.json
    • current.txt
    • previous.txt
    • versions/<semver>/manifest.json
    • versions/<semver>/updates-main.ps1
  • install-source.json is the canonical Windows authorization record. Required fields:
    • kind=standalone
    • channel=github-release
    • source_repo=amanthanvi/updates
    • scope=user
    • installed_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
  • Native Windows apply flow:
    • validate receipt + release metadata
    • download and verify updates-windows.zip, updates-release.json, and SHA256SUMS
    • extract into a staging directory
    • validate payload structure and bootstrap_min
    • update previous.txt, atomically switch current.txt, then relaunch once with UPDATES_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.

10) Security & Privacy

  • 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 asset digest values. 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-force is explicitly opt-in and documented as unsafe.
  • Privilege escalation: sudo is only used for Linux system package upgrades. Native Windows self-update never elevates; unknown or non-writable layouts warn and skip.

11) Reliability & Failure Modes

11.1 Failure modes table

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

11.2 Retries/timeouts

  • Self-update: no automatic retries. Metadata checks are throttled with a 24-hour cache; explicit --self-update bypasses 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).

12) Observability

  • Logging: --log-level controls verbosity (error/warn/info/debug). --log-file persists output.
  • What not to log: No PII, no environment variable values, no file contents.
  • Metrics: JSONL events include per-module timing (seconds field) and overall total_seconds.
  • Alerts: Not applicable (CLI tool, not a service). Users can parse JSONL summary events for CI alerting.
  • Debugging:
    • --log-level debug prints every command before execution.
    • --json provides structured events for programmatic analysis.
    • Self-update logs detailed skip reasons at debug level.

13) Rollout, Migration, Compatibility

13.1 v0.9.0 deprecation release

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.

13.2 v1.0.0 stable release

  • Remove all deprecated flags. Using them produces ERROR: and exit code 2.
  • CLI contract is frozen.

13.3 Flag migration table

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

13.4 v2.0.0 breaking release

  • Add native Windows support via updates.cmd and updates.ps1.
  • Remove UPDATES_SELF_UPDATE_REPO; setting it MUST produce ERROR: and exit 2.
  • Official install/update for updates itself is GitHub Releases only.
  • Native Windows self-update works only for official standalone installs with a valid install-source.json receipt.

14) Development & QA

14.1 Lint / test commands

  • Lint: ./scripts/lint.sh (runs bash -n, shellcheck, shfmt -d).
  • Tests: ./scripts/test.sh (runs ./tests/test_cli.sh).
  • Tests use temporary PATH stubs to avoid modifying the developer's machine.

14.2 Test plan

  • 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-mode enum validation tests.
    • --log-level output filtering tests.
    • --json JSONL output validation (parse each line as JSON in tests).
    • UPDATES_SELF_UPDATE_REPO error tests (v2.0.0).
    • Native Windows contract tests for receipt validation, relaunch guard, and self-update artifact verification.
  • Edge cases:
    • GO_BINARIES empty/unset (go module skips).
    • --json + --log-file interaction.
    • --only with new module names.
    • UTF-8 BOM config files.
    • HOME unset with USERPROFILE present 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.

14.3 Acceptance criteria (Given/When/Then)

  1. Given a macOS machine with brew, node, python, uv, mise, go, pipx, rustup, claude, pi installed, when updates runs, then all detected modules execute successfully and summary shows fail=0.
  2. Given --dry-run, when any module runs, then no mutating commands are executed.
  3. Given --json, when updates runs, then stdout contains only valid JSONL and stderr contains human output.
  4. Given ~/.updatesrc with SKIP_MODULES=python and CLI flag --only python, then CLI flag wins and python module runs.
  5. Given --brew-mode greedy, when brew module runs, then brew upgrade --greedy is called.
  6. Given GO_BINARIES="golang.org/x/tools/gopls" in config, when go module runs, then go install golang.org/x/tools/gopls@latest is called.
  7. Given a deprecated flag (e.g., --verbose) in 0.9.0, when used, then a WARN: deprecation message is printed and the flag maps to --log-level debug.
  8. Given a deprecated flag in 1.0.0, when used, then ERROR: is printed and exit code is 2.
  9. Given an official native Windows standalone install with a valid receipt, when updates --self-update finds a newer immutable release, then it verifies updates-windows.zip, updates-release.json, and SHA256SUMS, flips current.txt, and relaunches once.
  10. Given UPDATES_SELF_UPDATE_REPO is set, when updates runs, then it prints ERROR: and exits 2.
  11. Given native Windows receives Ctrl+C or Ctrl+Break, when updates is interrupted, then it exits 130.

15) Releases

Versioning:

  • SemVer, tagged as vX.Y.Z.
  • Script version is UPDATES_VERSION="<version>" inside updates.
  • Windows payload version is $script:UpdatesVersion = '<version>' inside updates-main.ps1.
  • Windows payload manifests and updates-release.json MUST carry the same release version as the Git tag.

Invariants (enforced in CI/release):

  • Tag version vX.Y.Z MUST match UPDATES_VERSION="X.Y.Z".
  • Tag version vX.Y.Z MUST match $script:UpdatesVersion = 'X.Y.Z'.
  • Tag version vX.Y.Z MUST match updates-release.json version.
  • CHANGELOG.md MUST contain a header ## [X.Y.Z] for the release.
  • Published GitHub Releases MUST include updates, updates-windows.zip, updates-release.json, and SHA256SUMS.
  • Published GitHub Releases MUST be immutable before they are eligible for runtime self-update.
  • Required release assets MUST expose GitHub digest values 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, and SHA256SUMS, verify uploaded asset digests and downloaded smoke artifacts, then publish.
  • Post-publish, run gh release verify <tag> and gh release verify-asset <tag> <artifact-path>.

16) Internal Structure

The Unix Bash entrypoint uses navigable section markers for organization:

grep '^# SECTION:' updates

Sections: globals, output, colors, platform, registry, utilities, self-update, cli, selection, modules, runner.

Native Windows uses:

  • install-windows.ps1 as the official standalone layout installer
  • updates.cmd as the thin launcher
  • updates.ps1 as the stable bootstrap
  • versions/<semver>/updates-main.ps1 as the mutable payload
  • install-source.json, current.txt, and previous.txt as the standalone update state

Config helpers

  • config_set_bool <CONFIG_KEY> <value> <VARIABLE> — validates and sets boolean config values. Used by read_config() for 7 boolean keys.

File size convention

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.

17) Decision Log

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

18) Assumptions, Open Questions, Risks

Assumptions

  • Users install and manage dependencies (brew, ncu, winget, bun, uv, mise, etc.) themselves.
  • Bash /bin/bash is 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 digest metadata through the REST API.
  • uv self update, bun upgrade, and winget upgrade remain stable subcommands/options.

Open questions

  • None (current behavior: in --json mode, --log-file captures the human stderr stream; JSONL remains on stdout).

Risks

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