Skip to content
156 changes: 13 additions & 143 deletions comfy_cli/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,11 +1052,12 @@ def run(


@app.command(
hidden=True,
help=(
"Validate a workflow without submitting (UI exports are converted to API format first). "
"Checks class_types, input shapes, enum values, edge wiring, and the dotted sub-inputs a "
"dynamic combo's selected option requires."
)
"[DEPRECATED — use 'comfy workflow validate'] Validate a workflow without submitting "
"(UI exports are converted to API format first). Checks class_types, input shapes, enum "
"values, edge wiring, and the dotted sub-inputs a dynamic combo's selected option requires."
),
)
@tracking.track_command()
def validate(
Expand All @@ -1081,145 +1082,14 @@ def validate(
typer.Option("--input", show_default=False, help="Path to a saved object_info JSON (offline mode)."),
] = None,
):
from pathlib import Path

from comfy_cli.command.run import is_ui_workflow
from comfy_cli.command.run.preflight import _detect_partner_nodes
from comfy_cli.cql.engine import Graph, LoadError
from comfy_cli.workflow_to_api import WorkflowConversionError, convert_ui_to_api

renderer = get_renderer()

# Load workflow
wf_path = Path(workflow).expanduser()
if not wf_path.is_file():
renderer.error(code="workflow_not_found", message=f"Workflow file not found: {workflow}", hint="check the path")
raise typer.Exit(code=1)
try:
wf_data = json.loads(wf_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
renderer.error(code="workflow_invalid_json", message=f"Invalid JSON: {e}", hint="re-export from ComfyUI")
raise typer.Exit(code=1) from e
if not isinstance(wf_data, dict):
renderer.error(
code="workflow_not_api_format", message="Workflow must be a JSON object", hint="use File > Export (API)"
)
raise typer.Exit(code=1)

# Load graph
mode = "local"
if where:
mode = where
else:
config = ConfigManager()
try:
decision = where_module.resolve(flag=None, config_value=config.get(where_module.CONFIG_KEY_WHERE_DEFAULT))
mode = decision.target.value
except Exception:
pass

try:
graph = Graph.load(mode=mode, input_path=input_path, host=host, port=port)
except LoadError as e:
renderer.error(
code="cql_no_graph",
message=str(e),
hint=e.details.get("hint", "pass --input <object_info.json>, or start the server"),
details=e.details,
)
raise typer.Exit(code=1) from e

# Detect a UI-export (frontend/canvas) workflow and lower it to API format
# before validating — exactly as `comfy run` does. Without this the wrapper
# keys (`nodes`, `links`, `groups`, `config`, …) each emit a `non_node_key`
# warning, zero nodes are checked, and the result is a vacuous `valid:true`.
# The converter reuses the object_info the graph was already built from
# (`graph.object_info`), so offline `--input` works and no second fetch happens.
converted_from_ui = False
if is_ui_workflow(wf_data):
if renderer.is_pretty():
rprint("[yellow]Detected UI-format workflow, converting to API format...[/yellow]")
try:
converted = convert_ui_to_api(wf_data, graph.object_info)
except WorkflowConversionError as e:
renderer.error(
code="workflow_not_api_format",
message=f"Workflow is a UI export that could not be converted to API format: {e}",
hint="use ComfyUI's 'File > Export (API)' to save as API format",
)
raise typer.Exit(code=1) from e
except Exception as e: # noqa: BLE001 — never leak a raw traceback to the agent flow
renderer.error(
code="conversion_crash",
message=f"Workflow conversion crashed unexpectedly: {type(e).__name__}: {e}",
hint="report this at https://github.com/Comfy-Org/comfy-cli/issues",
details={"exception_type": type(e).__name__},
)
raise typer.Exit(code=1) from e
if not converted:
renderer.error(
code="workflow_not_api_format",
message="Workflow is a UI export that converted to no executable nodes",
hint="use ComfyUI's 'File > Export (API)' to save as API format",
)
raise typer.Exit(code=1)
wf_data = converted
converted_from_ui = True

result = graph.validate_workflow(wf_data)

# Preview credit spend: partner-API (paid) nodes spend Comfy credits when the
# workflow is run. This is the same detection `comfy run` uses (authoritative
# `api_node: true`, `partner/...` category fallback), surfaced here read-only
# so agents can answer "will this spend credits?" without running. `wf_data`
# is API format at this point (any UI export already converted above), which
# is the format the detector reads. Purely informational — no exit-code gate.
partner_nodes = _detect_partner_nodes(wf_data, graph.object_info)

payload = {
"workflow": str(wf_path),
"valid": result["valid"],
"error_count": len(result["errors"]),
"warning_count": len(result["warnings"]),
"errors": result["errors"],
"warnings": result["warnings"],
"partner_nodes": partner_nodes,
"spends_credits": bool(partner_nodes),
}
if converted_from_ui:
# Signal that validation ran against the converted graph, not the file's
# literal bytes, and report how many nodes the conversion produced.
payload["converted_from_ui"] = True
payload["converted_node_count"] = len(wf_data)

if renderer.is_pretty():
if result["valid"]:
rprint(f"[bold green]✓[/bold green] workflow is valid ({len(wf_data)} nodes)")
for w in result["warnings"]:
rprint(f" [yellow]⚠[/yellow] {w.get('message', '')}")
else:
rprint(f"[bold red]✗[/bold red] {len(result['errors'])} error(s)")
for e in result["errors"]:
msg = e.get("message", "")
suggestions = e.get("suggestions", [])
if suggestions:
msg += f" (did you mean: {', '.join(suggestions[:3])}?)"
rprint(f" [red]•[/red] node {e.get('node_id') or '?'}: {msg}")
for w in result["warnings"]:
rprint(f" [yellow]⚠[/yellow] {w.get('message', '')}")
if partner_nodes:
# class_type strings come from the workflow / object_info and can
# contain Rich-markup metacharacters (e.g. `[/yellow]`); escape each
# so an odd node name can't raise MarkupError and crash the command.
from rich.markup import escape as _escape

rprint(
f"[yellow]⚠ uses partner-API (paid) nodes that spend Comfy credits: {', '.join(_escape(n) for n in partner_nodes)}[/yellow]"
)
renderer.emit(payload, command="validate", ok=result["valid"])

if not result["valid"]:
raise typer.Exit(code=1)
# Deprecated alias for `comfy workflow validate` (its canonical home). Kept
# functional for backward compatibility; warns and delegates to the shared
# implementation. The warning routes to stderr in JSON modes, so structured
# output stays clean.
from comfy_cli.command.workflow import validate_api_workflow

get_renderer().warn("`comfy validate` is deprecated — use `comfy workflow validate` instead.")
validate_api_workflow(workflow, where=where, host=host, port=port, input_path=input_path, command="validate")


@app.command(help="Upload files to the ComfyUI server's input directory.")
Expand Down
2 changes: 1 addition & 1 deletion comfy_cli/command/custom_nodes/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,7 @@ def validate_node_for_publishing():
return config


@app.command("validate", help="Run validation checks for publishing")
@app.command("validate", help="Validate this custom node for registry publishing")
@tracking.track_command("publish")
def validate():
"""
Expand Down
Loading
Loading