Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,13 @@ specify init --here --force

![Specify CLI bootstrapping a new project in the terminal](./media/specify_cli.gif)

In an interactive terminal, you will be prompted to select the coding agent integration you are using. In non-interactive sessions, such as CI or piped runs, `specify init` defaults to GitHub Copilot unless you pass `--integration`. You can also proactively specify the integration directly in the terminal:
In an interactive terminal, you will be prompted to select the coding agent integration you are using. In non-interactive sessions, such as CI or piped runs, `specify init` defaults to GitHub Copilot unless you pass `--integration` or the shorter `--ai` alias. You can also proactively specify the integration directly in the terminal:

```bash
specify init <project_name> --integration copilot
specify init <project_name> --integration gemini
specify init <project_name> --integration codex
specify init <project_name> --ai codex

# Or in current directory:
specify init . --integration copilot
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ specify init [<project_name>]

| Option | Description |
| ------------------------ | ------------------------------------------------------------------------ |
| `--integration <key>` | AI coding agent integration to use (e.g. `copilot`, `claude`, `gemini`). See the [Integrations reference](integrations.md) for all available keys |
| `--integration <key>` | AI coding agent integration to use (e.g. `copilot`, `claude`, `gemini`). Equivalent to `--ai <key>`; do not combine them. See the [Integrations reference](integrations.md) for all available keys |
| `--ai <key>` | Short alias for `--integration <key>`; do not combine with `--integration` |
| `--integration-options` | Options for the integration (e.g. `--integration-options="--commands-dir .myagent/cmds"`) |
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--here` | Initialize in the current directory instead of creating a new one |
Expand All @@ -28,13 +29,14 @@ Creates a new Spec Kit project with the necessary directory structure, templates

Use `<project_name>` to create a new directory, or `--here` (or `.`) to initialize in the current directory. If the directory already has files, use `--force` to merge without confirmation.

When `--integration` is omitted, interactive terminals prompt you to choose an integration. Non-interactive sessions, such as CI or piped runs, default to GitHub Copilot; pass `--integration <key>` to choose a different integration explicitly.
When `--integration` and `--ai` are omitted, interactive terminals prompt you to choose an integration. Non-interactive sessions, such as CI or piped runs, default to GitHub Copilot; pass `--integration <key>` or the equivalent shorter `--ai <key>` alias to choose a different integration explicitly. The two options select the same integration setting and may not be combined in the same invocation.

### Examples

```bash
# Create a new project with an integration
specify init my-project --integration copilot
specify init my-project --ai copilot

# Initialize in the current directory
specify init --here --integration copilot
Expand Down
7 changes: 5 additions & 2 deletions src/specify_cli/_agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ def _build_agent_config() -> dict[str, dict[str, Any]]:
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)."
f"Short alias for --integration. Choose: {', '.join(non_generic_agents)}, "
"or generic. For generic, provide a commands directory with "
"--ai-commands-dir; the --integration generic form can use "
'--integration-options="--commands-dir <dir>". Do not combine '
"--ai with --integration."
)
if not AI_ASSISTANT_ALIASES:
return base_help
Expand Down
44 changes: 1 addition & 43 deletions src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import annotations

import os
import shlex
import shutil
import sys
from pathlib import Path
Expand All @@ -28,32 +27,6 @@
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()

Expand Down Expand Up @@ -111,7 +84,7 @@ def init(
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="Coding agent integration to use (e.g. --integration copilot). --ai is a short alias for this option; do not combine them."),
integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'),
):
"""
Expand Down Expand Up @@ -160,7 +133,6 @@ def init(
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}'")
Expand Down Expand Up @@ -196,10 +168,6 @@ 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,
)

if ai_assistant or integration:
if ai_skills:
Expand Down Expand Up @@ -645,16 +613,6 @@ 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)

if git_default_notice:
default_change_notice = Panel(
"The git extension is currently enabled by default during [bold]specify init[/bold].\n"
Expand Down
56 changes: 41 additions & 15 deletions tests/integrations/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io
import json
import os
import re

import pytest
import yaml
Expand Down Expand Up @@ -42,6 +43,26 @@ def test_cli_phase_label_includes_target(self):
)


def _assert_no_ai_deprecation_output(output: str) -> None:
clean_output = strip_ansi(output)
lower_output = _normalize_cli_output(clean_output).lower()
assert "deprecation warning" not in lower_output
assert "no longer be available" not in lower_output
assert "commands will no longer be available" not in lower_output
assert "--ai is deprecated" not in lower_output
assert "deprecated --ai" not in lower_output

ai_related_blocks = [
_normalize_cli_output(block).lower()
for block in re.split(r"\n\s*\n", clean_output)
if "--ai" in block.lower()
]
for block in ai_related_blocks:
assert "deprecated" not in block
assert "0.10.0" not in block
assert "next steps" not in block
Comment on lines +55 to +63
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cf2295b. _assert_no_ai_deprecation_output now strips ANSI itself, normalizes only for whole-output assertions, and splits the raw cleaned output into blocks before checking AI-related sections. I also updated both callers to pass result.output instead of the already-normalized string. Verification: focused 3-test run passed, full tests/integrations/test_cli.py passed (77 tests), specify init --help ran, and git diff --check passed.



class TestInitIntegrationFlag:
def test_integration_and_ai_mutually_exclusive(self, tmp_path):
from typer.testing import CliRunner
Expand All @@ -53,6 +74,20 @@ def test_integration_and_ai_mutually_exclusive(self, tmp_path):
assert result.exit_code != 0
assert "mutually exclusive" in result.output

def test_init_help_explains_ai_alias_and_generic_options(self):
from typer.testing import CliRunner
from specify_cli import app

runner = CliRunner()
result = runner.invoke(app, ["init", "--help"])

normalized_output = _normalize_cli_output(result.output)
assert result.exit_code == 0, result.output
assert "Short alias for --integration" in normalized_output
assert "--ai-commands-dir" in normalized_output
assert "--integration generic" in normalized_output
assert "--integration-options" in normalized_output

def test_unknown_integration_rejected(self, tmp_path):
from typer.testing import CliRunner
from specify_cli import app
Expand Down Expand Up @@ -141,11 +176,11 @@ 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):
def test_ai_copilot_does_not_emit_deprecation_warning(self, tmp_path):
from typer.testing import CliRunner
from specify_cli import app

project = tmp_path / "warn-ai"
project = tmp_path / "keep-ai"
project.mkdir()
old_cwd = os.getcwd()
try:
Expand All @@ -159,20 +194,14 @@ def test_ai_emits_deprecation_warning_with_integration_replacement(self, tmp_pat

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_no_ai_deprecation_output(result.output)
assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists()

def test_ai_generic_warning_suggests_integration_options_equivalent(self, tmp_path):
def test_ai_generic_alias_does_not_emit_deprecation_warning(self, tmp_path):
from typer.testing import CliRunner
from specify_cli import app

project = tmp_path / "warn-generic"
project = tmp_path / "generic-ai"
project.mkdir()
old_cwd = os.getcwd()
try:
Expand All @@ -187,11 +216,8 @@ def test_ai_generic_warning_suggests_integration_options_equivalent(self, tmp_pa

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_no_ai_deprecation_output(result.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(
Expand Down