Fix registry URL and add registry browser to TUI

- Fix default registry URL: gitea.brrd.tech -> cmdforge.brrd.tech
  (gitea.brrd.tech was Gitea's API, not CmdForge registry)

- Add "Registry" button to TUI main menu
- Add registry browser overlay with:
  - Search input with live search
  - Tool list with owner/name display
  - Info panel showing name, publisher, version, description,
    downloads, and tags
  - Install button with confirmation dialog
  - Proper error handling for network issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-13 19:43:20 -04:00
parent b40f26622f
commit c89bd44be8
2 changed files with 197 additions and 2 deletions

View File

@ -16,7 +16,7 @@ CONFIG_DIR = Path.home() / ".cmdforge"
CONFIG_FILE = CONFIG_DIR / "config.yaml"
# Default registry URL (canonical base path)
DEFAULT_REGISTRY_URL = "https://gitea.brrd.tech/api/v1"
DEFAULT_REGISTRY_URL = "https://cmdforge.brrd.tech/api/v1"
@dataclass

View File

@ -9,6 +9,7 @@ from ..tool import (
get_tools_dir, DEFAULT_CATEGORIES
)
from ..providers import Provider, load_providers, add_provider, delete_provider, get_provider
from ..registry_client import RegistryClient, RegistryError
from .palette import PALETTE
from .widgets import (
@ -176,6 +177,7 @@ class CmdForgeUI:
edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool())
delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
test_btn = Button3DCompact("Test", lambda _: self._test_selected_tool())
registry_btn = Button3DCompact("Registry", lambda _: self.browse_registry())
providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
buttons_row = urwid.Columns([
@ -186,7 +188,9 @@ class CmdForgeUI:
('pack', delete_btn),
('pack', urwid.Text(" ")),
('pack', test_btn),
('pack', urwid.Text(" ")),
('pack', urwid.Text(" ")),
('pack', registry_btn),
('pack', urwid.Text(" ")),
('pack', providers_btn),
])
buttons_padded = urwid.Padding(buttons_row, align='left', left=1)
@ -1414,6 +1418,197 @@ No explanations, no markdown fencing, just the code."""
self.input_dialog("Test Input", "Enter test input", "Hello world", on_input)
# ==================== Registry Browser ====================
def browse_registry(self):
"""Browse and install tools from the registry."""
self._registry_search_query = ""
self._registry_tools = []
self._selected_registry_tool = None
self._show_registry_browser()
def _show_registry_browser(self, search_query: str = ""):
"""Show the registry browser screen."""
# Search input
search_edit = urwid.Edit(('label', "Search: "), search_query)
search_edit = urwid.AttrMap(search_edit, 'edit', 'edit_focus')
# Status/loading text
status_text = urwid.Text(('label', "Loading..."))
# Tool list (will be populated)
self._registry_walker = urwid.SimpleFocusListWalker([])
tool_listbox = ToolListBox(self._registry_walker, on_focus_change=self._on_registry_tool_focus)
tool_box = urwid.LineBox(tool_listbox, title='Registry Tools')
# Info panel
self._reg_info_name = urwid.Text("")
self._reg_info_desc = urwid.Text("")
self._reg_info_owner = urwid.Text("")
self._reg_info_version = urwid.Text("")
self._reg_info_downloads = urwid.Text("")
self._reg_info_tags = urwid.Text("")
info_content = urwid.Pile([
self._reg_info_name,
self._reg_info_owner,
self._reg_info_version,
urwid.Divider(),
self._reg_info_desc,
urwid.Divider(),
self._reg_info_downloads,
self._reg_info_tags,
])
info_filler = urwid.Filler(info_content, valign='top')
info_box = urwid.LineBox(info_filler, title='Tool Info')
def do_search(_=None):
query = search_edit.base_widget.edit_text.strip()
self._registry_search_query = query
status_text.set_text(('label', f"Searching for '{query}'..."))
self.refresh()
try:
client = RegistryClient()
result = client.search_tools(query=query if query else "*", per_page=50)
self._registry_tools = result.data
# Update the list
self._registry_walker.clear()
if self._registry_tools:
for tool in self._registry_tools:
display = f"{tool['owner']}/{tool['name']}"
item = SelectableToolItem(display, on_select=lambda n: self._install_registry_tool())
item.tool_data = tool
self._registry_walker.append(item)
# Select first item
self._selected_registry_tool = self._registry_tools[0]
self._on_registry_tool_focus(f"{self._registry_tools[0]['owner']}/{self._registry_tools[0]['name']}")
status_text.set_text(('label', f"Found {len(self._registry_tools)} tools"))
else:
self._registry_walker.append(urwid.Text(('label', " (no results) ")))
status_text.set_text(('label', "No tools found"))
except RegistryError as e:
status_text.set_text(('error', f"Error: {e}"))
self._registry_walker.clear()
self._registry_walker.append(urwid.Text(('error', f" Error: {e} ")))
except Exception as e:
status_text.set_text(('error', f"Error: {e}"))
self._registry_walker.clear()
self._registry_walker.append(urwid.Text(('error', f" Error: {e} ")))
self.refresh()
def on_install(_):
self._install_registry_tool()
def on_close(_):
self.close_overlay()
self._refresh_main_menu()
# Buttons
search_btn = ClickableButton("Search", do_search)
install_btn = ClickableButton("Install", on_install)
close_btn = ClickableButton("Close", on_close)
buttons = urwid.Columns([
('pack', search_btn),
('pack', urwid.Text(" ")),
('pack', install_btn),
('pack', urwid.Text(" ")),
('pack', close_btn),
])
buttons_centered = urwid.Padding(buttons, align='center', width='pack')
# Search row
search_row = urwid.Columns([
('weight', 1, search_edit),
('pack', urwid.Text(" ")),
('pack', search_btn),
])
# Main layout with columns for list and info
list_and_info = urwid.Columns([
('weight', 1, tool_box),
('weight', 1, info_box),
])
body = urwid.Pile([
('pack', search_row),
('pack', status_text),
('pack', urwid.Divider()),
('weight', 1, list_and_info),
('pack', urwid.Divider()),
('pack', buttons_centered),
])
# Wrap in frame
header = urwid.Text(('header', ' Browse Registry '), align='center')
footer = urwid.Text(('footer', ' Enter: Install | Tab: Navigate | Esc: Close '), align='center')
frame = urwid.Frame(body, header=header, footer=footer)
frame = urwid.LineBox(frame)
frame = urwid.AttrMap(frame, 'dialog')
self.show_overlay(frame, width=80, height=24)
# Do initial search
do_search()
def _on_registry_tool_focus(self, name):
"""Called when a registry tool is focused."""
# Find the tool data
for tool in self._registry_tools:
if f"{tool['owner']}/{tool['name']}" == name:
self._selected_registry_tool = tool
break
if self._selected_registry_tool:
tool = self._selected_registry_tool
self._reg_info_name.set_text(('label', f"Name: {tool['name']}"))
self._reg_info_owner.set_text(f"Publisher: {tool['owner']}")
self._reg_info_version.set_text(f"Version: {tool.get('version', 'unknown')}")
self._reg_info_desc.set_text(f"Description: {tool.get('description', '(none)')}")
self._reg_info_downloads.set_text(f"Downloads: {tool.get('downloads', 0)}")
tags = tool.get('tags', [])
if tags:
self._reg_info_tags.set_text(f"Tags: {', '.join(tags)}")
else:
self._reg_info_tags.set_text("Tags: (none)")
# Update selection state
if hasattr(self, '_registry_walker'):
for item in self._registry_walker:
if isinstance(item, SelectableToolItem):
item.set_selected(item.name == name)
def _install_registry_tool(self):
"""Install the selected registry tool."""
if not self._selected_registry_tool:
self.message_box("Install", "No tool selected.")
return
tool = self._selected_registry_tool
tool_name = f"{tool['owner']}/{tool['name']}"
def do_install():
try:
client = RegistryClient()
client.install_tool(tool_name)
self.message_box("Success", f"Installed {tool_name}\n\nRun 'cmdforge refresh' to create wrapper script.")
except RegistryError as e:
self.message_box("Error", f"Failed to install: {e}")
except Exception as e:
self.message_box("Error", f"Failed to install: {e}")
self.yes_no(
"Install Tool",
f"Install {tool_name}?",
on_yes=do_install
)
# ==================== Provider Management ====================
def manage_providers(self):