feat(extensions): Quality of life improvements for RFC-aligned catalog integration#1776
feat(extensions): Quality of life improvements for RFC-aligned catalog integration#1776mbachorik wants to merge 3 commits intogithub:mainfrom
Conversation
…ison and verbose output Implement extension management improvements: - All commands accept extension ID or display name - List: --available, --all, --verbose flags - Info: version comparison (installed vs catalog) - Doc updates to EXTENSION-USER-GUIDE.md and API reference - All 45 tests pass Squashed commit from 3 changes.
There was a problem hiding this comment.
Pull request overview
This PR enhances the Spec Kit extension CLI UX around RFC-aligned catalog integration by improving extension identification (ID vs display name), adding richer listing/info output, and documenting the new flags/behaviors.
Changes:
- Added
--verbosesupport tospecify extension list(IDs + extra metadata, plus update-available hints). - Enhanced
specify extension infowith version comparison (installed vs catalog) and a new--verbosemode. - Updated extension docs to describe verbose output and “ID or name” arguments across commands.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/specify_cli/__init__.py |
Adds verbose flags, richer extension list/info output, and resolves extensions by ID or display name in multiple commands. |
extensions/EXTENSION-USER-GUIDE.md |
Documents verbose list/info usage and removing by display name. |
extensions/EXTENSION-API-REFERENCE.md |
Updates CLI reference for --verbose and “ID or name” arguments. |
Comments suppressed due to low confidence (1)
src/specify_cli/init.py:2342
extension_info()now returns/raises in all branches above, but a large block of the previous implementation remains after the finalraise typer.Exit(1). This code is unreachable and duplicates output logic, which makes future edits error-prone; it should be deleted to avoid confusion and accidental drift.
# Case 3: Extension not found anywhere
console.print(f"[red]Error:[/red] Extension '{extension}' not found in catalog or installed locally")
console.print("\nTry: specify extension search")
raise typer.Exit(1)
# Header
verified_badge = " [green]✓ Verified[/green]" if ext_info.get("verified") else ""
console.print(f"\n[bold]{ext_info['name']}[/bold] (v{ext_info['version']}){verified_badge}")
console.print(f"ID: {ext_info['id']}")
console.print()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Handle catalog lookup failures with local fallback in extension info, optimize verbose list version checks by prefetching catalog once, add explicit ambiguous-name handling for update/enable/disable, and remove unreachable extension info code. Add CLI tests for verbose update indicators, catalog-failure fallback, and ambiguity handling.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
src/specify_cli/__init__.py
Outdated
| lookup_key = resolved_installed_id or extension | ||
| catalog_error = None | ||
| try: | ||
| ext_info = catalog.get_extension_info(lookup_key) | ||
| except ExtensionError as e: |
There was a problem hiding this comment.
extension_info claims to accept an extension display name, but catalog lookup still calls ExtensionCatalog.get_extension_info() with the raw argument when the extension is not installed. Since get_extension_info() only checks by catalog key (ID), specify extension info "<display name>" will fail for catalog-only extensions. Consider resolving catalog entries by exact display name (with ambiguity handling) before calling get_extension_info(), or extending ExtensionCatalog.get_extension_info() to support display-name lookups.
src/specify_cli/__init__.py
Outdated
| extension_id = extension | ||
|
|
||
| # Resolve by display name if ID lookup fails | ||
| if not manager.registry.is_installed(extension_id): | ||
| installed = manager.list_installed() | ||
| name_matches = [ext for ext in installed if ext["name"].lower() == extension.lower()] | ||
| if len(name_matches) == 1: | ||
| extension_id = name_matches[0]["id"] | ||
| elif len(name_matches) > 1: |
There was a problem hiding this comment.
The same installed-name-to-ID resolution logic is duplicated across multiple CLI commands (remove, update, enable, disable, info). This increases the risk of inconsistent behavior and makes future changes harder. Consider extracting a shared helper (e.g., resolve_installed_extension_id) that returns the resolved ID or raises a consistent Typer error for not-found/ambiguous cases.
src/specify_cli/__init__.py
Outdated
| # In verbose mode, prefetch catalog once to avoid repeated per-extension I/O. | ||
| catalog_versions_by_id = {} | ||
| if verbose: | ||
| try: | ||
| for catalog_ext in catalog.search(): | ||
| ext_id = catalog_ext.get("id") | ||
| ext_version = catalog_ext.get("version") | ||
| if ext_id and ext_version: | ||
| try: | ||
| catalog_versions_by_id[ext_id] = pkg_version.Version(ext_version) | ||
| except Exception: | ||
| continue | ||
| except Exception: | ||
| # Keep output usable if catalog is temporarily unavailable. | ||
| catalog_versions_by_id = {} | ||
|
|
||
| # Show installed extensions (default or with --all) | ||
| if not available or all_extensions: | ||
| if not installed: | ||
| console.print("[yellow]No extensions installed.[/yellow]") | ||
| if not (available or all_extensions): | ||
| console.print("\nInstall an extension with:") | ||
| console.print(" specify extension add <extension-name>") | ||
| return | ||
| else: | ||
| console.print("\n[bold cyan]Installed Extensions:[/bold cyan]\n") | ||
|
|
||
| for ext in installed: | ||
| status_icon = "✓" if ext["enabled"] else "✗" | ||
| status_color = "green" if ext["enabled"] else "red" | ||
|
|
||
| # Check if update available from catalog | ||
| update_indicator = "" | ||
| if verbose: | ||
| try: | ||
| installed_ver = pkg_version.Version(ext["version"]) | ||
| catalog_ver = catalog_versions_by_id.get(ext["id"]) | ||
| if catalog_ver and catalog_ver > installed_ver: | ||
| update_indicator = f" [yellow]→ v{catalog_ver} available[/yellow]" | ||
| except Exception: | ||
| pass | ||
|
|
||
| console.print( | ||
| f" [{status_color}]{status_icon}[/{status_color}] [bold]{ext['name']}[/bold] (v{ext['version']}){update_indicator}" | ||
| ) | ||
| if verbose: | ||
| console.print(f" [dim]id: {ext['id']}[/dim]") | ||
| console.print(f" {ext['description']}") | ||
| console.print(f" Commands: {ext['command_count']} | Hooks: {ext['hook_count']} | Status: {'Enabled' if ext['enabled'] else 'Disabled'}") | ||
| console.print() | ||
|
|
||
| # Show available extensions (with --available or --all) | ||
| if available or all_extensions: | ||
| console.print("\nInstall an extension:") | ||
| console.print(" [cyan]specify extension add <name>[/cyan]") | ||
| try: | ||
| catalog_extensions = catalog.search() | ||
| uninstalled = [ext for ext in catalog_extensions if ext["id"] not in installed_ids] |
There was a problem hiding this comment.
In verbose mode, extension_list prefetches catalog data via catalog.search(), but the --available/--all path calls catalog.search() again. This doubles catalog I/O (and may trigger a second network fetch if cache is invalid). Consider reusing the prefetched search results (or caching them in a local variable) for both update-indicator calculation and the available-extensions listing.
extensions/EXTENSION-USER-GUIDE.md
Outdated
| # By ID | ||
| specify extension info jira | ||
|
|
||
| # By display name |
There was a problem hiding this comment.
This guide states specify extension info "Jira Integration" works by display name. In the current CLI implementation, catalog lookups are still ID-keyed (and display-name resolution only happens for installed extensions), so this example will fail when the extension is not installed. Either adjust the implementation to support catalog display-name lookup (with ambiguity handling) or clarify the docs that name lookup only applies to installed extensions.
| # By display name | |
| # By display name (installed extensions only) |
| **Usage**: `specify extension info EXTENSION [OPTIONS]` | ||
|
|
||
| **Options**: | ||
|
|
||
| - `--verbose` - Show additional metadata and links | ||
|
|
||
| **Arguments**: | ||
|
|
||
| - `EXTENSION` - Extension ID | ||
| - `EXTENSION` - Extension ID or name | ||
|
|
There was a problem hiding this comment.
API reference now says extension info accepts an extension "ID or name", but catalog lookups are still ID-based (and display-name resolution only happens for installed extensions). This is misleading for users trying to query a catalog-only extension by name. Either implement catalog display-name resolution or narrow the docs to match actual behavior.
|
|
||
|
|
There was a problem hiding this comment.
New CLI behavior claims commands accept display names, but there is no test covering specify extension info "<catalog display name>" for an extension that is present in the cached catalog but not installed. Adding this test would catch the current gap between docs/intent and the catalog lookup behavior (and should also cover the ambiguous-name case when multiple catalog entries share the same display name).
| def test_extension_info_uses_catalog_display_name_for_uninstalled(self, project_dir, monkeypatch): | |
| """`extension info` should resolve catalog-only extensions by display name.""" | |
| _write_catalog_cache( | |
| project_dir, | |
| { | |
| "catalog-ext": { | |
| "id": "catalog-ext", | |
| "name": "Catalog Only Extension", | |
| "version": "2.0.0", | |
| "description": "Extension present only in the catalog", | |
| } | |
| }, | |
| ) | |
| # Do NOT install the extension locally; it should be resolved purely from the catalog. | |
| monkeypatch.chdir(project_dir) | |
| result = CliRunner().invoke(app, ["extension", "info", "Catalog Only Extension"]) | |
| assert result.exit_code == 0 | |
| # The output should reflect that we looked up the extension by its display name in the catalog. | |
| assert "Catalog Only Extension" in result.stdout | |
| assert "catalog-ext" in result.stdout | |
| def test_extension_info_ambiguous_catalog_display_name_errors(self, project_dir, monkeypatch): | |
| """`extension info` should error when a catalog display name is ambiguous.""" | |
| _write_catalog_cache( | |
| project_dir, | |
| { | |
| "dup-cat-one": { | |
| "id": "dup-cat-one", | |
| "name": "Duplicate Catalog Extension", | |
| "version": "1.0.0", | |
| "description": "First catalog entry with duplicate name", | |
| }, | |
| "dup-cat-two": { | |
| "id": "dup-cat-two", | |
| "name": "Duplicate Catalog Extension", | |
| "version": "1.1.0", | |
| "description": "Second catalog entry with duplicate name", | |
| }, | |
| }, | |
| ) | |
| # Nothing is installed; ambiguity arises purely from the cached catalog. | |
| monkeypatch.chdir(project_dir) | |
| result = CliRunner().invoke(app, ["extension", "info", "Duplicate Catalog Extension"]) | |
| assert result.exit_code == 1 | |
| assert "ambiguous" in result.stdout.lower() | |
| assert "dup-cat-one" in result.stdout | |
| assert "dup-cat-two" in result.stdout |
- Extract shared _resolve_installed_extension_id helper to eliminate code duplication across remove/update/enable/disable commands - Add catalog display name resolution with ambiguity detection - Implement _resolve_catalog_extension helper for extension_info catalog lookups - Fix double catalog fetch: reuse prefetched catalog in verbose + available/all mode - Update EXTENSION-USER-GUIDE.md to clarify display name support for both installed and catalog extensions - Update EXTENSION-API-REFERENCE.md with detailed argument descriptions - Add tests for catalog display name resolution (uninstalled and ambiguous cases) - Add Dict and Any to typing imports All 52 tests passing.
Quality of life comprehensive improvements for RFC-aligned catalog integration with version comparison and enhanced verbose output.
Changes:
Authored with GitHub Copilot