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:
parent
b40f26622f
commit
c89bd44be8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue