From 0f71f8e0bf751b8cc309d5325f6e255347767aca Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Wed, 1 May 2024 19:53:09 +0900 Subject: [PATCH 1/7] feat: comfy install --snapshot=<.yaml path> --- README.md | 3 ++- comfy_cli/cmdline.py | 18 +++++++++++++++--- comfy_cli/command/install.py | 15 +++++++++++++++ comfy_cli/env_checker.py | 4 ++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3f562fb..6ddd5dc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ will simply update the comfy.yaml file to reflect the local setup * `comfy install --skip-manager`: Install ComfyUI without ComfyUI-Manager. * `comfy --workspace= install`: Install ComfyUI into `/ComfyUI`. * For `comfy install`, if no path specification like `--workspace, --recent, or --here` is provided, it will be implicitly installed in `/comfy`. - + * **(WIP)** `comfy install --snapshot=`: Install ComfyUI and whole environments from snapshot. + * **(WIP)** Currently, only the installation of ComfyUI and custom nodes is being applied. ### Specifying execution path diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index d6d3443..06fca0f 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -92,10 +92,17 @@ def install( commit: Annotated[ str, typer.Option(help="Specify commit hash for ComfyUI") + ] = None, + snapshot: Annotated[ + str, + typer.Option(help="Specify path to comfy-lock.yaml ") ] = None ): checker = EnvChecker() + if snapshot is not None: + snapshot = os.path.abspath(snapshot) + # In the case of installation, since it involves installing in a non-existent path, get_workspace_path is not used. specified_workspace = ctx.obj.get(constants.CONTEXT_KEY_WORKSPACE) use_recent = ctx.obj.get(constants.CONTEXT_KEY_RECENT) @@ -126,6 +133,11 @@ def install( torch_mode = 'amd' install_inner.execute(url, manager_url, workspace_path, restore, skip_manager, torch_mode, commit=commit) + + if snapshot is not None: + checker.check() + install_inner.apply_snapshot(ctx, checker, snapshot) + workspace_manager.set_recent_workspace(workspace_path) @@ -316,6 +328,6 @@ def feedback(): print("Thank you for your feedback!") - 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="Manager ComfyUI-Manager.") +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="Manager ComfyUI-Manager.") diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index bb00fc5..8446bcd 100644 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -2,6 +2,8 @@ import subprocess from rich import print import sys +import typer +from comfy_cli.command import custom_nodes def install_comfyui_dependencies(repo_dir, torch_mode): @@ -81,3 +83,16 @@ def execute(url: str, manager_url: str, comfy_workspace: str, restore: bool, ski os.chdir(repo_dir) print("") + + +def apply_snapshot(ctx: typer.Context, checker, filepath): + if not os.path.exists(filepath): + print(f"[bold red]File not found: {filepath}[/bold red]") + raise typer.Exit(code=1) + + if checker.get_comfyui_manager_path() is not None and os.path.exists(checker.get_comfyui_manager_path()): + print(f"[bold red]If ComfyUI-Manager is not installed, the snapshot feature cannot be used.[/bold red]") + raise typer.Exit(code=1) + + custom_nodes.command.restore_snapshot(ctx, filepath) + diff --git a/comfy_cli/env_checker.py b/comfy_cli/env_checker.py index 007f4b5..d04a073 100644 --- a/comfy_cli/env_checker.py +++ b/comfy_cli/env_checker.py @@ -98,7 +98,7 @@ def get_comfyui_manager_path(self): return None # To check more robustly, verify up to the `.git` path. - manager_path = os.path.join(self.comfy_repo.working_dir, 'custom_nodes', 'ComfyUI-Manager') + manager_path = os.path.join(self.comfy_repo.working_dir, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager') return manager_path def is_comfyui_manager_installed(self): @@ -106,7 +106,7 @@ def is_comfyui_manager_installed(self): return False # To check more robustly, verify up to the `.git` path. - manager_git_path = os.path.join(self.comfy_repo.working_dir, 'custom_nodes', 'ComfyUI-Manager', '.git') + manager_git_path = os.path.join(self.comfy_repo.working_dir, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager', '.git') return os.path.exists(manager_git_path) def is_isolated_env(self): From ca288178e1a031935fabfac209b98c4e923def31 Mon Sep 17 00:00:00 2001 From: Yoland Y <4950057+yoland68@users.noreply.github.com> Date: Wed, 1 May 2024 15:08:21 -0700 Subject: [PATCH 2/7] Format all files --- .pre-commit-config.yaml | 6 + comfy_cli/__main__.py | 2 +- comfy_cli/cmdline.py | 481 ++++++++++++---------- comfy_cli/command/__init__.py | 2 +- comfy_cli/command/custom_nodes/command.py | 310 ++++++++++---- comfy_cli/command/install.py | 151 ++++--- comfy_cli/command/models/models.py | 218 +++++----- comfy_cli/command/run.py | 3 +- comfy_cli/config_manager.py | 161 ++++---- comfy_cli/constants.py | 52 +-- comfy_cli/env_checker.py | 34 +- comfy_cli/logging.py | 24 +- comfy_cli/meta_data.py | 26 +- comfy_cli/tracking.py | 71 ++-- comfy_cli/ui.py | 121 +++--- comfy_cli/utils.py | 4 +- comfy_cli/workspace_manager.py | 271 ++++++------ 17 files changed, 1120 insertions(+), 817 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..29fca09 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3.9 \ No newline at end of file diff --git a/comfy_cli/__main__.py b/comfy_cli/__main__.py index f1b9e62..10e280d 100644 --- a/comfy_cli/__main__.py +++ b/comfy_cli/__main__.py @@ -1,4 +1,4 @@ from comfy_cli.cmdline import main -if __name__ == '__main__': # pragma: nocover +if __name__ == "__main__": # pragma: nocover main() diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 06fca0f..9ee45da 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -30,123 +30,131 @@ def main(): - app() + app() @app.callback(invoke_without_command=True) def entry( - ctx: typer.Context, - workspace: Optional[str] = typer.Option(default=None, show_default=False, help="Path to ComfyUI workspace"), - recent: Optional[bool] = typer.Option(default=False, show_default=False, is_flag=True, - help="Execute from recent path"), - here: Optional[bool] = typer.Option(default=False, show_default=False, is_flag=True, - help="Execute from current path"), + ctx: typer.Context, + workspace: Optional[str] = typer.Option( + default=None, show_default=False, help="Path to ComfyUI workspace" + ), + recent: Optional[bool] = typer.Option( + default=False, show_default=False, is_flag=True, help="Execute from recent path" + ), + here: Optional[bool] = typer.Option( + default=False, + show_default=False, + is_flag=True, + help="Execute from current path", + ), ): - if ctx.invoked_subcommand is None: - print(ctx.get_help()) - ctx.exit() + if ctx.invoked_subcommand is None: + print(ctx.get_help()) + ctx.exit() - ctx.ensure_object(dict) # Ensure that ctx.obj exists and is a dict - workspace_manager.update_context(ctx, workspace, recent, here) - init() + ctx.ensure_object(dict) # Ensure that ctx.obj exists and is a dict + workspace_manager.update_context(ctx, workspace, recent, here) + init() def init(): - # TODO(yoland): after this - metadata_manager = MetadataManager() - start_time = time.time() - metadata_manager.scan_dir() - end_time = time.time() - logging.setup_logging() - tracking.prompt_tracking_consent() + # TODO(yoland): after this + metadata_manager = MetadataManager() + start_time = time.time() + metadata_manager.scan_dir() + end_time = time.time() + logging.setup_logging() + tracking.prompt_tracking_consent() - print(f"scan_dir took {end_time - start_time:.2f} seconds to run") + print(f"scan_dir took {end_time - start_time:.2f} seconds to run") @app.command(help="Download and install ComfyUI and ComfyUI-Manager") @tracking.track_command() def install( - ctx: typer.Context, - url: Annotated[ - str, - typer.Option(show_default=False) - ] = constants.COMFY_GITHUB_URL, - manager_url: Annotated[ - str, - typer.Option(show_default=False) - ] = constants.COMFY_MANAGER_GITHUB_URL, - restore: Annotated[ - bool, - lambda: typer.Option( - default=False, - help="Restore dependencies for installed ComfyUI if not installed") - ] = False, - skip_manager: Annotated[ - bool, - typer.Option(help="Skip installing the manager component") - ] = False, - amd: Annotated[ - bool, - typer.Option(help="Install for AMD gpu") - ] = False, - commit: Annotated[ - str, - typer.Option(help="Specify commit hash for ComfyUI") - ] = None, - snapshot: Annotated[ - str, - typer.Option(help="Specify path to comfy-lock.yaml ") - ] = None + ctx: typer.Context, + url: Annotated[str, typer.Option(show_default=False)] = constants.COMFY_GITHUB_URL, + manager_url: Annotated[ + str, typer.Option(show_default=False) + ] = constants.COMFY_MANAGER_GITHUB_URL, + restore: Annotated[ + bool, + lambda: typer.Option( + default=False, + help="Restore dependencies for installed ComfyUI if not installed", + ), + ] = False, + skip_manager: Annotated[ + bool, typer.Option(help="Skip installing the manager component") + ] = False, + amd: Annotated[bool, typer.Option(help="Install for AMD gpu")] = False, + commit: Annotated[str, typer.Option(help="Specify commit hash for ComfyUI")] = None, + snapshot: Annotated[ + str, typer.Option(help="Specify path to comfy-lock.yaml ") + ] = None, ): - checker = EnvChecker() - - if snapshot is not None: - snapshot = os.path.abspath(snapshot) - - # In the case of installation, since it involves installing in a non-existent path, get_workspace_path is not used. - specified_workspace = ctx.obj.get(constants.CONTEXT_KEY_WORKSPACE) - use_recent = ctx.obj.get(constants.CONTEXT_KEY_RECENT) - use_here = ctx.obj.get(constants.CONTEXT_KEY_HERE) - - if specified_workspace: - workspace_path = specified_workspace - elif use_recent: - workspace_path = workspace_manager.config_manager.get(constants.CONFIG_KEY_RECENT_WORKSPACE) - elif use_here: - workspace_path = os.getcwd() - else: # For installation, if not explicitly specified, it will only install in the default path. - workspace_path = os.path.expanduser('~/comfy') - - if checker.python_version.major < 3: - print( - "[bold red]Python version 3.6 or higher is required to run ComfyUI.[/bold red]" - ) - print( - f"You are currently using Python version {env_checker.format_python_version(checker.python_version)}." + checker = EnvChecker() + + if snapshot is not None: + snapshot = os.path.abspath(snapshot) + + # In the case of installation, since it involves installing in a non-existent path, get_workspace_path is not used. + specified_workspace = ctx.obj.get(constants.CONTEXT_KEY_WORKSPACE) + use_recent = ctx.obj.get(constants.CONTEXT_KEY_RECENT) + use_here = ctx.obj.get(constants.CONTEXT_KEY_HERE) + + if specified_workspace: + workspace_path = specified_workspace + elif use_recent: + workspace_path = workspace_manager.config_manager.get( + constants.CONFIG_KEY_RECENT_WORKSPACE + ) + elif use_here: + workspace_path = os.getcwd() + else: # For installation, if not explicitly specified, it will only install in the default path. + workspace_path = os.path.expanduser("~/comfy") + + if checker.python_version.major < 3: + print( + "[bold red]Python version 3.6 or higher is required to run ComfyUI.[/bold red]" + ) + print( + f"You are currently using Python version {env_checker.format_python_version(checker.python_version)}." + ) + if checker.currently_in_comfy_repo: + console = Console() + # TODO: warn user that you are teh + + torch_mode = None + if amd: + torch_mode = "amd" + + install_inner.execute( + url, + manager_url, + workspace_path, + restore, + skip_manager, + torch_mode, + commit=commit, ) - if checker.currently_in_comfy_repo: - console = Console() - # TODO: warn user that you are teh - torch_mode = None - if amd: - torch_mode = 'amd' + if snapshot is not None: + checker.check() + install_inner.apply_snapshot(ctx, checker, snapshot) - install_inner.execute(url, manager_url, workspace_path, restore, skip_manager, torch_mode, commit=commit) - - if snapshot is not None: - checker.check() - install_inner.apply_snapshot(ctx, checker, snapshot) - - workspace_manager.set_recent_workspace(workspace_path) + workspace_manager.set_recent_workspace(workspace_path) def update(self): - _env_checker = EnvChecker() - print(f"Updating ComfyUI in {self.workspace}...") - os.chdir(self.workspace) - subprocess.run(["git", "pull"], check=True) - subprocess.run([sys.executable, '-m', "pip", "install", "-r", "requirements.txt"], check=True) + _env_checker = EnvChecker() + print(f"Updating ComfyUI in {self.workspace}...") + os.chdir(self.workspace) + subprocess.run(["git", "pull"], check=True) + subprocess.run( + [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True + ) # @app.command(help="Run workflow file") @@ -158,175 +166,222 @@ def update(self): def validate_comfyui(_env_checker): - if _env_checker.comfy_repo is None: - print(f"[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]") - raise typer.Exit(code=1) - - -def launch_comfyui(_env_checker, _config_manager, extra, background=False): - validate_comfyui(_env_checker) - - if background: - if _config_manager.background is not None and utils.is_running(_config_manager.background[2]): - print(f"[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 check_comfy_server_running(port): - print(f"[bold red]The {port} port is already in use. A new ComfyUI server cannot be launched.\n[bold red]\n") + if _env_checker.comfy_repo is None: + print( + f"[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]" + ) raise typer.Exit(code=1) - if len(extra) > 0: - extra = ['--'] + extra - else: - extra = [] - - cmd = ['comfy', f'--workspace={os.path.join(os.getcwd(), "..")}', 'launch'] + extra - - process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - print(f"[bold yellow]Run ComfyUI in the background.[/bold yellow] ({listen}:{port})") - _config_manager.config['DEFAULT'][constants.CONFIG_KEY_BACKGROUND] = f"{(listen, port, process.pid)}" - _config_manager.write_config() - return - - env_path = _env_checker.get_isolated_env() - reboot_path = None - - new_env = os.environ.copy() - - if env_path is not None: - session_path = os.path.join(_config_manager.get_config_path(), 'tmp', str(uuid.uuid4())) - new_env['__COMFY_CLI_SESSION__'] = session_path - - # 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 [] - - while True: - subprocess.run([sys.executable, "main.py"] + extra, env=new_env, check=False) - - if not os.path.exists(reboot_path): - return - os.remove(reboot_path) +def launch_comfyui(_env_checker, _config_manager, extra, background=False): + validate_comfyui(_env_checker) + + if background: + if _config_manager.background is not None and utils.is_running( + _config_manager.background[2] + ): + print( + f"[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 check_comfy_server_running(port): + 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) + + if len(extra) > 0: + extra = ["--"] + extra + else: + extra = [] + + cmd = [ + "comfy", + f'--workspace={os.path.join(os.getcwd(), "..")}', + "launch", + ] + extra + + process = subprocess.Popen( + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + print( + f"[bold yellow]Run ComfyUI in the background.[/bold yellow] ({listen}:{port})" + ) + _config_manager.config["DEFAULT"][ + constants.CONFIG_KEY_BACKGROUND + ] = f"{(listen, port, process.pid)}" + _config_manager.write_config() + return + + env_path = _env_checker.get_isolated_env() + reboot_path = None + + new_env = os.environ.copy() + + if env_path is not None: + session_path = os.path.join( + _config_manager.get_config_path(), "tmp", str(uuid.uuid4()) + ) + new_env["__COMFY_CLI_SESSION__"] = session_path + + # 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 [] + + while True: + subprocess.run([sys.executable, "main.py"] + extra, env=new_env, check=False) + + if not os.path.exists(reboot_path): + return + + os.remove(reboot_path) @app.command(help="Stop background ComfyUI") def stop(): - _config_manager = ConfigManager() + _config_manager = ConfigManager() - if constants.CONFIG_KEY_BACKGROUND not in _config_manager.config['DEFAULT']: - print(f"[bold red]No ComfyUI is running in the background.[/bold red]\n") - raise typer.Exit(code=1) + if constants.CONFIG_KEY_BACKGROUND not in _config_manager.config["DEFAULT"]: + print(f"[bold red]No ComfyUI is running in the background.[/bold red]\n") + raise typer.Exit(code=1) - bg_info = _config_manager.background - is_killed = utils.kill_all(bg_info[2]) + bg_info = _config_manager.background + is_killed = utils.kill_all(bg_info[2]) - print(f"[bold yellow]Background ComfyUI is stopped.[/bold yellow] ({bg_info[0]}:{bg_info[1]})") + print( + f"[bold yellow]Background ComfyUI is stopped.[/bold yellow] ({bg_info[0]}:{bg_info[1]})" + ) - _config_manager.remove_background() + _config_manager.remove_background() @app.command(help="Launch ComfyUI: ?[--background] ?[-- ]") @tracking.track_command() def launch( - ctx: typer.Context, - background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False, - extra: List[str] = typer.Argument(None)): - _env_checker = EnvChecker() - _config_manager = ConfigManager() - - resolved_workspace = workspace_manager.get_workspace_path(ctx) - 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) + ctx: typer.Context, + background: Annotated[ + bool, typer.Option(help="Launch ComfyUI in background") + ] = False, + extra: List[str] = typer.Argument(None), +): + _env_checker = EnvChecker() + _config_manager = ConfigManager() + + resolved_workspace = workspace_manager.get_workspace_path(ctx) + 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) - print(f"\nLaunching ComfyUI from: {resolved_workspace}\n") + print(f"\nLaunching ComfyUI from: {resolved_workspace}\n") - os.chdir(resolved_workspace + '/ComfyUI') - _env_checker.check() # update environment checks + os.chdir(resolved_workspace + "/ComfyUI") + _env_checker.check() # update environment checks - # Update the recent workspace - workspace_manager.set_recent_workspace(resolved_workspace) + # Update the recent workspace + workspace_manager.set_recent_workspace(resolved_workspace) - launch_comfyui(_env_checker, _config_manager, extra, background=background) + launch_comfyui(_env_checker, _config_manager, extra, background=background) @app.command("set-default", help="Set default workspace") @tracking.track_command() def set_default(workspace_path: str): - workspace_path = os.path.expanduser(workspace_path) - comfy_path = os.path.join(workspace_path, 'ComfyUI') - if not os.path.exists(comfy_path): - print(f"Invalid workspace path: {workspace_path}\nThe workspace path must contain 'ComfyUI'.") - raise typer.Exit(code=1) + workspace_path = os.path.expanduser(workspace_path) + comfy_path = os.path.join(workspace_path, "ComfyUI") + if not os.path.exists(comfy_path): + print( + f"Invalid workspace path: {workspace_path}\nThe workspace path must contain 'ComfyUI'." + ) + raise typer.Exit(code=1) - workspace_manager.set_default_workspace(workspace_path) + workspace_manager.set_default_workspace(workspace_path) -@app.command(help='Show which ComfyUI is selected.') +@app.command(help="Show which ComfyUI is selected.") @tracking.track_command() def which(ctx: typer.Context): - comfy_path = workspace_manager.get_workspace_path(ctx) - if not os.path.exists(comfy_path) or not os.path.exists(os.path.join(comfy_path, 'ComfyUI')): - print(f"ComfyUI not found, please run 'comfy install', run 'comfy' in a ComfyUI directory, or specify the workspace path with '--workspace'.") - raise typer.Exit(code=1) + comfy_path = workspace_manager.get_workspace_path(ctx) + if not os.path.exists(comfy_path) or not os.path.exists( + os.path.join(comfy_path, "ComfyUI") + ): + print( + f"ComfyUI not found, please run 'comfy install', run 'comfy' in a ComfyUI directory, or specify the workspace path with '--workspace'." + ) + raise typer.Exit(code=1) - print(f"Target ComfyUI path: {comfy_path}") + print(f"Target ComfyUI path: {comfy_path}") @app.command(help="Print out current environment variables.") @tracking.track_command() def env(): - _env_checker = EnvChecker() - _env_checker.print() + _env_checker = EnvChecker() + _env_checker.print() @app.command(hidden=True) @tracking.track_command() def nodes(): - print("\n[bold red] No such command, did you mean 'comfy node' instead?[/bold red]\n") + print( + "\n[bold red] No such command, did you mean 'comfy node' instead?[/bold red]\n" + ) @app.command(hidden=True) @tracking.track_command() def models(): - print("\n[bold red] No such command, did you mean 'comfy model' instead?[/bold red]\n") + print( + "\n[bold red] No such command, did you mean 'comfy model' instead?[/bold red]\n" + ) @app.command(help="Provide feedback on the Comfy CLI tool.") @tracking.track_command() def feedback(): - print("Feedback Collection for Comfy CLI Tool\n") - - # General Satisfaction - general_satisfaction_score = ui.prompt_select( - question="On a scale of 1 to 5, how satisfied are you with the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)", - choices=["1", "2", "3", "4", "5"]) - tracking.track_event("feedback_general_satisfaction", {"score": general_satisfaction_score}) - - # Usability and User Experience - usability_satisfaction_score = ui.prompt_select( - question="On a scale of 1 to 5, how satisfied are you with the usability and user experience of the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)", - choices=["1", "2", "3", "4", "5"]) - tracking.track_event("feedback_usability_satisfaction", {"score": usability_satisfaction_score}) - - # Additional Feature-Specific Feedback - if questionary.confirm("Do you want to provide additional feature-specific feedback on our GitHub page?").ask(): - tracking.track_event("feedback_additional") - webbrowser.open("https://github.com/Comfy-Org/comfy-cli/issues/new/choose") - - print("Thank you for your feedback!") + print("Feedback Collection for Comfy CLI Tool\n") + + # General Satisfaction + general_satisfaction_score = ui.prompt_select( + question="On a scale of 1 to 5, how satisfied are you with the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)", + choices=["1", "2", "3", "4", "5"], + ) + tracking.track_event( + "feedback_general_satisfaction", {"score": general_satisfaction_score} + ) + + # Usability and User Experience + usability_satisfaction_score = ui.prompt_select( + question="On a scale of 1 to 5, how satisfied are you with the usability and user experience of the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)", + choices=["1", "2", "3", "4", "5"], + ) + tracking.track_event( + "feedback_usability_satisfaction", {"score": usability_satisfaction_score} + ) + + # Additional Feature-Specific Feedback + if questionary.confirm( + "Do you want to provide additional feature-specific feedback on our GitHub page?" + ).ask(): + tracking.track_event("feedback_additional") + webbrowser.open("https://github.com/Comfy-Org/comfy-cli/issues/new/choose") + + print("Thank you for your feedback!") + app.add_typer(models_command.app, name="model", help="Manage models.") app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.") diff --git a/comfy_cli/command/__init__.py b/comfy_cli/command/__init__.py index 3bbf1e4..59b5731 100644 --- a/comfy_cli/command/__init__.py +++ b/comfy_cli/command/__init__.py @@ -1,2 +1,2 @@ from . import custom_nodes -from . import install \ No newline at end of file +from . import install diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 83eeadb..7cfd9dd 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -20,28 +20,32 @@ def execute_cm_cli(ctx: typer.Context, args, channel=None, mode=None): _config_manager = ConfigManager() workspace_path = workspace_manager.get_workspace_path(ctx) - comfyui_path = os.path.join(workspace_path, 'ComfyUI') + comfyui_path = os.path.join(workspace_path, "ComfyUI") if not os.path.exists(comfyui_path): print(f"\nComfyUI not found: {comfyui_path}\n", file=sys.stderr) raise typer.Exit(code=1) - cm_cli_path = os.path.join(comfyui_path, 'custom_nodes', 'ComfyUI-Manager', 'cm-cli.py') + cm_cli_path = os.path.join( + comfyui_path, "custom_nodes", "ComfyUI-Manager", "cm-cli.py" + ) if not os.path.exists(cm_cli_path): print(f"\nComfyUI-Manager not found: {cm_cli_path}\n", file=sys.stderr) raise typer.Exit(code=1) cmd = [sys.executable, cm_cli_path] + args if channel is not None: - cmd += ['--channel', channel] + cmd += ["--channel", channel] if mode is not None: - cmd += ['--mode', channel] + cmd += ["--mode", channel] 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'] = comfyui_path + 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"] = comfyui_path print(f"Execute from: {comfyui_path}") @@ -53,209 +57,345 @@ def validate_comfyui_manager(_env_checker): manager_path = _env_checker.get_comfyui_manager_path() if manager_path is None: - print(f"[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]") + print( + f"[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]" + ) raise typer.Exit(code=1) elif not os.path.exists(manager_path): - print(f"[bold red]If ComfyUI-Manager is not installed, this feature cannot be used.[/bold red] \\[{manager_path}]") + print( + f"[bold red]If ComfyUI-Manager is not installed, this feature cannot be used.[/bold red] \\[{manager_path}]" + ) raise typer.Exit(code=1) - elif not os.path.exists(os.path.join(manager_path, '.git')): - print(f"[bold red]The ComfyUI-Manager installation is invalid. This feature cannot be used.[/bold red] \\[{manager_path}]") + elif not os.path.exists(os.path.join(manager_path, ".git")): + print( + f"[bold red]The ComfyUI-Manager installation is invalid. This feature cannot be used.[/bold red] \\[{manager_path}]" + ) raise typer.Exit(code=1) -@app.command('save-snapshot', help="Save a snapshot of the current ComfyUI environment") +@app.command("save-snapshot", help="Save a snapshot of the current ComfyUI environment") @tracking.track_command("node") def save_snapshot( - ctx: typer.Context, - output: Annotated[str, '--output', typer.Option(show_default=False, help="Specify the output file path. (.json/.yaml)")] = None + ctx: typer.Context, + output: Annotated[ + str, + "--output", + typer.Option( + show_default=False, help="Specify the output file path. (.json/.yaml)" + ), + ] = None, ): if output is None: - execute_cm_cli(ctx, ['save-snapshot']) + execute_cm_cli(ctx, ["save-snapshot"]) else: output = os.path.abspath(output) # to compensate chdir - execute_cm_cli(ctx, ['save-snapshot', '--output', output]) + execute_cm_cli(ctx, ["save-snapshot", "--output", output]) -@app.command('restore-snapshot') +@app.command("restore-snapshot") @tracking.track_command("node") def restore_snapshot(ctx: typer.Context, path: str): path = os.path.abspath(path) - execute_cm_cli(ctx, ['restore-snapshot', path]) + execute_cm_cli(ctx, ["restore-snapshot", path]) -@app.command('restore-dependencies') +@app.command("restore-dependencies") @tracking.track_command("node") def restore_dependencies(ctx: typer.Context): - execute_cm_cli(ctx, ['restore-dependencies']) + execute_cm_cli(ctx, ["restore-dependencies"]) -@manager_app.command('disable-gui') +@manager_app.command("disable-gui") @tracking.track_command("node") def disable_gui(ctx: typer.Context): - execute_cm_cli(ctx, ['cli-only-mode', 'enable']) + execute_cm_cli(ctx, ["cli-only-mode", "enable"]) -@manager_app.command('enable-gui') +@manager_app.command("enable-gui") @tracking.track_command("node") def enable_gui(ctx: typer.Context): - execute_cm_cli(ctx, ['cli-only-mode', 'disable']) + execute_cm_cli(ctx, ["cli-only-mode", "disable"]) @manager_app.command() @tracking.track_command("node") def clear(ctx: typer.Context, path: str): path = os.path.abspath(path) - execute_cm_cli(ctx, ['clear', path]) + execute_cm_cli(ctx, ["clear", path]) @app.command() @tracking.track_command("node") -def show(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def show( + ctx: typer.Context, + args: List[str] = typer.Argument( + ..., + help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]", + ), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): - valid_commands = ["installed", "enabled", "not-installed", "disabled", "all", "snapshot", "snapshot-list"] + valid_commands = [ + "installed", + "enabled", + "not-installed", + "disabled", + "all", + "snapshot", + "snapshot-list", + ] if not args or len(args) > 1 or args[0] not in valid_commands: typer.echo(f"Invalid command: `show {' '.join(args)}`", err=True) raise typer.Exit(code=1) valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['show'] + args, channel, mode) + execute_cm_cli(ctx, ["show"] + args, channel, mode) -@app.command('simple-show') +@app.command("simple-show") @tracking.track_command("node") -def simple_show(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def simple_show( + ctx: typer.Context, + args: List[str] = typer.Argument( + ..., + help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]", + ), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): - valid_commands = ["installed", "enabled", "not-installed", "disabled", "all", "snapshot", "snapshot-list"] + valid_commands = [ + "installed", + "enabled", + "not-installed", + "disabled", + "all", + "snapshot", + "snapshot-list", + ] if not args or len(args) > 1 or args[0] not in valid_commands: typer.echo(f"Invalid command: `show {' '.join(args)}`", err=True) raise typer.Exit(code=1) valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['simple-show'] + args, channel, mode) + execute_cm_cli(ctx, ["simple-show"] + args, channel, mode) # install, reinstall, uninstall @app.command() @tracking.track_command("node") -def install(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="install custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): - if 'all' in args: +def install( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="install custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): + if "all" in args: typer.echo(f"Invalid command: {mode}. `install all` is not allowed", err=True) raise typer.Exit(code=1) valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['install'] + args, channel, mode) + execute_cm_cli(ctx, ["install"] + args, channel, mode) @app.command() @tracking.track_command("node") -def reinstall(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="reinstall custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): - if 'all' in args: +def reinstall( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="reinstall custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): + if "all" in args: typer.echo(f"Invalid command: {mode}. `reinstall all` is not allowed", err=True) raise typer.Exit(code=1) valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['reinstall'] + args, channel, mode) + execute_cm_cli(ctx, ["reinstall"] + args, channel, mode) @app.command() @tracking.track_command("node") -def uninstall(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="uninstall custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): - if 'all' in args: +def uninstall( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="uninstall custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): + if "all" in args: typer.echo(f"Invalid command: {mode}. `uninstall all` is not allowed", err=True) raise typer.Exit(code=1) valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['uninstall'] + args, channel, mode) + execute_cm_cli(ctx, ["uninstall"] + args, channel, mode) # `update, disable, enable, fix` allows `all` param + @app.command() @tracking.track_command("node") -def update(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="update custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def update( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="update custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['update'] + args, channel, mode) + execute_cm_cli(ctx, ["update"] + args, channel, mode) @app.command() @tracking.track_command("node") -def disable(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="disable custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False,help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def disable( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="disable custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['disable'] + args, channel, mode) + execute_cm_cli(ctx, ["disable"] + args, channel, mode) @app.command() @tracking.track_command("node") -def enable(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="enable custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def enable( + ctx: typer.Context, + args: List[str] = typer.Argument(..., help="enable custom nodes"), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['enable'] + args, channel, mode) + execute_cm_cli(ctx, ["enable"] + args, channel, mode) @app.command() @tracking.track_command("node") -def fix(ctx: typer.Context, - args: List[str] = typer.Argument(..., help="fix dependencies for specified custom nodes"), - channel: Annotated[str, '--channel', typer.Option(show_default=False, help="Specify the operation mode")] = None, - mode: Annotated[str, '--mode', typer.Option(show_default=False, help="[remote|local|cache]")] = None): +def fix( + ctx: typer.Context, + args: List[str] = typer.Argument( + ..., help="fix dependencies for specified custom nodes" + ), + channel: Annotated[ + str, + "--channel", + typer.Option(show_default=False, help="Specify the operation mode"), + ] = None, + mode: Annotated[ + str, "--mode", typer.Option(show_default=False, help="[remote|local|cache]") + ] = None, +): valid_modes = ["remote", "local", "cache"] if mode and mode.lower() not in valid_modes: - typer.echo(f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", err=True) + typer.echo( + f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.", + err=True, + ) raise typer.Exit(code=1) - execute_cm_cli(ctx, ['fix'] + args, channel, mode) + execute_cm_cli(ctx, ["fix"] + args, channel, mode) diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index 8446bcd..e382875 100644 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -7,92 +7,109 @@ def install_comfyui_dependencies(repo_dir, torch_mode): - os.chdir(repo_dir) + os.chdir(repo_dir) - # install torch - if torch_mode == 'amd': - pip_url = ['--extra-index-url', 'https://download.pytorch.org/whl/rocm6.0'] - else: - pip_url = ['--extra-index-url', 'https://download.pytorch.org/whl/cu121'] - subprocess.run([sys.executable, '-m', "pip", "install", "torch", "torchvision", "torchaudio"] + pip_url) + # install torch + if torch_mode == "amd": + pip_url = ["--extra-index-url", "https://download.pytorch.org/whl/rocm6.0"] + else: + pip_url = ["--extra-index-url", "https://download.pytorch.org/whl/cu121"] + subprocess.run( + [sys.executable, "-m", "pip", "install", "torch", "torchvision", "torchaudio"] + + pip_url + ) - # install other requirements - subprocess.run([sys.executable, '-m', "pip", "install", "-r", "requirements.txt"]) + # install other requirements + subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) # install requirements for manager def install_manager_dependencies(repo_dir): - os.chdir(os.path.join(repo_dir, 'custom_nodes', 'ComfyUI-Manager')) - subprocess.run([sys.executable, '-m', "pip", "install", "-r", "requirements.txt"]) - - -def execute(url: str, manager_url: str, comfy_workspace: str, restore: bool, skip_manager: bool, torch_mode=None, commit=None, - *args, **kwargs): - print(f"Installing from {url}") - - # install ComfyUI - working_dir = os.path.expanduser(comfy_workspace) - repo_dir = os.path.join(working_dir, os.path.basename(url).replace(".git", "")) - repo_dir = os.path.abspath(repo_dir) - - if os.path.exists(os.path.join(repo_dir, '.git')): - if restore or commit is not None: - if commit is not None: - os.chdir(repo_dir) - subprocess.run(["git", "checkout", commit]) - - install_comfyui_dependencies(repo_dir, torch_mode) - else: - print( - "ComfyUI is installed already. Skipping installation.\nIf you want to restore dependencies, add the '--restore' option.") - else: - print("\nInstalling ComfyUI..") - os.makedirs(working_dir, exist_ok=True) - + os.chdir(os.path.join(repo_dir, "custom_nodes", "ComfyUI-Manager")) + subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) + + +def execute( + url: str, + manager_url: str, + comfy_workspace: str, + restore: bool, + skip_manager: bool, + torch_mode=None, + commit=None, + *args, + **kwargs, +): + print(f"Installing from {url}") + + # install ComfyUI + working_dir = os.path.expanduser(comfy_workspace) repo_dir = os.path.join(working_dir, os.path.basename(url).replace(".git", "")) repo_dir = os.path.abspath(repo_dir) - subprocess.run(["git", "clone", url, repo_dir]) - # checkout specified commit - if commit is not None: - os.chdir(repo_dir) - subprocess.run(["git", "checkout", commit]) + if os.path.exists(os.path.join(repo_dir, ".git")): + if restore or commit is not None: + if commit is not None: + os.chdir(repo_dir) + subprocess.run(["git", "checkout", commit]) + + install_comfyui_dependencies(repo_dir, torch_mode) + else: + print( + "ComfyUI is installed already. Skipping installation.\nIf you want to restore dependencies, add the '--restore' option." + ) + else: + print("\nInstalling ComfyUI..") + os.makedirs(working_dir, exist_ok=True) + + repo_dir = os.path.join(working_dir, os.path.basename(url).replace(".git", "")) + repo_dir = os.path.abspath(repo_dir) + subprocess.run(["git", "clone", url, repo_dir]) - install_comfyui_dependencies(repo_dir, torch_mode) + # checkout specified commit + if commit is not None: + os.chdir(repo_dir) + subprocess.run(["git", "checkout", commit]) - print("") + install_comfyui_dependencies(repo_dir, torch_mode) - # install ComfyUI-Manager - if skip_manager: - print("Skipping installation of ComfyUI-Manager. (by --skip-manager)") - else: - manager_repo_dir = os.path.join(repo_dir, 'custom_nodes', 'ComfyUI-Manager') + print("") - if os.path.exists(manager_repo_dir): - if restore: - install_manager_dependencies(repo_dir) - else: - print( - f"Directory {manager_repo_dir} already exists. Skipping installation of ComfyUI-Manager.\nIf you want to restore dependencies, add the '--restore' option.") + # install ComfyUI-Manager + if skip_manager: + print("Skipping installation of ComfyUI-Manager. (by --skip-manager)") else: - print("\nInstalling ComfyUI-Manager..") + manager_repo_dir = os.path.join(repo_dir, "custom_nodes", "ComfyUI-Manager") - subprocess.run(["git", "clone", manager_url, manager_repo_dir]) - install_manager_dependencies(repo_dir) + if os.path.exists(manager_repo_dir): + if restore: + install_manager_dependencies(repo_dir) + else: + print( + f"Directory {manager_repo_dir} already exists. Skipping installation of ComfyUI-Manager.\nIf you want to restore dependencies, add the '--restore' option." + ) + else: + print("\nInstalling ComfyUI-Manager..") - os.chdir(repo_dir) + subprocess.run(["git", "clone", manager_url, manager_repo_dir]) + install_manager_dependencies(repo_dir) - print("") + os.chdir(repo_dir) + print("") -def apply_snapshot(ctx: typer.Context, checker, filepath): - if not os.path.exists(filepath): - print(f"[bold red]File not found: {filepath}[/bold red]") - raise typer.Exit(code=1) - if checker.get_comfyui_manager_path() is not None and os.path.exists(checker.get_comfyui_manager_path()): - print(f"[bold red]If ComfyUI-Manager is not installed, the snapshot feature cannot be used.[/bold red]") - raise typer.Exit(code=1) +def apply_snapshot(ctx: typer.Context, checker, filepath): + if not os.path.exists(filepath): + print(f"[bold red]File not found: {filepath}[/bold red]") + raise typer.Exit(code=1) - custom_nodes.command.restore_snapshot(ctx, filepath) + if checker.get_comfyui_manager_path() is not None and os.path.exists( + checker.get_comfyui_manager_path() + ): + print( + f"[bold red]If ComfyUI-Manager is not installed, the snapshot feature cannot be used.[/bold red]" + ) + raise typer.Exit(code=1) + custom_nodes.command.restore_snapshot(ctx, filepath) diff --git a/comfy_cli/command/models/models.py b/comfy_cli/command/models/models.py index 6db89f9..dc81a58 100644 --- a/comfy_cli/command/models/models.py +++ b/comfy_cli/command/models/models.py @@ -14,137 +14,149 @@ def get_workspace(ctx: typer.Context) -> pathlib.Path: - workspace_path = workspace_manager.get_workspace_comfy_path(ctx) - return pathlib.Path(workspace_path) + workspace_path = workspace_manager.get_workspace_comfy_path(ctx) + return pathlib.Path(workspace_path) @app.command() @tracking.track_command("model") def download( - ctx: typer.Context, - url: Annotated[ - str, - typer.Option( - help="The URL from which to download the model", - show_default=False)], - relative_path: Annotated[ - Optional[str], - typer.Option( - help="The relative path from the current workspace to install the model.", - show_default=True)] = DEFAULT_COMFY_MODEL_PATH + ctx: typer.Context, + url: Annotated[ + str, + typer.Option( + help="The URL from which to download the model", show_default=False + ), + ], + relative_path: Annotated[ + Optional[str], + typer.Option( + help="The relative path from the current workspace to install the model.", + show_default=True, + ), + ] = DEFAULT_COMFY_MODEL_PATH, ): - """Download a model to a specified relative path if it is not already downloaded.""" - # Convert relative path to absolute path based on the current working directory - local_filename = url.split("/")[-1] - local_filepath = get_workspace(ctx) / relative_path / local_filename + """Download a model to a specified relative path if it is not already downloaded.""" + # Convert relative path to absolute path based on the current working directory + local_filename = url.split("/")[-1] + local_filepath = get_workspace(ctx) / relative_path / local_filename - # Check if the file already exists - if local_filepath.exists(): - typer.echo(f"File already exists: {local_filepath}") - return + # Check if the file already exists + if local_filepath.exists(): + typer.echo(f"File already exists: {local_filepath}") + return - # File does not exist, proceed with download - typer.echo(f"Start downloading URL: {url} into {local_filepath}") - download_file(url, local_filepath) + # File does not exist, proceed with download + typer.echo(f"Start downloading URL: {url} into {local_filepath}") + download_file(url, local_filepath) @app.command() @tracking.track_command("model") def remove( - ctx: typer.Context, - relative_path: str = typer.Option( - DEFAULT_COMFY_MODEL_PATH, - help="The relative path from the current workspace where the models are stored.", - show_default=True - ), - model_names: Optional[List[str]] = typer.Option( - None, - help="List of model filenames to delete, separated by spaces", - show_default=False - ) + ctx: typer.Context, + relative_path: str = typer.Option( + DEFAULT_COMFY_MODEL_PATH, + help="The relative path from the current workspace where the models are stored.", + show_default=True, + ), + model_names: Optional[List[str]] = typer.Option( + None, + help="List of model filenames to delete, separated by spaces", + show_default=False, + ), ): - """Remove one or more downloaded models, either by specifying them directly or through an interactive selection.""" - model_dir = get_workspace(ctx) / relative_path - available_models = list_models(model_dir) - - if not available_models: - typer.echo("No models found to remove.") - return - - to_delete = [] - # Scenario #1: User provided model names to delete - if model_names: - # Validate and filter models to delete based on provided names - missing_models = [] - for name in model_names: - model_path = model_dir / name - if model_path.exists(): - to_delete.append(model_path) - else: - missing_models.append(name) - - if missing_models: - typer.echo("The following models were not found and cannot be removed: " + ", ".join(missing_models)) - if not to_delete: - return # Exit if no valid models were found - - return - - # Scenario #2: User did not provide model names, prompt for selection - else: - selections = ui.prompt_multi_select("Select models to delete:", [model.name for model in available_models]) - if not selections: - typer.echo("No models selected for deletion.") - return - to_delete = [model_dir / selection for selection in selections] - - # Confirm deletion - if to_delete and ui.prompt_confirm_action("Are you sure you want to delete the selected files?"): - for model_path in to_delete: - model_path.unlink() - typer.echo(f"Deleted: {model_path}") - else: - typer.echo("Deletion canceled.") + """Remove one or more downloaded models, either by specifying them directly or through an interactive selection.""" + model_dir = get_workspace(ctx) / relative_path + available_models = list_models(model_dir) + + if not available_models: + typer.echo("No models found to remove.") + return + + to_delete = [] + # Scenario #1: User provided model names to delete + if model_names: + # Validate and filter models to delete based on provided names + missing_models = [] + for name in model_names: + model_path = model_dir / name + if model_path.exists(): + to_delete.append(model_path) + else: + missing_models.append(name) + + if missing_models: + typer.echo( + "The following models were not found and cannot be removed: " + + ", ".join(missing_models) + ) + if not to_delete: + return # Exit if no valid models were found + + return + + # Scenario #2: User did not provide model names, prompt for selection + else: + selections = ui.prompt_multi_select( + "Select models to delete:", [model.name for model in available_models] + ) + if not selections: + typer.echo("No models selected for deletion.") + return + to_delete = [model_dir / selection for selection in selections] + + # Confirm deletion + if to_delete and ui.prompt_confirm_action( + "Are you sure you want to delete the selected files?" + ): + for model_path in to_delete: + model_path.unlink() + typer.echo(f"Deleted: {model_path}") + else: + typer.echo("Deletion canceled.") @app.command() @tracking.track_command("model") def list( - ctx: typer.Context, - relative_path: str = typer.Option( - DEFAULT_COMFY_MODEL_PATH, - help="The relative path from the current workspace where the models are stored.", - show_default=True - ) + ctx: typer.Context, + relative_path: str = typer.Option( + DEFAULT_COMFY_MODEL_PATH, + help="The relative path from the current workspace where the models are stored.", + show_default=True, + ), ): - """Display a list of all models currently downloaded in a table format.""" - model_dir = get_workspace(ctx) / relative_path - models = list_models(model_dir) + """Display a list of all models currently downloaded in a table format.""" + model_dir = get_workspace(ctx) / relative_path + models = list_models(model_dir) - if not models: - typer.echo("No models found.") - return + if not models: + typer.echo("No models found.") + return - # Prepare data for table display - data = [(model.name, f"{model.stat().st_size // 1024} KB") for model in models] - column_names = ["Model Name", "Size"] - ui.display_table(data, column_names) + # Prepare data for table display + data = [(model.name, f"{model.stat().st_size // 1024} KB") for model in models] + column_names = ["Model Name", "Size"] + ui.display_table(data, column_names) def download_file(url: str, local_filepath: pathlib.Path): - """Helper function to download a file.""" + """Helper function to download a file.""" - import httpx + import httpx - local_filepath.parent.mkdir(parents=True, exist_ok=True) # Ensure the directory exists + local_filepath.parent.mkdir( + parents=True, exist_ok=True + ) # Ensure the directory exists - with httpx.stream("GET", url, follow_redirects=True) as response: - total = int(response.headers["Content-Length"]) - with open(local_filepath, "wb") as f: - for data in ui.show_progress(response.iter_bytes(), total): - f.write(data) + with httpx.stream("GET", url, follow_redirects=True) as response: + total = int(response.headers["Content-Length"]) + with open(local_filepath, "wb") as f: + for data in ui.show_progress(response.iter_bytes(), total): + f.write(data) def list_models(path: pathlib.Path) -> list: - """List all models in the specified directory.""" - return [file for file in path.iterdir() if file.is_file()] + """List all models in the specified directory.""" + return [file for file in path.iterdir() if file.is_file()] diff --git a/comfy_cli/command/run.py b/comfy_cli/command/run.py index 9b8d08f..80ccac6 100644 --- a/comfy_cli/command/run.py +++ b/comfy_cli/command/run.py @@ -1,3 +1,2 @@ - def execute(workflow_name: str): - print(f"Executing workflow: {workflow_name}") \ No newline at end of file + print(f"Executing workflow: {workflow_name}") diff --git a/comfy_cli/config_manager.py b/comfy_cli/config_manager.py index 4ce1a02..40a54c7 100644 --- a/comfy_cli/config_manager.py +++ b/comfy_cli/config_manager.py @@ -8,79 +8,88 @@ @singleton class ConfigManager(object): - def __init__(self): - self.config = configparser.ConfigParser() - self.background = None - self.load() - - @staticmethod - def get_config_path(): - return constants.DEFAULT_CONFIG[get_os()] - - def get_config_file_path(self): - return os.path.join(self.get_config_path(), 'config.ini') - - def write_config(self): - config_file_path = os.path.join(self.get_config_path(), 'config.ini') - dir_path = os.path.dirname(config_file_path) - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - with open(config_file_path, 'w') as configfile: - self.config.write(configfile) - - def set(self, key, value): - """ - Set a key-value pair in the config file. - """ - self.config['DEFAULT'][key] = value - self.write_config() # Write changes to file immediately - - def get(self, key): - """ - Get a value from the config file. Returns None if the key does not exist. - """ - return self.config['DEFAULT'].get(key, None) # Returns None if the key does not exist - - def load(self): - config_file_path = self.get_config_file_path() - if os.path.exists(config_file_path): - self.config = configparser.ConfigParser() - self.config.read(config_file_path) - - # TODO: We need a policy for clearing the tmp directory. - tmp_path = os.path.join(self.get_config_path(), 'tmp') - if not os.path.exists(tmp_path): - os.makedirs(tmp_path) - - if 'background' in self.config['DEFAULT']: - bg_info = self.config['DEFAULT']['background'].strip('()').split(',') - bg_info = [item.strip().strip("'") for item in bg_info] - self.background = bg_info[0], int(bg_info[1]), int(bg_info[2]) - - if not is_running(self.background[2]): - self.remove_background() - - - def fill_print_env(self, table): - table.add_row("Config Path", self.get_config_file_path()) - if self.config.has_option('DEFAULT', 'default_workspace'): - table.add_row("Default ComfyUI workspace", self.config['DEFAULT']['default_workspace']) - else: - table.add_row("Default ComfyUI workspace", "No default ComfyUI workspace") - - if self.config.has_option('DEFAULT', constants.CONFIG_KEY_RECENT_WORKSPACE): - table.add_row("Recent ComfyUI workspace", self.config['DEFAULT'][constants.CONFIG_KEY_RECENT_WORKSPACE]) - else: - table.add_row("Recent ComfyUI workspace", "No recent run") - - if self.config.has_option('DEFAULT', 'background'): - bg_info = self.background - table.add_row("Background ComfyUI", f'http://{bg_info[0]}:{bg_info[1]} (pid={bg_info[2]})') - else: - table.add_row("Background ComfyUI", "[bold red]No[/bold red]") - - def remove_background(self): - del self.config['DEFAULT']['background'] - self.write_config() - self.background = None + def __init__(self): + self.config = configparser.ConfigParser() + self.background = None + self.load() + + @staticmethod + def get_config_path(): + return constants.DEFAULT_CONFIG[get_os()] + + def get_config_file_path(self): + return os.path.join(self.get_config_path(), "config.ini") + + def write_config(self): + config_file_path = os.path.join(self.get_config_path(), "config.ini") + dir_path = os.path.dirname(config_file_path) + if not os.path.exists(dir_path): + os.mkdir(dir_path) + + with open(config_file_path, "w") as configfile: + self.config.write(configfile) + + def set(self, key, value): + """ + Set a key-value pair in the config file. + """ + self.config["DEFAULT"][key] = value + self.write_config() # Write changes to file immediately + + def get(self, key): + """ + Get a value from the config file. Returns None if the key does not exist. + """ + return self.config["DEFAULT"].get( + key, None + ) # Returns None if the key does not exist + + def load(self): + config_file_path = self.get_config_file_path() + if os.path.exists(config_file_path): + self.config = configparser.ConfigParser() + self.config.read(config_file_path) + + # TODO: We need a policy for clearing the tmp directory. + tmp_path = os.path.join(self.get_config_path(), "tmp") + if not os.path.exists(tmp_path): + os.makedirs(tmp_path) + + if "background" in self.config["DEFAULT"]: + bg_info = self.config["DEFAULT"]["background"].strip("()").split(",") + bg_info = [item.strip().strip("'") for item in bg_info] + self.background = bg_info[0], int(bg_info[1]), int(bg_info[2]) + + if not is_running(self.background[2]): + self.remove_background() + + def fill_print_env(self, table): + table.add_row("Config Path", self.get_config_file_path()) + if self.config.has_option("DEFAULT", "default_workspace"): + table.add_row( + "Default ComfyUI workspace", self.config["DEFAULT"]["default_workspace"] + ) + else: + table.add_row("Default ComfyUI workspace", "No default ComfyUI workspace") + + if self.config.has_option("DEFAULT", constants.CONFIG_KEY_RECENT_WORKSPACE): + table.add_row( + "Recent ComfyUI workspace", + self.config["DEFAULT"][constants.CONFIG_KEY_RECENT_WORKSPACE], + ) + else: + table.add_row("Recent ComfyUI workspace", "No recent run") + + if self.config.has_option("DEFAULT", "background"): + bg_info = self.background + table.add_row( + "Background ComfyUI", + f"http://{bg_info[0]}:{bg_info[1]} (pid={bg_info[2]})", + ) + else: + table.add_row("Background ComfyUI", "[bold red]No[/bold red]") + + def remove_background(self): + del self.config["DEFAULT"]["background"] + self.write_config() + self.background = None diff --git a/comfy_cli/constants.py b/comfy_cli/constants.py index 41f2591..b238c22 100644 --- a/comfy_cli/constants.py +++ b/comfy_cli/constants.py @@ -3,46 +3,48 @@ class OS(Enum): - WINDOWS = 'windows' - MACOS = 'macos' - LINUX = 'linux' + WINDOWS = "windows" + MACOS = "macos" + LINUX = "linux" -COMFY_GITHUB_URL = 'https://github.com/comfyanonymous/ComfyUI' -COMFY_MANAGER_GITHUB_URL = 'https://github.com/ltdrdata/ComfyUI-Manager' +COMFY_GITHUB_URL = "https://github.com/comfyanonymous/ComfyUI" +COMFY_MANAGER_GITHUB_URL = "https://github.com/ltdrdata/ComfyUI-Manager" DEFAULT_COMFY_MODEL_PATH = "models/checkpoints" DEFAULT_COMFY_WORKSPACE = { - OS.WINDOWS: os.path.join(os.path.expanduser('~'), 'Documents', 'ComfyUI'), - OS.MACOS: os.path.join(os.path.expanduser('~'), 'Documents', 'ComfyUI'), - OS.LINUX: os.path.join(os.path.expanduser('~'), 'ComfyUI'), + OS.WINDOWS: os.path.join(os.path.expanduser("~"), "Documents", "ComfyUI"), + OS.MACOS: os.path.join(os.path.expanduser("~"), "Documents", "ComfyUI"), + OS.LINUX: os.path.join(os.path.expanduser("~"), "ComfyUI"), } DEFAULT_CONFIG = { - OS.WINDOWS: os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'comfy-cli'), - OS.MACOS: os.path.join(os.path.expanduser('~'), 'Library', 'Application Support', 'comfy-cli'), - OS.LINUX: os.path.join(os.path.expanduser('~'), '.config', "comfy-cli"), + OS.WINDOWS: os.path.join(os.path.expanduser("~"), "AppData", "Local", "comfy-cli"), + OS.MACOS: os.path.join( + os.path.expanduser("~"), "Library", "Application Support", "comfy-cli" + ), + OS.LINUX: os.path.join(os.path.expanduser("~"), ".config", "comfy-cli"), } -CONTEXT_KEY_WORKSPACE = 'workspace' -CONTEXT_KEY_RECENT = 'recent' -CONTEXT_KEY_HERE = 'here' +CONTEXT_KEY_WORKSPACE = "workspace" +CONTEXT_KEY_RECENT = "recent" +CONTEXT_KEY_HERE = "here" -CONFIG_KEY_DEFAULT_WORKSPACE = 'default_workspace' -CONFIG_KEY_RECENT_WORKSPACE = 'recent_workspace' -CONFIG_KEY_ENABLE_TRACKING = 'enable_tracking' -CONFIG_KEY_BACKGROUND = 'background' +CONFIG_KEY_DEFAULT_WORKSPACE = "default_workspace" +CONFIG_KEY_RECENT_WORKSPACE = "recent_workspace" +CONFIG_KEY_ENABLE_TRACKING = "enable_tracking" +CONFIG_KEY_BACKGROUND = "background" # TODO: figure out a better way to check if this is a comfy repo COMFY_ORIGIN_URL_CHOICES = [ - "git@github.com:comfyanonymous/ComfyUI.git", - "git@github.com:drip-art/comfy.git", - "https://github.com/comfyanonymous/ComfyUI.git", - "https://github.com/drip-art/ComfyUI.git", - "https://github.com/comfyanonymous/ComfyUI", - "https://github.com/drip-art/ComfyUI", + "git@github.com:comfyanonymous/ComfyUI.git", + "git@github.com:drip-art/comfy.git", + "https://github.com/comfyanonymous/ComfyUI.git", + "https://github.com/drip-art/ComfyUI.git", + "https://github.com/comfyanonymous/ComfyUI", + "https://github.com/drip-art/ComfyUI", ] # Referencing supported pt extension from ComfyUI # https://github.com/comfyanonymous/ComfyUI/blob/a88b0ebc2d2f933c94e42aa689c42e836eedaf3c/folder_paths.py#L5 -SUPPORTED_PT_EXTENSIONS = ('.ckpt', '.pt', '.bin', '.pth', '.safetensors') +SUPPORTED_PT_EXTENSIONS = (".ckpt", ".pt", ".bin", ".pth", ".safetensors") diff --git a/comfy_cli/env_checker.py b/comfy_cli/env_checker.py index d04a073..34ca72f 100644 --- a/comfy_cli/env_checker.py +++ b/comfy_cli/env_checker.py @@ -31,8 +31,12 @@ def format_python_version(version_info): str: The formatted Python version string. """ if version_info.major == 3 and version_info.minor > 8: - return "{}.{}.{}".format(version_info.major, version_info.minor, version_info.micro) - return "[bold red]{}.{}.{}[/bold red]".format(version_info.major, version_info.minor, version_info.micro) + return "{}.{}.{}".format( + version_info.major, version_info.minor, version_info.micro + ) + return "[bold red]{}.{}.{}[/bold red]".format( + version_info.major, version_info.minor, version_info.micro + ) def check_comfy_server_running(port=8188): @@ -98,7 +102,9 @@ def get_comfyui_manager_path(self): return None # To check more robustly, verify up to the `.git` path. - manager_path = os.path.join(self.comfy_repo.working_dir, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager') + manager_path = os.path.join( + self.comfy_repo.working_dir, "ComfyUI", "custom_nodes", "ComfyUI-Manager" + ) return manager_path def is_comfyui_manager_installed(self): @@ -106,7 +112,13 @@ def is_comfyui_manager_installed(self): return False # To check more robustly, verify up to the `.git` path. - manager_git_path = os.path.join(self.comfy_repo.working_dir, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager', '.git') + manager_git_path = os.path.join( + self.comfy_repo.working_dir, + "ComfyUI", + "custom_nodes", + "ComfyUI-Manager", + ".git", + ) return os.path.exists(manager_git_path) def is_isolated_env(self): @@ -123,9 +135,7 @@ def get_isolated_env(self): def check(self): self.virtualenv_path = ( - os.environ.get("VIRTUAL_ENV") - if os.environ.get("VIRTUAL_ENV") - else None + os.environ.get("VIRTUAL_ENV") if os.environ.get("VIRTUAL_ENV") else None ) self.conda_env = ( os.environ.get("CONDA_DEFAULT_ENV") @@ -146,13 +156,19 @@ def print(self): table = Table(":laptop_computer: Environment", "Value") table.add_row("Python Version", format_python_version(sys.version_info)) table.add_row("Python Executable", sys.executable) - table.add_row("Virtualenv Path", self.virtualenv_path if self.virtualenv_path else "Not Used") + table.add_row( + "Virtualenv Path", + self.virtualenv_path if self.virtualenv_path else "Not Used", + ) table.add_row("Conda Env", self.conda_env if self.conda_env else "Not Used") ConfigManager().fill_print_env(table) if check_comfy_server_running(): - table.add_row("Comfy Server Running", "[bold green]Yes[/bold green]\nhttp://localhost:8188") + table.add_row( + "Comfy Server Running", + "[bold green]Yes[/bold green]\nhttp://localhost:8188", + ) else: table.add_row("Comfy Server Running", "[bold red]No[/bold red]") diff --git a/comfy_cli/logging.py b/comfy_cli/logging.py index fe12534..6ee98ee 100644 --- a/comfy_cli/logging.py +++ b/comfy_cli/logging.py @@ -10,26 +10,28 @@ def setup_logging(): - # TODO: consider supporting different ways of outputting logs - # Note: by default, the log level is set to INFO - log_level = os.getenv("LOG_LEVEL", "WARN").upper() - logging.basicConfig(level=log_level, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') + # TODO: consider supporting different ways of outputting logs + # Note: by default, the log level is set to INFO + log_level = os.getenv("LOG_LEVEL", "WARN").upper() + logging.basicConfig( + level=log_level, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) def debug(message): - logging.debug(message) + logging.debug(message) def info(message): - logging.info(message) + logging.info(message) def warning(message): - logging.warning(message) + logging.warning(message) def error(message): - logging.error(message) - # TODO: consider tracking errors to Mixpanel as well. + logging.error(message) + # TODO: consider tracking errors to Mixpanel as well. diff --git a/comfy_cli/meta_data.py b/comfy_cli/meta_data.py index 8ad50d1..f2977ce 100644 --- a/comfy_cli/meta_data.py +++ b/comfy_cli/meta_data.py @@ -16,6 +16,7 @@ class ModelPath: path: str + @dataclass class Model: name: Optional[str] = None @@ -24,6 +25,7 @@ class Model: hash: Optional[str] = None type: Optional[str] = None + @dataclass class Basics: name: Optional[str] = None @@ -32,7 +34,7 @@ class Basics: @dataclass class CustomNode: - #Todo: Add custom node fields for comfy-lock.yaml + # Todo: Add custom node fields for comfy-lock.yaml pass @@ -70,19 +72,23 @@ def load_yaml(file_path: str) -> ComfyLockYAMLStruct: def save_yaml(file_path: str, metadata: ComfyLockYAMLStruct): data = { - "basics": {"name": metadata.basics.name, "updated_at": metadata.basics.updated_at.isoformat()}, + "basics": { + "name": metadata.basics.name, + "updated_at": metadata.basics.updated_at.isoformat(), + }, "models": [ { "model": m.name, "url": m.url, "paths": [{"path": p.path} for p in m.paths], "hash": m.hash, - "type": m.type - } for m in metadata.models + "type": m.type, + } + for m in metadata.models ], - "custom_nodes": [] + "custom_nodes": [], } - with open(file_path, 'w', encoding='utf-8') as file: + with open(file_path, "w", encoding="utf-8") as file: yaml.safe_dump(data, file, default_flow_style=False, allow_unicode=True) @@ -99,13 +105,11 @@ class MetadataManager: Manages the metadata (comfy.yaml) for ComfyUI when running comfy cli, including loading, validating, and saving metadata to a file. """ + def __init__(self): self.metadata_file = None self.env_checker = EnvChecker() - self.metadata = ComfyLockYAMLStruct( - basics=Basics(), - models=[] - ) + self.metadata = ComfyLockYAMLStruct(basics=Basics(), models=[]) def scan_dir(self): model_files = [] @@ -121,7 +125,7 @@ def scan_dir_concur(self): # Use ThreadPoolExecutor to manage concurrency with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [executor.submit(check_file, p) for p in base_path.rglob('*')] + futures = [executor.submit(check_file, p) for p in base_path.rglob("*")] for future in concurrent.futures.as_completed(futures): if future.result(): model_files.append(future.result()) diff --git a/comfy_cli/tracking.py b/comfy_cli/tracking.py index 6c291d6..bf2f29f 100644 --- a/comfy_cli/tracking.py +++ b/comfy_cli/tracking.py @@ -7,7 +7,7 @@ from comfy_cli.config_manager import ConfigManager MIXPANEL_TOKEN = "93aeab8962b622d431ac19800ccc9f67" -DISABLE_TELEMETRY = os.getenv('DISABLE_TELEMETRY', False) +DISABLE_TELEMETRY = os.getenv("DISABLE_TELEMETRY", False) mp = Mixpanel(MIXPANEL_TOKEN) if MIXPANEL_TOKEN else None # Generate a unique tracing ID per command. @@ -16,45 +16,54 @@ def track_event(event_name: str, properties: any = None): - enable_tracking = config_manager.get(constants.CONFIG_KEY_ENABLE_TRACKING) - if enable_tracking: - mp.track(distinct_id=tracing_id, event_name=event_name, properties=properties) + enable_tracking = config_manager.get(constants.CONFIG_KEY_ENABLE_TRACKING) + if enable_tracking: + mp.track(distinct_id=tracing_id, event_name=event_name, properties=properties) def track_command(sub_command: str = None): - """ - A decorator factory that logs the command function name and selected arguments when it's called. - """ + """ + A decorator factory that logs the command function name and selected arguments when it's called. + """ - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - command_name = f"{sub_command}:{func.__name__}" if sub_command is not None else func.__name__ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + command_name = ( + f"{sub_command}:{func.__name__}" + if sub_command is not None + else func.__name__ + ) - # Copy kwargs to avoid mutating original dictionary - # Remove context and ctx from the dictionary as they are not needed for tracking and not serializable. - filtered_kwargs = {k: v for k, v in kwargs.items() if k != 'ctx' and k != 'context'} + # Copy kwargs to avoid mutating original dictionary + # Remove context and ctx from the dictionary as they are not needed for tracking and not serializable. + filtered_kwargs = { + k: v for k, v in kwargs.items() if k != "ctx" and k != "context" + } - logging.debug(f"Tracking command: {command_name} with arguments: {filtered_kwargs}") - track_event(command_name, properties=filtered_kwargs) + logging.debug( + f"Tracking command: {command_name} with arguments: {filtered_kwargs}" + ) + track_event(command_name, properties=filtered_kwargs) - return func(*args, **kwargs) + return func(*args, **kwargs) - return wrapper + return wrapper - return decorator + return decorator def prompt_tracking_consent(): - _config_manager = ConfigManager() - tracking_enabled = _config_manager.get(constants.CONFIG_KEY_ENABLE_TRACKING) - if tracking_enabled is not None: - return - - enable_tracking = ui.prompt_confirm_action( - "Do you agree to enable tracking to improve the application?") - _config_manager.set(constants.CONFIG_KEY_ENABLE_TRACKING, str(enable_tracking)) - - # Note: only called once when the user interacts with the CLI for the - # first time iff the permission is granted. - track_event("install") + _config_manager = ConfigManager() + tracking_enabled = _config_manager.get(constants.CONFIG_KEY_ENABLE_TRACKING) + if tracking_enabled is not None: + return + + enable_tracking = ui.prompt_confirm_action( + "Do you agree to enable tracking to improve the application?" + ) + _config_manager.set(constants.CONFIG_KEY_ENABLE_TRACKING, str(enable_tracking)) + + # Note: only called once when the user interacts with the CLI for the + # first time iff the permission is granted. + track_event("install") diff --git a/comfy_cli/ui.py b/comfy_cli/ui.py index 7e70b5b..02aa701 100644 --- a/comfy_cli/ui.py +++ b/comfy_cli/ui.py @@ -9,87 +9,86 @@ def show_progress(iterable, total, description="Downloading..."): - """ - Display progress bar for iterable processes, especially useful for file downloads. - Each item in the iterable should be a chunk of data, and the progress bar will advance - by the size of each chunk. - - Args: - iterable (Iterable[bytes]): An iterable that yields chunks of data. - total (int): The total size of the data (e.g., total number of bytes) to be downloaded. - description (str): Description text for the progress bar. - - Yields: - bytes: Chunks of data as they are processed. - """ - with Progress() as progress: - task = progress.add_task(description, total=total) - for chunk in iterable: - yield chunk - progress.update(task, advance=len(chunk)) + """ + Display progress bar for iterable processes, especially useful for file downloads. + Each item in the iterable should be a chunk of data, and the progress bar will advance + by the size of each chunk. + + Args: + iterable (Iterable[bytes]): An iterable that yields chunks of data. + total (int): The total size of the data (e.g., total number of bytes) to be downloaded. + description (str): Description text for the progress bar. + + Yields: + bytes: Chunks of data as they are processed. + """ + with Progress() as progress: + task = progress.add_task(description, total=total) + for chunk in iterable: + yield chunk + progress.update(task, advance=len(chunk)) def prompt_select(question: str, choices: list) -> str: - """ - Asks a single select question using questionary and returns the selected response. + """ + Asks a single select question using questionary and returns the selected response. - Args: - question (str): The question to display to the user. - choices (list): A list of string choices for the user to select from. + Args: + question (str): The question to display to the user. + choices (list): A list of string choices for the user to select from. - Returns: - str: The selected choice from the user. - """ - return questionary.select( - question, - choices=choices - ).ask() + Returns: + str: The selected choice from the user. + """ + return questionary.select(question, choices=choices).ask() def prompt_multi_select(prompt: str, choices: List[str]) -> List[str]: - """ - Prompts the user to select multiple items from a list of choices. + """ + Prompts the user to select multiple items from a list of choices. - Args: - prompt (str): The message to display to the user. - choices (List[str]): A list of choices from which the user can select. + Args: + prompt (str): The message to display to the user. + choices (List[str]): A list of choices from which the user can select. - Returns: - List[str]: A list of the selected items. - """ - selections = questionary.checkbox(prompt, choices=choices).ask() # returns list of selected items - return selections if selections else [] + Returns: + List[str]: A list of the selected items. + """ + selections = questionary.checkbox( + prompt, choices=choices + ).ask() # returns list of selected items + return selections if selections else [] def prompt_confirm_action(prompt: str) -> bool: - """ - Prompts the user for confirmation before proceeding with an action. + """ + Prompts the user for confirmation before proceeding with an action. - Args: - prompt (str): The confirmation message to display to the user. + Args: + prompt (str): The confirmation message to display to the user. - Returns: - bool: True if the user confirms, False otherwise. - """ + Returns: + bool: True if the user confirms, False otherwise. + """ - return typer.confirm(prompt) + return typer.confirm(prompt) def display_table(data: List[Tuple], column_names: List[str], title: str = "") -> None: - """ - Displays a list of tuples in a table format using Rich. + """ + Displays a list of tuples in a table format using Rich. - Args: - data (List[Tuple]): A list of tuples, where each tuple represents a row. - column_names (List[str]): A list of column names for the table. - title (str): The title of the table. - """ - table = Table(title=title) + Args: + data (List[Tuple]): A list of tuples, where each tuple represents a row. + column_names (List[str]): A list of column names for the table. + title (str): The title of the table. + """ + table = Table(title=title) - for name in column_names: - table.add_column(name, overflow="fold") + for name in column_names: + table.add_column(name, overflow="fold") - for row in data: - table.add_row(*[str(item) for item in row]) + for row in data: + table.add_row(*[str(item) for item in row]) - console.print(table) + console.print(table) diff --git a/comfy_cli/utils.py b/comfy_cli/utils.py index 20c6574..7baa0e5 100644 --- a/comfy_cli/utils.py +++ b/comfy_cli/utils.py @@ -28,9 +28,9 @@ def get_instance(*args, **kwargs): def get_os(): - if 'win' in sys.platform: + if "win" in sys.platform: return constants.OS.WINDOWS - elif sys.platform == 'darwin': + elif sys.platform == "darwin": return constants.OS.MACOS return constants.OS.LINUX diff --git a/comfy_cli/workspace_manager.py b/comfy_cli/workspace_manager.py index 0801895..690b086 100644 --- a/comfy_cli/workspace_manager.py +++ b/comfy_cli/workspace_manager.py @@ -13,122 +13,155 @@ @singleton class WorkspaceManager: - def __init__(self): - self.config_manager = ConfigManager() - - @staticmethod - def update_context(context: typer.Context, workspace: Optional[str], recent: Optional[bool], here: Optional[bool]): - """ - Updates the context object with the workspace and recent flags. - """ - - if [workspace is not None, recent, here].count(True) > 1: - print(f"--workspace, --recent, and --here options cannot be used together.", file=sys.stderr) - raise typer.Exit(code=1) - - if workspace: - context.obj[constants.CONTEXT_KEY_WORKSPACE] = workspace - if recent: - context.obj[constants.CONTEXT_KEY_RECENT] = recent - if here: - context.obj[constants.CONTEXT_KEY_HERE] = here - - def set_recent_workspace(self, path: str): - """ - Sets the most recent workspace path in the configuration. - """ - self.config_manager.set(constants.CONFIG_KEY_RECENT_WORKSPACE, os.path.abspath(path)) - - def set_default_workspace(self, path: str): - """ - Sets the default workspace path in the configuration. - """ - self.config_manager.set(constants.CONFIG_KEY_DEFAULT_WORKSPACE, os.path.abspath(path)) - - def get_workspace_comfy_path(self, context: typer.Context) -> str: - """ - Retrieves the workspace path and appends '/ComfyUI' to it. - """ - - return self.get_workspace_path(context) + '/ComfyUI' - - def get_workspace_path(self, context: typer.Context) -> str: - """ - Retrieves the workspace path based on the following precedence: - 1. Specified Workspace - 2. Most Recent (if use_recent is True) - 3. User Set Default Workspace - 4. Current Directory (if it contains a ComfyUI setup) - 5. Most Recent Workspace - 6. Fallback Default Workspace ('~/comfy') - - Raises: - FileNotFoundError: If no valid workspace is found. - """ - specified_workspace = context.obj.get(constants.CONTEXT_KEY_WORKSPACE) - use_recent = context.obj.get(constants.CONTEXT_KEY_RECENT) - use_here = context.obj.get(constants.CONTEXT_KEY_HERE) - - # Check for explicitly specified workspace first - if specified_workspace: - specified_path = os.path.expanduser(specified_workspace) - if os.path.exists(specified_path): - if os.path.exists(os.path.join(specified_path, 'ComfyUI')): - return specified_path - - print(f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]") # If a path has been explicitly specified, cancel the command for safety. - raise typer.Exit(code=1) - - # Check for recent workspace if requested - if use_recent: - recent_workspace = self.config_manager.get(constants.CONFIG_KEY_RECENT_WORKSPACE) - if recent_workspace and os.path.exists(recent_workspace): - if os.path.exists(os.path.join(recent_workspace, 'ComfyUI')): - return recent_workspace - - print(f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]") # If a path has been explicitly specified, cancel the command for safety. - raise typer.Exit(code=1) - - # Check for current workspace if requested - if use_here: - current_directory = os.getcwd() - if os.path.exists(os.path.join(current_directory, 'ComfyUI')): - return current_directory - - comfy_repo = EnvChecker().comfy_repo - if comfy_repo is not None: - if os.path.basename(comfy_repo.working_dir) == 'ComfyUI': - return os.path.abspath(os.path.join(comfy_repo.working_dir, '..')) - else: - print(f"[bold red]warn: The path name of the ComfyUI executed through 'comfy-cli' must be 'ComfyUI'. The current ComfyUI is being ignored.[/bold red]") - raise typer.Exit(code=1) - - print(f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]") # If a path has been explicitly specified, cancel the command for safety. - raise typer.Exit(code=1) - - # Check for user-set default workspace - default_workspace = self.config_manager.get(constants.CONFIG_KEY_DEFAULT_WORKSPACE) - if default_workspace and os.path.exists(os.path.join(default_workspace, 'ComfyUI')): - return default_workspace - - # Check the current directory for a ComfyUI setup - current_directory = os.getcwd() - if os.path.exists(os.path.join(current_directory, 'ComfyUI')): - return current_directory - - # Check the current directory for a ComfyUI repo - comfy_repo = EnvChecker().comfy_repo - if comfy_repo is not None and os.path.basename(comfy_repo.working_dir) == 'ComfyUI': - return os.path.abspath(os.path.join(comfy_repo.working_dir, '..')) - - # Fallback to the most recent workspace if it exists - if not use_recent: - recent_workspace = self.config_manager.get(constants.CONFIG_KEY_RECENT_WORKSPACE) - if recent_workspace and os.path.exists(os.path.join(recent_workspace, 'ComfyUI')): - return recent_workspace - - # Final fallback to a hardcoded default workspace - fallback_default = os.path.expanduser('~/comfy') - if not os.path.exists(fallback_default): - os.makedirs(fallback_default) # Ensure the directory exists if not found - return fallback_default + def __init__(self): + self.config_manager = ConfigManager() + + @staticmethod + def update_context( + context: typer.Context, + workspace: Optional[str], + recent: Optional[bool], + here: Optional[bool], + ): + """ + Updates the context object with the workspace and recent flags. + """ + + if [workspace is not None, recent, here].count(True) > 1: + print( + f"--workspace, --recent, and --here options cannot be used together.", + file=sys.stderr, + ) + raise typer.Exit(code=1) + + if workspace: + context.obj[constants.CONTEXT_KEY_WORKSPACE] = workspace + if recent: + context.obj[constants.CONTEXT_KEY_RECENT] = recent + if here: + context.obj[constants.CONTEXT_KEY_HERE] = here + + def set_recent_workspace(self, path: str): + """ + Sets the most recent workspace path in the configuration. + """ + self.config_manager.set( + constants.CONFIG_KEY_RECENT_WORKSPACE, os.path.abspath(path) + ) + + def set_default_workspace(self, path: str): + """ + Sets the default workspace path in the configuration. + """ + self.config_manager.set( + constants.CONFIG_KEY_DEFAULT_WORKSPACE, os.path.abspath(path) + ) + + def get_workspace_comfy_path(self, context: typer.Context) -> str: + """ + Retrieves the workspace path and appends '/ComfyUI' to it. + """ + + return self.get_workspace_path(context) + "/ComfyUI" + + def get_workspace_path(self, context: typer.Context) -> str: + """ + Retrieves the workspace path based on the following precedence: + 1. Specified Workspace + 2. Most Recent (if use_recent is True) + 3. User Set Default Workspace + 4. Current Directory (if it contains a ComfyUI setup) + 5. Most Recent Workspace + 6. Fallback Default Workspace ('~/comfy') + + Raises: + FileNotFoundError: If no valid workspace is found. + """ + specified_workspace = context.obj.get(constants.CONTEXT_KEY_WORKSPACE) + use_recent = context.obj.get(constants.CONTEXT_KEY_RECENT) + use_here = context.obj.get(constants.CONTEXT_KEY_HERE) + + # Check for explicitly specified workspace first + if specified_workspace: + specified_path = os.path.expanduser(specified_workspace) + if os.path.exists(specified_path): + if os.path.exists(os.path.join(specified_path, "ComfyUI")): + return specified_path + + print( + f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]" + ) # If a path has been explicitly specified, cancel the command for safety. + raise typer.Exit(code=1) + + # Check for recent workspace if requested + if use_recent: + recent_workspace = self.config_manager.get( + constants.CONFIG_KEY_RECENT_WORKSPACE + ) + if recent_workspace and os.path.exists(recent_workspace): + if os.path.exists(os.path.join(recent_workspace, "ComfyUI")): + return recent_workspace + + print( + f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]" + ) # If a path has been explicitly specified, cancel the command for safety. + raise typer.Exit(code=1) + + # Check for current workspace if requested + if use_here: + current_directory = os.getcwd() + if os.path.exists(os.path.join(current_directory, "ComfyUI")): + return current_directory + + comfy_repo = EnvChecker().comfy_repo + if comfy_repo is not None: + if os.path.basename(comfy_repo.working_dir) == "ComfyUI": + return os.path.abspath(os.path.join(comfy_repo.working_dir, "..")) + else: + print( + f"[bold red]warn: The path name of the ComfyUI executed through 'comfy-cli' must be 'ComfyUI'. The current ComfyUI is being ignored.[/bold red]" + ) + raise typer.Exit(code=1) + + print( + f"[bold red]warn: The specified workspace does not contain ComfyUI directory.[/bold red]" + ) # If a path has been explicitly specified, cancel the command for safety. + raise typer.Exit(code=1) + + # Check for user-set default workspace + default_workspace = self.config_manager.get( + constants.CONFIG_KEY_DEFAULT_WORKSPACE + ) + if default_workspace and os.path.exists( + os.path.join(default_workspace, "ComfyUI") + ): + return default_workspace + + # Check the current directory for a ComfyUI setup + current_directory = os.getcwd() + if os.path.exists(os.path.join(current_directory, "ComfyUI")): + return current_directory + + # Check the current directory for a ComfyUI repo + comfy_repo = EnvChecker().comfy_repo + if ( + comfy_repo is not None + and os.path.basename(comfy_repo.working_dir) == "ComfyUI" + ): + return os.path.abspath(os.path.join(comfy_repo.working_dir, "..")) + + # Fallback to the most recent workspace if it exists + if not use_recent: + recent_workspace = self.config_manager.get( + constants.CONFIG_KEY_RECENT_WORKSPACE + ) + if recent_workspace and os.path.exists( + os.path.join(recent_workspace, "ComfyUI") + ): + return recent_workspace + + # Final fallback to a hardcoded default workspace + fallback_default = os.path.expanduser("~/comfy") + if not os.path.exists(fallback_default): + os.makedirs(fallback_default) # Ensure the directory exists if not found + return fallback_default From dab366041595dd7a07b1da0dd3dfa8ee0fe18c35 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 May 2024 00:41:46 +0900 Subject: [PATCH 3/7] feat: `comfy backup --output` --- README.md | 11 ++++++ comfy_cli/cmdline.py | 41 +++++++++++++++++++++++ comfy_cli/command/custom_nodes/command.py | 8 +++-- comfy_cli/workspace_manager.py | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ddd5dc..5416e89 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,16 @@ will simply update the comfy.yaml file to reflect the local setup * **(WIP)** `comfy install --snapshot=`: Install ComfyUI and whole environments from snapshot. * **(WIP)** Currently, only the installation of ComfyUI and custom nodes is being applied. + +### Backup Snapshot [WIP] + +To backup ComfyUI Environment: + +`comfy backup --output=<.yaml path>` + +This command is used to perform a full backup of the currently installed ComfyUI Environment. +**(WIP)** Currently, only the ComfyUI and Custom node information are backed up. + ### Specifying execution path * You can specify the path of ComfyUI where the command will be applied through path indicators as follows: @@ -100,6 +110,7 @@ comfy allows you to easily install, update, and remove packages for ComfyUI. Her `comfy package list` + ### Managing Custom Nodes comfy provides a convenient way to manage custom nodes for extending ComfyUI's functionality. Here are some examples: diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 738fd28..a339912 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -4,6 +4,8 @@ import time import uuid import webbrowser +import yaml + from typing import Optional import questionary @@ -67,6 +69,45 @@ def init(): print(f"scan_dir took {end_time - start_time:.2f} seconds to run") +@app.command(help="Backup current snapshot") +@tracking.track_command() +def backup( + ctx: typer.Context, + output: Annotated[ + str, + "--output", + typer.Option( + show_default=False, help="Specify the output file path. (.yaml)" + ), + ], +): + + if not output.endswith('.yaml'): + print( + f"[bold red]The output path must end with '.yaml'.[/bold red]" + ) + raise typer.Exit(code=1) + + output_path = os.path.abspath(output) + + config_manager = workspace_manager.config_manager + tmp_path = os.path.join(config_manager.get_config_path(), "tmp", str(uuid.uuid4())) + '.yaml' + tmp_path = os.path.abspath(tmp_path) + custom_nodes.command.execute_cm_cli(ctx, ["save-snapshot", "--output", tmp_path], silent=True) + + with open(tmp_path, 'r', encoding="UTF-8") as yaml_file: + info = yaml.load(yaml_file, Loader=yaml.SafeLoader) + os.remove(tmp_path) + + info['basic'] = 'N/A' # TODO: + info['models'] = [] # TODO: + + with open(output_path, "w") as yaml_file: + yaml.dump(info, yaml_file, allow_unicode=True) + + print(f"Snapshot file is saved as `{output_path}`") + + @app.command(help="Download and install ComfyUI and ComfyUI-Manager") @tracking.track_command() def install( diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 7cfd9dd..aa05d0f 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -16,7 +16,7 @@ workspace_manager = WorkspaceManager() -def execute_cm_cli(ctx: typer.Context, args, channel=None, mode=None): +def execute_cm_cli(ctx: typer.Context, args, channel=None, mode=None, silent=False): _config_manager = ConfigManager() workspace_path = workspace_manager.get_workspace_path(ctx) @@ -49,7 +49,11 @@ def execute_cm_cli(ctx: typer.Context, args, channel=None, mode=None): print(f"Execute from: {comfyui_path}") - subprocess.run(cmd, env=new_env) + if silent: + subprocess.run(cmd, env=new_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + subprocess.run(cmd, env=new_env) + workspace_manager.set_recent_workspace(workspace_path) diff --git a/comfy_cli/workspace_manager.py b/comfy_cli/workspace_manager.py index 690b086..ea02d0b 100644 --- a/comfy_cli/workspace_manager.py +++ b/comfy_cli/workspace_manager.py @@ -62,7 +62,7 @@ def get_workspace_comfy_path(self, context: typer.Context) -> str: Retrieves the workspace path and appends '/ComfyUI' to it. """ - return self.get_workspace_path(context) + "/ComfyUI" + return os.path.join(self.get_workspace_path(context), "ComfyUI") def get_workspace_path(self, context: typer.Context) -> str: """ From c14fc2a171ff052f3a4ca8bcb403b1e40f742f59 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 May 2024 01:15:15 +0900 Subject: [PATCH 4/7] reformat --- comfy_cli/cmdline.py | 31 ++++++++++++----------- comfy_cli/command/custom_nodes/command.py | 4 ++- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index a339912..ddd5f1b 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -74,33 +74,34 @@ def init(): def backup( ctx: typer.Context, output: Annotated[ - str, - "--output", - typer.Option( - show_default=False, help="Specify the output file path. (.yaml)" - ), - ], + str, + "--output", + typer.Option(show_default=False, help="Specify the output file path. (.yaml)"), + ], ): - if not output.endswith('.yaml'): - print( - f"[bold red]The output path must end with '.yaml'.[/bold red]" - ) + if not output.endswith(".yaml"): + print(f"[bold red]The output path must end with '.yaml'.[/bold red]") raise typer.Exit(code=1) output_path = os.path.abspath(output) config_manager = workspace_manager.config_manager - tmp_path = os.path.join(config_manager.get_config_path(), "tmp", str(uuid.uuid4())) + '.yaml' + tmp_path = ( + os.path.join(config_manager.get_config_path(), "tmp", str(uuid.uuid4())) + + ".yaml" + ) tmp_path = os.path.abspath(tmp_path) - custom_nodes.command.execute_cm_cli(ctx, ["save-snapshot", "--output", tmp_path], silent=True) + custom_nodes.command.execute_cm_cli( + ctx, ["save-snapshot", "--output", tmp_path], silent=True + ) - with open(tmp_path, 'r', encoding="UTF-8") as yaml_file: + with open(tmp_path, "r", encoding="UTF-8") as yaml_file: info = yaml.load(yaml_file, Loader=yaml.SafeLoader) os.remove(tmp_path) - info['basic'] = 'N/A' # TODO: - info['models'] = [] # TODO: + info["basic"] = "N/A" # TODO: + info["models"] = [] # TODO: with open(output_path, "w") as yaml_file: yaml.dump(info, yaml_file, allow_unicode=True) diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index aa05d0f..a20cad1 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -50,7 +50,9 @@ def execute_cm_cli(ctx: typer.Context, args, channel=None, mode=None, silent=Fal print(f"Execute from: {comfyui_path}") if silent: - subprocess.run(cmd, env=new_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run( + cmd, env=new_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) else: subprocess.run(cmd, env=new_env) From 2ecbe2b125e912907ace0d1c7016e39ebedbccd7 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 May 2024 22:37:49 +0900 Subject: [PATCH 5/7] refactor: move `backup` to `snapshot save` --- comfy_cli/cmdline.py | 42 +------------------ comfy_cli/command/custom_nodes/command.py | 1 - comfy_cli/command/models/models.py | 3 -- comfy_cli/command/snapshot/command.py | 51 +++++++++++++++++++++++ 4 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 comfy_cli/command/snapshot/command.py diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index ad05184..6ef0bb4 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -4,7 +4,6 @@ import time import uuid import webbrowser -import yaml from typing import Optional @@ -17,6 +16,7 @@ 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.snapshot import command as snapshot_command from comfy_cli.command import run as run_inner from comfy_cli.command.models import models as models_command from comfy_cli.config_manager import ConfigManager @@ -89,45 +89,6 @@ def entry( logging.info(f"scan_dir took {end_time - start_time:.2f} seconds to run") -@app.command(help="Backup current snapshot") -@tracking.track_command() -def backup( - output: Annotated[ - str, - "--output", - typer.Option(show_default=False, help="Specify the output file path. (.yaml)"), - ], -): - - if not output.endswith(".yaml"): - print(f"[bold red]The output path must end with '.yaml'.[/bold red]") - raise typer.Exit(code=1) - - output_path = os.path.abspath(output) - - config_manager = workspace_manager.config_manager - tmp_path = ( - os.path.join(config_manager.get_config_path(), "tmp", str(uuid.uuid4())) - + ".yaml" - ) - tmp_path = os.path.abspath(tmp_path) - custom_nodes.command.execute_cm_cli( - ["save-snapshot", "--output", tmp_path], silent=True - ) - - with open(tmp_path, "r", encoding="UTF-8") as yaml_file: - info = yaml.load(yaml_file, Loader=yaml.SafeLoader) - os.remove(tmp_path) - - info["basic"] = "N/A" # TODO: - info["models"] = [] # TODO: - - with open(output_path, "w") as yaml_file: - yaml.dump(info, yaml_file, allow_unicode=True) - - print(f"Snapshot file is saved as `{output_path}`") - - @app.command(help="Download and install ComfyUI and ComfyUI-Manager") @tracking.track_command() def install( @@ -448,3 +409,4 @@ def feedback(): 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="Manager ComfyUI-Manager.") +app.add_typer(snapshot_command.app, name="snapshot", help="Manage custom nodes.") diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 642f4d2..613cc78 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -2,7 +2,6 @@ from typing_extensions import List, Annotated from comfy_cli import tracking -from comfy_cli.env_checker import EnvChecker import os import subprocess import sys diff --git a/comfy_cli/command/models/models.py b/comfy_cli/command/models/models.py index 91589a0..d0d2097 100644 --- a/comfy_cli/command/models/models.py +++ b/comfy_cli/command/models/models.py @@ -21,7 +21,6 @@ def get_workspace() -> pathlib.Path: @app.command() @tracking.track_command("model") def download( - ctx: typer.Context, url: Annotated[ str, typer.Option( @@ -54,7 +53,6 @@ def download( @app.command() @tracking.track_command("model") def remove( - ctx: typer.Context, relative_path: str = typer.Option( DEFAULT_COMFY_MODEL_PATH, help="The relative path from the current workspace where the models are stored.", @@ -120,7 +118,6 @@ def remove( @app.command() @tracking.track_command("model") def list( - ctx: typer.Context, relative_path: str = typer.Option( DEFAULT_COMFY_MODEL_PATH, help="The relative path from the current workspace where the models are stored.", diff --git a/comfy_cli/command/snapshot/command.py b/comfy_cli/command/snapshot/command.py new file mode 100644 index 0000000..df4d3c1 --- /dev/null +++ b/comfy_cli/command/snapshot/command.py @@ -0,0 +1,51 @@ +import os +import typer +from typing_extensions import Annotated +from comfy_cli import tracking, ui +from comfy_cli.workspace_manager import WorkspaceManager +from comfy_cli.command import custom_nodes +import uuid +import yaml + +workspace_manager = WorkspaceManager() + +app = typer.Typer() + + +@app.command(help="Save current snapshot") +@tracking.track_command() +def save( + output: Annotated[ + str, + "--output", + typer.Option(show_default=False, help="Specify the output file path. (.yaml)"), + ], +): + + if not output.endswith(".yaml"): + print("[bold red]The output path must end with '.yaml'.[/bold red]") + raise typer.Exit(code=1) + + output_path = os.path.abspath(output) + + config_manager = workspace_manager.config_manager + tmp_path = ( + os.path.join(config_manager.get_config_path(), "tmp", str(uuid.uuid4())) + + ".yaml" + ) + tmp_path = os.path.abspath(tmp_path) + custom_nodes.command.execute_cm_cli( + ["save-snapshot", "--output", tmp_path], silent=True + ) + + with open(tmp_path, "r", encoding="UTF-8") as yaml_file: + info = yaml.load(yaml_file, Loader=yaml.SafeLoader) + os.remove(tmp_path) + + info["basic"] = "N/A" # TODO: + info["models"] = [] # TODO: + + with open(output_path, "w", encoding="UTF-8") as yaml_file: + yaml.dump(info, yaml_file, allow_unicode=True) + + print(f"Snapshot file is saved as `{output_path}`") From b8dbbe8756e4ca4d51cf701da35349b68e86dac3 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 May 2024 23:11:12 +0900 Subject: [PATCH 6/7] feat: `comfy snapshot restore` refactor: move apply_snapshot to snapshot command from install update README.md --- README.md | 13 ++++++---- comfy_cli/cmdline.py | 5 ++-- comfy_cli/command/install.py | 18 -------------- comfy_cli/command/snapshot/command.py | 34 ++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 96b535e..3b33f6a 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,20 @@ will simply update the comfy.yaml file to reflect the local setup * **(WIP)** Currently, only the installation of ComfyUI and custom nodes is being applied. -### Backup Snapshot [WIP] +### Snapshot [WIP] -To backup ComfyUI Environment: +To save ComfyUI Environment: -`comfy backup --output=<.yaml path>` +`comfy snapshot save --output=<.yaml path>` -This command is used to perform a full backup of the currently installed ComfyUI Environment. +To retore ComfyUI Environment: + +`comfy snapshot restore --input=<.yaml path>` + +This command is used to perform a full backup/restore of the currently installed ComfyUI Environment. **(WIP)** Currently, only the ComfyUI and Custom node information are backed up. + ### Specifying execution path * You can specify the path of ComfyUI where the command will be applied through path indicators as follows: diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 6ef0bb4..013ccfa 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -157,8 +157,7 @@ def install( ) if snapshot is not None: - checker.check() - install_inner.apply_snapshot(checker, snapshot) + snapshot_command.apply_snapshot(snapshot) print(f"ComfyUI is installed at: {comfy_path}") @@ -199,7 +198,7 @@ def update(target: str = typer.Argument("comfy", help="[all|comfy]")): def validate_comfyui(_env_checker): if _env_checker.comfy_repo is None: print( - f"[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]" + "[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]" ) raise typer.Exit(code=1) diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index df53344..67e2fb0 100644 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -2,8 +2,6 @@ import subprocess from rich import print import sys -import typer -from comfy_cli.command import custom_nodes from comfy_cli.workspace_manager import WorkspaceManager @@ -114,19 +112,3 @@ def execute( os.chdir(repo_dir) print("") - - -def apply_snapshot(checker, filepath): - if not os.path.exists(filepath): - print(f"[bold red]File not found: {filepath}[/bold red]") - raise typer.Exit(code=1) - - if checker.get_comfyui_manager_path() is not None and os.path.exists( - checker.get_comfyui_manager_path() - ): - print( - f"[bold red]If ComfyUI-Manager is not installed, the snapshot feature cannot be used.[/bold red]" - ) - raise typer.Exit(code=1) - - custom_nodes.command.restore_snapshot(filepath) diff --git a/comfy_cli/command/snapshot/command.py b/comfy_cli/command/snapshot/command.py index df4d3c1..38386e1 100644 --- a/comfy_cli/command/snapshot/command.py +++ b/comfy_cli/command/snapshot/command.py @@ -4,6 +4,7 @@ from comfy_cli import tracking, ui from comfy_cli.workspace_manager import WorkspaceManager from comfy_cli.command import custom_nodes +from rich import print import uuid import yaml @@ -12,7 +13,7 @@ app = typer.Typer() -@app.command(help="Save current snapshot") +@app.command(help="Save current snapshot to .yaml file") @tracking.track_command() def save( output: Annotated[ @@ -49,3 +50,34 @@ def save( yaml.dump(info, yaml_file, allow_unicode=True) print(f"Snapshot file is saved as `{output_path}`") + + +@app.command(help="Restore from snapshot file") +@tracking.track_command() +def restore( + input: Annotated[ + str, + "--input", + typer.Option(show_default=False, help="Specify the input file path. (.yaml)"), + ], +): + input_path = os.path.abspath(input) + apply_snapshot(input_path) + + #TODO: restore other properties + + +def apply_snapshot(filepath): + if not os.path.exists(filepath): + print(f"[bold red]File not found: {filepath}[/bold red]") + raise typer.Exit(code=1) + + if workspace_manager.get_comfyui_manager_path() is None or not os.path.exists( + workspace_manager.get_comfyui_manager_path() + ): + print( + "[bold red]If ComfyUI-Manager is not installed, the snapshot feature cannot be used.[/bold red]" + ) + raise typer.Exit(code=1) + + custom_nodes.command.restore_snapshot(filepath) From 9f93ca55411df786f63ecf1d01761c90da564450 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 May 2024 23:14:59 +0900 Subject: [PATCH 7/7] reformat --- comfy_cli/command/snapshot/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_cli/command/snapshot/command.py b/comfy_cli/command/snapshot/command.py index 38386e1..21f7e47 100644 --- a/comfy_cli/command/snapshot/command.py +++ b/comfy_cli/command/snapshot/command.py @@ -64,7 +64,7 @@ def restore( input_path = os.path.abspath(input) apply_snapshot(input_path) - #TODO: restore other properties + # TODO: restore other properties def apply_snapshot(filepath):