Add TUI publish feature and fix web UI dropdown
- Add Publish button to TUI for publishing tools directly from the graphical interface without using the command line - Fix user dropdown menu in web UI header that stayed open after clicking (replaced Alpine.js directives with vanilla JS) - Update test to use correct registry URL (cmdforge.brrd.tech) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
79739369f0
commit
e18a575f76
|
|
@ -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 <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()
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -60,17 +60,16 @@
|
|||
|
||||
<!-- Auth links -->
|
||||
{% if session.get('user') %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
<div class="relative" id="user-dropdown-container">
|
||||
<button onclick="toggleUserDropdown(event)"
|
||||
class="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-slate-700 transition-colors">
|
||||
<span class="text-sm font-medium">{{ session.user.display_name }}</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
|
||||
<div id="user-dropdown"
|
||||
class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
|
||||
<a href="{{ url_for('web.dashboard') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
|
||||
<a href="{{ url_for('web.dashboard_settings') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a>
|
||||
<hr class="my-1">
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class TestConfig:
|
|||
|
||||
def test_default_config(self):
|
||||
config = Config()
|
||||
assert config.registry.url == "https://gitea.brrd.tech/api/v1"
|
||||
assert config.registry.url == "https://cmdforge.brrd.tech/api/v1"
|
||||
assert config.auto_fetch_from_registry is True
|
||||
assert config.client_id.startswith("anon_")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue