mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags (0.10.0) (#2872)
* Initial plan * feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags at 0.10.0 * refactor(tests): rename stale test_ai_help_* methods to test_agent_config_* * fix: address review — derive agent folder for generic integration and remove redundant test - Security notice now falls back to integration_parsed_options['commands_dir'] when AGENT_CONFIG folder is None (generic integration). - Remove test_agent_config_includes_kiro_cli which duplicates the assertion in test_runtime_config_uses_kiro_cli_and_removes_q. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: scrub all remaining --ai flag references from source and tests - Remove dead AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, and _build_ai_assistant_help() from _agent_config.py - Update comments/docstrings in extensions.py, presets.py, and integration subpackages to reference 'skills mode' or '--integration' instead of the removed flags - Fix catalog.json generic integration description - Update test docstrings/comments in test_extension_skills.py, test_extensions.py, and test_presets.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: remove legacy --ai flag rejection tests The flags are fully removed from the CLI; typer handles unknown options generically. No custom rejection logic exists to test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * revert: remove manual CHANGELOG.md entry CHANGELOG is generated automatically; manual edits should not be made. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make generic catalog description self-explanatory Include the required --commands-dir sub-option in the description so readers don't need to look up integration docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): rename duplicate test classes to avoid shadowing The rename from Test*AutoPromote to Test*Integration collided with the existing Test*Integration(SkillsIntegrationTests) base classes, causing the shared test suites to be silently overwritten. Rename the CLI init flow classes to Test*InitFlow instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -277,7 +277,7 @@
|
||||
"id": "generic",
|
||||
"name": "Generic (bring your own agent)",
|
||||
"version": "1.0.0",
|
||||
"description": "Generic integration for any agent via --ai-commands-dir",
|
||||
"description": "Generic integration for any agent via --integration-options=\"--commands-dir <dir>\"",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["generic"]
|
||||
|
||||
@@ -82,8 +82,6 @@ from ._version import (
|
||||
)
|
||||
from ._agent_config import (
|
||||
AGENT_CONFIG as AGENT_CONFIG,
|
||||
AI_ASSISTANT_ALIASES as AI_ASSISTANT_ALIASES,
|
||||
AI_ASSISTANT_HELP as AI_ASSISTANT_HELP,
|
||||
DEFAULT_INIT_INTEGRATION as DEFAULT_INIT_INTEGRATION,
|
||||
SCRIPT_TYPE_CHOICES as SCRIPT_TYPE_CHOICES,
|
||||
)
|
||||
|
||||
@@ -17,29 +17,4 @@ AGENT_CONFIG: dict[str, dict[str, Any]] = _build_agent_config()
|
||||
|
||||
DEFAULT_INIT_INTEGRATION = "copilot"
|
||||
|
||||
AI_ASSISTANT_ALIASES: dict[str, str] = {
|
||||
"kiro": "kiro-cli",
|
||||
}
|
||||
|
||||
|
||||
def _build_ai_assistant_help() -> str:
|
||||
non_generic_agents = sorted(agent for agent in AGENT_CONFIG if agent != "generic")
|
||||
base_help = (
|
||||
f"AI assistant to use: {', '.join(non_generic_agents)}, "
|
||||
"or generic (requires --ai-commands-dir)."
|
||||
)
|
||||
if not AI_ASSISTANT_ALIASES:
|
||||
return base_help
|
||||
alias_phrases = []
|
||||
for alias, target in sorted(AI_ASSISTANT_ALIASES.items()):
|
||||
alias_phrases.append(f"'{alias}' as an alias for '{target}'")
|
||||
if len(alias_phrases) == 1:
|
||||
aliases_text = alias_phrases[0]
|
||||
else:
|
||||
aliases_text = ", ".join(alias_phrases[:-1]) + " and " + alias_phrases[-1]
|
||||
return base_help + " Use " + aliases_text + "."
|
||||
|
||||
|
||||
AI_ASSISTANT_HELP: str = _build_ai_assistant_help()
|
||||
|
||||
SCRIPT_TYPE_CHOICES: dict[str, str] = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -14,8 +13,6 @@ from rich.panel import Panel
|
||||
|
||||
from .._agent_config import (
|
||||
AGENT_CONFIG,
|
||||
AI_ASSISTANT_ALIASES,
|
||||
AI_ASSISTANT_HELP,
|
||||
DEFAULT_INIT_INTEGRATION,
|
||||
SCRIPT_TYPE_CHOICES,
|
||||
)
|
||||
@@ -28,31 +25,6 @@ from .._assets import (
|
||||
from .._console import StepTracker, console, select_with_arrows, show_banner
|
||||
from .._utils import check_tool, init_git_repo, is_git_repo
|
||||
|
||||
def _build_integration_equivalent(
|
||||
integration_key: str,
|
||||
ai_commands_dir: str | None = None,
|
||||
) -> str:
|
||||
parts = [f"--integration {integration_key}"]
|
||||
if integration_key == "generic" and ai_commands_dir:
|
||||
parts.append(
|
||||
f'--integration-options="--commands-dir {shlex.quote(ai_commands_dir)}"'
|
||||
)
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _build_ai_deprecation_warning(
|
||||
integration_key: str,
|
||||
ai_commands_dir: str | None = None,
|
||||
) -> str:
|
||||
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 0.10.0 or later.\n\n"
|
||||
f"Use [bold]{replacement}[/bold] instead."
|
||||
)
|
||||
|
||||
|
||||
def _stdin_is_interactive() -> bool:
|
||||
return sys.stdin.isatty()
|
||||
@@ -97,8 +69,6 @@ def register(app: typer.Typer) -> None:
|
||||
@app.command()
|
||||
def init(
|
||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
|
||||
ai_assistant: str = typer.Option(None, "--ai", help=AI_ASSISTANT_HELP),
|
||||
ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", help="Directory for agent command files (required with --ai generic, e.g. .myagent/commands/)"),
|
||||
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
||||
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for coding agent tools like Claude Code"),
|
||||
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
||||
@@ -107,11 +77,10 @@ def register(app: typer.Typer) -> None:
|
||||
skip_tls: bool = typer.Option(False, "--skip-tls", help="Deprecated (no-op). Previously: skip SSL/TLS verification.", hidden=True),
|
||||
debug: bool = typer.Option(False, "--debug", help="Deprecated. Previously: show verbose diagnostic output; currently only prints additional diagnostic details on failure.", hidden=True),
|
||||
github_token: str = typer.Option(None, "--github-token", help="Deprecated (no-op). Previously: GitHub token for API requests.", hidden=True),
|
||||
ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai)"),
|
||||
offline: bool = typer.Option(False, "--offline", help="Deprecated (no-op). All scaffolding now uses bundled assets.", hidden=True),
|
||||
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
|
||||
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, …, 1000, … — expands past 999 automatically) or 'timestamp' (YYYYMMDD-HHMMSS)"),
|
||||
integration: str = typer.Option(None, "--integration", help="Use the new integration system (e.g. --integration copilot). Mutually exclusive with --ai."),
|
||||
integration: str = typer.Option(None, "--integration", help="AI coding agent integration to use (e.g. --integration copilot). See 'specify check' for available integrations."),
|
||||
integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'),
|
||||
):
|
||||
"""
|
||||
@@ -163,27 +132,6 @@ def register(app: typer.Typer) -> None:
|
||||
from ..integration_runtime import with_integration_setting as _with_integration_setting
|
||||
|
||||
show_banner()
|
||||
ai_deprecation_warning: str | None = None
|
||||
|
||||
if ai_assistant and ai_assistant.startswith("--"):
|
||||
console.print(f"[red]Error:[/red] Invalid value for --ai: '{ai_assistant}'")
|
||||
console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai?")
|
||||
console.print("[yellow]Example:[/yellow] specify init --integration claude --here")
|
||||
console.print(f"[yellow]Available agents:[/yellow] {', '.join(AGENT_CONFIG.keys())}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if ai_commands_dir and ai_commands_dir.startswith("--"):
|
||||
console.print(f"[red]Error:[/red] Invalid value for --ai-commands-dir: '{ai_commands_dir}'")
|
||||
console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai-commands-dir?")
|
||||
console.print("[yellow]Example:[/yellow] specify init --integration generic --integration-options=\"--commands-dir .myagent/commands/\"")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if ai_assistant:
|
||||
ai_assistant = AI_ASSISTANT_ALIASES.get(ai_assistant, ai_assistant)
|
||||
|
||||
if integration and ai_assistant:
|
||||
console.print("[red]Error:[/red] --integration and --ai are mutually exclusive")
|
||||
raise typer.Exit(1)
|
||||
|
||||
from ..integrations import INTEGRATION_REGISTRY, get_integration
|
||||
if integration:
|
||||
@@ -193,35 +141,6 @@ def register(app: typer.Typer) -> None:
|
||||
available = ", ".join(sorted(INTEGRATION_REGISTRY))
|
||||
console.print(f"[yellow]Available integrations:[/yellow] {available}")
|
||||
raise typer.Exit(1)
|
||||
ai_assistant = integration
|
||||
elif ai_assistant:
|
||||
resolved_integration = get_integration(ai_assistant)
|
||||
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,
|
||||
)
|
||||
|
||||
if ai_assistant or integration:
|
||||
if ai_skills:
|
||||
from ..integrations.base import SkillsIntegration as _SkillsCheck
|
||||
if isinstance(resolved_integration, _SkillsCheck):
|
||||
console.print(
|
||||
"[dim]Note: --ai-skills is not needed; "
|
||||
"skills are the default for this integration.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
"[dim]Note: --ai-skills has no effect with "
|
||||
f"{resolved_integration.key}; this integration uses commands, not skills.[/dim]"
|
||||
)
|
||||
if ai_commands_dir and resolved_integration.key != "generic":
|
||||
console.print(
|
||||
"[dim]Note: --ai-commands-dir is deprecated; "
|
||||
'use [bold]--integration generic --integration-options="--commands-dir <dir>"[/bold] instead.[/dim]'
|
||||
)
|
||||
|
||||
if no_git:
|
||||
console.print(
|
||||
@@ -242,11 +161,6 @@ def register(app: typer.Typer) -> None:
|
||||
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if ai_skills and not ai_assistant:
|
||||
console.print("[red]Error:[/red] --ai-skills requires --ai to be specified")
|
||||
console.print("[yellow]Usage:[/yellow] specify init <project> --ai <agent> --ai-skills")
|
||||
raise typer.Exit(1)
|
||||
|
||||
BRANCH_NUMBERING_CHOICES = {"sequential", "timestamp"}
|
||||
if branch_numbering and branch_numbering not in BRANCH_NUMBERING_CHOICES:
|
||||
console.print(f"[red]Error:[/red] Invalid --branch-numbering value '{branch_numbering}'. Choose from: {', '.join(sorted(BRANCH_NUMBERING_CHOICES))}")
|
||||
@@ -295,11 +209,11 @@ def register(app: typer.Typer) -> None:
|
||||
console.print(error_panel)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if ai_assistant:
|
||||
if ai_assistant not in AGENT_CONFIG:
|
||||
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}")
|
||||
if integration:
|
||||
if integration not in AGENT_CONFIG:
|
||||
console.print(f"[red]Error:[/red] Invalid integration '{integration}'. Choose from: {', '.join(AGENT_CONFIG.keys())}")
|
||||
raise typer.Exit(1)
|
||||
selected_ai = ai_assistant
|
||||
selected_ai = integration
|
||||
elif not _stdin_is_interactive():
|
||||
console.print(
|
||||
f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. "
|
||||
@@ -314,17 +228,16 @@ def register(app: typer.Typer) -> None:
|
||||
DEFAULT_INIT_INTEGRATION,
|
||||
)
|
||||
|
||||
if not ai_assistant:
|
||||
if not integration:
|
||||
resolved_integration = get_integration(selected_ai)
|
||||
if not resolved_integration:
|
||||
console.print(f"[red]Error:[/red] Unknown agent '{selected_ai}'")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if selected_ai == "generic" and not integration_options:
|
||||
if not ai_commands_dir:
|
||||
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic or --integration generic")
|
||||
console.print('[dim]Example: specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"[/dim]')
|
||||
raise typer.Exit(1)
|
||||
console.print("[red]Error:[/red] --integration generic requires --integration-options with --commands-dir")
|
||||
console.print('[dim]Example: specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"[/dim]')
|
||||
raise typer.Exit(1)
|
||||
|
||||
current_dir = Path.cwd()
|
||||
|
||||
@@ -414,10 +327,6 @@ def register(app: typer.Typer) -> None:
|
||||
)
|
||||
|
||||
integration_parsed_options: dict[str, Any] = {}
|
||||
if ai_commands_dir:
|
||||
integration_parsed_options["commands_dir"] = ai_commands_dir
|
||||
if ai_skills:
|
||||
integration_parsed_options["skills"] = True
|
||||
if integration_options:
|
||||
extra = _parse_integration_options(resolved_integration, integration_options)
|
||||
if extra:
|
||||
@@ -675,7 +584,7 @@ def register(app: typer.Typer) -> None:
|
||||
|
||||
agent_config = AGENT_CONFIG.get(selected_ai)
|
||||
if agent_config:
|
||||
agent_folder = ai_commands_dir if selected_ai == "generic" else agent_config["folder"]
|
||||
agent_folder = agent_config["folder"] or integration_parsed_options.get("commands_dir")
|
||||
if agent_folder:
|
||||
security_notice = Panel(
|
||||
f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n"
|
||||
@@ -687,16 +596,6 @@ def register(app: typer.Typer) -> None:
|
||||
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)
|
||||
|
||||
if git_default_notice:
|
||||
default_change_notice = Panel(
|
||||
"The git extension is currently enabled by default during [bold]specify init[/bold].\n"
|
||||
@@ -720,24 +619,24 @@ def register(app: typer.Typer) -> None:
|
||||
from ..integrations.base import SkillsIntegration as _SkillsInt
|
||||
_is_skills_integration = isinstance(resolved_integration, _SkillsInt) or getattr(resolved_integration, "_skills_mode", False)
|
||||
|
||||
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
|
||||
claude_skill_mode = selected_ai == "claude" and (ai_skills or _is_skills_integration)
|
||||
codex_skill_mode = selected_ai == "codex" and _is_skills_integration
|
||||
claude_skill_mode = selected_ai == "claude" and _is_skills_integration
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
||||
trae_skill_mode = selected_ai == "trae"
|
||||
cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration)
|
||||
cursor_agent_skill_mode = selected_ai == "cursor-agent" and _is_skills_integration
|
||||
copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration
|
||||
devin_skill_mode = selected_ai == "devin"
|
||||
cline_skill_mode = selected_ai == "cline"
|
||||
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode
|
||||
|
||||
if codex_skill_mode and not ai_skills:
|
||||
if codex_skill_mode:
|
||||
steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]")
|
||||
step_num += 1
|
||||
if claude_skill_mode and not ai_skills:
|
||||
if claude_skill_mode:
|
||||
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
|
||||
step_num += 1
|
||||
if cursor_agent_skill_mode and not ai_skills:
|
||||
if cursor_agent_skill_mode:
|
||||
steps_lines.append(f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]")
|
||||
step_num += 1
|
||||
if devin_skill_mode:
|
||||
|
||||
@@ -889,7 +889,7 @@ class ExtensionManager:
|
||||
|
||||
For every command in the extension manifest, creates a SKILL.md
|
||||
file in the agent's skills directory following the agentskills.io
|
||||
specification. This is only done when ``--ai-skills`` was used
|
||||
specification. This is only done when skills mode was used
|
||||
during project initialisation.
|
||||
|
||||
Args:
|
||||
@@ -1295,7 +1295,7 @@ class ExtensionManager:
|
||||
create_missing_active_skills_dir=True,
|
||||
)
|
||||
|
||||
# Auto-register extension commands as agent skills when --ai-skills
|
||||
# Auto-register extension commands as agent skills when skills mode
|
||||
# was used during project initialisation (feature parity).
|
||||
registered_skills = self._register_extension_skills(
|
||||
manifest, dest_dir, link_outputs=link_commands
|
||||
|
||||
@@ -22,7 +22,7 @@ class CursorAgentIntegration(SkillsIntegration):
|
||||
"folder": ".cursor/",
|
||||
"commands_subdir": "skills",
|
||||
"install_url": "https://docs.cursor.com/en/cli/overview",
|
||||
# IDE-first integration: ``specify init --ai cursor-agent`` must
|
||||
# IDE-first integration: ``specify init --integration cursor-agent`` must
|
||||
# work without the ``cursor-agent`` CLI installed (the IDE flow
|
||||
# uses skills directly). Workflow dispatch additionally requires
|
||||
# the CLI on PATH, but that's enforced at dispatch time via
|
||||
|
||||
@@ -7,7 +7,7 @@ AI agent framework by Nous Research. It stores skills in
|
||||
Usage::
|
||||
|
||||
specify init my-project --integration hermes
|
||||
specify init --here --ai hermes
|
||||
specify init --here --integration hermes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1219,7 +1219,7 @@ class PresetManager:
|
||||
directory. If so, the skill is overwritten with content derived
|
||||
from the preset's command file. This ensures that presets that
|
||||
override commands also propagate to the agentskills.io skill
|
||||
layer when ``--ai-skills`` was used during project initialisation.
|
||||
layer when skills mode was used during project initialisation.
|
||||
|
||||
Args:
|
||||
manifest: Preset manifest.
|
||||
@@ -1559,7 +1559,7 @@ class PresetManager:
|
||||
"registered_commands": registered_commands,
|
||||
})
|
||||
|
||||
# Update corresponding skills when --ai-skills was previously used
|
||||
# Update corresponding skills when skills mode was previously used
|
||||
# and persist that result as well.
|
||||
registered_skills = self._register_skills(manifest, dest_dir)
|
||||
self.registry.update(manifest.id, {
|
||||
|
||||
@@ -43,16 +43,6 @@ class TestCliDiagnosticFormatting:
|
||||
|
||||
|
||||
class TestInitIntegrationFlag:
|
||||
def test_integration_and_ai_mutually_exclusive(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", str(tmp_path / "test-project"), "--ai", "claude", "--integration", "copilot",
|
||||
])
|
||||
assert result.exit_code != 0
|
||||
assert "mutually exclusive" in result.output
|
||||
|
||||
def test_unknown_integration_rejected(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
@@ -131,7 +121,7 @@ class TestInitIntegrationFlag:
|
||||
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||||
assert data["integration"] == specify_cli.DEFAULT_INIT_INTEGRATION
|
||||
|
||||
def test_ai_copilot_auto_promotes(self, tmp_path):
|
||||
def test_integration_copilot_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
project = tmp_path / "promote-test"
|
||||
@@ -141,66 +131,13 @@ class TestInitIntegrationFlag:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "copilot", "--script", "sh", "--no-git",
|
||||
"init", "--here", "--integration", "copilot", "--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
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 "0.10.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_init_optional_preset_failure_reports_target_and_continues(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
@@ -237,7 +174,7 @@ class TestInitIntegrationFlag:
|
||||
assert "Continuing without the optional preset" in normalized
|
||||
assert "Project ready" in normalized
|
||||
|
||||
def test_ai_claude_here_preserves_preexisting_commands(self, tmp_path):
|
||||
def test_integration_claude_here_preserves_preexisting_commands(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -255,7 +192,7 @@ class TestInitIntegrationFlag:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--force", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git", "--ignore-agent-tools",
|
||||
"init", "--here", "--force", "--integration", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
@@ -800,7 +737,7 @@ class TestGitExtensionAutoInstall:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "claude", "--script", "sh",
|
||||
"init", "--here", "--integration", "claude", "--script", "sh",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -838,7 +775,7 @@ class TestGitExtensionAutoInstall:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "claude", "--script", "sh",
|
||||
"init", "--here", "--integration", "claude", "--script", "sh",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -862,7 +799,7 @@ class TestGitExtensionAutoInstall:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "claude", "--script", "sh",
|
||||
"init", "--here", "--integration", "claude", "--script", "sh",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -889,7 +826,7 @@ class TestGitExtensionAutoInstall:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "claude", "--script", "sh",
|
||||
"init", "--here", "--integration", "claude", "--script", "sh",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -915,7 +852,7 @@ class TestGitExtensionAutoInstall:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "claude", "--script", "sh",
|
||||
"init", "--here", "--integration", "claude", "--script", "sh",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
|
||||
@@ -29,19 +29,19 @@ class TestAgyIntegration(SkillsIntegrationTests):
|
||||
assert i.config["install_url"] == "https://antigravity.google/"
|
||||
|
||||
|
||||
class TestAgyAutoPromote:
|
||||
"""--ai agy auto-promotes to integration path."""
|
||||
class TestAgyInitFlow:
|
||||
"""--integration agy creates expected files."""
|
||||
|
||||
def test_ai_agy_without_ai_skills_auto_promotes(self, tmp_path):
|
||||
"""--ai agy should work the same as --integration agy."""
|
||||
def test_integration_agy_creates_skills(self, tmp_path):
|
||||
"""--integration agy should create skills directory."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"])
|
||||
result = runner.invoke(app, ["init", str(target), "--integration", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"])
|
||||
|
||||
assert result.exit_code == 0, f"init --ai agy failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration agy failed: {result.output}"
|
||||
assert (target / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
def test_agy_setup_warning(self, tmp_path):
|
||||
@@ -52,7 +52,7 @@ class TestAgyAutoPromote:
|
||||
# Click >= 8.2 separates stdout and stderr natively
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj2"
|
||||
result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"])
|
||||
result = runner.invoke(app, ["init", str(target), "--integration", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Warning: The .agents/ layout requires Antigravity v1.20.5 or newer" in result.stderr
|
||||
|
||||
@@ -179,9 +179,9 @@ class MarkdownIntegrationTests:
|
||||
assert "<!-- SPECKIT END -->" not in remaining
|
||||
assert "# My Rules" in remaining
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
# -- CLI integration flag -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
def test_integration_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -192,15 +192,15 @@ class MarkdownIntegrationTests:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git",
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory"
|
||||
assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory"
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
@@ -312,9 +312,9 @@ class SkillsIntegrationTests:
|
||||
assert "<!-- SPECKIT END -->" not in remaining
|
||||
assert "# My Rules" in remaining
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
# -- CLI integration flag -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
def test_integration_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -325,15 +325,15 @@ class SkillsIntegrationTests:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git",
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
skills_dir = i.skills_dest(project)
|
||||
assert skills_dir.is_dir(), f"--ai {self.KEY} did not create skills directory"
|
||||
assert skills_dir.is_dir(), f"--integration {self.KEY} did not create skills directory"
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
@@ -388,9 +388,9 @@ class TomlIntegrationTests:
|
||||
assert "<!-- SPECKIT END -->" not in remaining
|
||||
assert "# My Rules" in remaining
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
# -- CLI integration flag -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
def test_integration_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -405,7 +405,7 @@ class TomlIntegrationTests:
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--ai",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
@@ -416,10 +416,10 @@ class TomlIntegrationTests:
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory"
|
||||
assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory"
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
@@ -267,9 +267,9 @@ class YamlIntegrationTests:
|
||||
assert "<!-- SPECKIT END -->" not in remaining
|
||||
assert "# My Rules" in remaining
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
# -- CLI integration flag -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
def test_integration_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -284,7 +284,7 @@ class YamlIntegrationTests:
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--ai",
|
||||
"--integration",
|
||||
self.KEY,
|
||||
"--script",
|
||||
"sh",
|
||||
@@ -295,10 +295,10 @@ class YamlIntegrationTests:
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory"
|
||||
assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory"
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestClaudeIntegration:
|
||||
assert b"<!-- SPECKIT" not in remaining
|
||||
assert b"# CLAUDE.md" in remaining
|
||||
|
||||
def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path):
|
||||
def test_integration_flag_creates_skill_files_cli(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestClaudeIntegration:
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--ai",
|
||||
"--integration",
|
||||
"claude",
|
||||
"--script",
|
||||
"sh",
|
||||
@@ -234,7 +234,7 @@ class TestClaudeIntegration:
|
||||
assert init_options["integration"] == "claude"
|
||||
|
||||
def test_claude_init_remains_usable_when_converter_fails(self, tmp_path):
|
||||
"""Claude init should succeed even without install_ai_skills."""
|
||||
"""Claude init should succeed even without install_skills."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -243,7 +243,7 @@ class TestClaudeIntegration:
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["init", str(target), "--ai", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools"],
|
||||
["init", str(target), "--integration", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
@@ -14,19 +14,19 @@ class TestCodexIntegration(SkillsIntegrationTests):
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
|
||||
|
||||
class TestCodexAutoPromote:
|
||||
"""--ai codex auto-promotes to integration path."""
|
||||
class TestCodexInitFlow:
|
||||
"""--integration codex creates expected files."""
|
||||
|
||||
def test_ai_codex_without_ai_skills_auto_promotes(self, tmp_path):
|
||||
"""--ai codex should work the same as --integration codex."""
|
||||
def test_integration_codex_creates_skills(self, tmp_path):
|
||||
"""--integration codex should create skills in .agents/skills."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(app, ["init", str(target), "--ai", "codex", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||
result = runner.invoke(app, ["init", str(target), "--integration", "codex", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||
|
||||
assert result.exit_code == 0, f"init --ai codex failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration codex failed: {result.output}"
|
||||
assert (target / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
|
||||
|
||||
@@ -92,19 +92,19 @@ class TestCursorMdcFrontmatter:
|
||||
assert not ctx_path.exists()
|
||||
|
||||
|
||||
class TestCursorAgentAutoPromote:
|
||||
"""--ai cursor-agent auto-promotes to integration path."""
|
||||
class TestCursorAgentInitFlow:
|
||||
"""--integration cursor-agent creates expected files."""
|
||||
|
||||
def test_ai_cursor_agent_without_ai_skills_auto_promotes(self, tmp_path):
|
||||
"""--ai cursor-agent should work the same as --integration cursor-agent."""
|
||||
def test_integration_cursor_agent_creates_skills(self, tmp_path):
|
||||
"""--integration cursor-agent should create skills in .cursor/skills."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(app, ["init", str(target), "--ai", "cursor-agent", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||
result = runner.invoke(app, ["init", str(target), "--integration", "cursor-agent", "--no-git", "--ignore-agent-tools", "--script", "sh"])
|
||||
|
||||
assert result.exit_code == 0, f"init --ai cursor-agent failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration cursor-agent failed: {result.output}"
|
||||
assert (target / ".cursor" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class TestCursorAgentCliDispatch:
|
||||
def test_requires_cli_is_false_for_ide_first_flow(self):
|
||||
"""``requires_cli`` must stay False so the IDE-only flow keeps working.
|
||||
|
||||
``specify init --ai cursor-agent`` (without ``--ignore-agent-tools``)
|
||||
``specify init --integration cursor-agent`` (without ``--ignore-agent-tools``)
|
||||
treats ``requires_cli=True`` as a hard precheck and fails when the
|
||||
``cursor-agent`` CLI isn't on PATH — even though the Cursor IDE
|
||||
/ skills flow can run without it. Workflow dispatch support is
|
||||
|
||||
@@ -56,11 +56,11 @@ class TestDevinBuildExecArgs:
|
||||
assert args == ["devin", "-p", "hi", "--model", "claude-sonnet-4"]
|
||||
|
||||
|
||||
class TestDevinAutoPromote:
|
||||
"""--ai devin auto-promotes to integration path."""
|
||||
class TestDevinInitFlow:
|
||||
"""--integration devin creates expected files."""
|
||||
|
||||
def test_ai_devin_without_ai_skills_auto_promotes(self, tmp_path):
|
||||
"""--ai devin should work the same as --integration devin."""
|
||||
def test_integration_devin_creates_skills(self, tmp_path):
|
||||
"""--integration devin should create skills directory."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -68,8 +68,8 @@ class TestDevinAutoPromote:
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["init", str(target), "--ai", "devin", "--no-git", "--ignore-agent-tools", "--script", "sh"],
|
||||
["init", str(target), "--integration", "devin", "--no-git", "--ignore-agent-tools", "--script", "sh"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"init --ai devin failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration devin failed: {result.output}"
|
||||
assert (target / ".devin" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
@@ -245,7 +245,7 @@ class TestGenericIntegration:
|
||||
# -- CLI --------------------------------------------------------------
|
||||
|
||||
def test_cli_generic_without_commands_dir_fails(self, tmp_path):
|
||||
"""--integration generic without --ai-commands-dir should fail."""
|
||||
"""--integration generic without --integration-options should fail."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
runner = CliRunner()
|
||||
@@ -253,8 +253,7 @@ class TestGenericIntegration:
|
||||
"init", str(tmp_path / "test-generic"), "--integration", "generic",
|
||||
"--script", "sh", "--no-git",
|
||||
])
|
||||
# Generic requires --commands-dir / --ai-commands-dir
|
||||
# The integration path validates via setup()
|
||||
# Generic requires --commands-dir via --integration-options
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
@@ -270,7 +269,7 @@ class TestGenericIntegration:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", "generic",
|
||||
"--ai-commands-dir", ".myagent/commands",
|
||||
"--integration-options=--commands-dir .myagent/commands",
|
||||
"--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -281,7 +280,7 @@ class TestGenericIntegration:
|
||||
assert ext_cfg.get("context_file") == "AGENTS.md"
|
||||
|
||||
def test_complete_file_inventory_sh(self, tmp_path):
|
||||
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script sh."""
|
||||
"""Every file produced by specify init --integration generic --integration-options=--commands-dir ... --script sh."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -292,7 +291,7 @@ class TestGenericIntegration:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", "generic",
|
||||
"--ai-commands-dir", ".myagent/commands",
|
||||
"--integration-options=--commands-dir .myagent/commands",
|
||||
"--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
@@ -345,7 +344,7 @@ class TestGenericIntegration:
|
||||
)
|
||||
|
||||
def test_complete_file_inventory_ps(self, tmp_path):
|
||||
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script ps."""
|
||||
"""Every file produced by specify init --integration generic --integration-options=--commands-dir ... --script ps."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -356,7 +355,7 @@ class TestGenericIntegration:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", "generic",
|
||||
"--ai-commands-dir", ".myagent/commands",
|
||||
"--integration-options=--commands-dir .myagent/commands",
|
||||
"--script", "ps", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
|
||||
@@ -326,12 +326,11 @@ class TestHermesIntegration(SkillsIntegrationTests):
|
||||
)
|
||||
|
||||
|
||||
class TestHermesAutoPromote:
|
||||
"""--ai hermes auto-promotes to integration path."""
|
||||
class TestHermesInitFlow:
|
||||
"""--integration hermes creates expected files."""
|
||||
|
||||
def test_ai_hermes_without_ai_skills_auto_promotes(self, tmp_path, monkeypatch):
|
||||
"""--ai hermes should work the same as --integration hermes,
|
||||
creating global skills and a local marker."""
|
||||
def test_integration_hermes_creates_global_skills(self, tmp_path, monkeypatch):
|
||||
"""--integration hermes should create global skills and a local marker."""
|
||||
home = _fake_home(tmp_path)
|
||||
monkeypatch.setattr(Path, "home", lambda: home)
|
||||
|
||||
@@ -342,13 +341,13 @@ class TestHermesAutoPromote:
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(app, [
|
||||
"init", str(target),
|
||||
"--ai", "hermes",
|
||||
"--integration", "hermes",
|
||||
"--no-git",
|
||||
"--ignore-agent-tools",
|
||||
"--script", "sh",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, f"init --ai hermes failed: {result.output}"
|
||||
assert result.exit_code == 0, f"init --integration hermes failed: {result.output}"
|
||||
# Skills should be in global ~/.hermes/skills/
|
||||
assert (home / ".hermes" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
# Local marker should exist
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestKimiNextSteps:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "kimi", "--no-git",
|
||||
"init", "--here", "--integration", "kimi", "--no-git",
|
||||
"--ignore-agent-tools", "--script", "sh",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
|
||||
@@ -123,15 +123,15 @@ class TestKiroCliIntegration(MarkdownIntegrationTests):
|
||||
)
|
||||
|
||||
|
||||
class TestKiroAlias:
|
||||
"""--ai kiro alias normalizes to kiro-cli and auto-promotes."""
|
||||
class TestKiroIntegration:
|
||||
"""--integration kiro-cli creates expected files."""
|
||||
|
||||
def test_kiro_alias_normalized_to_kiro_cli(self, tmp_path):
|
||||
"""--ai kiro should normalize to canonical kiro-cli and auto-promote."""
|
||||
def test_integration_kiro_cli_creates_files(self, tmp_path):
|
||||
"""--integration kiro-cli should create files in .kiro/prompts."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
target = tmp_path / "kiro-alias-proj"
|
||||
target = tmp_path / "kiro-proj"
|
||||
target.mkdir()
|
||||
|
||||
old_cwd = os.getcwd()
|
||||
@@ -139,7 +139,7 @@ class TestKiroAlias:
|
||||
os.chdir(target)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "kiro",
|
||||
"init", "--here", "--integration", "kiro-cli",
|
||||
"--ignore-agent-tools", "--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
|
||||
@@ -294,11 +294,11 @@ class TestRovodevIntegration:
|
||||
assert init_options.get("ai_skills") is True
|
||||
assert init_options.get("script") == "sh"
|
||||
|
||||
def test_ai_flag_auto_promotes_to_integration(self, tmp_path):
|
||||
"""``--ai rovodev`` should reach the same end-state as ``--integration rovodev``."""
|
||||
project = tmp_path / "rovodev-ai"
|
||||
def test_integration_flag_creates_expected_files(self, tmp_path):
|
||||
"""``--integration rovodev`` should create all expected rovodev files."""
|
||||
project = tmp_path / "rovodev-int"
|
||||
project.mkdir()
|
||||
result = _run_init(project, "--ai", "rovodev")
|
||||
result = _run_init(project, "--integration", "rovodev")
|
||||
assert result.exit_code == 0, result.output
|
||||
assert (project / ".rovodev" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
assert (project / ".rovodev" / "prompts.yml").exists()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP
|
||||
from specify_cli import AGENT_CONFIG
|
||||
from specify_cli.extensions import CommandRegistrar
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -39,13 +39,6 @@ class TestAgentConfigConsistency:
|
||||
assert AGENT_CONFIG["codex"]["folder"] == ".agents/"
|
||||
assert AGENT_CONFIG["codex"]["commands_subdir"] == "skills"
|
||||
|
||||
def test_init_ai_help_includes_roo_and_kiro_alias(self):
|
||||
"""CLI help text for --ai should stay in sync with agent config and alias guidance."""
|
||||
assert "roo" in AI_ASSISTANT_HELP
|
||||
for alias, target in AI_ASSISTANT_ALIASES.items():
|
||||
assert alias in AI_ASSISTANT_HELP
|
||||
assert target in AI_ASSISTANT_HELP
|
||||
|
||||
def test_devcontainer_kiro_installer_uses_pinned_checksum(self):
|
||||
"""Devcontainer installer should always verify Kiro installer via pinned SHA256."""
|
||||
post_create_text = (REPO_ROOT / ".devcontainer" / "post-create.sh").read_text(
|
||||
@@ -80,9 +73,9 @@ class TestAgentConfigConsistency:
|
||||
assert cfg["args"] == "{{args}}"
|
||||
assert cfg["extension"] == ".toml"
|
||||
|
||||
def test_ai_help_includes_tabnine(self):
|
||||
"""CLI help text for --ai should include tabnine."""
|
||||
assert "tabnine" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_tabnine(self):
|
||||
"""AGENT_CONFIG should include tabnine."""
|
||||
assert "tabnine" in AGENT_CONFIG
|
||||
|
||||
# --- Kimi Code CLI consistency checks ---
|
||||
|
||||
@@ -102,9 +95,9 @@ class TestAgentConfigConsistency:
|
||||
assert kimi_cfg["dir"] == ".kimi/skills"
|
||||
assert kimi_cfg["extension"] == "/SKILL.md"
|
||||
|
||||
def test_ai_help_includes_kimi(self):
|
||||
"""CLI help text for --ai should include kimi."""
|
||||
assert "kimi" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_kimi(self):
|
||||
"""AGENT_CONFIG should include kimi."""
|
||||
assert "kimi" in AGENT_CONFIG
|
||||
|
||||
# --- Trae IDE consistency checks ---
|
||||
|
||||
@@ -126,9 +119,9 @@ class TestAgentConfigConsistency:
|
||||
assert trae_cfg["args"] == "$ARGUMENTS"
|
||||
assert trae_cfg["extension"] == "/SKILL.md"
|
||||
|
||||
def test_ai_help_includes_trae(self):
|
||||
"""CLI help text for --ai should include trae."""
|
||||
assert "trae" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_trae(self):
|
||||
"""AGENT_CONFIG should include trae."""
|
||||
assert "trae" in AGENT_CONFIG
|
||||
|
||||
# --- Pi Coding Agent consistency checks ---
|
||||
|
||||
@@ -151,9 +144,9 @@ class TestAgentConfigConsistency:
|
||||
assert pi_cfg["args"] == "$ARGUMENTS"
|
||||
assert pi_cfg["extension"] == ".md"
|
||||
|
||||
def test_ai_help_includes_pi(self):
|
||||
"""CLI help text for --ai should include pi."""
|
||||
assert "pi" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_pi(self):
|
||||
"""AGENT_CONFIG should include pi."""
|
||||
assert "pi" in AGENT_CONFIG
|
||||
|
||||
# --- iFlow CLI consistency checks ---
|
||||
|
||||
@@ -173,9 +166,9 @@ class TestAgentConfigConsistency:
|
||||
assert cfg["iflow"]["format"] == "markdown"
|
||||
assert cfg["iflow"]["args"] == "$ARGUMENTS"
|
||||
|
||||
def test_ai_help_includes_iflow(self):
|
||||
"""CLI help text for --ai should include iflow."""
|
||||
assert "iflow" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_iflow(self):
|
||||
"""AGENT_CONFIG should include iflow."""
|
||||
assert "iflow" in AGENT_CONFIG
|
||||
|
||||
# --- Goose consistency checks ---
|
||||
|
||||
@@ -195,9 +188,9 @@ class TestAgentConfigConsistency:
|
||||
assert cfg["goose"]["format"] == "yaml"
|
||||
assert cfg["goose"]["args"] == "{{args}}"
|
||||
|
||||
def test_ai_help_includes_goose(self):
|
||||
"""CLI help text for --ai should include goose."""
|
||||
assert "goose" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_goose(self):
|
||||
"""AGENT_CONFIG should include goose."""
|
||||
assert "goose" in AGENT_CONFIG
|
||||
|
||||
# --- invoke_separator propagation checks ---
|
||||
|
||||
@@ -304,6 +297,6 @@ class TestAgentConfigConsistency:
|
||||
assert rovodev_cfg["args"] == "$ARGUMENTS"
|
||||
assert rovodev_cfg["extension"] == "/SKILL.md"
|
||||
|
||||
def test_ai_help_includes_rovodev(self):
|
||||
"""CLI help text for --ai should include rovodev."""
|
||||
assert "rovodev" in AI_ASSISTANT_HELP
|
||||
def test_agent_config_includes_rovodev(self):
|
||||
"""AGENT_CONFIG should include rovodev."""
|
||||
assert "rovodev" in AGENT_CONFIG
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestSaveBranchNumbering:
|
||||
|
||||
project_dir = tmp_path / "proj"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
result = runner.invoke(app, ["init", str(project_dir), "--integration", "claude", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
saved = json.loads((project_dir / ".specify/init-options.json").read_text())
|
||||
@@ -51,7 +51,7 @@ class TestBranchNumberingValidation:
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--ai", "claude", "--branch-numbering", "foobar", "--ignore-agent-tools"])
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "foobar", "--ignore-agent-tools"])
|
||||
assert result.exit_code == 1
|
||||
assert "Invalid --branch-numbering" in result.output
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestBranchNumberingValidation:
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--ai", "claude", "--branch-numbering", "sequential", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "sequential", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
assert result.exit_code == 0
|
||||
assert "Invalid --branch-numbering" not in (result.output or "")
|
||||
|
||||
@@ -69,6 +69,6 @@ class TestBranchNumberingValidation:
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--ai", "claude", "--branch-numbering", "timestamp", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "timestamp", "--ignore-agent-tools", "--no-git", "--script", "sh"])
|
||||
assert result.exit_code == 0
|
||||
assert "Invalid --branch-numbering" not in (result.output or "")
|
||||
|
||||
@@ -16,14 +16,10 @@ def test_commands_init_importable():
|
||||
def test_agent_config_importable():
|
||||
from specify_cli._agent_config import (
|
||||
AGENT_CONFIG,
|
||||
AI_ASSISTANT_ALIASES,
|
||||
AI_ASSISTANT_HELP,
|
||||
DEFAULT_INIT_INTEGRATION,
|
||||
SCRIPT_TYPE_CHOICES,
|
||||
)
|
||||
assert isinstance(AGENT_CONFIG, dict)
|
||||
assert isinstance(AI_ASSISTANT_ALIASES, dict)
|
||||
assert isinstance(AI_ASSISTANT_HELP, str)
|
||||
assert DEFAULT_INIT_INTEGRATION == "copilot"
|
||||
assert "sh" in SCRIPT_TYPE_CHOICES
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Unit tests for extension skill auto-registration.
|
||||
|
||||
Tests cover:
|
||||
- SKILL.md generation when --ai-skills was used during init
|
||||
- SKILL.md generation when skills mode was used during init
|
||||
- No skills created when ai_skills not active
|
||||
- SKILL.md content correctness
|
||||
- Existing user-modified skills not overwritten
|
||||
@@ -162,7 +162,7 @@ def extension_dir(temp_dir):
|
||||
|
||||
@pytest.fixture
|
||||
def skills_project(project_dir):
|
||||
"""Create a project with --ai-skills enabled and skills directory."""
|
||||
"""Create a project with skills mode enabled and skills directory."""
|
||||
_create_init_options(project_dir, ai="claude", ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai="claude")
|
||||
return project_dir, skills_dir
|
||||
@@ -170,7 +170,7 @@ def skills_project(project_dir):
|
||||
|
||||
@pytest.fixture
|
||||
def no_skills_project(project_dir):
|
||||
"""Create a project without --ai-skills."""
|
||||
"""Create a project without skills mode."""
|
||||
_create_init_options(project_dir, ai="claude", ai_skills=False)
|
||||
return project_dir
|
||||
|
||||
|
||||
@@ -4932,7 +4932,7 @@ class TestHookInvocationRendering:
|
||||
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-plan" in message
|
||||
|
||||
def test_codex_hooks_render_dollar_skill_invocation(self, project_dir):
|
||||
"""Codex projects with --ai-skills should render $speckit-* invocations."""
|
||||
"""Codex projects with skills mode should render $speckit-* invocations."""
|
||||
init_options = project_dir / ".specify" / "init-options.json"
|
||||
init_options.parent.mkdir(parents=True, exist_ok=True)
|
||||
init_options.write_text(json.dumps({"ai": "codex", "ai_skills": True}))
|
||||
|
||||
@@ -2557,8 +2557,8 @@ class TestPresetSkills:
|
||||
return preset_dir
|
||||
|
||||
def test_skill_overridden_on_preset_install(self, project_dir, temp_dir):
|
||||
"""When --ai-skills was used, a preset command override should update the skill."""
|
||||
# Simulate --ai-skills having been used: write init-options + create skill
|
||||
"""When skills mode was used, a preset command override should update the skill."""
|
||||
# Simulate skills mode having been used: write init-options + create skill
|
||||
self._write_init_options(project_dir, ai="claude")
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify")
|
||||
@@ -2843,7 +2843,7 @@ class TestPresetSkills:
|
||||
assert "override taskstoissues body" in content
|
||||
|
||||
def test_skill_not_updated_when_ai_skills_disabled(self, project_dir, temp_dir):
|
||||
"""When --ai-skills was NOT used, preset install should not touch skills."""
|
||||
"""When skills mode was NOT used, preset install should not touch skills."""
|
||||
self._write_init_options(project_dir, ai="qwen", ai_skills=False)
|
||||
skills_dir = project_dir / ".qwen" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify", body="untouched")
|
||||
@@ -2962,7 +2962,7 @@ class TestPresetSkills:
|
||||
def test_no_skills_registered_when_no_skill_dir_exists(self, project_dir, temp_dir):
|
||||
"""Skills should not be created when no existing skill dir is found."""
|
||||
self._write_init_options(project_dir, ai="claude")
|
||||
# Don't create skills dir — simulate --ai-skills never created them
|
||||
# Don't create skills dir — simulate skills mode never created them
|
||||
|
||||
manager = PresetManager(project_dir)
|
||||
install_self_test_preset(manager)
|
||||
@@ -4123,7 +4123,7 @@ class TestWrapStrategy:
|
||||
"---\ndescription: core wrap-test\n---\n\n# Core Wrap-Test Body\n"
|
||||
)
|
||||
|
||||
# Set up skills dir (simulating --ai claude)
|
||||
# Set up skills dir (simulating --integration claude)
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
skill_subdir = skills_dir / "speckit-wrap-test"
|
||||
|
||||
Reference in New Issue
Block a user