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())
|
edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool())
|
||||||
delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
|
delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
|
||||||
test_btn = Button3DCompact("Test", lambda _: self._test_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())
|
registry_btn = Button3DCompact("Registry", lambda _: self.browse_registry())
|
||||||
providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
|
providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
|
||||||
|
|
||||||
|
|
@ -189,6 +190,8 @@ class CmdForgeUI:
|
||||||
('pack', urwid.Text(" ")),
|
('pack', urwid.Text(" ")),
|
||||||
('pack', test_btn),
|
('pack', test_btn),
|
||||||
('pack', urwid.Text(" ")),
|
('pack', urwid.Text(" ")),
|
||||||
|
('pack', publish_btn),
|
||||||
|
('pack', urwid.Text(" ")),
|
||||||
('pack', registry_btn),
|
('pack', registry_btn),
|
||||||
('pack', urwid.Text(" ")),
|
('pack', urwid.Text(" ")),
|
||||||
('pack', providers_btn),
|
('pack', providers_btn),
|
||||||
|
|
@ -347,6 +350,113 @@ class CmdForgeUI:
|
||||||
else:
|
else:
|
||||||
self.message_box("Test", "No tool selected.")
|
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):
|
def exit_app(self):
|
||||||
"""Exit the application."""
|
"""Exit the application."""
|
||||||
raise urwid.ExitMainLoop()
|
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)
|
// Mobile filters toggle (tools page)
|
||||||
function toggleMobileFilters() {
|
function toggleMobileFilters() {
|
||||||
const filters = document.getElementById('mobile-filters');
|
const filters = document.getElementById('mobile-filters');
|
||||||
|
|
|
||||||
|
|
@ -60,17 +60,16 @@
|
||||||
|
|
||||||
<!-- Auth links -->
|
<!-- Auth links -->
|
||||||
{% if session.get('user') %}
|
{% if session.get('user') %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" id="user-dropdown-container">
|
||||||
<button @click="open = !open"
|
<button onclick="toggleUserDropdown(event)"
|
||||||
class="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-slate-700 transition-colors">
|
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>
|
<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">
|
<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"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open"
|
<div id="user-dropdown"
|
||||||
@click.away="open = false"
|
class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
|
||||||
class="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') }}" 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>
|
<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">
|
<hr class="my-1">
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ class TestConfig:
|
||||||
|
|
||||||
def test_default_config(self):
|
def test_default_config(self):
|
||||||
config = Config()
|
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.auto_fetch_from_registry is True
|
||||||
assert config.client_id.startswith("anon_")
|
assert config.client_id.startswith("anon_")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue