#!/usr/bin/env bash # # Codex CLI system installer + proxy wrapper (Linux / macOS) # # Goal: # - Install the official standalone Codex CLI once for all local users. # - Expose /usr/local/bin/codex for root, claude, and future users. # - Route only the Codex main process through the configured proxy. # - Prevent commands executed by Codex from inheriting proxy env vars. # # Usage: # curl -fsSL https://claude.dahe2016.com/codex.sh | bash # curl -fsSL https://claude.dahe2016.com/codex.sh | bash -s -- install # curl -fsSL https://claude.dahe2016.com/codex.sh | bash -s -- upgrade # curl -fsSL https://claude.dahe2016.com/codex.sh | bash -s -- proxy # set -euo pipefail MODE="${1-}" # ========================== 在这里改成你自己的值 ========================== # 支持两种格式: # 1) host:port:user:pass # 2) http://user:pass@host:port / socks5h://user:pass@host:port PROXY_CN="http.dahe2017.com:28080:http:L1Ih9znBff9HtCml" PROXY_NON_CN="http-us.dahe2017.com:28080:http:L1Ih9znBff9HtCml" # 境外(选3=本机直连) DEFAULT_PROXY="${PROXY_CN}" PROXY_FROM_ENV=0 if [[ "${PROXY+x}" == "x" ]]; then PROXY_FROM_ENV=1 else PROXY="${DEFAULT_PROXY}" fi # PROXY="" 表示强制直连; 显式设置 PROXY 时跳过交互选择。 INSTALL_URL="https://chatgpt.com/codex/install.sh" INSTALL_ROOT="/opt/codex-real" REAL_BIN_DIR="${INSTALL_ROOT}/bin" REAL_CODEX="${REAL_BIN_DIR}/codex" REAL_CODEX_HOME="${INSTALL_ROOT}/state" NPM_PREFIX="${INSTALL_ROOT}/npm" NPM_CODEX="${NPM_PREFIX}/bin/codex" WRAPPER="/usr/local/bin/codex" NODE_MAJOR="20" # Codex 子进程必须剔除这些代理变量,避免污染 codex 执行的命令。 PROXY_EXCLUDE_TOML='["HTTP_PROXY","HTTPS_PROXY","ALL_PROXY","http_proxy","https_proxy","all_proxy","NO_PROXY","no_proxy"]' # ======================================================================== log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; } warn() { printf '\033[1;33m[!] %s\033[0m\n' "$*" >&2; } die() { printf '\033[1;31mX %s\033[0m\n' "$*" >&2; exit 1; } has_tty() { [[ -e /dev/tty ]] && { : /dev/null; } usage() { cat </dev/tty local a read -r -p "输入 1/2/3 [回车=1]: " a /dev/tty local a read -r -p "输入 1/2/3 [回车=1]: " a /dev/null || true)" [[ "${code}" =~ ^(200|301|302)$ ]] } curl_fetch() { local url="$1" out="$2" local curl_args=(-fsSL --retry 2 --connect-timeout 10 --max-time 300) if [[ -n "${PROXY_URL}" ]]; then curl_args+=(-x "${PROXY_URL}") else curl_args+=(--noproxy '*') fi curl "${curl_args[@]}" "${url}" -o "${out}" } curl_text() { local url="$1" local curl_args=(-fsSL --retry 2 --connect-timeout 10 --max-time 60) if [[ -n "${PROXY_URL}" ]]; then curl_args+=(-x "${PROXY_URL}") else curl_args+=(--noproxy '*') fi curl "${curl_args[@]}" "${url}" } ensure_base_tools() { local missing=() command -v curl >/dev/null 2>&1 || missing+=("curl") command -v tar >/dev/null 2>&1 || missing+=("tar") [[ ${#missing[@]} -eq 0 ]] && return 0 warn "缺少基础工具: ${missing[*]},尝试用系统包管理器安装" if command -v apt-get >/dev/null 2>&1; then apt-get update -y apt-get install -y curl ca-certificates tar elif command -v dnf >/dev/null 2>&1; then dnf install -y curl ca-certificates tar elif command -v yum >/dev/null 2>&1; then yum install -y curl ca-certificates tar else die "未找到 apt/dnf/yum,请先手动安装: ${missing[*]}" fi } file_sha256() { local path="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "${path}" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "${path}" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "${path}" | sed 's/^.*= //' else die "未找到 sha256sum/shasum/openssl,无法校验 Codex 下载包" fi } normalize_release() { local raw="${CODEX_RELEASE:-latest}" case "${raw}" in ""|latest) printf 'latest' ;; rust-v*) printf '%s' "${raw#rust-v}" ;; v*) printf '%s' "${raw#v}" ;; *) printf '%s' "${raw}" ;; esac } resolve_latest_release() { local normalized effective api_tag normalized="$(normalize_release)" if [[ "${normalized}" != "latest" ]]; then [[ "${normalized}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]] || die "CODEX_RELEASE 格式不支持: ${CODEX_RELEASE}" printf '%s' "${normalized}" return 0 fi # GitHub latest redirect is easier to parse reliably than the release JSON. effective="$(curl_text "https://github.com/openai/codex/releases/latest" 2>/dev/null | sed -n 's#.*href="/openai/codex/releases/tag/rust-v\([^"]*\)".*#\1#p' | head -n 1 || true)" if [[ -n "${effective}" ]]; then printf '%s' "${effective}" return 0 fi api_tag="$(curl_text "https://api.github.com/repos/openai/codex/releases/latest" 2>/dev/null | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"rust-v\([^"]*\)".*/\1/p' | head -n 1 || true)" [[ -n "${api_tag}" ]] || die "无法解析 Codex 最新版本" printf '%s' "${api_tag}" } detect_vendor_target() { local os arch case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *) die "Codex installer 仅支持 Linux/macOS; Windows 请用官方 install.ps1" ;; esac case "$(uname -m)" in x86_64|amd64) arch="x86_64" ;; arm64|aarch64) arch="aarch64" ;; *) die "不支持的 CPU 架构: $(uname -m)" ;; esac if [[ "${os}" == "darwin" && "${arch}" == "x86_64" ]]; then if [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" == "1" ]]; then arch="aarch64" fi fi case "${os}:${arch}" in darwin:aarch64) printf 'aarch64-apple-darwin' ;; darwin:x86_64) printf 'x86_64-apple-darwin' ;; linux:aarch64) printf 'aarch64-unknown-linux-musl' ;; linux:x86_64) printf 'x86_64-unknown-linux-musl' ;; *) die "不支持的平台: ${os}/${arch}" ;; esac } install_package_fallback() { local version target asset checksum_asset base_url archive checksum expected actual local standalone_root releases_dir release_dir stage current_link tmp_link version="$(resolve_latest_release)" target="$(detect_vendor_target)" asset="codex-package-${target}.tar.gz" checksum_asset="codex-package_SHA256SUMS" base_url="https://github.com/openai/codex/releases/download/rust-v${version}" standalone_root="${REAL_CODEX_HOME}/packages/standalone" releases_dir="${standalone_root}/releases" release_dir="${releases_dir}/${version}-${target}" current_link="${standalone_root}/current" log "官方 installer 失败,改用 standalone package fallback" log "Codex release: ${version} | target: ${target}" if [[ -x "${release_dir}/bin/codex" && -x "${release_dir}/bin/codex-code-mode-host" ]]; then log "已存在完整 release: ${release_dir}" else tmp="$(mktemp -d)" archive="${tmp}/${asset}" checksum="${tmp}/${checksum_asset}" stage="${releases_dir}/.staging.${version}-${target}.$$" trap 'rm -rf "${tmp:-}" "${stage:-}"' RETURN mkdir -p "${releases_dir}" log "下载 ${asset}" curl_fetch "${base_url}/${checksum_asset}" "${checksum}" curl_fetch "${base_url}/${asset}" "${archive}" expected="$(awk -v asset="${asset}" '$2 == asset && $1 ~ /^[0-9a-fA-F]{64}$/ {print tolower($1); found=1; exit} END {if (!found) exit 1}' "${checksum}" 2>/dev/null || true)" [[ -n "${expected}" ]] || die "校验文件中未找到 ${asset} 的 SHA-256" actual="$(file_sha256 "${archive}")" [[ "${actual}" == "${expected}" ]] || die "Codex 下载包 SHA-256 不匹配: expected=${expected} actual=${actual}" rm -rf "${stage}" "${release_dir}" mkdir -p "${stage}" tar -xzf "${archive}" -C "${stage}" chmod 0755 "${stage}/bin/codex" "${stage}/bin/codex-code-mode-host" "${stage}/codex-path/rg" [[ -f "${stage}/codex-resources/bwrap" ]] && chmod 0755 "${stage}/codex-resources/bwrap" ln -sf "bin/codex" "${stage}/codex" mv "${stage}" "${release_dir}" rm -rf "${tmp}" trap - RETURN fi mkdir -p "${standalone_root}" "${REAL_BIN_DIR}" tmp_link="${standalone_root}/.current.$$" rm -f "${tmp_link}" ln -s "${release_dir}" "${tmp_link}" mv -Tf "${tmp_link}" "${current_link}" 2>/dev/null || { rm -f "${current_link}"; mv -f "${tmp_link}" "${current_link}"; } tmp_link="${REAL_BIN_DIR}/.codex.$$" rm -f "${tmp_link}" ln -s "${current_link}/bin/codex" "${tmp_link}" mv -Tf "${tmp_link}" "${REAL_CODEX}" 2>/dev/null || { rm -f "${REAL_CODEX}"; mv -f "${tmp_link}" "${REAL_CODEX}"; } } install_node_if_needed() { if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then log "已存在 Node/npm: $(node -v) / npm $(npm -v)" return 0 fi log "未找到 Node/npm,安装 Node.js ${NODE_MAJOR}.x" if [[ -n "${PROXY_URL}" ]]; then export HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" fi if command -v apt-get >/dev/null 2>&1; then apt-get update -y apt-get install -y curl ca-certificates gnupg curl_fetch "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" "/tmp/nodesource_setup_${NODE_MAJOR}.sh" bash "/tmp/nodesource_setup_${NODE_MAJOR}.sh" apt-get install -y nodejs rm -f "/tmp/nodesource_setup_${NODE_MAJOR}.sh" elif command -v dnf >/dev/null 2>&1; then curl_fetch "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" "/tmp/nodesource_setup_${NODE_MAJOR}.sh" bash "/tmp/nodesource_setup_${NODE_MAJOR}.sh" dnf install -y nodejs rm -f "/tmp/nodesource_setup_${NODE_MAJOR}.sh" elif command -v yum >/dev/null 2>&1; then curl_fetch "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" "/tmp/nodesource_setup_${NODE_MAJOR}.sh" bash "/tmp/nodesource_setup_${NODE_MAJOR}.sh" yum install -y nodejs rm -f "/tmp/nodesource_setup_${NODE_MAJOR}.sh" else die "未找到 apt/dnf/yum,请先手动安装 Node.js/npm 后重试" fi command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 || die "Node/npm 安装失败" log "Node/npm 已安装: $(node -v) / npm $(npm -v)" } prepare_dirs() { install -d -m 0755 -o root -g root "${INSTALL_ROOT}" "${REAL_BIN_DIR}" "${REAL_CODEX_HOME}" "${NPM_PREFIX}" } run_npm_installer() { install_node_if_needed log "通过 npm 安装/更新 @openai/codex@latest (prefix: ${NPM_PREFIX})" local env_args=( npm_config_prefix="${NPM_PREFIX}" npm_config_update_notifier=false npm_config_fund=false npm_config_audit=false ) if [[ -n "${PROXY_URL}" ]]; then env_args+=( HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" ALL_PROXY="${PROXY_URL}" http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" all_proxy="${PROXY_URL}" npm_config_proxy="${PROXY_URL}" npm_config_https_proxy="${PROXY_URL}" NO_PROXY="localhost,127.0.0.1,::1" no_proxy="localhost,127.0.0.1,::1" ) else env_args+=( HTTP_PROXY= HTTPS_PROXY= ALL_PROXY= http_proxy= https_proxy= all_proxy= npm_config_proxy= npm_config_https_proxy= NO_PROXY= no_proxy= ) fi env "${env_args[@]}" npm install -g @openai/codex@latest [[ -x "${NPM_CODEX}" ]] || die "npm 安装后未找到 ${NPM_CODEX}" ln -sfn "${NPM_CODEX}" "${REAL_CODEX}" } run_official_installer() { local tmp tmp="$(mktemp)" trap 'rm -f "${tmp}"' RETURN log "下载官方 Codex installer (${INSTALL_URL})" if ! curl_fetch "${INSTALL_URL}" "${tmp}"; then warn "官方 Codex installer 下载失败,尝试 fallback 安装" install_package_fallback rm -f "${tmp}" trap - RETURN return 0 fi log "运行官方 Codex installer (真实安装目录: ${REAL_BIN_DIR})" if [[ -n "${PROXY_URL}" ]]; then if env \ HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" ALL_PROXY="${PROXY_URL}" \ http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" all_proxy="${PROXY_URL}" \ NO_PROXY="localhost,127.0.0.1,::1" no_proxy="localhost,127.0.0.1,::1" \ CODEX_HOME="${REAL_CODEX_HOME}" \ CODEX_INSTALL_DIR="${REAL_BIN_DIR}" \ CODEX_NON_INTERACTIVE=1 \ PATH="${REAL_BIN_DIR}:${PATH}" \ sh "${tmp}"; then : else warn "官方 Codex installer 执行失败,尝试 fallback 安装" install_package_fallback fi else if env \ HTTP_PROXY= HTTPS_PROXY= ALL_PROXY= http_proxy= https_proxy= all_proxy= NO_PROXY= no_proxy= \ CODEX_HOME="${REAL_CODEX_HOME}" \ CODEX_INSTALL_DIR="${REAL_BIN_DIR}" \ CODEX_NON_INTERACTIVE=1 \ PATH="${REAL_BIN_DIR}:${PATH}" \ sh "${tmp}"; then : else warn "官方 Codex installer 执行失败,尝试 fallback 安装" install_package_fallback fi fi rm -f "${tmp}" trap - RETURN } fix_install_permissions() { chown -R root:root "${INSTALL_ROOT}" chmod -R a+rX "${INSTALL_ROOT}" [[ -x "${REAL_CODEX}" ]] || die "安装后未找到可执行文件: ${REAL_CODEX}" } write_wrapper() { local tmp node_path node_bin_dir tmp="$(mktemp)" node_path="$(command -v node 2>/dev/null || true)" [[ -n "${node_path}" ]] && node_bin_dir="$(dirname "${node_path}")" || node_bin_dir="" cat > "${tmp}" <&2 echo "Run: curl -fsSL https://claude.dahe2016.com/codex.sh | sudo bash -s -- install" >&2 exit 127 fi if [[ -n "\${CODEX_PROXY}" ]]; then export HTTP_PROXY="\${CODEX_PROXY}" HTTPS_PROXY="\${CODEX_PROXY}" ALL_PROXY="\${CODEX_PROXY}" export http_proxy="\${CODEX_PROXY}" https_proxy="\${CODEX_PROXY}" all_proxy="\${CODEX_PROXY}" export NO_PROXY="localhost,127.0.0.1,::1" no_proxy="localhost,127.0.0.1,::1" else unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy NO_PROXY no_proxy 2>/dev/null || true fi if [[ -n "\${NODE_BIN_DIR}" && -d "\${NODE_BIN_DIR}" ]]; then export PATH="\${NODE_BIN_DIR}:\${PATH}" fi # Keep proxy env vars available to the Codex client itself, but strip them from # subprocesses that Codex launches (shell commands, tests, package managers, etc.). exec "\${REAL_CODEX}" \\ -c "shell_environment_policy.exclude=\${PROXY_EXCLUDE_TOML}" \\ "\$@" EOF install -m 0755 -o root -g root "${tmp}" "${WRAPPER}" rm -f "${tmp}" log "已写入全局命令 ${WRAPPER} (Codex 代理: $(mask_proxy "${PROXY_URL}"))" } write_path_shim() { local first resolved wrapper_resolved backup first="$(command -v codex 2>/dev/null || true)" [[ -n "${first}" && "${first}" != "${WRAPPER}" ]] || return 0 resolved="$(readlink -f "${first}" 2>/dev/null || true)" wrapper_resolved="$(readlink -f "${WRAPPER}" 2>/dev/null || true)" [[ -n "${wrapper_resolved}" && "${resolved}" == "${wrapper_resolved}" ]] && return 0 if [[ -L "${first}" && "${resolved}" == *"/lib/node_modules/@openai/codex/bin/codex.js" ]]; then backup="${first}.npm-original" if [[ ! -e "${backup}" && ! -L "${backup}" ]]; then cp -P "${first}" "${backup}" fi ln -sfn "${WRAPPER}" "${first}" log "已修正 PATH 优先入口 ${first} -> ${WRAPPER}" return 0 fi warn "当前 codex 优先命中 ${first}, 未自动覆盖。请确保 ${WRAPPER} 在 PATH 中排在它前面。" } prepare_user_home() { local name="$1" home="$2" group [[ -n "${home}" && -d "${home}" ]] || return 0 group="$(id -gn "${name}" 2>/dev/null || echo "${name}")" install -d -m 0700 -o "${name}" -g "${group}" "${home}/.codex" } user_home() { local name="$1" home="" if command -v getent >/dev/null 2>&1; then home="$(getent passwd "${name}" 2>/dev/null | cut -d: -f6 || true)" fi if [[ -z "${home}" ]]; then home="$(eval "printf '%s' ~${name}" 2>/dev/null || true)" [[ "${home}" == "~${name}" ]] && home="" fi printf '%s' "${home}" } prepare_common_users() { prepare_user_home "root" "$(user_home root)" if id claude >/dev/null 2>&1; then prepare_user_home "claude" "$(user_home claude)" else log "用户 claude 不存在,跳过 /home/claude/.codex 初始化" fi } show_summary() { local version if [[ -x "${REAL_CODEX}" ]]; then version="$("${REAL_CODEX}" --version 2>/dev/null || echo "已安装")" else version="未安装" fi log "完成" echo " - Codex CLI: ${version}" echo " - 真实命令: ${REAL_CODEX}" echo " - 全局 wrapper: ${WRAPPER}" echo " - Codex 代理: $(mask_proxy "${PROXY_URL}")" echo " - 用户状态目录: 每个用户各自默认 ~/.codex (root 与 claude 分开登录)" echo " - yolo 等参数: 原样透传,例如 codex --yolo / codex exec --yolo '...'" echo " - 登录示例: codex login" if id claude >/dev/null 2>&1; then echo " su - claude -c 'codex login'" fi } if [[ "${MODE}" == "proxy" ]]; then log "模式: proxy (只重写 ${WRAPPER})" [[ -x "${REAL_CODEX}" ]] || warn "真实 Codex 还不存在: ${REAL_CODEX}; wrapper 会提示先 install" write_wrapper write_path_shim show_summary exit 0 fi ensure_base_tools prepare_dirs log "安装策略: npm @latest 优先,失败后自动 fallback (代理: $(mask_proxy "${PROXY_URL}"))" if run_npm_installer; then log "npm @openai/codex@latest 安装完成" else warn "npm 安装失败,尝试官方 standalone installer/fallback" run_official_installer fi fix_install_permissions write_wrapper write_path_shim prepare_common_users show_summary