diff --git a/src/cmdforge/ui_urwid/__init__.py b/src/cmdforge/ui_urwid/__init__.py index 610d2cb..eb16609 100644 --- a/src/cmdforge/ui_urwid/__init__.py +++ b/src/cmdforge/ui_urwid/__init__.py @@ -177,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()) + publish_btn = Button3DCompact("Publish", lambda _: self._publish_selected_tool()) registry_btn = Button3DCompact("Registry", lambda _: self.browse_registry()) providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers()) @@ -189,6 +190,8 @@ class CmdForgeUI: ('pack', urwid.Text(" ")), ('pack', test_btn), ('pack', urwid.Text(" ")), + ('pack', publish_btn), + ('pack', urwid.Text(" ")), ('pack', registry_btn), ('pack', urwid.Text(" ")), ('pack', providers_btn), @@ -347,6 +350,113 @@ class CmdForgeUI: else: self.message_box("Test", "No tool selected.") + def _publish_selected_tool(self): + """Publish the currently selected tool to the registry.""" + if not self._selected_tool_name: + self.message_box("Publish", "No tool selected.") + return + + tool = load_tool(self._selected_tool_name) + if not tool: + self.message_box("Publish", "Could not load tool.") + return + + # Check for version + tool_dir = get_tools_dir() / self._selected_tool_name + config_path = tool_dir / "config.yaml" + + if not config_path.exists(): + self.message_box("Publish", "Tool config not found.") + return + + import yaml + config_text = config_path.read_text() + config_data = yaml.safe_load(config_text) + version = config_data.get("version", "") + + if not version: + # Prompt for version + self._prompt_for_version_and_publish(tool, config_path, config_data, config_text) + else: + self._do_publish(tool, version) + + def _prompt_for_version_and_publish(self, tool, config_path, config_data, config_text): + """Prompt user for version and then publish.""" + def on_version(version): + version = version.strip() + if not version: + self.message_box("Publish", "Version is required for publishing.") + return + + # Add version to config + import yaml + config_data["version"] = version + config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False)) + + self._do_publish(tool, version) + + self.input_dialog( + "Version Required", + "Enter version (e.g., 1.0.0)", + "1.0.0", + on_version + ) + + def _do_publish(self, tool, version): + """Perform the actual publish.""" + from ..config import load_config, set_registry_token + + config = load_config() + if not config.registry.token: + self.message_box( + "Authentication Required", + "No registry token configured.\n\n" + "To publish tools:\n" + "1. Register at cmdforge.brrd.tech/register\n" + "2. Log in and go to Dashboard > Tokens\n" + "3. Generate a token\n" + "4. Run: cmdforge config set-token " + ) + return + + def do_publish(): + try: + client = RegistryClient() + tool_dir = get_tools_dir() / tool.name + config_yaml = (tool_dir / "config.yaml").read_text() + readme_path = tool_dir / "README.md" + readme = readme_path.read_text() if readme_path.exists() else "" + + result = client.publish_tool(config_yaml, readme) + + status = result.get("status", "") + pr_url = result.get("pr_url", "") + + if status == "published" or result.get("version"): + owner = result.get("owner", "unknown") + name = result.get("name", tool.name) + self.message_box("Success", f"Published {owner}/{name}@{version}") + elif pr_url: + self.message_box("Pending Review", f"PR created: {pr_url}\n\nYour tool is pending review.") + else: + self.message_box("Success", "Tool published successfully!") + + except RegistryError as e: + if e.code == "UNAUTHORIZED": + self.message_box("Error", "Authentication failed.\nYour token may have expired.") + elif e.code == "VERSION_EXISTS": + self.message_box("Error", f"Version {version} already exists.\nBump the version and try again.") + else: + self.message_box("Error", f"Publish failed: {e.message}") + except Exception as e: + self.message_box("Error", f"Publish failed: {e}") + + self.yes_no( + "Publish Tool", + f"Publish {tool.name}@{version} to registry?", + on_yes=do_publish + ) + def exit_app(self): """Exit the application.""" raise urwid.ExitMainLoop() diff --git a/src/cmdforge/web/static/js/main.js b/src/cmdforge/web/static/js/main.js index 51ff060..c11b7b9 100644 --- a/src/cmdforge/web/static/js/main.js +++ b/src/cmdforge/web/static/js/main.js @@ -93,6 +93,24 @@ function closeMobileMenu() { } } +// User dropdown toggle +function toggleUserDropdown(event) { + event.stopPropagation(); + const dropdown = document.getElementById('user-dropdown'); + if (dropdown) { + dropdown.classList.toggle('hidden'); + } +} + +// Close user dropdown when clicking outside +document.addEventListener('click', function(event) { + const dropdown = document.getElementById('user-dropdown'); + const container = document.getElementById('user-dropdown-container'); + if (dropdown && container && !container.contains(event.target)) { + dropdown.classList.add('hidden'); + } +}); + // Mobile filters toggle (tools page) function toggleMobileFilters() { const filters = document.getElementById('mobile-filters'); diff --git a/src/cmdforge/web/templates/components/header.html b/src/cmdforge/web/templates/components/header.html index 1d6aa66..196ea8b 100644 --- a/src/cmdforge/web/templates/components/header.html +++ b/src/cmdforge/web/templates/components/header.html @@ -60,17 +60,16 @@ {% if session.get('user') %} -
- -
+