diff --git a/README.md b/README.md index fd46af2..13b80b4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ 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. + + +### Snapshot [WIP] + +To save ComfyUI Environment: + +`comfy snapshot save --output=<.yaml path>` + +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 diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 45e8169..e4b92d6 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -16,6 +16,7 @@ from comfy_cli.command import run as run_inner from comfy_cli.command.launch import launch as launch_command from comfy_cli.command.models import models as models_command +from comfy_cli.command.snapshot import command as snapshot_command from comfy_cli.config_manager import ConfigManager from comfy_cli.constants import GPU_OPTION, CUDAVersion from comfy_cli.env_checker import EnvChecker @@ -221,10 +222,14 @@ def install( help="Use new fast dependency installer", ), ] = False, + snapshot: Annotated[str, typer.Option(help="Specify path to comfy-lock.yaml")] = None, ): check_for_updates() checker = EnvChecker() + if snapshot is not None: + snapshot = os.path.abspath(snapshot) + comfy_path, _ = workspace_manager.get_workspace_path() is_comfy_installed_at_path, repo_dir = check_comfy_repo(comfy_path) @@ -325,6 +330,9 @@ def install( fast_deps=fast_deps, ) + if snapshot is not None: + snapshot_command.apply_snapshot(snapshot) + print(f"ComfyUI is installed at: {comfy_path}") @@ -654,3 +662,4 @@ def standalone( app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.") app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.") app.add_typer(tracking.app, name="tracking", help="Manage analytics tracking settings.") +app.add_typer(snapshot_command.app, name="snapshot", help="Manage custom nodes.") diff --git a/comfy_cli/command/custom_nodes/cm_cli_util.py b/comfy_cli/command/custom_nodes/cm_cli_util.py index 1e48652..823e62f 100644 --- a/comfy_cli/command/custom_nodes/cm_cli_util.py +++ b/comfy_cli/command/custom_nodes/cm_cli_util.py @@ -21,7 +21,7 @@ } -def execute_cm_cli(args, channel=None, fast_deps=False, mode=None) -> str | None: +def execute_cm_cli(args, channel=None, fast_deps=False, mode=None, silent=False) -> str | None: _config_manager = ConfigManager() workspace_path = workspace_manager.workspace_path @@ -58,7 +58,8 @@ def execute_cm_cli(args, channel=None, fast_deps=False, mode=None) -> str | None try: result = subprocess.run(cmd, env=new_env, check=True, capture_output=True, text=True) - print(result.stdout) + if not silent: + print(result.stdout) if fast_deps and args[0] in _dependency_cmds: # we're using the fast_deps behavior and just ran a command that invalidated the dependencies diff --git a/comfy_cli/command/models/models.py b/comfy_cli/command/models/models.py index 82740cb..84e0350 100644 --- a/comfy_cli/command/models/models.py +++ b/comfy_cli/command/models/models.py @@ -127,7 +127,6 @@ def request_civitai_model_api(model_id: int, version_id: int = None, headers: Op @app.command(help="Download model file from url") @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), @@ -238,7 +237,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.", @@ -302,7 +300,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..21f7e47 --- /dev/null +++ b/comfy_cli/command/snapshot/command.py @@ -0,0 +1,83 @@ +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 +from rich import print +import uuid +import yaml + +workspace_manager = WorkspaceManager() + +app = typer.Typer() + + +@app.command(help="Save current snapshot to .yaml file") +@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}`") + + +@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)