support CNR

This commit is contained in:
Dr.Lt.Data 2024-07-25 00:24:58 +09:00
parent 3c2eb84602
commit b3be556837
11 changed files with 2858 additions and 878 deletions

660
cm-cli.py
View File

@ -4,9 +4,9 @@ import traceback
import json import json
import asyncio import asyncio
import subprocess import subprocess
import shutil
import concurrent import concurrent
import threading import threading
import yaml
from typing import Optional from typing import Optional
import typer import typer
@ -17,10 +17,12 @@ import git
sys.path.append(os.path.dirname(__file__)) sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), "glob")) sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_core as core
import cm_global import cm_global
import manager_core as core
from manager_core import unified_manager
import cnr_utils
comfyui_manager_path = os.path.dirname(__file__) comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
comfy_path = os.environ.get('COMFYUI_PATH') comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None: if comfy_path is None:
@ -78,9 +80,7 @@ read_downgrade_blacklist() # This is a preparation step for manager_core
class Ctx: class Ctx:
def __init__(self): def __init__(self):
self.channel = 'default' self.channel = 'default'
self.mode = 'remote' self.mode = 'cache'
self.processed_install = set()
self.custom_node_map_cache = None
def set_channel_mode(self, channel, mode): def set_channel_mode(self, channel, mode):
if mode is not None: if mode is not None:
@ -97,196 +97,143 @@ class Ctx:
if channel is not None: if channel is not None:
self.channel = channel self.channel = channel
def post_install(self, url): asyncio.run(unified_manager.reload(cache_mode=self.mode == 'cache'))
try: asyncio.run(unified_manager.load_nightly(self.channel, self.mode))
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() channel_ctx = Ctx()
def install_node(node_name, is_all=False, cnt_msg=''): def install_node(node_spec_str, is_all=False, cnt_msg=''):
if core.is_valid_url(node_name): if core.is_valid_url(node_spec_str):
# install via urls # install via urls
res = core.gitclone_install([node_name]) res = asyncio.run(core.gitclone_install(node_spec_str))
if not res: if not res.result:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.[/bold red]") print(res.msg)
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
else: else:
print(f"{cnt_msg} [INSTALLED] {node_name:50}") print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
else: else:
node_path, node_item = cm_ctx.lookup_node_path(node_name) node_spec = unified_manager.resolve_node_spec(node_spec_str)
if os.path.exists(node_path): if node_spec is None:
if not is_all: return
print(f"{cnt_msg} [ SKIPPED ] {node_name:50} => Already installed")
elif os.path.exists(node_path + '.disabled'): node_name, version_spec, is_specified = node_spec
enable_node(node_name)
# NOTE: install node doesn't allow update if version is not specified
if not is_specified:
version_spec = None
res = asyncio.run(unified_manager.install_by_id(node_name, version_spec, channel_ctx.channel, channel_ctx.mode, instant_execution=True))
if res.action == 'skip':
print(f"{cnt_msg} [ SKIP ] {node_name:50} => Already installed")
elif res.action == 'enable':
print(f"{cnt_msg} [ ENABLED ] {node_name:50}")
elif res.action == 'install-git' and res.target == 'nightly':
print(f"{cnt_msg} [INSTALLED] {node_name:50}[NIGHTLY]")
elif res.action == 'install-git' and res.target == 'unknown':
print(f"{cnt_msg} [INSTALLED] {node_name:50}[UNKNOWN]")
elif res.action == 'install-cnr' and res.result:
print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")
elif res.action == 'switch-cnr' and res.result:
print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")
elif (res.action == 'switch-cnr' or res.action == 'install-cnr') and not res.result and node_name in unified_manager.cnr_map:
print(f"\nAvailable version of '{node_name}'")
show_versions(node_name)
print("")
else: else:
res = core.gitclone_install(node_item['files'], instant_execution=True, msg_prefix=f"[{cnt_msg}] ") print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
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=''): def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name) node_spec = unified_manager.resolve_node_spec(node_spec_str)
if os.path.exists(node_path): node_name, version_spec, _ = node_spec
shutil.rmtree(node_path)
if os.path.exists(node_path + '.disabled'):
shutil.rmtree(node_path + '.disabled')
unified_manager.unified_uninstall(node_name, version_spec == 'unknown')
install_node(node_name, is_all=is_all, cnt_msg=cnt_msg) install_node(node_name, is_all=is_all, cnt_msg=cnt_msg)
def fix_node(node_name, is_all=False, cnt_msg=''): def fix_node(node_spec_str, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True) node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='active')
files = node_item['files'] if node_item is not None else [node_path] if node_spec is None:
if not is_all:
if unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive') is not None:
print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Disabled")
else:
print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Not installed")
if os.path.exists(node_path): return
print(f"{cnt_msg} [ FIXING ]: {node_name:50} => Disabled")
res = core.gitclone_fix(files, instant_execution=True) node_name, version_spec, _ = node_spec
if not res:
print(f"ERROR: An error occurred while fixing '{node_name}'.") print(f"{cnt_msg} [ FIXING ]: {node_name:50}[{version_spec}]")
elif not is_all and os.path.exists(node_path + '.disabled'): res = unified_manager.unified_fix(node_name, version_spec)
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Disabled")
elif not is_all: if not res.result:
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed") print(f"ERROR: f{res.msg}")
def uninstall_node(node_name, is_all=False, cnt_msg=''): def uninstall_node(node_spec_str, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True) spec = node_spec_str.split('@')
if len(spec) == 2 and spec[1] == 'unknown':
files = node_item['files'] if node_item is not None else [node_path] node_name = spec[0]
is_unknown = True
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: else:
node_name = spec[0]
is_unknown = False
res = unified_manager.unified_uninstall(node_name, is_unknown)
if len(spec) == 1 and res.action == 'skip' and not is_unknown:
res = unified_manager.unified_uninstall(node_name, True)
if res.action == 'skip':
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed") print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed")
elif res.result:
print(f"{cnt_msg} [UNINSTALLED] {node_name:50}")
else:
print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
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] def update_node(node_spec_str, is_all=False, cnt_msg=''):
node_spec = unified_manager.resolve_node_spec(node_spec_str, 'active')
res = core.gitclone_update(files, skip_script=True, msg_prefix=f"[{cnt_msg}] ") if node_spec is None:
if unified_manager.resolve_node_spec(node_spec_str, 'inactive'):
if not res: print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Disabled")
print(f"ERROR: An error occurred while updating '{node_name}'.") else:
print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Not installed")
return None return None
return node_path node_name, version_spec, _ = node_spec
res = unified_manager.unified_update(node_name, version_spec, return_postinstall=True)
if not res.result:
print(f"ERROR: An error occurred while updating '{node_name}'.")
elif res.action == 'skip':
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => {res.msg}")
else:
print(f"{cnt_msg} [ UPDATED ]: {node_name:50} => ({version_spec} -> {res.target})")
return res.with_target(f'{node_name}@{res.target}')
def update_parallel(nodes): def update_parallel(nodes):
is_all = False is_all = False
if 'all' in nodes: if 'all' in nodes:
is_all = True 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 = []
for x in unified_manager.active_nodes.keys():
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']] nodes.append(x)
for x in unified_manager.unknown_active_nodes.keys():
nodes.append(x+"@unknown")
else:
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui']]
total = len(nodes) total = len(nodes)
@ -303,9 +250,9 @@ def update_parallel(nodes):
i += 1 i += 1
try: try:
node_path = update_node(x, is_all=is_all, cnt_msg=f'{i}/{total}') res = update_node(x, is_all=is_all, cnt_msg=f'{i}/{total}')
with lock: with lock:
processed.append(node_path) processed.append(res)
except Exception as e: except Exception as e:
print(f"ERROR: {e}") print(f"ERROR: {e}")
traceback.print_exc() traceback.print_exc()
@ -315,12 +262,11 @@ def update_parallel(nodes):
executor.submit(process_custom_node, item) executor.submit(process_custom_node, item)
i = 1 i = 1
for node_path in processed: for res in processed:
if node_path is None: if res is not None:
print(f"[{i}/{total}] Post update: ERROR") print(f"[{i}/{total}] Post update: {res.target}")
else: if res.postinstall is not None:
print(f"[{i}/{total}] Post update: {node_path}") res.postinstall()
cm_ctx.post_install(node_path)
i += 1 i += 1
@ -334,100 +280,158 @@ def update_comfyui():
print("ComfyUI is already up to date.") print("ComfyUI is already up to date.")
def enable_node(node_name, is_all=False, cnt_msg=''): def enable_node(node_spec_str, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager': if unified_manager.resolve_node_spec(node_spec_str, guess_mode='active') is not None:
print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Already enabled")
return return
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True) node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive')
if os.path.exists(node_path + '.disabled'): if node_spec is None:
current_name = node_path + '.disabled' print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Not found")
os.rename(current_name, node_path) return
node_name, version_spec, _ = node_spec
res = unified_manager.unified_enable(node_name, version_spec)
if res.action == 'skip':
print(f"{cnt_msg} [ SKIP ] {node_name:50} => {res.msg}")
elif res.result:
print(f"{cnt_msg} [ENABLED] {node_name:50}") print(f"{cnt_msg} [ENABLED] {node_name:50}")
elif os.path.exists(node_path): else:
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Already enabled") print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")
elif not is_all:
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Not installed")
def disable_node(node_name, is_all=False, cnt_msg=''): def disable_node(node_spec_str: str, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager': if 'comfyui-manager' in node_spec_str.lower():
return return
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True) node_spec = unified_manager.resolve_node_spec(node_spec_str, guess_mode='active')
if os.path.exists(node_path): if node_spec is None:
current_name = node_path if unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive') is not None:
new_name = node_path + '.disabled' print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Already disabled")
os.rename(current_name, new_name) else:
print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Not found")
return
node_name, version_spec, _ = node_spec
res = unified_manager.unified_disable(node_name, version_spec == 'unknown')
if res.action == 'skip':
print(f"{cnt_msg} [ SKIP ] {node_name:50} => {res.msg}")
elif res.result:
print(f"{cnt_msg} [DISABLED] {node_name:50}") print(f"{cnt_msg} [DISABLED] {node_name:50}")
elif os.path.exists(node_path + '.disabled'): else:
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Already disabled") print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")
elif not is_all:
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Not installed")
def show_list(kind, simple=False): def show_list(kind, simple=False):
for k, v in cm_ctx.get_custom_node_map().items(): custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=channel_ctx.channel, mode=channel_ctx.mode))
if v[1]:
continue
node_path = os.path.join(custom_nodes_path, k) # collect not-installed unknown nodes
not_installed_unknown_nodes = []
repo_unknown = {}
states = set() for k, v in custom_nodes.items():
if os.path.exists(node_path): if 'cnr_latest' not in v:
prefix = '[ ENABLED ] ' if len(v['files']) == 1:
states.add('installed') repo_url = v['files'][0]
states.add('enabled') node_name = repo_url.split('/')[-1]
states.add('all') if node_name not in unified_manager.unknown_inactive_nodes and node_name not in unified_manager.unknown_active_nodes:
elif os.path.exists(node_path + '.disabled'): not_installed_unknown_nodes.append(v)
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: else:
print(f"{prefix} {k:50} {'':20} (author: N/A)") repo_unknown[node_name] = v
processed = {}
unknown_processed = []
flag = kind in ['all', 'cnr', 'installed', 'enabled']
for k, v in unified_manager.active_nodes.items():
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
else:
processed[k] = None
if flag and kind != 'cnr':
for k, v in unified_manager.unknown_active_nodes.items():
item = repo_unknown.get(k)
if item is None:
continue
log_item = "[ ENABLED ] ", item['title'], k, item['author']
unknown_processed.append(log_item)
flag = kind in ['all', 'cnr', 'installed', 'disabled']
for k, v in unified_manager.cnr_inactive_nodes.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
else:
processed[k] = None
for k, v in unified_manager.nightly_inactive_nodes.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
else:
processed[k] = None
if flag and kind != 'cnr':
for k, v in unified_manager.unknown_inactive_nodes.items():
item = repo_unknown.get(k)
if item is None:
continue
log_item = "[ DISABLED ] ", item['title'], k, item['author']
unknown_processed.append(log_item)
flag = kind in ['all', 'cnr', 'not-installed']
for k, v in unified_manager.cnr_map.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map[k]
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
else:
processed[k] = None
if flag and kind != 'cnr':
for x in not_installed_unknown_nodes:
if len(x['files']) == 1:
node_id = os.path.basename(x['files'][0])
log_item = "[ NOT INSTALLED ] ", x['title'], node_id, x['author']
unknown_processed.append(log_item)
for x in processed.values():
if x is None:
continue
prefix, title, short_id, author, ver_spec = x
if simple:
print(title+'@'+ver_spec)
else:
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) \\[{ver_spec}]")
for x in unknown_processed:
prefix, title, short_id, author = x
if simple:
print(title+'@unknown')
else:
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) [UNKNOWN]")
def show_snapshot(simple_mode=False): def show_snapshot(simple_mode=False):
@ -467,13 +471,48 @@ def auto_save_snapshot():
print(f"Current snapshot is saved as `{path}`") print(f"Current snapshot is saved as `{path}`")
def get_all_installed_node_specs():
res = []
processed = set()
for k, v in unified_manager.active_nodes.items():
node_spec_str = f"{k}@{v[0]}"
res.append(node_spec_str)
processed.add(k)
for k, _ in unified_manager.cnr_inactive_nodes.keys():
if k in processed:
continue
latest = unified_manager.get_from_cnr_inactive_nodes(k)
if latest is not None:
node_spec_str = f"{k}@{latest}"
res.append(node_spec_str)
for k, _ in unified_manager.nightly_inactive_nodes.keys():
if k in processed:
continue
node_spec_str = f"{k}@nightly"
res.append(node_spec_str)
for k in unified_manager.unknown_active_nodes.keys():
node_spec_str = f"{k}@unknown"
res.append(node_spec_str)
for k in unified_manager.unknown_inactive_nodes.keys():
node_spec_str = f"{k}@unknown"
res.append(node_spec_str)
return res
def for_each_nodes(nodes, act, allow_all=True): def for_each_nodes(nodes, act, allow_all=True):
is_all = False is_all = False
if allow_all and 'all' in nodes: if allow_all and 'all' in nodes:
is_all = True 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 = get_all_installed_node_specs()
else:
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']] nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]
total = len(nodes) total = len(nodes)
i = 1 i = 1
@ -510,9 +549,9 @@ def install(
mode: str = typer.Option( mode: str = typer.Option(
None, None,
help="[remote|local|cache]" help="[remote|local|cache]"
), )
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=install_node) for_each_nodes(nodes, act=install_node)
@ -533,7 +572,7 @@ def reinstall(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=reinstall_node) for_each_nodes(nodes, act=reinstall_node)
@ -554,7 +593,7 @@ def uninstall(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
for_each_nodes(nodes, act=uninstall_node) for_each_nodes(nodes, act=uninstall_node)
@ -576,7 +615,7 @@ def update(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes: if 'all' in nodes:
auto_save_snapshot() auto_save_snapshot()
@ -607,7 +646,7 @@ def disable(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes: if 'all' in nodes:
auto_save_snapshot() auto_save_snapshot()
@ -633,7 +672,7 @@ def enable(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes: if 'all' in nodes:
auto_save_snapshot() auto_save_snapshot()
@ -659,7 +698,7 @@ def fix(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes: if 'all' in nodes:
auto_save_snapshot() auto_save_snapshot()
@ -667,10 +706,20 @@ def fix(
for_each_nodes(nodes, fix_node, allow_all=True) for_each_nodes(nodes, fix_node, allow_all=True)
@app.command("show", help="Show node list (simple mode)") @app.command("show-versions", help="Show all available versions of the node")
def show_versions(node_name: str):
versions = cnr_utils.all_versions_of_node(node_name)
if versions is None:
print(f"Node not found in Comfy Registry: {node_name}")
for x in versions:
print(f"[{x['createdAt'][:10]}] {x['version']} -- {x['changelog']}")
@app.command("show", help="Show node list")
def show( def show(
arg: str = typer.Argument( arg: str = typer.Argument(
help="[installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]" help="[installed|enabled|not-installed|disabled|all|cnr|snapshot|snapshot-list]"
), ),
channel: Annotated[ channel: Annotated[
str, str,
@ -690,6 +739,7 @@ def show(
"not-installed", "not-installed",
"disabled", "disabled",
"all", "all",
"cnr",
"snapshot", "snapshot",
"snapshot-list", "snapshot-list",
] ]
@ -697,7 +747,7 @@ def show(
typer.echo(f"Invalid command: `show {arg}`", err=True) typer.echo(f"Invalid command: `show {arg}`", err=True)
exit(1) exit(1)
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot': if arg == 'snapshot':
show_snapshot() show_snapshot()
elif arg == 'snapshot-list': elif arg == 'snapshot-list':
@ -736,7 +786,7 @@ def simple_show(
typer.echo(f"[bold red]Invalid command: `show {arg}`[/bold red]", err=True) typer.echo(f"[bold red]Invalid command: `show {arg}`[/bold red]", err=True)
exit(1) exit(1)
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot': if arg == 'snapshot':
show_snapshot(True) show_snapshot(True)
elif arg == 'snapshot-list': elif arg == 'snapshot-list':
@ -786,7 +836,7 @@ def deps_in_workflow(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
input_path = workflow input_path = workflow
output_path = output output_path = output
@ -795,7 +845,7 @@ def deps_in_workflow(
print(f"[bold red]File not found: {input_path}[/bold red]") print(f"[bold red]File not found: {input_path}[/bold red]")
exit(1) exit(1)
used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=cm_ctx.mode, channel_url=cm_ctx.channel)) used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=channel_ctx.mode, channel_url=channel_ctx.channel))
custom_nodes = {} custom_nodes = {}
for x in used_exts: for x in used_exts:
@ -870,53 +920,7 @@ def restore_snapshot(
exit(1) exit(1)
try: try:
cloned_repos = [] asyncio.run(core.restore_snapshot(snapshot_path, extras))
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: except Exception:
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]") print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
traceback.print_exc() traceback.print_exc()
@ -935,7 +939,7 @@ def restore_dependencies():
for x in node_paths: for x in node_paths:
print(f"----------------------------------------------------------------------------------------------------") print(f"----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}") print(f"Restoring [{i}/{total}]: {x}")
cm_ctx.post_install(x) unified_manager.execute_install_script('', x, instant_execution=True)
i += 1 i += 1
@ -947,7 +951,7 @@ def post_install(
help="path to custom node", help="path to custom node",
)): )):
path = os.path.expanduser(path) path = os.path.expanduser(path)
cm_ctx.post_install(path) unified_manager.execute_install_script('', path, instant_execution=True)
@app.command( @app.command(
@ -970,7 +974,7 @@ def install_deps(
help="[remote|local|cache]" help="[remote|local|cache]"
), ),
): ):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
auto_save_snapshot() auto_save_snapshot()
if not os.path.exists(deps): if not os.path.exists(deps):
@ -989,7 +993,7 @@ def install_deps(
if state == 'installed': if state == 'installed':
continue continue
elif state == 'not-installed': elif state == 'not-installed':
core.gitclone_install([k], instant_execution=True) asyncio.run(core.gitclone_install(k, instant_execution=True))
else: # disabled else: # disabled
core.gitclone_set_active([k], False) core.gitclone_set_active([k], False)
@ -1015,15 +1019,35 @@ def export_custom_node_ids(
None, None,
help="[remote|local|cache]" help="[remote|local|cache]"
)): )):
cm_ctx.set_channel_mode(channel, mode) channel_ctx.set_channel_mode(channel, mode)
with open(path, "w", encoding='utf-8') as output_file: with open(path, "w", encoding='utf-8') as output_file:
for x in cm_ctx.get_custom_node_map().keys(): for x in unified_manager.cnr_map.keys():
print(x, file=output_file) print(x, file=output_file)
custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=channel_ctx.channel, mode=channel_ctx.mode))
for x in custom_nodes.values():
if 'cnr_latest' not in x:
if len(x['files']) == 1:
repo_url = x['files'][0]
node_id = repo_url.split('/')[-1]
print(f"{node_id}@unknown", file=output_file)
if 'id' in x:
print(f"{x['id']}@unknown", file=output_file)
@app.command(
"migrate",
help="Migrate legacy node system to new node system",
)
def migrate():
asyncio.run(unified_manager.migrate_unmanaged_nodes())
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app()) sys.exit(app())
print(f"") print(f"")

2
cm-cli.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
python cm-cli.py $*

View File

@ -5,13 +5,20 @@ import traceback
import git import git
import configparser import configparser
import re
import json import json
import yaml import yaml
from torchvision.datasets.utils import download_url from torchvision.datasets.utils import download_url
from tqdm.auto import tqdm from tqdm.auto import tqdm
from git.remote import RemoteProgress from git.remote import RemoteProgress
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(os.path.dirname(__file__), '..', '..'))
config_path = os.path.join(os.path.dirname(__file__), "config.ini") config_path = os.path.join(os.path.dirname(__file__), "config.ini")
nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json") nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
working_directory = os.getcwd() working_directory = os.getcwd()
@ -35,9 +42,11 @@ class GitProgress(RemoteProgress):
self.pbar.refresh() self.pbar.refresh()
def gitclone(custom_nodes_path, url, target_hash=None): def gitclone(custom_nodes_path, url, target_hash=None, repo_path=None):
repo_name = os.path.splitext(os.path.basename(url))[0] repo_name = os.path.splitext(os.path.basename(url))[0]
repo_path = os.path.join(custom_nodes_path, repo_name)
if repo_path is None:
repo_path = os.path.join(custom_nodes_path, repo_name)
# Clone the repository from the remote URL # Clone the repository from the remote URL
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress()) repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
@ -70,7 +79,12 @@ def gitcheck(path, do_fetch=False):
# Get the current commit hash and the commit hash of the remote branch # Get the current commit hash and the commit hash of the remote branch
commit_hash = repo.head.commit.hexsha commit_hash = repo.head.commit.hexsha
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
if f'{remote_name}/{branch_name}' in repo.refs:
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
else:
print("CUSTOM NODE CHECK: True") # non default branch is treated as updatable
return
# Compare the commit hashes to determine if the local repository is behind the remote repository # Compare the commit hashes to determine if the local repository is behind the remote repository
if commit_hash != remote_commit_hash: if commit_hash != remote_commit_hash:
@ -89,11 +103,8 @@ def gitcheck(path, do_fetch=False):
def switch_to_default_branch(repo): def switch_to_default_branch(repo):
show_result = repo.git.remote("show", "origin") default_branch = repo.git.symbolic_ref('refs/remotes/origin/HEAD').replace('refs/remotes/origin/', '')
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result) repo.git.checkout(default_branch)
if matches:
default_branch = matches.group(1)
repo.git.checkout(default_branch)
def gitpull(path): def gitpull(path):
@ -117,6 +128,11 @@ def gitpull(path):
remote_name = current_branch.tracking_branch().remote_name remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name) remote = repo.remote(name=remote_name)
if f'{remote_name}/{branch_name}' not in repo.refs:
switch_to_default_branch(repo)
current_branch = repo.active_branch
branch_name = current_branch.name
remote.fetch() remote.fetch()
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
@ -142,9 +158,7 @@ def gitpull(path):
def checkout_comfyui_hash(target_hash): def checkout_comfyui_hash(target_hash):
repo_path = os.path.abspath(os.path.join(working_directory, '..')) # ComfyUI dir repo = git.Repo(comfy_path)
repo = git.Repo(repo_path)
commit_hash = repo.head.commit.hexsha commit_hash = repo.head.commit.hexsha
if commit_hash != target_hash: if commit_hash != target_hash:
@ -167,7 +181,7 @@ def checkout_custom_node_hash(git_custom_node_infos):
repo_name_to_url[repo_name] = url repo_name_to_url[repo_name] = url
for path in os.listdir(working_directory): for path in os.listdir(working_directory):
if path.endswith("ComfyUI-Manager"): if '@' in path or path.endswith("ComfyUI-Manager"):
continue continue
fullpath = os.path.join(working_directory, path) fullpath = os.path.join(working_directory, path)
@ -226,6 +240,9 @@ def checkout_custom_node_hash(git_custom_node_infos):
# clone missing # clone missing
for k, v in git_custom_node_infos.items(): for k, v in git_custom_node_infos.items():
if 'ComfyUI-Manager' in k:
continue
if not v['disabled']: if not v['disabled']:
repo_name = k.split('/')[-1] repo_name = k.split('/')[-1]
if repo_name.endswith('.git'): if repo_name.endswith('.git'):
@ -234,7 +251,7 @@ def checkout_custom_node_hash(git_custom_node_infos):
path = os.path.join(working_directory, repo_name) path = os.path.join(working_directory, repo_name)
if not os.path.exists(path): if not os.path.exists(path):
print(f"CLONE: {path}") print(f"CLONE: {path}")
gitclone(working_directory, k, v['hash']) gitclone(working_directory, k, target_hash=v['hash'])
def invalidate_custom_node_file(file_custom_node_infos): def invalidate_custom_node_file(file_custom_node_infos):
@ -286,6 +303,7 @@ def invalidate_custom_node_file(file_custom_node_infos):
def apply_snapshot(target): def apply_snapshot(target):
try: try:
# todo: should be if target is not in snapshots dir
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}") path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
if os.path.exists(path): if os.path.exists(path):
if not target.endswith('.json') and not target.endswith('.yaml'): if not target.endswith('.json') and not target.endswith('.yaml'):
@ -401,7 +419,11 @@ setup_environment()
try: try:
if sys.argv[1] == "--clone": if sys.argv[1] == "--clone":
gitclone(sys.argv[2], sys.argv[3]) repo_path = None
if len(sys.argv) > 4:
repo_path = sys.argv[4]
gitclone(sys.argv[2], sys.argv[3], repo_path=repo_path)
elif sys.argv[1] == "--check": elif sys.argv[1] == "--check":
gitcheck(sys.argv[2], False) gitcheck(sys.argv[2], False)
elif sys.argv[1] == "--fetch": elif sys.argv[1] == "--fetch":

112
glob/cnr_utils.py Normal file
View File

@ -0,0 +1,112 @@
from manager_util import *
import zipfile
import requests
from dataclasses import dataclass
from typing import List
base_url = "https://api.comfy.org"
async def get_cnr_data(page=1, limit=1000, cache_mode=True):
try:
uri = f'{base_url}/nodes?page={page}&limit={limit}'
json_obj = await get_data_with_cache(uri, cache_mode=cache_mode)
for v in json_obj['nodes']:
if 'latest_version' not in v:
v['latest_version'] = dict(version='nightly')
return json_obj['nodes']
except:
res = {}
print(f"Cannot connect to comfyregistry.")
return res
@dataclass
class NodeVersion:
changelog: str
dependencies: List[str]
deprecated: bool
id: str
version: str
download_url: str
def map_node_version(api_node_version):
"""
Maps node version data from API response to NodeVersion dataclass.
Args:
api_data (dict): The 'node_version' part of the API response.
Returns:
NodeVersion: An instance of NodeVersion dataclass populated with data from the API.
"""
return NodeVersion(
changelog=api_node_version.get(
"changelog", ""
), # Provide a default value if 'changelog' is missing
dependencies=api_node_version.get(
"dependencies", []
), # Provide a default empty list if 'dependencies' is missing
deprecated=api_node_version.get(
"deprecated", False
), # Assume False if 'deprecated' is not specified
id=api_node_version[
"id"
], # 'id' should be mandatory; raise KeyError if missing
version=api_node_version[
"version"
], # 'version' should be mandatory; raise KeyError if missing
download_url=api_node_version.get(
"downloadUrl", ""
), # Provide a default value if 'downloadUrl' is missing
)
def install_node(node_id, version=None):
"""
Retrieves the node version for installation.
Args:
node_id (str): The unique identifier of the node.
version (str, optional): Specific version of the node to retrieve. If omitted, the latest version is returned.
Returns:
NodeVersion: Node version data or error message.
"""
if version is None:
url = f"{base_url}/nodes/{node_id}/install"
else:
url = f"{base_url}/nodes/{node_id}/install?version={version}"
response = requests.get(url)
if response.status_code == 200:
# Convert the API response to a NodeVersion object
return map_node_version(response.json())
else:
return None
def all_versions_of_node(node_id):
url = f"https://api.comfy.org/nodes/{node_id}/versions"
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
return None
def extract_package_as_zip(file_path, extract_path):
try:
with zipfile.ZipFile(file_path, "r") as zip_ref:
zip_ref.extractall(extract_path)
extracted_files = zip_ref.namelist()
print(f"Extracted zip file to {extract_path}")
return extracted_files
except zipfile.BadZipFile:
print(f"File '{file_path}' is not a zip or is corrupted.")
return None

File diff suppressed because it is too large Load Diff

View File

@ -16,12 +16,15 @@ import git
from server import PromptServer from server import PromptServer
import manager_core as core import manager_core as core
import manager_util
import cm_global import cm_global
print(f"### Loading: ComfyUI-Manager ({core.version_str})") print(f"### Loading: ComfyUI-Manager ({core.version_str})")
comfy_ui_hash = "-" comfy_ui_hash = "-"
routes = PromptServer.instance.routes
def handle_stream(stream, prefix): def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
@ -59,7 +62,7 @@ def is_allowed_security_level(level):
async def get_risky_level(files): async def get_risky_level(files):
json_data1 = await core.get_data_by_mode('local', 'custom-node-list.json') json_data1 = await core.get_data_by_mode('local', 'custom-node-list.json')
json_data2 = await core.get_data_by_mode('cache', 'custom-node-list.json', channel_url='https://github.com/ltdrdata/ComfyUI-Manager/raw/main/custom-node-list.json') json_data2 = await core.get_data_by_mode('cache', 'custom-node-list.json', channel_url='https://github.com/ltdrdata/ComfyUI-Manager/raw/main')
all_urls = set() all_urls = set()
for x in json_data1['custom_nodes'] + json_data2['custom_nodes']: for x in json_data1['custom_nodes'] + json_data2['custom_nodes']:
@ -201,19 +204,6 @@ def print_comfyui_version():
print_comfyui_version() print_comfyui_version()
async def populate_github_stats(json_obj, json_obj_github):
if 'custom_nodes' in json_obj:
for i, node in enumerate(json_obj['custom_nodes']):
url = node['reference']
if url in json_obj_github:
json_obj['custom_nodes'][i]['stars'] = json_obj_github[url]['stars']
json_obj['custom_nodes'][i]['last_update'] = json_obj_github[url]['last_update']
json_obj['custom_nodes'][i]['trust'] = json_obj_github[url]['author_account_age_days'] > 180
else:
json_obj['custom_nodes'][i]['stars'] = -1
json_obj['custom_nodes'][i]['last_update'] = -1
json_obj['custom_nodes'][i]['trust'] = False
return json_obj
def setup_environment(): def setup_environment():
@ -280,7 +270,7 @@ def get_model_path(data):
return os.path.join(base_model, data['filename']) return os.path.join(base_model, data['filename'])
def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True, do_update=False): def check_state_of_git_node_pack(node_packs, do_fetch=False, do_update_check=True, do_update=False):
if do_fetch: if do_fetch:
print("Start fetching...", end="") print("Start fetching...", end="")
elif do_update: elif do_update:
@ -289,16 +279,17 @@ def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True,
print("Start update check...", end="") print("Start update check...", end="")
def process_custom_node(item): def process_custom_node(item):
core.check_a_custom_node_installed(item, do_fetch, do_update_check, do_update) core.check_state_of_git_node_pack_single(item, do_fetch, do_update_check, do_update)
with concurrent.futures.ThreadPoolExecutor(4) as executor: with concurrent.futures.ThreadPoolExecutor(4) as executor:
for item in json_obj['custom_nodes']: for k, v in node_packs.items():
executor.submit(process_custom_node, item) if v.get('active_version') in ['unknown', 'nightly']:
executor.submit(process_custom_node, v)
if do_fetch: if do_fetch:
print(f"\x1b[2K\rFetching done.") print(f"\x1b[2K\rFetching done.")
elif do_update: elif do_update:
update_exists = any(item['installed'] == 'Update' for item in json_obj['custom_nodes']) update_exists = any(item.get('updatable', False) for item in node_packs.values())
if update_exists: if update_exists:
print(f"\x1b[2K\rUpdate done.") print(f"\x1b[2K\rUpdate done.")
else: else:
@ -335,8 +326,11 @@ def nickname_filter(json_obj):
return json_obj return json_obj
@PromptServer.instance.routes.get("/customnode/getmappings") @routes.get("/customnode/getmappings")
async def fetch_customnode_mappings(request): async def fetch_customnode_mappings(request):
"""
provide unified (node -> node pack) mapping list
"""
mode = request.rel_url.query["mode"] mode = request.rel_url.query["mode"]
nickname_mode = False nickname_mode = False
@ -345,6 +339,7 @@ async def fetch_customnode_mappings(request):
nickname_mode = True nickname_mode = True
json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json') json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json')
json_obj = core.map_to_unified_keys(json_obj)
if nickname_mode: if nickname_mode:
json_obj = nickname_filter(json_obj) json_obj = nickname_filter(json_obj)
@ -367,25 +362,34 @@ async def fetch_customnode_mappings(request):
return web.json_response(json_obj, content_type='application/json') return web.json_response(json_obj, content_type='application/json')
@PromptServer.instance.routes.get("/customnode/fetch_updates") @routes.get("/customnode/fetch_updates")
async def fetch_updates(request): async def fetch_updates(request):
try: try:
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') if request.rel_url.query["mode"] == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
check_custom_nodes_installed(json_obj, True) await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
update_exists = any('custom_nodes' in json_obj and 'installed' in node and node['installed'] == 'Update' for node in res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
json_obj['custom_nodes'])
if update_exists: for x in res['failed']:
print(f"FETCH FAILED: {x}")
print("\nDone.")
if len(res['updated']) > 0:
return web.Response(status=201) return web.Response(status=201)
return web.Response(status=200) return web.Response(status=200)
except: except:
traceback.print_exc()
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/customnode/update_all") @routes.get("/customnode/update_all")
async def update_all(request): async def update_all(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -394,22 +398,37 @@ async def update_all(request):
try: try:
core.save_snapshot_with_postfix('autosave') core.save_snapshot_with_postfix('autosave')
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') if request.rel_url.query["mode"] == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
check_custom_nodes_installed(json_obj, do_update=True) await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
updated = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Update'] updated_cnr = []
failed = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Fail'] for k, v in core.unified_manager.active_nodes.items():
if v[0] != 'nightly':
res = core.unified_manager.unified_update(k, v[0])
if res.action == 'switch-cnr' and res:
updated_cnr.append(k)
res = {'updated': updated, 'failed': failed} res = core.unified_manager.fetch_or_pull_git_repo(is_pull=True)
if len(updated) == 0 and len(failed) == 0: res['updated'] += updated_cnr
for x in res['failed']:
print(f"PULL FAILED: {x}")
if len(res['updated']) == 0 and len(res['failed']) == 0:
status = 200 status = 200
else: else:
status = 201 status = 201
print(f"\nDone.")
return web.json_response(res, status=status, content_type='application/json') return web.json_response(res, status=status, content_type='application/json')
except: except:
traceback.print_exc()
return web.Response(status=400) return web.Response(status=400)
finally: finally:
core.clear_pip_cache() core.clear_pip_cache()
@ -450,17 +469,20 @@ def convert_markdown_to_html(input_text):
def populate_markdown(x): def populate_markdown(x):
if 'description' in x: if 'description' in x:
x['description'] = convert_markdown_to_html(x['description']) x['description'] = convert_markdown_to_html(manager_util.sanitize_tag(x['description']))
if 'name' in x: if 'name' in x:
x['name'] = x['name'].replace('<', '&lt;').replace('>', '&gt;') x['name'] = manager_util.sanitize_tag(x['name'])
if 'title' in x: if 'title' in x:
x['title'] = x['title'].replace('<', '&lt;').replace('>', '&gt;') x['title'] = manager_util.sanitize_tag(x['title'])
@PromptServer.instance.routes.get("/customnode/getlist") @routes.get("/customnode/getlist")
async def fetch_customnode_list(request): async def fetch_customnode_list(request):
"""
provide unified custom node list
"""
if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true": if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true":
skip_update = True skip_update = True
else: else:
@ -471,26 +493,14 @@ async def fetch_customnode_list(request):
else: else:
channel = core.get_config()['channel_url'] channel = core.get_config()['channel_url']
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') node_packs = await core.get_unified_total_nodes(channel, request.rel_url.query["mode"])
json_obj_github = await core.get_data_by_mode(request.rel_url.query["mode"], 'github-stats.json', 'default') json_obj_github = await core.get_data_by_mode(request.rel_url.query["mode"], 'github-stats.json', 'default')
json_obj = await populate_github_stats(json_obj, json_obj_github) core.populate_github_stats(node_packs, json_obj_github)
def is_ignored_notice(code): check_state_of_git_node_pack(node_packs, False, do_update_check=not skip_update)
if code is not None and code.startswith('#NOTICE_'):
try:
notice_version = [int(x) for x in code[8:].split('.')]
return notice_version[0] < core.version[0] or (notice_version[0] == core.version[0] and notice_version[1] <= core.version[1])
except Exception:
return False
else:
return False
json_obj['custom_nodes'] = [record for record in json_obj['custom_nodes'] if not is_ignored_notice(record.get('author'))] for v in node_packs.values():
populate_markdown(v)
check_custom_nodes_installed(json_obj, False, not skip_update)
for x in json_obj['custom_nodes']:
populate_markdown(x)
if channel != 'local': if channel != 'local':
found = 'custom' found = 'custom'
@ -502,48 +512,24 @@ async def fetch_customnode_list(request):
channel = found channel = found
json_obj['channel'] = channel result = dict(channel=channel, node_packs=node_packs)
return web.json_response(json_obj, content_type='application/json') return web.json_response(result, content_type='application/json')
@PromptServer.instance.routes.get("/customnode/alternatives") @routes.get("/customnode/alternatives")
async def fetch_customnode_alternatives(request): async def fetch_customnode_alternatives(request):
alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json') alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json')
res = {}
for item in alter_json['items']: for item in alter_json['items']:
populate_markdown(item) populate_markdown(item)
res[item['id']] = item
return web.json_response(alter_json, content_type='application/json')
res = core.map_to_unified_keys(res)
@PromptServer.instance.routes.get("/alternatives/getlist") return web.json_response(res, content_type='application/json')
async def fetch_alternatives_list(request):
if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true":
skip_update = True
else:
skip_update = False
alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json')
custom_node_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json')
fileurl_to_custom_node = {}
for item in custom_node_json['custom_nodes']:
for fileurl in item['files']:
fileurl_to_custom_node[fileurl] = item
for item in alter_json['items']:
fileurl = item['id']
if fileurl in fileurl_to_custom_node:
custom_node = fileurl_to_custom_node[fileurl]
core.check_a_custom_node_installed(custom_node, not skip_update)
populate_markdown(item)
populate_markdown(custom_node)
item['custom_node'] = custom_node
return web.json_response(alter_json, content_type='application/json')
def check_model_installed(json_obj): def check_model_installed(json_obj):
@ -567,7 +553,7 @@ def check_model_installed(json_obj):
executor.submit(process_model, item) executor.submit(process_model, item)
@PromptServer.instance.routes.get("/externalmodel/getlist") @routes.get("/externalmodel/getlist")
async def fetch_externalmodel_list(request): async def fetch_externalmodel_list(request):
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json') json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json')
@ -587,7 +573,7 @@ async def get_snapshot_list(request):
return web.json_response({'items': items}, content_type='application/json') return web.json_response({'items': items}, content_type='application/json')
@PromptServer.instance.routes.get("/snapshot/remove") @routes.get("/snapshot/remove")
async def remove_snapshot(request): async def remove_snapshot(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -605,7 +591,7 @@ async def remove_snapshot(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/snapshot/restore") @routes.get("/snapshot/restore")
async def remove_snapshot(request): async def remove_snapshot(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -631,7 +617,7 @@ async def remove_snapshot(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/snapshot/get_current") @routes.get("/snapshot/get_current")
async def get_current_snapshot_api(request): async def get_current_snapshot_api(request):
try: try:
return web.json_response(core.get_current_snapshot(), content_type='application/json') return web.json_response(core.get_current_snapshot(), content_type='application/json')
@ -639,7 +625,7 @@ async def get_current_snapshot_api(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/snapshot/save") @routes.get("/snapshot/save")
async def save_snapshot(request): async def save_snapshot(request):
try: try:
core.save_snapshot_with_postfix('snapshot') core.save_snapshot_with_postfix('snapshot')
@ -774,7 +760,34 @@ def copy_set_active(files, is_disable, js_path_name='.'):
return True return True
@PromptServer.instance.routes.post("/customnode/install") @routes.get("/customnode/versions/{node_name}")
async def get_cnr_versions(request):
node_name = request.match_info.get("node_name", None)
versions = core.cnr_utils.all_versions_of_node(node_name)
if versions:
return web.json_response(versions, content_type='application/json')
return web.Response(status=400)
@routes.get("/customnode/disabled_versions/{node_name}")
async def get_disabled_versions(request):
node_name = request.match_info.get("node_name", None)
versions = []
if node_name in core.unified_manager.nightly_inactive_nodes:
versions.append(dict(version='nightly'))
for v in core.unified_manager.cnr_inactive_nodes.get(node_name, {}).keys():
versions.append(dict(version=v))
if versions:
return web.json_response(versions, content_type='application/json')
return web.Response(status=400)
@routes.post("/customnode/install")
async def install_custom_node(request): async def install_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -782,46 +795,47 @@ async def install_custom_node(request):
json_data = await request.json() json_data = await request.json()
risky_level = await get_risky_level(json_data['files']) # non-nightly cnr is safe
risky_level = None
cnr_id = json_data.get('id')
skip_post_install = json_data.get('skip_post_install')
if json_data['version'] != 'unknown':
selected_version = json_data.get('selected_version', 'latest')
if selected_version != 'nightly':
risky_level = 'low'
node_spec_str = f"{cnr_id}@{selected_version}"
else:
node_spec_str = f"{cnr_id}@nightly"
else:
# unknown
unknown_name = os.path.basename(json_data['files'][0])
node_spec_str = f"{unknown_name}@unknown"
# apply security policy if not cnr node (nightly isn't regarded as cnr node)
if risky_level is None:
risky_level = await get_risky_level(json_data['files'])
if not is_allowed_security_level(risky_level): if not is_allowed_security_level(risky_level):
print(f"ERROR: This installation is not allowed in this security_level. Please contact the administrator.") print(f"ERROR: This installation is not allowed in this security_level. Please contact the administrator.")
return web.Response(status=404) return web.Response(status=404)
install_type = json_data['install_type'] node_spec = core.unified_manager.resolve_node_spec(node_spec_str)
print(f"Install custom node '{json_data['title']}'") if node_spec is None:
return
res = False node_name, version_spec, is_specified = node_spec
res = await core.unified_manager.install_by_id(node_name, version_spec, json_data['channel'], json_data['mode'], return_postinstall=skip_post_install)
# discard post install if skip_post_install mode
if len(json_data['files']) == 0: if res not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
return web.Response(status=400) return web.Response(status=400)
if install_type == "unzip": return web.Response(status=200)
res = unzip_install(json_data['files'])
if install_type == "copy":
js_path_name = json_data['js_path'] if 'js_path' in json_data else '.'
res = copy_install(json_data['files'], js_path_name)
elif install_type == "git-clone":
res = core.gitclone_install(json_data['files'])
if 'pip' in json_data:
for pname in json_data['pip']:
pkg = core.remap_pip_package(pname)
install_cmd = [sys.executable, "-m", "pip", "install", pkg]
core.try_install_script(json_data['files'][0], ".", install_cmd)
core.clear_pip_cache()
if res:
print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json')
return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/fix") @routes.post("/customnode/fix")
async def fix_custom_node(request): async def fix_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -829,49 +843,45 @@ async def fix_custom_node(request):
json_data = await request.json() json_data = await request.json()
install_type = json_data['install_type'] node_id = json_data.get('id')
node_ver = json_data['version']
print(f"Install custom node '{json_data['title']}'") if node_ver != 'unknown':
node_name = node_id
res = False
if len(json_data['files']) == 0:
return web.Response(status=400)
if install_type == "git-clone":
res = core.gitclone_fix(json_data['files'])
else: else:
return web.Response(status=400) # unknown
node_name = os.path.basename(json_data['files'][0])
if 'pip' in json_data: res = core.unified_manager.unified_fix(node_name, node_ver)
for pname in json_data['pip']:
install_cmd = [sys.executable, "-m", "pip", "install", '-U', pname]
core.try_install_script(json_data['files'][0], ".", install_cmd)
if res: if res.result:
print(f"After restarting ComfyUI, please refresh the browser.") print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json') return web.json_response({}, content_type='application/json')
print(f"ERROR: An error occurred while fixing '{node_name}@{node_ver}'.")
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/install/git_url") @routes.post("/customnode/install/git_url")
async def install_custom_node_git_url(request): async def install_custom_node_git_url(request):
if not is_allowed_security_level('high'): if not is_allowed_security_level('high'):
print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.") print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.")
return web.Response(status=403) return web.Response(status=403)
url = await request.text() url = await request.text()
res = core.gitclone_install([url]) res = await core.gitclone_install(url)
if res: if res.action == 'skip':
print(f"Already installed: '{res.target}'")
return web.Response(status=200)
elif res.result:
print(f"After restarting ComfyUI, please refresh the browser.") print(f"After restarting ComfyUI, please refresh the browser.")
return web.Response(status=200) return web.Response(status=200)
print(res.msg)
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/install/pip") @routes.post("/customnode/install/pip")
async def install_custom_node_git_url(request): async def install_custom_node_git_url(request):
if not is_allowed_security_level('high'): if not is_allowed_security_level('high'):
print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.") print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.")
@ -883,7 +893,7 @@ async def install_custom_node_git_url(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.post("/customnode/uninstall") @routes.post("/customnode/uninstall")
async def uninstall_custom_node(request): async def uninstall_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -891,27 +901,26 @@ async def uninstall_custom_node(request):
json_data = await request.json() json_data = await request.json()
install_type = json_data['install_type'] node_id = json_data.get('id')
if json_data['version'] != 'unknown':
is_unknown = False
node_name = node_id
else:
# unknown
is_unknown = True
node_name = os.path.basename(json_data['files'][0])
print(f"Uninstall custom node '{json_data['title']}'") res = core.unified_manager.unified_uninstall(node_name, is_unknown)
res = False if res.result:
if install_type == "copy":
js_path_name = json_data['js_path'] if 'js_path' in json_data else '.'
res = copy_uninstall(json_data['files'], js_path_name)
elif install_type == "git-clone":
res = core.gitclone_uninstall(json_data['files'])
if res:
print(f"After restarting ComfyUI, please refresh the browser.") print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json') return web.json_response({}, content_type='application/json')
print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/update") @routes.post("/customnode/update")
async def update_custom_node(request): async def update_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -919,25 +928,26 @@ async def update_custom_node(request):
json_data = await request.json() json_data = await request.json()
install_type = json_data['install_type'] node_id = json_data.get('id')
if json_data['version'] != 'unknown':
node_name = node_id
else:
# unknown
node_name = os.path.basename(json_data['files'][0])
print(f"Update custom node '{json_data['title']}'") res = core.unified_manager.unified_update(node_name, json_data['version'])
res = False
if install_type == "git-clone":
res = core.gitclone_update(json_data['files'])
core.clear_pip_cache() core.clear_pip_cache()
if res: if res.result:
print(f"After restarting ComfyUI, please refresh the browser.") print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json') return web.json_response({}, content_type='application/json')
print(f"ERROR: An error occurred while updating '{node_name}'.")
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/comfyui_manager/update_comfyui") @routes.get("/comfyui_manager/update_comfyui")
async def update_comfyui(request): async def update_comfyui(request):
print(f"Update ComfyUI") print(f"Update ComfyUI")
@ -957,21 +967,20 @@ async def update_comfyui(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/toggle_active") @routes.post("/customnode/disable")
async def toggle_active(request): async def disable_node(request):
json_data = await request.json() json_data = await request.json()
install_type = json_data['install_type'] node_id = json_data.get('id')
is_disabled = json_data['installed'] == "Disabled" if json_data['version'] != 'unknown':
is_unknown = False
node_name = node_id
else:
# unknown
is_unknown = True
node_name = os.path.basename(json_data['files'][0])
print(f"Update custom node '{json_data['title']}'") res = core.unified_manager.unified_disable(node_name, is_unknown)
res = False
if install_type == "git-clone":
res = core.gitclone_set_active(json_data['files'], not is_disabled)
elif install_type == "copy":
res = copy_set_active(json_data['files'], not is_disabled, json_data.get('js_path', None))
if res: if res:
return web.json_response({}, content_type='application/json') return web.json_response({}, content_type='application/json')
@ -979,7 +988,20 @@ async def toggle_active(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/model/install") @routes.get("/manager/migrate_unmanaged_nodes")
async def migrate_unmanaged_nodes(request):
print(f"[ComfyUI-Manager] Migrating unmanaged nodes...")
await core.unified_manager.migrate_unmanaged_nodes()
print("Done.")
return web.Response(status=200)
@routes.get("/manager/need_to_migrate")
async def need_to_migrate(request):
return web.Response(text=str(core.need_to_migrate), status=200)
@routes.post("/model/install")
async def install_model(request): async def install_model(request):
json_data = await request.json() json_data = await request.json()
@ -1046,7 +1068,7 @@ class ManagerTerminalHook:
manager_terminal_hook = ManagerTerminalHook() manager_terminal_hook = ManagerTerminalHook()
@PromptServer.instance.routes.get("/manager/terminal") @routes.get("/manager/terminal")
async def terminal_mode(request): async def terminal_mode(request):
if not is_allowed_security_level('high'): if not is_allowed_security_level('high'):
print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.") print(f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.")
@ -1061,7 +1083,7 @@ async def terminal_mode(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/preview_method") @routes.get("/manager/preview_method")
async def preview_method(request): async def preview_method(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
set_preview_method(request.rel_url.query['value']) set_preview_method(request.rel_url.query['value'])
@ -1072,7 +1094,7 @@ async def preview_method(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/badge_mode") @routes.get("/manager/badge_mode")
async def badge_mode(request): async def badge_mode(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
set_badge_mode(request.rel_url.query['value']) set_badge_mode(request.rel_url.query['value'])
@ -1083,7 +1105,7 @@ async def badge_mode(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/default_ui") @routes.get("/manager/default_ui")
async def default_ui_mode(request): async def default_ui_mode(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
set_default_ui_mode(request.rel_url.query['value']) set_default_ui_mode(request.rel_url.query['value'])
@ -1094,7 +1116,7 @@ async def default_ui_mode(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/component/policy") @routes.get("/manager/component/policy")
async def component_policy(request): async def component_policy(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
set_component_policy(request.rel_url.query['value']) set_component_policy(request.rel_url.query['value'])
@ -1105,7 +1127,7 @@ async def component_policy(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/dbl_click/policy") @routes.get("/manager/dbl_click/policy")
async def dbl_click_policy(request): async def dbl_click_policy(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
set_double_click_policy(request.rel_url.query['value']) set_double_click_policy(request.rel_url.query['value'])
@ -1116,7 +1138,7 @@ async def dbl_click_policy(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/channel_url_list") @routes.get("/manager/channel_url_list")
async def channel_url_list(request): async def channel_url_list(request):
channels = core.get_channel_dict() channels = core.get_channel_dict()
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
@ -1153,7 +1175,7 @@ def add_target_blank(html_text):
return modified_html return modified_html
@PromptServer.instance.routes.get("/manager/notice") @routes.get("/manager/notice")
async def get_notice(request): async def get_notice(request):
url = "github.com" url = "github.com"
path = "/ltdrdata/ltdrdata.github.io/wiki/News" path = "/ltdrdata/ltdrdata.github.io/wiki/News"
@ -1188,7 +1210,7 @@ async def get_notice(request):
return web.Response(text="Unable to retrieve Notice", status=200) return web.Response(text="Unable to retrieve Notice", status=200)
@PromptServer.instance.routes.get("/manager/reboot") @routes.get("/manager/reboot")
def restart(self): def restart(self):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.") print(f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.")
@ -1214,12 +1236,11 @@ def restart(self):
def sanitize_filename(input_string): def sanitize_filename(input_string):
# 알파벳, 숫자, 및 밑줄 이외의 문자를 밑줄로 대체
result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string) result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string)
return result_string return result_string
@PromptServer.instance.routes.post("/manager/component/save") @routes.post("/manager/component/save")
async def save_component(request): async def save_component(request):
try: try:
data = await request.json() data = await request.json()
@ -1249,7 +1270,7 @@ async def save_component(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.post("/manager/component/loads") @routes.post("/manager/component/loads")
async def load_components(request): async def load_components(request):
try: try:
json_files = [f for f in os.listdir(components_path) if f.endswith('.json')] json_files = [f for f in os.listdir(components_path) if f.endswith('.json')]
@ -1271,7 +1292,7 @@ async def load_components(request):
return web.Response(status=400) return web.Response(status=400)
@PromptServer.instance.routes.get("/manager/share_option") @routes.get("/manager/share_option")
async def share_option(request): async def share_option(request):
if "value" in request.rel_url.query: if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value'] core.get_config()['share_option'] = request.rel_url.query['value']
@ -1340,7 +1361,7 @@ def set_youml_settings(settings):
f.write(settings) f.write(settings)
@PromptServer.instance.routes.get("/manager/get_openart_auth") @routes.get("/manager/get_openart_auth")
async def api_get_openart_auth(request): async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...") # print("Getting stored Matrix credentials...")
openart_key = get_openart_auth() openart_key = get_openart_auth()
@ -1349,7 +1370,7 @@ async def api_get_openart_auth(request):
return web.json_response({"openart_key": openart_key}) return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/manager/set_openart_auth") @routes.post("/manager/set_openart_auth")
async def api_set_openart_auth(request): async def api_set_openart_auth(request):
json_data = await request.json() json_data = await request.json()
openart_key = json_data['openart_key'] openart_key = json_data['openart_key']
@ -1358,7 +1379,7 @@ async def api_set_openart_auth(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_matrix_auth") @routes.get("/manager/get_matrix_auth")
async def api_get_matrix_auth(request): async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...") # print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth() matrix_auth = get_matrix_auth()
@ -1367,7 +1388,7 @@ async def api_get_matrix_auth(request):
return web.json_response(matrix_auth) return web.json_response(matrix_auth)
@PromptServer.instance.routes.get("/manager/youml/settings") @routes.get("/manager/youml/settings")
async def api_get_youml_settings(request): async def api_get_youml_settings(request):
youml_settings = get_youml_settings() youml_settings = get_youml_settings()
if not youml_settings: if not youml_settings:
@ -1375,14 +1396,14 @@ async def api_get_youml_settings(request):
return web.json_response(json.loads(youml_settings)) return web.json_response(json.loads(youml_settings))
@PromptServer.instance.routes.post("/manager/youml/settings") @routes.post("/manager/youml/settings")
async def api_set_youml_settings(request): async def api_set_youml_settings(request):
json_data = await request.json() json_data = await request.json()
set_youml_settings(json.dumps(json_data)) set_youml_settings(json.dumps(json_data))
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth") @routes.get("/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request): async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken' # Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder # in the same directory as the ComfyUI base folder
@ -1400,7 +1421,7 @@ if hasattr(PromptServer.instance, "app"):
app.middlewares.append(cors_middleware) app.middlewares.append(cors_middleware)
@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images") @routes.post("/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request): async def set_esheep_workflow_and_images(request):
json_data = await request.json() json_data = await request.json()
current_workflow = json_data['workflow'] current_workflow = json_data['workflow']
@ -1410,7 +1431,7 @@ async def set_esheep_workflow_and_images(request):
return web.Response(status=200) return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images") @routes.get("/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request): async def get_esheep_workflow_and_images(request):
with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file: with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file) data = json.load(file)
@ -1481,7 +1502,7 @@ def compute_sha256_checksum(filepath):
return sha256.hexdigest() return sha256.hexdigest()
@PromptServer.instance.routes.post("/manager/share") @routes.post("/manager/share")
async def share_art(request): async def share_art(request):
# get json data # get json data
json_data = await request.json() json_data = await request.json()
@ -1654,15 +1675,11 @@ async def share_art(request):
}, content_type='application/json', status=200) }, content_type='application/json', status=200)
def sanitize(data):
return data.replace("<", "&lt;").replace(">", "&gt;")
async def _confirm_try_install(sender, custom_node_url, msg): async def _confirm_try_install(sender, custom_node_url, msg):
json_obj = await core.get_data_by_mode('default', 'custom-node-list.json') json_obj = await core.get_data_by_mode('default', 'custom-node-list.json')
sender = sanitize(sender) sender = manager_util.sanitize_tag(sender)
msg = sanitize(msg) msg = manager_util.sanitize_tag(msg)
target = core.lookup_customnode_by_url(json_obj, custom_node_url) target = core.lookup_customnode_by_url(json_obj, custom_node_url)
if target is not None: if target is not None:
@ -1684,10 +1701,10 @@ import asyncio
async def default_cache_update(): async def default_cache_update():
async def get_cache(filename): async def get_cache(filename):
uri = 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/' + filename uri = 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/' + filename
cache_uri = str(core.simple_hash(uri)) + '_' + filename cache_uri = str(manager_util.simple_hash(uri)) + '_' + filename
cache_uri = os.path.join(core.cache_dir, cache_uri) cache_uri = os.path.join(core.cache_dir, cache_uri)
json_obj = await core.get_data(uri, True) json_obj = await manager_util.get_data(uri, True)
with core.cache_lock: with core.cache_lock:
with open(cache_uri, "w", encoding='utf-8') as file: with open(cache_uri, "w", encoding='utf-8') as file:
@ -1700,7 +1717,7 @@ async def default_cache_update():
d = get_cache("alter-list.json") d = get_cache("alter-list.json")
e = get_cache("github-stats.json") e = get_cache("github-stats.json")
await asyncio.gather(a, b, c, d, e) await asyncio.gather(a, b, c, d, e, core.check_need_to_migrate())
threading.Thread(target=lambda: asyncio.run(default_cache_update())).start() threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()

View File

@ -1,3 +1,18 @@
import traceback
import aiohttp
import json
import threading
import os
from datetime import datetime
cache_lock = threading.Lock()
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
cache_dir = os.path.join(comfyui_manager_path, '.cache')
try: try:
from distutils.version import StrictVersion from distutils.version import StrictVersion
except: except:
@ -61,3 +76,64 @@ except:
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
def simple_hash(input_string):
hash_value = 0
for char in input_string:
hash_value = (hash_value * 31 + ord(char)) % (2**32)
return hash_value
def is_file_created_within_one_day(file_path):
if not os.path.exists(file_path):
return False
file_creation_time = os.path.getctime(file_path)
current_time = datetime.now().timestamp()
time_difference = current_time - file_creation_time
return time_difference <= 86400
async def get_data(uri, silent=False):
if not silent:
print(f"FETCH DATA from: {uri}", end="")
if uri.startswith("http"):
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.get(uri) as resp:
json_text = await resp.text()
else:
with cache_lock:
with open(uri, "r", encoding="utf-8") as f:
json_text = f.read()
json_obj = json.loads(json_text)
if not silent:
print(f" [DONE]")
return json_obj
async def get_data_with_cache(uri, silent=False, cache_mode=True):
cache_uri = str(simple_hash(uri)) + '_' + os.path.basename(uri).replace('&', "_").replace('?', "_").replace('=', "_")
cache_uri = os.path.join(cache_dir, cache_uri+'.json')
if cache_mode and is_file_created_within_one_day(cache_uri):
json_obj = await get_data(cache_uri, silent=silent)
else:
json_obj = await get_data(uri, silent=silent)
with cache_lock:
with open(cache_uri, "w", encoding='utf-8') as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
if not silent:
print(f"[ComfyUI-Manager] default cache updated: {uri}")
return json_obj
def sanitize_tag(x):
return x.replace('<', '&lt;').replace('>', '&gt;')

View File

@ -11,7 +11,7 @@ import {
showYouMLShareDialog showYouMLShareDialog
} from "./comfyui-share-common.js"; } from "./comfyui-share-common.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js"; import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message } from "./common.js"; import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, migrateAPI, setManagerInstance, show_message } from "./common.js";
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js"; import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
import { CustomNodesManager } from "./custom-nodes-manager.js"; import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js"; import { ModelManager } from "./model-manager.js";
@ -253,6 +253,18 @@ const style = `
color: white !important; color: white !important;
} }
.cm-button-orange {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
font-weight: bold;
background-color: orange !important;
color: black !important;
}
.cm-experimental-button { .cm-experimental-button {
width: 290px; width: 290px;
height: 30px; height: 30px;
@ -804,6 +816,28 @@ class ManagerMenuDialog extends ComfyDialog {
}), }),
]; ];
let migration_btn =
$el("button.cm-button-orange", {
type: "button",
textContent: "Migrate to New Node System",
onclick: () => migrateAPI()
});
migration_btn.style.display = 'none';
res.push(migration_btn);
api.fetchApi('/manager/need_to_migrate')
.then(response => response.text())
.then(text => {
if (text === 'True') {
migration_btn.style.display = 'block';
}
})
.catch(error => {
console.error('Error checking migration status:', error);
});
return res; return res;
} }

View File

@ -25,6 +25,23 @@ export function rebootAPI() {
return false; return false;
} }
export async function migrateAPI() {
if (confirm("When performing a migration, existing installed custom nodes will be renamed and the server will be restarted. Are you sure you want to apply this?\n\n(If you don't perform the migration, ComfyUI-Manager's start-up time will be longer each time due to re-checking during startup.)")) {
try {
await api.fetchApi("/manager/migrate_unmanaged_nodes");
api.fetchApi("/manager/reboot");
}
catch(exception) {
}
return true;
}
return false;
}
export var manager_instance = null; export var manager_instance = null;
export function setManagerInstance(obj) { export function setManagerInstance(obj) {

View File

@ -1,7 +1,9 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js"; import { ComfyDialog, $el } from "../../scripts/ui.js";
import { import { api } from "../../scripts/api.js";
manager_instance, rebootAPI, install_via_git_url,
import {
manager_instance, rebootAPI, install_via_git_url,
fetchData, md5, icons fetchData, md5, icons
} from "./common.js"; } from "./common.js";
@ -28,11 +30,11 @@ const pageCss = `
.cn-manager button { .cn-manager button {
font-size: 16px; font-size: 16px;
color: var(--input-text); color: var(--input-text);
background-color: var(--comfy-input-bg); background-color: var(--comfy-input-bg);
border-radius: 8px; border-radius: 8px;
border-color: var(--border-color); border-color: var(--border-color);
border-style: solid; border-style: solid;
margin: 0; margin: 0;
padding: 4px 8px; padding: 4px 8px;
min-width: 100px; min-width: 100px;
} }
@ -124,7 +126,7 @@ const pageCss = `
.cn-manager-grid .cn-node-desc a { .cn-manager-grid .cn-node-desc a {
color: #5555FF; color: #5555FF;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
} }
@ -191,7 +193,7 @@ const pageCss = `
.cn-tag-list > div { .cn-tag-list > div {
background-color: var(--border-color); background-color: var(--border-color);
border-radius: 5px; border-radius: 5px;
padding: 0 5px; padding: 0 5px;
} }
.cn-install-buttons { .cn-install-buttons {
@ -200,8 +202,8 @@ const pageCss = `
gap: 3px; gap: 3px;
padding: 3px; padding: 3px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
} }
.cn-selected-buttons { .cn-selected-buttons {
@ -212,17 +214,17 @@ const pageCss = `
} }
.cn-manager .cn-btn-enable { .cn-manager .cn-btn-enable {
background-color: blue; background-color: #333399;
color: white; color: white;
} }
.cn-manager .cn-btn-disable { .cn-manager .cn-btn-disable {
background-color: MediumSlateBlue; background-color: #442277;
color: white; color: white;
} }
.cn-manager .cn-btn-update { .cn-manager .cn-btn-update {
background-color: blue; background-color: #1155AA;
color: white; color: white;
} }
@ -247,41 +249,47 @@ const pageCss = `
} }
.cn-manager .cn-btn-uninstall { .cn-manager .cn-btn-uninstall {
background-color: red; background-color: #993333;
color: white; color: white;
} }
.cn-manager .cn-btn-switch {
background-color: #448833;
color: white;
}
@keyframes cn-btn-loading-bg { @keyframes cn-btn-loading-bg {
0% { 0% {
left: 0; left: 0;
} }
100% { 100% {
left: -105px; left: -105px;
} }
} }
.cn-manager button.cn-btn-loading { .cn-manager button.cn-btn-loading {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-color: rgb(0 119 207 / 80%); border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg); background-color: var(--comfy-input-bg);
} }
.cn-manager button.cn-btn-loading::after { .cn-manager button.cn-btn-loading::after {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
content: ""; content: "";
width: 500px; width: 500px;
height: 100%; height: 100%;
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
-45deg, -45deg,
rgb(0 119 207 / 30%), rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px, rgb(0 119 207 / 30%) 10px,
transparent 10px, transparent 10px,
transparent 15px transparent 15px
); );
animation: cn-btn-loading-bg 2s linear infinite; animation: cn-btn-loading-bg 2s linear infinite;
} }
.cn-manager-light .cn-node-name a { .cn-manager-light .cn-node-name a {
@ -356,7 +364,6 @@ export class CustomNodesManager {
} }
init() { init() {
if (!document.querySelector(`style[context="${this.id}"]`)) { if (!document.querySelector(`style[context="${this.id}"]`)) {
const $style = document.createElement("style"); const $style = document.createElement("style");
$style.setAttribute("context", this.id); $style.setAttribute("context", this.id);
@ -374,6 +381,130 @@ export class CustomNodesManager {
this.initGrid(); this.initGrid();
} }
showVersionSelectorDialog(versions, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 100003;
dialog.element.style.width = "300px";
dialog.element.style.padding = "0";
dialog.element.style.backgroundColor = "#2a2a2a";
dialog.element.style.border = "1px solid #3a3a3a";
dialog.element.style.borderRadius = "8px";
dialog.element.style.boxSizing = "border-box";
dialog.element.style.overflow = "hidden";
const contentStyle = {
width: "300px",
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "20px",
boxSizing: "border-box",
gap: "15px"
};
let selectedVersion = versions[0];
const versionList = $el("select", {
multiple: true,
size: Math.min(10, versions.length),
style: {
width: "260px",
height: "auto",
backgroundColor: "#383838",
color: "#ffffff",
border: "1px solid #4a4a4a",
borderRadius: "4px",
padding: "5px",
boxSizing: "border-box"
}
},
versions.map((v, index) => $el("option", {
value: v,
textContent: v,
selected: index === 0
}))
);
versionList.addEventListener('change', (e) => {
selectedVersion = e.target.value;
Array.from(e.target.options).forEach(opt => {
opt.selected = opt.value === selectedVersion;
});
});
const content = $el("div", {
style: contentStyle
}, [
$el("h3", {
textContent: "Select Version",
style: {
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: "10px 15px",
margin: "0 0 10px 0",
width: "260px",
textAlign: "center",
borderRadius: "4px",
boxSizing: "border-box",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
versionList,
$el("div", {
style: {
display: "flex",
justifyContent: "space-between",
width: "260px",
gap: "10px"
}
}, [
$el("button", {
textContent: "Cancel",
onclick: () => dialog.close(),
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4a4a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
$el("button", {
textContent: "Select",
onclick: () => {
if (selectedVersion) {
onSelect(selectedVersion);
dialog.close();
} else {
alert("Please select a version.");
}
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4CAF50",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
])
]);
dialog.show(content);
}
initFilter() { initFilter() {
const $filter = this.element.querySelector(".cn-manager-filter"); const $filter = this.element.querySelector(".cn-manager-filter");
const filterList = [{ const filterList = [{
@ -382,23 +513,31 @@ export class CustomNodesManager {
hasData: true hasData: true
}, { }, {
label: "Installed", label: "Installed",
value: "True", value: "installed",
hasData: true
}, {
label: "Enabled",
value: "enabled",
hasData: true hasData: true
}, { }, {
label: "Disabled", label: "Disabled",
value: "Disabled", value: "disabled",
hasData: true hasData: true
}, { }, {
label: "Import Failed", label: "Import Failed",
value: "Fail", value: "import-fail",
hasData: true hasData: true
}, { }, {
label: "Not Installed", label: "Not Installed",
value: "False", value: "not-installed",
hasData: true hasData: true
}, { }, {
label: "Unknown", label: "ComfyRegistry",
value: "None", value: "cnr",
hasData: true
}, {
label: "Non-ComfyRegistry",
value: "unknown",
hasData: true hasData: true
}, { }, {
label: "Update", label: "Update",
@ -423,16 +562,15 @@ export class CustomNodesManager {
return this.filterList.find(it => it.value === filter) return this.filterList.find(it => it.value === filter)
} }
getInstallButtons(installed, title) { getActionButtons(action, rowItem, is_selected_button) {
const buttons = { const buttons = {
"enable": { "enable": {
label: "Enable", label: "Enable",
mode: "toggle_active" mode: "enable"
}, },
"disable": { "disable": {
label: "Disable", label: "Disable",
mode: "toggle_active" mode: "disable"
}, },
"update": { "update": {
@ -460,34 +598,47 @@ export class CustomNodesManager {
"uninstall": { "uninstall": {
label: "Uninstall", label: "Uninstall",
mode: "uninstall" mode: "uninstall"
},
"switch": {
label: "Switch",
mode: "switch"
} }
} }
const installGroups = { const installGroups = {
"Disabled": ["enable", "uninstall"], "disabled": ["enable", "switch", "uninstall"],
"Update": ["update", "disable", "uninstall"], "updatable": ["update", "switch", "disable", "uninstall"],
"Fail": ["try-fix", "uninstall"], "import-fail": ["try-fix", "switch", "disable", "uninstall"],
"True": ["try-update", "disable", "uninstall"], "enabled": ["try-update", "switch", "disable", "uninstall"],
"False": ["install"], "not-installed": ["install"],
'None': ["try-install"] 'unknown': ["try-install"]
} }
if (!manager_instance.update_check_checkbox.checked) { if (!manager_instance.update_check_checkbox.checked) {
installGroups.True = installGroups.True.filter(it => it !== "try-update"); installGroups.enabled = installGroups.enabled.filter(it => it !== "try-update");
} }
if (title === "ComfyUI-Manager") { if (rowItem?.title === "ComfyUI-Manager") {
installGroups.True = installGroups.True.filter(it => it !== "disable"); installGroups.enabled = installGroups.enabled.filter(it => it !== "disable");
}
if (rowItem?.version === "unknown") {
installGroups.enabled = installGroups.enabled.filter(it => it !== "switch");
}
let list = installGroups[action];
if(is_selected_button) {
list = list.filter(it => it !== "switch");
} }
const list = installGroups[installed];
if (!list) { if (!list) {
return ""; return "";
} }
return list.map(id => { return list.map(id => {
const bt = buttons[id]; const bt = buttons[id];
return `<button class="cn-btn-${id}" group="${installed}" mode="${bt.mode}">${bt.label}</button>`; return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
}).join(""); }).join("");
} }
@ -621,18 +772,27 @@ export class CustomNodesManager {
this.showStatus(`${prevViewRowsLength.toLocaleString()} custom nodes`); this.showStatus(`${prevViewRowsLength.toLocaleString()} custom nodes`);
} }
}); });
grid.bind('onSelectChanged', (e, changes) => { grid.bind('onSelectChanged', (e, changes) => {
this.renderSelected(); this.renderSelected();
}); });
grid.bind('onClick', (e, d) => { grid.bind('onClick', (e, d) => {
const btn = this.getButton(d.e.target); const btn = this.getButton(d.e.target);
if (btn) { if (btn) {
this.installNodes([d.rowItem.hash], btn, d.rowItem.title); const item = this.grid.getRowItemBy("hash", d.rowItem.hash);
const { target, label, mode} = btn;
if((mode === "install" || mode === "switch" || mode == "enable") && item.originalData.version != 'unknown') {
// install after select version via dialog if item is cnr node
this.installNodeWithVersion(d.rowItem, btn, mode == 'enable');
}
else {
this.installNodes([d.rowItem.hash], btn, d.rowItem.title);
}
} }
}); });
grid.setOption({ grid.setOption({
theme: 'dark', theme: 'dark',
@ -651,7 +811,7 @@ export class CustomNodesManager {
bindContainerResize: true, bindContainerResize: true,
cellResizeObserver: (rowItem, columnItem) => { cellResizeObserver: (rowItem, columnItem) => {
const autoHeightColumns = ['title', 'installed', 'description', "alternatives"]; const autoHeightColumns = ['title', 'action', 'description', "alternatives"];
return autoHeightColumns.includes(columnItem.id) return autoHeightColumns.includes(columnItem.id)
}, },
@ -696,11 +856,11 @@ export class CustomNodesManager {
theme: colorPalette === "light" ? "" : "dark" theme: colorPalette === "light" ? "" : "dark"
}; };
const rows = this.custom_nodes || []; const rows = this.custom_nodes || {};
rows.forEach((item, i) => { for(let nodeKey in rows) {
item.id = i + 1; let item = rows[nodeKey];
const nodeKey = item.files[0];
const extensionInfo = this.extension_mappings[nodeKey]; const extensionInfo = this.extension_mappings[nodeKey];
if(extensionInfo) { if(extensionInfo) {
const { extensions, conflicts } = extensionInfo; const { extensions, conflicts } = extensionInfo;
if (extensions.length) { if (extensions.length) {
@ -712,7 +872,7 @@ export class CustomNodesManager {
item.conflictsList = conflicts; item.conflictsList = conflicts;
} }
} }
}); }
const columns = [{ const columns = [{
id: 'id', id: 'id',
@ -727,22 +887,47 @@ export class CustomNodesManager {
maxWidth: 500, maxWidth: 500,
classMap: 'cn-node-name', classMap: 'cn-node-name',
formatter: (title, rowItem, columnItem) => { formatter: (title, rowItem, columnItem) => {
return `${rowItem.installed === 'Fail' ? '<font color="red"><B>(IMPORT FAILED)</B></font>' : ''} return `${rowItem.action === 'import-fail' ? '<font color="red"><B>(IMPORT FAILED)</B></font>' : ''}
<a href=${rowItem.reference} target="_blank"><b>${title}</b></a>`; <a href=${rowItem.reference} target="_blank"><b>${title}</b></a>`;
} }
}, { }, {
id: 'installed', id: 'version',
name: 'Install', name: 'Version',
width: 200,
minWidth: 100,
maxWidth: 500,
classMap: 'cn-node-desc',
formatter: (version, rowItem, columnItem) => {
if(version == undefined) {
return `undef`;
}
else {
if(rowItem.cnr_latest && version != rowItem.cnr_latest) {
if(version == 'nightly') {
return `${version} [${rowItem.cnr_latest}]`;
}
else {
return `${version} [↑${rowItem.cnr_latest}]`;
}
}
else {
return `${version}`;
}
}
}
}, {
id: 'action',
name: 'Action',
width: 130, width: 130,
minWidth: 110, minWidth: 110,
maxWidth: 200, maxWidth: 200,
sortable: false, sortable: false,
align: 'center', align: 'center',
formatter: (installed, rowItem, columnItem) => { formatter: (action, rowItem, columnItem) => {
if (rowItem.restart) { if (rowItem.restart) {
return `<font color="red">Restart Required</span>`; return `<font color="red">Restart Required</span>`;
} }
const buttons = this.getInstallButtons(installed, rowItem.title); const buttons = this.getActionButtons(action, rowItem);
return `<div class="cn-install-buttons">${buttons}</div>`; return `<div class="cn-install-buttons">${buttons}</div>`;
} }
}, { }, {
@ -845,14 +1030,35 @@ export class CustomNodesManager {
} }
}]; }];
let rows_values = Object.keys(rows).map(key => rows[key]);
rows_values =
rows_values.sort((a, b) => {
if (a.version == 'unknown' && b.version != 'unknown') return 1;
if (a.version != 'unknown' && b.version == 'unknown') return -1;
if (a.stars !== b.stars) {
return b.stars - a.stars;
}
if (a.last_update !== b.last_update) {
return new Date(b.last_update) - new Date(a.last_update);
}
return 0;
});
this.grid.setData({ this.grid.setData({
options, options: options,
rows, rows: rows_values,
columns columns: columns
}); });
for(let i=0; i<rows_values.length; i++) {
rows_values[i].id = i+1;
}
this.grid.render(); this.grid.render();
} }
updateGrid() { updateGrid() {
@ -877,7 +1083,7 @@ export class CustomNodesManager {
const selectedMap = {}; const selectedMap = {};
selectedList.forEach(item => { selectedList.forEach(item => {
let type = item.installed; let type = item.action;
if (item.restart) { if (item.restart) {
type = "Restart Required"; type = "Restart Required";
} }
@ -895,7 +1101,7 @@ export class CustomNodesManager {
const filterItem = this.getFilterItem(v); const filterItem = this.getFilterItem(v);
list.push(`<div class="cn-selected-buttons"> list.push(`<div class="cn-selected-buttons">
<span>Selected <b>${selectedMap[v].length}</b> ${filterItem ? filterItem.label : v}</span> <span>Selected <b>${selectedMap[v].length}</b> ${filterItem ? filterItem.label : v}</span>
${this.grid.hasMask ? "" : this.getInstallButtons(v)} ${this.grid.hasMask ? "" : this.getActionButtons(v, null, true)}
</div>`); </div>`);
}); });
@ -913,8 +1119,67 @@ export class CustomNodesManager {
} }
} }
async installNodes(list, btn, title) { async installNodeWithVersion(rowItem, btn, is_enable) {
let hash = rowItem.hash;
let title = rowItem.title;
const item = this.grid.getRowItemBy("hash", hash);
let node_id = item.originalData.id;
this.showLoading();
let res;
if(is_enable) {
res = await api.fetchApi(`/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
}
else {
res = await api.fetchApi(`/customnode/versions/${node_id}`, { cache: "no-store" });
}
this.hideLoading();
if(res.status == 200) {
let obj = await res.json();
let versions = [];
let default_version;
let version_cnt = 0;
if(!is_enable) {
if(rowItem.cnr_latest != rowItem.originalData.active_version) {
versions.push('latest');
}
if(rowItem.originalData.active_version != 'nightly') {
versions.push('nightly');
default_version = 'nightly';
version_cnt++;
}
}
for(let v of obj) {
if(rowItem.originalData.active_version != v.version) {
default_version = v.version;
versions.push(v.version);
version_cnt++;
}
}
if(version_cnt == 1) {
// if only one version is available
this.installNodes([hash], btn, title, default_version);
}
else {
this.showVersionSelectorDialog(versions, (selected_version) => {
this.installNodes([hash], btn, title, selected_version);
});
}
}
else {
show_message('Failed to fetch versions from ComfyRegistry.');
}
}
async installNodes(list, btn, title, selected_version) {
const { target, label, mode} = btn; const { target, label, mode} = btn;
if(mode === "uninstall") { if(mode === "uninstall") {
@ -925,13 +1190,11 @@ export class CustomNodesManager {
} }
target.classList.add("cn-btn-loading"); target.classList.add("cn-btn-loading");
this.showLoading();
this.showError(""); this.showError("");
let needRestart = false; let needRestart = false;
let errorMsg = ""; let errorMsg = "";
for (const hash of list) { for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash); const item = this.grid.getRowItemBy("hash", hash);
if (!item) { if (!item) {
errorMsg = `Not found custom node: ${hash}`; errorMsg = `Not found custom node: ${hash}`;
@ -949,9 +1212,24 @@ export class CustomNodesManager {
this.showStatus(`${label} ${item.title} ...`); this.showStatus(`${label} ${item.title} ...`);
const data = item.originalData; const data = item.originalData;
const res = await fetchData(`/customnode/${mode}`, { data.selected_version = selected_version;
data.channel = this.channel;
data.mode = this.mode;
let install_mode = mode;
if(mode == 'switch') {
install_mode = 'install';
}
// don't post install if install_mode == 'enable'
data.skip_post_install = install_mode == 'enable';
let api_mode = install_mode;
if(install_mode == 'enable') {
api_mode = 'install';
}
const res = await api.fetchApi(`/customnode/${api_mode}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
@ -974,13 +1252,12 @@ export class CustomNodesManager {
this.grid.setRowSelected(item, false); this.grid.setRowSelected(item, false);
item.restart = true; item.restart = true;
this.restartMap[item.hash] = true; this.restartMap[item.hash] = true;
this.grid.updateCell(item, "installed"); this.grid.updateCell(item, "action");
//console.log(res.data); //console.log(res.data);
} }
this.hideLoading();
target.classList.remove("cn-btn-loading"); target.classList.remove("cn-btn-loading");
if (errorMsg) { if (errorMsg) {
@ -1064,26 +1341,28 @@ export class CustomNodesManager {
const mappings = res.data; const mappings = res.data;
// build regex->url map // build regex->url map
const regex_to_url = []; const regex_to_pack = [];
this.custom_nodes.forEach(node => { for(let k in this.custom_nodes) {
let node = this.custom_nodes[k];
if(node.nodename_pattern) { if(node.nodename_pattern) {
regex_to_url.push({ regex_to_pack.push({
regex: new RegExp(node.nodename_pattern), regex: new RegExp(node.nodename_pattern),
url: node.files[0] url: node.files[0]
}); });
} }
}); }
// build name->url map // build name->url map
const name_to_urls = {}; const name_to_packs = {};
for (const url in mappings) { for (const url in mappings) {
const names = mappings[url]; const names = mappings[url];
for(const name in names[0]) { for(const name in names[0]) {
let v = name_to_urls[names[0][name]]; let v = name_to_packs[names[0][name]];
if(v == undefined) { if(v == undefined) {
v = []; v = [];
name_to_urls[names[0][name]] = v; name_to_packs[names[0][name]] = v;
} }
v.push(url); v.push(url);
} }
@ -1110,15 +1389,15 @@ export class CustomNodesManager {
continue; continue;
if (!registered_nodes.has(node_type)) { if (!registered_nodes.has(node_type)) {
const urls = name_to_urls[node_type.trim()]; const packs = name_to_packs[node_type.trim()];
if(urls) if(packs)
urls.forEach(url => { packs.forEach(url => {
missing_nodes.add(url); missing_nodes.add(url);
}); });
else { else {
for(let j in regex_to_url) { for(let j in regex_to_pack) {
if(regex_to_url[j].regex.test(node_type)) { if(regex_to_pack[j].regex.test(node_type)) {
missing_nodes.add(regex_to_url[j].url); missing_nodes.add(regex_to_pack[j].url);
} }
} }
} }
@ -1129,19 +1408,27 @@ export class CustomNodesManager {
const unresolved = resUnresolved.data; const unresolved = resUnresolved.data;
if (unresolved && unresolved.nodes) { if (unresolved && unresolved.nodes) {
unresolved.nodes.forEach(node_type => { unresolved.nodes.forEach(node_type => {
const url = name_to_urls[node_type]; const packs = name_to_packs[node_type];
if(url) { if(packs) {
missing_nodes.add(url); packs.forEach(url => {
missing_nodes.add(url);
});
} }
}); });
} }
const hashMap = {}; const hashMap = {};
this.custom_nodes.forEach(item => { for(let k in this.custom_nodes) {
if (item.files.some(file => missing_nodes.has(file))) { let item = this.custom_nodes[k];
if(missing_nodes.has(item.id)) {
hashMap[item.hash] = true; hashMap[item.hash] = true;
} }
}); else if (item.files?.some(file => missing_nodes.has(file))) {
hashMap[item.hash] = true;
}
}
return hashMap; return hashMap;
} }
@ -1156,27 +1443,28 @@ export class CustomNodesManager {
} }
const hashMap = {}; const hashMap = {};
const { items } = res.data; const items = res.data;
items.forEach(item => { for(let i in items) {
let item = items[i];
let custom_node = this.custom_nodes[i];
const custom_node = this.custom_nodes.find(node => node.files.find(file => file === item.id));
if (!custom_node) { if (!custom_node) {
console.log(`Not found custom node: ${item.id}`); console.log(`Not found custom node: ${item.id}`);
return; continue;
} }
const tags = `${item.tags}`.split(",").map(tag => { const tags = `${item.tags}`.split(",").map(tag => {
return `<div>${tag.trim()}</div>`; return `<div>${tag.trim()}</div>`;
}).join("") }).join("");
hashMap[custom_node.hash] = { hashMap[custom_node.hash] = {
alternatives: `<div class="cn-tag-list">${tags}</div> ${item.description}` alternatives: `<div class="cn-tag-list">${tags}</div> ${item.description}`
} }
}); }
return hashMap return hashMap;
} }
async loadData(show_mode = ShowMode.NORMAL) { async loadData(show_mode = ShowMode.NORMAL) {
@ -1198,18 +1486,19 @@ export class CustomNodesManager {
return return
} }
const { channel, custom_nodes} = res.data; const { channel, node_packs } = res.data;
this.channel = channel; this.channel = channel;
this.custom_nodes = custom_nodes; this.mode = mode;
this.custom_nodes = node_packs;
if(this.channel !== 'default') { if(this.channel !== 'default') {
this.element.querySelector(".cn-manager-channel").innerHTML = `Channel: ${this.channel} (Incomplete list)`; this.element.querySelector(".cn-manager-channel").innerHTML = `Channel: ${this.channel} (Incomplete list)`;
} }
for (const item of custom_nodes) { for (const k in node_packs) {
let item = node_packs[k];
item.originalData = JSON.parse(JSON.stringify(item)); item.originalData = JSON.parse(JSON.stringify(item));
const message = item.title + item.files[0]; item.hash = md5(k);
item.hash = md5(message);
} }
const filterItem = this.getFilterItem(this.show_mode); const filterItem = this.getFilterItem(this.show_mode);
@ -1217,11 +1506,12 @@ export class CustomNodesManager {
let hashMap; let hashMap;
if(this.show_mode == ShowMode.UPDATE) { if(this.show_mode == ShowMode.UPDATE) {
hashMap = {}; hashMap = {};
custom_nodes.forEach(it => { for (const k in node_packs) {
if (it.installed === "Update") { let it = node_packs[k];
if (it['update-state'] === "true") {
hashMap[it.hash] = true; hashMap[it.hash] = true;
} }
}); }
} else if(this.show_mode == ShowMode.MISSING) { } else if(this.show_mode == ShowMode.MISSING) {
hashMap = await this.getMissingNodes(); hashMap = await this.getMissingNodes();
} else if(this.show_mode == ShowMode.ALTERNATIVES) { } else if(this.show_mode == ShowMode.ALTERNATIVES) {
@ -1231,10 +1521,23 @@ export class CustomNodesManager {
filterItem.hasData = true; filterItem.hasData = true;
} }
custom_nodes.forEach(nodeItem => { for(let k in node_packs) {
let nodeItem = node_packs[k];
if (this.restartMap[nodeItem.hash]) { if (this.restartMap[nodeItem.hash]) {
nodeItem.restart = true; nodeItem.restart = true;
} }
if(nodeItem['update-state'] == "true") {
nodeItem.action = 'updatable';
}
else if(nodeItem['import-fail']) {
nodeItem.action = 'import-fail';
}
else {
nodeItem.action = nodeItem.state;
}
const filterTypes = new Set(); const filterTypes = new Set();
this.filterList.forEach(filterItem => { this.filterList.forEach(filterItem => {
const { value, hashMap } = filterItem; const { value, hashMap } = filterItem;
@ -1243,29 +1546,51 @@ export class CustomNodesManager {
if (hashData) { if (hashData) {
filterTypes.add(value); filterTypes.add(value);
if (value === ShowMode.UPDATE) { if (value === ShowMode.UPDATE) {
nodeItem.installed = "Update"; nodeItem['update-state'] = "true";
}
if (value === ShowMode.MISSING) {
nodeItem['missing-node'] = "true";
} }
if (typeof hashData === "object") { if (typeof hashData === "object") {
Object.assign(nodeItem, hashData); Object.assign(nodeItem, hashData);
} }
} }
} else { } else {
if (nodeItem.installed === value) { if (nodeItem.state === value) {
filterTypes.add(value); filterTypes.add(value);
} }
const map = {
"Update": "True", switch(nodeItem.state) {
"Disabled": "True", case "enabled":
"Fail": "True", filterTypes.add("enabled");
"None": "False" case "disabled":
filterTypes.add("installed");
break;
case "not-installed":
filterTypes.add("not-installed");
break;
} }
if (map[nodeItem.installed]) {
filterTypes.add(map[nodeItem.installed]); if(nodeItem.version != 'unknown') {
filterTypes.add("cnr");
}
else {
filterTypes.add("unknown");
}
if(nodeItem['update-state'] == 'true') {
filterTypes.add("updatable");
}
if(nodeItem['import-fail']) {
filterTypes.add("import-fail");
} }
} }
}); });
nodeItem.filterTypes = Array.from(filterTypes); nodeItem.filterTypes = Array.from(filterTypes);
}); }
this.renderGrid(); this.renderGrid();

View File

@ -1,4 +1,3 @@
import datetime
import os import os
import subprocess import subprocess
import sys import sys
@ -70,11 +69,12 @@ cm_global.register_api('cm.register_message_collapse', register_message_collapse
cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extension) cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extension)
comfyui_manager_path = os.path.dirname(__file__) comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, "..")) custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, ".."))
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts") startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json") restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py") git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json") pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json")
@ -200,7 +200,7 @@ try:
write_stderr = wrapper_stderr write_stderr = wrapper_stderr
pat_tqdm = r'\d+%.*\[(.*?)\]' pat_tqdm = r'\d+%.*\[(.*?)\]'
pat_import_fail = r'seconds \(IMPORT FAILED\):.*[/\\]custom_nodes[/\\](.*)$' pat_import_fail = r'seconds \(IMPORT FAILED\):(.*)$'
is_start_mode = True is_start_mode = True
@ -233,7 +233,7 @@ try:
if is_start_mode: if is_start_mode:
match = re.search(pat_import_fail, message) match = re.search(pat_import_fail, message)
if match: if match:
import_failed_extensions.add(match.group(1)) import_failed_extensions.add(match.group(1).strip())
if 'Starting server' in message: if 'Starting server' in message:
is_start_mode = False is_start_mode = False
@ -255,7 +255,7 @@ try:
def sync_write(self, message, file_only=False): def sync_write(self, message, file_only=False):
with log_lock: with log_lock:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')[:-3] timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')[:-3]
if self.last_char != '\n': if self.last_char != '\n':
log_file.write(message) log_file.write(message)
else: else:
@ -321,7 +321,7 @@ try:
if is_start_mode: if is_start_mode:
match = re.search(pat_import_fail, message) match = re.search(pat_import_fail, message)
if match: if match:
import_failed_extensions.add(match.group(1)) import_failed_extensions.add(match.group(1).strip())
if 'Starting server' in message: if 'Starting server' in message:
is_start_mode = False is_start_mode = False
@ -361,7 +361,7 @@ except:
print(f"## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed") print(f"## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
print("** ComfyUI startup time:", datetime.datetime.now()) print("** ComfyUI startup time:", datetime.now())
print("** Platform:", platform.system()) print("** Platform:", platform.system())
print("** Python version:", sys.version) print("** Python version:", sys.version)
print("** Python executable:", sys.executable) print("** Python executable:", sys.executable)
@ -507,49 +507,12 @@ if os.path.exists(restore_snapshot_path):
print(prefix, msg, end="") print(prefix, msg, end="")
print(f"[ComfyUI-Manager] Restore snapshot.") print(f"[ComfyUI-Manager] Restore snapshot.")
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
new_env = os.environ.copy() new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path new_env["COMFYUI_PATH"] = comfy_path
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture, env=new_env) exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture, env=new_env)
repository_name = ''
for url in cloned_repos:
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')
this_exit_code = 0
if os.path.exists(requirements_path):
with open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file:
for line in file:
package_name = remap_pip_package(line.strip())
if package_name and not is_installed(package_name):
if not package_name.startswith('#'):
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
this_exit_code += process_wrap(install_cmd, repo_path)
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
processed_install.add(f'{repo_path}/install.py')
install_cmd = [sys.executable, install_script_path]
print(f">>> {install_cmd} / {repo_path}")
new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path
this_exit_code += process_wrap(install_cmd, repo_path, env=new_env)
if this_exit_code != 0:
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
except Exception as e:
print(e)
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
if exit_code != 0: if exit_code != 0:
print(f"[ComfyUI-Manager] Restore snapshot failed.") print(f"[ComfyUI-Manager] Restore snapshot failed.")
else: else: