5252ssl_context = truststore .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
5353client = httpx .Client (verify = ssl_context )
5454
55+ def _github_token (cli_token : str | None = None ) -> str | None :
56+ return cli_token or os .getenv ("GH_TOKEN" ) or os .getenv ("GITHUB_TOKEN" )
57+
58+ def _github_auth_headers (cli_token : str | None = None ) -> dict :
59+ """Headers for GitHub REST API requests.
60+ - Uses Bearer auth if token present
61+ """
62+ headers = {}
63+ token = _github_token (cli_token )
64+ if token :
65+ headers ["Authorization" ] = f"Bearer { token } "
66+ return headers
67+
5568# Constants
5669AI_CHOICES = {
5770 "copilot" : "GitHub Copilot" ,
@@ -418,7 +431,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
418431 os .chdir (original_cwd )
419432
420433
421- def download_template_from_github (ai_assistant : str , download_dir : Path , * , script_type : str = "sh" , verbose : bool = True , show_progress : bool = True , client : httpx .Client = None , debug : bool = False ) -> Tuple [Path , dict ]:
434+ def download_template_from_github (ai_assistant : str , download_dir : Path , * , script_type : str = "sh" , verbose : bool = True , show_progress : bool = True , client : httpx .Client = None , debug : bool = False , github_token : str = None ) -> Tuple [Path , dict ]:
422435 repo_owner = "github"
423436 repo_name = "spec-kit"
424437 if client is None :
@@ -429,7 +442,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
429442 api_url = f"https://api.github.com/repos/{ repo_owner } /{ repo_name } /releases/latest"
430443
431444 try :
432- response = client .get (api_url , timeout = 30 , follow_redirects = True )
445+ response = client .get (
446+ api_url ,
447+ timeout = 30 ,
448+ follow_redirects = True ,
449+ headers = _github_auth_headers (github_token ) or None ,
450+ )
433451 status = response .status_code
434452 if status != 200 :
435453 msg = f"GitHub API returned { status } for { api_url } "
@@ -475,7 +493,14 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
475493 console .print (f"[cyan]Downloading template...[/cyan]" )
476494
477495 try :
478- with client .stream ("GET" , download_url , timeout = 60 , follow_redirects = True ) as response :
496+ # Include auth header for initial GitHub request; it won't leak across cross-origin redirects
497+ with client .stream (
498+ "GET" ,
499+ download_url ,
500+ timeout = 60 ,
501+ follow_redirects = True ,
502+ headers = _github_auth_headers (github_token ) or None ,
503+ ) as response :
479504 if response .status_code != 200 :
480505 body_sample = response .text [:400 ]
481506 raise RuntimeError (f"Download failed with { response .status_code } \n Headers: { response .headers } \n Body (truncated): { body_sample } " )
@@ -519,7 +544,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
519544 return zip_path , metadata
520545
521546
522- def download_and_extract_template (project_path : Path , ai_assistant : str , script_type : str , is_current_dir : bool = False , * , verbose : bool = True , tracker : StepTracker | None = None , client : httpx .Client = None , debug : bool = False ) -> Path :
547+ def download_and_extract_template (project_path : Path , ai_assistant : str , script_type : str , is_current_dir : bool = False , * , verbose : bool = True , tracker : StepTracker | None = None , client : httpx .Client = None , debug : bool = False , github_token : str = None ) -> Path :
523548 """Download the latest release and extract it to create a new project.
524549 Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
525550 """
@@ -536,7 +561,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
536561 verbose = verbose and tracker is None ,
537562 show_progress = (tracker is None ),
538563 client = client ,
539- debug = debug
564+ debug = debug ,
565+ github_token = github_token
540566 )
541567 if tracker :
542568 tracker .complete ("fetch" , f"release { meta ['release' ]} ({ meta ['size' ]:,} bytes)" )
@@ -731,6 +757,7 @@ def init(
731757 here : bool = typer .Option (False , "--here" , help = "Initialize project in the current directory instead of creating a new one" ),
732758 skip_tls : bool = typer .Option (False , "--skip-tls" , help = "Skip SSL/TLS verification (not recommended)" ),
733759 debug : bool = typer .Option (False , "--debug" , help = "Show verbose diagnostic output for network and extraction failures" ),
760+ github_token : str = typer .Option (None , "--github-token" , help = "GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)" ),
734761):
735762 """
736763 Initialize a new Specify project from the latest template.
@@ -897,7 +924,7 @@ def init(
897924 local_ssl_context = ssl_context if verify else False
898925 local_client = httpx .Client (verify = local_ssl_context )
899926
900- download_and_extract_template (project_path , selected_ai , selected_script , here , verbose = False , tracker = tracker , client = local_client , debug = debug )
927+ download_and_extract_template (project_path , selected_ai , selected_script , here , verbose = False , tracker = tracker , client = local_client , debug = debug , github_token = github_token )
901928
902929 # Ensure scripts are executable (POSIX)
903930 ensure_executable_scripts (project_path , tracker = tracker )
0 commit comments