@@ -146,10 +146,6 @@ def _github_auth_headers(cli_token: str | None = None) -> dict:
146146 },
147147}
148148
149- # Derived dictionaries for backward compatibility
150- AI_CHOICES = {key : config ["name" ] for key , config in AGENT_CONFIG .items ()}
151- AGENT_FOLDER_MAP = {key : config ["folder" ] for key , config in AGENT_CONFIG .items ()}
152-
153149SCRIPT_TYPE_CHOICES = {"sh" : "POSIX Shell (bash/zsh)" , "ps" : "PowerShell" }
154150
155151CLAUDE_LOCAL_PATH = Path .home () / ".claude" / "local" / "claude"
@@ -202,7 +198,7 @@ def _update(self, key: str, status: str, detail: str):
202198 s ["detail" ] = detail
203199 self ._maybe_refresh ()
204200 return
205- # If not present, add it
201+
206202 self .steps .append ({"key" : key , "label" : key , "status" : status , "detail" : detail })
207203 self ._maybe_refresh ()
208204
@@ -219,7 +215,6 @@ def render(self):
219215 label = step ["label" ]
220216 detail_text = step ["detail" ].strip () if step ["detail" ] else ""
221217
222- # Circles (unchanged styling)
223218 status = step ["status" ]
224219 if status == "done" :
225220 symbol = "[green]●[/green]"
@@ -343,7 +338,6 @@ def run_selection_loop():
343338 console .print ("\n [red]Selection failed.[/red]" )
344339 raise typer .Exit (1 )
345340
346- # Suppress explicit selection print; tracker / later logic will report consolidated status
347341 return selected_key
348342
349343console = Console ()
@@ -367,7 +361,6 @@ def format_help(self, ctx, formatter):
367361
368362def show_banner ():
369363 """Display the ASCII art banner."""
370- # Create gradient effect with different colors
371364 banner_lines = BANNER .strip ().split ('\n ' )
372365 colors = ["bright_blue" , "blue" , "cyan" , "bright_cyan" , "white" , "bright_white" ]
373366
@@ -383,8 +376,6 @@ def show_banner():
383376@app .callback ()
384377def callback (ctx : typer .Context ):
385378 """Show banner when no subcommand is provided."""
386- # Show banner only when no subcommand and no help flag
387- # (help is handled by BannerGroup)
388379 if ctx .invoked_subcommand is None and "--help" not in sys .argv and "-h" not in sys .argv :
389380 show_banner ()
390381 console .print (Align .center ("[dim]Run 'specify --help' for usage information[/dim]" ))
@@ -515,7 +506,6 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
515506 console .print (Panel (str (e ), title = "Fetch Error" , border_style = "red" ))
516507 raise typer .Exit (1 )
517508
518- # Find the template asset for the specified AI assistant
519509 assets = release_data .get ("assets" , [])
520510 pattern = f"spec-kit-template-{ ai_assistant } -{ script_type } "
521511 matching_assets = [
@@ -600,7 +590,6 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
600590 """
601591 current_dir = Path .cwd ()
602592
603- # Step: fetch + download combined
604593 if tracker :
605594 tracker .start ("fetch" , "contacting GitHub API" )
606595 try :
@@ -633,34 +622,29 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
633622 console .print ("Extracting template..." )
634623
635624 try :
636- # Create project directory only if not using current directory
637625 if not is_current_dir :
638626 project_path .mkdir (parents = True )
639627
640628 with zipfile .ZipFile (zip_path , 'r' ) as zip_ref :
641- # List all files in the ZIP for debugging
642629 zip_contents = zip_ref .namelist ()
643630 if tracker :
644631 tracker .start ("zip-list" )
645632 tracker .complete ("zip-list" , f"{ len (zip_contents )} entries" )
646633 elif verbose :
647634 console .print (f"[cyan]ZIP contains { len (zip_contents )} items[/cyan]" )
648635
649- # For current directory, extract to a temp location first
650636 if is_current_dir :
651637 with tempfile .TemporaryDirectory () as temp_dir :
652638 temp_path = Path (temp_dir )
653639 zip_ref .extractall (temp_path )
654640
655- # Check what was extracted
656641 extracted_items = list (temp_path .iterdir ())
657642 if tracker :
658643 tracker .start ("extracted-summary" )
659644 tracker .complete ("extracted-summary" , f"temp { len (extracted_items )} items" )
660645 elif verbose :
661646 console .print (f"[cyan]Extracted { len (extracted_items )} items to temp location[/cyan]" )
662647
663- # Handle GitHub-style ZIP with a single root directory
664648 source_dir = temp_path
665649 if len (extracted_items ) == 1 and extracted_items [0 ].is_dir ():
666650 source_dir = extracted_items [0 ]
@@ -670,14 +654,12 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
670654 elif verbose :
671655 console .print (f"[cyan]Found nested directory structure[/cyan]" )
672656
673- # Copy contents to current directory
674657 for item in source_dir .iterdir ():
675658 dest_path = project_path / item .name
676659 if item .is_dir ():
677660 if dest_path .exists ():
678661 if verbose and not tracker :
679662 console .print (f"[yellow]Merging directory:[/yellow] { item .name } " )
680- # Recursively copy directory contents
681663 for sub_item in item .rglob ('*' ):
682664 if sub_item .is_file ():
683665 rel_path = sub_item .relative_to (item )
@@ -693,10 +675,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
693675 if verbose and not tracker :
694676 console .print (f"[cyan]Template files merged into current directory[/cyan]" )
695677 else :
696- # Extract directly to project directory (original behavior)
697678 zip_ref .extractall (project_path )
698679
699- # Check what was extracted
700680 extracted_items = list (project_path .iterdir ())
701681 if tracker :
702682 tracker .start ("extracted-summary" )
@@ -706,16 +686,14 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
706686 for item in extracted_items :
707687 console .print (f" - { item .name } ({ 'dir' if item .is_dir () else 'file' } )" )
708688
709- # Handle GitHub-style ZIP with a single root directory
710689 if len (extracted_items ) == 1 and extracted_items [0 ].is_dir ():
711- # Move contents up one level
712690 nested_dir = extracted_items [0 ]
713691 temp_move_dir = project_path .parent / f"{ project_path .name } _temp"
714- # Move the nested directory contents to temp location
692+
715693 shutil .move (str (nested_dir ), str (temp_move_dir ))
716- # Remove the now-empty project directory
694+
717695 project_path .rmdir ()
718- # Rename temp directory to project directory
696+
719697 shutil .move (str (temp_move_dir ), str (project_path ))
720698 if tracker :
721699 tracker .add ("flatten" , "Flatten nested directory" )
@@ -731,7 +709,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
731709 console .print (f"[red]Error extracting template:[/red] { e } " )
732710 if debug :
733711 console .print (Panel (str (e ), title = "Extraction Error" , border_style = "red" ))
734- # Clean up project directory if created and not current directory
712+
735713 if not is_current_dir and project_path .exists ():
736714 shutil .rmtree (project_path )
737715 raise typer .Exit (1 )
@@ -741,7 +719,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
741719 finally :
742720 if tracker :
743721 tracker .add ("cleanup" , "Remove temporary archive" )
744- # Clean up downloaded ZIP file
722+
745723 if zip_path .exists ():
746724 zip_path .unlink ()
747725 if tracker :
@@ -836,7 +814,6 @@ def init(
836814
837815 show_banner ()
838816
839- # Handle '.' as shorthand for current directory (equivalent to --here)
840817 if project_name == "." :
841818 here = True
842819 project_name = None # Clear project_name to use existing validation logic
@@ -887,47 +864,44 @@ def init(
887864 f"{ 'Working Path' :<15} [dim]{ current_dir } [/dim]" ,
888865 ]
889866
890- # Add target path only if different from working dir
891867 if not here :
892868 setup_lines .append (f"{ 'Target Path' :<15} [dim]{ project_path } [/dim]" )
893869
894870 console .print (Panel ("\n " .join (setup_lines ), border_style = "cyan" , padding = (1 , 2 )))
895871
896- # Check git only if we might need it (not --no-git)
897- # Only set to True if the user wants it and the tool is available
898872 should_init_git = False
899873 if not no_git :
900874 should_init_git = check_tool ("git" , "https://git-scm.com/downloads" )
901875 if not should_init_git :
902876 console .print ("[yellow]Git not found - will skip repository initialization[/yellow]" )
903877
904878 if ai_assistant :
905- if ai_assistant not in AI_CHOICES :
906- console .print (f"[red]Error:[/red] Invalid AI assistant '{ ai_assistant } '. Choose from: { ', ' .join (AI_CHOICES .keys ())} " )
879+ if ai_assistant not in AGENT_CONFIG :
880+ console .print (f"[red]Error:[/red] Invalid AI assistant '{ ai_assistant } '. Choose from: { ', ' .join (AGENT_CONFIG .keys ())} " )
907881 raise typer .Exit (1 )
908882 selected_ai = ai_assistant
909883 else :
910- # Use arrow-key selection interface
884+ # Create options dict for selection (agent_key: display_name)
885+ ai_choices = {key : config ["name" ] for key , config in AGENT_CONFIG .items ()}
911886 selected_ai = select_with_arrows (
912- AI_CHOICES ,
887+ ai_choices ,
913888 "Choose your AI assistant:" ,
914889 "copilot"
915890 )
916891
917- # Check agent tools unless ignored
918892 if not ignore_agent_tools :
919893 agent_config = AGENT_CONFIG .get (selected_ai )
920894 if agent_config and agent_config ["requires_cli" ]:
921895 cli_tool = selected_ai
922896 if selected_ai == "cursor" :
923897 cli_tool = "cursor-agent"
924-
898+
925899 install_url = agent_config ["install_url" ]
926900 if not check_tool (cli_tool , install_url ):
927901 error_panel = Panel (
928902 f"[cyan]{ selected_ai } [/cyan] not found\n "
929903 f"Install from: [cyan]{ install_url } [/cyan]\n "
930- f"{ AI_CHOICES [ selected_ai ]} is required to continue with this project type.\n \n "
904+ f"{ agent_config [ 'name' ]} is required to continue with this project type.\n \n "
931905 "Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check" ,
932906 title = "[red]Agent Detection Error[/red]" ,
933907 border_style = "red" ,
@@ -937,16 +911,14 @@ def init(
937911 console .print (error_panel )
938912 raise typer .Exit (1 )
939913
940- # Determine script type (explicit, interactive, or OS default)
941914 if script_type :
942915 if script_type not in SCRIPT_TYPE_CHOICES :
943916 console .print (f"[red]Error:[/red] Invalid script type '{ script_type } '. Choose from: { ', ' .join (SCRIPT_TYPE_CHOICES .keys ())} " )
944917 raise typer .Exit (1 )
945918 selected_script = script_type
946919 else :
947- # Auto-detect default
948920 default_script = "ps" if os .name == "nt" else "sh"
949- # Provide interactive selection similar to AI if stdin is a TTY
921+
950922 if sys .stdin .isatty ():
951923 selected_script = select_with_arrows (SCRIPT_TYPE_CHOICES , "Choose script type (or press Enter)" , default_script )
952924 else :
@@ -955,12 +927,10 @@ def init(
955927 console .print (f"[cyan]Selected AI assistant:[/cyan] { selected_ai } " )
956928 console .print (f"[cyan]Selected script type:[/cyan] { selected_script } " )
957929
958- # Download and set up project
959- # New tree-based progress (no emojis); include earlier substeps
960930 tracker = StepTracker ("Initialize Specify Project" )
961- # Flag to allow suppressing legacy headings
931+
962932 sys ._specify_tracker_active = True
963- # Pre steps recorded as completed before live rendering
933+
964934 tracker .add ("precheck" , "Check required tools" )
965935 tracker .complete ("precheck" , "ok" )
966936 tracker .add ("ai-select" , "Select AI assistant" )
@@ -980,21 +950,17 @@ def init(
980950 ]:
981951 tracker .add (key , label )
982952
983- # Use transient so live tree is replaced by the final static render (avoids duplicate output)
984953 with Live (tracker .render (), console = console , refresh_per_second = 8 , transient = True ) as live :
985954 tracker .attach_refresh (lambda : live .update (tracker .render ()))
986955 try :
987- # Create a httpx client with verify based on skip_tls
988956 verify = not skip_tls
989957 local_ssl_context = ssl_context if verify else False
990958 local_client = httpx .Client (verify = local_ssl_context )
991959
992960 download_and_extract_template (project_path , selected_ai , selected_script , here , verbose = False , tracker = tracker , client = local_client , debug = debug , github_token = github_token )
993961
994- # Ensure scripts are executable (POSIX)
995962 ensure_executable_scripts (project_path , tracker = tracker )
996963
997- # Git step
998964 if not no_git :
999965 tracker .start ("git" )
1000966 if is_git_repo (project_path ):
@@ -1026,16 +992,15 @@ def init(
1026992 shutil .rmtree (project_path )
1027993 raise typer .Exit (1 )
1028994 finally :
1029- # Force final render
1030995 pass
1031996
1032- # Final static tree (ensures finished state visible after Live context ends)
1033997 console .print (tracker .render ())
1034998 console .print ("\n [bold green]Project ready.[/bold green]" )
1035999
10361000 # Agent folder security notice
1037- if selected_ai in AGENT_FOLDER_MAP :
1038- agent_folder = AGENT_FOLDER_MAP [selected_ai ]
1001+ agent_config = AGENT_CONFIG .get (selected_ai )
1002+ if agent_config :
1003+ agent_folder = agent_config ["folder" ]
10391004 security_notice = Panel (
10401005 f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n "
10411006 f"Consider adding [cyan]{ agent_folder } [/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage." ,
@@ -1046,7 +1011,6 @@ def init(
10461011 console .print ()
10471012 console .print (security_notice )
10481013
1049- # Boxed "Next steps" section
10501014 steps_lines = []
10511015 if not here :
10521016 steps_lines .append (f"1. Go to the project folder: [cyan]cd { project_name } [/cyan]" )
0 commit comments