Skip to content

Commit 4deb90f

Browse files
authored
fix: restore alias compatibility for community extensions (#2110) (#2125)
Relax alias validation in _collect_manifest_command_names() to only enforce the 3-part speckit.{ext}.{cmd} pattern on primary command names. Aliases retain type and duplicate checking but are otherwise free-form, restoring pre-#1994 behavior. This unblocks community extensions (e.g. spec-kit-verify) that use 2-part aliases like 'speckit.verify'. Fixes #2110
1 parent 4d58ee9 commit 4deb90f

File tree

2 files changed

+25
-21
lines changed

2 files changed

+25
-21
lines changed

src/specify_cli/extensions.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -523,10 +523,11 @@ def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, st
523523
"""Collect command and alias names declared by a manifest.
524524
525525
Performs install-time validation for extension-specific constraints:
526-
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape
527-
- commands and aliases must use this extension's namespace
526+
- primary commands must use the canonical `speckit.{extension}.{command}` shape
527+
- primary commands must use this extension's namespace
528528
- command namespaces must not shadow core commands
529529
- duplicate command/alias names inside one manifest are rejected
530+
- aliases are validated for type and uniqueness only (no pattern enforcement)
530531
531532
Args:
532533
manifest: Parsed extension manifest
@@ -563,23 +564,26 @@ def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, st
563564
f"{kind.capitalize()} for command '{primary_name}' must be a string"
564565
)
565566

566-
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
567-
if match is None:
568-
raise ValidationError(
569-
f"Invalid {kind} '{name}': "
570-
"must follow pattern 'speckit.{extension}.{command}'"
571-
)
567+
# Enforce canonical pattern only for primary command names;
568+
# aliases are free-form to preserve community extension compat.
569+
if kind == "command":
570+
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
571+
if match is None:
572+
raise ValidationError(
573+
f"Invalid {kind} '{name}': "
574+
"must follow pattern 'speckit.{extension}.{command}'"
575+
)
572576

573-
namespace = match.group(1)
574-
if namespace != manifest.id:
575-
raise ValidationError(
576-
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
577-
)
577+
namespace = match.group(1)
578+
if namespace != manifest.id:
579+
raise ValidationError(
580+
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
581+
)
578582

579-
if namespace in CORE_COMMAND_NAMES:
580-
raise ValidationError(
581-
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
582-
)
583+
if namespace in CORE_COMMAND_NAMES:
584+
raise ValidationError(
585+
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
586+
)
583587

584588
if name in declared_names:
585589
raise ValidationError(

tests/test_extensions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,8 @@ def test_install_rejects_extension_id_in_core_namespace(self, temp_dir, project_
686686
with pytest.raises(ValidationError, match="conflicts with core command namespace"):
687687
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
688688

689-
def test_install_rejects_alias_without_extension_namespace(self, temp_dir, project_dir):
690-
"""Install should reject legacy short aliases that can shadow core commands."""
689+
def test_install_accepts_short_alias(self, temp_dir, project_dir):
690+
"""Install should accept legacy short aliases for community extension compat."""
691691
import yaml
692692

693693
ext_dir = temp_dir / "alias-shortcut"
@@ -718,8 +718,8 @@ def test_install_rejects_alias_without_extension_namespace(self, temp_dir, proje
718718
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
719719

720720
manager = ExtensionManager(project_dir)
721-
with pytest.raises(ValidationError, match="Invalid alias 'speckit.shortcut'"):
722-
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
721+
# Should not raise — short aliases are allowed
722+
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
723723

724724
def test_install_rejects_namespace_squatting(self, temp_dir, project_dir):
725725
"""Install should reject commands and aliases outside the extension namespace."""

0 commit comments

Comments
 (0)