Last active
June 24, 2026 13:36
-
-
Save megamen32/523e2d6325641df69f2e0aa47ea054df to your computer and use it in GitHub Desktop.
Change profiles of codex (vscode extension or app) easily
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # инстукция 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