From 5e763eabdbbfc6e37c6b1554af8fe45962b2f9ee Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 12:57:02 -0400 Subject: [PATCH 1/7] Add node bisect --- comfy_cli/cmdline.py | 5 + .../custom_nodes/bisect_custom_nodes.py | 184 ++++++++++++++++++ comfy_cli/command/custom_nodes/cm_cli_util.py | 65 +++++++ comfy_cli/command/custom_nodes/command.py | 51 +---- .../custom_nodes/test_bisect_custom_nodes.py | 129 ++++++++++++ .../command/nodes/test_bisect_custom_nodes.py | 126 ++++++++++++ 6 files changed, 512 insertions(+), 48 deletions(-) create mode 100644 comfy_cli/command/custom_nodes/bisect_custom_nodes.py create mode 100644 comfy_cli/command/custom_nodes/cm_cli_util.py create mode 100644 comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py create mode 100644 tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 99aa79b6..ea6f90e4 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -810,6 +810,11 @@ def feedback(): print("Thank you for your feedback!") +# @app.command(help="Bisect culprit custom node.") +# @tracking.track_command() +# def bisect_custom_node(): + + app.add_typer(models_command.app, name="model", help="Manage models.") app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.") app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.") diff --git a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py new file mode 100644 index 00000000..70bcc4ce --- /dev/null +++ b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Literal, NamedTuple + +import typer + +from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli + +bisect_app = typer.Typer() + +# File to store the state of bisect +default_state_file = Path("bisect_state.json") + + +class BisectState(NamedTuple): + status: Literal["idle", "running", "resolved"] + + # All nodes in the current bisect session + all: list[str] + + # The range of nodes that contains the bad node + range: list[str] + + # The active set of nodes to test + active: list[str] + + def good(self) -> BisectState: + """The active set of nodes is good, narrowing down the potential problem area.""" + if self.status != "running": + raise ValueError("No bisect session running.") + + new_range = list(set(self.range) - set(self.active)) + + if len(new_range) == 1: + return BisectState( + status="resolved", all=self.all, range=new_range, active=[] + ) + + return BisectState( + status="running", + all=self.all, + range=new_range, + active=new_range[len(new_range) // 2 :], + ) + + def bad(self) -> BisectState: + """The active set of nodes is bad, indicating the problem is within this set.""" + if self.status != "running": + raise ValueError("No bisect session running.") + + new_range = self.active + + if len(new_range) == 1: + return BisectState( + status="resolved", all=self.all, range=new_range, active=[] + ) + + return BisectState( + status="running", + all=self.all, + range=new_range, + active=new_range[len(new_range) // 2 :], + ) + + def save(self, state_file=None): + self.set_custom_node_enabled_states() + state_file = state_file or default_state_file + with state_file.open("w") as f: + json.dump(self._asdict(), f) # pylint: disable=no-member + + def reset(self): + BisectState( + "idle", all=self.all, range=self.all, active=self.all + ).set_custom_node_enabled_states() + return BisectState("idle", self.all, self.all, self.all) + + @classmethod + def load(cls, state_file=None) -> BisectState: + state_file = state_file or default_state_file + if state_file.exists(): + with state_file.open() as f: + return BisectState(**json.load(f)) + return BisectState("idle", [], [], []) + + @property + def inactive_nodes(self) -> list[str]: + return list(set(self.all) - set(self.active)) + + def set_custom_node_enabled_states(self): + if self.active: + execute_cm_cli(["enable", *self.active]) + if self.inactive_nodes: + execute_cm_cli(["disable", *self.inactive_nodes]) + + def __str__(self): + active_list = "\n".join( + [f"{i:3}. {node}" for i, node in enumerate(self.active)] + ) + return f"""BisectState(status={self.status}) +set of nodes with culprit: {len(self.range)} +set of nodes to test: {len(self.active)} +-------------------------- +{active_list}""" + + +@bisect_app.command( + help="Start a new bisect session with a comma-separated list of nodes." +) +def start(): + """Start a new bisect session with a comma-separated list of nodes. + The initial state is bad with all custom nodes enabled, good with + all custom nodes disabled.""" + + cm_output: str | None = execute_cm_cli(["simple-show", "enabled"]) + if cm_output is None: + typer.echo("Failed to fetch the list of nodes.") + raise typer.Exit() + + nodes_list = [ + line.strip() + for line in cm_output.strip().split("\n") + if not line.startswith("FETCH DATA") + ] + state = BisectState( + status="running", + all=nodes_list, + range=nodes_list, + active=nodes_list, + ) + state.save() + + typer.echo(f"Bisect session started.\n{state}") + bad() + + +@bisect_app.command( + help="Mark the current active set as good, indicating the problem is outside the test set." +) +def good(): + state = BisectState.load() + if state.status != "running": + typer.echo("No bisect session running or no active nodes to process.") + raise typer.Exit() + + new_state = state.good() + + if new_state.status == "resolved": + assert len(new_state.range) == 1 + typer.echo(f"Problematic node identified: {new_state.range[0]}") + reset() + else: + new_state.save() + typer.echo(new_state) + + +@bisect_app.command( + help="Mark the current active set as bad, indicating the problem is within the test set." +) +def bad(): + state = BisectState.load() + if state.status != "running": + typer.echo("No bisect session running or no active nodes to process.") + raise typer.Exit() + + new_state = state.bad() + + if new_state.status == "resolved": + assert len(new_state.range) == 1 + typer.echo(f"Problematic node identified: {new_state.range[0]}") + reset() + else: + new_state.save() + typer.echo(new_state) + + +@bisect_app.command(help="Reset the current bisect session.") +def reset(): + if default_state_file.exists(): + BisectState.load().reset() + typer.echo("Bisect session reset.") + else: + typer.echo("No bisect session to reset.") diff --git a/comfy_cli/command/custom_nodes/cm_cli_util.py b/comfy_cli/command/custom_nodes/cm_cli_util.py new file mode 100644 index 00000000..80ebb897 --- /dev/null +++ b/comfy_cli/command/custom_nodes/cm_cli_util.py @@ -0,0 +1,65 @@ +import os +import subprocess +import sys +import uuid + +import typer +from rich import print + +from comfy_cli.config_manager import ConfigManager +from comfy_cli.workspace_manager import WorkspaceManager + +workspace_manager = WorkspaceManager() + + +def execute_cm_cli(args, channel=None, mode=None) -> str | None: + _config_manager = ConfigManager() + + workspace_path = workspace_manager.workspace_path + + if not workspace_path: + print("\n[bold red]ComfyUI path is not resolved.[/bold red]\n", file=sys.stderr) + raise typer.Exit(code=1) + + cm_cli_path = os.path.join( + workspace_path, "custom_nodes", "ComfyUI-Manager", "cm-cli.py" + ) + if not os.path.exists(cm_cli_path): + print( + f"\n[bold red]ComfyUI-Manager not found: {cm_cli_path}[/bold red]\n", + file=sys.stderr, + ) + raise typer.Exit(code=1) + + cmd = [sys.executable, cm_cli_path] + args + if channel is not None: + cmd += ["--channel", channel] + + if mode is not None: + cmd += ["--mode", mode] + + new_env = os.environ.copy() + session_path = os.path.join( + _config_manager.get_config_path(), "tmp", str(uuid.uuid4()) + ) + new_env["__COMFY_CLI_SESSION__"] = session_path + new_env["COMFYUI_PATH"] = workspace_path + + print(f"Execute from: {workspace_path}") + + try: + result = subprocess.run( + cmd, env=new_env, check=True, capture_output=True, text=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + if e.returncode == 1: + print(f"\n[bold red]Execution error: {cmd}[/bold red]\n", file=sys.stderr) + return None + + if e.returncode == 2: + return None + + raise e + finally: + workspace_manager.set_recent_workspace(workspace_path) diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 2da9f6ac..218b796c 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -11,6 +11,8 @@ from typing_extensions import Annotated, List from comfy_cli import logging, tracking, ui, utils +from comfy_cli.command.custom_nodes.bisect_custom_nodes import bisect_app +from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli from comfy_cli.config_manager import ConfigManager from comfy_cli.constants import NODE_ZIP_FILENAME from comfy_cli.file_utils import ( @@ -27,59 +29,12 @@ from comfy_cli.workspace_manager import WorkspaceManager app = typer.Typer() +app.add_typer(bisect_app, name="bisect", help="Bisect custom nodes for culprit node.") manager_app = typer.Typer() workspace_manager = WorkspaceManager() registry_api = RegistryAPI() -def execute_cm_cli(args, channel=None, mode=None): - _config_manager = ConfigManager() - - workspace_path = workspace_manager.workspace_path - - if not workspace_path: - print("\n[bold red]ComfyUI path is not resolved.[/bold red]\n", file=sys.stderr) - raise typer.Exit(code=1) - - cm_cli_path = os.path.join( - workspace_path, "custom_nodes", "ComfyUI-Manager", "cm-cli.py" - ) - if not os.path.exists(cm_cli_path): - print( - f"\n[bold red]ComfyUI-Manager not found: {cm_cli_path}[/bold red]\n", - file=sys.stderr, - ) - raise typer.Exit(code=1) - - cmd = [sys.executable, cm_cli_path] + args - if channel is not None: - cmd += ["--channel", channel] - - if mode is not None: - cmd += ["--mode", mode] - - new_env = os.environ.copy() - session_path = os.path.join( - _config_manager.get_config_path(), "tmp", str(uuid.uuid4()) - ) - new_env["__COMFY_CLI_SESSION__"] = session_path - new_env["COMFYUI_PATH"] = workspace_path - - print(f"Execute from: {workspace_path}") - - try: - subprocess.run(cmd, env=new_env, check=True) - except subprocess.CalledProcessError as e: - if e.returncode == 1: - print(f"\n[bold red]Execution error: {cmd}[/bold red]\n", file=sys.stderr) - elif e.returncode == 2: - pass - else: - raise e - - workspace_manager.set_recent_workspace(workspace_path) - - def validate_comfyui_manager(_env_checker): manager_path = _env_checker.get_comfyui_manager_path() diff --git a/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py new file mode 100644 index 00000000..29b5380b --- /dev/null +++ b/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py @@ -0,0 +1,129 @@ +import json +from unittest.mock import patch + +import pytest + +from comfy_cli.command.custom_nodes.bisect_custom_nodes import BisectState + + +@pytest.fixture(scope="function") +def bisect_state(): + return BisectState( + status="running", + all=["node1", "node2", "node3"], + range=["node1", "node2", "node3"], + active=["node1", "node2"], + ) + + +def test_good(bisect_state): + new_state = bisect_state.good() + assert new_state.status == "running" + assert new_state.all == bisect_state.all + assert new_state.range == ["node2", "node3"] + assert new_state.active == ["node2"] + + +def test_good_resolved(bisect_state): + bisect_state.range = ["node2"] + new_state = bisect_state.good() + assert new_state.status == "resolved" + assert new_state.all == bisect_state.all + assert new_state.range == ["node2"] + assert new_state.active == ["node2"] + + +def test_bad(bisect_state): + new_state = bisect_state.bad() + assert new_state.status == "running" + assert new_state.all == bisect_state.all + assert new_state.range == ["node1", "node2"] + assert new_state.active == ["node1"] + + +def test_bad_resolved(bisect_state): + bisect_state.range = ["node1"] + new_state = bisect_state.bad() + assert new_state.status == "resolved" + assert new_state.all == bisect_state.all + assert new_state.range == ["node1"] + assert new_state.active == ["node1"] + + +def test_save(bisect_state, tmp_path): + bisect_state.save() + state_file = tmp_path / "bisect_state.json" + assert state_file.exists() + with state_file.open() as f: + saved_state = json.load(f) + assert saved_state == vars(bisect_state) + + +def test_reset(bisect_state): + bisect_state.reset() + assert bisect_state.status == "idle" + assert bisect_state.range == bisect_state.all + assert bisect_state.active == bisect_state.all + + +def test_load_existing_state(tmp_path): + state_file = tmp_path / "bisect_state.json" + state_data = { + "status": "running", + "all": ["node1", "node2", "node3"], + "range": ["node1", "node2", "node3"], + "active": ["node1", "node2"], + } + with state_file.open("w") as f: + json.dump(state_data, f) + loaded_state = BisectState.load() + assert loaded_state.status == state_data["status"] + assert loaded_state.all == state_data["all"] + assert loaded_state.range == state_data["range"] + assert loaded_state.active == state_data["active"] + + +def test_load_nonexistent_state(tmp_path): + state_file = tmp_path / "bisect_state.json" + loaded_state = BisectState.load(state_file) + assert loaded_state.status == "idle" + assert loaded_state.all == [] + assert loaded_state.range == [] + assert loaded_state.active == [] + + +def test_inactive_nodes(bisect_state): + bisect_state.active = ["node1", "node2"] + assert bisect_state.inactive_nodes == ["node3"] + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_set_custom_node_enabled_states(mock_execute_cm_cli, bisect_state): + bisect_state.set_custom_node_enabled_states() + mock_execute_cm_cli.assert_called_once_with(["enable", "node1", "node2"]) + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_set_custom_node_enabled_states_no_active_nodes( + mock_execute_cm_cli, bisect_state +): + bisect_state.active = [] + bisect_state.set_custom_node_enabled_states() + mock_execute_cm_cli.assert_called_once_with(["disable", "node1", "node2", "node3"]) + + +def test_str(bisect_state, capsys): + expected_output = """BisectState(status=running) + bad nodes: 3 + test set nodes: 2 + -------------------------- + 0. node1 + 1. node2 + """ + print(bisect_state) + captured = capsys.readouterr() + assert captured.out.strip() == expected_output.strip() + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py b/tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py new file mode 100644 index 00000000..60359d21 --- /dev/null +++ b/tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py @@ -0,0 +1,126 @@ +import json +from unittest.mock import patch + +import pytest + +from comfy_cli.command.custom_nodes.bisect_custom_nodes import BisectState + + +@pytest.fixture(scope="function") +def bisect_state(): + return BisectState( + status="running", + all=["node1", "node2", "node3"], + range=["node1", "node2", "node3"], + active=["node1", "node2"], + ) + + +def test_good(): + bisect_state = BisectState( + status="running", + all=["node1", "node2", "node3"], + range=["node1", "node2", "node3"], + active=["node1"], + ) + new_state = bisect_state.good() + assert new_state.status == "running" + assert new_state.all == bisect_state.all + assert set(new_state.range) == set(["node3", "node2"]) + assert len(new_state.active) == 1 + + +def test_good_resolved(bisect_state: BisectState): + new_state = bisect_state.good() + assert new_state.status == "resolved" + assert new_state.all == bisect_state.all + assert new_state.range == ["node3"] + assert new_state.active == [] + + +def test_bad(bisect_state): + new_state = bisect_state.bad() + assert new_state.status == "running" + assert new_state.all == bisect_state.all + assert new_state.range == ["node1", "node2"] + assert new_state.active == ["node2"] + + +def test_bad_resolved(): + bisect_state = BisectState( + status="running", + all=["node1", "node2", "node3"], + range=["node1", "node2", "node3"], + active=["node1"], + ) + new_state = bisect_state.bad() + assert new_state.status == "resolved" + assert new_state.all == bisect_state.all + assert new_state.range == ["node1"] + assert new_state.active == [] + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_save(mock_execute_cm_cli, bisect_state, tmp_path): + state_file = tmp_path / "bisect_state.json" + bisect_state.save(state_file) + assert state_file.exists() + assert mock_execute_cm_cli.call_count == 2 + with state_file.open() as f: + saved_state = json.load(f) + assert saved_state == bisect_state._asdict() + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_reset(mock_execute_cm_cli, bisect_state): + new_state = bisect_state.reset() + assert new_state.status == "idle" + assert new_state.all == ["node1", "node2", "node3"] + assert new_state.range == ["node1", "node2", "node3"] + assert new_state.active == ["node1", "node2", "node3"] + assert mock_execute_cm_cli.call_count == 1 + + +def test_load_existing_state(tmp_path): + state_file = tmp_path / "bisect_state.json" + state_data = { + "status": "running", + "all": ["node1", "node2", "node3"], + "range": ["node1", "node2", "node3"], + "active": ["node1", "node2"], + } + with state_file.open("w") as f: + json.dump(state_data, f) + + loaded_state = BisectState.load(state_file) + assert loaded_state.status == state_data["status"] + assert loaded_state.all == state_data["all"] + assert loaded_state.range == state_data["range"] + assert loaded_state.active == state_data["active"] + + +def test_load_nonexistent_state(tmp_path): + state_file = tmp_path / "bisect_state.json" + loaded_state = BisectState.load(state_file) + assert loaded_state.status == "idle" + assert loaded_state.all == [] + assert loaded_state.range == [] + assert loaded_state.active == [] + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_set_custom_node_enabled_states(mock_execute_cm_cli, bisect_state): + bisect_state.set_custom_node_enabled_states() + assert mock_execute_cm_cli.call_count == 2 + + +@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") +def test_set_custom_node_enabled_states_no_active_nodes(mock_execute_cm_cli): + bisect_state = BisectState( + status="running", + all=["node1", "node2", "node3"], + range=["node1", "node2", "node3"], + active=[], + ) + bisect_state.set_custom_node_enabled_states() + assert mock_execute_cm_cli.call_count == 1 From 78b76ae21a2bdee4b36defa2bb4b2325745ae80c Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 13:05:19 -0400 Subject: [PATCH 2/7] Fix annotation --- comfy_cli/command/custom_nodes/cm_cli_util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/comfy_cli/command/custom_nodes/cm_cli_util.py b/comfy_cli/command/custom_nodes/cm_cli_util.py index 80ebb897..1b40033b 100644 --- a/comfy_cli/command/custom_nodes/cm_cli_util.py +++ b/comfy_cli/command/custom_nodes/cm_cli_util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import subprocess import sys From 052baba3ac942721c86807d5dc350ea1b31adc6d Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 13:12:41 -0400 Subject: [PATCH 3/7] Remove auto-generated testfile --- .gitignore | 2 + .../custom_nodes/test_bisect_custom_nodes.py | 129 ------------------ 2 files changed, 2 insertions(+), 129 deletions(-) delete mode 100644 comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py diff --git a/.gitignore b/.gitignore index 729e3b4c..fffdff1f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ share/python-wheels/ *.spec venv/ + +bisect_state.json diff --git a/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py deleted file mode 100644 index 29b5380b..00000000 --- a/comfy_cli/command/custom_nodes/test_bisect_custom_nodes.py +++ /dev/null @@ -1,129 +0,0 @@ -import json -from unittest.mock import patch - -import pytest - -from comfy_cli.command.custom_nodes.bisect_custom_nodes import BisectState - - -@pytest.fixture(scope="function") -def bisect_state(): - return BisectState( - status="running", - all=["node1", "node2", "node3"], - range=["node1", "node2", "node3"], - active=["node1", "node2"], - ) - - -def test_good(bisect_state): - new_state = bisect_state.good() - assert new_state.status == "running" - assert new_state.all == bisect_state.all - assert new_state.range == ["node2", "node3"] - assert new_state.active == ["node2"] - - -def test_good_resolved(bisect_state): - bisect_state.range = ["node2"] - new_state = bisect_state.good() - assert new_state.status == "resolved" - assert new_state.all == bisect_state.all - assert new_state.range == ["node2"] - assert new_state.active == ["node2"] - - -def test_bad(bisect_state): - new_state = bisect_state.bad() - assert new_state.status == "running" - assert new_state.all == bisect_state.all - assert new_state.range == ["node1", "node2"] - assert new_state.active == ["node1"] - - -def test_bad_resolved(bisect_state): - bisect_state.range = ["node1"] - new_state = bisect_state.bad() - assert new_state.status == "resolved" - assert new_state.all == bisect_state.all - assert new_state.range == ["node1"] - assert new_state.active == ["node1"] - - -def test_save(bisect_state, tmp_path): - bisect_state.save() - state_file = tmp_path / "bisect_state.json" - assert state_file.exists() - with state_file.open() as f: - saved_state = json.load(f) - assert saved_state == vars(bisect_state) - - -def test_reset(bisect_state): - bisect_state.reset() - assert bisect_state.status == "idle" - assert bisect_state.range == bisect_state.all - assert bisect_state.active == bisect_state.all - - -def test_load_existing_state(tmp_path): - state_file = tmp_path / "bisect_state.json" - state_data = { - "status": "running", - "all": ["node1", "node2", "node3"], - "range": ["node1", "node2", "node3"], - "active": ["node1", "node2"], - } - with state_file.open("w") as f: - json.dump(state_data, f) - loaded_state = BisectState.load() - assert loaded_state.status == state_data["status"] - assert loaded_state.all == state_data["all"] - assert loaded_state.range == state_data["range"] - assert loaded_state.active == state_data["active"] - - -def test_load_nonexistent_state(tmp_path): - state_file = tmp_path / "bisect_state.json" - loaded_state = BisectState.load(state_file) - assert loaded_state.status == "idle" - assert loaded_state.all == [] - assert loaded_state.range == [] - assert loaded_state.active == [] - - -def test_inactive_nodes(bisect_state): - bisect_state.active = ["node1", "node2"] - assert bisect_state.inactive_nodes == ["node3"] - - -@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") -def test_set_custom_node_enabled_states(mock_execute_cm_cli, bisect_state): - bisect_state.set_custom_node_enabled_states() - mock_execute_cm_cli.assert_called_once_with(["enable", "node1", "node2"]) - - -@patch("comfy_cli.command.custom_nodes.bisect_custom_nodes.execute_cm_cli") -def test_set_custom_node_enabled_states_no_active_nodes( - mock_execute_cm_cli, bisect_state -): - bisect_state.active = [] - bisect_state.set_custom_node_enabled_states() - mock_execute_cm_cli.assert_called_once_with(["disable", "node1", "node2", "node3"]) - - -def test_str(bisect_state, capsys): - expected_output = """BisectState(status=running) - bad nodes: 3 - test set nodes: 2 - -------------------------- - 0. node1 - 1. node2 - """ - print(bisect_state) - captured = capsys.readouterr() - assert captured.out.strip() == expected_output.strip() - - -if __name__ == "__main__": - pytest.main() From 09bde2cf4918ee0e55edcb10fa8ad90537509414 Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 13:20:10 -0400 Subject: [PATCH 4/7] nit --- README.md | 11 ++++++++++- comfy_cli/cmdline.py | 5 ----- comfy_cli/command/custom_nodes/bisect_custom_nodes.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index abfd585f..9e1636db 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,16 @@ comfy node [show|simple-show] [installed|enabled|not-installed|disabled|all|snap - Generate deps: - `comfy node deps-in-workflow --workflow= --output=` + `comfy node deps-in-workflow --workflow= --output=` + +#### Bisect custom nodes + +The bisect tool can help you pinpoint the custom node that causes the issue. + +- `comfy node bisect start`: Start a new bisect session with a comma-separated list of nodes. +- `comfy node bisect good`: Mark the current active set as bad, indicating the problem is within the test set +- `comfy node bisect bad`: Mark the current active set as bad, indicating the problem is within the test set. +- `comfy node bisect reset`: Reset the current bisect session. ### Managing Models diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index ea6f90e4..99aa79b6 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -810,11 +810,6 @@ def feedback(): print("Thank you for your feedback!") -# @app.command(help="Bisect culprit custom node.") -# @tracking.track_command() -# def bisect_custom_node(): - - app.add_typer(models_command.app, name="model", help="Manage models.") app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.") app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.") diff --git a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py index 70bcc4ce..79bf5a81 100644 --- a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +++ b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py @@ -96,7 +96,7 @@ def set_custom_node_enabled_states(self): def __str__(self): active_list = "\n".join( - [f"{i:3}. {node}" for i, node in enumerate(self.active)] + [f"{i + 1:3}. {node}" for i, node in enumerate(self.active)] ) return f"""BisectState(status={self.status}) set of nodes with culprit: {len(self.range)} From 45082482cf9abb5141ab3281dde6a2fa94671eb1 Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 15:15:42 -0400 Subject: [PATCH 5/7] Auto launch --- comfy_cli/cmdline.py | 253 +---------------- .../custom_nodes/bisect_custom_nodes.py | 39 ++- comfy_cli/command/launch.py | 267 ++++++++++++++++++ 3 files changed, 304 insertions(+), 255 deletions(-) create mode 100644 comfy_cli/command/launch.py diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 99aa79b6..0b776ea2 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -1,9 +1,6 @@ -import asyncio import os import subprocess import sys -import threading -import uuid import webbrowser from typing import Optional @@ -11,23 +8,19 @@ import typer from rich import print from rich.console import Console -from rich.panel import Panel from typing_extensions import Annotated, List from comfy_cli import constants, env_checker, logging, tracking, ui, utils from comfy_cli.command import custom_nodes from comfy_cli.command import install as install_inner from comfy_cli.command import run as run_inner +from comfy_cli.command.launch import launch as launch_command from comfy_cli.command.models import models as models_command from comfy_cli.config_manager import ConfigManager from comfy_cli.constants import GPU_OPTION, CUDAVersion -from comfy_cli.env_checker import EnvChecker, check_comfy_server_running +from comfy_cli.env_checker import EnvChecker from comfy_cli.update import check_for_updates -from comfy_cli.workspace_manager import ( - WorkspaceManager, - WorkspaceType, - check_comfy_repo, -) +from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo logging.setup_logging() app = typer.Typer() @@ -432,215 +425,6 @@ def validate_comfyui(_env_checker): raise typer.Exit(code=1) -async def launch_and_monitor(cmd, listen, port): - """ - Monitor the process during the background launch. - - If a success message is captured, exit; - otherwise, return the log in case of failure. - """ - logging_flag = False - log = [] - logging_lock = threading.Lock() - - # NOTE: To prevent encoding error on Windows platform - env = dict(os.environ, PYTHONIOENCODING="utf-8") - env["COMFY_CLI_BACKGROUND"] = "true" - - if sys.platform == "win32": - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=env, - encoding="utf-8", - shell=True, # win32 only - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # win32 only - ) - else: - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=env, - encoding="utf-8", - ) - - def msg_hook(stream): - nonlocal log - nonlocal logging_flag - - while True: - line = stream.readline() - if "Launching ComfyUI from:" in line: - logging_flag = True - elif "To see the GUI go to:" in line: - print( - f"[bold yellow]ComfyUI is successfully launched in the background.[/bold yellow]\nTo see the GUI go to: http://{listen}:{port}" - ) - ConfigManager().config["DEFAULT"][ - constants.CONFIG_KEY_BACKGROUND - ] = f"{(listen, port, process.pid)}" - ConfigManager().write_config() - - # NOTE: os.exit(0) doesn't work. - os._exit(0) - - with logging_lock: - if logging_flag: - log.append(line) - - stdout_thread = threading.Thread(target=msg_hook, args=(process.stdout,)) - stderr_thread = threading.Thread(target=msg_hook, args=(process.stderr,)) - - stdout_thread.start() - stderr_thread.start() - - process.wait() - - return log - - -def background_launch(extra): - config_background = ConfigManager().background - if config_background is not None and utils.is_running(config_background[2]): - console.print( - "[bold red]ComfyUI is already running in background.\nYou cannot start more than one background service.[/bold red]\n" - ) - raise typer.Exit(code=1) - - port = 8188 - listen = "127.0.0.1" - - if extra is not None: - for i in range(len(extra) - 1): - if extra[i] == "--port": - port = extra[i + 1] - if listen[i] == "--listen": - listen = extra[i + 1] - - if len(extra) > 0: - extra = ["--"] + extra - else: - extra = [] - - if check_comfy_server_running(port): - console.print( - f"[bold red]The {port} port is already in use. A new ComfyUI server cannot be launched.\n[bold red]\n" - ) - raise typer.Exit(code=1) - - cmd = [ - "comfy", - f"--workspace={os.path.abspath(os.getcwd())}", - "launch", - ] + extra - - loop = asyncio.get_event_loop() - log = loop.run_until_complete(launch_and_monitor(cmd, listen, port)) - - if log is not None: - console.print( - Panel( - "".join(log), - title="[bold red]Error log during ComfyUI execution[/bold red]", - border_style="bright_red", - ) - ) - - console.print("\n[bold red]Execution error: failed to launch ComfyUI[/bold red]\n") - # NOTE: os.exit(0) doesn't work - os._exit(1) - - -def launch_comfyui(extra): - reboot_path = None - - new_env = os.environ.copy() - - session_path = os.path.join( - ConfigManager().get_config_path(), "tmp", str(uuid.uuid4()) - ) - new_env["__COMFY_CLI_SESSION__"] = session_path - new_env["PYTHONENCODING"] = "utf-8" - - # To minimize the possibility of leaving residue in the tmp directory, use files instead of directories. - reboot_path = os.path.join(session_path + ".reboot") - - extra = extra if extra is not None else [] - - process = None - - if "COMFY_CLI_BACKGROUND" not in os.environ: - # If not running in background mode, there's no need to use popen. This can prevent the issue of linefeeds occurring with tqdm. - while True: - res = subprocess.run( - [sys.executable, "main.py"] + extra, env=new_env, check=False - ) - - if reboot_path is None: - print("[bold red]ComfyUI is not installed.[/bold red]\n") - exit(res) - - if not os.path.exists(reboot_path): - exit(res) - - os.remove(reboot_path) - else: - # If running in background mode without using a popen, broken pipe errors may occur when flushing stdout/stderr. - def redirector_stderr(): - while True: - if process is not None: - print(process.stderr.readline(), end="") - - def redirector_stdout(): - while True: - if process is not None: - print(process.stdout.readline(), end="") - - threading.Thread(target=redirector_stderr).start() - threading.Thread(target=redirector_stdout).start() - - try: - while True: - if sys.platform == "win32": - process = subprocess.Popen( - [sys.executable, "main.py"] + extra, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=new_env, - encoding="utf-8", - shell=True, # win32 only - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # win32 only - ) - else: - process = subprocess.Popen( - [sys.executable, "main.py"] + extra, - text=True, - env=new_env, - encoding="utf-8", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - process.wait() - - if reboot_path is None: - print("[bold red]ComfyUI is not installed.[/bold red]\n") - os._exit(process.pid) - - if not os.path.exists(reboot_path): - os._exit(process.pid) - - os.remove(reboot_path) - except KeyboardInterrupt: - if process is not None: - os._exit(1) - - @app.command(help="Stop background ComfyUI") @tracking.track_command() def stop(): @@ -672,36 +456,7 @@ def launch( ] = False, extra: List[str] = typer.Argument(None), ): - check_for_updates() - resolved_workspace = workspace_manager.workspace_path - - if not resolved_workspace: - print( - "\nComfyUI is not available.\nTo install ComfyUI, you can run:\n\n\tcomfy install\n\n", - file=sys.stderr, - ) - raise typer.Exit(code=1) - - if ( - extra is None or len(extra) == 0 - ) and workspace_manager.workspace_type == WorkspaceType.DEFAULT: - launch_extras = workspace_manager.config_manager.config["DEFAULT"].get( - constants.CONFIG_KEY_DEFAULT_LAUNCH_EXTRAS, "" - ) - - if launch_extras != "": - extra = launch_extras.split(" ") - - print(f"\nLaunching ComfyUI from: {resolved_workspace}\n") - - # Update the recent workspace - workspace_manager.set_recent_workspace(resolved_workspace) - - os.chdir(resolved_workspace) - if background: - background_launch(extra) - else: - launch_comfyui(extra) + launch_command(background, extra) @app.command("set-default", help="Set default ComfyUI path") diff --git a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py index 79bf5a81..b4581661 100644 --- a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +++ b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py @@ -1,12 +1,14 @@ from __future__ import annotations import json +import os from pathlib import Path from typing import Literal, NamedTuple import typer from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli +from comfy_cli.command.launch import launch as launch_command bisect_app = typer.Typer() @@ -26,6 +28,9 @@ class BisectState(NamedTuple): # The active set of nodes to test active: list[str] + # The arguments to pass to the ComfyUI launch command + launch_args: list[str] = [] + def good(self) -> BisectState: """The active set of nodes is good, narrowing down the potential problem area.""" if self.status != "running": @@ -35,12 +40,17 @@ def good(self) -> BisectState: if len(new_range) == 1: return BisectState( - status="resolved", all=self.all, range=new_range, active=[] + status="resolved", + all=self.all, + launch_args=self.launch_args, + range=new_range, + active=[], ) return BisectState( status="running", all=self.all, + launch_args=self.launch_args, range=new_range, active=new_range[len(new_range) // 2 :], ) @@ -54,12 +64,17 @@ def bad(self) -> BisectState: if len(new_range) == 1: return BisectState( - status="resolved", all=self.all, range=new_range, active=[] + status="resolved", + all=self.all, + launch_args=self.launch_args, + range=new_range, + active=[], ) return BisectState( status="running", all=self.all, + launch_args=self.launch_args, range=new_range, active=new_range[len(new_range) // 2 :], ) @@ -72,9 +87,13 @@ def save(self, state_file=None): def reset(self): BisectState( - "idle", all=self.all, range=self.all, active=self.all + "idle", + all=self.all, + launch_args=self.launch_args, + range=self.all, + active=self.all, ).set_custom_node_enabled_states() - return BisectState("idle", self.all, self.all, self.all) + return BisectState("idle", self.all, self.all, self.all, self.launch_args) @classmethod def load(cls, state_file=None) -> BisectState: @@ -106,13 +125,17 @@ def __str__(self): @bisect_app.command( - help="Start a new bisect session with a comma-separated list of nodes." + help="Start a new bisect session with a comma-separated list of nodes. ?[-- ]" ) -def start(): +def start(extra: list[str] = typer.Argument(None)): """Start a new bisect session with a comma-separated list of nodes. The initial state is bad with all custom nodes enabled, good with all custom nodes disabled.""" + if BisectState.load().status != "idle": + typer.echo("A bisect session is already running.") + raise typer.Exit() + cm_output: str | None = execute_cm_cli(["simple-show", "enabled"]) if cm_output is None: typer.echo("Failed to fetch the list of nodes.") @@ -128,6 +151,7 @@ def start(): all=nodes_list, range=nodes_list, active=nodes_list, + launch_args=extra or [], ) state.save() @@ -153,6 +177,7 @@ def good(): else: new_state.save() typer.echo(new_state) + launch_command(background=False, extra=state.launch_args) @bisect_app.command( @@ -173,12 +198,14 @@ def bad(): else: new_state.save() typer.echo(new_state) + launch_command(background=False, extra=state.launch_args) @bisect_app.command(help="Reset the current bisect session.") def reset(): if default_state_file.exists(): BisectState.load().reset() + os.unlink(default_state_file) typer.echo("Bisect session reset.") else: typer.echo("No bisect session to reset.") diff --git a/comfy_cli/command/launch.py b/comfy_cli/command/launch.py new file mode 100644 index 00000000..7b374102 --- /dev/null +++ b/comfy_cli/command/launch.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import asyncio +import os +import subprocess +import sys +import threading +import uuid + +import typer +from rich import print +from rich.console import Console +from rich.panel import Panel + +from comfy_cli import constants, utils +from comfy_cli.config_manager import ConfigManager +from comfy_cli.env_checker import check_comfy_server_running +from comfy_cli.update import check_for_updates +from comfy_cli.workspace_manager import WorkspaceManager, WorkspaceType + +workspace_manager = WorkspaceManager() +console = Console() + + +def launch_comfyui(extra): + reboot_path = None + + new_env = os.environ.copy() + + session_path = os.path.join( + ConfigManager().get_config_path(), "tmp", str(uuid.uuid4()) + ) + new_env["__COMFY_CLI_SESSION__"] = session_path + new_env["PYTHONENCODING"] = "utf-8" + + # To minimize the possibility of leaving residue in the tmp directory, use files instead of directories. + reboot_path = os.path.join(session_path + ".reboot") + + extra = extra if extra is not None else [] + + process = None + + if "COMFY_CLI_BACKGROUND" not in os.environ: + # If not running in background mode, there's no need to use popen. This can prevent the issue of linefeeds occurring with tqdm. + while True: + res = subprocess.run( + [sys.executable, "main.py"] + extra, env=new_env, check=False + ) + + if reboot_path is None: + print("[bold red]ComfyUI is not installed.[/bold red]\n") + exit(res) + + if not os.path.exists(reboot_path): + exit(res) + + os.remove(reboot_path) + else: + # If running in background mode without using a popen, broken pipe errors may occur when flushing stdout/stderr. + def redirector_stderr(): + while True: + if process is not None: + print(process.stderr.readline(), end="") + + def redirector_stdout(): + while True: + if process is not None: + print(process.stdout.readline(), end="") + + threading.Thread(target=redirector_stderr).start() + threading.Thread(target=redirector_stdout).start() + + try: + while True: + if sys.platform == "win32": + process = subprocess.Popen( + [sys.executable, "main.py"] + extra, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=new_env, + encoding="utf-8", + shell=True, # win32 only + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # win32 only + ) + else: + process = subprocess.Popen( + [sys.executable, "main.py"] + extra, + text=True, + env=new_env, + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + process.wait() + + if reboot_path is None: + print("[bold red]ComfyUI is not installed.[/bold red]\n") + os._exit(process.pid) + + if not os.path.exists(reboot_path): + os._exit(process.pid) + + os.remove(reboot_path) + except KeyboardInterrupt: + if process is not None: + os._exit(1) + + +def launch( + background: bool = False, + extra: list[str] | None = None, +): + check_for_updates() + resolved_workspace = workspace_manager.workspace_path + + if not resolved_workspace: + print( + "\nComfyUI is not available.\nTo install ComfyUI, you can run:\n\n\tcomfy install\n\n", + file=sys.stderr, + ) + raise typer.Exit(code=1) + + if ( + extra is None or len(extra) == 0 + ) and workspace_manager.workspace_type == WorkspaceType.DEFAULT: + launch_extras = workspace_manager.config_manager.config["DEFAULT"].get( + constants.CONFIG_KEY_DEFAULT_LAUNCH_EXTRAS, "" + ) + + if launch_extras != "": + extra = launch_extras.split(" ") + + print(f"\nLaunching ComfyUI from: {resolved_workspace}\n") + + # Update the recent workspace + workspace_manager.set_recent_workspace(resolved_workspace) + + os.chdir(resolved_workspace) + if background: + background_launch(extra) + else: + launch_comfyui(extra) + + +def background_launch(extra): + config_background = ConfigManager().background + if config_background is not None and utils.is_running(config_background[2]): + console.print( + "[bold red]ComfyUI is already running in background.\nYou cannot start more than one background service.[/bold red]\n" + ) + raise typer.Exit(code=1) + + port = 8188 + listen = "127.0.0.1" + + if extra is not None: + for i in range(len(extra) - 1): + if extra[i] == "--port": + port = extra[i + 1] + if listen[i] == "--listen": + listen = extra[i + 1] + + if len(extra) > 0: + extra = ["--"] + extra + else: + extra = [] + + if check_comfy_server_running(port): + console.print( + f"[bold red]The {port} port is already in use. A new ComfyUI server cannot be launched.\n[bold red]\n" + ) + raise typer.Exit(code=1) + + cmd = [ + "comfy", + f"--workspace={os.path.abspath(os.getcwd())}", + "launch", + ] + extra + + loop = asyncio.get_event_loop() + log = loop.run_until_complete(launch_and_monitor(cmd, listen, port)) + + if log is not None: + console.print( + Panel( + "".join(log), + title="[bold red]Error log during ComfyUI execution[/bold red]", + border_style="bright_red", + ) + ) + + console.print("\n[bold red]Execution error: failed to launch ComfyUI[/bold red]\n") + # NOTE: os.exit(0) doesn't work + os._exit(1) + + +async def launch_and_monitor(cmd, listen, port): + """ + Monitor the process during the background launch. + + If a success message is captured, exit; + otherwise, return the log in case of failure. + """ + logging_flag = False + log = [] + logging_lock = threading.Lock() + + # NOTE: To prevent encoding error on Windows platform + env = dict(os.environ, PYTHONIOENCODING="utf-8") + env["COMFY_CLI_BACKGROUND"] = "true" + + if sys.platform == "win32": + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + encoding="utf-8", + shell=True, # win32 only + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # win32 only + ) + else: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + encoding="utf-8", + ) + + def msg_hook(stream): + nonlocal log + nonlocal logging_flag + + while True: + line = stream.readline() + if "Launching ComfyUI from:" in line: + logging_flag = True + elif "To see the GUI go to:" in line: + print( + f"[bold yellow]ComfyUI is successfully launched in the background.[/bold yellow]\nTo see the GUI go to: http://{listen}:{port}" + ) + ConfigManager().config["DEFAULT"][ + constants.CONFIG_KEY_BACKGROUND + ] = f"{(listen, port, process.pid)}" + ConfigManager().write_config() + + # NOTE: os.exit(0) doesn't work. + os._exit(0) + + with logging_lock: + if logging_flag: + log.append(line) + + stdout_thread = threading.Thread(target=msg_hook, args=(process.stdout,)) + stderr_thread = threading.Thread(target=msg_hook, args=(process.stderr,)) + + stdout_thread.start() + stderr_thread.start() + + process.wait() + + return log From a801541a8257cd7732f8edea99c340377e7dd6af Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 16:03:56 -0400 Subject: [PATCH 6/7] Allow pin nodes --- .../custom_nodes/bisect_custom_nodes.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py index b4581661..5b7a9554 100644 --- a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +++ b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py @@ -6,6 +6,7 @@ from typing import Literal, NamedTuple import typer +from typing_extensions import Annotated from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli from comfy_cli.command.launch import launch as launch_command @@ -125,9 +126,16 @@ def __str__(self): @bisect_app.command( - help="Start a new bisect session with a comma-separated list of nodes. ?[-- ]" + help="Start a new bisect session with a comma-separated list of nodes. " + + "?[--pinned-nodes PINNED_NODES]" + + "?[-- ]" ) -def start(extra: list[str] = typer.Argument(None)): +def start( + pinned_nodes: Annotated[ + str, typer.Option(help="Pinned nodes always enable during the bisect") + ] = "", + extra: list[str] = typer.Argument(None), +): """Start a new bisect session with a comma-separated list of nodes. The initial state is bad with all custom nodes enabled, good with all custom nodes disabled.""" @@ -136,6 +144,8 @@ def start(extra: list[str] = typer.Argument(None)): typer.echo("A bisect session is already running.") raise typer.Exit() + pinned_nodes = {s.strip() for s in pinned_nodes.split(",") if s} + cm_output: str | None = execute_cm_cli(["simple-show", "enabled"]) if cm_output is None: typer.echo("Failed to fetch the list of nodes.") @@ -144,7 +154,7 @@ def start(extra: list[str] = typer.Argument(None)): nodes_list = [ line.strip() for line in cm_output.strip().split("\n") - if not line.startswith("FETCH DATA") + if not line.startswith("FETCH DATA") and line.strip() not in pinned_nodes ] state = BisectState( status="running", @@ -156,6 +166,9 @@ def start(extra: list[str] = typer.Argument(None)): state.save() typer.echo(f"Bisect session started.\n{state}") + if pinned_nodes: + typer.echo(f"Pinned nodes: {', '.join(pinned_nodes)}") + bad() From d0086755da4fc4e165542f32c7c86d29abd7802f Mon Sep 17 00:00:00 2001 From: huchenlei Date: Wed, 17 Jul 2024 16:19:46 -0400 Subject: [PATCH 7/7] nit --- README.md | 2 +- comfy_cli/command/custom_nodes/bisect_custom_nodes.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e1636db..32e53663 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ comfy node [show|simple-show] [installed|enabled|not-installed|disabled|all|snap The bisect tool can help you pinpoint the custom node that causes the issue. -- `comfy node bisect start`: Start a new bisect session with a comma-separated list of nodes. +- `comfy node bisect start`: Start a new bisect session with optional ComfyUI launch args. - `comfy node bisect good`: Mark the current active set as bad, indicating the problem is within the test set - `comfy node bisect bad`: Mark the current active set as bad, indicating the problem is within the test set. - `comfy node bisect reset`: Reset the current bisect session. diff --git a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py index 5b7a9554..c6982477 100644 --- a/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +++ b/comfy_cli/command/custom_nodes/bisect_custom_nodes.py @@ -126,7 +126,7 @@ def __str__(self): @bisect_app.command( - help="Start a new bisect session with a comma-separated list of nodes. " + help="Start a new bisect session with optionally pinned nodes to always enable, and optional ComfyUI launch args." + "?[--pinned-nodes PINNED_NODES]" + "?[-- ]" ) @@ -136,9 +136,8 @@ def start( ] = "", extra: list[str] = typer.Argument(None), ): - """Start a new bisect session with a comma-separated list of nodes. - The initial state is bad with all custom nodes enabled, good with - all custom nodes disabled.""" + """Start a new bisect session. The initial state is bad with all custom nodes + enabled, good with all custom nodes disabled.""" if BisectState.load().status != "idle": typer.echo("A bisect session is already running.")