diff --git a/src/cmdforge/config.py b/src/cmdforge/config.py index c9f60c0..aa2cf92 100644 --- a/src/cmdforge/config.py +++ b/src/cmdforge/config.py @@ -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 diff --git a/src/cmdforge/ui_urwid/__init__.py b/src/cmdforge/ui_urwid/__init__.py index 7c0d433..e9e4990 100644 --- a/src/cmdforge/ui_urwid/__init__.py +++ b/src/cmdforge/ui_urwid/__init__.py @@ -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):