Last active
January 19, 2026 02:55
-
-
Save TTTPOB/5917cda8998e172d9e85ad5780c923bf to your computer and use it in GitHub Desktop.
old style codex management script. ai genereated but verified. official npm install way bundled all platform binaries, i don't like it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # --- Constants & Globals --- | |
| REPO="openai/codex" | |
| API_URL="https://api.github.com/repos/${REPO}/releases/latest" | |
| BIN_NAME="codex" | |
| STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/codex-install" | |
| STATE_FILE="${STATE_DIR}/state.json" | |
| # Global variable for temporary directory | |
| WORKDIR="" | |
| # --- Cleanup Logic --- | |
| cleanup() { | |
| if [[ -n "${WORKDIR:-}" ]] && [[ -d "$WORKDIR" ]]; then | |
| rm -rf "$WORKDIR" | |
| fi | |
| } | |
| trap cleanup EXIT | |
| # --- Helper Functions --- | |
| log() { printf '%s\n' "$*" >&2; } | |
| die() { log "error: $*"; exit 1; } | |
| usage() { | |
| cat >&2 <<'EOF' | |
| Usage: | |
| codex-install install | |
| codex-install update | |
| codex-install uninstall | |
| Optional env: | |
| CODEX_INSTALL_DIR Installation directory (default: /usr/local/bin if writable, else ~/.local/bin) | |
| GITHUB_TOKEN GitHub token to avoid API rate limits | |
| EOF | |
| } | |
| require() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; } | |
| is_linux() { [[ "$(uname -s)" == "Linux" ]] || die "this installer is for Linux only"; } | |
| norm_arch() { | |
| case "$(uname -m)" in | |
| x86_64|amd64) echo "x86_64" ;; | |
| aarch64|arm64) echo "aarch64" ;; | |
| *) die "unsupported architecture: $(uname -m) (supported: x86_64, aarch64)" ;; | |
| esac | |
| } | |
| github_headers() { | |
| echo "-H" "Accept: application/vnd.github+json" | |
| echo "-H" "User-Agent: codex-install" | |
| echo "-H" "X-GitHub-Api-Version: 2022-11-28" | |
| if [[ -n "${GITHUB_TOKEN:-}" ]]; then | |
| echo "-H" "Authorization: Bearer ${GITHUB_TOKEN}" | |
| fi | |
| } | |
| fetch_latest_release_json() { | |
| local out="$1" | |
| local -a hdr | |
| mapfile -t hdr < <(github_headers) | |
| curl -fsSL "${hdr[@]}" "$API_URL" > "$out" | |
| } | |
| jq_tag_name() { jq -r '.tag_name // empty' <"$1"; } | |
| jq_asset_field_by_name() { | |
| local json="$1" name="$2" field="$3" | |
| jq -r --arg n "$name" --arg f "$field" ' | |
| (.assets[]? | select(.name == $n) | .[$f]) // empty | |
| ' <"$json" | head -n1 | |
| } | |
| jq_asset_pick_fallback() { | |
| local json="$1" arch="$2" | |
| jq -r --arg a "$arch" ' | |
| .assets[]? | | |
| select( | |
| (.name | test("linux"; "i")) and | |
| ( | |
| (.name | test($a; "i")) or | |
| ( ($a=="x86_64") and (.name | test("amd64"; "i")) ) or | |
| ( ($a=="aarch64") and (.name | test("arm64"; "i")) ) | |
| ) and | |
| ((.name | endswith(".tar.gz")) or (.name | endswith(".tgz"))) | |
| ) | | |
| "\(.name)|\(.browser_download_url)|\(.digest // "")" | |
| ' <"$json" | head -n1 | |
| } | |
| # 删除“第一个数字”之前的所有字符 | |
| clean_version() { | |
| sed -E 's/^[^0-9]+//' <<<"${1:-}" | |
| } | |
| local_codex_path() { | |
| command -v "$BIN_NAME" 2>/dev/null || true | |
| } | |
| looks_like_npm_pnpm_install() { | |
| local p rp | |
| p="$(local_codex_path)" | |
| [[ -n "$p" ]] || return 1 | |
| rp="$(readlink -f "$p" 2>/dev/null || echo "$p")" | |
| if echo "$rp" | grep -qiE 'node_modules|/\.pnpm/|pnpm|npm|nvm|asdf|volta|yarn'; then | |
| return 0 | |
| fi | |
| if grep -a -m1 -qE '^#!.*\bnode(\s|$)' "$rp"; then | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| get_local_version() { | |
| local out | |
| out="$("$BIN_NAME" -V 2>/dev/null || true)" | |
| [[ -n "$out" ]] || return 1 | |
| awk '{print $2}' <<<"$out" | head -n1 | sed -E 's/^[^0-9]+//' || true | |
| } | |
| default_install_dir() { | |
| if [[ -n "${CODEX_INSTALL_DIR:-}" ]]; then | |
| echo "$CODEX_INSTALL_DIR" | |
| return | |
| fi | |
| if [[ -w "/usr/local/bin" ]]; then | |
| echo "/usr/local/bin" | |
| else | |
| echo "$HOME/.local/bin" | |
| fi | |
| } | |
| resolved_install_dir() { | |
| if [[ -f "$STATE_FILE" ]]; then | |
| local bp | |
| bp="$(jq -r '.bin_path // empty' "$STATE_FILE" 2>/dev/null || true)" | |
| if [[ -n "$bp" ]]; then | |
| echo "$(dirname "$bp")" | |
| return | |
| fi | |
| fi | |
| default_install_dir | |
| } | |
| sudo_if_needed() { | |
| local dir="$1"; shift | |
| if [[ -w "$dir" || "$EUID" -eq 0 ]]; then | |
| "$@" | |
| else | |
| command -v sudo >/dev/null 2>&1 || die "no write permission to ${dir} and sudo not available" | |
| sudo "$@" | |
| fi | |
| } | |
| verify_with_digest() { | |
| local file_path="$1" digest="$2" | |
| # (1) digest 缺失或格式异常 -> 直接失败 | |
| [[ -n "$digest" ]] || die "security error: asset digest missing from GitHub API; cannot verify download integrity." | |
| [[ "$digest" =~ ^sha256:[0-9a-fA-F]{64}$ ]] || die "security error: unexpected asset digest format: $digest" | |
| # (2) 强制依赖 sha256sum(不要跳过校验) | |
| require sha256sum | |
| local expected actual | |
| expected="${digest#sha256:}" | |
| actual="$(sha256sum "$file_path" | awk '{print $1}')" | |
| [[ "$actual" == "$expected" ]] || die "sha256 mismatch for $(basename "$file_path")" | |
| log "sha256 OK (verified against GitHub asset.digest)" | |
| } | |
| assert_is_codex_binary() { | |
| local path="$1" | |
| local out | |
| out="$("$path" -V 2>/dev/null || true)" | |
| # 形如: "codex-cli 0.87.0" | |
| [[ "$out" =~ ^codex-cli[[:space:]]+[0-9] ]] || die "extracted file is not a runnable codex binary: $path" | |
| } | |
| install_or_update() { | |
| local mode="$1" # install|update | |
| is_linux | |
| require curl | |
| require jq | |
| require tar | |
| require sha256sum # (2) 强制依赖 | |
| if [[ -n "$(local_codex_path)" ]] && looks_like_npm_pnpm_install; then | |
| log "detected: codex appears to be installed via npm/pnpm (node wrapper / node_modules)." | |
| log "this script will not override or remove it." | |
| log "action: uninstall via your package manager first (e.g., npm uninstall -g @openai/codex OR pnpm remove -g @openai/codex), then rerun." | |
| exit 3 | |
| fi | |
| local arch rel_json tag ver install_dir target | |
| arch="$(norm_arch)" | |
| install_dir="$(resolved_install_dir)" | |
| target="${install_dir}/${BIN_NAME}" | |
| mkdir -p "$install_dir" | |
| WORKDIR="$(mktemp -d)" | |
| rel_json="${WORKDIR}/release.json" | |
| fetch_latest_release_json "$rel_json" | |
| tag="$(jq_tag_name "$rel_json")" | |
| [[ -n "$tag" ]] || die "failed to read tag_name from GitHub API" | |
| ver="$(clean_version "$tag")" | |
| if [[ "$mode" == "update" ]]; then | |
| if [[ -n "$(local_codex_path)" ]]; then | |
| local local_ver | |
| local_ver="$(get_local_version || true)" | |
| if [[ -n "$local_ver" && "$local_ver" == "$ver" ]]; then | |
| log "already up to date (${ver})" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| local asset_name url digest | |
| if [[ "$arch" == "x86_64" ]]; then | |
| asset_name="codex-x86_64-unknown-linux-musl.tar.gz" | |
| else | |
| asset_name="codex-aarch64-unknown-linux-musl.tar.gz" | |
| fi | |
| url="$(jq_asset_field_by_name "$rel_json" "$asset_name" "browser_download_url")" | |
| digest="$(jq_asset_field_by_name "$rel_json" "$asset_name" "digest")" | |
| if [[ -z "$url" ]]; then | |
| log "note: expected asset not found (${asset_name}); falling back to heuristic match" | |
| local picked | |
| picked="$(jq_asset_pick_fallback "$rel_json" "$arch")" | |
| [[ -n "$picked" ]] || die "could not find a Linux tarball asset for arch=${arch}" | |
| asset_name="${picked%%|*}" | |
| url="$(cut -d'|' -f2 <<<"$picked")" | |
| digest="$(cut -d'|' -f3 <<<"$picked")" | |
| fi | |
| log "latest release: ${ver}" | |
| log "downloading: ${asset_name}" | |
| local tar_path | |
| tar_path="${WORKDIR}/${asset_name}" | |
| curl -fL --retry 3 --retry-delay 1 "$url" -o "$tar_path" | |
| verify_with_digest "$tar_path" "$digest" | |
| tar -xzf "$tar_path" -C "$WORKDIR" | |
| # (3) 更稳的二进制定位: | |
| # 先按资产名推导解压后期望文件名;不命中则在 maxdepth 2 里找,且排除压缩包 | |
| local expected extracted base | |
| base="${asset_name}" | |
| base="${base%.tar.gz}" | |
| base="${base%.tgz}" | |
| expected="${WORKDIR}/${base}" | |
| if [[ -f "$expected" ]]; then | |
| extracted="$expected" | |
| else | |
| extracted="$(find "$WORKDIR" -maxdepth 2 -type f \ | |
| \( -name 'codex' -o -name 'codex-*' \) \ | |
| ! -name '*.tar.gz' ! -name '*.tgz' \ | |
| -print -quit || true)" | |
| fi | |
| [[ -n "${extracted:-}" ]] || die "could not locate extracted codex binary in archive" | |
| chmod +x "$extracted" | |
| assert_is_codex_binary "$extracted" | |
| sudo_if_needed "$install_dir" install -m 0755 "$extracted" "$target" | |
| # 可选:验证已安装目标可运行(不依赖 PATH) | |
| "$target" -V >/dev/null 2>&1 || die "installed codex failed to run: $target" | |
| mkdir -p "$STATE_DIR" | |
| jq -n \ | |
| --arg tag_name "$tag" \ | |
| --arg version "$ver" \ | |
| --arg install_dir "$install_dir" \ | |
| --arg bin_path "$target" \ | |
| --arg asset "$asset_name" \ | |
| --arg digest "${digest:-}" \ | |
| --arg installed_at "$(date -Is)" \ | |
| '{tag_name:$tag_name, version:$version, install_dir:$install_dir, bin_path:$bin_path, asset:$asset, digest:$digest, installed_at:$installed_at}' \ | |
| > "$STATE_FILE" | |
| log "installed: $target" | |
| if [[ "$install_dir" == "$HOME/.local/bin" ]]; then | |
| log "note: ensure ~/.local/bin is in PATH (e.g., export PATH=\"$HOME/.local/bin:\$PATH\")" | |
| fi | |
| } | |
| uninstall() { | |
| is_linux | |
| if [[ -n "$(local_codex_path)" ]] && looks_like_npm_pnpm_install; then | |
| log "detected: codex appears to be installed via npm/pnpm (node wrapper / node_modules)." | |
| log "this script will not remove it." | |
| log "action: uninstall via your package manager (e.g., npm uninstall -g @openai/codex OR pnpm remove -g @openai/codex)." | |
| exit 3 | |
| fi | |
| local bin_path="" | |
| if [[ -f "$STATE_FILE" ]]; then | |
| bin_path="$(jq -r '.bin_path // empty' "$STATE_FILE" 2>/dev/null || true)" | |
| fi | |
| if [[ -z "$bin_path" ]]; then | |
| bin_path="$(local_codex_path)" | |
| fi | |
| [[ -n "$bin_path" ]] || die "could not determine installed codex path (not found in state or PATH)" | |
| [[ -e "$bin_path" ]] || die "codex not found at: $bin_path" | |
| local dir | |
| dir="$(dirname "$bin_path")" | |
| sudo_if_needed "$dir" rm -f "$bin_path" | |
| rm -f "$STATE_FILE" | |
| log "removed: $bin_path" | |
| log "state cleared: $STATE_FILE" | |
| } | |
| main() { | |
| local cmd="${1:-}" | |
| case "$cmd" in | |
| install) install_or_update "install" ;; | |
| update) install_or_update "update" ;; | |
| uninstall) uninstall ;; | |
| -h|--help|"") usage; exit 1 ;; | |
| *) usage; die "unknown command: $cmd" ;; | |
| esac | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment