From fa5c530fdcd8aa071b524d2143c0a2eacb2612e5 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:47:26 -0500 Subject: [PATCH 1/2] chore: deprecate --ai flag in favor of --integration on specify init - Adds deprecation warning when --ai is used - Shows equivalent --integration command replacement - Handles generic integration with --commands-dir mapping - Adds comprehensive test coverage for deprecation behavior - Warning displays as prominent red panel above Next Steps - --ai flag continues to function (non-breaking change) Fixes #2169 --- src/specify_cli/__init__.py | 45 +++++++++++++++++++++++++ tests/integrations/test_cli.py | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index c33281e2b4..ab1512ff5f 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -92,6 +92,36 @@ def _build_ai_assistant_help() -> str: return base_help + " Use " + aliases_text + "." AI_ASSISTANT_HELP = _build_ai_assistant_help() + +def _build_integration_equivalent( + integration_key: str, + ai_commands_dir: str | None = None, +) -> str: + """Build the modern --integration equivalent for legacy --ai usage.""" + + parts = [f"--integration {integration_key}"] + if integration_key == "generic" and ai_commands_dir: + parts.append( + f'--integration-options="--commands-dir {ai_commands_dir}"' + ) + return " ".join(parts) + + +def _build_ai_deprecation_warning( + integration_key: str, + ai_commands_dir: str | None = None, +) -> str: + """Build the legacy --ai deprecation warning message.""" + + replacement = _build_integration_equivalent( + integration_key, + ai_commands_dir=ai_commands_dir, + ) + return ( + "[bold]--ai[/bold] is deprecated and will no longer be available in version 1.0.0 or later.\n\n" + f"Use [bold]{replacement}[/bold] instead." + ) + SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" @@ -957,6 +987,7 @@ def init( """ show_banner() + ai_deprecation_warning: str | None = None # Detect when option values are likely misinterpreted flags (parameter ordering issue) if ai_assistant and ai_assistant.startswith("--"): @@ -995,6 +1026,10 @@ def init( if not resolved_integration: console.print(f"[red]Error:[/red] Unknown agent '{ai_assistant}'. Choose from: {', '.join(sorted(INTEGRATION_REGISTRY))}") raise typer.Exit(1) + ai_deprecation_warning = _build_ai_deprecation_warning( + resolved_integration.key, + ai_commands_dir=ai_commands_dir, + ) # Deprecation warnings for --ai-skills and --ai-commands-dir (only when # an integration has been resolved from --ai or --integration) @@ -1428,6 +1463,16 @@ def init( console.print() console.print(security_notice) + if ai_deprecation_warning: + deprecation_notice = Panel( + ai_deprecation_warning, + title="[bold red]Deprecation Warning[/bold red]", + border_style="red", + padding=(1, 2), + ) + console.print() + console.print(deprecation_notice) + steps_lines = [] if not here: steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]") diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 1e23e35a7d..d1479dda19 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -2,10 +2,17 @@ import json import os +import re import yaml +def _normalize_cli_output(output: str) -> str: + output = re.sub(r"\x1b\[[0-9;]*m", "", output) + output = re.sub(r"\s+", " ", output) + return output.strip() + + class TestInitIntegrationFlag: def test_integration_and_ai_mutually_exclusive(self, tmp_path): from typer.testing import CliRunner @@ -77,6 +84,59 @@ def test_ai_copilot_auto_promotes(self, tmp_path): assert result.exit_code == 0 assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() + def test_ai_emits_deprecation_warning_with_integration_replacement(self, tmp_path): + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / "warn-ai" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke(app, [ + "init", "--here", "--ai", "copilot", "--script", "sh", "--no-git", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + + normalized_output = _normalize_cli_output(result.output) + assert result.exit_code == 0, result.output + assert "Deprecation Warning" in normalized_output + assert "--ai" in normalized_output + assert "deprecated" in normalized_output + assert "no longer be available" in normalized_output + assert "1.0.0" in normalized_output + assert "--integration copilot" in normalized_output + assert normalized_output.index("Deprecation Warning") < normalized_output.index("Next Steps") + assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() + + def test_ai_generic_warning_suggests_integration_options_equivalent(self, tmp_path): + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / "warn-generic" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke(app, [ + "init", "--here", "--ai", "generic", "--ai-commands-dir", ".myagent/commands", + "--script", "sh", "--no-git", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + + normalized_output = _normalize_cli_output(result.output) + assert result.exit_code == 0, result.output + assert "Deprecation Warning" in normalized_output + assert "--integration generic" in normalized_output + assert "--integration-options" in normalized_output + assert ".myagent/commands" in normalized_output + assert normalized_output.index("Deprecation Warning") < normalized_output.index("Next Steps") + assert (project / ".myagent" / "commands" / "speckit.plan.md").exists() + def test_ai_claude_here_preserves_preexisting_commands(self, tmp_path): from typer.testing import CliRunner from specify_cli import app From b0318a0c9ba0592e0d0f97bbcc45d03dc43fee86 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:06:07 -0500 Subject: [PATCH 2/2] Address PR review feedback for issue #2169 - Use existing strip_ansi helper from conftest instead of duplicating ANSI escape pattern - Properly escape ai_commands_dir with shlex.quote() to handle paths with spaces - Add shlex import to support proper command-line argument escaping --- src/specify_cli/__init__.py | 3 ++- tests/integrations/test_cli.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index ab1512ff5f..eb4c306bf4 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -33,6 +33,7 @@ import json import json5 import stat +import shlex import yaml from pathlib import Path from typing import Any, Optional @@ -102,7 +103,7 @@ def _build_integration_equivalent( parts = [f"--integration {integration_key}"] if integration_key == "generic" and ai_commands_dir: parts.append( - f'--integration-options="--commands-dir {ai_commands_dir}"' + f'--integration-options="--commands-dir {shlex.quote(ai_commands_dir)}"' ) return " ".join(parts) diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index d1479dda19..bd73ccd664 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -2,14 +2,15 @@ import json import os -import re import yaml +from tests.conftest import strip_ansi + def _normalize_cli_output(output: str) -> str: - output = re.sub(r"\x1b\[[0-9;]*m", "", output) - output = re.sub(r"\s+", " ", output) + output = strip_ansi(output) + output = " ".join(output.split()) return output.strip()