mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
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
This commit is contained in:
@@ -523,10 +523,11 @@ class ExtensionManager:
|
|||||||
"""Collect command and alias names declared by a manifest.
|
"""Collect command and alias names declared by a manifest.
|
||||||
|
|
||||||
Performs install-time validation for extension-specific constraints:
|
Performs install-time validation for extension-specific constraints:
|
||||||
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape
|
- primary commands must use the canonical `speckit.{extension}.{command}` shape
|
||||||
- commands and aliases must use this extension's namespace
|
- primary commands must use this extension's namespace
|
||||||
- command namespaces must not shadow core commands
|
- command namespaces must not shadow core commands
|
||||||
- duplicate command/alias names inside one manifest are rejected
|
- duplicate command/alias names inside one manifest are rejected
|
||||||
|
- aliases are validated for type and uniqueness only (no pattern enforcement)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
manifest: Parsed extension manifest
|
manifest: Parsed extension manifest
|
||||||
@@ -563,23 +564,26 @@ class ExtensionManager:
|
|||||||
f"{kind.capitalize()} for command '{primary_name}' must be a string"
|
f"{kind.capitalize()} for command '{primary_name}' must be a string"
|
||||||
)
|
)
|
||||||
|
|
||||||
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
|
# Enforce canonical pattern only for primary command names;
|
||||||
if match is None:
|
# aliases are free-form to preserve community extension compat.
|
||||||
raise ValidationError(
|
if kind == "command":
|
||||||
f"Invalid {kind} '{name}': "
|
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
|
||||||
"must follow pattern 'speckit.{extension}.{command}'"
|
if match is None:
|
||||||
)
|
raise ValidationError(
|
||||||
|
f"Invalid {kind} '{name}': "
|
||||||
|
"must follow pattern 'speckit.{extension}.{command}'"
|
||||||
|
)
|
||||||
|
|
||||||
namespace = match.group(1)
|
namespace = match.group(1)
|
||||||
if namespace != manifest.id:
|
if namespace != manifest.id:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
|
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if namespace in CORE_COMMAND_NAMES:
|
if namespace in CORE_COMMAND_NAMES:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
|
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if name in declared_names:
|
if name in declared_names:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|||||||
@@ -686,8 +686,8 @@ class TestExtensionManager:
|
|||||||
with pytest.raises(ValidationError, match="conflicts with core command namespace"):
|
with pytest.raises(ValidationError, match="conflicts with core command namespace"):
|
||||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
def test_install_rejects_alias_without_extension_namespace(self, temp_dir, project_dir):
|
def test_install_accepts_short_alias(self, temp_dir, project_dir):
|
||||||
"""Install should reject legacy short aliases that can shadow core commands."""
|
"""Install should accept legacy short aliases for community extension compat."""
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
ext_dir = temp_dir / "alias-shortcut"
|
ext_dir = temp_dir / "alias-shortcut"
|
||||||
@@ -718,8 +718,8 @@ class TestExtensionManager:
|
|||||||
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
|
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
|
||||||
|
|
||||||
manager = ExtensionManager(project_dir)
|
manager = ExtensionManager(project_dir)
|
||||||
with pytest.raises(ValidationError, match="Invalid alias 'speckit.shortcut'"):
|
# Should not raise — short aliases are allowed
|
||||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
def test_install_rejects_namespace_squatting(self, temp_dir, project_dir):
|
def test_install_rejects_namespace_squatting(self, temp_dir, project_dir):
|
||||||
"""Install should reject commands and aliases outside the extension namespace."""
|
"""Install should reject commands and aliases outside the extension namespace."""
|
||||||
|
|||||||
Reference in New Issue
Block a user