Skip to content

Commit 0f3b96e

Browse files
feat: implement multi-agent support and integration switching
1 parent f5cf4cc commit 0f3b96e

4 files changed

Lines changed: 82 additions & 174 deletions

File tree

presets/community/architect-preview/preset.yml

Lines changed: 0 additions & 6 deletions
This file was deleted.

presets/community/architect-preview/scripts/impact_analyzer.py

Lines changed: 0 additions & 140 deletions
This file was deleted.

presets/community/architect-preview/templates/commands/preview.md

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/specify_cli/__init__.py

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -816,16 +816,32 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
816816

817817

818818
def 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

830846
def 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

Comments
 (0)