Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
65afe05
feat(workflow): add UI-to-API workflow converter
bigcat88 May 12, 2026
ba65ef4
refactor(run): convert UI workflows client-side via /object_info
bigcat88 May 12, 2026
c1efe00
refactor(workflow): drop dead pre-rewrite link_map build
bigcat88 May 12, 2026
6224ae4
fix(workflow): guard subgraph definition helpers against malformed en…
bigcat88 May 12, 2026
1819523
fix(workflow): guard Set/Get variable names against unhashable values
bigcat88 May 12, 2026
8ea2558
fix(workflow): make control_after_generate filter schema-aware
bigcat88 May 12, 2026
ec246d1
fix(workflow): close three frontend-parity gaps from full audit
bigcat88 May 12, 2026
83c7f2c
feat(workflow): resolve dynamicPrompts {a|b} groups in text widgets
bigcat88 May 12, 2026
668aeb1
fix(workflow): feed filtered list to _get_widget_name_order
bigcat88 May 12, 2026
ec46457
fix(workflow): guard internal link IDs against non-int / collision
bigcat88 May 12, 2026
d31ae9e
fix(workflow): guard link-id dict-key lookups in global pre-pass helpers
bigcat88 May 12, 2026
173331d
fix(workflow): resolve node schema once via _schema_for in all paths
bigcat88 May 14, 2026
9e00a50
fix(workflow): close two paranoid-review hardening gaps
bigcat88 May 14, 2026
8896a38
fix(workflow): make the three tracers iterative to avoid recursion limit
bigcat88 May 14, 2026
6a563cd
fix(workflow): handle '*' wildcard inputs and implicit seed companion
bigcat88 May 14, 2026
79a1b16
fix(workflow): align node-inclusion policy with frontend / cloud-mcp
bigcat88 May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 37 additions & 42 deletions comfy_cli/command/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from websocket import WebSocket, WebSocketException, WebSocketTimeoutException

from comfy_cli.env_checker import check_comfy_server_running
from comfy_cli.workflow_to_api import WorkflowConversionError, convert_ui_to_api
from comfy_cli.workspace_manager import WorkspaceManager

workspace_manager = WorkspaceManager()
Expand All @@ -37,55 +38,31 @@ def _validate_api_workflow(workflow):
return workflow


class WorkflowConverterUnavailable(Exception):
"""The running ComfyUI server doesn't expose /workflow/convert."""
def fetch_object_info(host: str, port: int, timeout: int) -> dict:
"""GET ``/object_info`` from the running ComfyUI server.


def convert_ui_workflow_via_server(workflow: dict, host: str, port: int, timeout: int) -> dict:
"""POST a UI-format workflow to the server's /workflow/convert and return API-format JSON.

Raises WorkflowConverterUnavailable if the server doesn't expose the endpoint.
Raises typer.Exit on other conversion failures.
The response describes every loaded node class's input schema and is what
the converter uses to map widget values to input names, fill defaults, etc.
"""
url = f"http://{host}:{port}/workflow/convert"
req = request.Request(url, json.dumps(workflow).encode("utf-8"))
req.add_header("Content-Type", "application/json")
url = f"http://{host}:{port}/object_info"
try:
resp = request.urlopen(req, timeout=timeout)
with request.urlopen(url, timeout=timeout) as resp:
body = resp.read()
except urllib.error.HTTPError as e:
if e.code in (404, 405):
raise WorkflowConverterUnavailable() from e
body = e.read().decode("utf-8", errors="replace").strip()
pprint(f"[bold red]Workflow conversion failed (HTTP {e.code}): {body[:500]}[/bold red]")
pprint(f"[bold red]Failed to fetch /object_info (HTTP {e.code}): {body[:500]}[/bold red]")
raise typer.Exit(code=1) from e
except urllib.error.URLError as e:
pprint(f"[bold red]Workflow conversion failed: {e.reason}[/bold red]")
pprint(f"[bold red]Failed to fetch /object_info: {e.reason}[/bold red]")
raise typer.Exit(code=1) from e
except TimeoutError as e:
pprint(f"[bold red]Failed to fetch /object_info: timed out after {timeout}s[/bold red]")
raise typer.Exit(code=1) from e
try:
converted = json.loads(resp.read())
return json.loads(body)
except json.JSONDecodeError as e:
pprint("[bold red]Workflow conversion failed: server returned invalid JSON[/bold red]")
pprint("[bold red]Failed to fetch /object_info: server returned invalid JSON[/bold red]")
raise typer.Exit(code=1) from e
if not isinstance(converted, dict) or not converted:
pprint("[bold red]Workflow conversion failed: expected a non-empty JSON object[/bold red]")
raise typer.Exit(code=1)
first = converted[next(iter(converted))]
if not isinstance(first, dict) or "class_type" not in first:
pprint("[bold red]Workflow conversion failed: returned data is not API workflow format[/bold red]")
raise typer.Exit(code=1)
return converted


def _print_converter_unavailable_help() -> None:
pprint(
"[bold red]This ComfyUI server doesn't expose a /workflow/convert endpoint[/bold red]\n"
"[bold red]to convert it to API format.[/bold red]\n"
"\n"
"[yellow]Workarounds:[/yellow]\n"
"[yellow] * Install a custom node that adds /workflow/convert on the server[/yellow]\n"
"[yellow] * Or, in the ComfyUI frontend, use 'File > Export (API)' to save[/yellow]\n"
"[yellow] your workflow as API format[/yellow]"
)


def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=False, timeout=30):
Expand All @@ -112,11 +89,29 @@ def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=Fal
raise typer.Exit(code=1) from e

if is_ui_workflow(raw_workflow):
pprint("[yellow]Detected UI-format workflow, converting via server's /workflow/convert...[/yellow]")
pprint("[yellow]Detected UI-format workflow, converting to API format...[/yellow]")
object_info = fetch_object_info(host, port, timeout)
try:
workflow = convert_ui_workflow_via_server(raw_workflow, host, port, timeout)
except WorkflowConverterUnavailable:
_print_converter_unavailable_help()
workflow = convert_ui_to_api(raw_workflow, object_info)
except WorkflowConversionError as e:
pprint(f"[bold red]Workflow conversion failed: {e}[/bold red]")
raise typer.Exit(code=1) from e
except Exception as e:
# The converter is experimental; an unexpected crash here is a bug
# in our code, not user error. Show a clean message and a pointer.
pprint(
f"[bold red]Workflow conversion crashed unexpectedly: {type(e).__name__}: {e}[/bold red]\n"
"[yellow]The UI-to-API converter is experimental. Please report this at[/yellow]\n"
"[yellow] https://github.com/Comfy-Org/comfy-cli/issues[/yellow]\n"
"[yellow]and attach the workflow file if possible.[/yellow]"
)
if verbose:
import traceback

traceback.print_exc()
raise typer.Exit(code=1) from e
if not workflow:
pprint("[bold red]Workflow conversion produced no executable nodes[/bold red]")
raise typer.Exit(code=1)
else:
workflow = _validate_api_workflow(raw_workflow)
Expand Down
Loading
Loading