Skip to content

feat: interactive approval flow for community extensions#3015

Open
DyanGalih wants to merge 2 commits into
github:mainfrom
DyanGalih:feature/interactive-catalog-approval
Open

feat: interactive approval flow for community extensions#3015
DyanGalih wants to merge 2 commits into
github:mainfrom
DyanGalih:feature/interactive-catalog-approval

Conversation

@DyanGalih

Copy link
Copy Markdown
Contributor

Description

This PR introduces an interactive approval flow when users attempt to install an extension from a non-approved catalog (like the community catalog). Previously, users were met with a hard error instructing them to manually edit .specify/extension-catalogs.yml.

Changes Made

  • Interactive Prompt: extension_add now 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.
  • Config Persistence: Added approve_catalog_install in ExtensionCatalog to cleanly serialize and write the updated active catalogs to YAML, preserving the full catalog stack (including defaults).
  • Consistency Updates:
    • Updated extension_search and extension_info CLI output to guide users to run specify extension add instead of manual file editing.
    • Replaced yaml.dump with yaml.safe_dump in all catalog-related commands for consistency and safety.
  • Developer QoL: Added __main__.py to specify_cli so the package can be invoked locally using python -m specify_cli without global installation collisions.
  • Tests: Added 7 new test cases covering prompt ordering, cancellation, duplicate approvals, and fallback behaviors.

@DyanGalih DyanGalih requested a review from mnriem as a code owner June 17, 2026 03:50
Copilot AI review requested due to automatic review settings June 17, 2026 03:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 persist install_allowed: true for an active catalog while preserving the catalog stack.
  • Update extension add UX to prompt for approval before starting any spinner/install work; refresh several related CLI messages.
  • Switch YAML writes to yaml.safe_dump and add src/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):
Comment on lines +2098 to +2101
if config_path.exists() and config_path.is_symlink():
raise ValidationError(
f"Refusing to write catalog config via symlink: {config_path}"
)
Comment thread src/specify_cli/__init__.py Outdated
Comment on lines +1784 to +1787
approved_catalog = catalog.approve_catalog_install(catalog_name)
console.print(
f"[green]✓[/green] Approved catalog '[bold]{approved_catalog.name}[/bold]' for installation"
)
Comment on lines +2069 to +2071
"""Persist install permission for a catalog while preserving the stack."""
active_catalogs = self.get_active_catalogs()
updated_catalogs: List[Dict[str, Any]] = []
Comment thread tests/test_extensions.py Outdated
"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):
@DyanGalih

Copy link
Copy Markdown
Contributor Author

We have addressed the PR review feedback! 🚀

Updates:

  1. Preserving User-Level Configs: We updated approve_catalog_install to base the serialization on the currently active stack instead of a hardcoded minimal default. This ensures we don't accidentally "freeze" or discard any custom user-level catalogs from ~/.specify/extension-catalogs.yml into the project config when an approval takes place.
  2. Simplified Approval Prompts: We implemented get_installable_extension_info to automatically select the first approved source for a given extension. If you have an approved version of the extension in another catalog, the CLI will now gracefully install it without reprompting you for approval on the blocked catalog.
  3. Test Formatting: Converted the very long with patch(...) blocks in test_extensions.py into multi-line parenthesized context managers for better readability, as requested.

All test suites have been run locally and are passing successfully! Let me know if there's anything else!

Copilot AI review requested due to automatic review settings June 17, 2026 09:51
@DyanGalih DyanGalih force-pushed the feature/interactive-catalog-approval branch from 94dfba1 to 83d8013 Compare June 17, 2026 09:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +2115 to +2122
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()
Comment on lines +1089 to +1094
# 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.
Comment thread tests/test_extensions.py
Comment on lines +4880 to +4893
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants