From 188e167c1cdece06c61b6799d39baec6d186c98b Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 20 Jul 2026 11:46:58 -0300 Subject: [PATCH] Fix M8 foundation: dry-run exception handling, GUI close race, registry API, contract serialization, semver parser --- src/cmdforge/cli/__init__.py | 2 +- src/cmdforge/cli/registry_commands.py | 39 ++++++++-------- src/cmdforge/gui/pages/tool_builder_page.py | 1 - src/cmdforge/preflight.py | 51 ++++++++++++++------- src/cmdforge/tool.py | 4 +- 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/cmdforge/cli/__init__.py b/src/cmdforge/cli/__init__.py index b0116cd..7745afb 100644 --- a/src/cmdforge/cli/__init__.py +++ b/src/cmdforge/cli/__init__.py @@ -527,7 +527,7 @@ def cmd_inspect(args): tool = load_tool(args.name) if not tool: - print(f"Error: Tool '{args.name}' not found.", file=open("/dev/stderr", "w")) + print(f"Error: Tool '{args.name}' not found.", file=sys.stderr) return 1 report = analyze_tool(tool) diff --git a/src/cmdforge/cli/registry_commands.py b/src/cmdforge/cli/registry_commands.py index f2805f6..8a95b1d 100644 --- a/src/cmdforge/cli/registry_commands.py +++ b/src/cmdforge/cli/registry_commands.py @@ -433,26 +433,27 @@ def _cmd_registry_publish(args): 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 + print(f"Registry preflight error: {e}", file=sys.stderr) + return 1 + errors = result.get("errors") or [] + warnings = result.get("warnings") or [] + suggestions = result.get("suggestions") 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 suggestions: + print(f"Suggestions ({len(suggestions)}):") + for sug in suggestions: + print(f" HINT: {sug}") + if not errors and not warnings and not suggestions: + print("Registry preflight passed.") + return 0 if not errors else 1 # Check for token config = load_config() diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index a4bd1c6..2cbd083 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -1094,7 +1094,6 @@ class ToolBuilderPage(QWidget): 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 diff --git a/src/cmdforge/preflight.py b/src/cmdforge/preflight.py index cf8e6cb..2fd7cf1 100644 --- a/src/cmdforge/preflight.py +++ b/src/cmdforge/preflight.py @@ -1,10 +1,10 @@ """Shared preflight analysis engine. Produces a PreflightReport used by the GUI, CLI, and registry workflow. -All checks are deterministic and evidence-based. Recommendations never -automate away human judgment. +All checks are deterministic and evidence-based. """ +import sys from dataclasses import dataclass, field from typing import Any, Dict, List, Optional @@ -62,19 +62,33 @@ def _check_config_integrity(tool: Tool, report: PreflightReport): report.errors.append("Tool name is required") if tool.version and not _is_semver(tool.version): report.errors.append(f"Version '{tool.version}' is not valid semver") - if not tool.steps: - report.warnings.append("Tool has no steps — output is a no-op passthrough") + if not tool.steps and not tool.arguments: + report.warnings.append( + "Tool has no steps and no arguments — output template is passthrough" + ) def _check_contracts(tool: Tool, report: PreflightReport): - if tool.output_schema: - schema = tool.output_schema - if not isinstance(schema, dict): - report.errors.append("output_schema must be a JSON Schema object") - elif schema.get("type") != "object": - report.warnings.append("output_schema root should have type: object") - else: - report.suggestions.append("Add an output_schema to enable automated verification") + if tool.output_schema is not None: + _validate_schema(tool.output_schema, "output_schema", report) + if tool.input_schema is not None: + _validate_schema(tool.input_schema, "input_schema", report) + if tool.output_schema is None and tool.input_schema is None: + report.suggestions.append( + "Add input_schema and output_schema to enable automated verification" + ) + + +def _validate_schema(schema: Any, label: str, report: PreflightReport): + if not isinstance(schema, dict): + report.errors.append(f"{label} must be a JSON Schema object") + return + schema_type = schema.get("type") + if schema_type and not isinstance(schema_type, (str, list)): + report.errors.append(f"{label} 'type' must be a string or array") + required = schema.get("required") + if required is not None and not isinstance(required, list): + report.errors.append(f"{label} 'required' must be an array") def _check_secrets(tool: Tool, report: PreflightReport): @@ -100,17 +114,20 @@ def _check_dependencies(tool: Tool, report: PreflightReport): def _check_similar_tools(tool: Tool, report: PreflightReport, client): try: - results = client.search_tools(tool.name, limit=5) - for item in results: + results = client.search_tools(tool.name, per_page=5) + items = results.data if hasattr(results, "data") else results + for item in items: report.similar_tools.append({ - "name": item.get("owner") + "/" + item.get("name"), + "name": (item.get("owner") or "") + "/" + item.get("name", ""), "description": item.get("description", ""), "downloads": item.get("downloads", 0), }) + if not items: + report.suggestions.append("No similar tools found on the registry") except Exception: report.warnings.append("Could not search registry for similar tools") def _is_semver(version: str) -> bool: - import re - return bool(re.match(r"^\d+\.\d+\.\d+", version)) + from .semver import Version + return Version.parse(version) is not None diff --git a/src/cmdforge/tool.py b/src/cmdforge/tool.py index d7fc158..bf0fcca 100644 --- a/src/cmdforge/tool.py +++ b/src/cmdforge/tool.py @@ -502,9 +502,9 @@ class Tool: # Only include visibility if it's not the default if self.visibility and self.visibility != "public": d["visibility"] = self.visibility - if self.input_schema: + if self.input_schema is not None: d["input_schema"] = self.input_schema - if self.output_schema: + if self.output_schema is not None: d["output_schema"] = self.output_schema # Include source attribution if present if self.source: