From 32205c36678721296dcfa21e8c21249c2ffa0420 Mon Sep 17 00:00:00 2001 From: UmmeHabiba1312 Date: Mon, 13 Apr 2026 02:24:08 +0500 Subject: [PATCH 1/4] feat: add architectural impact analyzer utility and spec --- presets/lean/commands/speckit.preview.md | 8 ++ scripts/impact_analyzer.py | 143 +++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 presets/lean/commands/speckit.preview.md create mode 100644 scripts/impact_analyzer.py diff --git a/presets/lean/commands/speckit.preview.md b/presets/lean/commands/speckit.preview.md new file mode 100644 index 0000000000..a7ff850130 --- /dev/null +++ b/presets/lean/commands/speckit.preview.md @@ -0,0 +1,8 @@ +--- +description: Preview the architectural impact and risks of a proposed change across all specifications. +--- + +## User Input + +```text +$ARGUMENTS (The description of the proposed change) \ No newline at end of file diff --git a/scripts/impact_analyzer.py b/scripts/impact_analyzer.py new file mode 100644 index 0000000000..e0caa42b4d --- /dev/null +++ b/scripts/impact_analyzer.py @@ -0,0 +1,143 @@ +import os +import json +import argparse +from openai import OpenAI +from dotenv import load_dotenv +load_dotenv() + +def setup_client(): + """ + Initializes the AI client using standard environment variables. + Defaults to OpenAI, but can be overridden via AI_BASE_URL. + """ + api_key = os.getenv("AI_API_KEY") + base_url = os.getenv("AI_BASE_URL", "https://api.openai.com/v1") + + if not api_key: + raise ValueError("CRITICAL ERROR: AI_API_KEY environment variable is not set.") + + return OpenAI(api_key=api_key, base_url=base_url) + +def load_project_context(target_dir): + """ + Aggregates content from all markdown files in the specified directory + to provide the LLM with full architectural context. + """ + context_accumulator = [] + if not os.path.exists(target_dir): + return None + + for filename in os.listdir(target_dir): + if filename.endswith(".md"): + file_path = os.path.join(target_dir, filename) + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + context_accumulator.append(f"--- FILE: {filename} ---\n{content}") + except Exception as e: + print(f"[!] Warning: Could not read {filename}: {e}") + + return "\n\n".join(context_accumulator) + +def perform_impact_analysis(client, context, change_request, model_id): + """ + Executes the analysis using the specified LLM and returns a structured JSON report. + """ + system_prompt = ( + "You are a Senior Systems Architect. Analyze the impact of a proposed change " + "on the provided technical specifications and output a structured JSON report." + ) + + user_prompt = f""" + ### PROJECT CONTEXT: + {context} + + ### PROPOSED CHANGE: + "{change_request}" + + ### OUTPUT REQUIREMENTS: + Return a JSON object with EXACTLY these keys: + - complexity_score_diff: (int 1-10) + - estimated_hours_delta: (str) + - affected_files: (list of filenames) + - technical_tasks: (list of strings) + - architecture_risks: (list of strings) + - executive_summary: (str) + """ + + try: + completion = client.chat.completions.create( + model=model_id, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + ) + return completion.choices[0].message.content + except Exception as e: + return json.dumps({"error": str(e)}) + +def render_architect_report(raw_json): + """ + Parses and renders the architectural report in a clean, professional format. + """ + try: + clean_json = raw_json.strip().replace("```json", "").replace("```", "") + data = json.loads(clean_json) + + if "error" in data: + print(f"[!] Analysis Failed: {data['error']}") + return + + score = data.get("complexity_score_diff", 0) + status = "CRITICAL/HIGH" if score >= 7 else "MODERATE" if score >= 4 else "LOW" + + print("\n" + "="*60) + print(" SYSTEM ARCHITECT IMPACT ANALYSIS") + print("="*60) + print(f" IMPACT LEVEL : {status} (Score: {score}/10)") + print(f" EST. EFFORT : {data.get('estimated_hours_delta')}") + + print(f"\n [ ] TARGETED FILES:") + for f in data.get('affected_files', []): print(f" * {f}") + + print(f"\n [ ] REQUIRED TASKS:") + for task in data.get('technical_tasks', []): print(f" - {task}") + + print(f"\n [!] ARCHITECTURAL RISKS:") + for risk in data.get('architecture_risks', []): print(f" ! {risk}") + + print(f"\n [*] SUMMARY: {data.get('executive_summary')}") + print("="*60 + "\n") + + except Exception: + print("[!] Error: Could not parse AI response as valid JSON.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Spec-Kit Architectural Impact Previewer") + parser.add_argument("--change", required=True, help="Description of the change request") + parser.add_argument("--model", default=os.getenv("AI_MODEL_ID"), help="Model ID to invoke") + + args = parser.parse_args() + + # Dynamic path resolution to ensure it works from root + script_dir = os.path.dirname(os.path.abspath(__file__)) + default_presets = os.path.abspath(os.path.join(script_dir, "..", "presets", "lean", "commands")) + search_path = os.getenv("SPECKIT_PRESETS_DIR", default_presets) + + try: + ai_client = setup_client() + project_context = load_project_context(search_path) + + if not project_context: + print(f"[!] Path Error: No specification files found at {search_path}") + else: + if not args.model: + print("[!] Configuration Error: No Model ID provided via --model or AI_MODEL_ID.") + else: + print(f"[*] Analyzing global impact for: '{args.change[:50]}...'") + raw_report = perform_impact_analysis(ai_client, project_context, args.change, args.model) + render_architect_report(raw_report) + + except Exception as error: + print(f"[!] Runtime Exception: {error}") \ No newline at end of file From 2de2a79660c35b14136a13105c37e7007f63a3d2 Mon Sep 17 00:00:00 2001 From: UmmeHabiba1312 Date: Mon, 13 Apr 2026 17:36:26 +0500 Subject: [PATCH 2/4] feat: add architectural impact preview command with specialized python analyzer --- scripts/impact_analyzer.py | 39 +++++++++---------- src/specify_cli/__init__.py | 1 + .../integrations/claude/__init__.py | 1 + .../commands/preview.md | 4 +- 4 files changed, 23 insertions(+), 22 deletions(-) rename presets/lean/commands/speckit.preview.md => templates/commands/preview.md (54%) diff --git a/scripts/impact_analyzer.py b/scripts/impact_analyzer.py index e0caa42b4d..2e0bcb9eb5 100644 --- a/scripts/impact_analyzer.py +++ b/scripts/impact_analyzer.py @@ -6,17 +6,12 @@ load_dotenv() def setup_client(): - """ - Initializes the AI client using standard environment variables. - Defaults to OpenAI, but can be overridden via AI_BASE_URL. - """ - api_key = os.getenv("AI_API_KEY") - base_url = os.getenv("AI_BASE_URL", "https://api.openai.com/v1") - + api_key = os.getenv("AI_API_KEY") or os.getenv("ANTHROPIC_API_KEY") or os.getenv("OPENAI_API_KEY") + base_url = os.getenv("AI_BASE_URL") if not api_key: - raise ValueError("CRITICAL ERROR: AI_API_KEY environment variable is not set.") - - return OpenAI(api_key=api_key, base_url=base_url) + raise ValueError("AI API Key not found. Please set AI_API_KEY or ensure your LLM provider is configured.") + + return OpenAI(api_key=api_key, base_url=base_url if base_url else "https://api.openai.com/v1") def load_project_context(target_dir): """ @@ -116,14 +111,19 @@ def render_architect_report(raw_json): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Spec-Kit Architectural Impact Previewer") parser.add_argument("--change", required=True, help="Description of the change request") - parser.add_argument("--model", default=os.getenv("AI_MODEL_ID"), help="Model ID to invoke") + parser.add_argument("--model", default=os.getenv("AI_MODEL_ID", "gpt-4o"), help="Model ID to invoke") args = parser.parse_args() - # Dynamic path resolution to ensure it works from root - script_dir = os.path.dirname(os.path.abspath(__file__)) - default_presets = os.path.abspath(os.path.join(script_dir, "..", "presets", "lean", "commands")) - search_path = os.getenv("SPECKIT_PRESETS_DIR", default_presets) + def get_search_path(): + project_specify = os.path.join(os.getcwd(), ".specify", "memory") + if os.path.exists(project_specify): + return project_specify + + script_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.abspath(os.path.join(script_dir, "..", "presets", "lean", "commands")) + + search_path = os.getenv("SPECKIT_PRESETS_DIR", get_search_path()) try: ai_client = setup_client() @@ -132,12 +132,9 @@ def render_architect_report(raw_json): if not project_context: print(f"[!] Path Error: No specification files found at {search_path}") else: - if not args.model: - print("[!] Configuration Error: No Model ID provided via --model or AI_MODEL_ID.") - else: - print(f"[*] Analyzing global impact for: '{args.change[:50]}...'") - raw_report = perform_impact_analysis(ai_client, project_context, args.change, args.model) - render_architect_report(raw_report) + print(f"[*] Analyzing global impact for: '{args.change[:50]}...'") + raw_report = perform_impact_analysis(ai_client, project_context, args.change, args.model) + render_architect_report(raw_report) except Exception as error: print(f"[!] Runtime Exception: {error}") \ No newline at end of file diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 0bbf42ad5a..004bb12f2b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -862,6 +862,7 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path: "plan": "Generate technical implementation plans from feature specifications.", "tasks": "Break down implementation plans into actionable task lists.", "implement": "Execute all tasks from the task breakdown to build the feature.", + "preview": "Predict the architectural impact, complexity, and risks of proposed changes.", "analyze": "Perform cross-artifact consistency analysis across spec.md, plan.md, and tasks.md.", "clarify": "Structured clarification workflow for underspecified requirements.", "constitution": "Create or update project governing principles and development guidelines.", diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 31972c4b0e..8af86a824c 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -17,6 +17,7 @@ "plan": "Optional guidance for the planning phase", "tasks": "Optional task generation constraints", "implement": "Optional implementation guidance or task filter", + "preview": "Predict the architectural impact, complexity, and risks of proposed changes.", "analyze": "Optional focus areas for analysis", "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", diff --git a/presets/lean/commands/speckit.preview.md b/templates/commands/preview.md similarity index 54% rename from presets/lean/commands/speckit.preview.md rename to templates/commands/preview.md index a7ff850130..45ef865087 100644 --- a/presets/lean/commands/speckit.preview.md +++ b/templates/commands/preview.md @@ -1,8 +1,10 @@ --- description: Preview the architectural impact and risks of a proposed change across all specifications. +scripts: + sh: python3 scripts/impact_analyzer.py --change "$ARGUMENTS" --model "${AI_MODEL_ID:-gpt-4o}" --- ## User Input ```text -$ARGUMENTS (The description of the proposed change) \ No newline at end of file +$ARGUMENTS \ No newline at end of file From f5cf4cc9305d9b780edcc91e11297c0c15e67ef6 Mon Sep 17 00:00:00 2001 From: UmmeHabiba1312 Date: Tue, 14 Apr 2026 18:22:51 +0500 Subject: [PATCH 3/4] refactor: move to pure community extension model, removing core file modifications --- presets/community/architect-preview/preset.yml | 6 ++++++ .../community/architect-preview/scripts}/impact_analyzer.py | 0 .../architect-preview/templates}/commands/preview.md | 2 +- src/specify_cli/__init__.py | 1 - src/specify_cli/integrations/claude/__init__.py | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 presets/community/architect-preview/preset.yml rename {scripts => presets/community/architect-preview/scripts}/impact_analyzer.py (100%) rename {templates => presets/community/architect-preview/templates}/commands/preview.md (60%) diff --git a/presets/community/architect-preview/preset.yml b/presets/community/architect-preview/preset.yml new file mode 100644 index 0000000000..665cec8250 --- /dev/null +++ b/presets/community/architect-preview/preset.yml @@ -0,0 +1,6 @@ +id: architect-preview +name: Architect Impact Previewer +version: 1.0.0 +description: Previews architectural risks and complexity before implementation. +author: Umme Habiba +type: command \ No newline at end of file diff --git a/scripts/impact_analyzer.py b/presets/community/architect-preview/scripts/impact_analyzer.py similarity index 100% rename from scripts/impact_analyzer.py rename to presets/community/architect-preview/scripts/impact_analyzer.py diff --git a/templates/commands/preview.md b/presets/community/architect-preview/templates/commands/preview.md similarity index 60% rename from templates/commands/preview.md rename to presets/community/architect-preview/templates/commands/preview.md index 45ef865087..35e39c2a8f 100644 --- a/templates/commands/preview.md +++ b/presets/community/architect-preview/templates/commands/preview.md @@ -1,7 +1,7 @@ --- description: Preview the architectural impact and risks of a proposed change across all specifications. scripts: - sh: python3 scripts/impact_analyzer.py --change "$ARGUMENTS" --model "${AI_MODEL_ID:-gpt-4o}" + sh: python3 ../../scripts/impact_analyzer.py --change "$ARGUMENTS" --model "${AI_MODEL_ID:-gpt-4o}" --- ## User Input diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 004bb12f2b..0bbf42ad5a 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -862,7 +862,6 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path: "plan": "Generate technical implementation plans from feature specifications.", "tasks": "Break down implementation plans into actionable task lists.", "implement": "Execute all tasks from the task breakdown to build the feature.", - "preview": "Predict the architectural impact, complexity, and risks of proposed changes.", "analyze": "Perform cross-artifact consistency analysis across spec.md, plan.md, and tasks.md.", "clarify": "Structured clarification workflow for underspecified requirements.", "constitution": "Create or update project governing principles and development guidelines.", diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 8af86a824c..31972c4b0e 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -17,7 +17,6 @@ "plan": "Optional guidance for the planning phase", "tasks": "Optional task generation constraints", "implement": "Optional implementation guidance or task filter", - "preview": "Predict the architectural impact, complexity, and risks of proposed changes.", "analyze": "Optional focus areas for analysis", "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", From 0f3b96e6c21538ca5d327941d0d2973b1fee357f Mon Sep 17 00:00:00 2001 From: UmmeHabiba1312 Date: Fri, 17 Apr 2026 22:45:31 +0500 Subject: [PATCH 4/4] feat: implement multi-agent support and integration switching --- .../community/architect-preview/preset.yml | 6 - .../scripts/impact_analyzer.py | 140 ------------------ .../templates/commands/preview.md | 10 -- src/specify_cli/__init__.py | 100 ++++++++++--- 4 files changed, 82 insertions(+), 174 deletions(-) delete mode 100644 presets/community/architect-preview/preset.yml delete mode 100644 presets/community/architect-preview/scripts/impact_analyzer.py delete mode 100644 presets/community/architect-preview/templates/commands/preview.md diff --git a/presets/community/architect-preview/preset.yml b/presets/community/architect-preview/preset.yml deleted file mode 100644 index 665cec8250..0000000000 --- a/presets/community/architect-preview/preset.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: architect-preview -name: Architect Impact Previewer -version: 1.0.0 -description: Previews architectural risks and complexity before implementation. -author: Umme Habiba -type: command \ No newline at end of file diff --git a/presets/community/architect-preview/scripts/impact_analyzer.py b/presets/community/architect-preview/scripts/impact_analyzer.py deleted file mode 100644 index 2e0bcb9eb5..0000000000 --- a/presets/community/architect-preview/scripts/impact_analyzer.py +++ /dev/null @@ -1,140 +0,0 @@ -import os -import json -import argparse -from openai import OpenAI -from dotenv import load_dotenv -load_dotenv() - -def setup_client(): - api_key = os.getenv("AI_API_KEY") or os.getenv("ANTHROPIC_API_KEY") or os.getenv("OPENAI_API_KEY") - base_url = os.getenv("AI_BASE_URL") - if not api_key: - raise ValueError("AI API Key not found. Please set AI_API_KEY or ensure your LLM provider is configured.") - - return OpenAI(api_key=api_key, base_url=base_url if base_url else "https://api.openai.com/v1") - -def load_project_context(target_dir): - """ - Aggregates content from all markdown files in the specified directory - to provide the LLM with full architectural context. - """ - context_accumulator = [] - if not os.path.exists(target_dir): - return None - - for filename in os.listdir(target_dir): - if filename.endswith(".md"): - file_path = os.path.join(target_dir, filename) - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - context_accumulator.append(f"--- FILE: {filename} ---\n{content}") - except Exception as e: - print(f"[!] Warning: Could not read {filename}: {e}") - - return "\n\n".join(context_accumulator) - -def perform_impact_analysis(client, context, change_request, model_id): - """ - Executes the analysis using the specified LLM and returns a structured JSON report. - """ - system_prompt = ( - "You are a Senior Systems Architect. Analyze the impact of a proposed change " - "on the provided technical specifications and output a structured JSON report." - ) - - user_prompt = f""" - ### PROJECT CONTEXT: - {context} - - ### PROPOSED CHANGE: - "{change_request}" - - ### OUTPUT REQUIREMENTS: - Return a JSON object with EXACTLY these keys: - - complexity_score_diff: (int 1-10) - - estimated_hours_delta: (str) - - affected_files: (list of filenames) - - technical_tasks: (list of strings) - - architecture_risks: (list of strings) - - executive_summary: (str) - """ - - try: - completion = client.chat.completions.create( - model=model_id, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt} - ] - ) - return completion.choices[0].message.content - except Exception as e: - return json.dumps({"error": str(e)}) - -def render_architect_report(raw_json): - """ - Parses and renders the architectural report in a clean, professional format. - """ - try: - clean_json = raw_json.strip().replace("```json", "").replace("```", "") - data = json.loads(clean_json) - - if "error" in data: - print(f"[!] Analysis Failed: {data['error']}") - return - - score = data.get("complexity_score_diff", 0) - status = "CRITICAL/HIGH" if score >= 7 else "MODERATE" if score >= 4 else "LOW" - - print("\n" + "="*60) - print(" SYSTEM ARCHITECT IMPACT ANALYSIS") - print("="*60) - print(f" IMPACT LEVEL : {status} (Score: {score}/10)") - print(f" EST. EFFORT : {data.get('estimated_hours_delta')}") - - print(f"\n [ ] TARGETED FILES:") - for f in data.get('affected_files', []): print(f" * {f}") - - print(f"\n [ ] REQUIRED TASKS:") - for task in data.get('technical_tasks', []): print(f" - {task}") - - print(f"\n [!] ARCHITECTURAL RISKS:") - for risk in data.get('architecture_risks', []): print(f" ! {risk}") - - print(f"\n [*] SUMMARY: {data.get('executive_summary')}") - print("="*60 + "\n") - - except Exception: - print("[!] Error: Could not parse AI response as valid JSON.") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Spec-Kit Architectural Impact Previewer") - parser.add_argument("--change", required=True, help="Description of the change request") - parser.add_argument("--model", default=os.getenv("AI_MODEL_ID", "gpt-4o"), help="Model ID to invoke") - - args = parser.parse_args() - - def get_search_path(): - project_specify = os.path.join(os.getcwd(), ".specify", "memory") - if os.path.exists(project_specify): - return project_specify - - script_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.abspath(os.path.join(script_dir, "..", "presets", "lean", "commands")) - - search_path = os.getenv("SPECKIT_PRESETS_DIR", get_search_path()) - - try: - ai_client = setup_client() - project_context = load_project_context(search_path) - - if not project_context: - print(f"[!] Path Error: No specification files found at {search_path}") - else: - print(f"[*] Analyzing global impact for: '{args.change[:50]}...'") - raw_report = perform_impact_analysis(ai_client, project_context, args.change, args.model) - render_architect_report(raw_report) - - except Exception as error: - print(f"[!] Runtime Exception: {error}") \ No newline at end of file diff --git a/presets/community/architect-preview/templates/commands/preview.md b/presets/community/architect-preview/templates/commands/preview.md deleted file mode 100644 index 35e39c2a8f..0000000000 --- a/presets/community/architect-preview/templates/commands/preview.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -description: Preview the architectural impact and risks of a proposed change across all specifications. -scripts: - sh: python3 ../../scripts/impact_analyzer.py --change "$ARGUMENTS" --model "${AI_MODEL_ID:-gpt-4o}" ---- - -## User Input - -```text -$ARGUMENTS \ No newline at end of file diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 0bbf42ad5a..f154696dce 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -816,16 +816,32 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker | def save_init_options(project_path: Path, options: dict[str, Any]) -> None: - """Persist the CLI options used during ``specify init``. - - Writes a small JSON file to ``.specify/init-options.json`` so that - later operations (e.g. preset install) can adapt their behaviour - without scanning the filesystem. - """ dest = project_path / INIT_OPTIONS_FILE dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_text(json.dumps(options, indent=2, sort_keys=True)) + + existing_data = {} + if dest.exists(): + try: + existing_data = json.loads(dest.read_text()) + except Exception: + pass + new_ai = options.get("ai") + installed = existing_data.get("installed_integrations", []) + + if not installed and "ai" in existing_data: + installed.append(existing_data["ai"]) + + if new_ai and new_ai not in installed: + installed.append(new_ai) + + final_options = {**existing_data, **options} + + final_options["default_integration"] = new_ai + final_options["installed_integrations"] = installed + final_options["ai"] = new_ai # Current active agent + + dest.write_text(json.dumps(final_options, indent=2, sort_keys=True)) def load_init_options(project_path: Path) -> dict[str, Any]: """Load the init options previously saved by ``specify init``. @@ -1045,17 +1061,20 @@ def init( console.print("[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]") console.print(f"[cyan]--force supplied: merging into existing directory '[cyan]{project_name}[/cyan]'[/cyan]") else: - error_panel = Panel( - f"Directory '[cyan]{project_name}[/cyan]' already exists\n" - "Please choose a different project name or remove the existing directory.\n" - "Use [bold]--force[/bold] to merge into the existing directory.", - title="[red]Directory Conflict[/red]", - border_style="red", - padding=(1, 2) - ) - console.print() - console.print(error_panel) - raise typer.Exit(1) + if (project_path / ".specify").exists(): + console.print(f"[cyan]Project folder detected. Adding new integration/agent to existing setup...[/cyan]") + elif existing_items: + if force: + console.print(f"[cyan]--force supplied: merging into existing directory '[cyan]{project_name}[/cyan]'[/cyan]") + else: + error_panel = Panel( + f"Directory '[cyan]{project_name}[/cyan]' already exists and is not empty.\n" + "Use [bold]--force[/bold] to initialize anyway.", + title="[red]Directory Conflict[/red]", + border_style="red" + ) + console.print(error_panel) + raise typer.Exit(1) if ai_assistant: if ai_assistant not in AGENT_CONFIG: @@ -1533,6 +1552,51 @@ def version(): console.print(panel) console.print() +@app.command() +def list_integrations(project_path: Path = typer.Option(Path.cwd(), "--path")): + """List all installed integrations in the project.""" + options = load_init_options(project_path) + installed = options.get("installed_integrations", []) + active = options.get("ai") + + if not installed: + console.print("[yellow]No integrations installed yet.[/yellow]") + return + + console.print("\n[bold cyan]Installed Integrations:[/bold cyan]") + for agent in installed: + status = "[green](active)[/green]" if agent == active else "" + console.print(f" - {agent} {status}") + +@app.command() +def use( + integration: str = typer.Argument(..., help="The integration/agent to switch to (e.g., claude, codex)"), + project_path: Path = typer.Option(Path.cwd(), "--path", help="Project directory path") +): + """ + Switch the active AI integration for the project. + """ + options = load_init_options(project_path) + installed = options.get("installed_integrations", []) + + if not installed: + if "ai" in options: + installed = [options["ai"]] + else: + console.print("[red]Error:[/red] No integrations found in this project.") + raise typer.Exit(1) + + if integration not in installed: + console.print(f"[red]Error:[/red] Integration '{integration}' is not installed.") + console.print(f"[yellow]Installed integrations:[/yellow] {', '.join(installed)}") + console.print(f"[dim]Hint: Run 'specify init --ai {integration}' to install it first.[/dim]") + raise typer.Exit(1) + + options["ai"] = integration + options["default_integration"] = integration + + save_init_options(project_path, options) + console.print(f"[green]✓[/green] Switched to [bold]{integration}[/bold] as the active integration.") # ===== Extension Commands =====