You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1019 lines
32 KiB

import os
import sys
import traceback
import json
import asyncio
import subprocess
import shutil
import concurrent
import threading
from typing import Optional
import typer
from rich import print
from typing_extensions import List, Annotated
import re
import git
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_core as core
import cm_global
comfyui_manager_path = os.path.dirname(__file__)
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
print(f"\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
comfy_path = os.path.abspath(os.path.join(comfyui_manager_path, '..', '..'))
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
custom_nodes_path = os.path.join(comfy_path, 'custom_nodes')
script_path = os.path.join(startup_script_path, "install-scripts.txt")
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json")
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
cm_global.pip_overrides = {}
if os.path.exists(pip_overrides_path):
with open(pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
cm_global.pip_overrides = json.load(json_file)
def check_comfyui_hash():
repo = git.Repo(comfy_path)
core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
comfy_ui_hash = repo.head.commit.hexsha
cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
check_comfyui_hash() # This is a preparation step for manager_core
def read_downgrade_blacklist():
try:
import configparser
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
config = configparser.ConfigParser()
config.read(config_path)
default_conf = config['default']
if 'downgrade_blacklist' in default_conf:
items = default_conf['downgrade_blacklist'].split(',')
items = [x.strip() for x in items if x != '']
cm_global.pip_downgrade_blacklist += items
cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
except:
pass
read_downgrade_blacklist() # This is a preparation step for manager_core
class Ctx:
def __init__(self):
self.channel = 'default'
self.mode = 'remote'
self.processed_install = set()
self.custom_node_map_cache = None
def set_channel_mode(self, channel, mode):
if mode is not None:
self.mode = mode
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,
)
exit(1)
if channel is not None:
self.channel = channel
def post_install(self, url):
try:
repository_name = url.split("/")[-1].strip()
repo_path = os.path.join(custom_nodes_path, repository_name)
repo_path = os.path.abspath(repo_path)
requirements_path = os.path.join(repo_path, 'requirements.txt')
install_script_path = os.path.join(repo_path, 'install.py')
if os.path.exists(requirements_path):
with (open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file):
for line in file:
package_name = core.remap_pip_package(line.strip())
if package_name and not core.is_installed(package_name):
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
output = subprocess.check_output(install_cmd, cwd=repo_path, text=True)
for msg_line in output.split('\n'):
if 'Requirement already satisfied:' in msg_line:
print('.', end='')
else:
print(msg_line)
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in self.processed_install:
self.processed_install.add(f'{repo_path}/install.py')
install_cmd = [sys.executable, install_script_path]
output = subprocess.check_output(install_cmd, cwd=repo_path, text=True)
for msg_line in output.split('\n'):
if 'Requirement already satisfied:' in msg_line:
print('.', end='')
else:
print(msg_line)
except Exception:
print(f"ERROR: Restoring '{url}' is failed.")
def restore_dependencies(self):
node_paths = [os.path.join(custom_nodes_path, name) for name in os.listdir(custom_nodes_path)
if os.path.isdir(os.path.join(custom_nodes_path, name)) and not name.endswith('.disabled')]
total = len(node_paths)
i = 1
for x in node_paths:
print(f"----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
self.post_install(x)
i += 1
def load_custom_nodes(self):
channel_dict = core.get_channel_dict()
if self.channel not in channel_dict:
print(f"[bold red]ERROR: Invalid channel is specified `--channel {self.channel}`[/bold red]", file=sys.stderr)
exit(1)
if self.mode not in ['remote', 'local', 'cache']:
print(f"[bold red]ERROR: Invalid mode is specified `--mode {self.mode}`[/bold red]", file=sys.stderr)
exit(1)
channel_url = channel_dict[self.channel]
res = {}
json_obj = asyncio.run(core.get_data_by_mode(self.mode, 'custom-node-list.json', channel_url=channel_url))
for x in json_obj['custom_nodes']:
for y in x['files']:
if 'github.com' in y and not (y.endswith('.py') or y.endswith('.js')):
repo_name = y.split('/')[-1]
res[repo_name] = (x, False)
if 'id' in x:
if x['id'] not in res:
res[x['id']] = (x, True)
return res
def get_custom_node_map(self):
if self.custom_node_map_cache is not None:
return self.custom_node_map_cache
self.custom_node_map_cache = self.load_custom_nodes()
return self.custom_node_map_cache
def lookup_node_path(self, node_name, robust=False):
if '..' in node_name:
print(f"\n[bold red]ERROR: Invalid node name '{node_name}'[/bold red]\n")
exit(2)
custom_node_map = self.get_custom_node_map()
if node_name in custom_node_map:
node_url = custom_node_map[node_name][0]['files'][0]
repo_name = node_url.split('/')[-1]
node_path = os.path.join(custom_nodes_path, repo_name)
return node_path, custom_node_map[node_name][0]
elif robust:
node_path = os.path.join(custom_nodes_path, node_name)
return node_path, None
print(f"\n[bold red]ERROR: Invalid node name '{node_name}'[/bold red]\n")
exit(2)
cm_ctx = Ctx()
def install_node(node_name, is_all=False, cnt_msg=''):
if '://' in node_name:
# install via urls
res = core.gitclone_install([node_name])
if not res:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.[/bold red]")
else:
print(f"{cnt_msg} [INSTALLED] {node_name:50}")
else:
node_path, node_item = cm_ctx.lookup_node_path(node_name)
if os.path.exists(node_path):
if not is_all:
print(f"{cnt_msg} [ SKIPPED ] {node_name:50} => Already installed")
elif os.path.exists(node_path + '.disabled'):
enable_node(node_name)
else:
res = core.gitclone_install(node_item['files'], instant_execution=True, msg_prefix=f"[{cnt_msg}] ")
if not res:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.[/bold red]")
else:
print(f"{cnt_msg} [INSTALLED] {node_name:50}")
def reinstall_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name)
if os.path.exists(node_path):
shutil.rmtree(node_path)
if os.path.exists(node_path + '.disabled'):
shutil.rmtree(node_path + '.disabled')
install_node(node_name, is_all=is_all, cnt_msg=cnt_msg)
def fix_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
files = node_item['files'] if node_item is not None else [node_path]
if os.path.exists(node_path):
print(f"{cnt_msg} [ FIXING ]: {node_name:50} => Disabled")
res = core.gitclone_fix(files, instant_execution=True)
if not res:
print(f"ERROR: An error occurred while fixing '{node_name}'.")
elif not is_all and os.path.exists(node_path + '.disabled'):
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Disabled")
elif not is_all:
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed")
def uninstall_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
files = node_item['files'] if node_item is not None else [node_path]
if os.path.exists(node_path) or os.path.exists(node_path + '.disabled'):
res = core.gitclone_uninstall(files)
if not res:
print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
else:
print(f"{cnt_msg} [UNINSTALLED] {node_name:50}")
else:
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed")
def update_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
files = node_item['files'] if node_item is not None else [node_path]
res = core.gitclone_update(files, skip_script=True, msg_prefix=f"[{cnt_msg}] ")
if not res:
print(f"ERROR: An error occurred while updating '{node_name}'.")
return None
return node_path
def update_parallel(nodes):
is_all = False
if 'all' in nodes:
is_all = True
nodes = [x for x in cm_ctx.get_custom_node_map().keys() if os.path.exists(os.path.join(custom_nodes_path, x)) or os.path.exists(os.path.join(custom_nodes_path, x) + '.disabled')]
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]
total = len(nodes)
lock = threading.Lock()
processed = []
i = 0
def process_custom_node(x):
nonlocal i
nonlocal processed
with lock:
i += 1
try:
node_path = update_node(x, is_all=is_all, cnt_msg=f'{i}/{total}')
with lock:
processed.append(node_path)
except Exception as e:
print(f"ERROR: {e}")
traceback.print_exc()
with concurrent.futures.ThreadPoolExecutor(4) as executor:
for item in nodes:
executor.submit(process_custom_node, item)
i = 1
for node_path in processed:
if node_path is None:
print(f"[{i}/{total}] Post update: ERROR")
else:
print(f"[{i}/{total}] Post update: {node_path}")
cm_ctx.post_install(node_path)
i += 1
def update_comfyui():
res = core.update_path(comfy_path, instant_execution=True)
if res == 'fail':
print("Updating ComfyUI has failed.")
elif res == 'updated':
print("ComfyUI is updated.")
else:
print("ComfyUI is already up to date.")
def enable_node(node_name, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager':
return
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
if os.path.exists(node_path + '.disabled'):
current_name = node_path + '.disabled'
os.rename(current_name, node_path)
print(f"{cnt_msg} [ENABLED] {node_name:50}")
elif os.path.exists(node_path):
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Already enabled")
elif not is_all:
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Not installed")
def disable_node(node_name, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager':
return
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
if os.path.exists(node_path):
current_name = node_path
new_name = node_path + '.disabled'
os.rename(current_name, new_name)
print(f"{cnt_msg} [DISABLED] {node_name:50}")
elif os.path.exists(node_path + '.disabled'):
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Already disabled")
elif not is_all:
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Not installed")
def show_list(kind, simple=False):
for k, v in cm_ctx.get_custom_node_map().items():
if v[1]:
continue
node_path = os.path.join(custom_nodes_path, k)
states = set()
if os.path.exists(node_path):
prefix = '[ ENABLED ] '
states.add('installed')
states.add('enabled')
states.add('all')
elif os.path.exists(node_path + '.disabled'):
prefix = '[ DISABLED ] '
states.add('installed')
states.add('disabled')
states.add('all')
else:
prefix = '[ NOT INSTALLED ] '
states.add('not-installed')
states.add('all')
if kind in states:
if simple:
print(f"{k:50}")
else:
short_id = v[0].get('id', "")
print(f"{prefix} {k:50} {short_id:20} (author: {v[0]['author']})")
# unregistered nodes
candidates = os.listdir(os.path.realpath(custom_nodes_path))
for k in candidates:
fullpath = os.path.join(custom_nodes_path, k)
if os.path.isfile(fullpath):
continue
if k in ['__pycache__']:
continue
states = set()
if k.endswith('.disabled'):
prefix = '[ DISABLED ] '
states.add('installed')
states.add('disabled')
states.add('all')
k = k[:-9]
else:
prefix = '[ ENABLED ] '
states.add('installed')
states.add('enabled')
states.add('all')
if k not in cm_ctx.get_custom_node_map():
if kind in states:
if simple:
print(f"{k:50}")
else:
print(f"{prefix} {k:50} {'':20} (author: N/A)")
def show_snapshot(simple_mode=False):
json_obj = core.get_current_snapshot()
if simple_mode:
print(f"[{json_obj['comfyui']}] comfyui")
for k, v in json_obj['git_custom_nodes'].items():
print(f"[{v['hash']}] {k}")
for v in json_obj['file_custom_nodes']:
print(f"[ N/A ] {v['filename']}")
else:
formatted_json = json.dumps(json_obj, ensure_ascii=False, indent=4)
print(formatted_json)
def show_snapshot_list(simple_mode=False):
snapshot_path = os.path.join(comfyui_manager_path, 'snapshots')
files = os.listdir(snapshot_path)
json_files = [x for x in files if x.endswith('.json')]
for x in sorted(json_files):
print(x)
def cancel():
if os.path.exists(script_path):
os.remove(script_path)
if os.path.exists(restore_snapshot_path):
os.remove(restore_snapshot_path)
def auto_save_snapshot():
path = core.save_snapshot_with_postfix('cli-autosave')
print(f"Current snapshot is saved as `{path}`")
def for_each_nodes(nodes, act, allow_all=True):
is_all = False
if allow_all and 'all' in nodes:
is_all = True
nodes = [x for x in cm_ctx.get_custom_node_map().keys() if os.path.exists(os.path.join(custom_nodes_path, x)) or os.path.exists(os.path.join(custom_nodes_path, x) + '.disabled')]
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]
total = len(nodes)
i = 1
for x in nodes:
try:
act(x, is_all=is_all, cnt_msg=f'{i}/{total}')
except Exception as e:
print(f"ERROR: {e}")
traceback.print_exc()
i += 1
app = typer.Typer()
@app.command(help="Display help for commands")
def help(ctx: typer.Context):
print(ctx.find_root().get_help())
ctx.exit(0)
@app.command(help="Install custom nodes")
def install(
nodes: List[str] = typer.Argument(
..., help="List of custom nodes to install"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=install_node)
@app.command(help="Reinstall custom nodes")
def reinstall(
nodes: List[str] = typer.Argument(
..., help="List of custom nodes to reinstall"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=reinstall_node)
@app.command(help="Uninstall custom nodes")
def uninstall(
nodes: List[str] = typer.Argument(
..., help="List of custom nodes to uninstall"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=uninstall_node)
@app.command(help="Disable custom nodes")
def update(
nodes: List[str] = typer.Argument(
...,
help="[all|List of custom nodes to update]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
for x in nodes:
if x.lower() in ['comfyui', 'comfy', 'all']:
update_comfyui()
break
update_parallel(nodes)
@app.command(help="Disable custom nodes")
def disable(
nodes: List[str] = typer.Argument(
...,
help="[all|List of custom nodes to disable]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
for_each_nodes(nodes, disable_node, allow_all=True)
@app.command(help="Enable custom nodes")
def enable(
nodes: List[str] = typer.Argument(
...,
help="[all|List of custom nodes to enable]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
for_each_nodes(nodes, enable_node, allow_all=True)
@app.command(help="Fix dependencies of custom nodes")
def fix(
nodes: List[str] = typer.Argument(
...,
help="[all|List of custom nodes to fix]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
for_each_nodes(nodes, fix_node, allow_all=True)
@app.command("show", help="Show node list (simple mode)")
def show(
arg: str = typer.Argument(
help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
valid_commands = [
"installed",
"enabled",
"not-installed",
"disabled",
"all",
"snapshot",
"snapshot-list",
]
if arg not in valid_commands:
typer.echo(f"Invalid command: `show {arg}`", err=True)
exit(1)
cm_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot':
show_snapshot()
elif arg == 'snapshot-list':
show_snapshot_list()
else:
show_list(arg)
@app.command("simple-show", help="Show node list (simple mode)")
def simple_show(
arg: str = typer.Argument(
help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]"
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
valid_commands = [
"installed",
"enabled",
"not-installed",
"disabled",
"all",
"snapshot",
"snapshot-list",
]
if arg not in valid_commands:
typer.echo(f"[bold red]Invalid command: `show {arg}`[/bold red]", err=True)
exit(1)
cm_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot':
show_snapshot(True)
elif arg == 'snapshot-list':
show_snapshot_list(True)
else:
show_list(arg, True)
@app.command('cli-only-mode', help="Set whether to use ComfyUI-Manager in CLI-only mode.")
def cli_only_mode(
mode: str = typer.Argument(
..., help="[enable|disable]"
)):
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
if mode.lower() == 'enable':
with open(cli_mode_flag, 'w') as file:
pass
print(f"\nINFO: `cli-only-mode` is enabled\n")
elif mode.lower() == 'disable':
if os.path.exists(cli_mode_flag):
os.remove(cli_mode_flag)
print(f"\nINFO: `cli-only-mode` is disabled\n")
else:
print(f"\n[bold red]Invalid value for cli-only-mode: {mode}[/bold red]\n")
exit(1)
@app.command(
"deps-in-workflow", help="Generate dependencies file from workflow (.json/.png)"
)
def deps_in_workflow(
workflow: Annotated[
str, typer.Option(show_default=False, help="Workflow file (.json/.png)")
],
output: Annotated[
str, typer.Option(show_default=False, help="Output file (.json)")
],
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
input_path = workflow
output_path = output
if not os.path.exists(input_path):
print(f"[bold red]File not found: {input_path}[/bold red]")
exit(1)
used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=cm_ctx.mode, channel_url=cm_ctx.channel))
custom_nodes = {}
for x in used_exts:
custom_nodes[x] = {'state': core.simple_check_custom_node(x),
'hash': '-'
}
res = {
'custom_nodes': custom_nodes,
'unknown_nodes': list(unknown_nodes)
}
with open(output_path, "w", encoding='utf-8') as output_file:
json.dump(res, output_file, indent=4)
print(f"Workflow dependencies are being saved into {output_path}.")
@app.command("save-snapshot", help="Save a snapshot of the current ComfyUI environment. If output path isn't provided. Save to ComfyUI-Manager/snapshots path.")
def save_snapshot(
output: Annotated[
str,
typer.Option(
show_default=False, help="Specify the output file path. (.json/.yaml)"
),
] = None,
):
path = core.save_snapshot_with_postfix('snapshot', output)
print(f"Current snapshot is saved as `{path}`")
@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
def restore_snapshot(
snapshot_name: str,
pip_non_url: Optional[bool] = typer.Option(
default=None,
show_default=False,
is_flag=True,
help="Restore for pip packages registered on PyPI.",
),
pip_non_local_url: Optional[bool] = typer.Option(
default=None,
show_default=False,
is_flag=True,
help="Restore for pip packages registered at web URLs.",
),
pip_local_url: Optional[bool] = typer.Option(
default=None,
show_default=False,
is_flag=True,
help="Restore for pip packages specified by local paths.",
),
):
extras = []
if pip_non_url:
extras.append('--pip-non-url')
if pip_non_local_url:
extras.append('--pip-non-local-url')
if pip_local_url:
extras.append('--pip-local-url')
print(f"PIPs restore mode: {extras}")
if os.path.exists(snapshot_name):
snapshot_path = os.path.abspath(snapshot_name)
else:
snapshot_path = os.path.join(core.comfyui_manager_path, 'snapshots', snapshot_name)
if not os.path.exists(snapshot_path):
print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")
exit(1)
try:
cloned_repos = []
checkout_repos = []
skipped_repos = []
enabled_repos = []
disabled_repos = []
is_failed = False
def extract_infos(msg):
nonlocal is_failed
for x in msg:
if x.startswith("CLONE: "):
cloned_repos.append(x[7:])
elif x.startswith("CHECKOUT: "):
checkout_repos.append(x[10:])
elif x.startswith("SKIPPED: "):
skipped_repos.append(x[9:])
elif x.startswith("ENABLE: "):
enabled_repos.append(x[8:])
elif x.startswith("DISABLE: "):
disabled_repos.append(x[9:])
elif 'APPLY SNAPSHOT: False' in x:
is_failed = True
print(f"Restore snapshot.")
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', snapshot_path] + extras
output = subprocess.check_output(cmd_str, cwd=custom_nodes_path, text=True)
msg_lines = output.split('\n')
extract_infos(msg_lines)
for url in cloned_repos:
cm_ctx.post_install(url)
# print summary
for x in cloned_repos:
print(f"[ INSTALLED ] {x}")
for x in checkout_repos:
print(f"[ CHECKOUT ] {x}")
for x in enabled_repos:
print(f"[ ENABLED ] {x}")
for x in disabled_repos:
print(f"[ DISABLED ] {x}")
if is_failed:
print(output)
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
except Exception:
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
traceback.print_exc()
raise typer.Exit(code=1)
@app.command(
"restore-dependencies", help="Restore dependencies from whole installed custom nodes."
)
def restore_dependencies():
node_paths = [os.path.join(custom_nodes_path, name) for name in os.listdir(custom_nodes_path)
if os.path.isdir(os.path.join(custom_nodes_path, name)) and not name.endswith('.disabled')]
total = len(node_paths)
i = 1
for x in node_paths:
print(f"----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
cm_ctx.post_install(x)
i += 1
@app.command(
"install-deps",
help="Install dependencies from dependencies file(.json) or workflow(.png/.json)",
)
def install_deps(
deps: str = typer.Argument(
help="Dependency spec file (.json)",
),
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
auto_save_snapshot()
if not os.path.exists(deps):
print(f"[bold red]File not found: {deps}[/bold red]")
exit(1)
else:
with open(deps, 'r', encoding="UTF-8", errors="ignore") as json_file:
try:
json_obj = json.load(json_file)
except:
print(f"[bold red]Invalid json file: {deps}[/bold red]")
exit(1)
for k in json_obj['custom_nodes'].keys():
state = core.simple_check_custom_node(k)
if state == 'installed':
continue
elif state == 'not-installed':
core.gitclone_install([k], instant_execution=True)
else: # disabled
core.gitclone_set_active([k], False)
print("Dependency installation and activation complete.")
@app.command(help="Clear reserved startup action in ComfyUI-Manager")
def clear():
cancel()
@app.command("export-custom-node-ids", help="Export custom node ids")
def export_custom_node_ids(
path: str,
channel: Annotated[
str,
typer.Option(
show_default=False,
help="Specify the operation mode"
),
] = None,
mode: str = typer.Option(
None,
help="[remote|local|cache]"
)):
cm_ctx.set_channel_mode(channel, mode)
with open(path, "w", encoding='utf-8') as output_file:
for x in cm_ctx.get_custom_node_map().keys():
print(x, file=output_file)
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())
print(f"")