@@ -816,16 +816,32 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
816816
817817
818818def save_init_options (project_path : Path , options : dict [str , Any ]) -> None :
819- """Persist the CLI options used during ``specify init``.
820-
821- Writes a small JSON file to ``.specify/init-options.json`` so that
822- later operations (e.g. preset install) can adapt their behaviour
823- without scanning the filesystem.
824- """
825819 dest = project_path / INIT_OPTIONS_FILE
826820 dest .parent .mkdir (parents = True , exist_ok = True )
827- dest .write_text (json .dumps (options , indent = 2 , sort_keys = True ))
821+
822+ existing_data = {}
823+ if dest .exists ():
824+ try :
825+ existing_data = json .loads (dest .read_text ())
826+ except Exception :
827+ pass
828828
829+ new_ai = options .get ("ai" )
830+ installed = existing_data .get ("installed_integrations" , [])
831+
832+ if not installed and "ai" in existing_data :
833+ installed .append (existing_data ["ai" ])
834+
835+ if new_ai and new_ai not in installed :
836+ installed .append (new_ai )
837+
838+ final_options = {** existing_data , ** options }
839+
840+ final_options ["default_integration" ] = new_ai
841+ final_options ["installed_integrations" ] = installed
842+ final_options ["ai" ] = new_ai # Current active agent
843+
844+ dest .write_text (json .dumps (final_options , indent = 2 , sort_keys = True ))
829845
830846def load_init_options (project_path : Path ) -> dict [str , Any ]:
831847 """Load the init options previously saved by ``specify init``.
@@ -1045,17 +1061,20 @@ def init(
10451061 console .print ("[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]" )
10461062 console .print (f"[cyan]--force supplied: merging into existing directory '[cyan]{ project_name } [/cyan]'[/cyan]" )
10471063 else :
1048- error_panel = Panel (
1049- f"Directory '[cyan]{ project_name } [/cyan]' already exists\n "
1050- "Please choose a different project name or remove the existing directory.\n "
1051- "Use [bold]--force[/bold] to merge into the existing directory." ,
1052- title = "[red]Directory Conflict[/red]" ,
1053- border_style = "red" ,
1054- padding = (1 , 2 )
1055- )
1056- console .print ()
1057- console .print (error_panel )
1058- raise typer .Exit (1 )
1064+ if (project_path / ".specify" ).exists ():
1065+ console .print (f"[cyan]Project folder detected. Adding new integration/agent to existing setup...[/cyan]" )
1066+ elif existing_items :
1067+ if force :
1068+ console .print (f"[cyan]--force supplied: merging into existing directory '[cyan]{ project_name } [/cyan]'[/cyan]" )
1069+ else :
1070+ error_panel = Panel (
1071+ f"Directory '[cyan]{ project_name } [/cyan]' already exists and is not empty.\n "
1072+ "Use [bold]--force[/bold] to initialize anyway." ,
1073+ title = "[red]Directory Conflict[/red]" ,
1074+ border_style = "red"
1075+ )
1076+ console .print (error_panel )
1077+ raise typer .Exit (1 )
10591078
10601079 if ai_assistant :
10611080 if ai_assistant not in AGENT_CONFIG :
@@ -1533,6 +1552,51 @@ def version():
15331552 console .print (panel )
15341553 console .print ()
15351554
1555+ @app .command ()
1556+ def list_integrations (project_path : Path = typer .Option (Path .cwd (), "--path" )):
1557+ """List all installed integrations in the project."""
1558+ options = load_init_options (project_path )
1559+ installed = options .get ("installed_integrations" , [])
1560+ active = options .get ("ai" )
1561+
1562+ if not installed :
1563+ console .print ("[yellow]No integrations installed yet.[/yellow]" )
1564+ return
1565+
1566+ console .print ("\n [bold cyan]Installed Integrations:[/bold cyan]" )
1567+ for agent in installed :
1568+ status = "[green](active)[/green]" if agent == active else ""
1569+ console .print (f" - { agent } { status } " )
1570+
1571+ @app .command ()
1572+ def use (
1573+ integration : str = typer .Argument (..., help = "The integration/agent to switch to (e.g., claude, codex)" ),
1574+ project_path : Path = typer .Option (Path .cwd (), "--path" , help = "Project directory path" )
1575+ ):
1576+ """
1577+ Switch the active AI integration for the project.
1578+ """
1579+ options = load_init_options (project_path )
1580+ installed = options .get ("installed_integrations" , [])
1581+
1582+ if not installed :
1583+ if "ai" in options :
1584+ installed = [options ["ai" ]]
1585+ else :
1586+ console .print ("[red]Error:[/red] No integrations found in this project." )
1587+ raise typer .Exit (1 )
1588+
1589+ if integration not in installed :
1590+ console .print (f"[red]Error:[/red] Integration '{ integration } ' is not installed." )
1591+ console .print (f"[yellow]Installed integrations:[/yellow] { ', ' .join (installed )} " )
1592+ console .print (f"[dim]Hint: Run 'specify init --ai { integration } ' to install it first.[/dim]" )
1593+ raise typer .Exit (1 )
1594+
1595+ options ["ai" ] = integration
1596+ options ["default_integration" ] = integration
1597+
1598+ save_init_options (project_path , options )
1599+ console .print (f"[green]✓[/green] Switched to [bold]{ integration } [/bold] as the active integration." )
15361600
15371601# ===== Extension Commands =====
15381602
0 commit comments