|
| 1 | +#!/usr/bin/env bash |
| 2 | +# gh-ratelimit - GitHub API rate-limit monitor for Bash and POSIX-style shells. |
| 3 | +# |
| 4 | +# What it does: |
| 5 | +# Queries GitHub's /rate_limit endpoint through GitHub CLI and renders the |
| 6 | +# current API buckets sorted by pressure so the tightest buckets appear first. |
| 7 | +# |
| 8 | +# Requirements: |
| 9 | +# - gh (GitHub CLI) |
| 10 | +# - jq |
| 11 | +# - an authenticated gh session |
| 12 | +# |
| 13 | +# Setup: |
| 14 | +# macOS: brew install gh jq |
| 15 | +# Ubuntu: sudo apt-get install gh jq |
| 16 | +# Then authenticate once: |
| 17 | +# gh auth login |
| 18 | +# |
| 19 | +# Notes: |
| 20 | +# - This script uses gh for both API access and auth context. |
| 21 | +# - Bash version requires jq for parsing and pretty-printing JSON. |
| 22 | +# - GitHub's /rate_limit endpoint is exempt from rate limiting, so polling it is safe. |
| 23 | +# |
| 24 | +# Usage: |
| 25 | +# gh-ratelimit |
| 26 | +# gh-ratelimit -w |
| 27 | +# gh-ratelimit -w -i 5 |
| 28 | +# gh-ratelimit -j |
| 29 | +# gh-ratelimit -q |
| 30 | +# gh-ratelimit -h |
| 31 | +# |
| 32 | +# Options: |
| 33 | +# -w, --watch Refresh continuously. |
| 34 | +# -i, --interval Watch refresh interval in seconds. Default: 10. |
| 35 | +# -j, --json Print the raw GitHub API response as formatted JSON. |
| 36 | +# -q, --quiet Only show buckets that are not full and have a non-zero limit. |
| 37 | +# -h, --help Show this help text. |
| 38 | +# |
| 39 | +# Examples: |
| 40 | +# gh-ratelimit # one-time snapshot |
| 41 | +# gh-ratelimit -q # only show buckets under pressure |
| 42 | +# gh-ratelimit -w -i 60 # refresh every 60 seconds |
| 43 | +# gh-ratelimit -j | jq . # inspect raw payload |
| 44 | + |
| 45 | +set -euo pipefail |
| 46 | + |
| 47 | +WATCH=0 |
| 48 | +INTERVAL=10 |
| 49 | +JSON=0 |
| 50 | +QUIET=0 |
| 51 | + |
| 52 | +show_usage() { |
| 53 | + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$0" |
| 54 | +} |
| 55 | + |
| 56 | +while [[ $# -gt 0 ]]; do |
| 57 | + case "$1" in |
| 58 | + -w|--watch) WATCH=1 ;; |
| 59 | + -i|--interval) INTERVAL="${2:-10}"; shift ;; |
| 60 | + -j|--json) JSON=1 ;; |
| 61 | + -q|--quiet) QUIET=1 ;; |
| 62 | + -h|--help) |
| 63 | + show_usage |
| 64 | + exit 0 |
| 65 | + ;; |
| 66 | + *) echo "unknown arg: $1" >&2; exit 2 ;; |
| 67 | + esac |
| 68 | + shift |
| 69 | +done |
| 70 | + |
| 71 | +command -v gh >/dev/null || { echo "gh not installed" >&2; exit 1; } |
| 72 | +command -v jq >/dev/null || { echo "jq not installed" >&2; exit 1; } |
| 73 | + |
| 74 | +if ! gh auth status >/dev/null 2>&1; then |
| 75 | + echo "gh is installed but not authenticated. Run: gh auth login" >&2 |
| 76 | + exit 1 |
| 77 | +fi |
| 78 | + |
| 79 | +# ANSI colours (skip if not a TTY) |
| 80 | +if [[ -t 1 ]]; then |
| 81 | + C_RESET=$'\033[0m'; C_DIM=$'\033[2m'; C_BOLD=$'\033[1m' |
| 82 | + C_RED=$'\033[31m'; C_YEL=$'\033[33m'; C_GRN=$'\033[32m'; C_CYN=$'\033[36m' |
| 83 | +else |
| 84 | + C_RESET=; C_DIM=; C_BOLD=; C_RED=; C_YEL=; C_GRN=; C_CYN= |
| 85 | +fi |
| 86 | + |
| 87 | +bar() { |
| 88 | + # bar <remaining> <limit> <width> |
| 89 | + local rem="$1" lim="$2" width="${3:-24}" |
| 90 | + if (( lim <= 0 )); then printf '%*s' "$width" ''; return; fi |
| 91 | + local filled=$(( rem * width / lim )) |
| 92 | + (( filled < 0 )) && filled=0 |
| 93 | + (( filled > width )) && filled=$width |
| 94 | + local empty=$(( width - filled )) |
| 95 | + local colour="$C_GRN" |
| 96 | + local pct=$(( rem * 100 / lim )) |
| 97 | + if (( pct < 10 )); then colour="$C_RED" |
| 98 | + elif (( pct < 33 )); then colour="$C_YEL" |
| 99 | + fi |
| 100 | + printf '%s' "$colour" |
| 101 | + if (( filled > 0 )); then |
| 102 | + printf '█%.0s' $(seq 1 "$filled") |
| 103 | + fi |
| 104 | + printf '%s' "$C_DIM" |
| 105 | + if (( empty > 0 )); then |
| 106 | + printf '░%.0s' $(seq 1 "$empty") |
| 107 | + fi |
| 108 | + printf '%s' "$C_RESET" |
| 109 | +} |
| 110 | + |
| 111 | +human_reset() { |
| 112 | + # Seconds-from-now → e.g. "in 31m12s" or "passed" |
| 113 | + local target="$1" now diff m s |
| 114 | + now=$(date +%s) |
| 115 | + diff=$(( target - now )) |
| 116 | + if (( diff <= 0 )); then echo "now"; return; fi |
| 117 | + m=$(( diff / 60 )); s=$(( diff % 60 )) |
| 118 | + if (( m > 0 )); then printf 'in %dm%02ds' "$m" "$s" |
| 119 | + else printf 'in %ds' "$s" |
| 120 | + fi |
| 121 | +} |
| 122 | + |
| 123 | +render() { |
| 124 | + local payload |
| 125 | + if ! payload=$(gh api rate_limit 2>/dev/null); then |
| 126 | + echo "${C_RED}gh api failed - are you authenticated? (gh auth status)${C_RESET}" >&2 |
| 127 | + return 1 |
| 128 | + fi |
| 129 | + |
| 130 | + if (( JSON )); then |
| 131 | + echo "$payload" | jq . |
| 132 | + return 0 |
| 133 | + fi |
| 134 | + |
| 135 | + # Header |
| 136 | + local user host now |
| 137 | + user=$(gh api user --jq .login 2>/dev/null || echo '?') |
| 138 | + host=$(gh auth status 2>&1 | awk -F'[ ()]+' '/Logged in to/{print $5; exit}' || echo 'github.com') |
| 139 | + now=$(date '+%H:%M:%S') |
| 140 | + printf '%s%sGitHub rate limits%s user=%s%s%s host=%s %s%s%s\n' \ |
| 141 | + "$C_BOLD" "$C_CYN" "$C_RESET" "$C_BOLD" "$user" "$C_RESET" "$host" "$C_DIM" "$now" "$C_RESET" |
| 142 | + echo |
| 143 | +
|
| 144 | + # Sort: most-pressured (lowest %) first |
| 145 | + local jq_filter=' |
| 146 | + .resources |
| 147 | + | to_entries |
| 148 | + | map({ |
| 149 | + key: .key, |
| 150 | + rem: .value.remaining, |
| 151 | + lim: .value.limit, |
| 152 | + used: .value.used, |
| 153 | + reset: .value.reset, |
| 154 | + pct: (if .value.limit > 0 then (.value.remaining * 100 / .value.limit) else 100 end) |
| 155 | + }) |
| 156 | + | sort_by(.pct) |
| 157 | + | .[] |
| 158 | + | [.key, (.rem|tostring), (.lim|tostring), (.used|tostring), (.reset|tostring), ((.pct|floor)|tostring)] |
| 159 | + | @tsv' |
| 160 | +
|
| 161 | + local key rem lim used reset pct |
| 162 | + while IFS=$'\t' read -r key rem lim used reset pct; do |
| 163 | + if (( QUIET )) && (( rem == lim || lim == 0 )); then continue; fi |
| 164 | + printf '%s%-26s%s %5d/%-5d %s %3d%% resets %s\n' \ |
| 165 | + "$C_BOLD" "$key" "$C_RESET" \ |
| 166 | + "$rem" "$lim" \ |
| 167 | + "$(bar "$rem" "$lim" 24)" \ |
| 168 | + "$pct" \ |
| 169 | + "$(human_reset "$reset")" |
| 170 | + done < <(echo "$payload" | jq -r "$jq_filter") |
| 171 | +} |
| 172 | +
|
| 173 | +if (( WATCH )); then |
| 174 | + trap 'tput cnorm 2>/dev/null; exit 0' INT TERM |
| 175 | + tput civis 2>/dev/null || true |
| 176 | + while true; do |
| 177 | + clear |
| 178 | + render || true |
| 179 | + printf '\n%srefresh every %ss - ctrl-c to exit%s\n' "$C_DIM" "$INTERVAL" "$C_RESET" |
| 180 | + sleep "$INTERVAL" |
| 181 | + done |
| 182 | +else |
| 183 | + render |
| 184 | +fi |
0 commit comments