4646
4747# For cross-platform keyboard input
4848import readchar
49+ import ssl
50+ import truststore
51+
52+ ssl_context = truststore .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
53+ client = httpx .Client (verify = ssl_context )
4954
5055# Constants
5156AI_CHOICES = {
@@ -385,19 +390,18 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
385390 os .chdir (original_cwd )
386391
387392
388- def download_template_from_github (ai_assistant : str , download_dir : Path , * , verbose : bool = True , show_progress : bool = True ):
389- """Download the latest template release from GitHub using HTTP requests.
390- Returns (zip_path, metadata_dict)
391- """
393+ def download_template_from_github (ai_assistant : str , download_dir : Path , * , verbose : bool = True , show_progress : bool = True , client : httpx .Client = None ):
392394 repo_owner = "github"
393395 repo_name = "spec-kit"
396+ if client is None :
397+ client = httpx .Client (verify = ssl_context )
394398
395399 if verbose :
396400 console .print ("[cyan]Fetching latest release information...[/cyan]" )
397401 api_url = f"https://api.github.com/repos/{ repo_owner } /{ repo_name } /releases/latest"
398402
399403 try :
400- response = httpx .get (api_url , timeout = 30 , follow_redirects = True )
404+ response = client .get (api_url , timeout = 30 , follow_redirects = True )
401405 response .raise_for_status ()
402406 release_data = response .json ()
403407 except httpx .RequestError as e :
@@ -437,18 +441,15 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
437441 console .print (f"[cyan]Downloading template...[/cyan]" )
438442
439443 try :
440- with httpx .stream ("GET" , download_url , timeout = 30 , follow_redirects = True ) as response :
444+ with client .stream ("GET" , download_url , timeout = 30 , follow_redirects = True ) as response :
441445 response .raise_for_status ()
442446 total_size = int (response .headers .get ('content-length' , 0 ))
443-
444447 with open (zip_path , 'wb' ) as f :
445448 if total_size == 0 :
446- # No content-length header, download without progress
447449 for chunk in response .iter_bytes (chunk_size = 8192 ):
448450 f .write (chunk )
449451 else :
450452 if show_progress :
451- # Show progress bar
452453 with Progress (
453454 SpinnerColumn (),
454455 TextColumn ("[progress.description]{task.description}" ),
@@ -462,10 +463,8 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
462463 downloaded += len (chunk )
463464 progress .update (task , completed = downloaded )
464465 else :
465- # Silent download loop
466466 for chunk in response .iter_bytes (chunk_size = 8192 ):
467467 f .write (chunk )
468-
469468 except httpx .RequestError as e :
470469 if verbose :
471470 console .print (f"[red]Error downloading template:[/red] { e } " )
@@ -483,7 +482,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
483482 return zip_path , metadata
484483
485484
486- def download_and_extract_template (project_path : Path , ai_assistant : str , is_current_dir : bool = False , * , verbose : bool = True , tracker : StepTracker | None = None ) -> Path :
485+ def download_and_extract_template (project_path : Path , ai_assistant : str , is_current_dir : bool = False , * , verbose : bool = True , tracker : StepTracker | None = None , client : httpx . Client = None ) -> Path :
487486 """Download the latest release and extract it to create a new project.
488487 Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
489488 """
@@ -497,12 +496,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
497496 ai_assistant ,
498497 current_dir ,
499498 verbose = verbose and tracker is None ,
500- show_progress = (tracker is None )
499+ show_progress = (tracker is None ),
500+ client = client
501501 )
502502 if tracker :
503503 tracker .complete ("fetch" , f"release { meta ['release' ]} ({ meta ['size' ]:,} bytes)" )
504504 tracker .add ("download" , "Download template" )
505- tracker .complete ("download" , meta ['filename' ]) # already downloaded inside helper
505+ tracker .complete ("download" , meta ['filename' ])
506506 except Exception as e :
507507 if tracker :
508508 tracker .error ("fetch" , str (e ))
@@ -642,6 +642,7 @@ def init(
642642 ignore_agent_tools : bool = typer .Option (False , "--ignore-agent-tools" , help = "Skip checks for AI agent tools like Claude Code" ),
643643 no_git : bool = typer .Option (False , "--no-git" , help = "Skip git repository initialization" ),
644644 here : bool = typer .Option (False , "--here" , help = "Initialize project in the current directory instead of creating a new one" ),
645+ skip_tls : bool = typer .Option (False , "--skip-tls" , help = "Skip SSL/TLS verification (not recommended)" ),
645646):
646647 """
647648 Initialize a new Specify project from the latest template.
@@ -770,7 +771,12 @@ def init(
770771 with Live (tracker .render (), console = console , refresh_per_second = 8 , transient = True ) as live :
771772 tracker .attach_refresh (lambda : live .update (tracker .render ()))
772773 try :
773- download_and_extract_template (project_path , selected_ai , here , verbose = False , tracker = tracker )
774+ # Create a httpx client with verify based on skip_tls
775+ verify = not skip_tls
776+ local_ssl_context = ssl_context if verify else False
777+ local_client = httpx .Client (verify = local_ssl_context )
778+
779+ download_and_extract_template (project_path , selected_ai , here , verbose = False , tracker = tracker , client = local_client )
774780
775781 # Git step
776782 if not no_git :
@@ -835,21 +841,25 @@ def init(
835841 # Removed farewell line per user request
836842
837843
844+ # Add skip_tls option to check
838845@app .command ()
839- def check ():
846+ def check (skip_tls : bool = typer . Option ( False , "--skip-tls" , help = "Skip SSL/TLS verification (not recommended)" ) ):
840847 """Check that all required tools are installed."""
841848 show_banner ()
842849 console .print ("[bold]Checking Specify requirements...[/bold]\n " )
843-
850+
844851 # Check if we have internet connectivity by trying to reach GitHub API
845852 console .print ("[cyan]Checking internet connectivity...[/cyan]" )
853+ verify = not skip_tls
854+ local_ssl_context = ssl_context if verify else False
855+ local_client = httpx .Client (verify = local_ssl_context )
846856 try :
847- response = httpx .get ("https://api.github.com" , timeout = 5 , follow_redirects = True )
857+ response = local_client .get ("https://api.github.com" , timeout = 5 , follow_redirects = True )
848858 console .print ("[green]✓[/green] Internet connection available" )
849859 except httpx .RequestError :
850860 console .print ("[red]✗[/red] No internet connection - required for downloading templates" )
851861 console .print ("[yellow]Please check your internet connection[/yellow]" )
852-
862+
853863 console .print ("\n [cyan]Optional tools:[/cyan]" )
854864 git_ok = check_tool ("git" , "https://git-scm.com/downloads" )
855865
0 commit comments