Merge branch 'main' into feature/add-qwen-support

This commit is contained in:
Ahmet Çetinkaya
2025-09-11 23:46:30 +03:00
committed by GitHub
3 changed files with 45 additions and 31 deletions

View File

@@ -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. 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 [Python 3.11+](https://www.python.org/downloads/)
1. Install [uv](https://docs.astral.sh/uv/) for package management 2. Install [uv](https://docs.astral.sh/uv/) for package management
1. Install [Git](https://git-scm.com/downloads) 3. 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) 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 ## 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. Fork and clone the repository
1. Configure and install the dependencies: `uv sync` 2. Configure and install the dependencies: `uv sync`
1. Make sure the CLI works on your machine: `uv run specify --help` 3. Make sure the CLI works on your machine: `uv run specify --help`
1. Create a new branch: `git checkout -b my-branch-name` 4. Create a new branch: `git checkout -b my-branch-name`
1. Make your change, add tests, and make sure everything still works 5. Make your change, add tests, and make sure everything still works
1. Test the CLI functionality with a sample project if relevant 6. Test the CLI functionality with a sample project if relevant
1. Push to your fork and submit a pull request 7. Push to your fork and submit a pull request
1. Wait for your pull request to be reviewed and merged. 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: Here are a few things you can do that will increase the likelihood of your pull request being accepted:

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "specify-cli" name = "specify-cli"
version = "0.0.2" version = "0.0.3"
description = "Setup tool for Specify spec-driven development projects" description = "Setup tool for Specify spec-driven development projects"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -9,6 +9,7 @@ dependencies = [
"httpx", "httpx",
"platformdirs", "platformdirs",
"readchar", "readchar",
"truststore>=0.10.4",
] ]
[project.scripts] [project.scripts]
@@ -19,4 +20,4 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"] packages = ["src/specify_cli"]

View File

@@ -46,6 +46,11 @@ from typer.core import TyperGroup
# For cross-platform keyboard input # For cross-platform keyboard input
import readchar import readchar
import ssl
import truststore
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client = httpx.Client(verify=ssl_context)
# Constants # Constants
AI_CHOICES = { AI_CHOICES = {
@@ -386,19 +391,18 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
os.chdir(original_cwd) os.chdir(original_cwd)
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True): def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None):
"""Download the latest template release from GitHub using HTTP requests.
Returns (zip_path, metadata_dict)
"""
repo_owner = "github" repo_owner = "github"
repo_name = "spec-kit" repo_name = "spec-kit"
if client is None:
client = httpx.Client(verify=ssl_context)
if verbose: if verbose:
console.print("[cyan]Fetching latest release information...[/cyan]") console.print("[cyan]Fetching latest release information...[/cyan]")
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
try: 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() response.raise_for_status()
release_data = response.json() release_data = response.json()
except httpx.RequestError as e: 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]") console.print(f"[cyan]Downloading template...[/cyan]")
try: 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() response.raise_for_status()
total_size = int(response.headers.get('content-length', 0)) total_size = int(response.headers.get('content-length', 0))
with open(zip_path, 'wb') as f: with open(zip_path, 'wb') as f:
if total_size == 0: if total_size == 0:
# No content-length header, download without progress
for chunk in response.iter_bytes(chunk_size=8192): for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk) f.write(chunk)
else: else:
if show_progress: if show_progress:
# Show progress bar
with Progress( with Progress(
SpinnerColumn(), SpinnerColumn(),
TextColumn("[progress.description]{task.description}"), TextColumn("[progress.description]{task.description}"),
@@ -463,10 +464,8 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
downloaded += len(chunk) downloaded += len(chunk)
progress.update(task, completed=downloaded) progress.update(task, completed=downloaded)
else: else:
# Silent download loop
for chunk in response.iter_bytes(chunk_size=8192): for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk) f.write(chunk)
except httpx.RequestError as e: except httpx.RequestError as e:
if verbose: if verbose:
console.print(f"[red]Error downloading template:[/red] {e}") 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 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. """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) 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, ai_assistant,
current_dir, current_dir,
verbose=verbose and tracker is None, verbose=verbose and tracker is None,
show_progress=(tracker is None) show_progress=(tracker is None),
client=client
) )
if tracker: if tracker:
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)") tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
tracker.add("download", "Download template") tracker.add("download", "Download template")
tracker.complete("download", meta['filename']) # already downloaded inside helper tracker.complete("download", meta['filename'])
except Exception as e: except Exception as e:
if tracker: if tracker:
tracker.error("fetch", str(e)) 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"), 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"), 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"), 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. 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: with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
tracker.attach_refresh(lambda: live.update(tracker.render())) tracker.attach_refresh(lambda: live.update(tracker.render()))
try: 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 # Git step
if not no_git: if not no_git:
@@ -847,21 +853,25 @@ def init(
# Removed farewell line per user request # Removed farewell line per user request
# Add skip_tls option to check
@app.command() @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.""" """Check that all required tools are installed."""
show_banner() show_banner()
console.print("[bold]Checking Specify requirements...[/bold]\n") console.print("[bold]Checking Specify requirements...[/bold]\n")
# Check if we have internet connectivity by trying to reach GitHub API # Check if we have internet connectivity by trying to reach GitHub API
console.print("[cyan]Checking internet connectivity...[/cyan]") 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: 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") console.print("[green]✓[/green] Internet connection available")
except httpx.RequestError: except httpx.RequestError:
console.print("[red]✗[/red] No internet connection - required for downloading templates") console.print("[red]✗[/red] No internet connection - required for downloading templates")
console.print("[yellow]Please check your internet connection[/yellow]") console.print("[yellow]Please check your internet connection[/yellow]")
console.print("\n[cyan]Optional tools:[/cyan]") console.print("\n[cyan]Optional tools:[/cyan]")
git_ok = check_tool("git", "https://git-scm.com/downloads") git_ok = check_tool("git", "https://git-scm.com/downloads")