diff --git a/src/cmdforge/cli/registry_commands.py b/src/cmdforge/cli/registry_commands.py index de3071d..f2805f6 100644 --- a/src/cmdforge/cli/registry_commands.py +++ b/src/cmdforge/cli/registry_commands.py @@ -414,13 +414,44 @@ def _cmd_registry_publish(args): return 1 if args.dry_run: - print("Dry run - validating only") + print("Dry run — validating locally and via registry preflight") print() - print(f"Would publish:") + + # Local validation print(f" Name: {name}") print(f" Version: {version}") print(f" Config: {len(config_yaml)} bytes") print(f" README: {len(readme)} bytes") + print() + + # Attempt registry preflight if token is configured + config = load_config() + if config.registry.token: + try: + client = get_client() + result = client.publish_tool( + config_yaml, readme=readme, defaults=defaults, + dry_run=True, + ) + errors = result.get("errors") or [] + warnings = result.get("warnings") or [] + if errors: + print(f"Preflight errors ({len(errors)}):") + for err in errors: + print(f" ERROR: {err}") + if warnings: + print(f"Preflight warnings ({len(warnings)}):") + for warn in warnings: + print(f" WARN: {warn}") + if not errors and not warnings: + print("Registry preflight passed.") + return 0 if not errors else 1 + except Exception as e: + print(f"Registry preflight unavailable: {e}") + print("Proceeding with local validation only.") + else: + print("No registry token configured — local validation only.") + print("Configure a token to get registry-side preflight checks.") return 0 # Check for token diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index e12923a..a4bd1c6 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -13,7 +13,7 @@ from PySide6.QtCore import Qt from ...tool import ( Tool, ToolArgument, PromptStep, CodeStep, ToolStep, - load_tool, save_tool, validate_tool_name, get_all_categories, + load_tool, save_tool, tool_exists, validate_tool_name, get_all_categories, ensure_settings ) from ..widgets.icons import get_prompt_icon, get_code_icon, get_tool_icon @@ -1075,6 +1075,41 @@ class ToolBuilderPage(QWidget): QMessageBox.warning(self, "Validation", error) return + # Block silent overwrite when creating a new tool + if (not self.editing or name != self.original_name) and tool_exists(name): + existing = load_tool(name) + existing_desc = f" ({existing.description})" if existing and existing.description else "" + msg = QMessageBox(self) + msg.setIcon(QMessageBox.Warning) + msg.setWindowTitle("Tool Already Exists") + msg.setText(f"'{name}' already exists.{existing_desc}") + msg.setInformativeText("What would you like to do?") + btn_open = msg.addButton("Open Existing", QMessageBox.ActionRole) + btn_copy = msg.addButton("Create Copy", QMessageBox.ActionRole) + btn_rename = msg.addButton("Choose Another Name", QMessageBox.ActionRole) + msg.setStandardButtons(QMessageBox.Cancel) + msg.setDefaultButton(btn_rename) + msg.exec() + + clicked = msg.clickedButton() + if clicked is btn_open: + self.main_window.open_tool_builder(name) + self.main_window.close_tool_builder() + return + elif clicked is btn_copy: + suffix = 2 + while tool_exists(f"{name}-{suffix}"): + suffix += 1 + copy_name = f"{name}-{suffix}" + self.name_input.setText(copy_name) + self.name_input.setFocus() + return + elif clicked is btn_rename: + self.name_input.setFocus() + return + else: + return + description = self.desc_input.text().strip() category = self.category_combo.currentText() output = self.output_input.toPlainText().strip() or "{response}"