-
Notifications
You must be signed in to change notification settings - Fork 7.2k
feat(extensions): scripts support, command filtering, and template discovery #1964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
62283b7
de2d9e6
d6e0773
aa1df03
6cc1eba
6901e64
5afa192
d97620e
dca335f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -140,11 +140,15 @@ def _validate(self): | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate provides section | ||||||||||||||||||||||||||||||||||||||||||||||||
| provides = self.data["provides"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "commands" not in provides or not provides["commands"]: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError("Extension must provide at least one command") | ||||||||||||||||||||||||||||||||||||||||||||||||
| has_commands = "commands" in provides and provides["commands"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| has_scripts = "scripts" in provides and provides["scripts"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not has_commands and not has_scripts: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Extension must provide at least one command or script" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate commands | ||||||||||||||||||||||||||||||||||||||||||||||||
| for cmd in provides["commands"]: | ||||||||||||||||||||||||||||||||||||||||||||||||
| for cmd in provides.get("commands", []): | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "name" not in cmd or "file" not in cmd: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError("Command missing 'name' or 'file'") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,6 +159,33 @@ def _validate(self): | |||||||||||||||||||||||||||||||||||||||||||||||
| "must follow pattern 'speckit.{extension}.{command}'" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate scripts | ||||||||||||||||||||||||||||||||||||||||||||||||
| for script in provides.get("scripts", []): | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "name" not in script or "file" not in script: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError("Script missing 'name' or 'file'") | ||||||||||||||||||||||||||||||||||||||||||||||||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate script name format | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not re.match(r'^[a-z0-9-]+$', script["name"]): | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid script name '{script['name']}': " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "must be lowercase alphanumeric with hyphens only" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate file path safety: must be relative, no anchored/drive | ||||||||||||||||||||||||||||||||||||||||||||||||
| # paths, and no parent traversal components | ||||||||||||||||||||||||||||||||||||||||||||||||
| file_path = script["file"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| p = Path(file_path) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if p.is_absolute() or p.anchor: | ||||||||||||||||||||||||||||||||||||||||||||||||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid script file path '{file_path}': " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "must be a relative path within the extension directory" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ".." in p.parts: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid script file path '{file_path}': " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "must be a relative path within the extension directory" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Enforce resolver/discovery conventions: | |
| # - script files must live under "scripts/" | |
| # - file name stem must match the script name | |
| # - extension must be one of the allowed script types | |
| if not p.parts or p.parts[0] != "scripts": | |
| raise ValidationError( | |
| f"Invalid script file path '{file_path}': " | |
| "script files must be located under the 'scripts/' directory" | |
| ) | |
| if p.suffix not in {".sh", ".ps1"}: | |
| raise ValidationError( | |
| f"Invalid script file path '{file_path}': " | |
| "script files must have a '.sh' or '.ps1' extension" | |
| ) | |
| if p.stem != script["name"]: | |
| raise ValidationError( | |
| f"Invalid script file path '{file_path}': " | |
| f"file name (without extension) must match script name '{script['name']}'" | |
| ) |
mbachorik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExtensionResolver.list_templates() docstring promises results are sorted by extension priority then alphabetically, but the implementation returns templates in “scan order” (subdir order + filesystem order) and never sorts results before returning. This can produce non-alphabetical output for template type (because both templates/ and extension root are scanned). Either sort results (e.g., by (priority, name)) before returning or adjust the docstring to match the actual ordering guarantee.
Uh oh!
There was an error while loading. Please reload this page.