Skip to content

Instantly share code, notes, and snippets.

@megamen32
Last active June 24, 2026 13:36
Show Gist options
  • Select an option

  • Save megamen32/523e2d6325641df69f2e0aa47ea054df to your computer and use it in GitHub Desktop.

Select an option

Save megamen32/523e2d6325641df69f2e0aa47ea054df to your computer and use it in GitHub Desktop.
Change profiles of codex (vscode extension or app) easily
#!/usr/bin/env bash
set -euo pipefail
# инстукция https://habr.com/ru/articles/1020450/
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
AUTH_FILE="$CODEX_DIR/auth.json"
OPENCODE_AUTH_FILE="${OPENCODE_AUTH_FILE:-$HOME/.local/share/opencode/auth.json}"
PROFILES_DIR="${CODEX_AUTH_PROFILES_DIR:-$HOME/.codex-auth-profiles}"
CURRENT_FILE="$PROFILES_DIR/.current_profile"
PROXY_FILE="$PROFILES_DIR/.default_proxy"
CACHE_TIME_FILE="$PROFILES_DIR/.cache_time"
CACHE_DIR="$PROFILES_DIR/.usage-cache"
DATE_MODE_FILE="$PROFILES_DIR/.date_mode"
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
BOLD=''
DIM=''
NC=''
fi
SHA256_MODE=""
usage() {
cat <<'USAGE'
Usage:
codex-auth List profiles with usage stats (streaming)
codex-auth usage Show detailed rate limits for current auth
codex-auth raw Show raw API response for current auth
codex-auth check Quick one-line status of current profile
codex-auth config Show all current settings
OpenCode sync:
When switching profiles, codex-auth also updates ~/.local/share/opencode/auth.json
if that file exists. Override with OPENCODE_AUTH_FILE=/path/to/auth.json.
codex-auth save <name> Save current auth.json as a new profile
codex-auth use <name> Switch to a saved profile
codex-auth rm <name> [--force] Delete a saved profile
codex-auth rename <old> <new> Rename a saved profile
codex-auth list List profiles with usage stats (streaming)
codex-auth list-fast List profiles without API calls
codex-auth backup Backup current auth.json with timestamp
codex-auth proxy set <url> Save default proxy for all curl calls
codex-auth proxy show Show saved default proxy
codex-auth proxy unset Remove saved default proxy
codex-auth cache-time Show usage cache ttl in seconds
codex-auth cache-time <sec> Set usage cache ttl (0 disables cache)
codex-auth cache-clear Clear all cached usage data
codex-auth date-mode Show current date display mode
codex-auth date-mode relative Show reset times as relative (5h, 3d 2h)
codex-auth date-mode absolute Show reset times as absolute local time
codex-auth completion bash Print bash completion script
codex-auth completion zsh Print zsh completion script
Aliases:
set, switch, current, show, force-set
USAGE
}
# ═══════════════════════════════════════════
# Utility functions
# ═══════════════════════════════════════════
ensure_dirs() {
mkdir -p "$PROFILES_DIR" "$CODEX_DIR" "$CACHE_DIR"
}
detect_sha256_mode() {
if command -v sha256sum >/dev/null 2>&1; then
SHA256_MODE="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
SHA256_MODE="shasum"
else
echo -e "${RED}Required command not found: sha256sum or shasum${NC}" >&2
exit 1
fi
}
sha256_string() {
local value="$1"
if [[ "$SHA256_MODE" == "sha256sum" ]]; then
printf '%s' "$value" | sha256sum | awk '{print $1}'
else
printf '%s' "$value" | shasum -a 256 | awk '{print $1}'
fi
}
file_sha256() {
local file="$1"
if [[ "$SHA256_MODE" == "sha256sum" ]]; then
sha256sum "$file" | awk '{print $1}'
else
shasum -a 256 "$file" | awk '{print $1}'
fi
}
base64_decode_stdin() {
if printf '' | base64 -d >/dev/null 2>&1; then
base64 -d
elif printf '' | base64 -D >/dev/null 2>&1; then
base64 -D
else
echo -e "${RED}Your base64 command does not support -d or -D${NC}" >&2
exit 1
fi
}
require_tools() {
local tools="jq curl awk sort tr basename dirname mktemp date stat"
local tool
for tool in $tools; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo -e "${RED}Required command not found: $tool${NC}" >&2
exit 1
fi
done
if ! command -v base64 >/dev/null 2>&1; then
echo -e "${RED}Required command not found: base64${NC}" >&2
exit 1
fi
detect_sha256_mode
}
validate_name() {
local name="$1"
if [[ ! "$name" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo -e "${RED}Invalid profile name: $name${NC}" >&2
exit 1
fi
}
require_current_auth() {
if [[ ! -f "$AUTH_FILE" ]]; then
echo -e "${RED}Current auth file not found: $AUTH_FILE${NC}" >&2
exit 1
fi
}
profile_path() {
printf '%s/%s.json\n' "$PROFILES_DIR" "$1"
}
write_current_hint() {
printf '%s\n' "$1" > "$CURRENT_FILE"
chmod 600 "$CURRENT_FILE"
}
clear_current_hint() {
rm -f "$CURRENT_FILE"
}
safe_install_file() {
local src="$1"
local dst="$2"
local tmp
mkdir -p "$(dirname "$dst")"
tmp="$(mktemp "$dst.tmp.XXXXXX")"
cp "$src" "$tmp"
chmod 600 "$tmp"
mv "$tmp" "$dst"
}
existing_opencode_auth_files() {
local seen=":"
local candidates=()
local f
candidates+=("$OPENCODE_AUTH_FILE")
candidates+=("$HOME/.local/share/opencode/auth.json")
candidates+=("$HOME/Library/Application Support/opencode/auth.json")
candidates+=("$HOME/Library/Application Support/OpenCode/auth.json")
for f in "${candidates[@]}"; do
[[ -n "$f" ]] || continue
[[ -f "$f" ]] || continue
case "$seen" in
*":$f:"*) continue ;;
esac
seen="$seen$f:"
printf '%s\n' "$f"
done
}
sync_opencode_auth_if_present() {
local src="$1"
local synced=0
local f
while IFS= read -r f; do
[[ -n "$f" ]] || continue
safe_install_file "$src" "$f"
echo "Synced OpenCode auth: $f"
synced=$((synced + 1))
done < <(existing_opencode_auth_files)
if (( synced == 0 )); then
echo "OpenCode auth not found; skipped: $OPENCODE_AUTH_FILE"
fi
}
# Get terminal width with sensible fallback
get_terminal_width() {
local width
if [[ -n "${COLUMNS:-}" ]] && [[ "$COLUMNS" =~ ^[0-9]+$ ]] && (( COLUMNS > 0 )); then
printf '%s' "$COLUMNS"
return
fi
if command -v tput >/dev/null 2>&1 && [[ -t 1 ]]; then
width="$(tput cols 2>/dev/null || true)"
if [[ -n "$width" ]] && [[ "$width" =~ ^[0-9]+$ ]] && (( width > 0 )); then
printf '%s' "$width"
return
fi
fi
printf '80'
}
json_get() {
jq -r "$2 // empty" "$1" 2>/dev/null || true
}
base64url_decode() {
local value="$1"
local padded="$1"
case $(( ${#value} % 4 )) in
2) padded="${value}==" ;;
3) padded="${value}=" ;;
esac
printf '%s' "$padded" | tr '_-' '/+' | base64_decode_stdin 2>/dev/null
}
# ═══════════════════════════════════════════
# Profile functions
# ═══════════════════════════════════════════
profile_payload_json() {
local file="$1"
local id_token payload
id_token="$(json_get "$file" '.tokens.id_token')"
[[ -n "$id_token" ]] || return 1
payload="${id_token#*.}"
[[ "$payload" != "$id_token" ]] || return 1
payload="${payload%%.*}"
payload="$(base64url_decode "$payload")"
[[ -n "$payload" ]] || return 1
jq -c . <<< "$payload" 2>/dev/null
}
profile_claim() {
local file="$1"
local query="$2"
local payload
payload="$(profile_payload_json "$file" || true)"
[[ -n "$payload" ]] || return 0
jq -r "$query // empty" <<< "$payload" 2>/dev/null || true
}
profile_auth_mode() { json_get "$1" '.auth_mode'; }
profile_account_id() { json_get "$1" '.tokens.account_id'; }
profile_email() { profile_claim "$1" '.email'; }
profile_name() { profile_claim "$1" '.name'; }
profile_subject() { profile_claim "$1" '.sub'; }
profile_identity() {
local file="$1"
local name email account_id
local result=""
name="$(profile_name "$file")"
email="$(profile_email "$file")"
account_id="$(profile_account_id "$file")"
if [[ -n "$name" ]]; then
result="$name"
fi
if [[ -n "$email" ]]; then
if [[ -n "$result" ]]; then
result="$result <$email>"
else
result="<$email>"
fi
fi
if [[ -z "$result" ]]; then
if [[ -n "$account_id" ]]; then
result="id:$account_id"
else
result="unidentified"
fi
fi
printf '%s' "$result"
}
profile_fingerprint() {
local file="$1"
local auth_mode account_id subject email
auth_mode="$(profile_auth_mode "$file")"
auth_mode="${auth_mode:-unknown}"
account_id="$(profile_account_id "$file")"
if [[ -n "$account_id" ]]; then
printf '%s|acc:%s' "$auth_mode" "$account_id"
return
fi
subject="$(profile_subject "$file")"
if [[ -n "$subject" ]]; then
printf '%s|sub:%s' "$auth_mode" "$subject"
return
fi
email="$(profile_email "$file")"
if [[ -n "$email" ]]; then
printf '%s|email:%s' "$auth_mode" "$email"
return
fi
printf '%s|sha:%s' "$auth_mode" "$(file_sha256 "$file")"
}
get_access_token() {
local file="$1"
local token
token="$(json_get "$file" '.tokens.access_token')"
[[ -z "$token" ]] && token="$(json_get "$file" '.access_token')"
[[ -z "$token" ]] && token="$(json_get "$file" '.accessToken')"
printf '%s' "$token"
}
# ── Token expiration ──
profile_token_exp() {
local file="$1"
local payload
payload="$(profile_payload_json "$file" || true)"
[[ -n "$payload" ]] || return 0
jq -r '.exp // empty' <<< "$payload" 2>/dev/null || true
}
# Returns: ok | expired | expiring:<minutes>
get_token_status() {
local file="$1"
local exp now diff
exp="$(profile_token_exp "$file")"
if [[ -z "$exp" || "$exp" == "null" ]]; then
printf 'unknown'
return
fi
now="$(date +%s)"
diff=$((exp - now))
if (( diff <= 0 )); then
printf 'expired'
elif (( diff < 3600 )); then
printf 'expiring:%d' "$((diff / 60))"
else
printf 'ok'
fi
}
# Human-readable token status for one-line display
format_token_status() {
local file="$1"
local status exp now diff
status="$(get_token_status "$file")"
case "$status" in
ok) printf 'ok' ;;
expired)
exp="$(profile_token_exp "$file")"
now="$(date +%s)"
diff=$((now - exp))
if (( diff < 3600 )); then
printf 'expired %dm ago' "$((diff / 60))"
elif (( diff < 86400 )); then
printf 'expired %dh ago' "$((diff / 3600))"
else
printf 'expired %dd ago' "$((diff / 86400))"
fi
;;
expiring:*)
local mins="${status#expiring:}"
printf 'exp in %dm' "$mins"
;;
unknown) printf '' ;;
esac
}
# ═══════════════════════════════════════════
# Date mode
# ═══════════════════════════════════════════
get_date_mode() {
# Check env var first (optimization for subshells)
if [[ -n "${DATE_MODE:-}" ]]; then
printf '%s' "$DATE_MODE"
return
fi
if [[ -f "$DATE_MODE_FILE" ]]; then
local mode
mode="$(tr -d '\r' < "$DATE_MODE_FILE")"
if [[ "$mode" == "absolute" || "$mode" == "relative" ]]; then
printf '%s' "$mode"
else
printf 'relative'
fi
else
printf 'relative'
fi
}
set_date_mode() {
local mode="$1"
if [[ "$mode" != "relative" && "$mode" != "absolute" ]]; then
echo -e "${RED}date-mode must be 'relative' or 'absolute'${NC}" >&2
exit 1
fi
printf '%s\n' "$mode" > "$DATE_MODE_FILE"
chmod 600 "$DATE_MODE_FILE"
echo "Date mode set to: $mode"
}
format_reset_time_relative() {
local reset_at="$1"
local now diff days hours mins
now="$(date +%s)"
diff=$((reset_at - now))
if (( diff <= 0 )); then
printf 'now'
return
fi
days=$((diff / 86400))
hours=$(((diff % 86400) / 3600))
mins=$(((diff % 3600) / 60))
if (( days > 0 )); then
if (( hours > 0 )); then
printf '%dd %dh' "$days" "$hours"
else
printf '%dd' "$days"
fi
return
fi
if (( hours > 0 )); then
if (( mins > 0 )); then
printf '%dh %dm' "$hours" "$mins"
else
printf '%dh' "$hours"
fi
return
fi
if (( mins > 0 )); then
printf '%dm' "$mins"
else
printf '<1m'
fi
}
format_reset_time_absolute() {
local reset_at="$1"
date -d "@$reset_at" '+%m-%d %H:%M' 2>/dev/null || date -r "$reset_at" '+%m-%d %H:%M' 2>/dev/null || printf '%s' "$reset_at"
}
format_reset_time() {
local reset_at="$1"
local mode="${2:-}"
[[ -z "$mode" ]] && mode="$(get_date_mode)"
if [[ "$mode" == "absolute" ]]; then
format_reset_time_absolute "$reset_at"
else
format_reset_time_relative "$reset_at"
fi
}
# ═══════════════════════════════════════════
# Proxy
# ═══════════════════════════════════════════
get_default_proxy() {
[[ -f "$PROXY_FILE" ]] || return 0
tr -d '\r' < "$PROXY_FILE"
}
validate_proxy_url() {
local proxy="$1"
if [[ ! "$proxy" =~ ^https?://[^[:space:]]+$ ]]; then
echo -e "${RED}Invalid proxy URL. Example: http://127.0.0.1:8888${NC}" >&2
exit 1
fi
}
# ═══════════════════════════════════════════
# Cache
# ═══════════════════════════════════════════
get_cache_time() {
if [[ -f "$CACHE_TIME_FILE" ]]; then
tr -d '\r' < "$CACHE_TIME_FILE"
else
printf '15'
fi
}
set_cache_time() {
local value="$1"
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
echo -e "${RED}cache-time must be a non-negative integer${NC}" >&2
exit 1
fi
printf '%s\n' "$value" > "$CACHE_TIME_FILE"
chmod 600 "$CACHE_TIME_FILE"
}
file_mtime() {
local file="$1"
local value
value="$(stat -c %Y "$file" 2>/dev/null || true)"
if [[ "$value" =~ ^[0-9]+$ ]]; then
printf '%s' "$value"
return 0
fi
value="$(stat -f %m "$file" 2>/dev/null || true)"
if [[ "$value" =~ ^[0-9]+$ ]]; then
printf '%s' "$value"
return 0
fi
return 1
}
cache_key_for_usage() {
local token="$1"
local account_id="$2"
sha256_string "${token}|${account_id}|$(get_default_proxy || true)"
}
cache_file_for_usage() {
local token="$1"
local account_id="$2"
printf '%s/%s.json\n' "$CACHE_DIR" "$(cache_key_for_usage "$token" "$account_id")"
}
read_cached_usage_if_fresh() {
local token="$1"
local account_id="$2"
local ttl cache_file now mtime age
ttl="$(get_cache_time)"
[[ "$ttl" == "0" ]] && return 1
cache_file="$(cache_file_for_usage "$token" "$account_id")"
[[ -f "$cache_file" ]] || return 1
now="$(date +%s)"
mtime="$(file_mtime "$cache_file" || true)"
[[ -n "$mtime" ]] || return 1
[[ "$mtime" =~ ^[0-9]+$ ]] || return 1
age=$((now - mtime))
(( age <= ttl )) || return 1
cat "$cache_file"
}
write_cached_usage() {
local token="$1"
local account_id="$2"
local json="$3"
local ttl cache_file tmp
ttl="$(get_cache_time)"
[[ "$ttl" == "0" ]] && return 0
cache_file="$(cache_file_for_usage "$token" "$account_id")"
tmp="$(mktemp "$cache_file.tmp.XXXXXX")"
printf '%s' "$json" > "$tmp"
chmod 600 "$tmp"
mv "$tmp" "$cache_file"
}
cmd_cache_clear() {
ensure_dirs
if [[ -d "$CACHE_DIR" ]]; then
local count
count="$(find "$CACHE_DIR" -name '*.json' 2>/dev/null | wc -l)"
rm -rf "${CACHE_DIR:?}"/*
echo "Cleared $count cached usage file(s)."
else
echo "No cache directory found."
fi
}
# ═══════════════════════════════════════════
# Fetch usage
# ═══════════════════════════════════════════
fetch_usage_uncached() {
local token="$1"
local account_id="$2"
local proxy
local curl_args
local headers
proxy="$(get_default_proxy || true)"
curl_args=(-s --max-time 12)
[[ -n "$proxy" ]] && curl_args+=(--proxy "$proxy")
headers=(
-H "Authorization: Bearer $token"
-H "User-Agent: CodexCLI"
-H "Accept: application/json"
)
[[ -n "$account_id" ]] && headers+=(-H "ChatGPT-Account-Id: $account_id")
curl "${curl_args[@]}" "https://chatgpt.com/backend-api/wham/usage" "${headers[@]}" 2>/dev/null || true
}
fetch_usage() {
local token="$1"
local account_id="$2"
local cached json
cached="$(read_cached_usage_if_fresh "$token" "$account_id" || true)"
if [[ -n "$cached" ]]; then
printf '%s' "$cached"
return
fi
json="$(fetch_usage_uncached "$token" "$account_id")"
[[ -n "$json" ]] && write_cached_usage "$token" "$account_id" "$json"
printf '%s' "$json"
}
# ═══════════════════════════════════════════
# Error shortening
# ═══════════════════════════════════════════
shorten_error() {
local msg="$1"
if echo "$msg" | grep -qi "expired"; then
printf 'expired'
elif echo "$msg" | grep -qi "invalid.*token\|token.*invalid"; then
printf 'bad token'
elif echo "$msg" | grep -qi "unauthorized\|not authorized"; then
printf 'unauthorized'
elif echo "$msg" | grep -qi "forbidden"; then
printf 'forbidden'
elif echo "$msg" | grep -qi "rate.limit\|too many"; then
printf 'rate limited'
elif echo "$msg" | grep -qi "network\|timeout\|connection"; then
printf 'network err'
elif (( ${#msg} > 12 )); then
printf '%.9s...' "$msg"
else
printf '%s' "$msg"
fi
}
# ═══════════════════════════════════════════
# Format compact record for streaming
# ═══════════════════════════════════════════
# format_usage_compact_record json [date_mode]
# Output: TAB-separated p_label p_left p_reset w_left w_reset
format_usage_compact_record() {
local json="$1"
local mode="${2:-relative}"
local p_label p_left p_reset w_left w_reset err_msg
if echo "$json" | jq -e '.error' >/dev/null 2>&1; then
err_msg="$(echo "$json" | jq -r '.error.message // .error // "Error"')"
local short_err
short_err="$(shorten_error "$err_msg")"
printf 'ERR\t%s\t--\t--\t--\n' "$short_err"
return
fi
p_label="$(echo "$json" | jq -r '.rate_limit.primary_window.limit_window_seconds // empty' | awk '{ if ($1 == "") print "--"; else printf "%dh", $1/3600 }')"
p_left="$(echo "$json" | jq -r '.rate_limit.primary_window.used_percent // empty' | awk '{ if ($1 == "") print "--"; else printf "%d%%", 100-int($1) }')"
local p_reset_at
p_reset_at="$(echo "$json" | jq -r '.rate_limit.primary_window.reset_at // empty')"
if [[ -n "$p_reset_at" && "$p_reset_at" != "null" && "$p_reset_at" != "" ]]; then
p_reset="$(format_reset_time "$p_reset_at" "$mode")"
else
p_reset="--"
fi
w_left="$(echo "$json" | jq -r '.rate_limit.secondary_window.used_percent // empty' | awk '{ if ($1 == "") print "--"; else printf "%d%%", 100-int($1) }')"
local w_reset_at
w_reset_at="$(echo "$json" | jq -r '.rate_limit.secondary_window.reset_at // empty')"
if [[ -n "$w_reset_at" && "$w_reset_at" != "null" && "$w_reset_at" != "" ]]; then
w_reset="$(format_reset_time "$w_reset_at" "$mode")"
else
w_reset="--"
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$p_label" "$p_left" "$p_reset" "$w_left" "$w_reset"
}
# ═══════════════════════════════════════════
# Color helper for left% values
# ═══════════════════════════════════════════
# Prints a left%-value padded to width, with color if critical.
# Usage: print_left_col "0%" 4 or print_left_col "99%" 4
print_left_col() {
local val="$1"
local width="$2"
local num="${val%\%}"
num="${num%.*}"
if [[ "$val" == "--" || "$val" == "expired" || -z "$num" || ! "$num" =~ ^[0-9]+$ ]]; then
printf "%-${width}s" "$val"
return
fi
if (( num == 0 )); then
printf "%s%-${width}s%s" "$RED" "$val" "$NC"
elif (( num < 20 )); then
printf "%s%-${width}s%s" "$YELLOW" "$val" "$NC"
else
printf "%-${width}s" "$val"
fi
}
# ═══════════════════════════════════════════
# Auto-update current profile if auth changed
# ═══════════════════════════════════════════
auto_update_current_profile() {
[[ -f "$AUTH_FILE" ]] || return 0
local current_fp saved_file saved_hash current_hash matches line match_name
current_fp="$(profile_fingerprint "$AUTH_FILE")"
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(profile_names_for_fingerprint "$current_fp")
if (( ${#matches[@]} == 1 )); then
match_name="${matches[0]}"
saved_file="$(profile_path "$match_name")"
current_hash="$(file_sha256 "$AUTH_FILE")"
saved_hash="$(file_sha256 "$saved_file")"
if [[ "$current_hash" != "$saved_hash" ]]; then
save_profile "$match_name"
fi
write_current_hint "$match_name"
fi
}
# ═══════════════════════════════════════════
# Detailed usage (codex-auth usage)
# ═══════════════════════════════════════════
cmd_usage() {
require_current_auth
local token account_id json proxy cache_ttl
local email plan_type
local rl_allowed rl_reached
local p_used p_left p_window p_reset
local w_used w_left w_window w_reset
local cr_used cr_left cr_window cr_reset
local has_credits unlimited balance
token="$(get_access_token "$AUTH_FILE")"
if [[ -z "$token" ]]; then
echo -e "${RED}access_token not found in $AUTH_FILE${NC}" >&2
exit 1
fi
account_id="$(profile_account_id "$AUTH_FILE")"
proxy="$(get_default_proxy || true)"
cache_ttl="$(get_cache_time)"
json="$(fetch_usage "$token" "$account_id")"
if [[ -z "$json" ]]; then
echo -e "${RED}Empty response from API${NC}" >&2
exit 1
fi
if echo "$json" | jq -e '.error' >/dev/null 2>&1; then
echo -e "${RED}API error:${NC} $(echo "$json" | jq -r '.error.message // .error // "unknown error"')"
exit 1
fi
email="$(echo "$json" | jq -r '.email // "-"')"
plan_type="$(echo "$json" | jq -r '.plan_type // "-"')"
rl_allowed="$(echo "$json" | jq -r '.rate_limit.allowed // "-"')"
rl_reached="$(echo "$json" | jq -r '.rate_limit.limit_reached // "-"')"
p_used="$(echo "$json" | jq -r '.rate_limit.primary_window.used_percent // empty')"
if [[ -n "$p_used" ]]; then
p_left="$((100 - ${p_used%.*}))%"
else
p_left="--"
fi
p_window="$(echo "$json" | jq -r '.rate_limit.primary_window.limit_window_seconds // empty' | awk '{ if ($1 == "") print "--"; else printf "%dh", $1/3600 }')"
local p_reset_at
p_reset_at="$(echo "$json" | jq -r '.rate_limit.primary_window.reset_at // empty')"
if [[ -n "$p_reset_at" && "$p_reset_at" != "null" ]]; then
p_reset="$(format_reset_time "$p_reset_at")"
else
p_reset="--"
fi
w_used="$(echo "$json" | jq -r '.rate_limit.secondary_window.used_percent // empty')"
if [[ -n "$w_used" && "$w_used" != "null" ]]; then
w_left="$((100 - ${w_used%.*}))%"
else
w_left="--"
fi
w_window="week"
local w_reset_at
w_reset_at="$(echo "$json" | jq -r '.rate_limit.secondary_window.reset_at // empty')"
if [[ -n "$w_reset_at" && "$w_reset_at" != "null" ]]; then
w_reset="$(format_reset_time "$w_reset_at")"
else
w_reset="--"
fi
cr_used="$(echo "$json" | jq -r '.code_review_rate_limit.primary_window.used_percent // empty')"
if [[ -n "$cr_used" && "$cr_used" != "null" ]]; then
cr_left="$((100 - ${cr_used%.*}))%"
else
cr_left="--"
fi
cr_window="$(echo "$json" | jq -r '.code_review_rate_limit.primary_window.limit_window_seconds // empty' | awk '{ if ($1 == "") print "--"; else if ($1 >= 604800) print "week"; else printf "%dh", $1/3600 }')"
local cr_reset_at
cr_reset_at="$(echo "$json" | jq -r '.code_review_rate_limit.primary_window.reset_at // empty')"
if [[ -n "$cr_reset_at" && "$cr_reset_at" != "null" ]]; then
cr_reset="$(format_reset_time "$cr_reset_at")"
else
cr_reset="--"
fi
has_credits="$(echo "$json" | jq -r '.credits.has_credits // false')"
unlimited="$(echo "$json" | jq -r '.credits.unlimited // false')"
balance="$(echo "$json" | jq -r '.credits.balance // "0"')"
local tok_status
tok_status="$(format_token_status "$AUTH_FILE")"
[[ -n "$proxy" ]] && echo "Proxy: $proxy"
echo "Cache TTL: ${cache_ttl}s"
echo "Date mode: $(get_date_mode)"
echo "Email: $email"
echo "Plan: $plan_type"
echo "Token: $tok_status"
echo
echo "Main rate limit:"
echo " Allowed: $rl_allowed"
echo " Reached: $rl_reached"
echo " [p] $p_window: left $p_left reset in $p_reset"
echo " [w] week: left $w_left reset in $w_reset"
echo
echo "Code review limit:"
echo " [p] $cr_window: left $cr_left reset in $cr_reset"
echo
echo "Credits:"
echo " Has credits: $has_credits"
echo " Unlimited: $unlimited"
echo " Balance: $balance"
}
# ═══════════════════════════════════════════
# Raw API response (bypasses cache)
# ═══════════════════════════════════════════
cmd_raw() {
require_current_auth
local token account_id json
token="$(get_access_token "$AUTH_FILE")"
if [[ -z "$token" ]]; then
echo -e "${RED}access_token not found in $AUTH_FILE${NC}" >&2
exit 1
fi
account_id="$(profile_account_id "$AUTH_FILE")"
# Direct API call, bypassing cache
json="$(fetch_usage_uncached "$token" "$account_id")"
if [[ -z "$json" ]]; then
echo -e "${RED}Empty response from API${NC}" >&2
exit 1
fi
echo "$json" | jq . 2>/dev/null || echo "$json"
}
# ═══════════════════════════════════════════
# Quick one-line check (codex-auth check)
# ═══════════════════════════════════════════
cmd_check() {
require_current_auth
local token account_id json name_line
local p_left p_reset w_left w_reset err_msg
token="$(get_access_token "$AUTH_FILE")"
if [[ -z "$token" ]]; then
echo "ERR: no access token"
return 1
fi
# Profile name
local cur_name
cur_name="$(cat "$CURRENT_FILE" 2>/dev/null || true)"
if [[ -z "$cur_name" ]]; then
cur_name="$(profile_identity "$AUTH_FILE")"
fi
account_id="$(profile_account_id "$AUTH_FILE")"
json="$(fetch_usage "$token" "$account_id")"
if [[ -z "$json" ]]; then
echo "$cur_name: ERR empty response"
return 1
fi
# Token status
local tok
tok="$(format_token_status "$AUTH_FILE")"
if echo "$json" | jq -e '.error' >/dev/null 2>&1; then
err_msg="$(echo "$json" | jq -r '.error.message // .error // "Error"')"
local short_err
short_err="$(shorten_error "$err_msg")"
printf '%s: ERR %s' "$cur_name" "$short_err"
[[ -n "$tok" && "$tok" != "ok" ]] && printf ' [token: %s]' "$tok"
printf '\n'
return 1
fi
p_left="$(echo "$json" | jq -r '.rate_limit.primary_window.used_percent // empty' | awk '{ if ($1 == "") print "--"; else printf "%d%%", 100-int($1) }')"
local p_reset_at
p_reset_at="$(echo "$json" | jq -r '.rate_limit.primary_window.reset_at // empty')"
if [[ -n "$p_reset_at" && "$p_reset_at" != "null" && "$p_reset_at" != "" ]]; then
p_reset="$(format_reset_time "$p_reset_at")"
else
p_reset="--"
fi
w_left="$(echo "$json" | jq -r '.rate_limit.secondary_window.used_percent // empty' | awk '{ if ($1 == "") print "--"; else printf "%d%%", 100-int($1) }')"
local w_reset_at
w_reset_at="$(echo "$json" | jq -r '.rate_limit.secondary_window.reset_at // empty')"
if [[ -n "$w_reset_at" && "$w_reset_at" != "null" && "$w_reset_at" != "" ]]; then
w_reset="$(format_reset_time "$w_reset_at")"
else
w_reset="--"
fi
printf '%s: p %s (r:%s) | wk %s (r:%s)' "$cur_name" "$p_left" "$p_reset" "$w_left" "$w_reset"
[[ -n "$tok" && "$tok" != "ok" ]] && printf ' [token: %s]' "$tok"
printf '\n'
}
# ═══════════════════════════════════════════
# Show all settings (codex-auth config)
# ═══════════════════════════════════════════
cmd_config() {
ensure_dirs
local cur_name proxy cache_ttl date_mode tok_status
cur_name="$(cat "$CURRENT_FILE" 2>/dev/null || echo '(none)')"
proxy="$(get_default_proxy || echo '(none)')"
cache_ttl="$(get_cache_time)"
date_mode="$(get_date_mode)"
echo "Current profile: $cur_name"
echo "Proxy: $proxy"
echo "Cache TTL: ${cache_ttl}s"
echo "Date mode: $date_mode"
echo "Profiles dir: $PROFILES_DIR"
echo "Auth file: $AUTH_FILE"
echo "OpenCode auth: $OPENCODE_AUTH_FILE"
echo "Cache dir: $CACHE_DIR"
if [[ -f "$AUTH_FILE" ]]; then
tok_status="$(format_token_status "$AUTH_FILE")"
echo "Token status: $tok_status"
fi
}
# ═══════════════════════════════════════════
# Profile helpers
# ═══════════════════════════════════════════
profile_names_for_fingerprint() {
local fp="$1"
local f
for f in "$PROFILES_DIR"/*.json; do
[[ -f "$f" ]] || continue
if [[ "$(profile_fingerprint "$f")" == "$fp" ]]; then
basename "$f" .json
fi
done | sort
}
current_saved_profile_names() {
require_current_auth
profile_names_for_fingerprint "$(profile_fingerprint "$AUTH_FILE")"
}
sync_current_hint_from_real_auth() {
local matches line
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(current_saved_profile_names)
if (( ${#matches[@]} == 1 )); then
write_current_hint "${matches[0]}"
else
clear_current_hint
fi
}
save_profile() {
safe_install_file "$AUTH_FILE" "$(profile_path "$1")"
}
join_by() {
local IFS="$1"
shift
echo "$*"
}
# ═══════════════════════════════════════════
# Handle current profile before switch
# ═══════════════════════════════════════════
handle_current_before_switch() {
local target_name="$1"
local auto_save="$2"
local target current_fp target_fp
local curr_hash targ_hash
local matches line
[[ -f "$AUTH_FILE" ]] || return 0
target="$(profile_path "$target_name")"
current_fp="$(profile_fingerprint "$AUTH_FILE")"
target_fp="$(profile_fingerprint "$target")"
if [[ "$current_fp" == "$target_fp" ]]; then
curr_hash="$(file_sha256 "$AUTH_FILE")"
targ_hash="$(file_sha256 "$target")"
if [[ "$curr_hash" != "$targ_hash" ]]; then
save_profile "$target_name"
echo "Refreshed saved copy: $target_name"
else
echo "Already active: $target_name"
fi
write_current_hint "$target_name"
return 10
fi
if [[ "$auto_save" == "yes" ]]; then
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(profile_names_for_fingerprint "$current_fp")
if (( ${#matches[@]} == 1 )); then
local saved_file saved_hash
saved_file="$(profile_path "${matches[0]}")"
saved_hash="$(file_sha256 "$saved_file")"
curr_hash="$(file_sha256 "$AUTH_FILE")"
if [[ "$curr_hash" != "$saved_hash" ]]; then
save_profile "${matches[0]}"
echo "Auto-saved current (auth changed): ${matches[0]}"
else
echo "Auto-saved current: ${matches[0]}"
fi
fi
fi
return 0
}
# ═══════════════════════════════════════════
# Profile management commands
# ═══════════════════════════════════════════
cmd_save() {
local name="${1:-}"
local dst current_fp existing_fp
local matches line
[[ -n "$name" ]] || { usage; exit 1; }
validate_name "$name"
ensure_dirs
require_current_auth
dst="$(profile_path "$name")"
current_fp="$(profile_fingerprint "$AUTH_FILE")"
if [[ -f "$dst" ]]; then
existing_fp="$(profile_fingerprint "$dst")"
if [[ "$existing_fp" == "$current_fp" ]]; then
echo "Profile already exists for current account: $name"
return 0
fi
echo -e "${RED}Profile name '$name' already exists for different account${NC}" >&2
exit 1
fi
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(profile_names_for_fingerprint "$current_fp")
if (( ${#matches[@]} > 0 )); then
echo -e "${YELLOW}Current account already saved as: ${matches[*]}${NC}" >&2
exit 1
fi
save_profile "$name"
write_current_hint "$name"
echo -e "${GREEN}Saved profile: $name${NC}"
}
cmd_use_internal() {
local name="$1"
local auto_save="${2:-yes}"
local target
local ret=0
validate_name "$name"
ensure_dirs
target="$(profile_path "$name")"
if [[ ! -f "$target" ]]; then
echo -e "${RED}Profile not found: $name${NC}" >&2
exit 1
fi
if [[ -f "$AUTH_FILE" ]]; then
handle_current_before_switch "$name" "$auto_save" || ret=$?
if [[ $ret -eq 10 ]]; then
sync_opencode_auth_if_present "$AUTH_FILE"
return 0
fi
fi
safe_install_file "$target" "$AUTH_FILE"
sync_opencode_auth_if_present "$target"
write_current_hint "$name"
echo -e "${GREEN}Activated profile: $name${NC}"
echo "Hint: Reload VS Code window if needed."
}
cmd_use() { cmd_use_internal "${1:-}" "yes"; }
cmd_force_set() { cmd_use_internal "${1:-}" "no"; }
cmd_current() {
ensure_dirs
require_current_auth
local matches line
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(current_saved_profile_names)
sync_current_hint_from_real_auth
if (( ${#matches[@]} == 1 )); then
echo "${matches[0]}"
elif (( ${#matches[@]} > 1 )); then
echo "Ambiguous: $(join_by ', ' "${matches[@]}")"
else
echo "Current auth is not saved."
fi
}
cmd_show() {
ensure_dirs
require_current_auth
local matches line
matches=()
while IFS= read -r line; do
matches+=("$line")
done < <(current_saved_profile_names)
sync_current_hint_from_real_auth
if (( ${#matches[@]} == 0 )); then
echo "Current auth does not match any saved profile."
else
echo "Matches: $(join_by ', ' "${matches[@]}")"
fi
}
cmd_backup() {
ensure_dirs
require_current_auth
local ts dst
ts="$(date +%Y%m%d_%H%M%S)"
dst="$PROFILES_DIR/backup_$ts.json"
safe_install_file "$AUTH_FILE" "$dst"
echo "Backup saved: $dst"
}
cmd_rm() {
local name=""
local force="no"
while [[ $# -gt 0 ]]; do
case "$1" in
--force|-f) force="yes"; shift ;;
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
*)
if [[ -n "$name" ]]; then
echo "Unexpected argument: $1" >&2; exit 1
fi
name="$1"
shift
;;
esac
done
[[ -n "$name" ]] || { echo "Usage: codex-auth rm <name> [--force]" >&2; exit 1; }
validate_name "$name"
ensure_dirs
local target
target="$(profile_path "$name")"
if [[ ! -f "$target" ]]; then
echo -e "${RED}Profile not found: $name${NC}" >&2
exit 1
fi
# Prevent deleting the currently active profile
if [[ -f "$AUTH_FILE" ]]; then
local current_fp target_fp
current_fp="$(profile_fingerprint "$AUTH_FILE")"
target_fp="$(profile_fingerprint "$target")"
if [[ "$current_fp" == "$target_fp" ]]; then
echo -e "${RED}Cannot delete the currently active profile. Switch to another first.${NC}" >&2
exit 1
fi
fi
# Confirmation
if [[ "$force" != "yes" ]]; then
if [[ ! -t 0 ]]; then
echo -e "${RED}Non-interactive mode. Use --force to delete without confirmation.${NC}" >&2
exit 1
fi
echo -ne "${YELLOW}Delete profile '$name'? Type 'yes' to confirm: ${NC}"
local answer
read -r answer
if [[ "$answer" != "yes" ]]; then
echo "Cancelled."
return 0
fi
fi
rm -f "$target"
# Clear current hint if it pointed to the deleted profile
if [[ -f "$CURRENT_FILE" ]]; then
local hint
hint="$(tr -d '\r' < "$CURRENT_FILE")"
if [[ "$hint" == "$name" ]]; then
clear_current_hint
fi
fi
echo -e "${GREEN}Deleted profile: $name${NC}"
}
cmd_rename() {
local old_name="${1:-}"
local new_name="${2:-}"
[[ -n "$old_name" && -n "$new_name" ]] || {
echo "Usage: codex-auth rename <old> <new>" >&2
exit 1
}
validate_name "$old_name"
validate_name "$new_name"
ensure_dirs
local old_path new_path
old_path="$(profile_path "$old_name")"
new_path="$(profile_path "$new_name")"
if [[ ! -f "$old_path" ]]; then
echo -e "${RED}Profile not found: $old_name${NC}" >&2
exit 1
fi
if [[ -f "$new_path" ]]; then
echo -e "${RED}Profile '$new_name' already exists${NC}" >&2
exit 1
fi
if [[ "$old_name" == "$new_name" ]]; then
echo "Same name, nothing to do."
return 0
fi
mv "$old_path" "$new_path"
# Update current hint if it pointed to the old name
if [[ -f "$CURRENT_FILE" ]]; then
local hint
hint="$(tr -d '\r' < "$CURRENT_FILE")"
if [[ "$hint" == "$old_name" ]]; then
write_current_hint "$new_name"
fi
fi
echo -e "${GREEN}Renamed: $old_name -> $new_name${NC}"
}
# ═══════════════════════════════════════════
# Proxy commands
# ═══════════════════════════════════════════
cmd_proxy_set() {
local proxy="${1:-}"
[[ -n "$proxy" ]] || {
echo "Usage: codex-auth proxy set <url>" >&2
exit 1
}
ensure_dirs
validate_proxy_url "$proxy"
printf '%s\n' "$proxy" > "$PROXY_FILE"
chmod 600 "$PROXY_FILE"
echo "Default proxy saved: $proxy"
}
cmd_proxy_show() {
ensure_dirs
local proxy
proxy="$(get_default_proxy || true)"
if [[ -n "$proxy" ]]; then
echo "$proxy"
else
echo "No default proxy set."
fi
}
cmd_proxy_unset() {
ensure_dirs
rm -f "$PROXY_FILE"
echo "Default proxy removed."
}
cmd_proxy() {
local sub="${1:-}"
shift || true
case "$sub" in
set) cmd_proxy_set "${1:-}" ;;
show) cmd_proxy_show ;;
unset|clear|rm) cmd_proxy_unset ;;
*)
echo "Usage: codex-auth proxy {set <url>|show|unset}" >&2
exit 1
;;
esac
}
# ═══════════════════════════════════════════
# Settings commands
# ═══════════════════════════════════════════
cmd_cache_time() {
ensure_dirs
local value="${1:-}"
if [[ -z "$value" ]]; then
echo "$(get_cache_time)"
return 0
fi
set_cache_time "$value"
echo "Cache TTL set to ${value}s"
}
cmd_date_mode() {
ensure_dirs
local mode="${1:-}"
if [[ -z "$mode" ]]; then
echo "$(get_date_mode)"
return 0
fi
set_date_mode "$mode"
}
# ═══════════════════════════════════════════
# Streaming list with async (max 3 parallel)
# ASCII table, color-coded limits, progress indicator
# ═══════════════════════════════════════════
# Print one profile row. All visual formatting is here.
# Args: name id is_current p_label p_left p_reset w_left w_reset token_status
print_profile_row() {
local name="$1"
local id="$2"
local is_current="$3"
local p_label="$4"
local p_left="$5"
local p_reset="$6"
local w_left="$7"
local w_reset="$8"
local token_status="$9"
# Fixed column widths (visible chars, no ANSI codes in width-controlled fields)
local CW_WIND=4 # window label
local CW_LEFT=4 # left%
local CW_RESET=7 # reset time
local CW_NAME=12 # profile name
# Token indicator (1 char)
local tok_ind=" "
local tok_color=""
case "$token_status" in
expired) tok_ind="X"; tok_color="$RED" ;;
expiring:*) tok_ind="!"; tok_color="$YELLOW" ;;
esac
if [[ "$p_label" == "ERR" ]]; then
# Error row: "ERR <short_error>" spans the first columns
printf "${RED} ERR %-7s %-7s %-4s %-7s${NC} ${tok_color}%s${NC} " \
"$p_left" "--" "--" "--" "$tok_ind"
else
# Normal row
printf " %-${CW_WIND}s " "${p_label}:"
print_left_col "$p_left" "$CW_LEFT"
printf " %-${CW_RESET}s " "$p_reset"
print_left_col "$w_left" "$CW_LEFT"
printf " %-${CW_RESET}s ${tok_color}%s${NC} " "$w_reset" "$tok_ind"
fi
# Profile name and identity — adapt to terminal width
# Fixed columns before identity ≈ 56 chars (rates + token + name padding)
local COLS_BEFORE_ID=56
local avail_for_id=$(( TERMINAL_WIDTH - COLS_BEFORE_ID ))
local display_id=""
if (( avail_for_id >= 8 )); then
display_id="$id"
# Truncate identity if it exceeds available space
if (( ${#id} > avail_for_id )); then
display_id="${id:0:$((avail_for_id - 2))}.."
fi
fi
if [[ "$is_current" == "yes" ]]; then
if [[ -n "$display_id" ]]; then
printf "${GREEN}* %-${CW_NAME}s %s${NC}\n" "$name" "$display_id"
else
printf "${GREEN}* %s${NC}\n" "$name"
fi
else
if [[ -n "$display_id" ]]; then
printf " %-${CW_NAME}s %s\n" "$name" "$display_id"
else
printf " %s\n" "$name"
fi
fi
}
cmd_list_internal() {
local fetch_stats="$1"
local show_hint="${2:-no}"
# Detect terminal width for adaptive layout
TERMINAL_WIDTH="$(get_terminal_width)"
ensure_dirs
shopt -s nullglob
local files=("$PROFILES_DIR"/*.json)
if (( ${#files[@]} == 0 )); then
echo "No saved profiles."
[[ "$show_hint" == "yes" ]] && echo "More commands: codex-auth help"
return 0
fi
# Auto-update current profile auth data if changed
auto_update_current_profile
local current_fp=""
if [[ -f "$AUTH_FILE" ]]; then
current_fp="$(profile_fingerprint "$AUTH_FILE")"
fi
local names=()
local name file fp id
for file in "${files[@]}"; do
name="$(basename "$file" .json)"
names+=("$name")
id="$(profile_identity "$file")"
eval "id_map_$name=\"\$id\""
fp="$(profile_fingerprint "$file")"
eval "fp_map_$name=\"\$fp\""
done
readarray -t names < <(printf '%s\n' "${names[@]}" | sort)
if [[ "$fetch_stats" != "yes" ]]; then
# Fast list without stats
local max_name_len=4
for name in "${names[@]}"; do
eval "id=\"\$id_map_$name\""
(( ${#name} > max_name_len )) && max_name_len=${#name}
done
# Available width for identity in fast mode
local fast_before_id=$(( max_name_len + 4 )) # marker + name + spaces
local avail_for_id=$(( TERMINAL_WIDTH - fast_before_id - 3 )) # -3 for " T" tok indicator
for name in "${names[@]}"; do
eval "fp=\"\$fp_map_$name\""
eval "id=\"\$id_map_$name\""
# Show token indicator even in fast mode
local tok_ind=" "
local tok_color=""
local ts
ts="$(get_token_status "$(profile_path "$name")")"
case "$ts" in
expired) tok_ind="X"; tok_color="$RED" ;;
expiring:*) tok_ind="!"; tok_color="$YELLOW" ;;
esac
# Build identity part (truncated or hidden)
local display_id=""
if (( avail_for_id >= 8 )); then
display_id="$id"
if (( ${#id} > avail_for_id )); then
display_id="${id:0:$((avail_for_id - 2))}.."
fi
fi
if [[ "$fp" == "$current_fp" ]]; then
if [[ -n "$display_id" ]]; then
printf "${GREEN}* %-${max_name_len}s %s${NC} ${tok_color}%s${NC}\n" "$name" "$display_id" "$tok_ind"
else
printf "${GREEN}* %s${NC} ${tok_color}%s${NC}\n" "$name" "$tok_ind"
fi
else
if [[ -n "$display_id" ]]; then
printf " %-${max_name_len}s %s ${tok_color}%s${NC}\n" "$name" "$display_id" "$tok_ind"
else
printf " %s ${tok_color}%s${NC}\n" "$name" "$tok_ind"
fi
fi
done
return 0
fi
# Streaming list with stats, async max 3 at a time
local tmpdir
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/codex-auth-list.XXXXXX")"
local total=${#names[@]}
local launched=0
local finished=0
local max_parallel=3
local use_progress=0
# Optimize: read date_mode once, export for subshells
export DATE_MODE="$(get_date_mode)"
# Print header — adapt to terminal width
local COLS_BEFORE_ID=56
local avail_for_id=$(( TERMINAL_WIDTH - COLS_BEFORE_ID ))
if (( avail_for_id >= 8 )); then
local id_header="identity"
if (( avail_for_id < ${#id_header} )); then
id_header="${id_header:0:$((avail_for_id - 2))}.."
fi
printf "${DIM} wind left reset wk left reset T profile %s${NC}\n" "$id_header"
else
printf "${DIM} wind left reset wk left reset T profile${NC}\n"
fi
printf "${DIM} T = token: X expired, ! expiring soon${NC}\n"
# Show progress indicator only if terminal
if [[ -t 1 ]]; then
use_progress=1
printf "Loading 0/%d..." "$total"
fi
while (( finished < total )); do
# Launch background jobs up to max_parallel
while (( launched < total && launched - finished < max_parallel )); do
name="${names[$launched]}"
file="$(profile_path "$name")"
(
# Disable set -e inside subshell so curl failures don't prevent .done marker
set +e
token="$(get_access_token "$file")"
if [[ -n "$token" ]]; then
json="$(fetch_usage "$token" "$(profile_account_id "$file")")"
printf '%s' "$json" > "$tmpdir/$name.json"
else
printf '' > "$tmpdir/$name.nodata"
fi
printf 'done' > "$tmpdir/$name.done"
) 2>/dev/null &
(( ++launched ))
done
# Poll for any finished job and print it immediately
local found=0
for name in "${names[@]}"; do
[[ -f "$tmpdir/$name.done" && ! -f "$tmpdir/$name.printed" ]] || continue
# Clear progress indicator
if (( use_progress )); then
printf "\r\033[K"
fi
# This one is ready — process and print
eval "fp=\"\$fp_map_$name\""
eval "id=\"\$id_map_$name\""
local is_current="no"
[[ "$fp" == "$current_fp" ]] && is_current="yes"
local p_label="--" p_left="--" p_reset="--" w_left="--" w_reset="--"
local token_status=""
if [[ -f "$tmpdir/$name.json" ]]; then
local json
json="$(cat "$tmpdir/$name.json")"
if [[ -n "$json" ]]; then
IFS=$'\t' read -r p_label p_left p_reset w_left w_reset <<EOF
$(format_usage_compact_record "$json" "$DATE_MODE")
EOF
fi
fi
token_status="$(get_token_status "$(profile_path "$name")")"
print_profile_row "$name" "$id" "$is_current" \
"$p_label" "$p_left" "$p_reset" "$w_left" "$w_reset" "$token_status"
touch "$tmpdir/$name.printed"
(( ++finished ))
(( ++found ))
# Show updated progress if more to come
if (( finished < total && use_progress )); then
printf "Loading %d/%d..." "$finished" "$total"
fi
done
# Small sleep if nothing was ready yet
if (( found == 0 )); then
sleep 0.1
fi
done
# Cleanup
rm -rf "$tmpdir"
wait 2>/dev/null || true
# Unset exported var
unset DATE_MODE
if [[ "$show_hint" == "yes" ]]; then
echo
echo "More commands: codex-auth help"
fi
}
cmd_list() {
cmd_list_internal "yes" "no"
}
cmd_list_default() {
cmd_list_internal "yes" "yes"
}
cmd_list_fast() {
cmd_list_internal "no" "no"
}
# ═══════════════════════════════════════════
# Shell completion
# ═══════════════════════════════════════════
cmd_completion() {
local shell="${1:-}"
local _dir="${CODEX_AUTH_PROFILES_DIR:-$HOME/.codex-auth-profiles}"
case "$shell" in
bash)
cat <<'BASH_EOF'
_codex_auth() {
local cur prev opts profiles
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="usage raw check config save use rm rename list list-fast current show backup proxy cache-time cache-clear date-mode force-set completion help"
profiles="$(ls "${CODEX_AUTH_PROFILES_DIR:-$HOME/.codex-auth-profiles}"/*.json 2>/dev/null | while read -r f; do basename "$f" .json; done)"
if (( COMP_CWORD == 1 )); then
COMPREPLY=( $(compgen -W "$opts" -- "$cur") $(compgen -W "$profiles" -- "$cur") )
return 0
fi
case "${COMP_WORDS[1]}" in
save|use|set|switch|force-set)
COMPREPLY=( $(compgen -W "$profiles" -- "$cur") )
;;
rm|remove|delete)
COMPREPLY=( $(compgen -W "$profiles --force -f" -- "$cur") )
;;
rename|mv)
COMPREPLY=( $(compgen -W "$profiles" -- "$cur") )
;;
proxy)
case "$prev" in
proxy) COMPREPLY=( $(compgen -W "set show unset" -- "$cur") ) ;;
esac
;;
date-mode)
COMPREPLY=( $(compgen -W "relative absolute" -- "$cur") )
;;
cache-time)
;;
completion)
COMPREPLY=( $(compgen -W "bash zsh" -- "$cur") )
;;
esac
return 0
}
complete -F _codex_auth codex-auth
BASH_EOF
;;
zsh)
cat <<'ZSH_EOF'
#compdef codex-auth
_codex_auth() {
local -a commands profiles
local _dir="${CODEX_AUTH_PROFILES_DIR:-$HOME/.codex-auth-profiles}"
profiles=(${(f)"$(ls "$_dir"/*.json(N) 2>/dev/null | while read -r f; do basename "${f}" .json; done)"})
commands=(
'usage:Show detailed rate limits for current auth'
'raw:Show raw API response for current auth'
'check:Quick one-line status of current profile'
'config:Show all current settings'
'save:Save current auth.json as a new profile'
'use:Switch to a saved profile'
'rm:Delete a saved profile'
'rename:Rename a saved profile'
'list:List profiles with usage stats'
'list-fast:List profiles without API calls'
'current:Show current profile name'
'show:Show matching profiles'
'backup:Backup current auth.json'
'proxy:Proxy settings'
'cache-time:Cache TTL setting'
'cache-clear:Clear all cached usage data'
'date-mode:Date display mode'
'force-set:Force switch to profile'
'completion:Generate shell completion script'
'help:Show help'
)
if (( CURRENT == 2 )); then
_describe 'command' commands
_describe 'profile' profiles
return
fi
case $words[2] in
save|use|set|switch|force-set)
_describe 'profile' profiles
;;
rm|remove|delete)
_describe 'profile' profiles
_arguments '--force[Skip confirmation]' '-f[Skip confirmation]'
;;
rename|mv)
_describe 'profile' profiles
;;
proxy)
if (( CURRENT == 3 )); then
_values 'subcommand' \
'set[Set proxy URL]' \
'show[Show current proxy]' \
'unset[Remove proxy]'
fi
;;
date-mode)
_values 'mode' \
'relative[Show relative times like 5h, 3d 2h]' \
'absolute[Show absolute local time]'
;;
cache-time)
_message 'cache TTL in seconds (0 disables)'
;;
completion)
_values 'shell' 'bash' 'zsh'
;;
esac
}
_codex_auth "$@"
ZSH_EOF
;;
*)
echo "Usage: codex-auth completion {bash|zsh}" >&2
echo "" >&2
echo "Install:" >&2
echo " bash: source <(codex-auth completion bash)" >&2
echo " zsh: source <(codex-auth completion zsh)" >&2
echo "" >&2
echo "Permanent install:" >&2
echo " bash: codex-auth completion bash > ~/.local/share/bash-completion/completions/codex-auth" >&2
echo " zsh: codex-auth completion zsh > ~/.zfunc/_codex-auth" >&2
exit 1
;;
esac
}
# ═══════════════════════════════════════════
# Main
# ═══════════════════════════════════════════
main() {
require_tools
local cmd="${1:-}"
if [[ -z "$cmd" ]]; then
cmd_list_default
exit 0
fi
shift || true
case "$cmd" in
usage) cmd_usage ;;
raw) cmd_raw ;;
check) cmd_check ;;
config) cmd_config ;;
save) cmd_save "${1:-}" ;;
use|set|switch) cmd_use "${1:-}" ;;
force-set) cmd_force_set "${1:-}" ;;
rm|remove|delete) cmd_rm "$@" ;;
rename|mv) cmd_rename "${1:-}" "${2:-}" ;;
list) cmd_list ;;
list-fast) cmd_list_fast ;;
current) cmd_current ;;
show) cmd_show ;;
backup) cmd_backup ;;
proxy) cmd_proxy "$@" ;;
cache-time) cmd_cache_time "${1:-}" ;;
cache-clear) cmd_cache_clear ;;
date-mode) cmd_date_mode "${1:-}" ;;
completion) cmd_completion "${1:-}" ;;
-h|--help|help) usage ;;
*)
if [[ -f "$(profile_path "$cmd")" ]]; then
cmd_use "$cmd"
else
echo -e "${RED}Unknown command: $cmd${NC}" >&2
usage
exit 1
fi
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment