feat: interactive approval flow for community extensions#3015
feat: interactive approval flow for community extensions#3015DyanGalih wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds an interactive “catalog approval” flow to unblock installing extensions from discovery-only catalogs, and persists that approval into the project’s .specify/extension-catalogs.yml. It also tightens YAML serialization safety and adds a __main__.py entry-point.
Changes:
- Add
ExtensionCatalog.approve_catalog_install()to persistinstall_allowed: truefor an active catalog while preserving the catalog stack. - Update
extension addUX to prompt for approval before starting any spinner/install work; refresh several related CLI messages. - Switch YAML writes to
yaml.safe_dumpand addsrc/specify_cli/__main__.py.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tests/test_extensions.py | Adds coverage for approval persistence, symlink safety behavior, and prompt/spinner ordering in extension add. |
| src/specify_cli/extensions.py | Adds catalog approval persistence method and adjusts compatibility version check behavior. |
| src/specify_cli/main.py | Adds module entry-point wrapper calling main(). |
| src/specify_cli/init.py | Updates CLI flow for approvals + safer YAML dumping and spinner placement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| specifier = SpecifierSet(required) | ||
| if current not in specifier: | ||
| if not specifier.contains(current, prereleases=True): |
| if config_path.exists() and config_path.is_symlink(): | ||
| raise ValidationError( | ||
| f"Refusing to write catalog config via symlink: {config_path}" | ||
| ) |
| approved_catalog = catalog.approve_catalog_install(catalog_name) | ||
| console.print( | ||
| f"[green]✓[/green] Approved catalog '[bold]{approved_catalog.name}[/bold]' for installation" | ||
| ) |
| """Persist install permission for a catalog while preserving the stack.""" | ||
| active_catalogs = self.get_active_catalogs() | ||
| updated_catalogs: List[Dict[str, Any]] = [] |
| "description": "Security review extension", | ||
| "_catalog_name": "community", | ||
| "_install_allowed": False, | ||
| }), patch("specify_cli.extensions.ExtensionCatalog.download_extension", return_value=zip_path), patch("specify_cli.extensions.ExtensionManager.install_from_zip", return_value=mock_manifest), patch("typer.confirm", return_value=True), patch.object(Path, "cwd", return_value=project_dir): |
|
We have addressed the PR review feedback! 🚀 Updates:
All test suites have been run locally and are passing successfully! Let me know if there's anything else! |
94dfba1 to
83d8013
Compare
| config_path = self.project_root / ".specify" / self.CONFIG_FILENAME | ||
|
|
||
| # Base the update on the project-level config if it exists | ||
| if config_path.exists(): | ||
| base_catalogs = self._load_catalog_config(config_path) or [] | ||
| else: | ||
| # Otherwise, preserve the currently active stack so user-level catalogs remain available. | ||
| base_catalogs = self.get_active_catalogs() |
| # If a different approved source exists, use it instead of prompting. | ||
| installable_info = catalog.get_installable_extension_info(resolved_id) | ||
| if installable_info is not None: | ||
| ext_info = installable_info | ||
|
|
||
| # Enforce install_allowed policy only when no approved source exists. |
| with ( | ||
| patch("specify_cli.extensions.ExtensionCatalog.get_extension_info", return_value={ | ||
| "id": "security-review", | ||
| "name": "Security Review", | ||
| "version": "1.0.0", | ||
| "description": "Security review extension", | ||
| "_catalog_name": "community", | ||
| "_install_allowed": False, | ||
| }), | ||
| patch("specify_cli.extensions.ExtensionCatalog.download_extension", return_value=zip_path), | ||
| patch("specify_cli.extensions.ExtensionManager.install_from_zip", return_value=mock_manifest), | ||
| patch("typer.confirm", return_value=True), | ||
| patch.object(Path, "cwd", return_value=project_dir), | ||
| ): |
|
|
||
| def approve_catalog_install(self, catalog_name: str) -> CatalogEntry: | ||
| """Persist install permission for a catalog while preserving the stack.""" | ||
| config_path = self.project_root / ".specify" / self.CONFIG_FILENAME |
| ) | ||
|
|
||
| project_root = self.project_root.resolve() | ||
| config_path = self.project_root / ".specify" / self.CONFIG_FILENAME |
Description
This PR introduces an interactive approval flow when users attempt to install an extension from a non-approved catalog (like the
communitycatalog). Previously, users were met with a hard error instructing them to manually edit.specify/extension-catalogs.yml.Changes Made
extension_addnow prompts the user with a warning panel if the extension's catalog is not approved. If approved interactively, it updates the config and proceeds with installation.approve_catalog_installinExtensionCatalogto cleanly serialize and write the updated active catalogs to YAML, preserving the full catalog stack (including defaults).extension_searchandextension_infoCLI output to guide users to runspecify extension addinstead of manual file editing.yaml.dumpwithyaml.safe_dumpin all catalog-related commands for consistency and safety.__main__.pytospecify_cliso the package can be invoked locally usingpython -m specify_cliwithout global installation collisions.