From c1ff22bc6cd2bb9461b9974e8af48e7ace4a26b4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 25 Jan 2026 10:29:20 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20This=20PR=20adds=20a=20first-party=20cu?= =?UTF-8?q?rl-based=20installer=20that=20downloads=20only=20the=20binaries?= =?UTF-8?q?=20needed=20for=20the=20current=20platform=20and=20wires=20it?= =?UTF-8?q?=20into=20Codex=E2=80=99s=20existing=20update=20UX.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It introduces a dedicated `installer/` folder for non-Rust install/update assets, a `CODEX_HOME`-aware on-disk layout, and a wrapper that makes helper CLIs available to Codex without globally polluting the user’s shell. ## Why Today the recommended install path is `npm i -g @openai/codex`, but the npm package bundles native binaries for all platforms (and `rg`), leading to very large installs (hundreds of MB unpacked) even though a single platform binary is much smaller. Homebrew avoids this on macOS by downloading only the matching artifact. This PR brings that same property to a cross-platform one-liner: ```sh curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/install.sh | bash ``` Key goals: - Download only the user’s platform artifacts. - Keep curl-managed installs isolated under `CODEX_HOME`. - Preserve compatibility with npm/brew installs (shadowing is fine, breaking is not). - Provide a clean place to add helper CLIs that should be available when Codex runs. ## How It Works ### Install root and layout All curl-managed artifacts live under `CODEX_HOME` (default: `~/.codex`). The layout is: - `CODEX_HOME/bin/` - `CODEX_HOME/versions//bin/` - `CODEX_HOME/versions/current` (symlink) - `CODEX_HOME/tools/bin/` This design supports atomic version switches and helper CLIs: - The user’s global `PATH` only needs `CODEX_HOME/bin`. - The runtime `PATH` is extended inside the wrapper to include helper CLIs. See `installer/README.md:1`. ### Installer scripts #### `installer/install.sh` `installer/install.sh:1` is the public entrypoint: - Requires Bash explicitly (we use Bash features like `mapfile`). - Loads `installer/lib.sh` locally when run from a checkout, or downloads it when piped from curl. - Detects `arch`/`os`, resolves the version (via `CODEX_VERSION` or GitHub Releases), downloads the correct tarball, installs it, activates it, and updates the user’s rc file. #### `installer/lib.sh` `installer/lib.sh:10` centralizes the mechanics: - Resolves `CODEX_HOME` with a fallback to `~/.codex`. - Detects OS/arch and builds release URLs. - Idempotently updates the user rc file via a marker block that respects non-default `CODEX_HOME` values: `installer/lib.sh:106`. - Installs a wrapper at `CODEX_HOME/bin/codex` that: - Sets `CODEX_MANAGED_BY_CURL=1`. - Extends `PATH` at runtime to include: - `CODEX_HOME/bin` - `CODEX_HOME/tools/bin` - `CODEX_HOME/versions/current/bin` - Execs the active versioned binary: `installer/lib.sh:154`. - Installs the tarball into a versioned `bin/` directory and preserves any additional CLIs present in the tarball (not just `codex`): `installer/lib.sh:181`. - Keeps the last two installed versions and avoids deleting the `current` symlink: `installer/lib.sh:239`. #### `installer/update.sh` `installer/update.sh:1` mirrors the installer flow but always targets the latest release. ### Update UX integration in the CLI Codex’s update prompt already knows how to run a package-manager-specific update command after the TUI exits. This PR adds a curl-managed update action in `codex-rs/tui/src/update_action.rs:3`: - Adds `UpdateAction::CurlInstallerUpdate`. - Detects curl-managed installs via `CODEX_MANAGED_BY_CURL` (set by the wrapper): `codex-rs/tui/src/update_action.rs:40`. - Uses the curl updater one-liner for the update command: `codex-rs/tui/src/update_action.rs:21`. This keeps update behavior consistent with npm/brew while avoiding fragile path heuristics. ## Docs updates - The curl installer is now the first recommended install path in `README.md:1` and `README.md:18`. - `docs/install.md:11` adds a curl install section and points to `installer/README.md` for the detailed mechanics. ## Compatibility and safety notes - This does not modify npm or Homebrew installs. - The rc-file change only adds `CODEX_HOME/bin` to `PATH`. - Additional helper CLIs are available to Codex via the wrapper’s runtime `PATH`, rather than being forced into the user’s global shell. - The installer honors `CODEX_HOME` everywhere, falling back to `~/.codex`. ## Testing Commands run: ```sh cd codex-rs just fmt cargo test -p codex-tui ``` Notes: - In this environment, `cargo test -p codex-tui` required running outside the sandbox due to a macOS `SystemConfiguration` panic; it passed once run with escalated permissions. - No new VS Code diagnostics were reported. ## Follow-ups (explicitly not in this PR) - Ensure the release tarballs include any helper CLIs we expect to be available at runtime (for example, Windows sandbox helpers). - Add an explicit `codex self-update` command that delegates to `installer/update.sh`. - Decide how we want to manage third-party tools like `rg` under `CODEX_HOME/tools/` and whether the CLI should probe a managed fallback path. --- README.md | 9 +- codex-rs/tui/src/update_action.rs | 36 ++++- docs/install.md | 9 ++ installer/README.md | 104 ++++++++++++ installer/install.sh | 73 +++++++++ installer/lib.sh | 256 ++++++++++++++++++++++++++++++ installer/update.sh | 66 ++++++++ 7 files changed, 547 insertions(+), 6 deletions(-) create mode 100644 installer/README.md create mode 100755 installer/install.sh create mode 100755 installer/lib.sh create mode 100755 installer/update.sh diff --git a/README.md b/README.md index eb4ace74f2..733c915d43 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

npm i -g @openai/codex
or brew install --cask codex

+

curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/install.sh | bash
or brew install --cask codex
or npm i -g @openai/codex

Codex CLI is a coding agent from OpenAI that runs locally on your computer.

Codex CLI splash @@ -15,6 +15,13 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), ("bun", &["install", "-g", "@openai/codex"]), UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]), + UpdateAction::CurlInstallerUpdate => ( + "bash", + &[ + "-c", + "set -euo pipefail; curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/update.sh | bash", + ], + ), } } @@ -32,12 +41,14 @@ pub(crate) fn get_update_action() -> Option { let exe = std::env::current_exe().unwrap_or_default(); let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some(); let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some(); + let managed_by_curl = std::env::var_os("CODEX_MANAGED_BY_CURL").is_some(); detect_update_action( cfg!(target_os = "macos"), &exe, managed_by_npm, managed_by_bun, + managed_by_curl, ) } @@ -47,11 +58,14 @@ fn detect_update_action( current_exe: &std::path::Path, managed_by_npm: bool, managed_by_bun: bool, + managed_by_curl: bool, ) -> Option { if managed_by_npm { Some(UpdateAction::NpmGlobalLatest) } else if managed_by_bun { Some(UpdateAction::BunGlobalLatest) + } else if managed_by_curl { + Some(UpdateAction::CurlInstallerUpdate) } else if is_macos && (current_exe.starts_with("/opt/homebrew") || current_exe.starts_with("/usr/local")) { @@ -68,15 +82,21 @@ mod tests { #[test] fn detects_update_action_without_env_mutation() { assert_eq!( - detect_update_action(false, std::path::Path::new("/any/path"), false, false), + detect_update_action( + false, + std::path::Path::new("/any/path"), + false, + false, + false + ), None ); assert_eq!( - detect_update_action(false, std::path::Path::new("/any/path"), true, false), + detect_update_action(false, std::path::Path::new("/any/path"), true, false, false), Some(UpdateAction::NpmGlobalLatest) ); assert_eq!( - detect_update_action(false, std::path::Path::new("/any/path"), false, true), + detect_update_action(false, std::path::Path::new("/any/path"), false, true, false), Some(UpdateAction::BunGlobalLatest) ); assert_eq!( @@ -84,7 +104,8 @@ mod tests { true, std::path::Path::new("/opt/homebrew/bin/codex"), false, - false + false, + false, ), Some(UpdateAction::BrewUpgrade) ); @@ -93,9 +114,14 @@ mod tests { true, std::path::Path::new("/usr/local/bin/codex"), false, - false + false, + false, ), Some(UpdateAction::BrewUpgrade) ); + assert_eq!( + detect_update_action(false, std::path::Path::new("/any/path"), false, false, true), + Some(UpdateAction::CurlInstallerUpdate) + ); } } diff --git a/docs/install.md b/docs/install.md index 20d8b54a54..d0306ccbf3 100644 --- a/docs/install.md +++ b/docs/install.md @@ -8,6 +8,15 @@ | Git (optional, recommended) | 2.23+ for built-in PR helpers | | RAM | 4-GB minimum (8-GB recommended) | +### Install via curl + +```bash +curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/install.sh | bash +``` + +The curl installer writes under `CODEX_HOME` (default: `~/.codex`). See +`installer/README.md` for the detailed layout and mechanics. + ### DotSlash The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development. diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000000..34ac29a040 --- /dev/null +++ b/installer/README.md @@ -0,0 +1,104 @@ +# Codex Curl Installer + +This folder contains the non-Rust assets for the curl-based Codex installer +and update flow. Rust code lives under `codex-rs/`. + +## Goals + +- Download only the binaries needed for the current platform. +- Keep the install isolated under `CODEX_HOME` (default: `~/.codex`). +- Avoid breaking npm/brew installs; shadowing is acceptable. +- Support additional helper CLIs without globally polluting `PATH`. + +## Install Root (`CODEX_HOME`) + +The installer treats `CODEX_HOME` as the install root: + +- If `CODEX_HOME` is set, it is used as-is. +- Otherwise, the default is `~/.codex`. + +All curl-managed artifacts should live under this root. + +## On-Disk Layout + +The layout is designed to support multiple versions, helper binaries, and +atomic updates: + +- `CODEX_HOME/bin/` +- `CODEX_HOME/versions//` +- `CODEX_HOME/versions//bin/` +- `CODEX_HOME/tools///` +- `CODEX_HOME/tools/bin/` + +Key conventions: + +- The user-facing entrypoint is `CODEX_HOME/bin/codex`. +- `CODEX_HOME/versions/current` is a symlink to the active version directory. +- Helper CLIs that ship with Codex (for example, Windows sandbox helpers) live + in `CODEX_HOME/versions//bin/`. +- Third-party tools we fetch (for example, `rg`) live under + `CODEX_HOME/tools/...`, with optional shims in `CODEX_HOME/tools/bin/`. + +## PATH Strategy + +We separate the user's global `PATH` from Codex's runtime `PATH`: + +1. The installer ensures `CODEX_HOME/bin` is on the user's `PATH`. +2. The `codex` wrapper augments `PATH` at runtime to include: + - `CODEX_HOME/bin` + - `CODEX_HOME/tools/bin` + - `CODEX_HOME/versions/current/bin` + +This keeps helper CLIs available to Codex without exposing them as global +commands in every shell session. + +## Versioning And Atomic Updates + +Curl-managed installs should be versioned: + +1. Download into a versioned directory: + - `CODEX_HOME/versions//` +2. Link `CODEX_HOME/versions/current` to the new version atomically. +3. Keep a small number of prior versions for rollback. + +Because the wrapper resolves through `versions/current`, repointing the +symlink updates the effective version without editing shell rc files again. + +## Helper CLI Placement + +Any additional CLIs that Codex needs at runtime should follow these rules: + +- Bundled CLIs that are version-coupled to Codex: + - Place in `CODEX_HOME/versions//bin/` +- Third-party tools that may be shared across versions: + - Place in `CODEX_HOME/tools///` + - Optionally add a stable shim in `CODEX_HOME/tools/bin/` + +The wrapper then makes them available during execution. + +## Ripgrep (`rg`) + +The preferred approach is: + +- Use a system `rg` when available. +- Otherwise, allow curl-managed installs to place `rg` under + `CODEX_HOME/tools/rg//` with a shim in `CODEX_HOME/tools/bin/rg`. + +Codex CLI can optionally honor an explicit `CODEX_RG_PATH` to point directly +to a managed `rg`. + +## Scripts + +Planned/expected scripts in this folder: + +- `installer/install.sh` +- `installer/lib.sh` +- `installer/update.sh` + +The public one-liner should look like: + +```sh +curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/install.sh | bash +``` + +All scripts must honor `CODEX_HOME` with a fallback to `~/.codex`. diff --git a/installer/install.sh b/installer/install.sh new file mode 100755 index 0000000000..7005392c07 --- /dev/null +++ b/installer/install.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +if [ -z "${BASH_VERSION:-}" ]; then + echo "This installer requires bash." >&2 + echo "Re-run with: curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/install.sh | bash" >&2 + exit 1 +fi + +set -euo pipefail + +INSTALLER_BASE_URL="${CODEX_INSTALLER_BASE_URL:-https://raw.githubusercontent.com/openai/codex/main/installer}" + +load_lib() { + local script_dir lib_path tmp_lib + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" + lib_path="${script_dir}/lib.sh" + + if [ -f "$lib_path" ]; then + # Running from a repo checkout. + # shellcheck disable=SC1090 + source "$lib_path" + return + fi + + tmp_lib="$(mktemp)" + curl -fsSL "${INSTALLER_BASE_URL}/lib.sh" -o "$tmp_lib" + # shellcheck disable=SC1090 + source "$tmp_lib" + rm -f "$tmp_lib" +} + +load_lib + +main() { + ensure_dirs + + local arch os tag version url tarball rc_file resolved_home + arch="$(detect_arch)" + os="$(detect_os)" + + if [ -n "${CODEX_VERSION:-}" ]; then + version="$(normalize_version "$CODEX_VERSION")" + tag="$(release_tag_for_version "$version")" + else + tag="$(latest_tag)" + if [ -z "$tag" ]; then + echo "Failed to determine the latest Codex release tag." >&2 + exit 1 + fi + version="$(normalize_version "$tag")" + fi + + url="$(release_url "$version" "$arch" "$os")" + tarball="$(mktemp)" + curl -fsSL "$url" -o "$tarball" + + install_version_from_tarball "$version" "$tarball" "$arch" "$os" + activate_version "$version" + install_wrapper + cleanup_old_versions 2 + + rc_file="$(choose_rc_file)" + resolved_home="$(codex_home)" + ensure_path_block "$rc_file" "$resolved_home" + + rm -f "$tarball" + + echo "Codex ${version} installed to ${resolved_home}" + echo "Updated PATH in ${rc_file}" + echo "Open a new shell or run: source ${rc_file}" +} + +main "$@" diff --git a/installer/lib.sh b/installer/lib.sh new file mode 100755 index 0000000000..d55682c05f --- /dev/null +++ b/installer/lib.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash + +if [ -z "${BASH_VERSION:-}" ]; then + echo "Codex installer requires bash." >&2 + exit 1 +fi + +set -euo pipefail + +CODEX_HOME_DEFAULT="${HOME}/.codex" +CODEX_HOME_RESOLVED="${CODEX_HOME:-$CODEX_HOME_DEFAULT}" + +codex_home() { + printf '%s\n' "$CODEX_HOME_RESOLVED" +} + +codex_bin_dir() { + printf '%s/bin\n' "$(codex_home)" +} + +codex_versions_dir() { + printf '%s/versions\n' "$(codex_home)" +} + +codex_tools_dir() { + printf '%s/tools\n' "$(codex_home)" +} + +codex_tools_bin_dir() { + printf '%s/bin\n' "$(codex_tools_dir)" +} + +current_version_link() { + printf '%s/current\n' "$(codex_versions_dir)" +} + +detect_os() { + local uname_s + uname_s="$(uname -s)" + case "$uname_s" in + Darwin) printf '%s\n' "apple-darwin" ;; + Linux) printf '%s\n' "unknown-linux-musl" ;; + *) + echo "Unsupported OS: $uname_s" >&2 + exit 1 + ;; + esac +} + +detect_arch() { + local uname_m + uname_m="$(uname -m)" + case "$uname_m" in + arm64|aarch64) printf '%s\n' "aarch64" ;; + x86_64|amd64) printf '%s\n' "x86_64" ;; + *) + echo "Unsupported architecture: $uname_m" >&2 + exit 1 + ;; + esac +} + +github_api() { + local path="$1" + curl -fsSL \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/openai/codex/${path}" +} + +latest_tag() { + github_api "releases/latest" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 +} + +normalize_version() { + local tag="$1" + tag="${tag#rust-v}" + tag="${tag#v}" + printf '%s\n' "$tag" +} + +release_tag_for_version() { + local version="$1" + printf '%s\n' "rust-v${version}" +} + +expected_binary_name() { + local arch="$1" + local os="$2" + printf '%s\n' "codex-${arch}-${os}" +} + +release_url() { + local version="$1" + local arch="$2" + local os="$3" + local tag + tag="$(release_tag_for_version "$version")" + printf '%s\n' "https://github.com/openai/codex/releases/download/${tag}/codex-${arch}-${os}.tar.gz" +} + +ensure_dirs() { + mkdir -p "$(codex_bin_dir)" "$(codex_versions_dir)" "$(codex_tools_bin_dir)" +} + +ensure_path_block() { + local rc_file="$1" + local resolved_home="$2" + local start_marker="# >>> codex >>>" + + if [ ! -f "$rc_file" ]; then + touch "$rc_file" + fi + + if grep -Fq "$start_marker" "$rc_file"; then + return + fi + + if [ "$resolved_home" = "${HOME}/.codex" ]; then + cat >>"$rc_file" <<'EOF' +# >>> codex >>> +export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +export PATH="$CODEX_HOME/bin:$PATH" +# <<< codex <<< +EOF + else + cat >>"$rc_file" <>> codex >>> +export CODEX_HOME="${resolved_home}" +export PATH="\$CODEX_HOME/bin:\$PATH" +# <<< codex <<< +EOF + fi +} + +choose_rc_file() { + local shell_name + shell_name="$(basename "${SHELL:-}")" + case "$shell_name" in + zsh) printf '%s\n' "${HOME}/.zshrc" ;; + bash) + if [ -f "${HOME}/.bashrc" ]; then + printf '%s\n' "${HOME}/.bashrc" + else + printf '%s\n' "${HOME}/.bash_profile" + fi + ;; + *) + printf '%s\n' "${HOME}/.profile" + ;; + esac +} + +install_wrapper() { + local wrapper_path + wrapper_path="$(codex_bin_dir)/codex" + + cat >"$wrapper_path" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +CURRENT_LINK="$CODEX_HOME/versions/current" +TARGET="$CURRENT_LINK/bin/codex" + +if [ ! -x "$TARGET" ]; then + echo "Codex is not installed under $CODEX_HOME." >&2 + echo "Run the installer again to repair the installation." >&2 + exit 1 +fi + +export CODEX_MANAGED_BY_CURL=1 +export PATH="$CODEX_HOME/bin:$CODEX_HOME/tools/bin:$CURRENT_LINK/bin:$PATH" + +exec "$TARGET" "$@" +EOF + + chmod +x "$wrapper_path" +} + +install_version_from_tarball() { + local version="$1" + local tarball="$2" + local arch="$3" + local os="$4" + + local versions_dir version_dir bin_dir expected_name tmpdir main_candidate + versions_dir="$(codex_versions_dir)" + version_dir="${versions_dir}/${version}" + bin_dir="${version_dir}/bin" + expected_name="$(expected_binary_name "$arch" "$os")" + + rm -rf "$version_dir" + mkdir -p "$bin_dir" + + tmpdir="$(mktemp -d)" + tar -xzf "$tarball" -C "$tmpdir" + + if [ -f "$tmpdir/$expected_name" ]; then + main_candidate="$tmpdir/$expected_name" + elif [ -f "$tmpdir/codex" ]; then + main_candidate="$tmpdir/codex" + else + main_candidate="$(find "$tmpdir" -type f -name 'codex*' | head -n1 || true)" + fi + if [ -z "$main_candidate" ]; then + echo "Failed to locate codex binary in tarball." >&2 + exit 1 + fi + + local files file base dest + mapfile -t files < <(find "$tmpdir" -type f) + for file in "${files[@]}"; do + base="$(basename "$file")" + if [ "$file" = "$main_candidate" ]; then + dest="$bin_dir/codex" + else + dest="$bin_dir/$base" + fi + mv "$file" "$dest" + chmod +x "$dest" 2>/dev/null || true + done + + if [ ! -x "$bin_dir/codex" ]; then + echo "Failed to install codex binary." >&2 + exit 1 + fi + + rm -rf "$tmpdir" +} + +activate_version() { + local version="$1" + local link + link="$(current_version_link)" + ln -sfn "$(codex_versions_dir)/$version" "$link" +} + +cleanup_old_versions() { + local keep="${1:-2}" + local versions_dir + versions_dir="$(codex_versions_dir)" + if [ ! -d "$versions_dir" ]; then + return + fi + if ! compgen -G "$versions_dir/*" >/dev/null; then + return + fi + + # Remove older versions while keeping the most recent N directories. + # We intentionally ignore errors here so cleanup never blocks install. + ls -1dt "$versions_dir"/* 2>/dev/null \ + | grep -v '/current$' \ + | tail -n +"$((keep + 1))" \ + | xargs -I{} rm -rf "{}" 2>/dev/null || true +} diff --git a/installer/update.sh b/installer/update.sh new file mode 100755 index 0000000000..3cdee7e81f --- /dev/null +++ b/installer/update.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +if [ -z "${BASH_VERSION:-}" ]; then + echo "This updater requires bash." >&2 + echo "Re-run with: curl -fsSL https://raw.githubusercontent.com/openai/codex/main/installer/update.sh | bash" >&2 + exit 1 +fi + +set -euo pipefail + +INSTALLER_BASE_URL="${CODEX_INSTALLER_BASE_URL:-https://raw.githubusercontent.com/openai/codex/main/installer}" + +load_lib() { + local script_dir lib_path tmp_lib + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" + lib_path="${script_dir}/lib.sh" + + if [ -f "$lib_path" ]; then + # Running from a repo checkout. + # shellcheck disable=SC1090 + source "$lib_path" + return + fi + + tmp_lib="$(mktemp)" + curl -fsSL "${INSTALLER_BASE_URL}/lib.sh" -o "$tmp_lib" + # shellcheck disable=SC1090 + source "$tmp_lib" + rm -f "$tmp_lib" +} + +load_lib + +main() { + ensure_dirs + + local arch os tag version url tarball rc_file resolved_home + arch="$(detect_arch)" + os="$(detect_os)" + + tag="$(latest_tag)" + if [ -z "$tag" ]; then + echo "Failed to determine the latest Codex release tag." >&2 + exit 1 + fi + version="$(normalize_version "$tag")" + + url="$(release_url "$version" "$arch" "$os")" + tarball="$(mktemp)" + curl -fsSL "$url" -o "$tarball" + + install_version_from_tarball "$version" "$tarball" "$arch" "$os" + activate_version "$version" + install_wrapper + cleanup_old_versions 2 + + rc_file="$(choose_rc_file)" + resolved_home="$(codex_home)" + ensure_path_block "$rc_file" "$resolved_home" + + rm -f "$tarball" + + echo "Codex updated to ${version}" +} + +main "$@"