Skip to content

Commit 3571f75

Browse files
committed
Validate extension command namespaces
1 parent b210d9b commit 3571f75

5 files changed

Lines changed: 147 additions & 62 deletions

File tree

extensions/EXTENSION-API-REFERENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ provides:
4444
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
4545
file: string # Required, relative path to command file
4646
description: string # Required
47-
aliases: [string] # Optional, same pattern as name; must not shadow core commands
47+
aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
4848

4949
config: # Optional, array of config files
5050
- name: string # Config file name

extensions/EXTENSION-DEVELOPMENT-GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ What the extension provides.
186186
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
187187
- `file`: Path to command file (relative to extension root)
188188
- `description`: Command description (optional)
189-
- `aliases`: Alternative command names (optional, array)
189+
- `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`)
190190

191191
### Optional Fields
192192

extensions/RFC-EXTENSION-SYSTEM.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,18 +1517,18 @@ specify extension add github-projects
15171517
/speckit.github.taskstoissues
15181518
```
15191519

1520-
**Compatibility shim** (if needed):
1520+
**Migration alias** (if needed):
15211521

15221522
```yaml
15231523
# extension.yml
15241524
provides:
15251525
commands:
15261526
- name: "speckit.github.taskstoissues"
15271527
file: "commands/taskstoissues.md"
1528-
aliases: ["speckit.github.sync-taskstoissues"] # Backward compatibility
1528+
aliases: ["speckit.github.sync-taskstoissues"] # Alternate namespaced entry point
15291529
```
15301530

1531-
AI agent registers both names, so old scripts work.
1531+
AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like `/speckit.taskstoissues`.
15321532

15331533
---
15341534

src/specify_cli/extensions.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from packaging import version as pkg_version
2626
from packaging.specifiers import SpecifierSet, InvalidSpecifier
2727

28-
CORE_COMMAND_NAMES = frozenset({
28+
_FALLBACK_CORE_COMMAND_NAMES = frozenset({
2929
"analyze",
3030
"checklist",
3131
"clarify",
@@ -39,6 +39,36 @@
3939
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
4040

4141

42+
def _load_core_command_names() -> frozenset[str]:
43+
"""Discover bundled core command names from the packaged templates.
44+
45+
Prefer the wheel-time ``core_pack`` bundle when present, and fall back to
46+
the source checkout when running from the repository. If neither is
47+
available, use the baked-in fallback set so validation still works.
48+
"""
49+
candidate_dirs = [
50+
Path(__file__).parent / "core_pack" / "commands",
51+
Path(__file__).resolve().parent.parent.parent / "templates" / "commands",
52+
]
53+
54+
for commands_dir in candidate_dirs:
55+
if not commands_dir.is_dir():
56+
continue
57+
58+
command_names = {
59+
command_file.stem
60+
for command_file in commands_dir.iterdir()
61+
if command_file.is_file() and command_file.suffix == ".md"
62+
}
63+
if command_names:
64+
return frozenset(command_names)
65+
66+
return _FALLBACK_CORE_COMMAND_NAMES
67+
68+
69+
CORE_COMMAND_NAMES = _load_core_command_names()
70+
71+
4272
class ExtensionError(Exception):
4373
"""Base exception for extension-related errors."""
4474
pass
@@ -463,9 +493,9 @@ def __init__(self, project_root: Path):
463493
def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, str]:
464494
"""Collect command and alias names declared by a manifest.
465495
466-
Performs install-time validation for extension-specific constraints that
467-
should not invalidate already-installed legacy manifests:
468-
- aliases must use the canonical `speckit.{extension}.{command}` shape
496+
Performs install-time validation for extension-specific constraints:
497+
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape
498+
- commands and aliases must use this extension's namespace
469499
- command namespaces must not shadow core commands
470500
- duplicate command/alias names inside one manifest are rejected
471501
@@ -512,6 +542,11 @@ def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, st
512542
)
513543

514544
namespace = match.group(1)
545+
if namespace != manifest.id:
546+
raise ValidationError(
547+
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
548+
)
549+
515550
if namespace in CORE_COMMAND_NAMES:
516551
raise ValidationError(
517552
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"

0 commit comments

Comments
 (0)