diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d8441e..888b809 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,20 +9,23 @@ Please note that this project is released with a [Contributor Code of Conduct](C These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process. 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/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Qwen CLI](https://github.com/QwenLM/qwen-code) +2. Install [uv](https://docs.astral.sh/uv/) for package management +3. Install [Git](https://git-scm.com/downloads) +4. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Qwen Code](https://github.com/QwenLM/qwen-code). 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` -1. Create a new branch: `git checkout -b my-branch-name` -1. Make your change, add tests, and make sure everything still works -1. Test the CLI functionality with a sample project if relevant -1. Push to your fork and submit a pull request -1. Wait for your pull request to be reviewed and merged. +2. Configure and install the dependencies: `uv sync` +3. Make sure the CLI works on your machine: `uv run specify --help` +4. Create a new branch: `git checkout -b my-branch-name` +5. Make your change, add tests, and make sure everything still works +6. Test the CLI functionality with a sample project if relevant +7. Push to your fork and submit a pull request +8. Wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: diff --git a/pyproject.toml b/pyproject.toml index 2cad65f..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 = [ @@ -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 936fb73..b825925 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 = { @@ -386,19 +391,18 @@ 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]") 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: @@ -438,18 +442,15 @@ 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)) - 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}"), @@ -463,10 +464,8 @@ 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 httpx.RequestError as e: if verbose: console.print(f"[red]Error downloading template:[/red] {e}") @@ -484,7 +483,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) """ @@ -498,12 +497,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)) @@ -643,6 +643,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. @@ -776,7 +777,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: @@ -847,21 +853,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 = httpx.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 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")