Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -9,6 +9,7 @@ dependencies = [
"httpx",
"platformdirs",
"readchar",
"truststore>=0.10.4",
]

[project.scripts]
Expand All @@ -19,4 +20,4 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"]
packages = ["src/specify_cli"]
48 changes: 29 additions & 19 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@

# 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 = {
Expand Down Expand Up @@ -385,19 +390,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:
Expand Down Expand Up @@ -437,18 +441,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}"),
Expand All @@ -462,10 +463,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}")
Expand All @@ -483,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)
"""
Expand All @@ -497,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))
Expand Down Expand Up @@ -642,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.
Expand Down Expand Up @@ -770,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:
Expand Down Expand Up @@ -835,21 +841,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")

Expand Down