Skip to content

Commit a877af5

Browse files
committed
Fixes #970
1 parent 2508d92 commit a877af5

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ All notable changes to the Specify CLI and templates are documented here.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.0.21] - 2025-10-21
11+
12+
- Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)).
13+
- Adds support for Amp CLI.
14+
- Adds support for VS Code hand-offs and moves prompts to be full-fledged chat modes.
15+
- Adds support for `version` command (addresses [#811](https://github.com/github/spec-kit/issues/811) and [#486](https://github.com/github/spec-kit/issues/486), thank you [@mcasalaina](https://github.com/github/spec-kit/issues/811) and [@dentity007](https://github.com/dentity007)).
16+
- Adds support for rendering the rate limit errors from the CLI when encountered ([#970](https://github.com/github/spec-kit/issues/970), thank you [@psmman](https://github.com/psmman)).
17+
1018
## [0.0.20] - 2025-10-14
1119

1220
### Added

src/specify_cli/__init__.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import readchar
5252
import ssl
5353
import truststore
54+
from datetime import datetime, timezone
5455

5556
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
5657
client = httpx.Client(verify=ssl_context)
@@ -64,6 +65,63 @@ def _github_auth_headers(cli_token: str | None = None) -> dict:
6465
token = _github_token(cli_token)
6566
return {"Authorization": f"Bearer {token}"} if token else {}
6667

68+
def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
69+
"""Extract and parse GitHub rate-limit headers."""
70+
info = {}
71+
72+
# Standard GitHub rate-limit headers
73+
if "X-RateLimit-Limit" in headers:
74+
info["limit"] = headers.get("X-RateLimit-Limit")
75+
if "X-RateLimit-Remaining" in headers:
76+
info["remaining"] = headers.get("X-RateLimit-Remaining")
77+
if "X-RateLimit-Reset" in headers:
78+
reset_epoch = int(headers.get("X-RateLimit-Reset", "0"))
79+
if reset_epoch:
80+
reset_time = datetime.fromtimestamp(reset_epoch, tz=timezone.utc)
81+
info["reset_epoch"] = reset_epoch
82+
info["reset_time"] = reset_time
83+
info["reset_local"] = reset_time.astimezone()
84+
85+
# Retry-After header (seconds or HTTP-date)
86+
if "Retry-After" in headers:
87+
retry_after = headers.get("Retry-After")
88+
try:
89+
info["retry_after_seconds"] = int(retry_after)
90+
except ValueError:
91+
# HTTP-date format - not implemented, just store as string
92+
info["retry_after"] = retry_after
93+
94+
return info
95+
96+
def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str:
97+
"""Format a user-friendly error message with rate-limit information."""
98+
rate_info = _parse_rate_limit_headers(headers)
99+
100+
lines = [f"GitHub API returned status {status_code} for {url}"]
101+
lines.append("")
102+
103+
if rate_info:
104+
lines.append("[bold]Rate Limit Information:[/bold]")
105+
if "limit" in rate_info:
106+
lines.append(f" • Rate Limit: {rate_info['limit']} requests/hour")
107+
if "remaining" in rate_info:
108+
lines.append(f" • Remaining: {rate_info['remaining']}")
109+
if "reset_local" in rate_info:
110+
reset_str = rate_info["reset_local"].strftime("%Y-%m-%d %H:%M:%S %Z")
111+
lines.append(f" • Resets at: {reset_str}")
112+
if "retry_after_seconds" in rate_info:
113+
lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds")
114+
lines.append("")
115+
116+
# Add troubleshooting guidance
117+
lines.append("[bold]Troubleshooting Tips:[/bold]")
118+
lines.append(" • If you're on a shared CI or corporate environment, you may be rate-limited.")
119+
lines.append(" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN")
120+
lines.append(" environment variable to increase rate limits.")
121+
lines.append(" • Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.")
122+
123+
return "\n".join(lines)
124+
67125
# Agent configuration with name, folder, install URL, and CLI tool requirement
68126
AGENT_CONFIG = {
69127
"copilot": {
@@ -571,10 +629,11 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
571629
)
572630
status = response.status_code
573631
if status != 200:
574-
msg = f"GitHub API returned {status} for {api_url}"
632+
# Format detailed error message with rate-limit info
633+
error_msg = _format_rate_limit_error(status, response.headers, api_url)
575634
if debug:
576-
msg += f"\nResponse headers: {response.headers}\nBody (truncated 500): {response.text[:500]}"
577-
raise RuntimeError(msg)
635+
error_msg += f"\n\n[dim]Response body (truncated 500):[/dim]\n{response.text[:500]}"
636+
raise RuntimeError(error_msg)
578637
try:
579638
release_data = response.json()
580639
except ValueError as je:
@@ -621,8 +680,11 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
621680
headers=_github_auth_headers(github_token),
622681
) as response:
623682
if response.status_code != 200:
624-
body_sample = response.text[:400]
625-
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
683+
# Handle rate-limiting on download as well
684+
error_msg = _format_rate_limit_error(response.status_code, response.headers, download_url)
685+
if debug:
686+
error_msg += f"\n\n[dim]Response body (truncated 400):[/dim]\n{response.text[:400]}"
687+
raise RuntimeError(error_msg)
626688
total_size = int(response.headers.get('content-length', 0))
627689
with open(zip_path, 'wb') as f:
628690
if total_size == 0:

0 commit comments

Comments
 (0)