|
25 | 25 | from packaging import version as pkg_version |
26 | 26 | from packaging.specifiers import SpecifierSet, InvalidSpecifier |
27 | 27 |
|
28 | | -CORE_COMMAND_NAMES = frozenset({ |
| 28 | +_FALLBACK_CORE_COMMAND_NAMES = frozenset({ |
29 | 29 | "analyze", |
30 | 30 | "checklist", |
31 | 31 | "clarify", |
|
39 | 39 | EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$") |
40 | 40 |
|
41 | 41 |
|
| 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 | + |
42 | 72 | class ExtensionError(Exception): |
43 | 73 | """Base exception for extension-related errors.""" |
44 | 74 | pass |
@@ -463,9 +493,9 @@ def __init__(self, project_root: Path): |
463 | 493 | def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, str]: |
464 | 494 | """Collect command and alias names declared by a manifest. |
465 | 495 |
|
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 |
469 | 499 | - command namespaces must not shadow core commands |
470 | 500 | - duplicate command/alias names inside one manifest are rejected |
471 | 501 |
|
@@ -512,6 +542,11 @@ def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, st |
512 | 542 | ) |
513 | 543 |
|
514 | 544 | 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 | + |
515 | 550 | if namespace in CORE_COMMAND_NAMES: |
516 | 551 | raise ValidationError( |
517 | 552 | f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'" |
|
0 commit comments