|
33 | 33 | import shlex |
34 | 34 | import json |
35 | 35 | from pathlib import Path |
36 | | -from typing import Optional, Tuple |
| 36 | +from typing import Callable, Optional, Tuple, TypedDict |
37 | 37 |
|
38 | 38 | import typer |
39 | 39 | import httpx |
|
56 | 56 | ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) |
57 | 57 | client = httpx.Client(verify=ssl_context) |
58 | 58 |
|
| 59 | + |
| 60 | +class AgentConfigItem(TypedDict): |
| 61 | + """Type definition for agent configuration entries.""" |
| 62 | + name: str |
| 63 | + folder: str |
| 64 | + install_url: Optional[str] |
| 65 | + requires_cli: bool |
| 66 | + |
| 67 | + |
| 68 | +class StepInfo(TypedDict): |
| 69 | + """Type definition for step tracking entries.""" |
| 70 | + key: str |
| 71 | + label: str |
| 72 | + status: str |
| 73 | + detail: str |
| 74 | + |
59 | 75 | def _github_token(cli_token: str | None = None) -> str | None: |
60 | 76 | """Return sanitized GitHub token (cli arg takes precedence) or None.""" |
61 | 77 | return ((cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") or "").strip()) or None |
@@ -123,7 +139,7 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) |
123 | 139 | return "\n".join(lines) |
124 | 140 |
|
125 | 141 | # Agent configuration with name, folder, install URL, and CLI tool requirement |
126 | | -AGENT_CONFIG = { |
| 142 | +AGENT_CONFIG: dict[str, AgentConfigItem] = { |
127 | 143 | "copilot": { |
128 | 144 | "name": "GitHub Copilot", |
129 | 145 | "folder": ".github/", |
@@ -236,11 +252,11 @@ class StepTracker: |
236 | 252 | """ |
237 | 253 | def __init__(self, title: str): |
238 | 254 | self.title = title |
239 | | - self.steps = [] # list of dicts: {key, label, status, detail} |
| 255 | + self.steps: list[StepInfo] = [] |
240 | 256 | self.status_order = {"pending": 0, "running": 1, "done": 2, "error": 3, "skipped": 4} |
241 | | - self._refresh_cb = None # callable to trigger UI refresh |
| 257 | + self._refresh_cb: Optional[Callable[[], None]] = None |
242 | 258 |
|
243 | | - def attach_refresh(self, cb): |
| 259 | + def attach_refresh(self, cb: Callable[[], None]) -> None: |
244 | 260 | self._refresh_cb = cb |
245 | 261 |
|
246 | 262 | def add(self, key: str, label: str): |
@@ -335,7 +351,7 @@ def get_key(): |
335 | 351 |
|
336 | 352 | return key |
337 | 353 |
|
338 | | -def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str: |
| 354 | +def select_with_arrows(options: dict[str, str], prompt_text: str = "Select an option", default_key: Optional[str] = None) -> str: |
339 | 355 | """ |
340 | 356 | Interactive selection using arrow keys with Rich Live display. |
341 | 357 | |
@@ -469,7 +485,7 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False |
469 | 485 | raise |
470 | 486 | return None |
471 | 487 |
|
472 | | -def check_tool(tool: str, tracker: StepTracker = None) -> bool: |
| 488 | +def check_tool(tool: str, tracker: Optional["StepTracker"] = None) -> bool: |
473 | 489 | """Check if a tool is installed. Optionally update tracker. |
474 | 490 | |
475 | 491 | Args: |
@@ -500,7 +516,7 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool: |
500 | 516 |
|
501 | 517 | return found |
502 | 518 |
|
503 | | -def is_git_repo(path: Path = None) -> bool: |
| 519 | +def is_git_repo(path: Optional[Path] = None) -> bool: |
504 | 520 | """Check if the specified path is inside a git repository.""" |
505 | 521 | if path is None: |
506 | 522 | path = Path.cwd() |
@@ -622,7 +638,7 @@ def deep_merge(base: dict, update: dict) -> dict: |
622 | 638 |
|
623 | 639 | return merged |
624 | 640 |
|
625 | | -def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]: |
| 641 | +def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: Optional[httpx.Client] = None, debug: bool = False, github_token: Optional[str] = None) -> Tuple[Path, dict]: |
626 | 642 | repo_owner = "github" |
627 | 643 | repo_name = "spec-kit" |
628 | 644 | if client is None: |
@@ -736,7 +752,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri |
736 | 752 | } |
737 | 753 | return zip_path, metadata |
738 | 754 |
|
739 | | -def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path: |
| 755 | +def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: Optional["StepTracker"] = None, client: Optional[httpx.Client] = None, debug: bool = False, github_token: Optional[str] = None) -> Path: |
740 | 756 | """Download the latest release and extract it to create a new project. |
741 | 757 | Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) |
742 | 758 | """ |
@@ -927,22 +943,22 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = |
927 | 943 | console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]") |
928 | 944 | if failures: |
929 | 945 | console.print("[yellow]Some scripts could not be updated:[/yellow]") |
930 | | - for f in failures: |
931 | | - console.print(f" - {f}") |
| 946 | + for failure in failures: |
| 947 | + console.print(f" - {failure}") |
932 | 948 |
|
933 | 949 | @app.command() |
934 | 950 | def init( |
935 | | - project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), |
936 | | - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, or q"), |
937 | | - script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), |
| 951 | + project_name: Optional[str] = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), |
| 952 | + ai_assistant: Optional[str] = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, or q"), |
| 953 | + script_type: Optional[str] = typer.Option(None, "--script", help="Script type to use: sh or ps"), |
938 | 954 | ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), |
939 | 955 | no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), |
940 | 956 | here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"), |
941 | 957 | force: bool = typer.Option(False, "--force", help="Force merge/overwrite when using --here (skip confirmation)"), |
942 | 958 | skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"), |
943 | 959 | debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"), |
944 | | - github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"), |
945 | | -): |
| 960 | + github_token: Optional[str] = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"), |
| 961 | +) -> None: |
946 | 962 | """ |
947 | 963 | Initialize a new Specify project from the latest template. |
948 | 964 | |
@@ -998,6 +1014,7 @@ def init( |
998 | 1014 | console.print("[yellow]Operation cancelled[/yellow]") |
999 | 1015 | raise typer.Exit(0) |
1000 | 1016 | else: |
| 1017 | + assert project_name is not None # Validated above |
1001 | 1018 | project_path = Path(project_name).resolve() |
1002 | 1019 | if project_path.exists(): |
1003 | 1020 | error_panel = Panel( |
@@ -1081,8 +1098,6 @@ def init( |
1081 | 1098 |
|
1082 | 1099 | tracker = StepTracker("Initialize Specify Project") |
1083 | 1100 |
|
1084 | | - sys._specify_tracker_active = True |
1085 | | - |
1086 | 1101 | tracker.add("precheck", "Check required tools") |
1087 | 1102 | tracker.complete("precheck", "ok") |
1088 | 1103 | tracker.add("ai-select", "Select AI assistant") |
|
0 commit comments