Skip to content

Instantly share code, notes, and snippets.

@fuzzbuster
Created March 4, 2026 10:02
Show Gist options
  • Select an option

  • Save fuzzbuster/a06abb96d626ef03a67e929e6f73c996 to your computer and use it in GitHub Desktop.

Select an option

Save fuzzbuster/a06abb96d626ef03a67e929e6f73c996 to your computer and use it in GitHub Desktop.
Quickly setup Codex and Claude Code on macOS or Debian based Linux
#!/usr/bin/env bash
#
# macos_ai_bootstrap.sh - Claude Code + Codex 一鍵安裝與配置腳本(macOS)
#
# 目標:
# 1) 安裝 claude-code / codex(優先 Homebrew cask)
# 2) 配置 API Key 與可選 Base URL
# 3) 為 Codex 生成可管理的自訂 provider/profile
#
# 設計原則:可重複執行、盡量幂等、不覆蓋使用者既有未託管配置
#
# 參考項目:
# - Claude Code (official): https://github.com/anthropics/claude-code
# - Codex CLI (official): https://github.com/openai/codex
# - Community reference: https://github.com/PatchedReality/ai-cli-switchboard
set -euo pipefail
# ------------------------------------------------------------------------------
# 顏色與輸出
# ------------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
echo_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
# ------------------------------------------------------------------------------
# 全域預設值
# ------------------------------------------------------------------------------
NON_INTERACTIVE=false
NO_INSTALL=false
INSTALL_CLAUDE=true
INSTALL_CODEX=true
CONFIGURE_CLAUDE=true
CONFIGURE_CODEX=true
CLAUDE_API_KEY="${CLAUDE_API_KEY:-}"
CLAUDE_BASE_URL="${CLAUDE_BASE_URL:-}"
CODEX_API_KEY="${CODEX_API_KEY:-${OPENAI_API_KEY:-}}"
CODEX_BASE_URL="${CODEX_BASE_URL:-}"
CODEX_PROVIDER_ID="${CODEX_PROVIDER_ID:-ai_setup_openai_proxy}"
CODEX_PROFILE_NAME="${CODEX_PROFILE_NAME:-ai_setup_openai_proxy}"
CODEX_WIRE_API="${CODEX_WIRE_API:-responses}"
SET_CODEX_PROFILE_DEFAULT=true
WRITE_CLAUDE_SETTINGS=true
WRITE_CODEX_AUTH_JSON=false
ENABLE_BACKUP=true
TARGET_SHELL="${BOOTSTRAP_SHELL:-auto}"
AUTO_DETECT_KEYS=true
INSTALL_PARSER_TOOLS=true
SHELL_NAME=""
SHELL_RC_FILE=""
SHELL_PROFILE_FILE=""
AI_ENV_FILE="${AI_ENV_FILE:-${HOME}/.config/ai-cli/env.sh}"
CODEX_CONFIG_FILE="${HOME}/.codex/config.toml"
CODEX_AUTH_FILE="${HOME}/.codex/auth.json"
CLAUDE_SETTINGS_FILE="${HOME}/.claude/settings.json"
BACKUP_ROOT_DIR="${HOME}/.ai-bootstrap-backups"
RUN_TS="$(date +%Y%m%d_%H%M%S)"
RUN_BACKUP_DIR=""
CLAUDE_KEY_SOURCE=""
CODEX_KEY_SOURCE=""
HAVE_BREW=false
BREW_BIN=""
PLATFORM=""
IS_DEBIAN=false
APT_UPDATED=false
SUDO_CMD=""
# ------------------------------------------------------------------------------
# 幫助
# ------------------------------------------------------------------------------
usage() {
cat <<'USAGE'
Usage: bash macos_ai_bootstrap.sh [options]
Options:
--claude-api-key, --cak, -A <key> Set ANTHROPIC_API_KEY
--claude-base-url, --cbu, -U <url> Set ANTHROPIC_BASE_URL
--codex-api-key, --oak, -K <key> Set OPENAI_API_KEY
--codex-base-url, --obu, -B <url> Configure Codex custom provider base_url
--codex-wire-api, --cwa, -W <responses|chat> Codex provider wire API
--codex-provider-id, --cpi, -I <id> Codex custom provider id
--codex-profile, --cpf, -P <name> Codex profile name
--no-codex-default-profile Do not set Codex default profile in config.toml
--write-codex-auth-json, --wca, -a Write ~/.codex/auth.json with OPENAI_API_KEY
--no-claude-settings Skip writing ~/.claude/settings.json
--no-auto-detect-keys Disable migration auto-detect from existing files
--skip-parser-tools Do not auto-install jq/yq helper tools
--shell, -s <auto|zsh|bash> Select target shell rc/profile (default: auto)
--skip-install, --si, -x Skip installation, only configure
--skip-claude, --sc, -c Skip Claude install/config
--skip-codex, --sx, -d Skip Codex install/config
--no-backup, --nb Disable backup before file changes
--backup-dir, --bd <path> Backup root dir (default: ~/.ai-bootstrap-backups)
--non-interactive, --ni, -n Do not prompt, only use args/env
-h, --help Show this help
Examples:
# Short flags
bash macos_ai_bootstrap.sh -A 'sk-ant-xxx' -K 'sk-proj-xxx' -B 'https://api.openai.com/v1' -n
# Long flags
bash macos_ai_bootstrap.sh \
--claude-api-key 'sk-ant-xxx' \
--claude-base-url 'https://your-gateway.example.com' \
--codex-api-key 'sk-proj-xxx' \
--codex-base-url 'https://api.openai.com/v1'
bash macos_ai_bootstrap.sh --skip-install --non-interactive \
--codex-base-url 'https://us.api.openai.com/v1'
USAGE
}
# ------------------------------------------------------------------------------
# 參數解析
# ------------------------------------------------------------------------------
require_arg() {
local name="$1"
local value="${2:-}"
[[ -n "$value" ]] || echo_error "Missing value for $name"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--claude-api-key|--cak|-A)
require_arg "$1" "${2:-}"
CLAUDE_API_KEY="$2"
shift 2
;;
--claude-base-url|--cbu|-U)
require_arg "$1" "${2:-}"
CLAUDE_BASE_URL="$2"
shift 2
;;
--codex-api-key|--oak|-K)
require_arg "$1" "${2:-}"
CODEX_API_KEY="$2"
shift 2
;;
--codex-base-url|--obu|-B)
require_arg "$1" "${2:-}"
CODEX_BASE_URL="$2"
shift 2
;;
--codex-wire-api|--cwa|-W)
require_arg "$1" "${2:-}"
CODEX_WIRE_API="$2"
shift 2
;;
--codex-provider-id|--cpi|-I)
require_arg "$1" "${2:-}"
CODEX_PROVIDER_ID="$2"
shift 2
;;
--codex-profile|--cpf|-P)
require_arg "$1" "${2:-}"
CODEX_PROFILE_NAME="$2"
shift 2
;;
--no-codex-default-profile)
SET_CODEX_PROFILE_DEFAULT=false
shift
;;
--write-codex-auth-json|--wca|-a)
WRITE_CODEX_AUTH_JSON=true
shift
;;
--no-claude-settings)
WRITE_CLAUDE_SETTINGS=false
shift
;;
--no-auto-detect-keys)
AUTO_DETECT_KEYS=false
shift
;;
--skip-parser-tools)
INSTALL_PARSER_TOOLS=false
shift
;;
--shell|-s)
require_arg "$1" "${2:-}"
TARGET_SHELL="$2"
shift 2
;;
--skip-install|--si|-x)
NO_INSTALL=true
shift
;;
--skip-claude|--sc|-c)
INSTALL_CLAUDE=false
CONFIGURE_CLAUDE=false
shift
;;
--skip-codex|--sx|-d)
INSTALL_CODEX=false
CONFIGURE_CODEX=false
shift
;;
--non-interactive|--ni|-n)
NON_INTERACTIVE=true
shift
;;
--no-backup|--nb)
ENABLE_BACKUP=false
shift
;;
--backup-dir|--bd)
require_arg "$1" "${2:-}"
BACKUP_ROOT_DIR="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo_error "Unknown option: $1"
;;
esac
done
if [[ ! "$CODEX_PROVIDER_ID" =~ ^[A-Za-z0-9_]+$ ]]; then
echo_error "--codex-provider-id only supports letters, numbers, underscore"
fi
if [[ ! "$CODEX_PROFILE_NAME" =~ ^[A-Za-z0-9_]+$ ]]; then
echo_error "--codex-profile only supports letters, numbers, underscore"
fi
if [[ "$TARGET_SHELL" != "auto" && "$TARGET_SHELL" != "zsh" && "$TARGET_SHELL" != "bash" ]]; then
echo_error "--shell must be one of: auto, zsh, bash"
fi
# ------------------------------------------------------------------------------
# 基礎工具函式
# ------------------------------------------------------------------------------
ensure_dir() {
local dir="$1"
[[ -d "$dir" ]] || mkdir -p "$dir"
}
detect_platform() {
local uname_out
uname_out="$(uname -s)"
case "$uname_out" in
Darwin)
PLATFORM="macos"
;;
Linux)
PLATFORM="linux"
if [[ -f /etc/debian_version ]]; then
IS_DEBIAN=true
else
echo_error "Linux support currently targets Debian/Ubuntu only"
fi
;;
*)
echo_error "Unsupported platform: ${uname_out}"
;;
esac
}
resolve_sudo_cmd() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
SUDO_CMD=""
return 0
fi
if command -v sudo >/dev/null 2>&1; then
SUDO_CMD="sudo"
return 0
fi
SUDO_CMD=""
}
apt_update_once() {
[[ "$IS_DEBIAN" == true ]] || return 0
[[ "$APT_UPDATED" == true ]] && return 0
if [[ -n "$SUDO_CMD" ]]; then
DEBIAN_FRONTEND=noninteractive "$SUDO_CMD" apt-get update -y
else
DEBIAN_FRONTEND=noninteractive apt-get update -y
fi
APT_UPDATED=true
}
apt_install_packages() {
[[ "$IS_DEBIAN" == true ]] || return 0
[[ $# -gt 0 ]] || return 0
apt_update_once
if [[ -n "$SUDO_CMD" ]]; then
DEBIAN_FRONTEND=noninteractive "$SUDO_CMD" apt-get install -y "$@"
else
DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
fi
}
ensure_debian_pkg_for_cmd() {
local cmd="$1"
shift
if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi
if [[ "$IS_DEBIAN" != true ]]; then
return 0
fi
if [[ "${EUID:-$(id -u)}" -ne 0 && -z "$SUDO_CMD" ]]; then
echo_warn "Cannot install $cmd automatically (need root or sudo)"
return 0
fi
echo_info "Installing missing dependency: $cmd ($*)"
apt_install_packages "$@" || echo_warn "Failed to install package(s): $*"
}
resolve_shell_files() {
local detected
if [[ "$TARGET_SHELL" == "auto" ]]; then
detected="$(basename "${SHELL:-}")"
else
detected="$TARGET_SHELL"
fi
case "$detected" in
zsh)
SHELL_NAME="zsh"
SHELL_RC_FILE="${HOME}/.zshrc"
SHELL_PROFILE_FILE="${HOME}/.zprofile"
;;
bash)
SHELL_NAME="bash"
SHELL_RC_FILE="${HOME}/.bashrc"
SHELL_PROFILE_FILE="${HOME}/.bash_profile"
;;
*)
SHELL_NAME="zsh"
SHELL_RC_FILE="${HOME}/.zshrc"
SHELL_PROFILE_FILE="${HOME}/.zprofile"
echo_warn "Unknown shell '$detected', fallback to zsh rc/profile"
;;
esac
}
ensure_shell_rc_block() {
local marker="$1"
local content="$2"
touch "$SHELL_RC_FILE"
if ! grep -qF "$marker" "$SHELL_RC_FILE" 2>/dev/null; then
echo_info "Appending block to $SHELL_RC_FILE: $marker"
cat <<BLOCK >>"$SHELL_RC_FILE"
$content
BLOCK
fi
}
init_backup_dir() {
if [[ "$ENABLE_BACKUP" != true ]]; then
return 0
fi
if [[ -z "$RUN_BACKUP_DIR" ]]; then
RUN_BACKUP_DIR="${BACKUP_ROOT_DIR}/${RUN_TS}"
mkdir -p "$RUN_BACKUP_DIR"
echo_info "Backup directory: $RUN_BACKUP_DIR"
fi
}
backup_path_if_exists() {
local path="$1"
local label="$2"
local sanitized target
[[ "$ENABLE_BACKUP" == true ]] || return 0
[[ -e "$path" ]] || return 0
init_backup_dir
sanitized="$(printf "%s" "$path" | sed 's#/#__#g')"
target="${RUN_BACKUP_DIR}/${sanitized}"
if [[ ! -e "$target" ]]; then
cp -a "$path" "$target"
echo_info "Backed up ${label}: $path -> $target"
fi
}
json_escape() {
# Minimal JSON escape for string values (portable on macOS/BSD awk).
awk '
BEGIN { first = 1 }
{
gsub(/\\/, "\\\\")
gsub(/"/, "\\\"")
if (first == 0) printf "\\n"
printf "%s", $0
first = 0
}
' <<<"$1"
}
shell_single_quote() {
# 將任意字串包成可安全寫入 shell 的單引號字面值
local raw="$1"
printf "'%s'" "$(printf "%s" "$raw" | sed "s/'/'\"'\"'/g")"
}
strip_wrapping_quotes() {
local v="$1"
local first last
if (( ${#v} < 2 )); then
printf "%s" "$v"
return 0
fi
first="${v:0:1}"
last="${v: -1}"
if [[ ( "$first" == "'" && "$last" == "'" ) || ( "$first" == '"' && "$last" == '"' ) ]]; then
printf "%s" "${v:1:${#v}-2}"
else
printf "%s" "$v"
fi
}
is_placeholder_value() {
local value lowered
value="$1"
[[ -n "$value" ]] || return 0
lowered="$(printf "%s" "$value" | tr '[:upper:]' '[:lower:]')"
case "$lowered" in
x|xx|xxx|changeme|change_me|replace_me|your_key|your_api_key|your-api-key|api_key_here|sk-ant-xxx|sk-proj-xxx|sk-xxx)
return 0
;;
esac
return 1
}
extract_shell_var_value() {
local file="$1" key="$2" line value
[[ -f "$file" ]] || return 1
line="$(awk -v k="$key" '
$0 ~ ("^[[:space:]]*(export[[:space:]]+)?" k "[[:space:]]*=") { found=$0 }
END { if (found != "") print found }
' "$file")"
[[ -n "$line" ]] || return 1
value="${line#*=}"
value="$(printf "%s" "$value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
value="$(strip_wrapping_quotes "$value")"
[[ -n "$value" ]] || return 1
printf "%s" "$value"
}
extract_json_string_value() {
local file="$1" key="$2" value
[[ -f "$file" ]] || return 1
if command -v jq >/dev/null 2>&1; then
value="$(jq -r --arg k "$key" 'first(.. | objects | .[$k]? | select(type == "string")) // empty' "$file" 2>/dev/null || true)"
if [[ -n "$value" ]]; then
printf "%s" "$value"
return 0
fi
fi
awk -v k="$key" '
{
pat = "\"" k "\"[[:space:]]*:[[:space:]]*\""
pos = match($0, pat)
if (pos) {
s = substr($0, RSTART + RLENGTH)
end = index(s, "\"")
if (end > 0) val = substr(s, 1, end - 1)
}
}
END {
if (val != "") print val
else exit 1
}
' "$file"
}
extract_toml_string_value() {
local file="$1" key="$2" line value
[[ -f "$file" ]] || return 1
if command -v yq >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
if yq --help 2>/dev/null | grep -q "eval"; then
value="$(yq eval -p=toml -o=json '.' "$file" 2>/dev/null | jq -r --arg k "$key" 'first(.. | objects | .[$k]? | select(type == "string")) // empty' 2>/dev/null || true)"
if [[ -n "$value" ]]; then
printf "%s" "$value"
return 0
fi
fi
fi
line="$(awk -v k="$key" '
$0 ~ ("^[[:space:]]*" k "[[:space:]]*=") { found=$0 }
END { if (found != "") print found }
' "$file")"
[[ -n "$line" ]] || return 1
value="${line#*=}"
value="$(printf "%s" "$value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
value="$(strip_wrapping_quotes "$value")"
[[ -n "$value" ]] || return 1
printf "%s" "$value"
}
auto_detect_migration_values() {
local v f
[[ "$AUTO_DETECT_KEYS" == true ]] || return 0
if [[ -z "$CLAUDE_API_KEY" ]]; then
for f in "$AI_ENV_FILE" "$HOME/.zsh/ai_cli.env" "$HOME/.config/ai-cli/env.sh"; do
v="$(extract_shell_var_value "$f" "ANTHROPIC_API_KEY" 2>/dev/null || true)"
if [[ -z "$v" ]]; then
v="$(extract_shell_var_value "$f" "X_ANTHROPIC_API_KEY" 2>/dev/null || true)"
fi
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CLAUDE_API_KEY="$v"
CLAUDE_KEY_SOURCE="$f"
break
fi
done
fi
if [[ -z "$CLAUDE_API_KEY" ]]; then
for f in "$CLAUDE_SETTINGS_FILE" "$HOME/.claude/settings.json" "$HOME/.claude.json"; do
v="$(extract_json_string_value "$f" "ANTHROPIC_API_KEY" 2>/dev/null || true)"
if [[ -z "$v" ]]; then
v="$(extract_json_string_value "$f" "X_ANTHROPIC_API_KEY" 2>/dev/null || true)"
fi
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CLAUDE_API_KEY="$v"
CLAUDE_KEY_SOURCE="$f"
break
fi
done
fi
if [[ -z "$CLAUDE_BASE_URL" ]]; then
for f in "$AI_ENV_FILE" "$HOME/.zsh/ai_cli.env" "$HOME/.config/ai-cli/env.sh"; do
v="$(extract_shell_var_value "$f" "ANTHROPIC_BASE_URL" 2>/dev/null || true)"
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CLAUDE_BASE_URL="$v"
break
fi
done
fi
if [[ -z "$CLAUDE_BASE_URL" ]]; then
for f in "$CLAUDE_SETTINGS_FILE" "$HOME/.claude/settings.json"; do
v="$(extract_json_string_value "$f" "ANTHROPIC_BASE_URL" 2>/dev/null || true)"
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CLAUDE_BASE_URL="$v"
break
fi
done
fi
if [[ -z "$CODEX_API_KEY" ]]; then
for f in "$AI_ENV_FILE" "$HOME/.zsh/ai_cli.env" "$HOME/.config/ai-cli/env.sh"; do
v="$(extract_shell_var_value "$f" "OPENAI_API_KEY" 2>/dev/null || true)"
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CODEX_API_KEY="$v"
CODEX_KEY_SOURCE="$f"
break
fi
done
fi
if [[ -z "$CODEX_API_KEY" ]]; then
v="$(extract_json_string_value "$CODEX_AUTH_FILE" "OPENAI_API_KEY" 2>/dev/null || true)"
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CODEX_API_KEY="$v"
CODEX_KEY_SOURCE="$CODEX_AUTH_FILE"
fi
fi
if [[ -z "$CODEX_BASE_URL" ]]; then
v="$(extract_toml_string_value "$CODEX_CONFIG_FILE" "base_url" 2>/dev/null || true)"
if [[ -n "$v" ]] && ! is_placeholder_value "$v"; then
CODEX_BASE_URL="$v"
fi
fi
if [[ -n "$CLAUDE_KEY_SOURCE" ]]; then
echo_info "Auto-detected Claude key from $CLAUDE_KEY_SOURCE"
fi
if [[ -n "$CODEX_KEY_SOURCE" ]]; then
echo_info "Auto-detected Codex key from $CODEX_KEY_SOURCE"
fi
}
upsert_export() {
local file="$1" key="$2" value="$3"
local quoted line tmp found
[[ -n "$value" ]] || return 0
quoted="$(shell_single_quote "$value")"
line="export ${key}=${quoted}"
ensure_dir "$(dirname "$file")"
touch "$file"
chmod 600 "$file" || true
tmp="$(mktemp)"
found=false
while IFS= read -r current || [[ -n "$current" ]]; do
if [[ "$current" =~ ^export[[:space:]]+${key}= ]]; then
if [[ "$found" == false ]]; then
printf "%s\n" "$line" >>"$tmp"
found=true
fi
else
printf "%s\n" "$current" >>"$tmp"
fi
done <"$file"
if [[ "$found" == false ]]; then
printf "%s\n" "$line" >>"$tmp"
fi
mv "$tmp" "$file"
chmod 600 "$file" || true
echo_info "Updated $key in $file"
}
remove_marked_block() {
local file="$1" start_marker="$2" end_marker="$3"
local tmp
[[ -f "$file" ]] || return 0
tmp="$(mktemp)"
awk -v s="$start_marker" -v e="$end_marker" '
$0 == s { skipping=1; next }
$0 == e { skipping=0; next }
!skipping { print }
' "$file" >"$tmp"
mv "$tmp" "$file"
}
append_managed_block() {
local file="$1" start_marker="$2" end_marker="$3" content="$4"
local tmp
ensure_dir "$(dirname "$file")"
touch "$file"
remove_marked_block "$file" "$start_marker" "$end_marker"
tmp="$(mktemp)"
{
if [[ -s "$file" ]]; then
cat "$file"
printf "\n"
fi
printf "%s\n" "$start_marker"
printf "%s\n" "$content"
printf "%s\n" "$end_marker"
} >"$tmp"
mv "$tmp" "$file"
}
prepend_managed_block() {
local file="$1" start_marker="$2" end_marker="$3" content="$4"
local tmp
ensure_dir "$(dirname "$file")"
touch "$file"
remove_marked_block "$file" "$start_marker" "$end_marker"
tmp="$(mktemp)"
{
printf "%s\n" "$start_marker"
printf "%s\n" "$content"
printf "%s\n" "$end_marker"
printf "\n"
cat "$file"
} >"$tmp"
mv "$tmp" "$file"
}
has_root_profile_key() {
local file="$1"
[[ -f "$file" ]] || return 1
awk '
/^[[:space:]]*#/ { next }
/^[[:space:]]*$/ { next }
/^[[:space:]]*\[/ { exit(found ? 0 : 1) }
/^[[:space:]]*profile[[:space:]]*=/ { found=1; exit 0 }
END { exit(found ? 0 : 1) }
' "$file"
}
prompt_if_empty() {
local var_name="$1" message="$2" secret="${3:-false}"
local value="${!var_name:-}"
[[ -n "$value" ]] && return 0
[[ "$NON_INTERACTIVE" == true ]] && return 0
if [[ "$secret" == true ]]; then
read -r -s -p "$message" value
echo
else
read -r -p "$message" value
fi
printf -v "$var_name" '%s' "$value"
}
# ------------------------------------------------------------------------------
# Homebrew / 安裝
# ------------------------------------------------------------------------------
ensure_homebrew() {
local brew_candidate=""
if [[ "$PLATFORM" != "macos" ]]; then
if command -v brew &>/dev/null; then
BREW_BIN="$(command -v brew)"
HAVE_BREW=true
echo_info "Homebrew detected on Linux: ${BREW_BIN}"
else
HAVE_BREW=false
echo_info "Homebrew not required on ${PLATFORM}"
fi
return 0
fi
if command -v brew &>/dev/null; then
BREW_BIN="$(command -v brew)"
else
[[ -x "/opt/homebrew/bin/brew" ]] && brew_candidate="/opt/homebrew/bin/brew"
[[ -z "$brew_candidate" && -x "/usr/local/bin/brew" ]] && brew_candidate="/usr/local/bin/brew"
[[ -n "$brew_candidate" ]] && BREW_BIN="$brew_candidate"
fi
if [[ -n "$BREW_BIN" ]]; then
touch "$SHELL_PROFILE_FILE"
local shellenv_line
shellenv_line="eval \"\$(${BREW_BIN} shellenv)\""
grep -qxF "$shellenv_line" "$SHELL_PROFILE_FILE" || printf "%s\n" "$shellenv_line" >>"$SHELL_PROFILE_FILE"
eval "$($BREW_BIN shellenv)"
HAVE_BREW=true
echo_info "Homebrew is ready."
else
HAVE_BREW=false
echo_warn "Homebrew not found; skipping Claude/Codex brew install"
fi
}
ensure_brew_pkg() {
local pkg="$1"
if [[ "$HAVE_BREW" != true ]]; then
return 0
fi
if ! brew list --formula "$pkg" >/dev/null 2>&1; then
echo_info "Installing brew package: $pkg"
brew install "$pkg" || echo_warn "Failed to install brew package: $pkg"
fi
}
ensure_brew_cask() {
local cask="$1"
if [[ "$HAVE_BREW" != true ]]; then
echo_warn "brew unavailable, skip cask: $cask"
return 0
fi
if ! brew list --cask "$cask" >/dev/null 2>&1; then
echo_info "Installing cask: $cask"
brew install --cask "$cask" || echo_warn "Failed to install cask: $cask"
else
echo_info "Cask already installed: $cask"
fi
}
ensure_parser_tools() {
[[ "$INSTALL_PARSER_TOOLS" == true ]] || return 0
if [[ "$PLATFORM" == "macos" ]]; then
if [[ "$HAVE_BREW" == true ]]; then
ensure_brew_pkg jq
ensure_brew_pkg yq
else
echo_warn "Skip parser tools install on macOS because brew is unavailable"
fi
return 0
fi
if [[ "$IS_DEBIAN" == true ]]; then
ensure_debian_pkg_for_cmd jq jq
ensure_debian_pkg_for_cmd yq yq
return 0
fi
}
ensure_npm_available() {
if command -v npm >/dev/null 2>&1; then
return 0
fi
if [[ "$IS_DEBIAN" == true ]]; then
ensure_debian_pkg_for_cmd nodejs nodejs
ensure_debian_pkg_for_cmd npm npm
fi
}
ensure_claude_installed() {
if command -v claude >/dev/null 2>&1; then
echo_info "Claude Code already installed"
return 0
fi
if [[ "$PLATFORM" == "macos" ]]; then
ensure_brew_cask "claude-code"
fi
if ! command -v claude >/dev/null 2>&1 && [[ "$IS_DEBIAN" == true ]]; then
ensure_debian_pkg_for_cmd curl curl ca-certificates
fi
if ! command -v claude >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
echo_warn "Trying official Claude installer via curl"
curl -fsSL https://claude.ai/install.sh | bash || echo_warn "Official Claude installer failed"
fi
}
ensure_codex_installed() {
if command -v codex >/dev/null 2>&1; then
echo_info "Codex already installed"
return 0
fi
if [[ "$PLATFORM" == "macos" ]]; then
ensure_brew_cask "codex"
fi
if ! command -v codex >/dev/null 2>&1; then
ensure_npm_available
fi
if ! command -v codex >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
echo_warn "Trying npm install for Codex"
if ! npm install -g @openai/codex; then
if [[ -n "$SUDO_CMD" ]]; then
"$SUDO_CMD" npm install -g @openai/codex || echo_warn "npm install @openai/codex failed"
else
echo_warn "npm install @openai/codex failed"
fi
fi
fi
}
# ------------------------------------------------------------------------------
# 配置
# ------------------------------------------------------------------------------
configure_shell_env() {
local loader_marker="# Load AI CLI env (Claude/Codex)"
local loader_block
loader_block=$(cat <<BLOCK
# Load AI CLI env (Claude/Codex)
[[ -f "$AI_ENV_FILE" ]] && source "$AI_ENV_FILE"
BLOCK
)
backup_path_if_exists "$SHELL_RC_FILE" "${SHELL_NAME} rc file"
backup_path_if_exists "$AI_ENV_FILE" "AI env file"
ensure_dir "$(dirname "$AI_ENV_FILE")"
touch "$AI_ENV_FILE"
chmod 600 "$AI_ENV_FILE" || true
ensure_shell_rc_block "$loader_marker" "$loader_block"
if [[ -n "$CLAUDE_API_KEY" ]]; then
if [[ "$CONFIGURE_CLAUDE" == true ]]; then
upsert_export "$AI_ENV_FILE" "ANTHROPIC_API_KEY" "$CLAUDE_API_KEY"
fi
fi
if [[ -n "$CLAUDE_BASE_URL" ]]; then
if [[ "$CONFIGURE_CLAUDE" == true ]]; then
# 注意:此變數常用於相容 gateway;未在 Claude Code 最新設定頁明確列為通用變數。
upsert_export "$AI_ENV_FILE" "ANTHROPIC_BASE_URL" "$CLAUDE_BASE_URL"
echo_warn "Configured ANTHROPIC_BASE_URL for Claude-compatible gateway usage"
fi
fi
if [[ -n "$CODEX_API_KEY" ]]; then
if [[ "$CONFIGURE_CODEX" == true ]]; then
upsert_export "$AI_ENV_FILE" "OPENAI_API_KEY" "$CODEX_API_KEY"
fi
fi
}
configure_claude_settings_file() {
local f="$CLAUDE_SETTINGS_FILE"
local api_key_escaped base_url_escaped
if [[ "$CONFIGURE_CLAUDE" != true || "$WRITE_CLAUDE_SETTINGS" != true ]]; then
return 0
fi
if [[ -z "$CLAUDE_API_KEY" && -z "$CLAUDE_BASE_URL" ]]; then
echo_info "Claude key/base_url not provided, skip writing $f"
return 0
fi
ensure_dir "$(dirname "$f")"
backup_path_if_exists "$f" "Claude settings.json"
api_key_escaped="$(json_escape "$CLAUDE_API_KEY")"
base_url_escaped="$(json_escape "$CLAUDE_BASE_URL")"
cat >"$f" <<EOF
{
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
"DISABLE_ERROR_REPORTING": "1",
"DISABLE_BUG_COMMAND": "1",
"DISABLE_TELEMETRY": "1",
"ANTHROPIC_API_KEY": "${api_key_escaped}",
"X_ANTHROPIC_API_KEY": "${api_key_escaped}",
"ANTHROPIC_BASE_URL": "${base_url_escaped}"
},
"skipWebFetchPreflight": true
}
EOF
chmod 600 "$f" || true
echo_info "Updated Claude settings file: $f"
}
configure_codex_auth_file() {
local f="$CODEX_AUTH_FILE"
local api_key_escaped
if [[ "$CONFIGURE_CODEX" != true || "$WRITE_CODEX_AUTH_JSON" != true ]]; then
return 0
fi
if [[ -z "$CODEX_API_KEY" ]]; then
echo_warn "Skipping ~/.codex/auth.json because OPENAI_API_KEY is empty"
return 0
fi
ensure_dir "$(dirname "$f")"
backup_path_if_exists "$f" "Codex auth.json"
api_key_escaped="$(json_escape "$CODEX_API_KEY")"
cat >"$f" <<EOF
{
"OPENAI_API_KEY": "${api_key_escaped}"
}
EOF
chmod 600 "$f" || true
echo_info "Updated Codex auth file: $f"
}
configure_codex_provider() {
local cfg="$CODEX_CONFIG_FILE"
local provider_block_start="# >>> ai-agent-setup codex provider >>>"
local provider_block_end="# <<< ai-agent-setup codex provider <<<"
local profile_block_start="# >>> ai-agent-setup codex default profile >>>"
local profile_block_end="# <<< ai-agent-setup codex default profile <<<"
[[ -n "$CODEX_BASE_URL" ]] || {
echo_info "CODEX_BASE_URL not set, skip Codex provider configuration"
return 0
}
if [[ "$CODEX_WIRE_API" != "responses" && "$CODEX_WIRE_API" != "chat" ]]; then
echo_error "--codex-wire-api must be 'responses' or 'chat'"
fi
ensure_dir "$(dirname "$cfg")"
backup_path_if_exists "$cfg" "Codex config.toml"
touch "$cfg"
local provider_block
provider_block=$(cat <<TOML
[model_providers.${CODEX_PROVIDER_ID}]
name = "AI Agent Setup Proxy"
base_url = "${CODEX_BASE_URL}"
env_key = "OPENAI_API_KEY"
wire_api = "${CODEX_WIRE_API}"
[profiles.${CODEX_PROFILE_NAME}]
model_provider = "${CODEX_PROVIDER_ID}"
TOML
)
append_managed_block "$cfg" "$provider_block_start" "$provider_block_end" "$provider_block"
echo_info "Updated Codex provider/profile block in $cfg"
if [[ "$SET_CODEX_PROFILE_DEFAULT" == true ]]; then
if has_root_profile_key "$cfg"; then
echo_warn "Detected existing top-level 'profile' in $cfg; keep existing value"
echo_warn "If needed, run: codex --profile ${CODEX_PROFILE_NAME}"
else
prepend_managed_block "$cfg" "$profile_block_start" "$profile_block_end" "profile = \"${CODEX_PROFILE_NAME}\""
echo_info "Set Codex default profile to ${CODEX_PROFILE_NAME}"
fi
else
echo_info "Default profile not changed. Use: codex --profile ${CODEX_PROFILE_NAME}"
fi
}
# ------------------------------------------------------------------------------
# 主流程
# ------------------------------------------------------------------------------
echo_info "Starting AI CLI bootstrap (Claude Code + Codex)..."
detect_platform
resolve_sudo_cmd
resolve_shell_files
echo_info "Platform: ${PLATFORM}"
echo_info "Target shell: ${SHELL_NAME} (rc: ${SHELL_RC_FILE}, profile: ${SHELL_PROFILE_FILE})"
auto_detect_migration_values
if [[ "$NON_INTERACTIVE" != true ]]; then
if [[ "$CONFIGURE_CLAUDE" == true ]]; then
prompt_if_empty CLAUDE_API_KEY "Enter Claude API key (ANTHROPIC_API_KEY, leave blank to skip): " true
prompt_if_empty CLAUDE_BASE_URL "Enter Claude base URL (optional, leave blank to skip): " false
fi
if [[ "$CONFIGURE_CODEX" == true ]]; then
prompt_if_empty CODEX_API_KEY "Enter Codex/OpenAI API key (OPENAI_API_KEY, leave blank to skip): " true
prompt_if_empty CODEX_BASE_URL "Enter Codex custom base URL (optional, leave blank to skip): " false
fi
fi
if [[ "$NO_INSTALL" != true ]]; then
ensure_homebrew
ensure_parser_tools
if [[ "$INSTALL_CLAUDE" == true ]]; then
ensure_claude_installed
fi
if [[ "$INSTALL_CODEX" == true ]]; then
ensure_codex_installed
fi
else
echo_info "Skipping installation by --skip-install"
fi
configure_shell_env
configure_claude_settings_file
configure_codex_auth_file
if [[ "$CONFIGURE_CODEX" == true ]]; then
configure_codex_provider
fi
echo_info "Verifying installed binaries..."
if command -v claude >/dev/null 2>&1; then
echo_info "claude: $(claude --version 2>/dev/null || echo 'installed')"
else
echo_warn "claude not found in PATH"
fi
if command -v codex >/dev/null 2>&1; then
echo_info "codex: $(codex --version 2>/dev/null || echo 'installed')"
else
echo_warn "codex not found in PATH"
fi
echo_info "Done. Reload shell with: source ${SHELL_RC_FILE}"
if [[ -n "$CODEX_BASE_URL" ]]; then
echo_info "Codex active profile: ${CODEX_PROFILE_NAME}"
fi
echo_info "Auth summary:"
if [[ -n "$CLAUDE_API_KEY" ]]; then
echo_info "1) Claude API key configured. You can skip interactive login."
else
echo_info "1) Claude API key not set. Use /login or rerun with --cak."
fi
if [[ -n "$CODEX_API_KEY" ]]; then
echo_info "2) Codex/OpenAI API key configured. You can skip ChatGPT login."
else
echo_info "2) Codex API key not set. Use login or rerun with --oak."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment