Skip to content

feat(extensions): Quality of life improvements for RFC-aligned catalog integration#1776

Open
mbachorik wants to merge 3 commits intogithub:mainfrom
mbachorik:feat/extension-catalog-integration
Open

feat(extensions): Quality of life improvements for RFC-aligned catalog integration#1776
mbachorik wants to merge 3 commits intogithub:mainfrom
mbachorik:feat/extension-catalog-integration

Conversation

@mbachorik
Copy link
Contributor

Quality of life comprehensive improvements for RFC-aligned catalog integration with version comparison and enhanced verbose output.

Changes:

  • Extension commands accept both ID and display name
  • List: --available, --all, --verbose flags with rich metadata
  • Info: version comparison installed vs catalog
  • Documentation updates
  • All 45 tests pass

Authored with GitHub Copilot

…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.
@mbachorik mbachorik requested a review from mnriem as a code owner March 7, 2026 09:00
Copilot AI review requested due to automatic review settings March 7, 2026 09:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 --verbose support to specify extension list (IDs + extra metadata, plus update-available hints).
  • Enhanced specify extension info with version comparison (installed vs catalog) and a new --verbose mode.
  • 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 final raise 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.
Copy link
Contributor

Copilot AI left a comment

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 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.

Comment on lines +2212 to +2216
lookup_key = resolved_installed_id or extension
catalog_error = None
try:
ext_info = catalog.get_extension_info(lookup_key)
except ExtensionError as e:
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +2046 to +2054
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:
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1806 to +1861
# 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]
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
# By ID
specify extension info jira

# By display name
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
# By display name
# By display name (installed extensions only)

Copilot uses AI. Check for mistakes.
Comment on lines +591 to 600
**Usage**: `specify extension info EXTENSION [OPTIONS]`

**Options**:

- `--verbose` - Show additional metadata and links

**Arguments**:

- `EXTENSION` - Extension ID
- `EXTENSION` - Extension ID or name

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +917 to +918


Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
- 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.
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