fred-dev/comfy_ui_ali
0
1import os2import sys3import traceback4import json5import asyncio6import concurrent7import threading8from typing import Optional9 10import typer11from rich import print12from typing_extensions import List, Annotated13import re14import git15import importlib16 17 18sys.path.append(os.path.dirname(__file__))19sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))20 21import manager_util22 23# read env vars24# COMFYUI_FOLDERS_BASE_PATH is not required in cm-cli.py25# `comfy_path` should be resolved before importing manager_core26comfy_path = os.environ.get('COMFYUI_PATH')27if comfy_path is None:28 try:29 import folder_paths30 comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))31 except:32 print("\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)33 comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))34 35# This should be placed here36sys.path.append(comfy_path)37 38import utils.extra_config39import cm_global40import manager_core as core41from manager_core import unified_manager42import cnr_utils43 44comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))45 46cm_global.pip_blacklist = {'torch', 'torchsde', 'torchvision'}47cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']48cm_global.pip_overrides = {'numpy': 'numpy<2'}49 50if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json")):51 with open(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json"), 'r', encoding="UTF-8", errors="ignore") as json_file:52 cm_global.pip_overrides = json.load(json_file)53 54 55if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_blacklist.list")):56 with open(os.path.join(manager_util.comfyui_manager_path, "pip_blacklist.list"), 'r', encoding="UTF-8", errors="ignore") as f:57 for x in f.readlines():58 y = x.strip()59 if y != '':60 cm_global.pip_blacklist.add(y)61 62 63def check_comfyui_hash():64 try:65 repo = git.Repo(comfy_path)66 core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))67 core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime68 except:69 print('[bold yellow]INFO: Frozen ComfyUI mode.[/bold yellow]')70 core.comfy_ui_revision = 071 core.comfy_ui_commit_datetime = 072 73 cm_global.variables['comfyui.revision'] = core.comfy_ui_revision74 75 76check_comfyui_hash() # This is a preparation step for manager_core77core.check_invalid_nodes()78 79 80def read_downgrade_blacklist():81 try:82 import configparser83 config = configparser.ConfigParser(strict=False)84 config.read(core.manager_config.path)85 default_conf = config['default']86 87 if 'downgrade_blacklist' in default_conf:88 items = default_conf['downgrade_blacklist'].split(',')89 items = [x.strip() for x in items if x != '']90 cm_global.pip_downgrade_blacklist += items91 cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))92 except:93 pass94 95 96read_downgrade_blacklist() # This is a preparation step for manager_core97 98 99class Ctx:100 folder_paths = None101 102 def __init__(self):103 self.channel = 'default'104 self.no_deps = False105 self.mode = 'cache'106 self.user_directory = None107 self.custom_nodes_paths = [os.path.join(core.comfy_base_path, 'custom_nodes')]108 self.manager_files_directory = os.path.dirname(__file__)109 110 if Ctx.folder_paths is None:111 try:112 Ctx.folder_paths = importlib.import_module('folder_paths')113 except ImportError:114 print("Warning: Unable to import folder_paths module")115 116 def set_channel_mode(self, channel, mode):117 if mode is not None:118 self.mode = mode119 120 valid_modes = ["remote", "local", "cache"]121 if mode and mode.lower() not in valid_modes:122 typer.echo(123 f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.",124 err=True,125 )126 exit(1)127 128 if channel is not None:129 self.channel = channel130 131 asyncio.run(unified_manager.reload(cache_mode=self.mode, dont_wait=False))132 asyncio.run(unified_manager.load_nightly(self.channel, self.mode))133 134 def set_no_deps(self, no_deps):135 self.no_deps = no_deps136 137 def set_user_directory(self, user_directory):138 if user_directory is None:139 return140 141 extra_model_paths_yaml = os.path.join(user_directory, 'extra_model_paths.yaml')142 if os.path.exists(extra_model_paths_yaml):143 utils.extra_config.load_extra_path_config(extra_model_paths_yaml)144 145 core.update_user_directory(user_directory)146 147 if os.path.exists(core.manager_pip_overrides_path):148 with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:149 cm_global.pip_overrides = json.load(json_file)150 cm_global.pip_overrides = {'numpy': 'numpy<2'}151 152 if os.path.exists(core.manager_pip_blacklist_path):153 with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:154 for x in f.readlines():155 y = x.strip()156 if y != '':157 cm_global.pip_blacklist.add(y)158 159 def update_custom_nodes_dir(self, target_dir):160 import folder_paths161 a, b = folder_paths.folder_names_and_paths['custom_nodes']162 folder_paths.folder_names_and_paths['custom_nodes'] = [os.path.abspath(target_dir)], set()163 164 @staticmethod165 def get_startup_scripts_path():166 return os.path.join(core.manager_startup_script_path, "install-scripts.txt")167 168 @staticmethod169 def get_restore_snapshot_path():170 return os.path.join(core.manager_startup_script_path, "restore-snapshot.json")171 172 @staticmethod173 def get_snapshot_path():174 return core.manager_snapshot_path175 176 @staticmethod177 def get_custom_nodes_paths():178 if Ctx.folder_paths is None:179 print("Error: folder_paths module is not available")180 return []181 return Ctx.folder_paths.get_folder_paths('custom_nodes')182 183 184cmd_ctx = Ctx()185 186 187def install_node(node_spec_str, is_all=False, cnt_msg=''):188 if core.is_valid_url(node_spec_str):189 # install via urls190 res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))191 if not res.result:192 print(res.msg)193 print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")194 else:195 print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")196 else:197 node_spec = unified_manager.resolve_node_spec(node_spec_str)198 199 if node_spec is None:200 return201 202 node_name, version_spec, is_specified = node_spec203 204 # NOTE: install node doesn't allow update if version is not specified205 if not is_specified:206 version_spec = None207 208 res = asyncio.run(unified_manager.install_by_id(node_name, version_spec, cmd_ctx.channel, cmd_ctx.mode, instant_execution=True, no_deps=cmd_ctx.no_deps))209 210 if res.action == 'skip':211 print(f"{cnt_msg} [ SKIP ] {node_name:50} => Already installed")212 elif res.action == 'enable':213 print(f"{cnt_msg} [ ENABLED ] {node_name:50}")214 elif res.action == 'install-git' and res.target == 'nightly':215 print(f"{cnt_msg} [INSTALLED] {node_name:50}[NIGHTLY]")216 elif res.action == 'install-git' and res.target == 'unknown':217 print(f"{cnt_msg} [INSTALLED] {node_name:50}[UNKNOWN]")218 elif res.action == 'install-cnr' and res.result:219 print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")220 elif res.action == 'switch-cnr' and res.result:221 print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")222 elif (res.action == 'switch-cnr' or res.action == 'install-cnr') and not res.result and node_name in unified_manager.cnr_map:223 print(f"\nAvailable version of '{node_name}'")224 show_versions(node_name)225 print("")226 else:227 print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")228 229 230def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):231 node_spec = unified_manager.resolve_node_spec(node_spec_str)232 233 node_name, version_spec, _ = node_spec234 235 unified_manager.unified_uninstall(node_name, version_spec == 'unknown')236 install_node(node_name, is_all=is_all, cnt_msg=cnt_msg)237 238 239def fix_node(node_spec_str, is_all=False, cnt_msg=''):240 node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='active')241 242 if node_spec is None:243 if not is_all:244 if unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive') is not None:245 print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Disabled")246 else:247 print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Not installed")248 249 return250 251 node_name, version_spec, _ = node_spec252 253 print(f"{cnt_msg} [ FIXING ]: {node_name:50}[{version_spec}]")254 res = unified_manager.unified_fix(node_name, version_spec, no_deps=cmd_ctx.no_deps)255 256 if not res.result:257 print(f"[bold red]ERROR: f{res.msg}[/bold red]")258 259 260def uninstall_node(node_spec_str: str, is_all: bool = False, cnt_msg: str = ''):261 spec = node_spec_str.split('@')262 if len(spec) == 2 and spec[1] == 'unknown':263 node_name = spec[0]264 is_unknown = True265 else:266 node_name = spec[0]267 is_unknown = False268 269 res = unified_manager.unified_uninstall(node_name, is_unknown)270 if len(spec) == 1 and res.action == 'skip' and not is_unknown:271 res = unified_manager.unified_uninstall(node_name, True)272 273 if res.action == 'skip':274 print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed")275 276 elif res.result:277 print(f"{cnt_msg} [UNINSTALLED] {node_name:50}")278 else:279 print(f"ERROR: An error occurred while uninstalling '{node_name}'.")280 281 282def update_node(node_spec_str, is_all=False, cnt_msg=''):283 node_spec = unified_manager.resolve_node_spec(node_spec_str, 'active')284 285 if node_spec is None:286 if unified_manager.resolve_node_spec(node_spec_str, 'inactive'):287 print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Disabled")288 else:289 print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Not installed")290 return None291 292 node_name, version_spec, _ = node_spec293 294 res = unified_manager.unified_update(node_name, version_spec, no_deps=cmd_ctx.no_deps, return_postinstall=True)295 296 if not res.result:297 print(f"ERROR: An error occurred while updating '{node_name}'.")298 elif res.action == 'skip':299 print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => {res.msg}")300 else:301 print(f"{cnt_msg} [ UPDATED ]: {node_name:50} => ({version_spec} -> {res.target})")302 303 return res.with_target(f'{node_name}@{res.target}')304 305 306def update_parallel(nodes):307 is_all = False308 if 'all' in nodes:309 is_all = True310 nodes = []311 for x in unified_manager.active_nodes.keys():312 nodes.append(x)313 for x in unified_manager.unknown_active_nodes.keys():314 nodes.append(x+"@unknown")315 else:316 nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui']]317 318 total = len(nodes)319 320 lock = threading.Lock()321 processed = []322 323 i = 0324 325 def process_custom_node(x):326 nonlocal i327 nonlocal processed328 329 with lock:330 i += 1331 332 try:333 res = update_node(x, is_all=is_all, cnt_msg=f'{i}/{total}')334 with lock:335 processed.append(res)336 except Exception as e:337 print(f"ERROR: {e}")338 traceback.print_exc()339 340 with concurrent.futures.ThreadPoolExecutor(4) as executor:341 for item in nodes:342 executor.submit(process_custom_node, item)343 344 i = 1345 for res in processed:346 if res is not None:347 print(f"[{i}/{total}] Post update: {res.target}")348 if res.postinstall is not None:349 res.postinstall()350 i += 1351 352 353def update_comfyui():354 res = core.update_path(comfy_path, instant_execution=True)355 if res == 'fail':356 print("Updating ComfyUI has failed.")357 elif res == 'updated':358 print("ComfyUI is updated.")359 else:360 print("ComfyUI is already up to date.")361 362 363def enable_node(node_spec_str, is_all=False, cnt_msg=''):364 if unified_manager.resolve_node_spec(node_spec_str, guess_mode='active') is not None:365 print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Already enabled")366 return367 368 node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive')369 370 if node_spec is None:371 print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Not found")372 return373 374 node_name, version_spec, _ = node_spec375 376 res = unified_manager.unified_enable(node_name, version_spec)377 378 if res.action == 'skip':379 print(f"{cnt_msg} [ SKIP ] {node_name:50} => {res.msg}")380 elif res.result:381 print(f"{cnt_msg} [ENABLED] {node_name:50}")382 else:383 print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")384 385 386def disable_node(node_spec_str: str, is_all=False, cnt_msg=''):387 if 'comfyui-manager' in node_spec_str.lower():388 return389 390 node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='active')391 392 if node_spec is None:393 if unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive') is not None:394 print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Already disabled")395 else:396 print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Not found")397 return398 399 node_name, version_spec, _ = node_spec400 401 res = unified_manager.unified_disable(node_name, version_spec == 'unknown')402 403 if res.action == 'skip':404 print(f"{cnt_msg} [ SKIP ] {node_name:50} => {res.msg}")405 elif res.result:406 print(f"{cnt_msg} [DISABLED] {node_name:50}")407 else:408 print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")409 410 411def show_list(kind, simple=False):412 custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=cmd_ctx.channel, mode=cmd_ctx.mode))413 414 # collect not-installed unknown nodes415 not_installed_unknown_nodes = []416 repo_unknown = {}417 418 for k, v in custom_nodes.items():419 if 'cnr_latest' not in v:420 if len(v['files']) == 1:421 repo_url = v['files'][0]422 node_name = repo_url.split('/')[-1]423 if node_name not in unified_manager.unknown_inactive_nodes and node_name not in unified_manager.unknown_active_nodes:424 not_installed_unknown_nodes.append(v)425 else:426 repo_unknown[node_name] = v427 428 processed = {}429 unknown_processed = []430 431 flag = kind in ['all', 'cnr', 'installed', 'enabled']432 for k, v in unified_manager.active_nodes.items():433 if flag:434 cnr = unified_manager.cnr_map[k]435 processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]436 else:437 processed[k] = None438 439 if flag and kind != 'cnr':440 for k, v in unified_manager.unknown_active_nodes.items():441 item = repo_unknown.get(k)442 443 if item is None:444 continue445 446 log_item = "[ ENABLED ] ", item['title'], k, item['author']447 unknown_processed.append(log_item)448 449 flag = kind in ['all', 'cnr', 'installed', 'disabled']450 for k, v in unified_manager.cnr_inactive_nodes.items():451 if k in processed:452 continue453 454 if flag:455 cnr = unified_manager.cnr_map[k]456 processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))457 else:458 processed[k] = None459 460 for k, v in unified_manager.nightly_inactive_nodes.items():461 if k in processed:462 continue463 464 if flag:465 cnr = unified_manager.cnr_map[k]466 processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'467 else:468 processed[k] = None469 470 if flag and kind != 'cnr':471 for k, v in unified_manager.unknown_inactive_nodes.items():472 item = repo_unknown.get(k)473 474 if item is None:475 continue476 477 log_item = "[ DISABLED ] ", item['title'], k, item['author']478 unknown_processed.append(log_item)479 480 flag = kind in ['all', 'cnr', 'not-installed']481 for k, v in unified_manager.cnr_map.items():482 if k in processed:483 continue484 485 if flag:486 cnr = unified_manager.cnr_map[k]487 ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'488 processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec489 else:490 processed[k] = None491 492 if flag and kind != 'cnr':493 for x in not_installed_unknown_nodes:494 if len(x['files']) == 1:495 node_id = os.path.basename(x['files'][0])496 log_item = "[ NOT INSTALLED ] ", x['title'], node_id, x['author']497 unknown_processed.append(log_item)498 499 for x in processed.values():500 if x is None:501 continue502 503 prefix, title, short_id, author, ver_spec = x504 if simple:505 print(title+'@'+ver_spec)506 else:507 print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) \\[{ver_spec}]")508 509 for x in unknown_processed:510 prefix, title, short_id, author = x511 if simple:512 print(title+'@unknown')513 else:514 print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) [UNKNOWN]")515 516 517async def show_snapshot(simple_mode=False):518 json_obj = await core.get_current_snapshot()519 520 if simple_mode:521 print(f"[{json_obj['comfyui']}] comfyui")522 for k, v in json_obj['git_custom_nodes'].items():523 print(f"[{v['hash']}] {k}")524 for v in json_obj['file_custom_nodes']:525 print(f"[ N/A ] {v['filename']}")526 527 else:528 formatted_json = json.dumps(json_obj, ensure_ascii=False, indent=4)529 print(formatted_json)530 531 532def show_snapshot_list(simple_mode=False):533 snapshot_path = cmd_ctx.get_snapshot_path()534 535 files = os.listdir(snapshot_path)536 json_files = [x for x in files if x.endswith('.json')]537 for x in sorted(json_files):538 print(x)539 540 541def cancel():542 if os.path.exists(cmd_ctx.get_startup_scripts_path()):543 os.remove(cmd_ctx.get_startup_scripts_path())544 545 if os.path.exists(cmd_ctx.get_restore_snapshot_path()):546 os.remove(cmd_ctx.get_restore_snapshot_path())547 548 549async def auto_save_snapshot():550 path = await core.save_snapshot_with_postfix('cli-autosave')551 print(f"Current snapshot is saved as `{path}`")552 553 554def get_all_installed_node_specs():555 res = []556 processed = set()557 for k, v in unified_manager.active_nodes.items():558 node_spec_str = f"{k}@{v[0]}"559 res.append(node_spec_str)560 processed.add(k)561 562 for k in unified_manager.cnr_inactive_nodes.keys():563 if k in processed:564 continue565 566 latest = unified_manager.get_from_cnr_inactive_nodes(k)567 if latest is not None:568 node_spec_str = f"{k}@{str(latest[0])}"569 res.append(node_spec_str)570 571 for k in unified_manager.nightly_inactive_nodes.keys():572 if k in processed:573 continue574 575 node_spec_str = f"{k}@nightly"576 res.append(node_spec_str)577 578 for k in unified_manager.unknown_active_nodes.keys():579 node_spec_str = f"{k}@unknown"580 res.append(node_spec_str)581 582 for k in unified_manager.unknown_inactive_nodes.keys():583 node_spec_str = f"{k}@unknown"584 res.append(node_spec_str)585 586 return res587 588 589def for_each_nodes(nodes, act, allow_all=True):590 is_all = False591 if allow_all and 'all' in nodes:592 is_all = True593 nodes = get_all_installed_node_specs()594 else:595 nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]596 597 total = len(nodes)598 i = 1599 for x in nodes:600 try:601 act(x, is_all=is_all, cnt_msg=f'{i}/{total}')602 except Exception as e:603 print(f"ERROR: {e}")604 traceback.print_exc()605 i += 1606 607 608app = typer.Typer()609 610 611@app.command(help="Display help for commands")612def help(ctx: typer.Context):613 print(ctx.find_root().get_help())614 ctx.exit(0)615 616 617@app.command(help="Install custom nodes")618def install(619 nodes: List[str] = typer.Argument(620 ..., help="List of custom nodes to install"621 ),622 channel: Annotated[623 str,624 typer.Option(625 show_default=False,626 help="Specify the operation mode"627 ),628 ] = None,629 mode: str = typer.Option(630 None,631 help="[remote|local|cache]"632 ),633 no_deps: Annotated[634 Optional[bool],635 typer.Option(636 "--no-deps",637 show_default=False,638 help="Skip installing any Python dependencies",639 ),640 ] = False,641 user_directory: str = typer.Option(642 None,643 help="user directory"644 ),645):646 cmd_ctx.set_user_directory(user_directory)647 cmd_ctx.set_channel_mode(channel, mode)648 cmd_ctx.set_no_deps(no_deps)649 650 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)651 for_each_nodes(nodes, act=install_node)652 pip_fixer.fix_broken()653 654 655@app.command(help="Reinstall custom nodes")656def reinstall(657 nodes: List[str] = typer.Argument(658 ..., help="List of custom nodes to reinstall"659 ),660 channel: Annotated[661 str,662 typer.Option(663 show_default=False,664 help="Specify the operation mode"665 ),666 ] = None,667 mode: str = typer.Option(668 None,669 help="[remote|local|cache]"670 ),671 no_deps: Annotated[672 Optional[bool],673 typer.Option(674 "--no-deps",675 show_default=False,676 help="Skip installing any Python dependencies",677 ),678 ] = False,679 user_directory: str = typer.Option(680 None,681 help="user directory"682 ),683):684 cmd_ctx.set_user_directory(user_directory)685 cmd_ctx.set_channel_mode(channel, mode)686 cmd_ctx.set_no_deps(no_deps)687 688 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)689 for_each_nodes(nodes, act=reinstall_node)690 pip_fixer.fix_broken()691 692 693@app.command(help="Uninstall custom nodes")694def uninstall(695 nodes: List[str] = typer.Argument(696 ..., help="List of custom nodes to uninstall"697 ),698 channel: Annotated[699 str,700 typer.Option(701 show_default=False,702 help="Specify the operation mode"703 ),704 ] = None,705 mode: str = typer.Option(706 None,707 help="[remote|local|cache]"708 ),709):710 cmd_ctx.set_channel_mode(channel, mode)711 for_each_nodes(nodes, act=uninstall_node)712 713 714@app.command(help="Update custom nodes")715def update(716 nodes: List[str] = typer.Argument(717 ...,718 help="[all|List of custom nodes to update]"719 ),720 channel: Annotated[721 str,722 typer.Option(723 show_default=False,724 help="Specify the operation mode"725 ),726 ] = None,727 mode: str = typer.Option(728 None,729 help="[remote|local|cache]"730 ),731 user_directory: str = typer.Option(732 None,733 help="user directory"734 ),735):736 cmd_ctx.set_user_directory(user_directory)737 cmd_ctx.set_channel_mode(channel, mode)738 739 if 'all' in nodes:740 asyncio.run(auto_save_snapshot())741 742 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)743 744 for x in nodes:745 if x.lower() in ['comfyui', 'comfy', 'all']:746 update_comfyui()747 break748 749 update_parallel(nodes)750 pip_fixer.fix_broken()751 752 753@app.command(help="Disable custom nodes")754def disable(755 nodes: List[str] = typer.Argument(756 ...,757 help="[all|List of custom nodes to disable]"758 ),759 channel: Annotated[760 str,761 typer.Option(762 show_default=False,763 help="Specify the operation mode"764 ),765 ] = None,766 mode: str = typer.Option(767 None,768 help="[remote|local|cache]"769 ),770 user_directory: str = typer.Option(771 None,772 help="user directory"773 ),774):775 cmd_ctx.set_user_directory(user_directory)776 cmd_ctx.set_channel_mode(channel, mode)777 778 if 'all' in nodes:779 asyncio.run(auto_save_snapshot())780 781 for_each_nodes(nodes, disable_node, allow_all=True)782 783 784@app.command(help="Enable custom nodes")785def enable(786 nodes: List[str] = typer.Argument(787 ...,788 help="[all|List of custom nodes to enable]"789 ),790 channel: Annotated[791 str,792 typer.Option(793 show_default=False,794 help="Specify the operation mode"795 ),796 ] = None,797 mode: str = typer.Option(798 None,799 help="[remote|local|cache]"800 ),801 user_directory: str = typer.Option(802 None,803 help="user directory"804 ),805):806 cmd_ctx.set_user_directory(user_directory)807 cmd_ctx.set_channel_mode(channel, mode)808 809 if 'all' in nodes:810 asyncio.run(auto_save_snapshot())811 812 for_each_nodes(nodes, enable_node, allow_all=True)813 814 815@app.command(help="Fix dependencies of custom nodes")816def fix(817 nodes: List[str] = typer.Argument(818 ...,819 help="[all|List of custom nodes to fix]"820 ),821 channel: Annotated[822 str,823 typer.Option(824 show_default=False,825 help="Specify the operation mode"826 ),827 ] = None,828 mode: str = typer.Option(829 None,830 help="[remote|local|cache]"831 ),832 user_directory: str = typer.Option(833 None,834 help="user directory"835 ),836):837 cmd_ctx.set_user_directory(user_directory)838 cmd_ctx.set_channel_mode(channel, mode)839 840 if 'all' in nodes:841 asyncio.run(auto_save_snapshot())842 843 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)844 for_each_nodes(nodes, fix_node, allow_all=True)845 pip_fixer.fix_broken()846 847 848@app.command("show-versions", help="Show all available versions of the node")849def show_versions(node_name: str):850 versions = cnr_utils.all_versions_of_node(node_name)851 if versions is None:852 print(f"Node not found in Comfy Registry: {node_name}")853 854 for x in versions:855 print(f"[{x['createdAt'][:10]}] {x['version']} -- {x['changelog']}")856 857 858@app.command("show", help="Show node list")859def show(860 arg: str = typer.Argument(861 help="[installed|enabled|not-installed|disabled|all|cnr|snapshot|snapshot-list]"862 ),863 channel: Annotated[864 str,865 typer.Option(866 show_default=False,867 help="Specify the operation mode"868 ),869 ] = None,870 mode: str = typer.Option(871 None,872 help="[remote|local|cache]"873 ),874 user_directory: str = typer.Option(875 None,876 help="user directory"877 ),878):879 valid_commands = [880 "installed",881 "enabled",882 "not-installed",883 "disabled",884 "all",885 "cnr",886 "snapshot",887 "snapshot-list",888 ]889 if arg not in valid_commands:890 typer.echo(f"Invalid command: `show {arg}`", err=True)891 exit(1)892 893 cmd_ctx.set_user_directory(user_directory)894 cmd_ctx.set_channel_mode(channel, mode)895 if arg == 'snapshot':896 show_snapshot()897 elif arg == 'snapshot-list':898 show_snapshot_list()899 else:900 show_list(arg)901 902 903@app.command("simple-show", help="Show node list (simple mode)")904def simple_show(905 arg: str = typer.Argument(906 help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]"907 ),908 channel: Annotated[909 str,910 typer.Option(911 show_default=False,912 help="Specify the operation mode"913 ),914 ] = None,915 mode: str = typer.Option(916 None,917 help="[remote|local|cache]"918 ),919 user_directory: str = typer.Option(920 None,921 help="user directory"922 ),923):924 valid_commands = [925 "installed",926 "enabled",927 "not-installed",928 "disabled",929 "all",930 "snapshot",931 "snapshot-list",932 ]933 if arg not in valid_commands:934 typer.echo(f"[bold red]Invalid command: `show {arg}`[/bold red]", err=True)935 exit(1)936 937 cmd_ctx.set_user_directory(user_directory)938 cmd_ctx.set_channel_mode(channel, mode)939 940 if arg == 'snapshot':941 show_snapshot(True)942 elif arg == 'snapshot-list':943 show_snapshot_list(True)944 else:945 show_list(arg, True)946 947 948@app.command('cli-only-mode', help="Set whether to use ComfyUI-Manager in CLI-only mode.")949def cli_only_mode(950 mode: str = typer.Argument(951 ..., help="[enable|disable]"952 ),953 user_directory: str = typer.Option(954 None,955 help="user directory"956 )957):958 cmd_ctx.set_user_directory(user_directory)959 cli_mode_flag = os.path.join(cmd_ctx.manager_files_directory, '.enable-cli-only-mode')960 961 if mode.lower() == 'enable':962 with open(cli_mode_flag, 'w'):963 pass964 print("\nINFO: `cli-only-mode` is enabled\n")965 elif mode.lower() == 'disable':966 if os.path.exists(cli_mode_flag):967 os.remove(cli_mode_flag)968 print("\nINFO: `cli-only-mode` is disabled\n")969 else:970 print(f"\n[bold red]Invalid value for cli-only-mode: {mode}[/bold red]\n")971 exit(1)972 973 974@app.command(975 "deps-in-workflow", help="Generate dependencies file from workflow (.json/.png)"976)977def deps_in_workflow(978 workflow: Annotated[979 str, typer.Option(show_default=False, help="Workflow file (.json/.png)")980 ],981 output: Annotated[982 str, typer.Option(show_default=False, help="Output file (.json)")983 ],984 channel: Annotated[985 str,986 typer.Option(987 show_default=False,988 help="Specify the operation mode"989 ),990 ] = None,991 mode: str = typer.Option(992 None,993 help="[remote|local|cache]"994 ),995 user_directory: str = typer.Option(996 None,997 help="user directory"998 )999):1000 cmd_ctx.set_user_directory(user_directory)1001 cmd_ctx.set_channel_mode(channel, mode)1002 1003 input_path = workflow1004 output_path = output1005 1006 if not os.path.exists(input_path):1007 print(f"[bold red]File not found: {input_path}[/bold red]")1008 exit(1)1009 1010 used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=cmd_ctx.mode, channel_url=cmd_ctx.channel))1011 1012 custom_nodes = {}1013 for x in used_exts:1014 custom_nodes[x] = {'state': core.simple_check_custom_node(x),1015 'hash': '-'1016 }1017 1018 res = {1019 'custom_nodes': custom_nodes,1020 'unknown_nodes': list(unknown_nodes)1021 }1022 1023 with open(output_path, "w", encoding='utf-8') as output_file:1024 json.dump(res, output_file, indent=4)1025 1026 print(f"Workflow dependencies are being saved into {output_path}.")1027 1028 1029@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.")1030def save_snapshot(1031 output: Annotated[1032 str,1033 typer.Option(1034 show_default=False, help="Specify the output file path. (.json/.yaml)"1035 ),1036 ] = None,1037 user_directory: str = typer.Option(1038 None,1039 help="user directory"1040 ),1041 full_snapshot: Annotated[1042 bool,1043 typer.Option(1044 show_default=False, help="If the snapshot should include custom node, ComfyUI version and pip versions (default), or only custom node details"1045 ),1046 ] = True,1047):1048 cmd_ctx.set_user_directory(user_directory)1049 1050 if output is None:1051 print("[bold red]ERROR: missing output path[/bold red]")1052 raise typer.Exit(code=1)1053 1054 if(not output.endswith('.json') and not output.endswith('.yaml')):1055 print("[bold red]ERROR: output path should be either '.json' or '.yaml' file.[/bold red]")1056 raise typer.Exit(code=1)1057 1058 dir_path = os.path.dirname(output)1059 if(dir_path != '' and not os.path.exists(dir_path)):1060 print(f"[bold red]ERROR: {output} path not exists.[/bold red]")1061 raise typer.Exit(code=1)1062 1063 path = asyncio.run(core.save_snapshot_with_postfix('snapshot', output, not full_snapshot))1064 print(f"Current snapshot is saved as `{path}`")1065 1066 1067@app.command("restore-snapshot", help="Restore snapshot from snapshot file")1068def restore_snapshot(1069 snapshot_name: str, 1070 pip_non_url: Optional[bool] = typer.Option(1071 default=None,1072 show_default=False,1073 is_flag=True,1074 help="Restore for pip packages registered on PyPI.",1075 ),1076 pip_non_local_url: Optional[bool] = typer.Option(1077 default=None,1078 show_default=False,1079 is_flag=True,1080 help="Restore for pip packages registered at web URLs.",1081 ),1082 pip_local_url: Optional[bool] = typer.Option(1083 default=None,1084 show_default=False,1085 is_flag=True,1086 help="Restore for pip packages specified by local paths.",1087 ),1088 user_directory: str = typer.Option(1089 None,1090 help="user directory"1091 ),1092 restore_to: Optional[str] = typer.Option(1093 None,1094 help="Manually specify the installation path for the custom node. Ignore user directory."1095 )1096):1097 cmd_ctx.set_user_directory(user_directory)1098 1099 if restore_to:1100 cmd_ctx.update_custom_nodes_dir(restore_to)1101 1102 extras = []1103 if pip_non_url:1104 extras.append('--pip-non-url')1105 1106 if pip_non_local_url:1107 extras.append('--pip-non-local-url')1108 1109 if pip_local_url:1110 extras.append('--pip-local-url')1111 1112 print(f"PIPs restore mode: {extras}")1113 1114 if os.path.exists(snapshot_name):1115 snapshot_path = os.path.abspath(snapshot_name)1116 else:1117 snapshot_path = os.path.join(cmd_ctx.get_snapshot_path(), snapshot_name)1118 if not os.path.exists(snapshot_path):1119 print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")1120 exit(1)1121 1122 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)1123 try:1124 asyncio.run(core.restore_snapshot(snapshot_path, extras))1125 except Exception:1126 print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")1127 traceback.print_exc()1128 raise typer.Exit(code=1)1129 pip_fixer.fix_broken()1130 1131 1132@app.command(1133 "restore-dependencies", help="Restore dependencies from whole installed custom nodes."1134)1135def restore_dependencies(1136 user_directory: str = typer.Option(1137 None,1138 help="user directory"1139 )1140):1141 cmd_ctx.set_user_directory(user_directory)1142 1143 node_paths = []1144 1145 for base_path in cmd_ctx.get_custom_nodes_paths():1146 for name in os.listdir(base_path):1147 target = os.path.join(base_path, name)1148 if os.path.isdir(target) and not name.endswith('.disabled'):1149 node_paths.append(target)1150 1151 total = len(node_paths)1152 i = 11153 1154 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)1155 for x in node_paths:1156 print("----------------------------------------------------------------------------------------------------")1157 print(f"Restoring [{i}/{total}]: {x}")1158 unified_manager.execute_install_script('', x, instant_execution=True)1159 i += 11160 pip_fixer.fix_broken()1161 1162 1163@app.command(1164 "post-install", help="Install dependencies and execute installation script"1165)1166def post_install(1167 path: str = typer.Argument(1168 help="path to custom node",1169 )1170):1171 path = os.path.expanduser(path)1172 1173 pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)1174 unified_manager.execute_install_script('', path, instant_execution=True)1175 pip_fixer.fix_broken()1176 1177 1178@app.command(1179 "install-deps",1180 help="Install dependencies from dependencies file(.json) or workflow(.png/.json)",1181)1182def install_deps(1183 deps: str = typer.Argument(1184 help="Dependency spec file (.json)",1185 ),1186 channel: Annotated[1187 str,1188 typer.Option(1189 show_default=False,1190 help="Specify the operation mode"1191 ),1192 ] = None,1193 mode: str = typer.Option(1194 None,1195 help="[remote|local|cache]"1196 ),1197 user_directory: str = typer.Option(1198 None,1199 help="user directory"1200 ),