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

646
cm-cli.py
View File

@ -4,9 +4,9 @@ import traceback
import json
import asyncio
import subprocess
import shutil
import concurrent
import threading
import yaml
from typing import Optional
import typer
@ -17,10 +17,12 @@ import git
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_core as core
import cm_global
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')
if comfy_path is None:
@ -78,9 +80,7 @@ read_downgrade_blacklist() # This is a preparation step for manager_core
class Ctx:
def __init__(self):
self.channel = 'default'
self.mode = 'remote'
self.processed_install = set()
self.custom_node_map_cache = None
self.mode = 'cache'
def set_channel_mode(self, channel, mode):
if mode is not None:
@ -97,196 +97,143 @@ class Ctx:
if channel is not None:
self.channel = channel
def post_install(self, url):
try:
repository_name = url.split("/")[-1].strip()
repo_path = os.path.join(custom_nodes_path, repository_name)
repo_path = os.path.abspath(repo_path)
requirements_path = os.path.join(repo_path, 'requirements.txt')
install_script_path = os.path.join(repo_path, 'install.py')
if os.path.exists(requirements_path):
with (open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file):
for line in file:
package_name = core.remap_pip_package(line.strip())
if package_name and not core.is_installed(package_name):
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
output = subprocess.check_output(install_cmd, cwd=repo_path, text=True)
for msg_line in output.split('\n'):
if 'Requirement already satisfied:' in msg_line:
print('.', end='')
else:
print(msg_line)
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in self.processed_install:
self.processed_install.add(f'{repo_path}/install.py')
install_cmd = [sys.executable, install_script_path]
output = subprocess.check_output(install_cmd, cwd=repo_path, text=True)
for msg_line in output.split('\n'):
if 'Requirement already satisfied:' in msg_line:
print('.', end='')
else:
print(msg_line)
except Exception:
print(f"ERROR: Restoring '{url}' is failed.")
def restore_dependencies(self):
node_paths = [os.path.join(custom_nodes_path, name) for name in os.listdir(custom_nodes_path)
if os.path.isdir(os.path.join(custom_nodes_path, name)) and not name.endswith('.disabled')]
total = len(node_paths)
i = 1
for x in node_paths:
print(f"----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
self.post_install(x)
i += 1
def load_custom_nodes(self):
channel_dict = core.get_channel_dict()
if self.channel not in channel_dict:
print(f"[bold red]ERROR: Invalid channel is specified `--channel {self.channel}`[/bold red]", file=sys.stderr)
exit(1)
if self.mode not in ['remote', 'local', 'cache']:
print(f"[bold red]ERROR: Invalid mode is specified `--mode {self.mode}`[/bold red]", file=sys.stderr)
exit(1)
channel_url = channel_dict[self.channel]
res = {}
json_obj = asyncio.run(core.get_data_by_mode(self.mode, 'custom-node-list.json', channel_url=channel_url))
for x in json_obj['custom_nodes']:
for y in x['files']:
if 'github.com' in y and not (y.endswith('.py') or y.endswith('.js')):
repo_name = y.split('/')[-1]
res[repo_name] = (x, False)
if 'id' in x:
if x['id'] not in res:
res[x['id']] = (x, True)
return res
def get_custom_node_map(self):
if self.custom_node_map_cache is not None:
return self.custom_node_map_cache
self.custom_node_map_cache = self.load_custom_nodes()
return self.custom_node_map_cache
def lookup_node_path(self, node_name, robust=False):
if '..' in node_name:
print(f"\n[bold red]ERROR: Invalid node name '{node_name}'[/bold red]\n")
exit(2)
custom_node_map = self.get_custom_node_map()
if node_name in custom_node_map:
node_url = custom_node_map[node_name][0]['files'][0]
repo_name = node_url.split('/')[-1]
node_path = os.path.join(custom_nodes_path, repo_name)
return node_path, custom_node_map[node_name][0]
elif robust:
node_path = os.path.join(custom_nodes_path, node_name)
return node_path, None
print(f"\n[bold red]ERROR: Invalid node name '{node_name}'[/bold red]\n")
exit(2)
asyncio.run(unified_manager.reload(cache_mode=self.mode == 'cache'))
asyncio.run(unified_manager.load_nightly(self.channel, self.mode))
cm_ctx = Ctx()
channel_ctx = Ctx()
def install_node(node_name, is_all=False, cnt_msg=''):
if core.is_valid_url(node_name):
def install_node(node_spec_str, is_all=False, cnt_msg=''):
if core.is_valid_url(node_spec_str):
# install via urls
res = core.gitclone_install([node_name])
if not res:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.[/bold red]")
res = asyncio.run(core.gitclone_install(node_spec_str))
if not res.result:
print(res.msg)
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
else:
print(f"{cnt_msg} [INSTALLED] {node_name:50}")
print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
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 not is_all:
print(f"{cnt_msg} [ SKIPPED ] {node_name:50} => Already installed")
elif os.path.exists(node_path + '.disabled'):
enable_node(node_name)
if node_spec is None:
return
node_name, version_spec, is_specified = node_spec
# 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:
res = core.gitclone_install(node_item['files'], instant_execution=True, msg_prefix=f"[{cnt_msg}] ")
if not res:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.[/bold red]")
else:
print(f"{cnt_msg} [INSTALLED] {node_name:50}")
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
def reinstall_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name)
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
node_spec = unified_manager.resolve_node_spec(node_spec_str)
if os.path.exists(node_path):
shutil.rmtree(node_path)
if os.path.exists(node_path + '.disabled'):
shutil.rmtree(node_path + '.disabled')
node_name, version_spec, _ = node_spec
unified_manager.unified_uninstall(node_name, version_spec == 'unknown')
install_node(node_name, is_all=is_all, cnt_msg=cnt_msg)
def fix_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
def fix_node(node_spec_str, is_all=False, cnt_msg=''):
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):
print(f"{cnt_msg} [ FIXING ]: {node_name:50} => Disabled")
res = core.gitclone_fix(files, instant_execution=True)
if not res:
print(f"ERROR: An error occurred while fixing '{node_name}'.")
elif not is_all and os.path.exists(node_path + '.disabled'):
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Disabled")
elif not is_all:
return
node_name, version_spec, _ = node_spec
print(f"{cnt_msg} [ FIXING ]: {node_name:50}[{version_spec}]")
res = unified_manager.unified_fix(node_name, version_spec)
if not res.result:
print(f"ERROR: f{res.msg}")
def uninstall_node(node_spec_str, is_all=False, cnt_msg=''):
spec = node_spec_str.split('@')
if len(spec) == 2 and spec[1] == 'unknown':
node_name = spec[0]
is_unknown = True
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")
def uninstall_node(node_name, is_all=False, cnt_msg=''):
node_path, node_item = cm_ctx.lookup_node_path(node_name, robust=True)
files = node_item['files'] if node_item is not None else [node_path]
if os.path.exists(node_path) or os.path.exists(node_path + '.disabled'):
res = core.gitclone_uninstall(files)
if not res:
print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
else:
elif res.result:
print(f"{cnt_msg} [UNINSTALLED] {node_name:50}")
else:
print(f"{cnt_msg} [ SKIPPED ]: {node_name:50} => Not installed")
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)
def update_node(node_spec_str, is_all=False, cnt_msg=''):
node_spec = unified_manager.resolve_node_spec(node_spec_str, 'active')
files = node_item['files'] if node_item is not None else [node_path]
res = core.gitclone_update(files, skip_script=True, msg_prefix=f"[{cnt_msg}] ")
if not res:
print(f"ERROR: An error occurred while updating '{node_name}'.")
if node_spec is None:
if unified_manager.resolve_node_spec(node_spec_str, 'inactive'):
print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Disabled")
else:
print(f"{cnt_msg} [ SKIPPED ]: {node_spec_str:50} => Not installed")
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):
is_all = False
if 'all' in nodes:
is_all = True
nodes = [x for x in cm_ctx.get_custom_node_map().keys() if os.path.exists(os.path.join(custom_nodes_path, x)) or os.path.exists(os.path.join(custom_nodes_path, x) + '.disabled')]
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]
nodes = []
for x in unified_manager.active_nodes.keys():
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)
@ -303,9 +250,9 @@ def update_parallel(nodes):
i += 1
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:
processed.append(node_path)
processed.append(res)
except Exception as e:
print(f"ERROR: {e}")
traceback.print_exc()
@ -315,12 +262,11 @@ def update_parallel(nodes):
executor.submit(process_custom_node, item)
i = 1
for node_path in processed:
if node_path is None:
print(f"[{i}/{total}] Post update: ERROR")
else:
print(f"[{i}/{total}] Post update: {node_path}")
cm_ctx.post_install(node_path)
for res in processed:
if res is not None:
print(f"[{i}/{total}] Post update: {res.target}")
if res.postinstall is not None:
res.postinstall()
i += 1
@ -334,100 +280,158 @@ def update_comfyui():
print("ComfyUI is already up to date.")
def enable_node(node_name, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager':
def enable_node(node_spec_str, is_all=False, cnt_msg=''):
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
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'):
current_name = node_path + '.disabled'
os.rename(current_name, node_path)
if node_spec is None:
print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Not found")
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}")
elif os.path.exists(node_path):
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Already enabled")
elif not is_all:
print(f"{cnt_msg} [SKIPPED] {node_name:50} => Not installed")
else:
print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")
def disable_node(node_name, is_all=False, cnt_msg=''):
if node_name == 'ComfyUI-Manager':
def disable_node(node_spec_str: str, is_all=False, cnt_msg=''):
if 'comfyui-manager' in node_spec_str.lower():
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):
current_name = node_path
new_name = node_path + '.disabled'
os.rename(current_name, new_name)
if node_spec is None:
if unified_manager.resolve_node_spec(node_spec_str, guess_mode='inactive') is not None:
print(f"{cnt_msg} [ SKIP ] {node_spec_str:50} => Already disabled")
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}")
elif os.path.exists(node_path + '.disabled'):
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Already disabled")
elif not is_all:
print(f"{cnt_msg} [ SKIPPED] {node_name:50} => Not installed")
else:
print(f"{cnt_msg} [ FAIL ] {node_name:50} => {res.msg}")
def show_list(kind, simple=False):
for k, v in cm_ctx.get_custom_node_map().items():
if v[1]:
custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=channel_ctx.channel, mode=channel_ctx.mode))
# collect not-installed unknown nodes
not_installed_unknown_nodes = []
repo_unknown = {}
for k, v in custom_nodes.items():
if 'cnr_latest' not in v:
if len(v['files']) == 1:
repo_url = v['files'][0]
node_name = repo_url.split('/')[-1]
if node_name not in unified_manager.unknown_inactive_nodes and node_name not in unified_manager.unknown_active_nodes:
not_installed_unknown_nodes.append(v)
else:
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
node_path = os.path.join(custom_nodes_path, k)
log_item = "[ ENABLED ] ", item['title'], k, item['author']
unknown_processed.append(log_item)
states = set()
if os.path.exists(node_path):
prefix = '[ ENABLED ] '
states.add('installed')
states.add('enabled')
states.add('all')
elif os.path.exists(node_path + '.disabled'):
prefix = '[ DISABLED ] '
states.add('installed')
states.add('disabled')
states.add('all')
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:
prefix = '[ NOT INSTALLED ] '
states.add('not-installed')
states.add('all')
processed[k] = None
if kind in states:
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(f"{k:50}")
print(title+'@'+ver_spec)
else:
short_id = v[0].get('id', "")
print(f"{prefix} {k:50} {short_id:20} (author: {v[0]['author']})")
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) \\[{ver_spec}]")
# 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:
for x in unknown_processed:
prefix, title, short_id, author = x
if simple:
print(f"{k:50}")
print(title+'@unknown')
else:
print(f"{prefix} {k:50} {'':20} (author: N/A)")
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) [UNKNOWN]")
def show_snapshot(simple_mode=False):
@ -467,12 +471,47 @@ def auto_save_snapshot():
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):
is_all = False
if allow_all and 'all' in nodes:
is_all = True
nodes = [x for x in cm_ctx.get_custom_node_map().keys() if os.path.exists(os.path.join(custom_nodes_path, x)) or os.path.exists(os.path.join(custom_nodes_path, x) + '.disabled')]
nodes = get_all_installed_node_specs()
else:
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui', 'all']]
total = len(nodes)
@ -510,9 +549,9 @@ def install(
mode: str = typer.Option(
None,
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)
@ -533,7 +572,7 @@ def reinstall(
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)
@ -554,7 +593,7 @@ def uninstall(
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)
@ -576,7 +615,7 @@ def update(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
@ -607,7 +646,7 @@ def disable(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
@ -633,7 +672,7 @@ def enable(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
@ -659,7 +698,7 @@ def fix(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if 'all' in nodes:
auto_save_snapshot()
@ -667,10 +706,20 @@ def fix(
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(
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[
str,
@ -690,6 +739,7 @@ def show(
"not-installed",
"disabled",
"all",
"cnr",
"snapshot",
"snapshot-list",
]
@ -697,7 +747,7 @@ def show(
typer.echo(f"Invalid command: `show {arg}`", err=True)
exit(1)
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot':
show_snapshot()
elif arg == 'snapshot-list':
@ -736,7 +786,7 @@ def simple_show(
typer.echo(f"[bold red]Invalid command: `show {arg}`[/bold red]", err=True)
exit(1)
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
if arg == 'snapshot':
show_snapshot(True)
elif arg == 'snapshot-list':
@ -786,7 +836,7 @@ def deps_in_workflow(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
input_path = workflow
output_path = output
@ -795,7 +845,7 @@ def deps_in_workflow(
print(f"[bold red]File not found: {input_path}[/bold red]")
exit(1)
used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=cm_ctx.mode, channel_url=cm_ctx.channel))
used_exts, unknown_nodes = asyncio.run(core.extract_nodes_from_workflow(input_path, mode=channel_ctx.mode, channel_url=channel_ctx.channel))
custom_nodes = {}
for x in used_exts:
@ -870,53 +920,7 @@ def restore_snapshot(
exit(1)
try:
cloned_repos = []
checkout_repos = []
skipped_repos = []
enabled_repos = []
disabled_repos = []
is_failed = False
def extract_infos(msg):
nonlocal is_failed
for x in msg:
if x.startswith("CLONE: "):
cloned_repos.append(x[7:])
elif x.startswith("CHECKOUT: "):
checkout_repos.append(x[10:])
elif x.startswith("SKIPPED: "):
skipped_repos.append(x[9:])
elif x.startswith("ENABLE: "):
enabled_repos.append(x[8:])
elif x.startswith("DISABLE: "):
disabled_repos.append(x[9:])
elif 'APPLY SNAPSHOT: False' in x:
is_failed = True
print(f"Restore snapshot.")
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', snapshot_path] + extras
output = subprocess.check_output(cmd_str, cwd=custom_nodes_path, text=True)
msg_lines = output.split('\n')
extract_infos(msg_lines)
for url in cloned_repos:
cm_ctx.post_install(url)
# print summary
for x in cloned_repos:
print(f"[ INSTALLED ] {x}")
for x in checkout_repos:
print(f"[ CHECKOUT ] {x}")
for x in enabled_repos:
print(f"[ ENABLED ] {x}")
for x in disabled_repos:
print(f"[ DISABLED ] {x}")
if is_failed:
print(output)
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
asyncio.run(core.restore_snapshot(snapshot_path, extras))
except Exception:
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
traceback.print_exc()
@ -935,7 +939,7 @@ def restore_dependencies():
for x in node_paths:
print(f"----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
cm_ctx.post_install(x)
unified_manager.execute_install_script('', x, instant_execution=True)
i += 1
@ -947,7 +951,7 @@ def post_install(
help="path to custom node",
)):
path = os.path.expanduser(path)
cm_ctx.post_install(path)
unified_manager.execute_install_script('', path, instant_execution=True)
@app.command(
@ -970,7 +974,7 @@ def install_deps(
help="[remote|local|cache]"
),
):
cm_ctx.set_channel_mode(channel, mode)
channel_ctx.set_channel_mode(channel, mode)
auto_save_snapshot()
if not os.path.exists(deps):
@ -989,7 +993,7 @@ def install_deps(
if state == 'installed':
continue
elif state == 'not-installed':
core.gitclone_install([k], instant_execution=True)
asyncio.run(core.gitclone_install(k, instant_execution=True))
else: # disabled
core.gitclone_set_active([k], False)
@ -1015,15 +1019,35 @@ def export_custom_node_ids(
None,
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:
for x in cm_ctx.get_custom_node_map().keys():
for x in unified_manager.cnr_map.keys():
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__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())
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 configparser
import re
import json
import yaml
from torchvision.datasets.utils import download_url
from tqdm.auto import tqdm
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")
nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
working_directory = os.getcwd()
@ -35,8 +42,10 @@ class GitProgress(RemoteProgress):
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]
if repo_path is None:
repo_path = os.path.join(custom_nodes_path, repo_name)
# Clone the repository from the remote URL
@ -70,7 +79,12 @@ def gitcheck(path, do_fetch=False):
# Get the current commit hash and the commit hash of the remote branch
commit_hash = repo.head.commit.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
if commit_hash != remote_commit_hash:
@ -89,10 +103,7 @@ def gitcheck(path, do_fetch=False):
def switch_to_default_branch(repo):
show_result = repo.git.remote("show", "origin")
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
if matches:
default_branch = matches.group(1)
default_branch = repo.git.symbolic_ref('refs/remotes/origin/HEAD').replace('refs/remotes/origin/', '')
repo.git.checkout(default_branch)
@ -117,6 +128,11 @@ def gitpull(path):
remote_name = current_branch.tracking_branch().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_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
@ -142,9 +158,7 @@ def gitpull(path):
def checkout_comfyui_hash(target_hash):
repo_path = os.path.abspath(os.path.join(working_directory, '..')) # ComfyUI dir
repo = git.Repo(repo_path)
repo = git.Repo(comfy_path)
commit_hash = repo.head.commit.hexsha
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
for path in os.listdir(working_directory):
if path.endswith("ComfyUI-Manager"):
if '@' in path or path.endswith("ComfyUI-Manager"):
continue
fullpath = os.path.join(working_directory, path)
@ -226,6 +240,9 @@ def checkout_custom_node_hash(git_custom_node_infos):
# clone missing
for k, v in git_custom_node_infos.items():
if 'ComfyUI-Manager' in k:
continue
if not v['disabled']:
repo_name = k.split('/')[-1]
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)
if not os.path.exists(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):
@ -286,6 +303,7 @@ def invalidate_custom_node_file(file_custom_node_infos):
def apply_snapshot(target):
try:
# todo: should be if target is not in snapshots dir
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
if os.path.exists(path):
if not target.endswith('.json') and not target.endswith('.yaml'):
@ -401,7 +419,11 @@ setup_environment()
try:
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":
gitcheck(sys.argv[2], False)
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
import manager_core as core
import manager_util
import cm_global
print(f"### Loading: ComfyUI-Manager ({core.version_str})")
comfy_ui_hash = "-"
routes = PromptServer.instance.routes
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
@ -59,7 +62,7 @@ def is_allowed_security_level(level):
async def get_risky_level(files):
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()
for x in json_data1['custom_nodes'] + json_data2['custom_nodes']:
@ -201,19 +204,6 @@ def 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():
@ -280,7 +270,7 @@ def get_model_path(data):
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:
print("Start fetching...", end="")
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="")
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:
for item in json_obj['custom_nodes']:
executor.submit(process_custom_node, item)
for k, v in node_packs.items():
if v.get('active_version') in ['unknown', 'nightly']:
executor.submit(process_custom_node, v)
if do_fetch:
print(f"\x1b[2K\rFetching done.")
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:
print(f"\x1b[2K\rUpdate done.")
else:
@ -335,8 +326,11 @@ def nickname_filter(json_obj):
return json_obj
@PromptServer.instance.routes.get("/customnode/getmappings")
@routes.get("/customnode/getmappings")
async def fetch_customnode_mappings(request):
"""
provide unified (node -> node pack) mapping list
"""
mode = request.rel_url.query["mode"]
nickname_mode = False
@ -345,6 +339,7 @@ async def fetch_customnode_mappings(request):
nickname_mode = True
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:
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')
@PromptServer.instance.routes.get("/customnode/fetch_updates")
@routes.get("/customnode/fetch_updates")
async def fetch_updates(request):
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
json_obj['custom_nodes'])
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
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=200)
except:
traceback.print_exc()
return web.Response(status=400)
@PromptServer.instance.routes.get("/customnode/update_all")
@routes.get("/customnode/update_all")
async def update_all(request):
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.")
@ -394,22 +398,37 @@ async def update_all(request):
try:
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']
failed = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Fail']
updated_cnr = []
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
else:
status = 201
print(f"\nDone.")
return web.json_response(res, status=status, content_type='application/json')
except:
traceback.print_exc()
return web.Response(status=400)
finally:
core.clear_pip_cache()
@ -450,17 +469,20 @@ def convert_markdown_to_html(input_text):
def populate_markdown(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:
x['name'] = x['name'].replace('<', '&lt;').replace('>', '&gt;')
x['name'] = manager_util.sanitize_tag(x['name'])
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):
"""
provide unified custom node list
"""
if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true":
skip_update = True
else:
@ -471,26 +493,14 @@ async def fetch_customnode_list(request):
else:
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 = await populate_github_stats(json_obj, json_obj_github)
core.populate_github_stats(node_packs, json_obj_github)
def is_ignored_notice(code):
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
check_state_of_git_node_pack(node_packs, False, do_update_check=not skip_update)
json_obj['custom_nodes'] = [record for record in json_obj['custom_nodes'] if not is_ignored_notice(record.get('author'))]
check_custom_nodes_installed(json_obj, False, not skip_update)
for x in json_obj['custom_nodes']:
populate_markdown(x)
for v in node_packs.values():
populate_markdown(v)
if channel != 'local':
found = 'custom'
@ -502,48 +512,24 @@ async def fetch_customnode_list(request):
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):
alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json')
for item in alter_json['items']:
populate_markdown(item)
return web.json_response(alter_json, content_type='application/json')
@PromptServer.instance.routes.get("/alternatives/getlist")
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
res = {}
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
res[item['id']] = item
return web.json_response(alter_json, content_type='application/json')
res = core.map_to_unified_keys(res)
return web.json_response(res, content_type='application/json')
def check_model_installed(json_obj):
@ -567,7 +553,7 @@ def check_model_installed(json_obj):
executor.submit(process_model, item)
@PromptServer.instance.routes.get("/externalmodel/getlist")
@routes.get("/externalmodel/getlist")
async def fetch_externalmodel_list(request):
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')
@PromptServer.instance.routes.get("/snapshot/remove")
@routes.get("/snapshot/remove")
async def remove_snapshot(request):
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.")
@ -605,7 +591,7 @@ async def remove_snapshot(request):
return web.Response(status=400)
@PromptServer.instance.routes.get("/snapshot/restore")
@routes.get("/snapshot/restore")
async def remove_snapshot(request):
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.")
@ -631,7 +617,7 @@ async def remove_snapshot(request):
return web.Response(status=400)
@PromptServer.instance.routes.get("/snapshot/get_current")
@routes.get("/snapshot/get_current")
async def get_current_snapshot_api(request):
try:
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)
@PromptServer.instance.routes.get("/snapshot/save")
@routes.get("/snapshot/save")
async def save_snapshot(request):
try:
core.save_snapshot_with_postfix('snapshot')
@ -774,7 +760,34 @@ def copy_set_active(files, is_disable, js_path_name='.'):
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):
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.")
@ -782,46 +795,47 @@ async def install_custom_node(request):
json_data = await request.json()
# 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):
print(f"ERROR: This installation is not allowed in this security_level. Please contact the administrator.")
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)
if install_type == "unzip":
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)
return web.Response(status=200)
@PromptServer.instance.routes.post("/customnode/fix")
@routes.post("/customnode/fix")
async def fix_custom_node(request):
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.")
@ -829,49 +843,45 @@ async def fix_custom_node(request):
json_data = await request.json()
install_type = json_data['install_type']
print(f"Install custom node '{json_data['title']}'")
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'])
node_id = json_data.get('id')
node_ver = json_data['version']
if node_ver != 'unknown':
node_name = node_id
else:
return web.Response(status=400)
# unknown
node_name = os.path.basename(json_data['files'][0])
if 'pip' in json_data:
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)
res = core.unified_manager.unified_fix(node_name, node_ver)
if res:
if res.result:
print(f"After restarting ComfyUI, please refresh the browser.")
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)
@PromptServer.instance.routes.post("/customnode/install/git_url")
@routes.post("/customnode/install/git_url")
async def install_custom_node_git_url(request):
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.")
return web.Response(status=403)
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.")
return web.Response(status=200)
print(res.msg)
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):
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.")
@ -883,7 +893,7 @@ async def install_custom_node_git_url(request):
return web.Response(status=200)
@PromptServer.instance.routes.post("/customnode/uninstall")
@routes.post("/customnode/uninstall")
async def uninstall_custom_node(request):
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.")
@ -891,27 +901,26 @@ async def uninstall_custom_node(request):
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 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:
if res.result:
print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json')
print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/update")
@routes.post("/customnode/update")
async def update_custom_node(request):
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.")
@ -919,25 +928,26 @@ async def update_custom_node(request):
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 = False
if install_type == "git-clone":
res = core.gitclone_update(json_data['files'])
res = core.unified_manager.unified_update(node_name, json_data['version'])
core.clear_pip_cache()
if res:
if res.result:
print(f"After restarting ComfyUI, please refresh the browser.")
return web.json_response({}, content_type='application/json')
print(f"ERROR: An error occurred while updating '{node_name}'.")
return web.Response(status=400)
@PromptServer.instance.routes.get("/comfyui_manager/update_comfyui")
@routes.get("/comfyui_manager/update_comfyui")
async def update_comfyui(request):
print(f"Update ComfyUI")
@ -957,21 +967,20 @@ async def update_comfyui(request):
return web.Response(status=400)
@PromptServer.instance.routes.post("/customnode/toggle_active")
async def toggle_active(request):
@routes.post("/customnode/disable")
async def disable_node(request):
json_data = await request.json()
install_type = json_data['install_type']
is_disabled = json_data['installed'] == "Disabled"
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"Update custom node '{json_data['title']}'")
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))
res = core.unified_manager.unified_disable(node_name, is_unknown)
if res:
return web.json_response({}, content_type='application/json')
@ -979,7 +988,20 @@ async def toggle_active(request):
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):
json_data = await request.json()
@ -1046,7 +1068,7 @@ class ManagerTerminalHook:
manager_terminal_hook = ManagerTerminalHook()
@PromptServer.instance.routes.get("/manager/terminal")
@routes.get("/manager/terminal")
async def terminal_mode(request):
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.")
@ -1061,7 +1083,7 @@ async def terminal_mode(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/preview_method")
@routes.get("/manager/preview_method")
async def preview_method(request):
if "value" in request.rel_url.query:
set_preview_method(request.rel_url.query['value'])
@ -1072,7 +1094,7 @@ async def preview_method(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/badge_mode")
@routes.get("/manager/badge_mode")
async def badge_mode(request):
if "value" in request.rel_url.query:
set_badge_mode(request.rel_url.query['value'])
@ -1083,7 +1105,7 @@ async def badge_mode(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/default_ui")
@routes.get("/manager/default_ui")
async def default_ui_mode(request):
if "value" in request.rel_url.query:
set_default_ui_mode(request.rel_url.query['value'])
@ -1094,7 +1116,7 @@ async def default_ui_mode(request):
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/component/policy")
@routes.get("/manager/component/policy")
async def component_policy(request):
if "value" in request.rel_url.query:
set_component_policy(request.rel_url.query['value'])
@ -1105,7 +1127,7 @@ async def component_policy(request):
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):
if "value" in request.rel_url.query:
set_double_click_policy(request.rel_url.query['value'])
@ -1116,7 +1138,7 @@ async def dbl_click_policy(request):
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):
channels = core.get_channel_dict()
if "value" in request.rel_url.query:
@ -1153,7 +1175,7 @@ def add_target_blank(html_text):
return modified_html
@PromptServer.instance.routes.get("/manager/notice")
@routes.get("/manager/notice")
async def get_notice(request):
url = "github.com"
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)
@PromptServer.instance.routes.get("/manager/reboot")
@routes.get("/manager/reboot")
def restart(self):
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.")
@ -1214,12 +1236,11 @@ def restart(self):
def sanitize_filename(input_string):
# 알파벳, 숫자, 및 밑줄 이외의 문자를 밑줄로 대체
result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string)
return result_string
@PromptServer.instance.routes.post("/manager/component/save")
@routes.post("/manager/component/save")
async def save_component(request):
try:
data = await request.json()
@ -1249,7 +1270,7 @@ async def save_component(request):
return web.Response(status=400)
@PromptServer.instance.routes.post("/manager/component/loads")
@routes.post("/manager/component/loads")
async def load_components(request):
try:
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)
@PromptServer.instance.routes.get("/manager/share_option")
@routes.get("/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value']
@ -1340,7 +1361,7 @@ def set_youml_settings(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):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
@ -1349,7 +1370,7 @@ async def api_get_openart_auth(request):
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):
json_data = await request.json()
openart_key = json_data['openart_key']
@ -1358,7 +1379,7 @@ async def api_set_openart_auth(request):
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):
# print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth()
@ -1367,7 +1388,7 @@ async def api_get_matrix_auth(request):
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):
youml_settings = get_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))
@PromptServer.instance.routes.post("/manager/youml/settings")
@routes.post("/manager/youml/settings")
async def api_set_youml_settings(request):
json_data = await request.json()
set_youml_settings(json.dumps(json_data))
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):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
@ -1400,7 +1421,7 @@ if hasattr(PromptServer.instance, "app"):
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):
json_data = await request.json()
current_workflow = json_data['workflow']
@ -1410,7 +1431,7 @@ async def set_esheep_workflow_and_images(request):
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):
with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
@ -1481,7 +1502,7 @@ def compute_sha256_checksum(filepath):
return sha256.hexdigest()
@PromptServer.instance.routes.post("/manager/share")
@routes.post("/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
@ -1654,15 +1675,11 @@ async def share_art(request):
}, 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):
json_obj = await core.get_data_by_mode('default', 'custom-node-list.json')
sender = sanitize(sender)
msg = sanitize(msg)
sender = manager_util.sanitize_tag(sender)
msg = manager_util.sanitize_tag(msg)
target = core.lookup_customnode_by_url(json_obj, custom_node_url)
if target is not None:
@ -1684,10 +1701,10 @@ import asyncio
async def default_cache_update():
async def get_cache(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)
json_obj = await core.get_data(uri, True)
json_obj = await manager_util.get_data(uri, True)
with core.cache_lock:
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")
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()

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:
from distutils.version import StrictVersion
except:
@ -61,3 +76,64 @@ except:
def __ne__(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
} from "./comfyui-share-common.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 { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
@ -253,6 +253,18 @@ const style = `
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 {
width: 290px;
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;
}

View File

@ -25,6 +25,23 @@ export function rebootAPI() {
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 function setManagerInstance(obj) {

View File

@ -1,5 +1,7 @@
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
import {
manager_instance, rebootAPI, install_via_git_url,
fetchData, md5, icons
@ -212,17 +214,17 @@ const pageCss = `
}
.cn-manager .cn-btn-enable {
background-color: blue;
background-color: #333399;
color: white;
}
.cn-manager .cn-btn-disable {
background-color: MediumSlateBlue;
background-color: #442277;
color: white;
}
.cn-manager .cn-btn-update {
background-color: blue;
background-color: #1155AA;
color: white;
}
@ -247,10 +249,16 @@ const pageCss = `
}
.cn-manager .cn-btn-uninstall {
background-color: red;
background-color: #993333;
color: white;
}
.cn-manager .cn-btn-switch {
background-color: #448833;
color: white;
}
@keyframes cn-btn-loading-bg {
0% {
left: 0;
@ -356,7 +364,6 @@ export class CustomNodesManager {
}
init() {
if (!document.querySelector(`style[context="${this.id}"]`)) {
const $style = document.createElement("style");
$style.setAttribute("context", this.id);
@ -374,6 +381,130 @@ export class CustomNodesManager {
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() {
const $filter = this.element.querySelector(".cn-manager-filter");
const filterList = [{
@ -382,23 +513,31 @@ export class CustomNodesManager {
hasData: true
}, {
label: "Installed",
value: "True",
value: "installed",
hasData: true
}, {
label: "Enabled",
value: "enabled",
hasData: true
}, {
label: "Disabled",
value: "Disabled",
value: "disabled",
hasData: true
}, {
label: "Import Failed",
value: "Fail",
value: "import-fail",
hasData: true
}, {
label: "Not Installed",
value: "False",
value: "not-installed",
hasData: true
}, {
label: "Unknown",
value: "None",
label: "ComfyRegistry",
value: "cnr",
hasData: true
}, {
label: "Non-ComfyRegistry",
value: "unknown",
hasData: true
}, {
label: "Update",
@ -423,16 +562,15 @@ export class CustomNodesManager {
return this.filterList.find(it => it.value === filter)
}
getInstallButtons(installed, title) {
getActionButtons(action, rowItem, is_selected_button) {
const buttons = {
"enable": {
label: "Enable",
mode: "toggle_active"
mode: "enable"
},
"disable": {
label: "Disable",
mode: "toggle_active"
mode: "disable"
},
"update": {
@ -460,34 +598,47 @@ export class CustomNodesManager {
"uninstall": {
label: "Uninstall",
mode: "uninstall"
},
"switch": {
label: "Switch",
mode: "switch"
}
}
const installGroups = {
"Disabled": ["enable", "uninstall"],
"Update": ["update", "disable", "uninstall"],
"Fail": ["try-fix", "uninstall"],
"True": ["try-update", "disable", "uninstall"],
"False": ["install"],
'None': ["try-install"]
"disabled": ["enable", "switch", "uninstall"],
"updatable": ["update", "switch", "disable", "uninstall"],
"import-fail": ["try-fix", "switch", "disable", "uninstall"],
"enabled": ["try-update", "switch", "disable", "uninstall"],
"not-installed": ["install"],
'unknown': ["try-install"]
}
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") {
installGroups.True = installGroups.True.filter(it => it !== "disable");
if (rowItem?.title === "ComfyUI-Manager") {
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) {
return "";
}
return list.map(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("");
}
@ -630,8 +781,17 @@ export class CustomNodesManager {
grid.bind('onClick', (e, d) => {
const btn = this.getButton(d.e.target);
if (btn) {
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({
@ -651,7 +811,7 @@ export class CustomNodesManager {
bindContainerResize: true,
cellResizeObserver: (rowItem, columnItem) => {
const autoHeightColumns = ['title', 'installed', 'description', "alternatives"];
const autoHeightColumns = ['title', 'action', 'description', "alternatives"];
return autoHeightColumns.includes(columnItem.id)
},
@ -696,11 +856,11 @@ export class CustomNodesManager {
theme: colorPalette === "light" ? "" : "dark"
};
const rows = this.custom_nodes || [];
rows.forEach((item, i) => {
item.id = i + 1;
const nodeKey = item.files[0];
const rows = this.custom_nodes || {};
for(let nodeKey in rows) {
let item = rows[nodeKey];
const extensionInfo = this.extension_mappings[nodeKey];
if(extensionInfo) {
const { extensions, conflicts } = extensionInfo;
if (extensions.length) {
@ -712,7 +872,7 @@ export class CustomNodesManager {
item.conflictsList = conflicts;
}
}
});
}
const columns = [{
id: 'id',
@ -727,22 +887,47 @@ export class CustomNodesManager {
maxWidth: 500,
classMap: 'cn-node-name',
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>`;
}
}, {
id: 'installed',
name: 'Install',
id: 'version',
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,
minWidth: 110,
maxWidth: 200,
sortable: false,
align: 'center',
formatter: (installed, rowItem, columnItem) => {
formatter: (action, rowItem, columnItem) => {
if (rowItem.restart) {
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>`;
}
}, {
@ -845,14 +1030,35 @@ export class CustomNodesManager {
}
}];
this.grid.setData({
options,
rows,
columns
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.render();
this.grid.setData({
options: options,
rows: rows_values,
columns: columns
});
for(let i=0; i<rows_values.length; i++) {
rows_values[i].id = i+1;
}
this.grid.render();
}
updateGrid() {
@ -877,7 +1083,7 @@ export class CustomNodesManager {
const selectedMap = {};
selectedList.forEach(item => {
let type = item.installed;
let type = item.action;
if (item.restart) {
type = "Restart Required";
}
@ -895,7 +1101,7 @@ export class CustomNodesManager {
const filterItem = this.getFilterItem(v);
list.push(`<div class="cn-selected-buttons">
<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>`);
});
@ -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;
if(mode === "uninstall") {
@ -925,13 +1190,11 @@ export class CustomNodesManager {
}
target.classList.add("cn-btn-loading");
this.showLoading();
this.showError("");
let needRestart = false;
let errorMsg = "";
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
if (!item) {
errorMsg = `Not found custom node: ${hash}`;
@ -949,9 +1212,24 @@ export class CustomNodesManager {
this.showStatus(`${label} ${item.title} ...`);
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
@ -974,13 +1252,12 @@ export class CustomNodesManager {
this.grid.setRowSelected(item, false);
item.restart = true;
this.restartMap[item.hash] = true;
this.grid.updateCell(item, "installed");
this.grid.updateCell(item, "action");
//console.log(res.data);
}
this.hideLoading();
target.classList.remove("cn-btn-loading");
if (errorMsg) {
@ -1064,26 +1341,28 @@ export class CustomNodesManager {
const mappings = res.data;
// build regex->url map
const regex_to_url = [];
this.custom_nodes.forEach(node => {
const regex_to_pack = [];
for(let k in this.custom_nodes) {
let node = this.custom_nodes[k];
if(node.nodename_pattern) {
regex_to_url.push({
regex_to_pack.push({
regex: new RegExp(node.nodename_pattern),
url: node.files[0]
});
}
});
}
// build name->url map
const name_to_urls = {};
const name_to_packs = {};
for (const url in mappings) {
const names = mappings[url];
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) {
v = [];
name_to_urls[names[0][name]] = v;
name_to_packs[names[0][name]] = v;
}
v.push(url);
}
@ -1110,15 +1389,15 @@ export class CustomNodesManager {
continue;
if (!registered_nodes.has(node_type)) {
const urls = name_to_urls[node_type.trim()];
if(urls)
urls.forEach(url => {
const packs = name_to_packs[node_type.trim()];
if(packs)
packs.forEach(url => {
missing_nodes.add(url);
});
else {
for(let j in regex_to_url) {
if(regex_to_url[j].regex.test(node_type)) {
missing_nodes.add(regex_to_url[j].url);
for(let j in regex_to_pack) {
if(regex_to_pack[j].regex.test(node_type)) {
missing_nodes.add(regex_to_pack[j].url);
}
}
}
@ -1129,19 +1408,27 @@ export class CustomNodesManager {
const unresolved = resUnresolved.data;
if (unresolved && unresolved.nodes) {
unresolved.nodes.forEach(node_type => {
const url = name_to_urls[node_type];
if(url) {
const packs = name_to_packs[node_type];
if(packs) {
packs.forEach(url => {
missing_nodes.add(url);
});
}
});
}
const hashMap = {};
this.custom_nodes.forEach(item => {
if (item.files.some(file => missing_nodes.has(file))) {
for(let k in this.custom_nodes) {
let item = this.custom_nodes[k];
if(missing_nodes.has(item.id)) {
hashMap[item.hash] = true;
}
});
else if (item.files?.some(file => missing_nodes.has(file))) {
hashMap[item.hash] = true;
}
}
return hashMap;
}
@ -1156,27 +1443,28 @@ export class CustomNodesManager {
}
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) {
console.log(`Not found custom node: ${item.id}`);
return;
continue;
}
const tags = `${item.tags}`.split(",").map(tag => {
return `<div>${tag.trim()}</div>`;
}).join("")
}).join("");
hashMap[custom_node.hash] = {
alternatives: `<div class="cn-tag-list">${tags}</div> ${item.description}`
}
});
}
return hashMap
return hashMap;
}
async loadData(show_mode = ShowMode.NORMAL) {
@ -1198,18 +1486,19 @@ export class CustomNodesManager {
return
}
const { channel, custom_nodes} = res.data;
const { channel, node_packs } = res.data;
this.channel = channel;
this.custom_nodes = custom_nodes;
this.mode = mode;
this.custom_nodes = node_packs;
if(this.channel !== 'default') {
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));
const message = item.title + item.files[0];
item.hash = md5(message);
item.hash = md5(k);
}
const filterItem = this.getFilterItem(this.show_mode);
@ -1217,11 +1506,12 @@ export class CustomNodesManager {
let hashMap;
if(this.show_mode == ShowMode.UPDATE) {
hashMap = {};
custom_nodes.forEach(it => {
if (it.installed === "Update") {
for (const k in node_packs) {
let it = node_packs[k];
if (it['update-state'] === "true") {
hashMap[it.hash] = true;
}
});
}
} else if(this.show_mode == ShowMode.MISSING) {
hashMap = await this.getMissingNodes();
} else if(this.show_mode == ShowMode.ALTERNATIVES) {
@ -1231,10 +1521,23 @@ export class CustomNodesManager {
filterItem.hasData = true;
}
custom_nodes.forEach(nodeItem => {
for(let k in node_packs) {
let nodeItem = node_packs[k];
if (this.restartMap[nodeItem.hash]) {
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();
this.filterList.forEach(filterItem => {
const { value, hashMap } = filterItem;
@ -1243,29 +1546,51 @@ export class CustomNodesManager {
if (hashData) {
filterTypes.add(value);
if (value === ShowMode.UPDATE) {
nodeItem.installed = "Update";
nodeItem['update-state'] = "true";
}
if (value === ShowMode.MISSING) {
nodeItem['missing-node'] = "true";
}
if (typeof hashData === "object") {
Object.assign(nodeItem, hashData);
}
}
} else {
if (nodeItem.installed === value) {
if (nodeItem.state === value) {
filterTypes.add(value);
}
const map = {
"Update": "True",
"Disabled": "True",
"Fail": "True",
"None": "False"
switch(nodeItem.state) {
case "enabled":
filterTypes.add("enabled");
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);
});
}
this.renderGrid();

View File

@ -1,4 +1,3 @@
import datetime
import os
import subprocess
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)
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, ".."))
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
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")
@ -200,7 +200,7 @@ try:
write_stderr = wrapper_stderr
pat_tqdm = r'\d+%.*\[(.*?)\]'
pat_import_fail = r'seconds \(IMPORT FAILED\):.*[/\\]custom_nodes[/\\](.*)$'
pat_import_fail = r'seconds \(IMPORT FAILED\):(.*)$'
is_start_mode = True
@ -233,7 +233,7 @@ try:
if is_start_mode:
match = re.search(pat_import_fail, message)
if match:
import_failed_extensions.add(match.group(1))
import_failed_extensions.add(match.group(1).strip())
if 'Starting server' in message:
is_start_mode = False
@ -255,7 +255,7 @@ try:
def sync_write(self, message, file_only=False):
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':
log_file.write(message)
else:
@ -321,7 +321,7 @@ try:
if is_start_mode:
match = re.search(pat_import_fail, message)
if match:
import_failed_extensions.add(match.group(1))
import_failed_extensions.add(match.group(1).strip())
if 'Starting server' in message:
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("** ComfyUI startup time:", datetime.datetime.now())
print("** ComfyUI startup time:", datetime.now())
print("** Platform:", platform.system())
print("** Python version:", sys.version)
print("** Python executable:", sys.executable)
@ -507,49 +507,12 @@ if os.path.exists(restore_snapshot_path):
print(prefix, msg, end="")
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["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)
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:
print(f"[ComfyUI-Manager] Restore snapshot failed.")
else: