feat(extensions): scripts support, command filtering, and template discovery#1964
feat(extensions): scripts support, command filtering, and template discovery#1964mbachorik wants to merge 7 commits intogithub:mainfrom
Conversation
…e discovery Bring extension system to parity with presets across three areas: - github#1847: Extensions now support `provides.scripts` alongside commands, with name format and path safety validation, executable permissions, and CLI display in list/add/info. - github#1848: Add `_filter_commands_for_installed_extensions()` to CommandRegistrar for cross-boundary command filtering (matching the preset filtering at presets.py:518-529). - github#1846: Add `list_available()` to PresetResolver for template discovery across the 4-tier priority stack with source attribution, and a new `specify preset list-templates` CLI command. Closes github#1846, closes github#1847, closes github#1848 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extends Spec Kit’s extension and preset ecosystems to improve parity and discoverability: extensions can now declare scripts, commands can be filtered based on installed extensions, and presets gain a resolver-backed template discovery/listing command.
Changes:
- Add extension manifest + manager support for
provides.scripts, including validation, install-time.shexecutable bits, and CLI surface area (list/add/info). - Introduce
CommandRegistrar._filter_commands_for_installed_extensions()plus tests to filterspeckit.<ext-id>.<cmd>commands when the target extension isn’t installed. - Add
PresetResolver.list_available()and a newspecify preset list-templates --type <...>command, with template discovery tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions.py |
Adds scripts support to extension manifests/installs and introduces command filtering utility. |
src/specify_cli/presets.py |
Adds PresetResolver.list_available() for priority-stack template discovery with sources. |
src/specify_cli/__init__.py |
Adds preset list-templates CLI command and surfaces script counts in extension CLI output. |
tests/test_extensions.py |
Updates manifest validation tests and adds coverage for scripts support + command filtering utility. |
tests/test_presets.py |
Adds tests for PresetResolver.list_available() behavior (priority, sources, sorting). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Move extension template resolution and discovery into a dedicated ExtensionResolver class in extensions.py. PresetResolver now delegates its tier-3 (extension) lookups to ExtensionResolver instead of walking extension directories directly. This gives extensions their own resolution/discovery API without coupling them to the preset system. PresetResolver remains the unified resolver across all 4 tiers but no longer owns extension-specific logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use Path.anchor to reject drive-relative/UNC paths in script validation, not just os.path.isabs + normpath - chmod only adds execute bits (0o111) and is gated to POSIX - Command filter treats missing extensions dir as empty (filters out all extension-scoped commands), matching preset behavior - list_available() rejects unsupported template_type with ValueError - CLI list-templates validates --type before calling resolver Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Make chmod best-effort (try/except OSError) so permission edge cases don't abort extension installation - Filter overrides by name pattern in list_available(): commands must contain dots, templates must not, preventing cross-contamination in the shared overrides directory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After the ExtensionResolver refactor, PresetResolver no longer uses ExtensionRegistry directly — that dependency moved into ExtensionResolver. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Coerce None values from provides.commands/scripts via `or []` and validate they are lists, preventing TypeError on null YAML values - Discover both .sh and .ps1 scripts in ExtensionResolver and PresetResolver.list_available() instead of only .sh - Remove unused ExtensionRegistry import (ruff F401) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/specify_cli/extensions.py:160
- The commands/scripts validators assume each entry is a dict with string fields. If a manifest has
provides.commands: ["foo"]orname/filevalues that aren’t strings, this will raiseTypeError/KeyError(e.g., oncmd["name"]/ regex) rather than aValidationError. Validateisinstance(cmd, dict)and thatname/fileare strings before running regex/path checks.
# Validate commands
for cmd in commands:
if "name" not in cmd or "file" not in cmd:
raise ValidationError("Command missing 'name' or 'file'")
# Validate command name format
if not re.match(r'^speckit\.[a-z0-9-]+\.[a-z0-9-]+$', cmd["name"]):
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add type checks for extension/requires/provides top-level fields (must be mappings, not null) to raise ValidationError instead of AttributeError on malformed manifests - Add type checks for script entries (must be dict with string name/file) to prevent TypeError on non-dict or non-string values - PresetResolver.resolve() now tries both .sh and .ps1 for scripts across all tiers, consistent with list_available() discovery Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Brings the extension system to parity with presets across three areas:
Extension system: Add template type diversity (scripts support) #1847 — Scripts support: Extensions now accept
provides.scriptsalongside commands in the manifest. Includes name format validation (^[a-z0-9-]+$), path safety checks (no traversal), executable permissions on.shfiles during install, and CLI display inextension list,extension add, andextension info.Extension system: Add extension filtering for ext-specific commands #1848 — Command filtering: Adds
_filter_commands_for_installed_extensions()toCommandRegistrar— filters extension-specific commands (speckit.<ext-id>.<cmd>) against installed extension directories. Mirrors the preset filtering logic atpresets.py:518-529. Available as a utility for cross-boundary filtering (e.g. when presets provide commands for extensions that may not be installed). Not applied during extension self-registration since all commands in an extension's own manifest are always valid.Extension system: Add template resolution system #1846 — Template resolution & discovery: Introduces
ExtensionResolver— a dedicated class inextensions.pythat owns extension template resolution, source attribution, and discovery.PresetResolvernow delegates its tier-3 (extension) lookups toExtensionResolverinstead of walking extension directories directly. Newspecify preset list-templates --type <type>CLI command for template discovery across the full 4-tier stack.Why ExtensionResolver instead of using PresetResolver?
The
PresetResolveralready had extension logic baked in (tier 3 of its resolution stack), but this meant extensions had to go through the preset system to discover their own templates — mixing concerns.ExtensionResolvergives extensions their own resolution/discovery API:resolve(name, type)— find a template from extensionsresolve_with_source(name, type)— with source attributionlist_templates(type)— discover all templates provided by extensionsPresetResolverremains the unified resolver across all 4 tiers (overrides → presets → extensions → core) but now delegates toExtensionResolverfor the extension tier rather than owning that logic directly. Each system owns its own domain.Closes #1846, closes #1847, closes #1848
Test plan
test_search_with_cached_dataunrelated to this PR)specify extension addwith a scripts-only extensionspecify preset list-templatesoutputs templates with correct source attribution🤖 Generated with Claude Code