Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
7 changes: 7 additions & 0 deletions extensions/git/extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ extension:
author: spec-kit-core
repository: https://github.com/github/spec-kit
license: MIT
install_notice: |
The git extension is currently enabled by default, but starting with
specify-cli v1.0.0 it will require explicit opt-in.
To opt in after specify-cli v1.0.0:
• specify init --extension git
• specify extension add git (post-init)
requires:
speckit_version: ">=0.2.0"
Expand Down
17 changes: 16 additions & 1 deletion src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,8 @@ def init(

ensure_constitution_from_template(project_path, tracker=tracker)

_git_ext_freshly_installed = False
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can this live in extensions.py somehow? I want extensions to be handled by just one point in the code. If not possible, please explain why.

_git_ext_install_notice: str | None = None
if not no_git:
tracker.start("git")
git_messages = []
Expand Down Expand Up @@ -1307,10 +1309,12 @@ def init(
if manager.registry.is_installed("git"):
git_messages.append("extension already installed")
else:
manager.install_from_directory(
ext_manifest = manager.install_from_directory(
bundled_path, get_speckit_version()
)
git_messages.append("extension installed")
_git_ext_freshly_installed = True
_git_ext_install_notice = ext_manifest.install_notice
else:
git_has_error = True
git_messages.append("bundled extension not found")
Expand Down Expand Up @@ -1454,6 +1458,17 @@ def init(
console.print(tracker.render())
console.print("\n[bold green]Project ready.[/bold green]")

if _git_ext_freshly_installed and _git_ext_install_notice:
console.print()
console.print(
Panel(
_git_ext_install_notice.strip(),
title="[yellow]⚠ Deprecation notice: git extension[/yellow]",
Comment thread
aaronrsun marked this conversation as resolved.
Outdated
border_style="yellow",
padding=(1, 2),
)
)

# Agent folder security notice
agent_config = AGENT_CONFIG.get(selected_ai)
if agent_config:
Expand Down
9 changes: 9 additions & 0 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,15 @@ def hooks(self) -> Dict[str, Any]:
"""Get hook definitions."""
return self.data.get("hooks", {})

@property
def install_notice(self) -> str | None:
"""Get optional install notice message.

Extensions can specify an 'install_notice' field to display
important information to users when the extension is first installed.
"""
return self.data.get("extension", {}).get("install_notice")
Comment thread
aaronrsun marked this conversation as resolved.
Outdated

def get_hash(self) -> str:
"""Calculate SHA256 hash of manifest file."""
with open(self.path, 'rb') as f:
Expand Down
115 changes: 115 additions & 0 deletions tests/extensions/git/test_git_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,118 @@ def test_test_feature_branch_accepts_single_prefix(self, tmp_path: Path):
text=True,
)
assert result.returncode == 0


# ── Deprecation Notice Tests ──────────────────────────────────────────────────


class TestGitExtDeprecationNotice:
"""Tests for the v1.0.0 deprecation notice shown during specify init."""

def test_deprecation_notice_shown_on_fresh_install(self, tmp_path: Path):
"""specify init shows the git extension deprecation notice on first install."""
Comment thread
aaronrsun marked this conversation as resolved.
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_manifest = MagicMock()
mock_manifest.install_notice = (
"The git extension is currently enabled by default, but starting with\n"
"specify-cli v1.0.0 it will require explicit opt-in.\n\n"
"To opt in after specify-cli v1.0.0:\n"
" • specify init --extension git\n"
" • specify extension add git (post-init)"
)

mock_registry = MagicMock()
mock_registry.is_installed.return_value = False

mock_manager = MagicMock()
mock_manager.registry = mock_registry
mock_manager.install_from_directory.return_value = mock_manifest

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)
Comment thread
aaronrsun marked this conversation as resolved.
Outdated

assert result.exit_code == 0, result.output
assert "Deprecation notice: git extension" in result.output
assert "v1.0.0" in result.output
assert "specify extension add git" in result.output

def test_deprecation_notice_not_shown_when_already_installed(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice when git extension is already installed."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_registry = MagicMock()
mock_registry.is_installed.return_value = True

mock_manager = MagicMock()
mock_manager.registry = mock_registry

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git extension" not in result.output

def test_deprecation_notice_not_shown_with_no_git_flag(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice when --no-git is passed."""
from typer.testing import CliRunner
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--no-git", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git extension" not in result.output

def test_deprecation_notice_not_shown_when_no_install_notice(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice if extension has no install_notice."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_manifest = MagicMock()
mock_manifest.install_notice = None # No notice defined

mock_registry = MagicMock()
mock_registry.is_installed.return_value = False

mock_manager = MagicMock()
mock_manager.registry = mock_registry
mock_manager.install_from_directory.return_value = mock_manifest

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git extension" not in result.output