-
Notifications
You must be signed in to change notification settings - Fork 7.6k
fix: unofficial PyPI warning (#1982) and legacy extension command name auto-correction (#2017) #2027
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
fix: unofficial PyPI warning (#1982) and legacy extension command name auto-correction (#2017) #2027
Changes from all commits
879ddd4
b2c4f99
44d1996
fc99452
16f03ff
d643d0f
61984d4
ee106d1
bc3edb1
b7d22da
b137f44
8744262
8de8cdf
dfb88bb
b82e39e
5d5b02f
2ae702c
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 |
|---|---|---|
|
|
@@ -130,6 +130,7 @@ def __init__(self, manifest_path: Path): | |
| ValidationError: If manifest is invalid | ||
| """ | ||
| self.path = manifest_path | ||
| self.warnings: List[str] = [] | ||
| self.data = self._load_yaml(manifest_path) | ||
| self._validate() | ||
|
|
||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
@@ -215,17 +216,98 @@ def _validate(self): | |
| f"Hook '{hook_name}' missing required 'command' field" | ||
| ) | ||
|
|
||
| # Validate commands (if present) | ||
| # Validate commands; track renames so hook references can be rewritten. | ||
| rename_map: Dict[str, str] = {} | ||
| for cmd in commands: | ||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if not isinstance(cmd, dict): | ||
| raise ValidationError( | ||
| "Each command entry in 'provides.commands' must be a mapping" | ||
| ) | ||
| if "name" not in cmd or "file" not in cmd: | ||
| raise ValidationError("Command missing 'name' or 'file'") | ||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Validate command name format | ||
| if EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]) is None: | ||
| if not EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]): | ||
| corrected = self._try_correct_command_name(cmd["name"], ext["id"]) | ||
| if corrected: | ||
| self.warnings.append( | ||
| f"Command name '{cmd['name']}' does not follow the required pattern " | ||
| f"'speckit.{{extension}}.{{command}}'. Registering as '{corrected}'. " | ||
| f"The extension author should update the manifest to use this name." | ||
| ) | ||
| rename_map[cmd["name"]] = corrected | ||
| cmd["name"] = corrected | ||
mnriem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| raise ValidationError( | ||
| f"Invalid command name '{cmd['name']}': " | ||
| "must follow pattern 'speckit.{extension}.{command}'" | ||
| ) | ||
|
|
||
| # Validate alias types; no pattern enforcement on aliases — they are | ||
| # intentionally free-form to preserve community extension compatibility | ||
| # (e.g. 'speckit.verify' short aliases used by existing extensions). | ||
| aliases = cmd.get("aliases") | ||
| if aliases is None: | ||
| cmd["aliases"] = [] | ||
| aliases = [] | ||
| if not isinstance(aliases, list): | ||
|
Comment on lines
+246
to
+253
|
||
| raise ValidationError( | ||
| f"Invalid command name '{cmd['name']}': " | ||
| "must follow pattern 'speckit.{extension}.{command}'" | ||
| f"Aliases for command '{cmd['name']}' must be a list" | ||
| ) | ||
| for alias in aliases: | ||
| if not isinstance(alias, str): | ||
| raise ValidationError( | ||
| f"Aliases for command '{cmd['name']}' must be strings" | ||
| ) | ||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+246
to
+261
|
||
|
|
||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # Rewrite any hook command references that pointed at a renamed command or | ||
| # an alias-form ref (ext.cmd → speckit.ext.cmd). Always emit a warning when | ||
| # the reference is changed so extension authors know to update the manifest. | ||
| for hook_name, hook_data in self.data.get("hooks", {}).items(): | ||
| if not isinstance(hook_data, dict): | ||
| raise ValidationError( | ||
| f"Hook '{hook_name}' must be a mapping, got {type(hook_data).__name__}" | ||
| ) | ||
| command_ref = hook_data.get("command") | ||
| if not isinstance(command_ref, str): | ||
| continue | ||
| # Step 1: apply any rename from the auto-correction pass. | ||
| after_rename = rename_map.get(command_ref, command_ref) | ||
| # Step 2: lift alias-form '{ext_id}.cmd' to canonical 'speckit.{ext_id}.cmd'. | ||
| parts = after_rename.split(".") | ||
| if len(parts) == 2 and parts[0] == ext["id"]: | ||
| final_ref = f"speckit.{ext['id']}.{parts[1]}" | ||
| else: | ||
| final_ref = after_rename | ||
| if final_ref != command_ref: | ||
| hook_data["command"] = final_ref | ||
| self.warnings.append( | ||
| f"Hook '{hook_name}' referenced command '{command_ref}'; " | ||
| f"updated to canonical form '{final_ref}'. " | ||
| f"The extension author should update the manifest." | ||
mbachorik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| @staticmethod | ||
| def _try_correct_command_name(name: str, ext_id: str) -> Optional[str]: | ||
| """Try to auto-correct a non-conforming command name to the required pattern. | ||
|
|
||
| Handles the two legacy formats used by community extensions: | ||
| - 'speckit.command' → 'speckit.{ext_id}.command' | ||
| - '{ext_id}.command' → 'speckit.{ext_id}.command' | ||
|
|
||
| The 'X.Y' form is only corrected when X matches ext_id to ensure the | ||
| result passes the install-time namespace check. Any other prefix is | ||
| uncorrectable and will produce a ValidationError at the call site. | ||
|
|
||
| Returns the corrected name, or None if no safe correction is possible. | ||
| """ | ||
| parts = name.split('.') | ||
| if len(parts) == 2: | ||
| if parts[0] == 'speckit' or parts[0] == ext_id: | ||
| candidate = f"speckit.{ext_id}.{parts[1]}" | ||
| if EXTENSION_COMMAND_NAME_PATTERN.match(candidate): | ||
| return candidate | ||
| return None | ||
|
|
||
| @property | ||
| def id(self) -> str: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.