Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
487c8db
docs: generate integrations reference from catalog
DyanGalih May 14, 2026
fc0f3bf
refactor: integrate table rendering into specify integration search -…
DyanGalih May 14, 2026
863c7c7
fix: address Copilot review feedback on catalog_docs and integration_…
DyanGalih May 14, 2026
6965eda
fix: add sync test, INTEGRATIONS_REFERENCE_PATH constant, and fix naming
DyanGalih May 15, 2026
f150f02
revert: restore docs/reference/integrations.md to upstream/main; remo…
DyanGalih May 15, 2026
67cdfd7
fix: remove dead INTEGRATIONS_REFERENCE_PATH, drop URL-length padding…
DyanGalih May 15, 2026
894b7ae
fix: send --markdown warnings/errors to stderr, rename test for clarity
DyanGalih May 15, 2026
a48a2ae
fix: detect stale doc-map keys, test _render_cell escaping, strengthe…
DyanGalih May 15, 2026
8e4cd52
refactor: promote _render_cell to public render_cell function
DyanGalih May 15, 2026
a025d91
test: mock registry and doc maps to avoid brittle live registry coupling
DyanGalih May 15, 2026
f905184
refactor: flatten patches, remove unused imports, fix trailing whites…
DyanGalih May 15, 2026
8a67660
refactor: make validation non-fatal, fix context manager syntax, add …
DyanGalih May 15, 2026
1edc828
fix: improve docstring clarity, test robustness, and exception handling
DyanGalih May 15, 2026
870f07d
fix: improve test assertions, disable warnings by default, enhance ex…
DyanGalih May 15, 2026
7ef3362
fix: make CLI tests deterministic and improve config access resilience
DyanGalih May 15, 2026
6efe138
fix: remove extra blank line, add stale keys validation, add regressi…
DyanGalih May 16, 2026
4229e96
Fix 5 remaining feedback items:
DyanGalih May 16, 2026
234a9f6
address all outstanding copilot review feedback on PR 2563
DyanGalih May 18, 2026
7382206
Address Copilot feedback: escape URLs in markdown links, deduplicate …
DyanGalih May 18, 2026
ee30870
Address 3 new Copilot feedback: add URL escaping test, fix parse_firs…
DyanGalih May 18, 2026
34d22b6
Address 3 new Copilot feedback: escape id field, remove unused alias,…
DyanGalih May 18, 2026
99b3b99
Address 3 new Copilot feedback: fix comment name, include all integra…
DyanGalih May 18, 2026
4a68e82
Fix architectural issue: escape raw fields before composing Markdown …
DyanGalih May 18, 2026
4e60369
Deduplicate _escape_url_for_markdown_link and add URL escaping test
DyanGalih May 18, 2026
2a54d7d
Address 4 new Copilot feedback: add trailing newline, fix test helper…
DyanGalih May 18, 2026
4589bc4
Address 4 new Copilot feedback: make escape function public, fix erro…
DyanGalih May 18, 2026
70b512f
Update error message in test_missing_catalog_file for clarity
DyanGalih May 19, 2026
ed78679
Remove obsolete integrations sync test
DyanGalih May 20, 2026
777df6a
keep integrations docs in sync
DyanGalih May 21, 2026
216e93a
fix: allow prerelease spec-kit versions in compatibility checks
DyanGalih May 24, 2026
f44cbec
fix: isolate prerelease compatibility gate changes
DyanGalih May 24, 2026
032ef4c
Address PR 2695 feedback: Centralize prerelease policy and add bounda…
DyanGalih Jun 26, 2026
1f6fafe
Address remaining Copilot PR feedback: revert docs and add preset pre…
DyanGalih Jun 26, 2026
c2793bd
Remove unreachable raise CompatibilityError
DyanGalih Jun 26, 2026
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
1 change: 0 additions & 1 deletion src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,6 @@ def _require_specify_project() -> Path:
raise typer.Exit(1)



# ===== Preset Commands =====

# Moved to presets/_commands.py — registered here to preserve CLI surface.
Expand Down
23 changes: 23 additions & 0 deletions src/specify_cli/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,26 @@ def _display_project_path(project_root: Path, path: str | Path) -> str:
except (OSError, ValueError):
return path_obj.as_posix()
return rel_path.as_posix()

def version_satisfies(current: str, required: str) -> bool:
Comment on lines 306 to +308
"""Check if current version satisfies required version specifier.

Evaluates the version against the specifier using the project's
prerelease policy (prereleases are allowed).

Args:
current: Current version (e.g., "0.1.5")
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")

Returns:
True if version satisfies requirement
"""
from packaging import version as pkg_version
from packaging.specifiers import InvalidSpecifier, SpecifierSet

try:
current_ver = pkg_version.Version(current)
specifier = SpecifierSet(required)
return specifier.contains(current_ver, prereleases=True)
except (pkg_version.InvalidVersion, InvalidSpecifier):
return False
34 changes: 9 additions & 25 deletions src/specify_cli/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from .._init_options import is_ai_skills_enabled
from .._invocation_style import is_dollar_skills_agent, is_slash_skills_agent
from .._utils import dump_frontmatter, relative_extension_path_violation
from .._utils import dump_frontmatter, relative_extension_path_violation, version_satisfies
from ..catalogs import CatalogEntry as BaseCatalogEntry
from ..catalogs import CatalogStackBase
from ..shared_infra import verify_archive_sha256
Expand Down Expand Up @@ -1279,20 +1279,20 @@ def check_compatibility(
CompatibilityError: If extension is incompatible
"""
required = manifest.requires_speckit_version
current = pkg_version.Version(speckit_version)

# Parse version specifier (e.g., ">=0.1.0,<2.0.0")
try:
specifier = SpecifierSet(required)
if current not in specifier:
raise CompatibilityError(
f"Extension requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
SpecifierSet(required) # Just to validate
except InvalidSpecifier:
raise CompatibilityError(f"Invalid version specifier: {required}")

if not version_satisfies(speckit_version, required):
raise CompatibilityError(
f"Extension requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)

return True

def install_from_directory(
Expand Down Expand Up @@ -1871,22 +1871,6 @@ def get_extension(self, extension_id: str) -> Optional[ExtensionManifest]:
return None


def version_satisfies(current: str, required: str) -> bool:
"""Check if current version satisfies required version specifier.

Args:
current: Current version (e.g., "0.1.5")
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")

Returns:
True if version satisfies requirement
"""
try:
current_ver = pkg_version.Version(current)
specifier = SpecifierSet(required)
return current_ver in specifier
except (pkg_version.InvalidVersion, InvalidSpecifier):
return False


Comment on lines 1871 to 1875
class CommandRegistrar:
Expand Down
19 changes: 8 additions & 11 deletions src/specify_cli/presets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ..extensions import REINSTALL_COMMAND, ExtensionRegistry, normalize_priority
from .._init_options import is_ai_skills_enabled
from ..integrations.base import IntegrationBase
from .._utils import dump_frontmatter
from .._utils import dump_frontmatter, version_satisfies
from ..shared_infra import verify_archive_sha256


Expand Down Expand Up @@ -572,19 +572,16 @@ def check_compatibility(
PresetCompatibilityError: If pack is incompatible
"""
required = manifest.requires_speckit_version
current = pkg_version.Version(speckit_version)

try:
specifier = SpecifierSet(required)
if current not in specifier:
raise PresetCompatibilityError(
f"Preset requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
SpecifierSet(required) # Just to validate
except InvalidSpecifier:
raise PresetCompatibilityError(f"Invalid version specifier: {required}")

if not version_satisfies(speckit_version, required):
Comment thread
DyanGalih marked this conversation as resolved.
raise PresetCompatibilityError(
f"Invalid version specifier: {required}"
f"Preset requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)

return True
Expand Down
16 changes: 15 additions & 1 deletion tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
ValidationError,
CompatibilityError,
normalize_priority,
version_satisfies,
)
from specify_cli._utils import version_satisfies


def can_create_symlink(tmp_path: Path) -> bool:
Expand Down Expand Up @@ -1001,6 +1001,14 @@ def test_check_compatibility_invalid(self, extension_dir, project_dir):
with pytest.raises(CompatibilityError, match="Extension requires spec-kit"):
manager.check_compatibility(manifest, "0.0.1")

def test_check_compatibility_allows_prerelease_builds(self, extension_dir, project_dir):
"""Prerelease spec-kit builds should satisfy compatible version ranges."""
manager = ExtensionManager(project_dir)
manifest = ExtensionManifest(extension_dir / "extension.yml")

result = manager.check_compatibility(manifest, "0.8.8.dev0")
assert result is True

def test_install_from_directory(self, extension_dir, project_dir):
"""Test installing extension from directory."""
manager = ExtensionManager(project_dir)
Expand Down Expand Up @@ -2625,6 +2633,12 @@ def test_version_satisfies_complex(self):
assert version_satisfies("1.0.5", ">=1.0.0,!=1.0.3")
assert not version_satisfies("1.0.3", ">=1.0.0,!=1.0.3")

def test_version_satisfies_prerelease(self):
"""Prerelease builds should satisfy compatible lower bounds, but not higher bounds."""
assert version_satisfies("0.8.8.dev0", ">=0.2.0")
assert not version_satisfies("0.2.0.dev0", ">=0.2.0")
assert not version_satisfies("0.8.7.dev1", ">=0.8.8")

def test_version_satisfies_invalid(self):
"""Test invalid version strings."""
assert not version_satisfies("invalid", ">=1.0.0")
Expand Down
9 changes: 9 additions & 0 deletions tests/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,15 @@ def test_check_compatibility_valid(self, pack_dir, temp_dir):
manifest = PresetManifest(pack_dir / "preset.yml")
assert manager.check_compatibility(manifest, "0.1.5") is True

def test_check_compatibility_prerelease(self, pack_dir, temp_dir):
"""Test compatibility check allows prereleases and fails on boundary."""
manager = PresetManager(temp_dir)
manifest = PresetManifest(pack_dir / "preset.yml")
# manifest requires >=0.1.0
assert manager.check_compatibility(manifest, "0.8.8.dev0") is True
with pytest.raises(PresetCompatibilityError, match="Preset requires spec-kit"):
manager.check_compatibility(manifest, "0.1.0.dev0")

def test_check_compatibility_invalid(self, pack_dir, temp_dir):
"""Test compatibility check with invalid specifier."""
manager = PresetManager(temp_dir)
Expand Down