@@ -5346,65 +5346,31 @@ def workflow_step_list():
53465346 project_root = Path .cwd ()
53475347 specify_dir = project_root / ".specify"
53485348
5349- # Load custom steps if in a spec-kit project
5350- custom_keys : set [ str ] = set ()
5349+ # Read installed custom steps from registry only — no dynamic imports
5350+ installed : dict = {}
53515351 if specify_dir .exists ():
5352- from .workflows import load_custom_steps
5353- loaded = load_custom_steps (project_root )
5354- custom_keys .update (loaded )
5355- # Also read registry for metadata
53565352 registry = StepRegistry (project_root )
53575353 installed = registry .list ()
5358- custom_keys .update (installed .keys ())
53595354
53605355 console .print ("\n [bold cyan]Installed Step Types:[/bold cyan]\n " )
53615356
5362- built_in : list [str ] = []
5363- custom : list [str ] = []
5364- for key in sorted (STEP_REGISTRY .keys ()):
5365- if key in custom_keys :
5366- custom .append (key )
5367- else :
5368- built_in .append (key )
5369-
5357+ built_in = sorted (k for k in STEP_REGISTRY if k not in installed )
53705358 if built_in :
53715359 console .print (" [bold]Built-in:[/bold]" )
53725360 for key in built_in :
53735361 console .print (f" • { key } " )
53745362 console .print ()
53755363
5376- if custom :
5364+ if installed :
53775365 console .print (" [bold]Custom (installed):[/bold]" )
5378- if specify_dir .exists ():
5379- registry = StepRegistry (project_root )
5380- for key in custom :
5381- meta = registry .get (key ) or {}
5382- name = meta .get ("name" , key )
5383- version = meta .get ("version" , "?" )
5384- console .print (f" • [bold]{ name } [/bold] ({ key } ) v{ version } " )
5385- else :
5386- for key in custom :
5387- console .print (f" • { key } " )
5366+ for key in sorted (installed ):
5367+ meta = installed [key ] or {}
5368+ name = meta .get ("name" , key )
5369+ version = meta .get ("version" , "?" )
5370+ console .print (f" • [bold]{ name } [/bold] ({ key } ) v{ version } " )
53885371 console .print ()
53895372
5390- # Show custom steps that are installed but failed to load
5391- if specify_dir .exists ():
5392- registry = StepRegistry (project_root )
5393- installed = registry .list ()
5394- failed = [
5395- k for k in sorted (installed .keys ())
5396- if k not in STEP_REGISTRY
5397- ]
5398- if failed :
5399- console .print (" [bold yellow]Custom (installed, failed to load):[/bold yellow]" )
5400- for key in failed :
5401- meta = installed .get (key , {})
5402- name = meta .get ("name" , key )
5403- version = meta .get ("version" , "?" )
5404- console .print (f" • [dim]{ name } [/dim] ({ key } ) v{ version } " )
5405- console .print ()
5406-
5407- if not built_in and not custom :
5373+ if not built_in and not installed :
54085374 console .print ("[yellow]No step types found.[/yellow]" )
54095375
54105376 if specify_dir .exists ():
@@ -5505,48 +5471,55 @@ def _safe_fetch(url: str) -> bytes:
55055471 except ValueError :
55065472 console .print (f"[red]Error:[/red] Invalid step id '{ step_id } '" )
55075473 raise typer .Exit (1 )
5508- step_dir .mkdir (parents = True , exist_ok = True )
55095474
5510- try :
5511- step_yml_content = _safe_fetch (step_yml_url )
5512- init_py_content = _safe_fetch (init_url )
5513- except Exception as exc :
5514- import shutil
5515- shutil .rmtree (step_dir , ignore_errors = True )
5516- console .print (f"[red]Error:[/red] Failed to download step files: { exc } " )
5517- raise typer .Exit (1 )
5475+ import shutil
5476+ import tempfile
55185477
5519- # Validate step.yml
5478+ # Download and validate in a temp directory first; only move to the final
5479+ # location on success so a transient failure can never corrupt or delete a
5480+ # pre-existing directory at step_dir.
5481+ tmp_path = Path (tempfile .mkdtemp (prefix = "speckit_step_" ))
5482+ _install_success = False
55205483 try :
5521- import yaml as _yaml
5484+ try :
5485+ step_yml_content = _safe_fetch (step_yml_url )
5486+ init_py_content = _safe_fetch (init_url )
5487+ except Exception as exc :
5488+ console .print (f"[red]Error:[/red] Failed to download step files: { exc } " )
5489+ raise typer .Exit (1 )
55225490
5523- meta = _yaml .safe_load (step_yml_content .decode ("utf-8" )) or {}
5524- except Exception as exc :
5525- import shutil
5526- shutil .rmtree (step_dir , ignore_errors = True )
5527- console .print (f"[red]Error:[/red] Invalid step.yml: { exc } " )
5528- raise typer .Exit (1 )
5491+ # Validate step.yml
5492+ try :
5493+ import yaml as _yaml
55295494
5530- step_meta = meta .get ("step" , {})
5531- type_key = step_meta .get ("type_key" , "" )
5532- if not type_key :
5533- import shutil
5534- shutil .rmtree (step_dir , ignore_errors = True )
5535- console .print ("[red]Error:[/red] step.yml missing 'step.type_key' field" )
5536- raise typer .Exit (1 )
5495+ meta = _yaml .safe_load (step_yml_content .decode ("utf-8" )) or {}
5496+ except Exception as exc :
5497+ console .print (f"[red]Error:[/red] Invalid step.yml: { exc } " )
5498+ raise typer .Exit (1 )
55375499
5538- if type_key != step_id :
5539- import shutil
5540- shutil .rmtree (step_dir , ignore_errors = True )
5541- console .print (
5542- f"[red]Error:[/red] step.yml type_key ({ type_key !r} ) does not match "
5543- f"catalog ID ({ step_id !r} )"
5544- )
5545- raise typer .Exit (1 )
5500+ step_meta = meta .get ("step" , {})
5501+ type_key = step_meta .get ("type_key" , "" )
5502+ if not type_key :
5503+ console .print ("[red]Error:[/red] step.yml missing 'step.type_key' field" )
5504+ raise typer .Exit (1 )
55465505
5547- # Write files
5548- (step_dir / "step.yml" ).write_bytes (step_yml_content )
5549- (step_dir / "__init__.py" ).write_bytes (init_py_content )
5506+ if type_key != step_id :
5507+ console .print (
5508+ f"[red]Error:[/red] step.yml type_key ({ type_key !r} ) does not match "
5509+ f"catalog ID ({ step_id !r} )"
5510+ )
5511+ raise typer .Exit (1 )
5512+
5513+ # Write validated files to temp location, then move atomically
5514+ (tmp_path / "step.yml" ).write_bytes (step_yml_content )
5515+ (tmp_path / "__init__.py" ).write_bytes (init_py_content )
5516+
5517+ steps_base_dir .mkdir (parents = True , exist_ok = True )
5518+ shutil .move (str (tmp_path ), str (step_dir ))
5519+ _install_success = True
5520+ finally :
5521+ if not _install_success :
5522+ shutil .rmtree (tmp_path , ignore_errors = True )
55505523
55515524 # Register in step registry
55525525 registry = StepRegistry (project_root )
@@ -5659,10 +5632,6 @@ def workflow_step_info(
56595632 console .print ("[red]Error:[/red] Not a spec-kit project (no .specify/ directory)" )
56605633 raise typer .Exit (1 )
56615634
5662- # Load custom steps so registry is up to date
5663- from .workflows import load_custom_steps
5664- load_custom_steps (project_root )
5665-
56665635 registry = StepRegistry (project_root )
56675636 installed_meta = registry .get (step_id )
56685637
0 commit comments