Register preset command overrides with all detected AI agents. Scans the preset's templates for type "command", reads each command file, and writes it to every detected agent directory using the CommandRegistrar from the agents module. When a command uses a composit
(
self,
manifest: PresetManifest,
preset_dir: Path
)
| 587 | return True |
| 588 | |
| 589 | def _register_commands( |
| 590 | self, |
| 591 | manifest: PresetManifest, |
| 592 | preset_dir: Path |
| 593 | ) -> Dict[str, List[str]]: |
| 594 | """Register preset command overrides with all detected AI agents. |
| 595 | |
| 596 | Scans the preset's templates for type "command", reads each command |
| 597 | file, and writes it to every detected agent directory using the |
| 598 | CommandRegistrar from the agents module. |
| 599 | |
| 600 | When a command uses a composition strategy (prepend, append, wrap), |
| 601 | the content is composed with the lower-priority command before |
| 602 | registration. |
| 603 | |
| 604 | Args: |
| 605 | manifest: Preset manifest |
| 606 | preset_dir: Installed preset directory |
| 607 | |
| 608 | Returns: |
| 609 | Dictionary mapping agent names to lists of registered command names |
| 610 | """ |
| 611 | command_templates = [ |
| 612 | t for t in manifest.templates if t.get("type") == "command" |
| 613 | ] |
| 614 | if not command_templates: |
| 615 | return {} |
| 616 | |
| 617 | # Filter out extension command overrides if the extension isn't installed. |
| 618 | # Command names follow the pattern: speckit.<ext-id>.<cmd-name> |
| 619 | # Core commands (e.g. speckit.specify) have only one dot — always register. |
| 620 | extensions_dir = self.project_root / ".specify" / "extensions" |
| 621 | filtered = [] |
| 622 | for cmd in command_templates: |
| 623 | parts = cmd["name"].split(".") |
| 624 | if len(parts) >= 3 and parts[0] == "speckit": |
| 625 | ext_id = parts[1] |
| 626 | if not (extensions_dir / ext_id).is_dir(): |
| 627 | continue |
| 628 | filtered.append(cmd) |
| 629 | |
| 630 | if not filtered: |
| 631 | return {} |
| 632 | |
| 633 | # Handle composition strategies: resolve composed content for non-replace commands |
| 634 | resolver = PresetResolver(self.project_root) |
| 635 | composed_dir = None |
| 636 | commands_to_register = [] |
| 637 | for cmd in filtered: |
| 638 | strategy = cmd.get("strategy", "replace") |
| 639 | if strategy != "replace": |
| 640 | # Only pre-compose if this preset is the top composing layer. |
| 641 | # If a higher-priority replace already wins, skip composition |
| 642 | # here — reconciliation will write the correct content. |
| 643 | layers = resolver.collect_all_layers(cmd["name"], "command") |
| 644 | top_layer_is_ours = ( |
| 645 | layers and layers[0]["path"].is_relative_to(preset_dir) |
| 646 | ) |
no test coverage detected