Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,25 @@ Comfy provides commands that allow you to easily run the installed ComfyUI.
`comfy --workspace=~/comfy launch --background -- --listen 10.0.0.10 --port 8000`

- Instances launched with `--background` are displayed in the "Background ComfyUI" section of `comfy env`, providing management functionalities for a single background instance only.
- Since "Comfy Server Running" in `comfy env` only shows the default port 8188, it doesn't display ComfyUI running on a different port.
- Background-running ComfyUI can be stopped with `comfy stop`.

- To point **every** command (not just one) at a ComfyUI on a non-default
address — e.g. a server you started _outside_ comfy-cli on `:8189` — export
the `COMFY_LOCAL_URL` environment variable:

`export COMFY_LOCAL_URL=http://127.0.0.1:8189`

- Accepts `http://host:port`, `host:port`, or `http://host` (the port
defaults to `8188`; the scheme is optional and, if present, must be
`http`). IPv6 literals are bracketed: `COMFY_LOCAL_URL=http://[::1]:8189`.
- Honored by `comfy env`, `comfy run`, `comfy jobs`, `comfy upload`/`download`,
`comfy nodes`, and every other local-targeting command, so `comfy env`'s
"Comfy Server Running" line now probes and reports the resolved address.
- Precedence (per command): a per-command `--host`/`--port` flag wins, then
`COMFY_LOCAL_URL`, then a comfy-cli-launched background server, then the
`127.0.0.1:8188` default. A malformed value is ignored with a one-line
stderr warning rather than breaking the command.

- to run ComfyUI with a specific pull request:

`comfy install --pr "#1234"`
Expand Down
12 changes: 5 additions & 7 deletions comfy_cli/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ def validate(
pass

try:
graph = Graph.load(mode=mode, input_path=input_path, host=host or "127.0.0.1", port=port or 8188)
graph = Graph.load(mode=mode, input_path=input_path, host=host, port=port)
except LoadError as e:
renderer.error(
code="cql_no_graph",
Expand Down Expand Up @@ -1327,13 +1327,11 @@ def which():
if renderer.is_pretty():
import sys as _sys

from comfy_cli.env_checker import _display_url
from comfy_cli.local_address import resolve_local_host_port
from comfy_cli.output.panels import which_panel

cfg_bg = ConfigManager().background
if cfg_bg is not None:
host, port = cfg_bg[0], cfg_bg[1]
else:
host, port = "127.0.0.1", 8188
host, port = resolve_local_host_port(None, None, background=ConfigManager().background)
Comment thread
mattmillerai marked this conversation as resolved.
try:
server_running = env_checker.check_comfy_server_running(host=host, port=port, timeout=0.5)
except Exception: # noqa: BLE001
Expand All @@ -1345,7 +1343,7 @@ def which():
python_executable=_sys.executable,
python_version=f"{_sys.version_info.major}.{_sys.version_info.minor}.{_sys.version_info.micro}",
server_running=server_running,
server_url=f"http://{host}:{port}",
server_url=_display_url(host, port),
version=ConfigManager().get_cli_version(),
)
)
Expand Down
13 changes: 10 additions & 3 deletions comfy_cli/command/job_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,16 @@ def watch_job(
def _poll_local_once(state: jobs_state.JobState, *, host: str | None, port: int | None) -> bool:
"""Update ``state`` in-place from a local ComfyUI server. Return True if terminal."""
from comfy_cli.command import jobs as jobs_module

h = host or state.host or "127.0.0.1"
p = port or state.port or 8188
from comfy_cli.local_address import resolve_local_host_port

# Per-job recorded state (state.host/port, captured when the job was
# submitted) still wins over the env var, so a watcher keeps polling the
# server it was launched against: flag > state > COMFY_LOCAL_URL > default.
h, p = resolve_local_host_port(host or state.host, port or state.port)
# Bracket IPv6 literals so ``_snapshot`` builds a well-formed URL (it takes
# an already-bracketed host, like the `jobs` resolver produces).
if ":" in h and not h.startswith("["):
h = f"[{h}]"
try:
snap = jobs_module._snapshot(h, p, state.prompt_id)
except Exception as e: # noqa: BLE001 — never crash the watcher on transient errors
Expand Down
8 changes: 4 additions & 4 deletions comfy_cli/command/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def _get_graph(
return Graph.load(
mode=mode,
input_path=input_path,
host=host or "127.0.0.1",
port=port or 8188,
host=host,
port=port,
)
# Live fetch goes through the resilient loader: auto-cache on success,
# refresh-and-retry once, then fall back to the last cached dump (with
Expand All @@ -84,8 +84,8 @@ def _get_graph(

raw = resilient_load_object_info(
mode=mode,
host=host or "127.0.0.1",
port=port or 8188,
host=host,
port=port,
on_stale=on_stale,
)
graph = Graph.from_object_info(raw)
Expand Down
9 changes: 6 additions & 3 deletions comfy_cli/command/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,13 @@ def _verify_cloud(console) -> None:

def _verify_local(console) -> None:
"""Check if local ComfyUI server is running."""
from comfy_cli.env_checker import check_comfy_server_running
from comfy_cli.config_manager import ConfigManager
from comfy_cli.env_checker import _bracket_host, check_comfy_server_running
from comfy_cli.local_address import resolve_local_host_port

if check_comfy_server_running(8188, "127.0.0.1"):
pprint(" [bold green]✓[/bold green] Local server running on [cyan]127.0.0.1:8188[/cyan]")
host, port = resolve_local_host_port(None, None, background=ConfigManager().background)
Comment thread
mattmillerai marked this conversation as resolved.
if check_comfy_server_running(port, host):
pprint(f" [bold green]✓[/bold green] Local server running on [cyan]{_bracket_host(host)}:{port}[/cyan]")
else:
pprint(" [dim yellow]⚠[/dim yellow] Local server not running")
pprint(" [dim] Start it with: [cyan]comfy launch[/cyan][/dim]")
Expand Down
6 changes: 3 additions & 3 deletions comfy_cli/command/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _get_graph(input_path: str | None, host: str | None, port: int | None, on_st
try:
if input_path is not None:
# Explicit offline dump — Graph.load reads + annotates it.
return Graph.load(input_path=input_path, host=host or "127.0.0.1", port=port or 8188)
return Graph.load(input_path=input_path, host=host, port=port)
# Live fetch: resolve mode from global routing chain, then use resilient loader.
from comfy_cli import where as where_module

Expand All @@ -99,8 +99,8 @@ def _get_graph(input_path: str | None, host: str | None, port: int | None, on_st

raw = resilient_load_object_info(
mode=mode,
host=host or "127.0.0.1",
port=port or 8188,
host=host,
port=port,
on_stale=on_stale,
)
graph = Graph.from_object_info(raw)
Expand Down
2 changes: 1 addition & 1 deletion comfy_cli/command/workflow_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def _load_object_info(renderer, *, input_path: str | None, host: str | None, por

decision = where_module.resolve_default()
mode = "cloud" if decision.target is where_module.WhereTarget.CLOUD else "local"
return resilient_load_object_info(mode=mode, host=host or "127.0.0.1", port=port or 8188)
return resilient_load_object_info(mode=mode, host=host, port=port)
except (OSError, json.JSONDecodeError) as e:
renderer.error(
code="object_info_unavailable",
Expand Down
6 changes: 3 additions & 3 deletions comfy_cli/cql/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,8 +908,8 @@ def load(
*,
mode: str = "local",
input_path: str | None = None,
host: str = "127.0.0.1",
port: int = 8188,
host: str | None = None,
port: int | None = None,
supported_nodes_yaml: bytes | None = None,
cloud_disable_yaml: bytes | None = None,
no_gpu_json: bytes | None = None,
Expand Down Expand Up @@ -1113,7 +1113,7 @@ def _load_from_file(path: str) -> dict[str, Any]:
raise LoadError(f"invalid JSON in {p}: {e}", details={"path": str(p)}) from e


def _load_from_target(*, mode: str = "local", host: str = "127.0.0.1", port: int = 8188) -> dict[str, Any]:
def _load_from_target(*, mode: str = "local", host: str | None = None, port: int | None = None) -> dict[str, Any]:
"""Fetch /object_info from the resolved target — local or cloud.

Both paths are the same HTTP fetch; only the base URL, path prefix,
Expand Down
6 changes: 3 additions & 3 deletions comfy_cli/cql/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def read_object_info_cache(host_key: str) -> dict[str, Any] | None:
return data if isinstance(data, dict) else None


def _resolve_host_key(mode: str, host: str, port: int) -> str:
def _resolve_host_key(mode: str, host: str | None, port: int | None) -> str:
"""Resolve the cache key (the target base URL) without doing any I/O.

Mirrors how the engine resolves its fetch target so the cache key matches
Expand All @@ -359,8 +359,8 @@ def _resolve_host_key(mode: str, host: str, port: int) -> str:
def resilient_load_object_info(
*,
mode: str = "local",
host: str = "127.0.0.1",
port: int = 8188,
host: str | None = None,
port: int | None = None,
input_path: str | None = None,
_warn=None,
on_stale=None,
Expand Down
42 changes: 37 additions & 5 deletions comfy_cli/env_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ def format_python_version(version_info):
return f"[bold red]{version_info.major}.{version_info.minor}.{version_info.micro}[/bold red]"


def _bracket_host(host: str) -> str:
"""Bracket a bare IPv6 literal (``::1`` -> ``[::1]``) for use in a URL.

Idempotent: an already-bracketed host (as returned by
``host_port.resolve_host_port``) and hostnames / IPv4 (no ``:``) pass
through unchanged, so it's safe to apply at a shared choke point regardless
of whether the caller pre-bracketed.
"""
if ":" in host and not host.startswith("["):
return f"[{host}]"
return host


def check_comfy_server_running(port=8188, host="localhost", timeout: float = 5.0):
"""
Checks if the Comfy server is running by making a GET request to the /history endpoint.
Expand All @@ -43,12 +56,29 @@ def check_comfy_server_running(port=8188, host="localhost", timeout: float = 5.0
bool: True if the Comfy server is running, False otherwise.
"""
try:
response = requests.get(f"http://{host}:{port}/history", timeout=timeout)
response = requests.get(f"http://{_bracket_host(host)}:{port}/history", timeout=timeout)
return response.status_code == 200
except requests.exceptions.RequestException:
return False


def _resolved_local_address() -> tuple[str, int]:
"""The local ComfyUI ``(host, port)`` ``comfy env`` should probe + report.

Honors the same precedence as every other local command minus the
per-command flag (``comfy env`` takes none): ``COMFY_LOCAL_URL`` env >
``config.background`` > ``127.0.0.1:8188``.
"""
from comfy_cli.local_address import resolve_local_host_port

return resolve_local_host_port(None, None, background=ConfigManager().background)


def _display_url(host: str, port: int) -> str:
"""``http://host:port`` with IPv6 literals bracketed for a valid URL."""
return f"http://{_bracket_host(host)}:{port}"


@singleton
class EnvChecker:
"""
Expand Down Expand Up @@ -106,11 +136,12 @@ def fill_print_table(self):
config_data = ConfigManager().get_env_data()
data.extend(config_data)

if check_comfy_server_running():
host, port = _resolved_local_address()
if check_comfy_server_running(port=port, host=host):
Comment thread
mattmillerai marked this conversation as resolved.
data.append(
(
"Comfy Server Running",
"[bold green]Yes[/bold green]\nhttp://localhost:8188",
f"[bold green]Yes[/bold green]\n{_display_url(host, port)}",
)
)
else:
Expand All @@ -126,7 +157,8 @@ def fill_data(self) -> dict:
against ``schemas/env.json``.
"""
cm = ConfigManager()
server_running = check_comfy_server_running()
host, port = _resolved_local_address()
server_running = check_comfy_server_running(port=port, host=host)
return {
"python": {
"version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
Expand All @@ -137,6 +169,6 @@ def fill_data(self) -> dict:
"config": cm.get_data(),
"server": {
"running": server_running,
"url": "http://localhost:8188" if server_running else None,
"url": _display_url(host, port) if server_running else None,
},
}
19 changes: 9 additions & 10 deletions comfy_cli/host_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,15 @@ def _to_port(s: str, original: str) -> int:


def resolve_host_port(host: str | None, port: int | None) -> tuple[str, int]:
"""Fill host/port from ``config.background`` then defaults; validate and
bracket IPv6 literals so callers building ``'http://{host}:{port}'`` get a
well-formed URL (e.g. ``'::1'`` -> ``'[::1]'``)."""
"""Resolve host/port by precedence — explicit flag > ``COMFY_LOCAL_URL``
env > ``config.background`` > defaults — then validate and bracket IPv6
literals so callers building ``'http://{host}:{port}'`` get a well-formed
URL (e.g. ``'::1'`` -> ``'[::1]'``)."""
from comfy_cli.local_address import resolve_local_host_port

cfg = ConfigManager()
bg = cfg.background
if not host and bg is not None:
host = bg[0]
if not port and bg is not None:
port = bg[1]
h = validate_host(host or DEFAULT_HOST)
host, port = resolve_local_host_port(host, port, background=cfg.background)
h = validate_host(host)
if ":" in h and not h.startswith("["):
h = f"[{h}]"
return (h, int(port or DEFAULT_PORT))
return (h, int(port))
Loading
Loading