diff --git a/src/cmdforge/cli/__init__.py b/src/cmdforge/cli/__init__.py index 7e0721d..7ef3570 100644 --- a/src/cmdforge/cli/__init__.py +++ b/src/cmdforge/cli/__init__.py @@ -186,6 +186,12 @@ def main(): p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools") p_reg_mytools.set_defaults(func=cmd_registry) + # registry status + p_reg_status = registry_sub.add_parser("status", help="Check moderation status of a tool") + p_reg_status.add_argument("tool", help="Tool name to check status for") + p_reg_status.add_argument("--sync", action="store_true", help="Sync status from server and update local config") + p_reg_status.set_defaults(func=cmd_registry) + # registry browse p_reg_browse = registry_sub.add_parser("browse", help="Browse tools (TUI)") p_reg_browse.set_defaults(func=cmd_registry) diff --git a/src/cmdforge/cli/registry_commands.py b/src/cmdforge/cli/registry_commands.py index 365af39..bf9e9d1 100644 --- a/src/cmdforge/cli/registry_commands.py +++ b/src/cmdforge/cli/registry_commands.py @@ -30,6 +30,8 @@ def cmd_registry(args): return _cmd_registry_publish(args) elif args.registry_cmd == "my-tools": return _cmd_registry_my_tools(args) + elif args.registry_cmd == "status": + return _cmd_registry_status(args) elif args.registry_cmd == "browse": return _cmd_registry_browse(args) elif args.registry_cmd == "config": @@ -45,7 +47,8 @@ def cmd_registry(args): print(" update Update local index cache") print(" publish [path] Publish a tool") print(" my-tools List your published tools") - print(" browse Browse tools (TUI)") + print(" status Check moderation status of a tool") + print(" browse Browse tools (GUI)") print(" config [action] Manage registry settings (admin)") return 0 @@ -503,6 +506,118 @@ def _cmd_registry_my_tools(args): return 0 +def _cmd_registry_status(args): + """Check moderation status of a tool.""" + from ..registry_client import RegistryError, get_client + from ..tool import get_tools_dir + + tool_name = args.tool + do_sync = getattr(args, 'sync', False) + + # Check if tool exists locally + config_path = get_tools_dir() / tool_name / "config.yaml" + if not config_path.exists(): + print(f"Error: Tool '{tool_name}' not found locally", file=sys.stderr) + return 1 + + try: + config_data = yaml.safe_load(config_path.read_text()) or {} + except yaml.YAMLError as e: + print(f"Error reading tool config: {e}", file=sys.stderr) + return 1 + + local_status = config_data.get("registry_status", "not_published") + local_hash = config_data.get("registry_hash") + local_feedback = config_data.get("registry_feedback") + + if not local_hash: + print(f"Tool '{tool_name}' has not been published to the registry.") + print() + print("Publish with: cmdforge registry publish") + return 0 + + # If syncing, fetch from server + if do_sync: + try: + client = get_client() + status_data = client.get_my_tool_status(tool_name) + + new_status = status_data.get("status", "pending") + new_hash = status_data.get("config_hash") + new_feedback = status_data.get("feedback") + + changed = False + if local_status != new_status: + config_data["registry_status"] = new_status + local_status = new_status + changed = True + if new_hash and local_hash != new_hash: + config_data["registry_hash"] = new_hash + local_hash = new_hash + changed = True + if new_feedback != local_feedback: + if new_feedback: + config_data["registry_feedback"] = new_feedback + local_feedback = new_feedback + elif "registry_feedback" in config_data: + del config_data["registry_feedback"] + local_feedback = None + changed = True + + if changed: + config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False)) + print("Status synced from server.\n") + + except RegistryError as e: + if e.code == "UNAUTHORIZED": + print("Not logged in. Set your registry token to sync.", file=sys.stderr) + elif e.code == "TOOL_NOT_FOUND": + print(f"Tool '{tool_name}' not found in registry.", file=sys.stderr) + else: + print(f"Error syncing: {e.message}", file=sys.stderr) + return 1 + except Exception as e: + print(f"Error syncing: {e}", file=sys.stderr) + return 1 + + # Display status + print(f"Tool: {tool_name}") + print(f"Registry Hash: {local_hash[:16]}...") + + status_colors = { + "approved": "\033[32mApproved\033[0m", # Green + "pending": "\033[33mPending Review\033[0m", # Yellow + "changes_requested": "\033[93mChanges Requested\033[0m", # Light yellow/orange + "rejected": "\033[31mRejected\033[0m", # Red + } + status_display = status_colors.get(local_status, local_status) + print(f"Status: {status_display}") + + if local_feedback: + print() + print("Feedback from moderator:") + print("-" * 40) + print(local_feedback) + print("-" * 40) + + if local_status == "changes_requested": + print() + print("Action required: Address the feedback above and republish.") + print(" cmdforge registry publish") + elif local_status == "rejected": + print() + print("Your tool was rejected. Review the feedback above.") + elif local_status == "pending": + print() + print("Your tool is waiting for moderator review.") + print("Use --sync to check for updates.") + elif local_status == "approved": + print() + print("Your tool is live in the registry!") + + return 0 + + def _cmd_registry_browse(args): """Browse tools (GUI).""" from ..gui import run_gui diff --git a/src/cmdforge/gui/pages/tools_page.py b/src/cmdforge/gui/pages/tools_page.py index 6d6a3ad..f4fb928 100644 --- a/src/cmdforge/gui/pages/tools_page.py +++ b/src/cmdforge/gui/pages/tools_page.py @@ -11,7 +11,7 @@ from PySide6.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QTextEdit, QLabel, QPushButton, QGroupBox, QMessageBox, QFrame ) -from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtCore import Qt, QThread, Signal, QTimer from PySide6.QtGui import QFont, QColor, QBrush from ...tool import ( @@ -68,9 +68,11 @@ class StatusSyncWorker(QThread): status_data = client.get_my_tool_status(tool_name) new_status = status_data.get("status", "pending") new_hash = status_data.get("config_hash") + new_feedback = status_data.get("feedback") old_status = config_data.get("registry_status", "pending") old_hash = config_data.get("registry_hash") + old_feedback = config_data.get("registry_feedback") changed = False if old_status != new_status: @@ -79,6 +81,12 @@ class StatusSyncWorker(QThread): if new_hash and old_hash != new_hash: config_data["registry_hash"] = new_hash changed = True + if new_feedback != old_feedback: + if new_feedback: + config_data["registry_feedback"] = new_feedback + elif "registry_feedback" in config_data: + del config_data["registry_feedback"] + changed = True if changed: config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False)) @@ -93,6 +101,8 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: Tuple of (state, registry_hash) where state is: - "published" - approved in registry and current hash matches - "pending" - submitted but awaiting moderation + - "changes_requested" - admin requested changes before approval + - "rejected" - rejected by admin - "modified" - has registry_hash but current hash differs - "local" - no registry_hash (never published/downloaded) """ @@ -123,8 +133,12 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: # Hash matches - check moderation status if registry_status == "approved": return ("published", registry_hash) + elif registry_status == "changes_requested": + return ("changes_requested", registry_hash) + elif registry_status == "rejected": + return ("rejected", registry_hash) else: - # pending, rejected, or unknown + # pending or unknown return ("pending", registry_hash) except Exception: return ("local", None) @@ -139,6 +153,8 @@ class ToolsPage(QWidget): self._current_tool = None self._sync_worker = None self._syncing = False # Prevent re-sync during update + self._poll_timer = None # Timer for automatic status polling + self._has_pending_tools = False # Track if we need to poll self._setup_ui() self.refresh() @@ -269,6 +285,7 @@ class ToolsPage(QWidget): self.tool_tree.clear() self._current_tool = None self.info_text.clear() + self._has_pending_tools = False # Reset, will be set during tree building tools = list_tools() @@ -301,6 +318,10 @@ class ToolsPage(QWidget): # Get publish state state, registry_hash = get_tool_publish_state(name) + # Track if we have pending tools for polling + if state in ("pending", "changes_requested"): + self._has_pending_tools = True + # Show state indicator in display name if state == "published": display_name = f"{name} ✓" @@ -310,6 +331,14 @@ class ToolsPage(QWidget): display_name = f"{name} ◐" tooltip = "Submitted to registry - pending review" color = QColor(214, 158, 46) # Yellow/amber + elif state == "changes_requested": + display_name = f"{name} ⚠" + tooltip = "Changes requested - see feedback" + color = QColor(245, 158, 11) # Orange/amber + elif state == "rejected": + display_name = f"{name} ✗" + tooltip = "Rejected by moderator" + color = QColor(220, 38, 38) # Red elif state == "modified": display_name = f"{name} ●" tooltip = "Published to registry - local modifications" @@ -346,6 +375,9 @@ class ToolsPage(QWidget): # Start background sync for published tools self._start_background_sync(tools) + # Manage polling timer based on pending state + self._manage_poll_timer() + def _start_background_sync(self, tools: list): """Start background sync for tools that have been published.""" if self._syncing: @@ -385,6 +417,51 @@ class ToolsPage(QWidget): """Handle background sync completion.""" self._syncing = False + def _manage_poll_timer(self): + """Start or stop the polling timer based on pending tools.""" + config = load_config() + should_poll = self._has_pending_tools and config.registry.token + + if should_poll: + if not self._poll_timer: + # Create timer that polls every 30 seconds + self._poll_timer = QTimer(self) + self._poll_timer.timeout.connect(self._poll_status) + if not self._poll_timer.isActive(): + self._poll_timer.start(30000) # 30 seconds + else: + # No pending tools, stop polling + if self._poll_timer and self._poll_timer.isActive(): + self._poll_timer.stop() + + def _poll_status(self): + """Timer callback to poll status for pending tools.""" + if self._syncing: + return # Already syncing + + # Get list of tools with pending status + tools = list_tools() + pending_tools = [] + + for name in tools: + config_path = get_tools_dir() / name / "config.yaml" + if config_path.exists(): + try: + config_data = yaml.safe_load(config_path.read_text()) or {} + status = config_data.get("registry_status") + if status in ("pending", "changes_requested") and config_data.get("registry_hash"): + pending_tools.append(name) + except Exception: + pass + + if pending_tools: + self._start_background_sync(pending_tools) + else: + # No more pending tools, stop timer + if self._poll_timer and self._poll_timer.isActive(): + self._poll_timer.stop() + self._has_pending_tools = False + def _on_tool_status_updated(self, tool_name: str): """Handle background sync updating a tool's status.""" # Refresh the display - _syncing flag prevents re-triggering sync @@ -420,6 +497,17 @@ class ToolsPage(QWidget): if tool_name: self._edit_tool() + def _get_tool_feedback(self, tool_name: str) -> Optional[str]: + """Get the feedback for a tool from its config.""" + config_path = get_tools_dir() / tool_name / "config.yaml" + if config_path.exists(): + try: + config_data = yaml.safe_load(config_path.read_text()) or {} + return config_data.get("registry_feedback") + except Exception: + pass + return None + def _show_tool_info(self, tool: Tool): """Display tool information.""" lines = [] @@ -443,6 +531,36 @@ class ToolsPage(QWidget): "border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>" "◐ Submitted to registry - pending review

" ) + elif state == "changes_requested": + lines.append( + "

" + "⚠ Changes requested - please address feedback and republish

" + ) + # Show feedback if available + feedback = self._get_tool_feedback(tool.name) + if feedback: + feedback_escaped = feedback.replace("<", "<").replace(">", ">").replace("\n", "
") + lines.append( + f"
" + f"Feedback:
{feedback_escaped}
" + ) + elif state == "rejected": + lines.append( + "

" + "✗ Rejected by moderator

" + ) + # Show feedback if available + feedback = self._get_tool_feedback(tool.name) + if feedback: + feedback_escaped = feedback.replace("<", "<").replace(">", ">").replace("\n", "
") + lines.append( + f"
" + f"Reason:
{feedback_escaped}
" + ) elif state == "modified": lines.append( "

- + @@ -194,7 +194,7 @@

- +
@@ -215,7 +215,7 @@
- +