mirror of
https://github.com/github/spec-kit.git
synced 2026-02-03 06:23:36 +00:00
Cleanup
This commit is contained in:
@@ -64,7 +64,6 @@ def _github_auth_headers(cli_token: str | None = None) -> dict:
|
|||||||
token = _github_token(cli_token)
|
token = _github_token(cli_token)
|
||||||
return {"Authorization": f"Bearer {token}"} if token else {}
|
return {"Authorization": f"Bearer {token}"} if token else {}
|
||||||
|
|
||||||
# Constants
|
|
||||||
AI_CHOICES = {
|
AI_CHOICES = {
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"claude": "Claude Code",
|
"claude": "Claude Code",
|
||||||
@@ -79,13 +78,11 @@ AI_CHOICES = {
|
|||||||
"roo": "Roo Code",
|
"roo": "Roo Code",
|
||||||
"q": "Amazon Q Developer CLI",
|
"q": "Amazon Q Developer CLI",
|
||||||
}
|
}
|
||||||
# Add script type choices
|
|
||||||
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
||||||
|
|
||||||
# Claude CLI local installation path after migrate-installer
|
|
||||||
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
||||||
|
|
||||||
# ASCII Art Banner
|
|
||||||
BANNER = """
|
BANNER = """
|
||||||
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
||||||
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
|
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
|
||||||
@@ -182,40 +179,26 @@ class StepTracker:
|
|||||||
tree.add(line)
|
tree.add(line)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
MINI_BANNER = """
|
|
||||||
╔═╗╔═╗╔═╗╔═╗╦╔═╗╦ ╦
|
|
||||||
╚═╗╠═╝║╣ ║ ║╠╣ ╚╦╝
|
|
||||||
╚═╝╩ ╚═╝╚═╝╩╚ ╩
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_key():
|
def get_key():
|
||||||
"""Get a single keypress in a cross-platform way using readchar."""
|
"""Get a single keypress in a cross-platform way using readchar."""
|
||||||
key = readchar.readkey()
|
key = readchar.readkey()
|
||||||
|
|
||||||
# Arrow keys
|
|
||||||
if key == readchar.key.UP or key == readchar.key.CTRL_P:
|
if key == readchar.key.UP or key == readchar.key.CTRL_P:
|
||||||
return 'up'
|
return 'up'
|
||||||
if key == readchar.key.DOWN or key == readchar.key.CTRL_N:
|
if key == readchar.key.DOWN or key == readchar.key.CTRL_N:
|
||||||
return 'down'
|
return 'down'
|
||||||
|
|
||||||
# Enter/Return
|
|
||||||
if key == readchar.key.ENTER:
|
if key == readchar.key.ENTER:
|
||||||
return 'enter'
|
return 'enter'
|
||||||
|
|
||||||
# Escape
|
|
||||||
if key == readchar.key.ESC:
|
if key == readchar.key.ESC:
|
||||||
return 'escape'
|
return 'escape'
|
||||||
|
|
||||||
# Ctrl+C
|
|
||||||
if key == readchar.key.CTRL_C:
|
if key == readchar.key.CTRL_C:
|
||||||
raise KeyboardInterrupt
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
|
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
Interactive selection using arrow keys with Rich Live display.
|
Interactive selection using arrow keys with Rich Live display.
|
||||||
@@ -292,11 +275,8 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def
|
|||||||
# Suppress explicit selection print; tracker / later logic will report consolidated status
|
# Suppress explicit selection print; tracker / later logic will report consolidated status
|
||||||
return selected_key
|
return selected_key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
class BannerGroup(TyperGroup):
|
class BannerGroup(TyperGroup):
|
||||||
"""Custom group that shows banner before help."""
|
"""Custom group that shows banner before help."""
|
||||||
|
|
||||||
@@ -314,7 +294,6 @@ app = typer.Typer(
|
|||||||
cls=BannerGroup,
|
cls=BannerGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def show_banner():
|
def show_banner():
|
||||||
"""Display the ASCII art banner."""
|
"""Display the ASCII art banner."""
|
||||||
# Create gradient effect with different colors
|
# Create gradient effect with different colors
|
||||||
@@ -330,7 +309,6 @@ def show_banner():
|
|||||||
console.print(Align.center(Text(TAGLINE, style="italic bright_yellow")))
|
console.print(Align.center(Text(TAGLINE, style="italic bright_yellow")))
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def callback(ctx: typer.Context):
|
def callback(ctx: typer.Context):
|
||||||
"""Show banner when no subcommand is provided."""
|
"""Show banner when no subcommand is provided."""
|
||||||
@@ -341,7 +319,6 @@ def callback(ctx: typer.Context):
|
|||||||
console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]"))
|
console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]"))
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
def run_command(cmd: list[str], check_return: bool = True, capture: bool = False, shell: bool = False) -> Optional[str]:
|
def run_command(cmd: list[str], check_return: bool = True, capture: bool = False, shell: bool = False) -> Optional[str]:
|
||||||
"""Run a shell command and optionally capture output."""
|
"""Run a shell command and optionally capture output."""
|
||||||
try:
|
try:
|
||||||
@@ -360,7 +337,6 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
|
|||||||
raise
|
raise
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_tool_for_tracker(tool: str, tracker: StepTracker) -> bool:
|
def check_tool_for_tracker(tool: str, tracker: StepTracker) -> bool:
|
||||||
"""Check if a tool is installed and update tracker."""
|
"""Check if a tool is installed and update tracker."""
|
||||||
if shutil.which(tool):
|
if shutil.which(tool):
|
||||||
@@ -370,7 +346,6 @@ def check_tool_for_tracker(tool: str, tracker: StepTracker) -> bool:
|
|||||||
tracker.error(tool, "not found")
|
tracker.error(tool, "not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_tool(tool: str, install_hint: str) -> bool:
|
def check_tool(tool: str, install_hint: str) -> bool:
|
||||||
"""Check if a tool is installed."""
|
"""Check if a tool is installed."""
|
||||||
|
|
||||||
@@ -388,7 +363,6 @@ def check_tool(tool: str, install_hint: str) -> bool:
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_git_repo(path: Path = None) -> bool:
|
def is_git_repo(path: Path = None) -> bool:
|
||||||
"""Check if the specified path is inside a git repository."""
|
"""Check if the specified path is inside a git repository."""
|
||||||
if path is None:
|
if path is None:
|
||||||
@@ -409,7 +383,6 @@ def is_git_repo(path: Path = None) -> bool:
|
|||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
||||||
"""Initialize a git repository in the specified path.
|
"""Initialize a git repository in the specified path.
|
||||||
quiet: if True suppress console output (tracker handles status)
|
quiet: if True suppress console output (tracker handles status)
|
||||||
@@ -433,7 +406,6 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
|||||||
finally:
|
finally:
|
||||||
os.chdir(original_cwd)
|
os.chdir(original_cwd)
|
||||||
|
|
||||||
|
|
||||||
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]:
|
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]:
|
||||||
repo_owner = "github"
|
repo_owner = "github"
|
||||||
repo_name = "spec-kit"
|
repo_name = "spec-kit"
|
||||||
@@ -545,7 +517,6 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
}
|
}
|
||||||
return zip_path, metadata
|
return zip_path, metadata
|
||||||
|
|
||||||
|
|
||||||
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:
|
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:
|
||||||
"""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)
|
||||||
@@ -775,15 +746,7 @@ def init(
|
|||||||
Examples:
|
Examples:
|
||||||
specify init my-project
|
specify init my-project
|
||||||
specify init my-project --ai claude
|
specify init my-project --ai claude
|
||||||
specify init my-project --ai gemini
|
|
||||||
specify init my-project --ai copilot --no-git
|
specify init my-project --ai copilot --no-git
|
||||||
specify init my-project --ai cursor
|
|
||||||
specify init my-project --ai qwen
|
|
||||||
specify init my-project --ai opencode
|
|
||||||
specify init my-project --ai codex
|
|
||||||
specify init my-project --ai windsurf
|
|
||||||
specify init my-project --ai auggie
|
|
||||||
specify init my-project --ai q
|
|
||||||
specify init --ignore-agent-tools my-project
|
specify init --ignore-agent-tools my-project
|
||||||
specify init . --ai claude # Initialize in current directory
|
specify init . --ai claude # Initialize in current directory
|
||||||
specify init . # Initialize in current directory (interactive AI selection)
|
specify init . # Initialize in current directory (interactive AI selection)
|
||||||
@@ -792,7 +755,7 @@ def init(
|
|||||||
specify init --here
|
specify init --here
|
||||||
specify init --here --force # Skip confirmation when current directory not empty
|
specify init --here --force # Skip confirmation when current directory not empty
|
||||||
"""
|
"""
|
||||||
# Show banner first
|
|
||||||
show_banner()
|
show_banner()
|
||||||
|
|
||||||
# Handle '.' as shorthand for current directory (equivalent to --here)
|
# Handle '.' as shorthand for current directory (equivalent to --here)
|
||||||
@@ -800,7 +763,6 @@ def init(
|
|||||||
here = True
|
here = True
|
||||||
project_name = None # Clear project_name to use existing validation logic
|
project_name = None # Clear project_name to use existing validation logic
|
||||||
|
|
||||||
# Validate arguments
|
|
||||||
if here and project_name:
|
if here and project_name:
|
||||||
console.print("[red]Error:[/red] Cannot specify both project name and --here flag")
|
console.print("[red]Error:[/red] Cannot specify both project name and --here flag")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
@@ -809,12 +771,10 @@ def init(
|
|||||||
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
|
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Determine project directory
|
|
||||||
if here:
|
if here:
|
||||||
project_name = Path.cwd().name
|
project_name = Path.cwd().name
|
||||||
project_path = Path.cwd()
|
project_path = Path.cwd()
|
||||||
|
|
||||||
# Check if current directory has any files
|
|
||||||
existing_items = list(project_path.iterdir())
|
existing_items = list(project_path.iterdir())
|
||||||
if existing_items:
|
if existing_items:
|
||||||
console.print(f"[yellow]Warning:[/yellow] Current directory is not empty ({len(existing_items)} items)")
|
console.print(f"[yellow]Warning:[/yellow] Current directory is not empty ({len(existing_items)} items)")
|
||||||
@@ -822,14 +782,12 @@ def init(
|
|||||||
if force:
|
if force:
|
||||||
console.print("[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]")
|
console.print("[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]")
|
||||||
else:
|
else:
|
||||||
# Ask for confirmation
|
|
||||||
response = typer.confirm("Do you want to continue?")
|
response = typer.confirm("Do you want to continue?")
|
||||||
if not response:
|
if not response:
|
||||||
console.print("[yellow]Operation cancelled[/yellow]")
|
console.print("[yellow]Operation cancelled[/yellow]")
|
||||||
raise typer.Exit(0)
|
raise typer.Exit(0)
|
||||||
else:
|
else:
|
||||||
project_path = Path(project_name).resolve()
|
project_path = Path(project_name).resolve()
|
||||||
# Check if project directory already exists
|
|
||||||
if project_path.exists():
|
if project_path.exists():
|
||||||
error_panel = Panel(
|
error_panel = Panel(
|
||||||
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
|
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
|
||||||
@@ -842,7 +800,6 @@ def init(
|
|||||||
console.print(error_panel)
|
console.print(error_panel)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Create formatted setup info with column alignment
|
|
||||||
current_dir = Path.cwd()
|
current_dir = Path.cwd()
|
||||||
|
|
||||||
setup_lines = [
|
setup_lines = [
|
||||||
@@ -866,7 +823,6 @@ def init(
|
|||||||
if not should_init_git:
|
if not should_init_git:
|
||||||
console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
||||||
|
|
||||||
# AI assistant selection
|
|
||||||
if ai_assistant:
|
if ai_assistant:
|
||||||
if ai_assistant not in AI_CHOICES:
|
if ai_assistant not in AI_CHOICES:
|
||||||
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AI_CHOICES.keys())}")
|
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AI_CHOICES.keys())}")
|
||||||
@@ -1141,10 +1097,8 @@ def check():
|
|||||||
if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or kilocode_ok or opencode_ok or codex_ok or auggie_ok or q_ok):
|
if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or kilocode_ok or opencode_ok or codex_ok or auggie_ok or q_ok):
|
||||||
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app()
|
app()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user