Skip to content

Commit fe4de3c

Browse files
Merge branch 'main' into feature/add-qwen-support
2 parents 73a9af7 + 0f0e19d commit fe4de3c

File tree

3 files changed

+45
-31
lines changed

3 files changed

+45
-31
lines changed

CONTRIBUTING.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,23 @@ Please note that this project is released with a [Contributor Code of Conduct](C
99
These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process.
1010

1111
1. Install [Python 3.11+](https://www.python.org/downloads/)
12-
1. Install [uv](https://docs.astral.sh/uv/) for package management
13-
1. Install [Git](https://git-scm.com/downloads)
14-
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)
12+
2. Install [uv](https://docs.astral.sh/uv/) for package management
13+
3. Install [Git](https://git-scm.com/downloads)
14+
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.
1515

1616
## Submitting a pull request
1717

18+
>[!NOTE]
19+
>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.
20+
1821
1. Fork and clone the repository
19-
1. Configure and install the dependencies: `uv sync`
20-
1. Make sure the CLI works on your machine: `uv run specify --help`
21-
1. Create a new branch: `git checkout -b my-branch-name`
22-
1. Make your change, add tests, and make sure everything still works
23-
1. Test the CLI functionality with a sample project if relevant
24-
1. Push to your fork and submit a pull request
25-
1. Wait for your pull request to be reviewed and merged.
22+
2. Configure and install the dependencies: `uv sync`
23+
3. Make sure the CLI works on your machine: `uv run specify --help`
24+
4. Create a new branch: `git checkout -b my-branch-name`
25+
5. Make your change, add tests, and make sure everything still works
26+
6. Test the CLI functionality with a sample project if relevant
27+
7. Push to your fork and submit a pull request
28+
8. Wait for your pull request to be reviewed and merged.
2629

2730
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
2831

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.0.2"
3+
version = "0.0.3"
44
description = "Setup tool for Specify spec-driven development projects"
55
requires-python = ">=3.11"
66
dependencies = [
@@ -9,6 +9,7 @@ dependencies = [
99
"httpx",
1010
"platformdirs",
1111
"readchar",
12+
"truststore>=0.10.4",
1213
]
1314

1415
[project.scripts]
@@ -19,4 +20,4 @@ requires = ["hatchling"]
1920
build-backend = "hatchling.build"
2021

2122
[tool.hatch.build.targets.wheel]
22-
packages = ["src/specify_cli"]
23+
packages = ["src/specify_cli"]

src/specify_cli/__init__.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646

4747
# For cross-platform keyboard input
4848
import 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
5156
AI_CHOICES = {
@@ -386,19 +391,18 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
386391
os.chdir(original_cwd)
387392

388393

389-
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True):
390-
"""Download the latest template release from GitHub using HTTP requests.
391-
Returns (zip_path, metadata_dict)
392-
"""
394+
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None):
393395
repo_owner = "github"
394396
repo_name = "spec-kit"
397+
if client is None:
398+
client = httpx.Client(verify=ssl_context)
395399

396400
if verbose:
397401
console.print("[cyan]Fetching latest release information...[/cyan]")
398402
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
399403

400404
try:
401-
response = httpx.get(api_url, timeout=30, follow_redirects=True)
405+
response = client.get(api_url, timeout=30, follow_redirects=True)
402406
response.raise_for_status()
403407
release_data = response.json()
404408
except httpx.RequestError as e:
@@ -438,18 +442,15 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
438442
console.print(f"[cyan]Downloading template...[/cyan]")
439443

440444
try:
441-
with httpx.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
445+
with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
442446
response.raise_for_status()
443447
total_size = int(response.headers.get('content-length', 0))
444-
445448
with open(zip_path, 'wb') as f:
446449
if total_size == 0:
447-
# No content-length header, download without progress
448450
for chunk in response.iter_bytes(chunk_size=8192):
449451
f.write(chunk)
450452
else:
451453
if show_progress:
452-
# Show progress bar
453454
with Progress(
454455
SpinnerColumn(),
455456
TextColumn("[progress.description]{task.description}"),
@@ -463,10 +464,8 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
463464
downloaded += len(chunk)
464465
progress.update(task, completed=downloaded)
465466
else:
466-
# Silent download loop
467467
for chunk in response.iter_bytes(chunk_size=8192):
468468
f.write(chunk)
469-
470469
except httpx.RequestError as e:
471470
if verbose:
472471
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
484483
return zip_path, metadata
485484

486485

487-
def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None) -> Path:
486+
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:
488487
"""Download the latest release and extract it to create a new project.
489488
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
490489
"""
@@ -498,12 +497,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
498497
ai_assistant,
499498
current_dir,
500499
verbose=verbose and tracker is None,
501-
show_progress=(tracker is None)
500+
show_progress=(tracker is None),
501+
client=client
502502
)
503503
if tracker:
504504
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
505505
tracker.add("download", "Download template")
506-
tracker.complete("download", meta['filename']) # already downloaded inside helper
506+
tracker.complete("download", meta['filename'])
507507
except Exception as e:
508508
if tracker:
509509
tracker.error("fetch", str(e))
@@ -643,6 +643,7 @@ def init(
643643
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
644644
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
645645
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
646+
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
646647
):
647648
"""
648649
Initialize a new Specify project from the latest template.
@@ -776,7 +777,12 @@ def init(
776777
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
777778
tracker.attach_refresh(lambda: live.update(tracker.render()))
778779
try:
779-
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker)
780+
# Create a httpx client with verify based on skip_tls
781+
verify = not skip_tls
782+
local_ssl_context = ssl_context if verify else False
783+
local_client = httpx.Client(verify=local_ssl_context)
784+
785+
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client)
780786

781787
# Git step
782788
if not no_git:
@@ -847,21 +853,25 @@ def init(
847853
# Removed farewell line per user request
848854

849855

856+
# Add skip_tls option to check
850857
@app.command()
851-
def check():
858+
def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")):
852859
"""Check that all required tools are installed."""
853860
show_banner()
854861
console.print("[bold]Checking Specify requirements...[/bold]\n")
855-
862+
856863
# Check if we have internet connectivity by trying to reach GitHub API
857864
console.print("[cyan]Checking internet connectivity...[/cyan]")
865+
verify = not skip_tls
866+
local_ssl_context = ssl_context if verify else False
867+
local_client = httpx.Client(verify=local_ssl_context)
858868
try:
859-
response = httpx.get("https://api.github.com", timeout=5, follow_redirects=True)
869+
response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True)
860870
console.print("[green]✓[/green] Internet connection available")
861871
except httpx.RequestError:
862872
console.print("[red]✗[/red] No internet connection - required for downloading templates")
863873
console.print("[yellow]Please check your internet connection[/yellow]")
864-
874+
865875
console.print("\n[cyan]Optional tools:[/cyan]")
866876
git_ok = check_tool("git", "https://git-scm.com/downloads")
867877

0 commit comments

Comments
 (0)