Skip to content

Instantly share code, notes, and snippets.

@TTTPOB
Last active January 19, 2026 02:55
Show Gist options
  • Select an option

  • Save TTTPOB/5917cda8998e172d9e85ad5780c923bf to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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