From 6e2af26867c54428ad6f474d855248be34eca167 Mon Sep 17 00:00:00 2001 From: Ram Date: Tue, 9 Sep 2025 21:30:45 -0400 Subject: [PATCH 1/3] Refactor HTTP client usage to utilize truststore for SSL context --- pyproject.toml | 3 ++- src/specify_cli/__init__.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2cad65f..7cfd392 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "httpx", "platformdirs", "readchar", + "truststore>=0.10.4", ] [project.scripts] @@ -19,4 +20,4 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/specify_cli"] \ No newline at end of file +packages = ["src/specify_cli"] diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index aa3cf3c..f25956d 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -46,6 +46,11 @@ from typer.core import TyperGroup # For cross-platform keyboard input import readchar +import ssl +import truststore + +ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +client = httpx.Client(verify=ssl_context) # Constants AI_CHOICES = { @@ -397,10 +402,10 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" try: - response = httpx.get(api_url, timeout=30, follow_redirects=True) + response = client.get(api_url, timeout=30, follow_redirects=True) response.raise_for_status() release_data = response.json() - except httpx.RequestError as e: + except client.RequestError as e: if verbose: console.print(f"[red]Error fetching release information:[/red] {e}") raise typer.Exit(1) @@ -437,7 +442,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb console.print(f"[cyan]Downloading template...[/cyan]") try: - with httpx.stream("GET", download_url, timeout=30, follow_redirects=True) as response: + with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response: response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) @@ -466,7 +471,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb for chunk in response.iter_bytes(chunk_size=8192): f.write(chunk) - except httpx.RequestError as e: + except client.RequestError as e: if verbose: console.print(f"[red]Error downloading template:[/red] {e}") if zip_path.exists(): @@ -843,9 +848,9 @@ def check(): # Check if we have internet connectivity by trying to reach GitHub API console.print("[cyan]Checking internet connectivity...[/cyan]") try: - response = httpx.get("https://api.github.com", timeout=5, follow_redirects=True) + response = client.get("https://api.github.com", timeout=5, follow_redirects=True) console.print("[green]✓[/green] Internet connection available") - except httpx.RequestError: + except client.RequestError: console.print("[red]✗[/red] No internet connection - required for downloading templates") console.print("[yellow]Please check your internet connection[/yellow]") From e21820fb92880c8d019cfcb60c3ef978a9275700 Mon Sep 17 00:00:00 2001 From: Ram Date: Wed, 10 Sep 2025 22:27:38 -0400 Subject: [PATCH 2/3] Enhance HTTP client initialization with optional SSL verification and bump version to 0.0.3 --- pyproject.toml | 2 +- src/specify_cli/__init__.py | 45 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7cfd392..bd24288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.0.2" +version = "0.0.3" description = "Setup tool for Specify spec-driven development projects" requires-python = ">=3.11" dependencies = [ diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f25956d..82e789b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -390,12 +390,11 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool: os.chdir(original_cwd) -def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True): - """Download the latest template release from GitHub using HTTP requests. - Returns (zip_path, metadata_dict) - """ +def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None): repo_owner = "github" repo_name = "spec-kit" + if client is None: + client = httpx.Client(verify=ssl_context) if verbose: console.print("[cyan]Fetching latest release information...[/cyan]") @@ -405,7 +404,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb response = client.get(api_url, timeout=30, follow_redirects=True) response.raise_for_status() release_data = response.json() - except client.RequestError as e: + except httpx.RequestError as e: if verbose: console.print(f"[red]Error fetching release information:[/red] {e}") raise typer.Exit(1) @@ -445,15 +444,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response: response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) - with open(zip_path, 'wb') as f: if total_size == 0: - # No content-length header, download without progress for chunk in response.iter_bytes(chunk_size=8192): f.write(chunk) else: if show_progress: - # Show progress bar with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), @@ -467,11 +463,9 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb downloaded += len(chunk) progress.update(task, completed=downloaded) else: - # Silent download loop for chunk in response.iter_bytes(chunk_size=8192): f.write(chunk) - - except client.RequestError as e: + except httpx.RequestError as e: if verbose: console.print(f"[red]Error downloading template:[/red] {e}") if zip_path.exists(): @@ -488,7 +482,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb return zip_path, metadata -def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None) -> Path: +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: """Download the latest release and extract it to create a new project. Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) """ @@ -502,12 +496,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr ai_assistant, current_dir, verbose=verbose and tracker is None, - show_progress=(tracker is None) + show_progress=(tracker is None), + client=client ) if tracker: tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)") tracker.add("download", "Download template") - tracker.complete("download", meta['filename']) # already downloaded inside helper + tracker.complete("download", meta['filename']) except Exception as e: if tracker: tracker.error("fetch", str(e)) @@ -647,6 +642,7 @@ def init( ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"), + skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"), ): """ Initialize a new Specify project from the latest template. @@ -775,7 +771,12 @@ def init( with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live: tracker.attach_refresh(lambda: live.update(tracker.render())) try: - download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker) + # Create a httpx client with verify based on skip_tls + verify = not skip_tls + local_ssl_context = ssl_context if verify else False + local_client = httpx.Client(verify=local_ssl_context) + + download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client) # Git step if not no_git: @@ -839,21 +840,25 @@ def init( # Removed farewell line per user request +# Add skip_tls option to check @app.command() -def check(): +def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")): """Check that all required tools are installed.""" show_banner() console.print("[bold]Checking Specify requirements...[/bold]\n") - + # Check if we have internet connectivity by trying to reach GitHub API console.print("[cyan]Checking internet connectivity...[/cyan]") + verify = not skip_tls + local_ssl_context = ssl_context if verify else False + local_client = httpx.Client(verify=local_ssl_context) try: - response = client.get("https://api.github.com", timeout=5, follow_redirects=True) + response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True) console.print("[green]✓[/green] Internet connection available") - except client.RequestError: + except httpx.RequestError: console.print("[red]✗[/red] No internet connection - required for downloading templates") console.print("[yellow]Please check your internet connection[/yellow]") - + console.print("\n[cyan]Optional tools:[/cyan]") git_ok = check_tool("git", "https://git-scm.com/downloads") From 708e887022ec47120f3c2b372cdca5dae48cbbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:26:53 -0700 Subject: [PATCH 3/3] Update CONTRIBUTING.md --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7a75c2..0b1cbd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,13 @@ These are one time installations required to be able to test your changes locall 1. Install [Python 3.11+](https://www.python.org/downloads/) 1. Install [uv](https://docs.astral.sh/uv/) for package management 1. Install [Git](https://git-scm.com/downloads) -1. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) +1. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) are recommended, but we're working on adding support for other agents as well. ## Submitting a pull request +>[!NOTE] +>If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed. + 1. Fork and clone the repository 1. Configure and install the dependencies: `uv sync` 1. Make sure the CLI works on your machine: `uv run specify --help`