diff --git a/__init__.py b/__init__.py
index fd8b291..ab85975 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,1702 +1,15 @@
-import mimetypes
-import traceback
-
-import folder_paths
-import locale
-import subprocess # don't remove this
-import concurrent
-import nodes
-import hashlib
import os
-import sys
-import threading
-import re
-import shutil
-from datetime import datetime
-import git
-
-from server import PromptServer
-from .glob import manager_core as core
-import cm_global
-
-version = [2, 19]
-version_str = f"V{version[0]}.{version[1]}" + (f'.{version[2]}' if len(version) > 2 else '')
-print(f"### Loading: ComfyUI-Manager ({version_str})")
-
-
-comfy_ui_hash = "-"
-
-
-def handle_stream(stream, prefix):
- stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
- for msg in stream:
- if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
- if msg.startswith('100%'):
- print('\r' + msg, end="", file=sys.stderr),
- else:
- print('\r' + msg[:-1], end="", file=sys.stderr),
- else:
- if prefix == '[!]':
- print(prefix, msg, end="", file=sys.stderr)
- else:
- print(prefix, msg, end="")
-
-
-from comfy.cli_args import args
-import latent_preview
-
-
-class ManagerFuncsInComfyUI(core.ManagerFuncs):
- def get_current_preview_method(self):
- if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
- return "auto"
- elif args.preview_method == latent_preview.LatentPreviewMethod.Latent2RGB:
- return "latent2rgb"
- elif args.preview_method == latent_preview.LatentPreviewMethod.TAESD:
- return "taesd"
- else:
- return "none"
-
- def run_script(self, cmd, cwd='.'):
- if len(cmd) > 0 and cmd[0].startswith("#"):
- print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
- return 0
-
- process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
-
- stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, ""))
- stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]"))
-
- stdout_thread.start()
- stderr_thread.start()
-
- stdout_thread.join()
- stderr_thread.join()
-
- return process.wait()
-
-
-core.manager_funcs = ManagerFuncsInComfyUI()
-
-
-sys.path.append('../..')
-
-
-from torchvision.datasets.utils import download_url
-
-
-comfy_path = os.path.dirname(folder_paths.__file__)
-core.js_path = os.path.join(comfy_path, "web", "extensions")
-
-local_db_model = os.path.join(core.comfyui_manager_path, "model-list.json")
-local_db_alter = os.path.join(core.comfyui_manager_path, "alter-list.json")
-local_db_custom_node_list = os.path.join(core.comfyui_manager_path, "custom-node-list.json")
-local_db_extension_node_mappings = os.path.join(core.comfyui_manager_path, "extension-node-map.json")
-components_path = os.path.join(core.comfyui_manager_path, 'components')
-
-
-def set_preview_method(method):
- if method == 'auto':
- args.preview_method = latent_preview.LatentPreviewMethod.Auto
- elif method == 'latent2rgb':
- args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
- elif method == 'taesd':
- args.preview_method = latent_preview.LatentPreviewMethod.TAESD
- else:
- args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
-
- core.get_config()['preview_method'] = args.preview_method
-
-
-set_preview_method(core.get_config()['preview_method'])
-
-
-def set_badge_mode(mode):
- core.get_config()['badge_mode'] = mode
-
-
-def set_default_ui_mode(mode):
- core.get_config()['default_ui'] = mode
-
-
-def set_component_policy(mode):
- core.get_config()['component_policy'] = mode
-
-
-def set_double_click_policy(mode):
- core.get_config()['double_click_policy'] = mode
-
-
-def print_comfyui_version():
- global comfy_ui_hash
-
- is_detached = False
- try:
- repo = git.Repo(os.path.dirname(folder_paths.__file__))
- core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
-
- comfy_ui_hash = repo.head.commit.hexsha
- cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
-
- core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
- cm_global.variables['comfyui.commit_datetime'] = core.comfy_ui_commit_datetime
-
- is_detached = repo.head.is_detached
- current_branch = repo.active_branch.name
-
- try:
- if core.comfy_ui_commit_datetime.date() < core.comfy_ui_required_commit_datetime.date():
- print(f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n")
- except:
- pass
-
- # process on_revision_detected -->
- if 'cm.on_revision_detected_handler' in cm_global.variables:
- for k, f in cm_global.variables['cm.on_revision_detected_handler']:
- try:
- f(core.comfy_ui_revision)
- except Exception:
- print(f"[ERROR] '{k}' on_revision_detected_handler")
- traceback.print_exc()
-
- del cm_global.variables['cm.on_revision_detected_handler']
- else:
- print(f"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated.")
- # <--
-
- if current_branch == "master":
- print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'")
- else:
- print(f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'")
- except:
- if is_detached:
- print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'")
- else:
- print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)")
-
-
-print_comfyui_version()
-
-
-async def populate_github_stats(json_obj, filename, silent=False):
- uri = os.path.join(core.comfyui_manager_path, filename)
- with open(uri, "r", encoding='utf-8') as f:
- github_stats = json.load(f)
- if 'custom_nodes' in json_obj:
- for i, node in enumerate(json_obj['custom_nodes']):
- url = node['reference']
- if url in github_stats:
- json_obj['custom_nodes'][i]['stars'] = github_stats[url]['stars']
- json_obj['custom_nodes'][i]['last_update'] = github_stats[url]['last_update']
- else:
- json_obj['custom_nodes'][i]['stars'] = -1
- json_obj['custom_nodes'][i]['last_update'] = -1
- return json_obj
-
-
-def setup_environment():
- git_exe = core.get_config()['git_exe']
-
- if git_exe != '':
- git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe)
-
-
-setup_environment()
-
-
-# Expand Server api
-
-import server
-from aiohttp import web
-import aiohttp
-import json
-import zipfile
-import urllib.request
-
-
-def get_model_dir(data):
- if data['save_path'] != 'default':
- if '..' in data['save_path'] or data['save_path'].startswith('/'):
- print(f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'.")
- base_model = "etc"
- else:
- if data['save_path'].startswith("custom_nodes"):
- base_model = os.path.join(comfy_path, data['save_path'])
- else:
- base_model = os.path.join(folder_paths.models_dir, data['save_path'])
- else:
- model_type = data['type']
- if model_type == "checkpoints":
- base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0]
- elif model_type == "unclip":
- base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0]
- elif model_type == "VAE":
- base_model = folder_paths.folder_names_and_paths["vae"][0][0]
- elif model_type == "lora":
- base_model = folder_paths.folder_names_and_paths["loras"][0][0]
- elif model_type == "T2I-Adapter":
- base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
- elif model_type == "T2I-Style":
- base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
- elif model_type == "controlnet":
- base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
- elif model_type == "clip_vision":
- base_model = folder_paths.folder_names_and_paths["clip_vision"][0][0]
- elif model_type == "gligen":
- base_model = folder_paths.folder_names_and_paths["gligen"][0][0]
- elif model_type == "upscale":
- base_model = folder_paths.folder_names_and_paths["upscale_models"][0][0]
- elif model_type == "embeddings":
- base_model = folder_paths.folder_names_and_paths["embeddings"][0][0]
- else:
- base_model = "etc"
-
- return base_model
-
-
-def get_model_path(data):
- base_model = get_model_dir(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):
- if do_fetch:
- print("Start fetching...", end="")
- elif do_update:
- print("Start updating...", end="")
- elif do_update_check:
- print("Start update check...", end="")
-
- def process_custom_node(item):
- core.check_a_custom_node_installed(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)
-
- 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'])
- if update_exists:
- print(f"\x1b[2K\rUpdate done.")
- else:
- print(f"\x1b[2K\rAll extensions are already up-to-date.")
- elif do_update_check:
- print(f"\x1b[2K\rUpdate check done.")
-
-
-def nickname_filter(json_obj):
- preemptions_map = {}
-
- for k, x in json_obj.items():
- if 'preemptions' in x[1]:
- for y in x[1]['preemptions']:
- preemptions_map[y] = k
- elif k.endswith("/ComfyUI"):
- for y in x[0]:
- preemptions_map[y] = k
-
- updates = {}
- for k, x in json_obj.items():
- removes = set()
- for y in x[0]:
- k2 = preemptions_map.get(y)
- if k2 is not None and k != k2:
- removes.add(y)
-
- if len(removes) > 0:
- updates[k] = [y for y in x[0] if y not in removes]
-
- for k, v in updates.items():
- json_obj[k][0] = v
-
- return json_obj
-
-
-@PromptServer.instance.routes.get("/customnode/getmappings")
-async def fetch_customnode_mappings(request):
- mode = request.rel_url.query["mode"]
-
- nickname_mode = False
- if mode == "nickname":
- mode = "local"
- nickname_mode = True
-
- json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json')
-
- if nickname_mode:
- json_obj = nickname_filter(json_obj)
-
- all_nodes = set()
- patterns = []
- for k, x in json_obj.items():
- all_nodes.update(set(x[0]))
-
- if 'nodename_pattern' in x[1]:
- patterns.append((x[1]['nodename_pattern'], x[0]))
-
- missing_nodes = set(nodes.NODE_CLASS_MAPPINGS.keys()) - all_nodes
-
- for x in missing_nodes:
- for pat, item in patterns:
- if re.match(pat, x):
- item.append(x)
-
- return web.json_response(json_obj, content_type='application/json')
-
-
-@PromptServer.instance.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')
-
- check_custom_nodes_installed(json_obj, True)
-
- update_exists = any('custom_nodes' in json_obj and 'installed' in node and node['installed'] == 'Update' for node in
- json_obj['custom_nodes'])
-
- if update_exists:
- return web.Response(status=201)
-
- return web.Response(status=200)
- except:
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.get("/customnode/update_all")
-async def update_all(request):
- try:
- save_snapshot_with_postfix('autosave')
-
- json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json')
-
- check_custom_nodes_installed(json_obj, do_update=True)
-
- 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']
-
- res = {'updated': updated, 'failed': failed}
-
- if len(updated) == 0 and len(failed) == 0:
- status = 200
- else:
- status = 201
-
- return web.json_response(res, status=status, content_type='application/json')
- except:
- return web.Response(status=400)
- finally:
- core.clear_pip_cache()
-
-
-def convert_markdown_to_html(input_text):
- pattern_a = re.compile(r'\[a/([^]]+)\]\(([^)]+)\)')
- pattern_w = re.compile(r'\[w/([^]]+)\]')
- pattern_i = re.compile(r'\[i/([^]]+)\]')
- pattern_bold = re.compile(r'\*\*([^*]+)\*\*')
- pattern_white = re.compile(r'%%([^*]+)%%')
-
- def replace_a(match):
- return f"{match.group(1)}"
-
- def replace_w(match):
- return f"
{match.group(1)}
"
-
- def replace_i(match):
- return f"{match.group(1)}
"
-
- def replace_bold(match):
- return f"{match.group(1)}"
-
- def replace_white(match):
- return f"{match.group(1)}"
-
- input_text = input_text.replace('\\[', '[').replace('\\]', ']').replace('<', '<').replace('>', '>')
-
- result_text = re.sub(pattern_a, replace_a, input_text)
- result_text = re.sub(pattern_w, replace_w, result_text)
- result_text = re.sub(pattern_i, replace_i, result_text)
- result_text = re.sub(pattern_bold, replace_bold, result_text)
- result_text = re.sub(pattern_white, replace_white, result_text)
-
- return result_text.replace("\n", "
")
-
-
-def populate_markdown(x):
- if 'description' in x:
- x['description'] = convert_markdown_to_html(x['description'])
-
- if 'name' in x:
- x['name'] = x['name'].replace('<', '<').replace('>', '>')
-
- if 'title' in x:
- x['title'] = x['title'].replace('<', '<').replace('>', '>')
-
-
-@PromptServer.instance.routes.get("/customnode/getlist")
-async def fetch_customnode_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
-
- if request.rel_url.query["mode"] == "local":
- channel = 'local'
- else:
- channel = core.get_config()['channel_url']
-
- json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json')
- json_obj = await populate_github_stats(json_obj, "github-stats.json")
-
- def is_ignored_notice(code):
- global version
-
- if code is not None and code.startswith('#NOTICE_'):
- try:
- notice_version = [int(x) for x in code[8:].split('.')]
- return notice_version[0] < version[0] or (notice_version[0] == version[0] and notice_version[1] <= 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'))]
-
- check_custom_nodes_installed(json_obj, False, not skip_update)
-
- for x in json_obj['custom_nodes']:
- populate_markdown(x)
-
- if channel != 'local':
- found = 'custom'
-
- for name, url in core.get_channel_dict().items():
- if url == channel:
- found = name
- break
-
- channel = found
-
- json_obj['channel'] = channel
-
- return web.json_response(json_obj, 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
-
- 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 process_model(item):
- model_path = get_model_path(item)
- item['installed'] = 'None'
-
- if model_path is not None:
- if os.path.exists(model_path):
- item['installed'] = 'True'
- else:
- item['installed'] = 'False'
-
- with concurrent.futures.ThreadPoolExecutor(8) as executor:
- for item in json_obj['models']:
- executor.submit(process_model, item)
-
-
-@PromptServer.instance.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')
-
- check_model_installed(json_obj)
-
- for x in json_obj['models']:
- populate_markdown(x)
-
- return web.json_response(json_obj, content_type='application/json')
-
-
-@PromptServer.instance.routes.get("/snapshot/get_current")
-async def get_snapshot_list(request):
- json_obj = get_current_snapshot()
- return web.json_response(json_obj, content_type='application/json')
-
-
-@PromptServer.instance.routes.get("/snapshot/getlist")
-async def get_snapshot_list(request):
- snapshots_directory = os.path.join(os.path.dirname(__file__), 'snapshots')
- items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')]
- items.sort(reverse=True)
- return web.json_response({'items': items}, content_type='application/json')
-
-
-@PromptServer.instance.routes.get("/snapshot/remove")
-async def remove_snapshot(request):
- try:
- target = request.rel_url.query["target"]
-
- path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
- if os.path.exists(path):
- os.remove(path)
-
- return web.Response(status=200)
- except:
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.get("/snapshot/restore")
-async def remove_snapshot(request):
- try:
- target = request.rel_url.query["target"]
-
- path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
- if os.path.exists(path):
- if not os.path.exists(core.startup_script_path):
- os.makedirs(core.startup_script_path)
-
- target_path = os.path.join(core.startup_script_path, "restore-snapshot.json")
- shutil.copy(path, target_path)
-
- print(f"Snapshot restore scheduled: `{target}`")
- return web.Response(status=200)
-
- print(f"Snapshot file not found: `{path}`")
- return web.Response(status=400)
- except:
- return web.Response(status=400)
-
-
-def get_current_snapshot():
- # Get ComfyUI hash
- repo_path = os.path.dirname(folder_paths.__file__)
-
- if not os.path.exists(os.path.join(repo_path, '.git')):
- print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
- return web.Response(status=400)
-
- repo = git.Repo(repo_path)
- comfyui_commit_hash = repo.head.commit.hexsha
-
- git_custom_nodes = {}
- file_custom_nodes = []
-
- # Get custom nodes hash
- for path in os.listdir(core.custom_nodes_path):
- fullpath = os.path.join(core.custom_nodes_path, path)
-
- if os.path.isdir(fullpath):
- is_disabled = path.endswith(".disabled")
-
- try:
- git_dir = os.path.join(fullpath, '.git')
-
- if not os.path.exists(git_dir):
- continue
-
- repo = git.Repo(fullpath)
- commit_hash = repo.head.commit.hexsha
- url = repo.remotes.origin.url
- git_custom_nodes[url] = {
- 'hash': commit_hash,
- 'disabled': is_disabled
- }
-
- except:
- print(f"Failed to extract snapshots for the custom node '{path}'.")
-
- elif path.endswith('.py'):
- is_disabled = path.endswith(".py.disabled")
- filename = os.path.basename(path)
- item = {
- 'filename': filename,
- 'disabled': is_disabled
- }
-
- file_custom_nodes.append(item)
-
- return {
- 'comfyui': comfyui_commit_hash,
- 'git_custom_nodes': git_custom_nodes,
- 'file_custom_nodes': file_custom_nodes,
- }
-
-
-def save_snapshot_with_postfix(postfix):
- now = datetime.now()
-
- date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
- file_name = f"{date_time_format}_{postfix}"
-
- path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json")
- with open(path, "w") as json_file:
- json.dump(get_current_snapshot(), json_file, indent=4)
-
-
-@PromptServer.instance.routes.get("/snapshot/get_current")
-async def get_current_snapshot_api(request):
- try:
- return web.json_response(get_current_snapshot(), content_type='application/json')
- except:
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.get("/snapshot/save")
-async def save_snapshot(request):
- try:
- save_snapshot_with_postfix('snapshot')
- return web.Response(status=200)
- except:
- return web.Response(status=400)
-
-
-def unzip_install(files):
- temp_filename = 'manager-temp.zip'
- for url in files:
- if url.endswith("/"):
- url = url[:-1]
- try:
- headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
-
- req = urllib.request.Request(url, headers=headers)
- response = urllib.request.urlopen(req)
- data = response.read()
-
- with open(temp_filename, 'wb') as f:
- f.write(data)
-
- with zipfile.ZipFile(temp_filename, 'r') as zip_ref:
- zip_ref.extractall(core.custom_nodes_path)
-
- os.remove(temp_filename)
- except Exception as e:
- print(f"Install(unzip) error: {url} / {e}", file=sys.stderr)
- return False
-
- print("Installation was successful.")
- return True
-
-
-def download_url_with_agent(url, save_path):
- try:
- headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
-
- req = urllib.request.Request(url, headers=headers)
- response = urllib.request.urlopen(req)
- data = response.read()
-
- if not os.path.exists(os.path.dirname(save_path)):
- os.makedirs(os.path.dirname(save_path))
-
- with open(save_path, 'wb') as f:
- f.write(data)
-
- except Exception as e:
- print(f"Download error: {url} / {e}", file=sys.stderr)
- return False
-
- print("Installation was successful.")
- return True
-
-
-def copy_install(files, js_path_name=None):
- for url in files:
- if url.endswith("/"):
- url = url[:-1]
- try:
- if url.endswith(".py"):
- download_url(url, core.custom_nodes_path)
- else:
- path = os.path.join(core.js_path, js_path_name) if js_path_name is not None else core.js_path
- if not os.path.exists(path):
- os.makedirs(path)
- download_url(url, path)
-
- except Exception as e:
- print(f"Install(copy) error: {url} / {e}", file=sys.stderr)
- return False
-
- print("Installation was successful.")
- return True
-
-
-def copy_uninstall(files, js_path_name='.'):
- for url in files:
- if url.endswith("/"):
- url = url[:-1]
- dir_name = os.path.basename(url)
- base_path = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
- file_path = os.path.join(base_path, dir_name)
-
- try:
- if os.path.exists(file_path):
- os.remove(file_path)
- elif os.path.exists(file_path + ".disabled"):
- os.remove(file_path + ".disabled")
- except Exception as e:
- print(f"Uninstall(copy) error: {url} / {e}", file=sys.stderr)
- return False
-
- print("Uninstallation was successful.")
- return True
-
-
-def copy_set_active(files, is_disable, js_path_name='.'):
- if is_disable:
- action_name = "Disable"
- else:
- action_name = "Enable"
-
- for url in files:
- if url.endswith("/"):
- url = url[:-1]
- dir_name = os.path.basename(url)
- base_path = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
- file_path = os.path.join(base_path, dir_name)
-
- try:
- if is_disable:
- current_name = file_path
- new_name = file_path + ".disabled"
- else:
- current_name = file_path + ".disabled"
- new_name = file_path
-
- os.rename(current_name, new_name)
-
- except Exception as e:
- print(f"{action_name}(copy) error: {url} / {e}", file=sys.stderr)
-
- return False
-
- print(f"{action_name} was successful.")
- return True
-
-
-@PromptServer.instance.routes.post("/customnode/install")
-async def install_custom_node(request):
- json_data = await request.json()
- install_type = json_data['install_type']
+cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
- print(f"Install custom node '{json_data['title']}'")
+if not os.path.exists(cli_mode_flag):
+ from .glob import manager_server
+ WEB_DIRECTORY = "js"
+else:
+ print(f"\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
- res = False
-
- if len(json_data['files']) == 0:
- 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)
-
-
-@PromptServer.instance.routes.post("/customnode/fix")
-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'])
- else:
- return web.Response(status=400)
-
- 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)
-
- 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.get("/customnode/install/git_url")
-async def install_custom_node_git_url(request):
- res = False
- if "url" in request.rel_url.query:
- url = request.rel_url.query['url']
- res = core.gitclone_install([url])
-
- if res:
- print(f"After restarting ComfyUI, please refresh the browser.")
- return web.Response(status=200)
-
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.get("/customnode/install/pip")
-async def install_custom_node_git_url(request):
- res = False
- if "packages" in request.rel_url.query:
- packages = request.rel_url.query['packages']
- core.pip_install(packages.split(' '))
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.routes.post("/customnode/uninstall")
-async def uninstall_custom_node(request):
- json_data = await request.json()
-
- install_type = json_data['install_type']
-
- print(f"Uninstall custom node '{json_data['title']}'")
-
- 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:
- 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/update")
-async def update_custom_node(request):
- json_data = await request.json()
-
- install_type = json_data['install_type']
-
- print(f"Update custom node '{json_data['title']}'")
-
- res = False
-
- if install_type == "git-clone":
- res = core.gitclone_update(json_data['files'])
-
- 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.get("/comfyui_manager/update_comfyui")
-async def update_comfyui(request):
- print(f"Update ComfyUI")
-
- try:
- repo_path = os.path.dirname(folder_paths.__file__)
-
- if not os.path.exists(os.path.join(repo_path, '.git')):
- print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
- return web.Response(status=400)
-
- # version check
- repo = git.Repo(repo_path)
-
- if repo.head.is_detached:
- core.switch_to_default_branch(repo)
-
- current_branch = repo.active_branch
- branch_name = current_branch.name
-
- if current_branch.tracking_branch() is None:
- print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})")
- remote_name = 'origin'
- else:
- remote_name = current_branch.tracking_branch().remote_name
- remote = repo.remote(name=remote_name)
-
- try:
- remote.fetch()
- except Exception as e:
- if 'detected dubious' in str(e):
- print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on 'ComfyUI' repository")
- safedir_path = comfy_path.replace('\\', '/')
- subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path])
- try:
- remote.fetch()
- except Exception:
- print(f"\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n"
- f"-----------------------------------------------------------------------------------------\n"
- f'git config --global --add safe.directory "{safedir_path}"\n'
- f"-----------------------------------------------------------------------------------------\n")
-
- commit_hash = repo.head.commit.hexsha
- remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
-
- if commit_hash != remote_commit_hash:
- core.git_pull(repo_path)
- core.execute_install_script("ComfyUI", repo_path)
- return web.Response(status=201)
- else:
- return web.Response(status=200)
- except Exception as e:
- print(f"ComfyUI update fail: {e}", file=sys.stderr)
- pass
-
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.post("/customnode/toggle_active")
-async def toggle_active(request):
- json_data = await request.json()
-
- install_type = json_data['install_type']
- is_disabled = json_data['installed'] == "Disabled"
-
- 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))
-
- if res:
- return web.json_response({}, content_type='application/json')
-
- return web.Response(status=400)
-
-
-@PromptServer.instance.routes.post("/model/install")
-async def install_model(request):
- json_data = await request.json()
-
- model_path = get_model_path(json_data)
-
- res = False
-
- try:
- if model_path is not None:
- print(f"Install model '{json_data['name']}' into '{model_path}'")
-
- model_url = json_data['url']
- if not core.get_config()['model_download_by_agent'] and (model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')):
- model_dir = get_model_dir(json_data)
- download_url(model_url, model_dir, filename=json_data['filename'])
-
- return web.json_response({}, content_type='application/json')
- else:
- res = download_url_with_agent(model_url, model_path)
- else:
- print(f"Model installation error: invalid model type - {json_data['type']}")
-
- if res:
- return web.json_response({}, content_type='application/json')
- except Exception as e:
- print(f"[ERROR] {e}", file=sys.stderr)
- pass
-
- return web.Response(status=400)
-
-
-class ManagerTerminalHook:
- def write_stderr(self, msg):
- PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg})
-
- def write_stdout(self, msg):
- PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg})
-
-
-manager_terminal_hook = ManagerTerminalHook()
-
-
-@PromptServer.instance.routes.get("/manager/terminal")
-async def terminal_mode(request):
- if "mode" in request.rel_url.query:
- if request.rel_url.query['mode'] == 'true':
- sys.__comfyui_manager_terminal_hook.add_hook('cm', manager_terminal_hook)
- else:
- sys.__comfyui_manager_terminal_hook.remove_hook('cm')
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.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'])
- core.write_config()
- else:
- return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.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'])
- core.write_config()
- else:
- return web.Response(text=core.get_config()['badge_mode'], status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.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'])
- core.write_config()
- else:
- return web.Response(text=core.get_config()['default_ui'], status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.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'])
- core.write_config()
- else:
- return web.Response(text=core.get_config()['component_policy'], status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.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'])
- core.write_config()
- else:
- return web.Response(text=core.get_config()['double_click_policy'], status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.routes.get("/manager/channel_url_list")
-async def channel_url_list(request):
- channels = core.get_channel_dict()
- if "value" in request.rel_url.query:
- channel_url = channels.get(request.rel_url.query['value'])
- if channel_url is not None:
- core.get_config()['channel_url'] = channel_url
- core.write_config()
- else:
- selected = 'custom'
- selected_url = core.get_config()['channel_url']
-
- for name, url in channels.items():
- if url == selected_url:
- selected = name
- break
-
- res = {'selected': selected,
- 'list': core.get_channel_list()}
- return web.json_response(res, status=200)
-
- return web.Response(status=200)
-
-
-@PromptServer.instance.routes.get("/manager/notice")
-async def get_notice(request):
- url = "github.com"
- path = "/ltdrdata/ltdrdata.github.io/wiki/News"
-
- async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
- async with session.get(f"https://{url}{path}") as response:
- if response.status == 200:
- # html_content = response.read().decode('utf-8')
- html_content = await response.text()
-
- pattern = re.compile(r'([\s\S]*?)
')
- match = pattern.search(html_content)
-
- if match:
- markdown_content = match.group(1)
- markdown_content += f"
ComfyUI: {core.comfy_ui_revision}[{comfy_ui_hash[:6]}]({core.comfy_ui_commit_datetime.date()})"
- # markdown_content += f"
()"
- markdown_content += f"
Manager: {version_str}"
-
- try:
- if core.comfy_ui_required_commit_datetime.date() > core.comfy_ui_commit_datetime.date():
- markdown_content = f'Your ComfyUI is too OUTDATED!!!
' + markdown_content
- except:
- pass
-
- return web.Response(text=markdown_content, status=200)
- else:
- return web.Response(text="Unable to retrieve Notice", status=200)
- else:
- return web.Response(text="Unable to retrieve Notice", status=200)
-
-
-@PromptServer.instance.routes.get("/manager/reboot")
-def restart(self):
- try:
- sys.stdout.close_log()
- except Exception as e:
- pass
-
- return os.execv(sys.executable, [sys.executable] + sys.argv)
-
-
-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")
-async def save_component(request):
- try:
- data = await request.json()
- name = data['name']
- workflow = data['workflow']
-
- if not os.path.exists(components_path):
- os.mkdir(components_path)
-
- if 'packname' in workflow and workflow['packname'] != '':
- sanitized_name = sanitize_filename(workflow['packname'])+'.pack'
- else:
- sanitized_name = sanitize_filename(name)+'.json'
-
- filepath = os.path.join(components_path, sanitized_name)
- components = {}
- if os.path.exists(filepath):
- with open(filepath) as f:
- components = json.load(f)
-
- components[name] = workflow
-
- with open(filepath, 'w') as f:
- json.dump(components, f, indent=4, sort_keys=True)
- return web.Response(text=filepath, status=200)
- except:
- return web.Response(status=400)
-
-
-@PromptServer.instance.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')]
- pack_files = [f for f in os.listdir(components_path) if f.endswith('.pack')]
-
- components = {}
- for json_file in json_files + pack_files:
- file_path = os.path.join(components_path, json_file)
- with open(file_path, 'r') as file:
- try:
- # When there is a conflict between the .pack and the .json, the pack takes precedence and overrides.
- components.update(json.load(file))
- except json.JSONDecodeError as e:
- print(f"[ComfyUI-Manager] Error decoding component file in file {json_file}: {e}")
-
- return web.json_response(components)
- except Exception as e:
- print(f"[ComfyUI-Manager] failed to load components\n{e}")
- return web.Response(status=400)
-
-
-@PromptServer.instance.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']
- core.write_config()
- else:
- return web.Response(text=core.get_config()['share_option'], status=200)
-
- return web.Response(status=200)
-
-
-def get_openart_auth():
- if not os.path.exists(os.path.join(core.comfyui_manager_path, ".openart_key")):
- return None
- try:
- with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "r") as f:
- openart_key = f.read().strip()
- return openart_key if openart_key else None
- except:
- return None
-
-
-def get_matrix_auth():
- if not os.path.exists(os.path.join(core.comfyui_manager_path, "matrix_auth")):
- return None
- try:
- with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "r") as f:
- matrix_auth = f.read()
- homeserver, username, password = matrix_auth.strip().split("\n")
- if not homeserver or not username or not password:
- return None
- return {
- "homeserver": homeserver,
- "username": username,
- "password": password,
- }
- except:
- return None
-
-
-def get_comfyworkflows_auth():
- if not os.path.exists(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey")):
- return None
- try:
- with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "r") as f:
- share_key = f.read()
- if not share_key.strip():
- return None
- return share_key
- except:
- return None
-
-
-def get_youml_settings():
- if not os.path.exists(os.path.join(core.comfyui_manager_path, ".youml")):
- return None
- try:
- with open(os.path.join(core.comfyui_manager_path, ".youml"), "r") as f:
- youml_settings = f.read().strip()
- return youml_settings if youml_settings else None
- except:
- return None
-
-
-def set_youml_settings(settings):
- with open(os.path.join(core.comfyui_manager_path, ".youml"), "w") as f:
- f.write(settings)
-
-
-@PromptServer.instance.routes.get("/manager/get_openart_auth")
-async def api_get_openart_auth(request):
- # print("Getting stored Matrix credentials...")
- openart_key = get_openart_auth()
- if not openart_key:
- return web.Response(status=404)
- return web.json_response({"openart_key": openart_key})
-
-
-@PromptServer.instance.routes.post("/manager/set_openart_auth")
-async def api_set_openart_auth(request):
- json_data = await request.json()
- openart_key = json_data['openart_key']
- with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "w") as f:
- f.write(openart_key)
- return web.Response(status=200)
-
-
-@PromptServer.instance.routes.get("/manager/get_matrix_auth")
-async def api_get_matrix_auth(request):
- # print("Getting stored Matrix credentials...")
- matrix_auth = get_matrix_auth()
- if not matrix_auth:
- return web.Response(status=404)
- return web.json_response(matrix_auth)
-
-
-@PromptServer.instance.routes.get("/manager/youml/settings")
-async def api_get_youml_settings(request):
- youml_settings = get_youml_settings()
- if not youml_settings:
- return web.Response(status=404)
- return web.json_response(json.loads(youml_settings))
-
-
-@PromptServer.instance.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")
-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
- # print("Getting stored Comfyworkflows.com auth...")
- comfyworkflows_auth = get_comfyworkflows_auth()
- if not comfyworkflows_auth:
- return web.Response(status=404)
- return web.json_response({"comfyworkflows_sharekey" : comfyworkflows_auth})
-
-args.enable_cors_header = "*"
-if hasattr(PromptServer.instance, "app"):
- app = PromptServer.instance.app
- cors_middleware = server.create_cors_middleware(args.enable_cors_header)
- app.middlewares.append(cors_middleware)
-
-
-@PromptServer.instance.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']
- images = json_data['images']
- with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
- json.dump(json_data, file, indent=4)
- return web.Response(status=200)
-
-
-@PromptServer.instance.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)
- return web.Response(status=200, text=json.dumps(data))
-
-
-def set_matrix_auth(json_data):
- homeserver = json_data['homeserver']
- username = json_data['username']
- password = json_data['password']
- with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "w") as f:
- f.write("\n".join([homeserver, username, password]))
-
-
-def set_comfyworkflows_auth(comfyworkflows_sharekey):
- with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "w") as f:
- f.write(comfyworkflows_sharekey)
-
-
-def has_provided_matrix_auth(matrix_auth):
- return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
-
-
-def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
- return comfyworkflows_sharekey.strip()
-
-
-def extract_model_file_names(json_data):
- """Extract unique file names from the input JSON data."""
- file_names = set()
- model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
-
- # Recursively search for file names in the JSON data
- def recursive_search(data):
- if isinstance(data, dict):
- for value in data.values():
- recursive_search(value)
- elif isinstance(data, list):
- for item in data:
- recursive_search(item)
- elif isinstance(data, str) and '.' in data:
- file_names.add(os.path.basename(data)) # file_names.add(data)
-
- recursive_search(json_data)
- return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
-
-
-def find_file_paths(base_dir, file_names):
- """Find the paths of the files in the base directory."""
- file_paths = {}
-
- for root, dirs, files in os.walk(base_dir):
- # Exclude certain directories
- dirs[:] = [d for d in dirs if d not in ['.git']]
-
- for file in files:
- if file in file_names:
- file_paths[file] = os.path.join(root, file)
- return file_paths
-
-
-def compute_sha256_checksum(filepath):
- """Compute the SHA256 checksum of a file, in chunks"""
- sha256 = hashlib.sha256()
- with open(filepath, 'rb') as f:
- for chunk in iter(lambda: f.read(4096), b''):
- sha256.update(chunk)
- return sha256.hexdigest()
-
-
-@PromptServer.instance.routes.post("/manager/share")
-async def share_art(request):
- # get json data
- json_data = await request.json()
-
- matrix_auth = json_data['matrix_auth']
- comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
-
- set_matrix_auth(matrix_auth)
- set_comfyworkflows_auth(comfyworkflows_sharekey)
-
- share_destinations = json_data['share_destinations']
- credits = json_data['credits']
- title = json_data['title']
- description = json_data['description']
- is_nsfw = json_data['is_nsfw']
- prompt = json_data['prompt']
- potential_outputs = json_data['potential_outputs']
- selected_output_index = json_data['selected_output_index']
-
- try:
- output_to_share = potential_outputs[int(selected_output_index)]
- except:
- # for now, pick the first output
- output_to_share = potential_outputs[0]
-
- assert output_to_share['type'] in ('image', 'output')
- output_dir = folder_paths.get_output_directory()
-
- if output_to_share['type'] == 'image':
- asset_filename = output_to_share['image']['filename']
- asset_subfolder = output_to_share['image']['subfolder']
-
- if output_to_share['image']['type'] == 'temp':
- output_dir = folder_paths.get_temp_directory()
- else:
- asset_filename = output_to_share['output']['filename']
- asset_subfolder = output_to_share['output']['subfolder']
-
- if asset_subfolder:
- asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename)
- else:
- asset_filepath = os.path.join(output_dir, asset_filename)
-
- # get the mime type of the asset
- assetFileType = mimetypes.guess_type(asset_filepath)[0]
-
- share_website_host = "UNKNOWN"
- if "comfyworkflows" in share_destinations:
- share_website_host = "https://comfyworkflows.com"
- share_endpoint = f"{share_website_host}/api"
-
- # get presigned urls
- async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
- async with session.post(
- f"{share_endpoint}/get_presigned_urls",
- json={
- "assetFileName": asset_filename,
- "assetFileType": assetFileType,
- "workflowJsonFileName" : 'workflow.json',
- "workflowJsonFileType" : 'application/json',
- },
- ) as resp:
- assert resp.status == 200
- presigned_urls_json = await resp.json()
- assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
- assetFileKey = presigned_urls_json["assetFileKey"]
- workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
- workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
-
- # upload asset
- async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
- async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
- assert resp.status == 200
-
- # upload workflow json
- async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
- async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
- assert resp.status == 200
-
- model_filenames = extract_model_file_names(prompt['workflow'])
- model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
-
- models_info = {}
- for filename, filepath in model_file_paths.items():
- models_info[filename] = {
- "filename": filename,
- "sha256_checksum": compute_sha256_checksum(filepath),
- "relative_path": os.path.relpath(filepath, folder_paths.base_path),
- }
-
- # make a POST request to /api/upload_workflow with form data key values
- async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
- form = aiohttp.FormData()
- if comfyworkflows_sharekey:
- form.add_field("shareKey", comfyworkflows_sharekey)
- form.add_field("source", "comfyui_manager")
- form.add_field("assetFileKey", assetFileKey)
- form.add_field("assetFileType", assetFileType)
- form.add_field("workflowJsonFileKey", workflowJsonFileKey)
- form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
- form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
- form.add_field("shareWorkflowCredits", credits)
- form.add_field("shareWorkflowTitle", title)
- form.add_field("shareWorkflowDescription", description)
- form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
- form.add_field("currentSnapshot", json.dumps(get_current_snapshot()))
- form.add_field("modelsInfo", json.dumps(models_info))
-
- async with session.post(
- f"{share_endpoint}/upload_workflow",
- data=form,
- ) as resp:
- assert resp.status == 200
- upload_workflow_json = await resp.json()
- workflowId = upload_workflow_json["workflowId"]
-
- # check if the user has provided Matrix credentials
- if "matrix" in share_destinations:
- comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
- filename = os.path.basename(asset_filepath)
- content_type = assetFileType
-
- try:
- from matrix_client.api import MatrixHttpApi
- from matrix_client.client import MatrixClient
-
- homeserver = 'matrix.org'
- if matrix_auth:
- homeserver = matrix_auth.get('homeserver', 'matrix.org')
- homeserver = homeserver.replace("http://", "https://")
- if not homeserver.startswith("https://"):
- homeserver = "https://" + homeserver
-
- client = MatrixClient(homeserver)
- try:
- token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
- if not token:
- return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400)
- except:
- return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400)
-
- matrix = MatrixHttpApi(homeserver, token=token)
- with open(asset_filepath, 'rb') as f:
- mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
-
- workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri']
-
- text_content = ""
- if title:
- text_content += f"{title}\n"
- if description:
- text_content += f"{description}\n"
- if credits:
- text_content += f"\ncredits: {credits}\n"
- response = matrix.send_message(comfyui_share_room_id, text_content)
- response = matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
- response = matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
- except:
- import traceback
- traceback.print_exc()
- return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
-
- return web.json_response({
- "comfyworkflows": {
- "url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
- },
- "matrix": {
- "success": None if "matrix" not in share_destinations else True
- }
- }, content_type='application/json', status=200)
-
-
-def sanitize(data):
- return data.replace("<", "<").replace(">", ">")
-
-
-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)
- target = core.lookup_customnode_by_url(json_obj, custom_node_url)
-
- if target is not None:
- PromptServer.instance.send_sync("cm-api-try-install-customnode",
- {"sender": sender, "target": target, "msg": msg})
- else:
- print(f"[ComfyUI Manager API] Failed to try install - Unknown custom node url '{custom_node_url}'")
-
-
-def confirm_try_install(sender, custom_node_url, msg):
- asyncio.run(_confirm_try_install(sender, custom_node_url, msg))
-
-
-cm_global.register_api('cm.try-install-custom-node', confirm_try_install)
-
-
-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 = os.path.join(core.cache_dir, cache_uri)
-
- json_obj = await core.get_data(uri, True)
-
- with core.cache_lock:
- with open(cache_uri, "w", encoding='utf-8') as file:
- json.dump(json_obj, file, indent=4, sort_keys=True)
- print(f"[ComfyUI-Manager] default cache updated: {uri}")
-
- a = get_cache("custom-node-list.json")
- b = get_cache("extension-node-map.json")
- c = get_cache("model-list.json")
- d = get_cache("alter-list.json")
-
- await asyncio.gather(a, b, c, d)
-
-
-threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()
-
-
-if not os.path.exists(core.config_path):
- core.get_config()
- core.write_config()
-
-
-WEB_DIRECTORY = "js"
NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS']
-cm_global.register_extension('ComfyUI-Manager',
- {'version': version,
- 'name': 'ComfyUI Manager',
- 'nodes': {'Terminal Log //CM'},
- 'description': 'It provides the ability to manage custom nodes in ComfyUI.', })
+
diff --git a/cm-cli.py b/cm-cli.py
new file mode 100644
index 0000000..2015317
--- /dev/null
+++ b/cm-cli.py
@@ -0,0 +1,465 @@
+
+import os
+import sys
+import traceback
+import json
+import asyncio
+import subprocess
+
+sys.path.append("./glob")
+import manager_core as core
+import cm_global
+import git
+
+
+print(f"\n-= ComfyUI-Manager CLI ({core.version_str}) =-\n")
+
+comfyui_manager_path = os.path.dirname(__file__)
+comfy_path = os.environ.get('COMFYUI_PATH')
+
+if comfy_path is None:
+ print(f"WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.\n", file=sys.stderr)
+ comfy_path = os.path.abspath(os.path.join(comfyui_manager_path, '..', '..'))
+
+startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
+custom_nodes_path = os.path.join(comfy_path, 'custom_nodes')
+
+script_path = os.path.join(startup_script_path, "install-scripts.txt")
+restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
+pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json")
+git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
+
+cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
+cm_global.pip_overrides = {}
+if os.path.exists(pip_overrides_path):
+ with open(pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
+ cm_global.pip_overrides = json.load(json_file)
+
+
+processed_install = set()
+
+
+def post_install(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 processed_install:
+ 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_snapshot(snapshot_name):
+ global processed_install
+
+ snapshot_path = os.path.join(core.comfyui_manager_path, 'snapshots', snapshot_name)
+ if not os.path.exists(snapshot_path):
+ print(f"ERROR: `{snapshot_path}` is not exists.")
+ exit(-1)
+
+ try:
+ cloned_repos = []
+ checkout_repos = []
+ skipped_repos = []
+ enabled_repos = []
+ disabled_repos = []
+ is_failed = False
+
+ def extract_infos(msg_lines):
+ nonlocal is_failed
+
+ for x in msg_lines:
+ 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]
+ 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:
+ 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("ERROR: Failed to restore snapshot.")
+
+ except Exception:
+ print("ERROR: Failed to restore snapshot.")
+ traceback.print_exc()
+ exit(-1)
+
+
+def check_comfyui_hash():
+ repo = git.Repo(comfy_path)
+ core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
+
+ comfy_ui_hash = repo.head.commit.hexsha
+ cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
+
+ core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
+
+
+check_comfyui_hash()
+
+
+def read_downgrade_blacklist():
+ try:
+ import configparser
+ config_path = os.path.join(os.path.dirname(__file__), "config.ini")
+ config = configparser.ConfigParser()
+ config.read(config_path)
+ default_conf = config['default']
+
+ if 'downgrade_blacklist' in default_conf:
+ items = default_conf['downgrade_blacklist'].split(',')
+ items = [x.strip() for x in items if x != '']
+ cm_global.pip_downgrade_blacklist += items
+ cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
+ except:
+ pass
+
+
+read_downgrade_blacklist()
+
+
+if not (len(sys.argv) == 2 and sys.argv[1] == 'save-snapshot') and len(sys.argv) < 3:
+ print(f"\npython cm-cli.py [OPTIONS]\n\n"
+ f"OPTIONS:\n"
+ f" [install|uninstall|update|disable|enable|fix] node_name ... ?[--channel ] ?[--mode [remote|local|cache]]\n"
+ f" [update|disable|enable|fix] all ?[--channel ] ?[--mode [remote|local|cache]]\n"
+ f" [simple-show|show] [installed|enabled|not-installed|disabled|all|snapshot|snapshot-list] ?[--channel ] ?[--mode [remote|local|cache]]\n"
+ f" save-snapshot\n"
+ f" restore-snapshot \n"
+ f" cli-only-mode [enable|disable]\n"
+ f" clear\n")
+ exit(-1)
+
+
+channel = 'default'
+mode = 'remote'
+nodes = set()
+
+
+def load_custom_nodes():
+ channel_dict = core.get_channel_dict()
+ if channel not in channel_dict:
+ print(f"ERROR: Invalid channel is specified `--channel {channel}`", file=sys.stderr)
+ exit(-1)
+
+ if mode not in ['remote', 'local', 'cache']:
+ print(f"ERROR: Invalid mode is specified `--mode {mode}`", file=sys.stderr)
+ exit(-1)
+
+ channel_url = channel_dict[channel]
+
+ res = {}
+ json_obj = asyncio.run(core.get_data_by_mode(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
+
+ return res
+
+
+def process_args():
+ global channel
+ global mode
+
+ i = 2
+ while i < len(sys.argv):
+ if sys.argv[i] == '--channel':
+ if i+1 < len(sys.argv):
+ channel = sys.argv[i+1]
+ i += 1
+ elif sys.argv[i] == '--mode':
+ if i+1 < len(sys.argv):
+ mode = sys.argv[i+1]
+ i += 1
+ else:
+ nodes.add(sys.argv[i])
+
+ i += 1
+
+
+process_args()
+custom_node_map = load_custom_nodes()
+
+
+def lookup_node_path(node_name):
+ # Currently, the node_name is used directly as the node_path, but in the future, I plan to allow nicknames.
+ if node_name in custom_node_map:
+ node_path = os.path.join(custom_nodes_path, node_name)
+ return node_path, custom_node_map[node_name]
+
+ print(f"ERROR: invalid node name '{node_name}'")
+ exit(-1)
+
+
+def install_node(node_name, is_all=False):
+ node_path, node_item = lookup_node_path(node_name)
+
+ if os.path.exists(node_path):
+ if not is_all:
+ print(f"[ SKIPPED ] {node_name:50} => Already installed")
+ elif os.path.exists(node_path+'.disabled'):
+ enable_node(node_name)
+ else:
+ res = core.gitclone_install(node_item['files'], instant_execution=True)
+ if not res:
+ print(f"ERROR: An error occurred while installing '{node_name}'.")
+ else:
+ print(f"[INSTALLED] {node_name:50}")
+
+
+def fix_node(node_name, is_all=False):
+ node_path, node_item = lookup_node_path(node_name)
+ if os.path.exists(node_path):
+ res = core.gitclone_fix(node_item['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"[ SKIPPED ]: {node_name:50} => Disabled")
+ elif not is_all:
+ print(f"[ SKIPPED ]: {node_name:50} => Not installed")
+
+
+def uninstall_node(node_name, is_all=False):
+ node_path, node_item = lookup_node_path(node_name)
+ if os.path.exists(node_path) or os.path.exists(node_path+'.disabled'):
+ res = core.gitclone_uninstall(node_item['files'])
+ if not res:
+ print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
+ else:
+ print(f"[UNINSTALLED] {node_name:50}")
+ else:
+ print(f"[ SKIPPED ]: {node_name:50} => Not installed")
+
+
+def update_node(node_name, is_all=False):
+ node_path, node_item = lookup_node_path(node_name)
+ res = core.gitclone_update(node_item['files'], skip_script=True)
+ post_install(node_path)
+ if not res:
+ print(f"ERROR: An error occurred while uninstalling '{node_name}'.")
+
+
+def enable_node(node_name, is_all=False):
+ if node_name == 'ComfyUI-Manager':
+ return
+
+ node_path, _ = lookup_node_path(node_name)
+
+ if os.path.exists(node_path+'.disabled'):
+ current_name = node_path+'.disabled'
+ os.rename(current_name, node_path)
+ print(f"[ENABLED] {node_name:50}")
+ elif os.path.exists(node_path):
+ print(f"[SKIPPED] {node_name:50} => Already enabled")
+ elif not is_all:
+ print(f"[SKIPPED] {node_name:50} => Not installed")
+
+
+def disable_node(node_name, is_all=False):
+ if node_name == 'ComfyUI-Manager':
+ return
+
+ node_path, _ = lookup_node_path(node_name)
+
+ if os.path.exists(node_path):
+ current_name = node_path
+ new_name = node_path+'.disabled'
+ os.rename(current_name, new_name)
+ print(f"[DISABLED] {node_name:50}")
+ elif os.path.exists(node_path+'.disabled'):
+ print(f"[ SKIPPED] {node_name:50} => Already disabled")
+ elif not is_all:
+ print(f"[ SKIPPED] {node_name:50} => Not installed")
+
+
+def show_list(kind, simple=False):
+ for k, v in custom_node_map.items():
+ node_path = os.path.join(custom_nodes_path, k)
+
+ states = set()
+ if os.path.exists(node_path):
+ prefix = '[ ENABLED ] '
+ states.add('installed')
+ states.add('enabled')
+ states.add('all')
+ elif os.path.exists(node_path+'.disabled'):
+ prefix = '[ DISABLED ] '
+ states.add('installed')
+ states.add('disabled')
+ states.add('all')
+ else:
+ prefix = '[ NOT INSTALLED ] '
+ states.add('not-installed')
+ states.add('all')
+
+ if kind in states:
+ if simple:
+ print(f"{k:50}")
+ else:
+ print(f"{prefix} {k:50}(author: {v['author']})")
+
+
+def show_snapshot(simple_mode=False):
+ json_obj = core.get_current_snapshot()
+
+ if simple_mode:
+ print(f"[{json_obj['comfyui']}] comfyui")
+ for k, v in json_obj['git_custom_nodes'].items():
+ print(f"[{v['hash']}] {k}")
+ for v in json_obj['file_custom_nodes']:
+ print(f"[ N/A ] {v['filename']}")
+
+ else:
+ formatted_json = json.dumps(json_obj, ensure_ascii=False, indent=4)
+ print(formatted_json)
+
+
+def show_snapshot_list(simple_mode=False):
+ path = os.path.join(comfyui_manager_path, 'snapshots')
+
+ files = os.listdir(path)
+ json_files = [x for x in files if x.endswith('.json')]
+ for x in sorted(json_files):
+ print(x)
+
+
+def cancel():
+ if os.path.exists(script_path):
+ os.remove(script_path)
+
+ if os.path.exists(restore_snapshot_path):
+ os.remove(restore_snapshot_path)
+
+
+def for_each_nodes(act, allow_all=True):
+ global nodes
+
+ is_all = False
+ if allow_all and 'all' in nodes:
+ is_all = True
+ nodes = [x for x in 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')]
+
+ for x in nodes:
+ try:
+ act(x, is_all=is_all)
+ except Exception as e:
+ print(f"ERROR: {e}")
+ traceback.print_exc()
+
+
+op = sys.argv[1]
+
+
+if op == 'install':
+ for_each_nodes(install_node)
+
+elif op == 'uninstall':
+ for_each_nodes(uninstall_node)
+
+elif op == 'update':
+ for_each_nodes(update_node, allow_all=True)
+
+elif op == 'disable':
+ for_each_nodes(disable_node, allow_all=True)
+
+elif op == 'enable':
+ for_each_nodes(enable_node, allow_all=True)
+
+elif op == 'fix':
+ for_each_nodes(fix_node, allow_all=True)
+
+elif op == 'show':
+ if sys.argv[2] == 'snapshot':
+ show_snapshot()
+ elif sys.argv[2] == 'snapshot-list':
+ show_snapshot_list()
+ else:
+ show_list(sys.argv[2])
+
+elif op == 'simple-show':
+ if sys.argv[2] == 'snapshot':
+ show_snapshot(True)
+ elif sys.argv[2] == 'snapshot-list':
+ show_snapshot_list(True)
+ else:
+ show_list(sys.argv[2], True)
+
+elif op == 'cli-only-mode':
+ cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
+ if sys.argv[2] == 'enable':
+ with open(cli_mode_flag, 'w') as file:
+ pass
+ print(f"\ncli-only-mode: enabled\n")
+ elif sys.argv[2] == 'disable':
+ os.remove(cli_mode_flag)
+ print(f"\ncli-only-mode: disabled\n")
+ else:
+ print(f"\ninvalid value for cli-only-mode: {sys.argv[2]}\n")
+
+elif op == 'save-snapshot':
+ path = core.save_snapshot_with_postfix('snapshot')
+ print(f"Current snapshot is saved as `{path}`")
+
+elif op == 'restore-snapshot':
+ restore_snapshot(sys.argv[2])
+
+elif op == 'clear':
+ cancel()
+
+else:
+ print(f"\nInvalid command `{op}`")
+
+print(f"")
diff --git a/git_helper.py b/git_helper.py
index dbe3271..37d6dcc 100644
--- a/git_helper.py
+++ b/git_helper.py
@@ -176,24 +176,32 @@ def checkout_custom_node_hash(git_custom_node_infos):
if repo_name.endswith('.disabled'):
repo_name = repo_name[:-9]
- item = git_custom_node_infos[repo_name_to_url[repo_name]]
- if item['disabled'] and is_disabled:
- pass
- elif item['disabled'] and not is_disabled:
- # disable
- print(f"DISABLE: {repo_name}")
- new_path = fullpath + ".disabled"
- os.rename(fullpath, new_path)
- pass
- elif not item['disabled'] and is_disabled:
- # enable
- print(f"ENABLE: {repo_name}")
- new_path = fullpath[:-9]
- os.rename(fullpath, new_path)
- fullpath = new_path
- need_checkout = True
+ if repo_name not in repo_name_to_url:
+ if not is_disabled:
+ # should be disabled
+ print(f"DISABLE: {repo_name}")
+ new_path = fullpath + ".disabled"
+ os.rename(fullpath, new_path)
+ need_checkout = False
else:
- need_checkout = True
+ item = git_custom_node_infos[repo_name_to_url[repo_name]]
+ if item['disabled'] and is_disabled:
+ pass
+ elif item['disabled'] and not is_disabled:
+ # disable
+ print(f"DISABLE: {repo_name}")
+ new_path = fullpath + ".disabled"
+ os.rename(fullpath, new_path)
+
+ elif not item['disabled'] and is_disabled:
+ # enable
+ print(f"ENABLE: {repo_name}")
+ new_path = fullpath[:-9]
+ os.rename(fullpath, new_path)
+ fullpath = new_path
+ need_checkout = True
+ else:
+ need_checkout = True
if need_checkout:
repo = git.Repo(fullpath)
@@ -202,6 +210,7 @@ def checkout_custom_node_hash(git_custom_node_infos):
if commit_hash != item['hash']:
print(f"CHECKOUT: {repo_name} [{item['hash']}]")
repo.git.checkout(item['hash'])
+
except Exception:
print(f"Failed to restore snapshots for the custom node '{path}'")
diff --git a/glob/manager_core.py b/glob/manager_core.py
index eb82769..1a9d356 100644
--- a/glob/manager_core.py
+++ b/glob/manager_core.py
@@ -21,8 +21,12 @@ sys.path.append(glob_path)
import cm_global
from manager_util import *
+version = [2, 20]
+version_str = f"V{version[0]}.{version[1]}" + (f'.{version[2]}' if len(version) > 2 else '')
+
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, '..'))
+comfy_path = os.path.abspath(os.path.join(custom_nodes_path, '..'))
channel_list_path = os.path.join(comfyui_manager_path, 'channels.list')
config_path = os.path.join(comfyui_manager_path, "config.ini")
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
@@ -106,6 +110,33 @@ def is_blacklisted(name):
return False
+def is_installed(name):
+ name = name.strip()
+
+ if name.startswith('#'):
+ return True
+
+ pattern = r'([^<>!=]+)([<>!=]=?)(.*)'
+ match = re.search(pattern, name)
+
+ if match:
+ name = match.group(1)
+
+ if name in cm_global.pip_downgrade_blacklist:
+ pips = get_installed_packages()
+
+ if match is None:
+ if name in pips:
+ return True
+ elif match.group(2) in ['<=', '==', '<']:
+ if name in pips:
+ if StrictVersion(pips[name]) >= StrictVersion(match.group(3)):
+ print(f"[ComfyUI-Manager] skip black listed pip installation: '{name}'")
+ return True
+
+ return name.lower() in get_installed_packages()
+
+
def get_channel_dict():
global channel_dict
@@ -337,7 +368,7 @@ def __win_check_git_pull(path):
process.wait()
-def execute_install_script(url, repo_path, lazy_mode=False):
+def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False):
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@@ -353,12 +384,12 @@ def execute_install_script(url, repo_path, lazy_mode=False):
if package_name:
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
if package_name.strip() != "":
- try_install_script(url, repo_path, install_cmd)
+ try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
if os.path.exists(install_script_path):
print(f"Install: install script")
install_cmd = [sys.executable, "install.py"]
- try_install_script(url, repo_path, install_cmd)
+ try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
return True
@@ -466,8 +497,8 @@ def is_valid_url(url):
return False
-def gitclone_install(files):
- print(f"install: {files}")
+def gitclone_install(files, instant_execution=False):
+ print(f"Install: {files}")
for url in files:
if not is_valid_url(url):
print(f"Invalid git url: '{url}'")
@@ -481,7 +512,7 @@ def gitclone_install(files):
repo_path = os.path.join(custom_nodes_path, repo_name)
# Clone the repository from the remote URL
- if platform.system() == 'Windows':
+ if not instant_execution and platform.system() == 'Windows':
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", custom_nodes_path, url])
if res != 0:
return False
@@ -490,7 +521,7 @@ def gitclone_install(files):
repo.git.clear_cache()
repo.close()
- if not execute_install_script(url, repo_path):
+ if not execute_install_script(url, repo_path, instant_execution=instant_execution):
return False
except Exception as e:
@@ -566,13 +597,17 @@ def is_file_created_within_one_day(file_path):
return time_difference <= 86400
-async def get_data_by_mode(mode, filename):
+async def get_data_by_mode(mode, filename, channel_url=None):
try:
if mode == "local":
uri = os.path.join(comfyui_manager_path, filename)
json_obj = await get_data(uri)
else:
- uri = get_config()['channel_url'] + '/' + filename
+ if channel_url is None:
+ uri = get_config()['channel_url'] + '/' + filename
+ else:
+ uri = channel_url + '/' + filename
+
cache_uri = str(simple_hash(uri))+'_'+filename
cache_uri = os.path.join(cache_dir, cache_uri)
@@ -585,7 +620,6 @@ async def get_data_by_mode(mode, filename):
with open(cache_uri, "w", encoding='utf-8') as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
else:
- uri = get_config()['channel_url'] + '/' + filename
json_obj = await get_data(uri)
with cache_lock:
with open(cache_uri, "w", encoding='utf-8') as file:
@@ -611,6 +645,9 @@ def gitclone_fix(files):
repo_name = os.path.splitext(os.path.basename(url))[0]
repo_path = os.path.join(custom_nodes_path, repo_name)
+ if os.path.exists(repo_path+'.disabled'):
+ repo_path = repo_path+'.disabled'
+
if not execute_install_script(url, repo_path):
return False
@@ -653,7 +690,7 @@ def rmtree(path):
def gitclone_uninstall(files):
import os
- print(f"uninstall: {files}")
+ print(f"Uninstall: {files}")
for url in files:
if url.endswith("/"):
url = url[:-1]
@@ -708,7 +745,7 @@ def gitclone_set_active(files, is_disable):
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_path = os.path.join(custom_nodes_path, dir_name)
- # safey check
+ # safety check
if dir_path == '/' or dir_path[1:] == ":/" or dir_path == '':
print(f"{action_name}(git-clone) error: invalid path '{dir_path}' for '{url}'")
return False
@@ -739,7 +776,7 @@ def gitclone_set_active(files, is_disable):
return True
-def gitclone_update(files):
+def gitclone_update(files, instant_execution=False, skip_script=False):
import os
print(f"Update: {files}")
@@ -749,16 +786,26 @@ def gitclone_update(files):
try:
repo_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
repo_path = os.path.join(custom_nodes_path, repo_name)
+
+ if os.path.exists(repo_path+'.disabled'):
+ repo_path = repo_path+'.disabled'
+
git_pull(repo_path)
- if not execute_install_script(url, repo_path, lazy_mode=True):
- return False
+ if not skip_script:
+ if instant_execution:
+ if not execute_install_script(url, repo_path, lazy_mode=False, instant_execution=True):
+ return False
+ else:
+ if not execute_install_script(url, repo_path, lazy_mode=True):
+ return False
except Exception as e:
print(f"Update(git-clone) error: {url} / {e}", file=sys.stderr)
return False
- print("Update was successful.")
+ if not skip_script:
+ print("Update was successful.")
return True
@@ -832,3 +879,73 @@ def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do
item['installed'] = 'Disabled'
else:
item['installed'] = 'False'
+
+
+def get_current_snapshot():
+ # Get ComfyUI hash
+ repo_path = comfy_path
+
+ print(f"comfy_path: {comfy_path}")
+ if not os.path.exists(os.path.join(repo_path, '.git')):
+ print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
+ return {}
+
+ repo = git.Repo(repo_path)
+ comfyui_commit_hash = repo.head.commit.hexsha
+
+ git_custom_nodes = {}
+ file_custom_nodes = []
+
+ # Get custom nodes hash
+ for path in os.listdir(custom_nodes_path):
+ fullpath = os.path.join(custom_nodes_path, path)
+
+ if os.path.isdir(fullpath):
+ is_disabled = path.endswith(".disabled")
+
+ try:
+ git_dir = os.path.join(fullpath, '.git')
+
+ if not os.path.exists(git_dir):
+ continue
+
+ repo = git.Repo(fullpath)
+ commit_hash = repo.head.commit.hexsha
+ url = repo.remotes.origin.url
+ git_custom_nodes[url] = {
+ 'hash': commit_hash,
+ 'disabled': is_disabled
+ }
+
+ except:
+ print(f"Failed to extract snapshots for the custom node '{path}'.")
+
+ elif path.endswith('.py'):
+ is_disabled = path.endswith(".py.disabled")
+ filename = os.path.basename(path)
+ item = {
+ 'filename': filename,
+ 'disabled': is_disabled
+ }
+
+ file_custom_nodes.append(item)
+
+ return {
+ 'comfyui': comfyui_commit_hash,
+ 'git_custom_nodes': git_custom_nodes,
+ 'file_custom_nodes': file_custom_nodes,
+ }
+
+
+def save_snapshot_with_postfix(postfix):
+ now = datetime.now()
+
+ date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
+ file_name = f"{date_time_format}_{postfix}"
+
+ path = os.path.join(comfyui_manager_path, 'snapshots', f"{file_name}.json")
+ with open(path, "w") as json_file:
+ json.dump(get_current_snapshot(), json_file, indent=4)
+
+ return file_name+'.json'
+
diff --git a/glob/manager_server.py b/glob/manager_server.py
new file mode 100644
index 0000000..4ee7584
--- /dev/null
+++ b/glob/manager_server.py
@@ -0,0 +1,1623 @@
+import mimetypes
+import traceback
+
+import folder_paths
+import locale
+import subprocess # don't remove this
+import concurrent
+import nodes
+import hashlib
+import os
+import sys
+import threading
+import re
+import shutil
+import git
+
+from server import PromptServer
+import manager_core as core
+import cm_global
+
+print(f"### Loading: ComfyUI-Manager ({core.version_str})")
+
+comfy_ui_hash = "-"
+
+
+def handle_stream(stream, prefix):
+ stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
+ for msg in stream:
+ if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
+ if msg.startswith('100%'):
+ print('\r' + msg, end="", file=sys.stderr),
+ else:
+ print('\r' + msg[:-1], end="", file=sys.stderr),
+ else:
+ if prefix == '[!]':
+ print(prefix, msg, end="", file=sys.stderr)
+ else:
+ print(prefix, msg, end="")
+
+
+from comfy.cli_args import args
+import latent_preview
+
+
+class ManagerFuncsInComfyUI(core.ManagerFuncs):
+ def get_current_preview_method(self):
+ if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
+ return "auto"
+ elif args.preview_method == latent_preview.LatentPreviewMethod.Latent2RGB:
+ return "latent2rgb"
+ elif args.preview_method == latent_preview.LatentPreviewMethod.TAESD:
+ return "taesd"
+ else:
+ return "none"
+
+ def run_script(self, cmd, cwd='.'):
+ if len(cmd) > 0 and cmd[0].startswith("#"):
+ print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
+ return 0
+
+ process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
+
+ stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, ""))
+ stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]"))
+
+ stdout_thread.start()
+ stderr_thread.start()
+
+ stdout_thread.join()
+ stderr_thread.join()
+
+ return process.wait()
+
+
+core.manager_funcs = ManagerFuncsInComfyUI()
+
+sys.path.append('../..')
+
+from torchvision.datasets.utils import download_url
+
+core.comfy_path = os.path.dirname(folder_paths.__file__)
+core.js_path = os.path.join(core.comfy_path, "web", "extensions")
+
+local_db_model = os.path.join(core.comfyui_manager_path, "model-list.json")
+local_db_alter = os.path.join(core.comfyui_manager_path, "alter-list.json")
+local_db_custom_node_list = os.path.join(core.comfyui_manager_path, "custom-node-list.json")
+local_db_extension_node_mappings = os.path.join(core.comfyui_manager_path, "extension-node-map.json")
+components_path = os.path.join(core.comfyui_manager_path, 'components')
+
+
+def set_preview_method(method):
+ if method == 'auto':
+ args.preview_method = latent_preview.LatentPreviewMethod.Auto
+ elif method == 'latent2rgb':
+ args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
+ elif method == 'taesd':
+ args.preview_method = latent_preview.LatentPreviewMethod.TAESD
+ else:
+ args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
+
+ core.get_config()['preview_method'] = args.preview_method
+
+
+set_preview_method(core.get_config()['preview_method'])
+
+
+def set_badge_mode(mode):
+ core.get_config()['badge_mode'] = mode
+
+
+def set_default_ui_mode(mode):
+ core.get_config()['default_ui'] = mode
+
+
+def set_component_policy(mode):
+ core.get_config()['component_policy'] = mode
+
+
+def set_double_click_policy(mode):
+ core.get_config()['double_click_policy'] = mode
+
+
+def print_comfyui_version():
+ global comfy_ui_hash
+
+ is_detached = False
+ try:
+ repo = git.Repo(os.path.dirname(folder_paths.__file__))
+ core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
+
+ comfy_ui_hash = repo.head.commit.hexsha
+ cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
+
+ core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
+ cm_global.variables['comfyui.commit_datetime'] = core.comfy_ui_commit_datetime
+
+ is_detached = repo.head.is_detached
+ current_branch = repo.active_branch.name
+
+ try:
+ if core.comfy_ui_commit_datetime.date() < core.comfy_ui_required_commit_datetime.date():
+ print(f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n")
+ except:
+ pass
+
+ # process on_revision_detected -->
+ if 'cm.on_revision_detected_handler' in cm_global.variables:
+ for k, f in cm_global.variables['cm.on_revision_detected_handler']:
+ try:
+ f(core.comfy_ui_revision)
+ except Exception:
+ print(f"[ERROR] '{k}' on_revision_detected_handler")
+ traceback.print_exc()
+
+ del cm_global.variables['cm.on_revision_detected_handler']
+ else:
+ print(f"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated.")
+ # <--
+
+ if current_branch == "master":
+ print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'")
+ else:
+ print(f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'")
+ except:
+ if is_detached:
+ print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'")
+ else:
+ print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)")
+
+
+print_comfyui_version()
+
+
+async def populate_github_stats(json_obj, filename, silent=False):
+ uri = os.path.join(core.comfyui_manager_path, filename)
+ with open(uri, "r", encoding='utf-8') as f:
+ github_stats = json.load(f)
+ if 'custom_nodes' in json_obj:
+ for i, node in enumerate(json_obj['custom_nodes']):
+ url = node['reference']
+ if url in github_stats:
+ json_obj['custom_nodes'][i]['stars'] = github_stats[url]['stars']
+ json_obj['custom_nodes'][i]['last_update'] = github_stats[url]['last_update']
+ else:
+ json_obj['custom_nodes'][i]['stars'] = -1
+ json_obj['custom_nodes'][i]['last_update'] = -1
+ return json_obj
+
+
+def setup_environment():
+ git_exe = core.get_config()['git_exe']
+
+ if git_exe != '':
+ git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe)
+
+
+setup_environment()
+
+# Expand Server api
+
+import server
+from aiohttp import web
+import aiohttp
+import json
+import zipfile
+import urllib.request
+
+
+def get_model_dir(data):
+ if data['save_path'] != 'default':
+ if '..' in data['save_path'] or data['save_path'].startswith('/'):
+ print(f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'.")
+ base_model = "etc"
+ else:
+ if data['save_path'].startswith("custom_nodes"):
+ base_model = os.path.join(core.comfy_path, data['save_path'])
+ else:
+ base_model = os.path.join(folder_paths.models_dir, data['save_path'])
+ else:
+ model_type = data['type']
+ if model_type == "checkpoints":
+ base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0]
+ elif model_type == "unclip":
+ base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0]
+ elif model_type == "VAE":
+ base_model = folder_paths.folder_names_and_paths["vae"][0][0]
+ elif model_type == "lora":
+ base_model = folder_paths.folder_names_and_paths["loras"][0][0]
+ elif model_type == "T2I-Adapter":
+ base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
+ elif model_type == "T2I-Style":
+ base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
+ elif model_type == "controlnet":
+ base_model = folder_paths.folder_names_and_paths["controlnet"][0][0]
+ elif model_type == "clip_vision":
+ base_model = folder_paths.folder_names_and_paths["clip_vision"][0][0]
+ elif model_type == "gligen":
+ base_model = folder_paths.folder_names_and_paths["gligen"][0][0]
+ elif model_type == "upscale":
+ base_model = folder_paths.folder_names_and_paths["upscale_models"][0][0]
+ elif model_type == "embeddings":
+ base_model = folder_paths.folder_names_and_paths["embeddings"][0][0]
+ else:
+ base_model = "etc"
+
+ return base_model
+
+
+def get_model_path(data):
+ base_model = get_model_dir(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):
+ if do_fetch:
+ print("Start fetching...", end="")
+ elif do_update:
+ print("Start updating...", end="")
+ elif do_update_check:
+ print("Start update check...", end="")
+
+ def process_custom_node(item):
+ core.check_a_custom_node_installed(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)
+
+ 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'])
+ if update_exists:
+ print(f"\x1b[2K\rUpdate done.")
+ else:
+ print(f"\x1b[2K\rAll extensions are already up-to-date.")
+ elif do_update_check:
+ print(f"\x1b[2K\rUpdate check done.")
+
+
+def nickname_filter(json_obj):
+ preemptions_map = {}
+
+ for k, x in json_obj.items():
+ if 'preemptions' in x[1]:
+ for y in x[1]['preemptions']:
+ preemptions_map[y] = k
+ elif k.endswith("/ComfyUI"):
+ for y in x[0]:
+ preemptions_map[y] = k
+
+ updates = {}
+ for k, x in json_obj.items():
+ removes = set()
+ for y in x[0]:
+ k2 = preemptions_map.get(y)
+ if k2 is not None and k != k2:
+ removes.add(y)
+
+ if len(removes) > 0:
+ updates[k] = [y for y in x[0] if y not in removes]
+
+ for k, v in updates.items():
+ json_obj[k][0] = v
+
+ return json_obj
+
+
+@PromptServer.instance.routes.get("/customnode/getmappings")
+async def fetch_customnode_mappings(request):
+ mode = request.rel_url.query["mode"]
+
+ nickname_mode = False
+ if mode == "nickname":
+ mode = "local"
+ nickname_mode = True
+
+ json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json')
+
+ if nickname_mode:
+ json_obj = nickname_filter(json_obj)
+
+ all_nodes = set()
+ patterns = []
+ for k, x in json_obj.items():
+ all_nodes.update(set(x[0]))
+
+ if 'nodename_pattern' in x[1]:
+ patterns.append((x[1]['nodename_pattern'], x[0]))
+
+ missing_nodes = set(nodes.NODE_CLASS_MAPPINGS.keys()) - all_nodes
+
+ for x in missing_nodes:
+ for pat, item in patterns:
+ if re.match(pat, x):
+ item.append(x)
+
+ return web.json_response(json_obj, content_type='application/json')
+
+
+@PromptServer.instance.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')
+
+ check_custom_nodes_installed(json_obj, True)
+
+ update_exists = any('custom_nodes' in json_obj and 'installed' in node and node['installed'] == 'Update' for node in
+ json_obj['custom_nodes'])
+
+ if update_exists:
+ return web.Response(status=201)
+
+ return web.Response(status=200)
+ except:
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.get("/customnode/update_all")
+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')
+
+ check_custom_nodes_installed(json_obj, do_update=True)
+
+ 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']
+
+ res = {'updated': updated, 'failed': failed}
+
+ if len(updated) == 0 and len(failed) == 0:
+ status = 200
+ else:
+ status = 201
+
+ return web.json_response(res, status=status, content_type='application/json')
+ except:
+ return web.Response(status=400)
+ finally:
+ core.clear_pip_cache()
+
+
+def convert_markdown_to_html(input_text):
+ pattern_a = re.compile(r'\[a/([^]]+)\]\(([^)]+)\)')
+ pattern_w = re.compile(r'\[w/([^]]+)\]')
+ pattern_i = re.compile(r'\[i/([^]]+)\]')
+ pattern_bold = re.compile(r'\*\*([^*]+)\*\*')
+ pattern_white = re.compile(r'%%([^*]+)%%')
+
+ def replace_a(match):
+ return f"{match.group(1)}"
+
+ def replace_w(match):
+ return f"{match.group(1)}
"
+
+ def replace_i(match):
+ return f"{match.group(1)}
"
+
+ def replace_bold(match):
+ return f"{match.group(1)}"
+
+ def replace_white(match):
+ return f"{match.group(1)}"
+
+ input_text = input_text.replace('\\[', '[').replace('\\]', ']').replace('<', '<').replace('>', '>')
+
+ result_text = re.sub(pattern_a, replace_a, input_text)
+ result_text = re.sub(pattern_w, replace_w, result_text)
+ result_text = re.sub(pattern_i, replace_i, result_text)
+ result_text = re.sub(pattern_bold, replace_bold, result_text)
+ result_text = re.sub(pattern_white, replace_white, result_text)
+
+ return result_text.replace("\n", "
")
+
+
+def populate_markdown(x):
+ if 'description' in x:
+ x['description'] = convert_markdown_to_html(x['description'])
+
+ if 'name' in x:
+ x['name'] = x['name'].replace('<', '<').replace('>', '>')
+
+ if 'title' in x:
+ x['title'] = x['title'].replace('<', '<').replace('>', '>')
+
+
+@PromptServer.instance.routes.get("/customnode/getlist")
+async def fetch_customnode_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
+
+ if request.rel_url.query["mode"] == "local":
+ channel = 'local'
+ else:
+ channel = core.get_config()['channel_url']
+
+ json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json')
+ json_obj = await populate_github_stats(json_obj, "github-stats.json")
+
+ def is_ignored_notice(code):
+ global version
+
+ if code is not None and code.startswith('#NOTICE_'):
+ try:
+ notice_version = [int(x) for x in code[8:].split('.')]
+ return notice_version[0] < version[0] or (notice_version[0] == version[0] and notice_version[1] <= 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'))]
+
+ check_custom_nodes_installed(json_obj, False, not skip_update)
+
+ for x in json_obj['custom_nodes']:
+ populate_markdown(x)
+
+ if channel != 'local':
+ found = 'custom'
+
+ for name, url in core.get_channel_dict().items():
+ if url == channel:
+ found = name
+ break
+
+ channel = found
+
+ json_obj['channel'] = channel
+
+ return web.json_response(json_obj, 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
+
+ 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 process_model(item):
+ model_path = get_model_path(item)
+ item['installed'] = 'None'
+
+ if model_path is not None:
+ if os.path.exists(model_path):
+ item['installed'] = 'True'
+ else:
+ item['installed'] = 'False'
+
+ with concurrent.futures.ThreadPoolExecutor(8) as executor:
+ for item in json_obj['models']:
+ executor.submit(process_model, item)
+
+
+@PromptServer.instance.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')
+
+ check_model_installed(json_obj)
+
+ for x in json_obj['models']:
+ populate_markdown(x)
+
+ return web.json_response(json_obj, content_type='application/json')
+
+
+@PromptServer.instance.routes.get("/snapshot/get_current")
+async def get_snapshot_list(request):
+ json_obj = core.get_current_snapshot()
+ return web.json_response(json_obj, content_type='application/json')
+
+
+@PromptServer.instance.routes.get("/snapshot/getlist")
+async def get_snapshot_list(request):
+ snapshots_directory = os.path.join(core.comfyui_manager_path, 'snapshots')
+ items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')]
+ items.sort(reverse=True)
+ return web.json_response({'items': items}, content_type='application/json')
+
+
+@PromptServer.instance.routes.get("/snapshot/remove")
+async def remove_snapshot(request):
+ try:
+ target = request.rel_url.query["target"]
+
+ path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json")
+ if os.path.exists(path):
+ os.remove(path)
+
+ return web.Response(status=200)
+ except:
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.get("/snapshot/restore")
+async def remove_snapshot(request):
+ try:
+ target = request.rel_url.query["target"]
+
+ path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json")
+ if os.path.exists(path):
+ if not os.path.exists(core.startup_script_path):
+ os.makedirs(core.startup_script_path)
+
+ target_path = os.path.join(core.startup_script_path, "restore-snapshot.json")
+ shutil.copy(path, target_path)
+
+ print(f"Snapshot restore scheduled: `{target}`")
+ return web.Response(status=200)
+
+ print(f"Snapshot file not found: `{path}`")
+ return web.Response(status=400)
+ except:
+ return web.Response(status=400)
+
+
+@PromptServer.instance.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')
+ except:
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.get("/snapshot/save")
+async def save_snapshot(request):
+ try:
+ core.save_snapshot_with_postfix('snapshot')
+ return web.Response(status=200)
+ except:
+ return web.Response(status=400)
+
+
+def unzip_install(files):
+ temp_filename = 'manager-temp.zip'
+ for url in files:
+ if url.endswith("/"):
+ url = url[:-1]
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
+
+ req = urllib.request.Request(url, headers=headers)
+ response = urllib.request.urlopen(req)
+ data = response.read()
+
+ with open(temp_filename, 'wb') as f:
+ f.write(data)
+
+ with zipfile.ZipFile(temp_filename, 'r') as zip_ref:
+ zip_ref.extractall(core.custom_nodes_path)
+
+ os.remove(temp_filename)
+ except Exception as e:
+ print(f"Install(unzip) error: {url} / {e}", file=sys.stderr)
+ return False
+
+ print("Installation was successful.")
+ return True
+
+
+def download_url_with_agent(url, save_path):
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
+
+ req = urllib.request.Request(url, headers=headers)
+ response = urllib.request.urlopen(req)
+ data = response.read()
+
+ if not os.path.exists(os.path.dirname(save_path)):
+ os.makedirs(os.path.dirname(save_path))
+
+ with open(save_path, 'wb') as f:
+ f.write(data)
+
+ except Exception as e:
+ print(f"Download error: {url} / {e}", file=sys.stderr)
+ return False
+
+ print("Installation was successful.")
+ return True
+
+
+def copy_install(files, js_path_name=None):
+ for url in files:
+ if url.endswith("/"):
+ url = url[:-1]
+ try:
+ if url.endswith(".py"):
+ download_url(url, core.custom_nodes_path)
+ else:
+ path = os.path.join(core.js_path, js_path_name) if js_path_name is not None else core.js_path
+ if not os.path.exists(path):
+ os.makedirs(path)
+ download_url(url, path)
+
+ except Exception as e:
+ print(f"Install(copy) error: {url} / {e}", file=sys.stderr)
+ return False
+
+ print("Installation was successful.")
+ return True
+
+
+def copy_uninstall(files, js_path_name='.'):
+ for url in files:
+ if url.endswith("/"):
+ url = url[:-1]
+ dir_name = os.path.basename(url)
+ base_path = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
+ file_path = os.path.join(base_path, dir_name)
+
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ elif os.path.exists(file_path + ".disabled"):
+ os.remove(file_path + ".disabled")
+ except Exception as e:
+ print(f"Uninstall(copy) error: {url} / {e}", file=sys.stderr)
+ return False
+
+ print("Uninstallation was successful.")
+ return True
+
+
+def copy_set_active(files, is_disable, js_path_name='.'):
+ if is_disable:
+ action_name = "Disable"
+ else:
+ action_name = "Enable"
+
+ for url in files:
+ if url.endswith("/"):
+ url = url[:-1]
+ dir_name = os.path.basename(url)
+ base_path = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
+ file_path = os.path.join(base_path, dir_name)
+
+ try:
+ if is_disable:
+ current_name = file_path
+ new_name = file_path + ".disabled"
+ else:
+ current_name = file_path + ".disabled"
+ new_name = file_path
+
+ os.rename(current_name, new_name)
+
+ except Exception as e:
+ print(f"{action_name}(copy) error: {url} / {e}", file=sys.stderr)
+
+ return False
+
+ print(f"{action_name} was successful.")
+ return True
+
+
+@PromptServer.instance.routes.post("/customnode/install")
+async def install_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 == "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)
+
+
+@PromptServer.instance.routes.post("/customnode/fix")
+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'])
+ else:
+ return web.Response(status=400)
+
+ 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)
+
+ 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.get("/customnode/install/git_url")
+async def install_custom_node_git_url(request):
+ res = False
+ if "url" in request.rel_url.query:
+ url = request.rel_url.query['url']
+ res = core.gitclone_install([url])
+
+ if res:
+ print(f"After restarting ComfyUI, please refresh the browser.")
+ return web.Response(status=200)
+
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.get("/customnode/install/pip")
+async def install_custom_node_git_url(request):
+ res = False
+ if "packages" in request.rel_url.query:
+ packages = request.rel_url.query['packages']
+ core.pip_install(packages.split(' '))
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.routes.post("/customnode/uninstall")
+async def uninstall_custom_node(request):
+ json_data = await request.json()
+
+ install_type = json_data['install_type']
+
+ print(f"Uninstall custom node '{json_data['title']}'")
+
+ 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:
+ 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/update")
+async def update_custom_node(request):
+ json_data = await request.json()
+
+ install_type = json_data['install_type']
+
+ print(f"Update custom node '{json_data['title']}'")
+
+ res = False
+
+ if install_type == "git-clone":
+ res = core.gitclone_update(json_data['files'])
+
+ 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.get("/comfyui_manager/update_comfyui")
+async def update_comfyui(request):
+ print(f"Update ComfyUI")
+
+ try:
+ repo_path = os.path.dirname(folder_paths.__file__)
+
+ if not os.path.exists(os.path.join(repo_path, '.git')):
+ print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
+ return web.Response(status=400)
+
+ # version check
+ repo = git.Repo(repo_path)
+
+ if repo.head.is_detached:
+ core.switch_to_default_branch(repo)
+
+ current_branch = repo.active_branch
+ branch_name = current_branch.name
+
+ if current_branch.tracking_branch() is None:
+ print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})")
+ remote_name = 'origin'
+ else:
+ remote_name = current_branch.tracking_branch().remote_name
+ remote = repo.remote(name=remote_name)
+
+ try:
+ remote.fetch()
+ except Exception as e:
+ if 'detected dubious' in str(e):
+ print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on 'ComfyUI' repository")
+ safedir_path = core.comfy_path.replace('\\', '/')
+ subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path])
+ try:
+ remote.fetch()
+ except Exception:
+ print(f"\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n"
+ f"-----------------------------------------------------------------------------------------\n"
+ f'git config --global --add safe.directory "{safedir_path}"\n'
+ f"-----------------------------------------------------------------------------------------\n")
+
+ commit_hash = repo.head.commit.hexsha
+ remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
+
+ if commit_hash != remote_commit_hash:
+ core.git_pull(repo_path)
+ core.execute_install_script("ComfyUI", repo_path)
+ return web.Response(status=201)
+ else:
+ return web.Response(status=200)
+ except Exception as e:
+ print(f"ComfyUI update fail: {e}", file=sys.stderr)
+ pass
+
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.post("/customnode/toggle_active")
+async def toggle_active(request):
+ json_data = await request.json()
+
+ install_type = json_data['install_type']
+ is_disabled = json_data['installed'] == "Disabled"
+
+ 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))
+
+ if res:
+ return web.json_response({}, content_type='application/json')
+
+ return web.Response(status=400)
+
+
+@PromptServer.instance.routes.post("/model/install")
+async def install_model(request):
+ json_data = await request.json()
+
+ model_path = get_model_path(json_data)
+
+ res = False
+
+ try:
+ if model_path is not None:
+ print(f"Install model '{json_data['name']}' into '{model_path}'")
+
+ model_url = json_data['url']
+ if not core.get_config()['model_download_by_agent'] and (
+ model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')):
+ model_dir = get_model_dir(json_data)
+ download_url(model_url, model_dir, filename=json_data['filename'])
+
+ return web.json_response({}, content_type='application/json')
+ else:
+ res = download_url_with_agent(model_url, model_path)
+ else:
+ print(f"Model installation error: invalid model type - {json_data['type']}")
+
+ if res:
+ return web.json_response({}, content_type='application/json')
+ except Exception as e:
+ print(f"[ERROR] {e}", file=sys.stderr)
+ pass
+
+ return web.Response(status=400)
+
+
+class ManagerTerminalHook:
+ def write_stderr(self, msg):
+ PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg})
+
+ def write_stdout(self, msg):
+ PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg})
+
+
+manager_terminal_hook = ManagerTerminalHook()
+
+
+@PromptServer.instance.routes.get("/manager/terminal")
+async def terminal_mode(request):
+ if "mode" in request.rel_url.query:
+ if request.rel_url.query['mode'] == 'true':
+ sys.__comfyui_manager_terminal_hook.add_hook('cm', manager_terminal_hook)
+ else:
+ sys.__comfyui_manager_terminal_hook.remove_hook('cm')
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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'])
+ core.write_config()
+ else:
+ return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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'])
+ core.write_config()
+ else:
+ return web.Response(text=core.get_config()['badge_mode'], status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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'])
+ core.write_config()
+ else:
+ return web.Response(text=core.get_config()['default_ui'], status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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'])
+ core.write_config()
+ else:
+ return web.Response(text=core.get_config()['component_policy'], status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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'])
+ core.write_config()
+ else:
+ return web.Response(text=core.get_config()['double_click_policy'], status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.routes.get("/manager/channel_url_list")
+async def channel_url_list(request):
+ channels = core.get_channel_dict()
+ if "value" in request.rel_url.query:
+ channel_url = channels.get(request.rel_url.query['value'])
+ if channel_url is not None:
+ core.get_config()['channel_url'] = channel_url
+ core.write_config()
+ else:
+ selected = 'custom'
+ selected_url = core.get_config()['channel_url']
+
+ for name, url in channels.items():
+ if url == selected_url:
+ selected = name
+ break
+
+ res = {'selected': selected,
+ 'list': core.get_channel_list()}
+ return web.json_response(res, status=200)
+
+ return web.Response(status=200)
+
+
+@PromptServer.instance.routes.get("/manager/notice")
+async def get_notice(request):
+ url = "github.com"
+ path = "/ltdrdata/ltdrdata.github.io/wiki/News"
+
+ async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ async with session.get(f"https://{url}{path}") as response:
+ if response.status == 200:
+ # html_content = response.read().decode('utf-8')
+ html_content = await response.text()
+
+ pattern = re.compile(r'([\s\S]*?)
')
+ match = pattern.search(html_content)
+
+ if match:
+ markdown_content = match.group(1)
+ markdown_content += f"
ComfyUI: {core.comfy_ui_revision}[{comfy_ui_hash[:6]}]({core.comfy_ui_commit_datetime.date()})"
+ # markdown_content += f"
()"
+ markdown_content += f"
Manager: {version_str}"
+
+ try:
+ if core.comfy_ui_required_commit_datetime.date() > core.comfy_ui_commit_datetime.date():
+ markdown_content = f'Your ComfyUI is too OUTDATED!!!
' + markdown_content
+ except:
+ pass
+
+ return web.Response(text=markdown_content, status=200)
+ else:
+ return web.Response(text="Unable to retrieve Notice", status=200)
+ else:
+ return web.Response(text="Unable to retrieve Notice", status=200)
+
+
+@PromptServer.instance.routes.get("/manager/reboot")
+def restart(self):
+ try:
+ sys.stdout.close_log()
+ except Exception as e:
+ pass
+
+ return os.execv(sys.executable, [sys.executable] + sys.argv)
+
+
+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")
+async def save_component(request):
+ try:
+ data = await request.json()
+ name = data['name']
+ workflow = data['workflow']
+
+ if not os.path.exists(components_path):
+ os.mkdir(components_path)
+
+ if 'packname' in workflow and workflow['packname'] != '':
+ sanitized_name = sanitize_filename(workflow['packname']) + '.pack'
+ else:
+ sanitized_name = sanitize_filename(name) + '.json'
+
+ filepath = os.path.join(components_path, sanitized_name)
+ components = {}
+ if os.path.exists(filepath):
+ with open(filepath) as f:
+ components = json.load(f)
+
+ components[name] = workflow
+
+ with open(filepath, 'w') as f:
+ json.dump(components, f, indent=4, sort_keys=True)
+ return web.Response(text=filepath, status=200)
+ except:
+ return web.Response(status=400)
+
+
+@PromptServer.instance.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')]
+ pack_files = [f for f in os.listdir(components_path) if f.endswith('.pack')]
+
+ components = {}
+ for json_file in json_files + pack_files:
+ file_path = os.path.join(components_path, json_file)
+ with open(file_path, 'r') as file:
+ try:
+ # When there is a conflict between the .pack and the .json, the pack takes precedence and overrides.
+ components.update(json.load(file))
+ except json.JSONDecodeError as e:
+ print(f"[ComfyUI-Manager] Error decoding component file in file {json_file}: {e}")
+
+ return web.json_response(components)
+ except Exception as e:
+ print(f"[ComfyUI-Manager] failed to load components\n{e}")
+ return web.Response(status=400)
+
+
+@PromptServer.instance.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']
+ core.write_config()
+ else:
+ return web.Response(text=core.get_config()['share_option'], status=200)
+
+ return web.Response(status=200)
+
+
+def get_openart_auth():
+ if not os.path.exists(os.path.join(core.comfyui_manager_path, ".openart_key")):
+ return None
+ try:
+ with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "r") as f:
+ openart_key = f.read().strip()
+ return openart_key if openart_key else None
+ except:
+ return None
+
+
+def get_matrix_auth():
+ if not os.path.exists(os.path.join(core.comfyui_manager_path, "matrix_auth")):
+ return None
+ try:
+ with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "r") as f:
+ matrix_auth = f.read()
+ homeserver, username, password = matrix_auth.strip().split("\n")
+ if not homeserver or not username or not password:
+ return None
+ return {
+ "homeserver": homeserver,
+ "username": username,
+ "password": password,
+ }
+ except:
+ return None
+
+
+def get_comfyworkflows_auth():
+ if not os.path.exists(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey")):
+ return None
+ try:
+ with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "r") as f:
+ share_key = f.read()
+ if not share_key.strip():
+ return None
+ return share_key
+ except:
+ return None
+
+
+def get_youml_settings():
+ if not os.path.exists(os.path.join(core.comfyui_manager_path, ".youml")):
+ return None
+ try:
+ with open(os.path.join(core.comfyui_manager_path, ".youml"), "r") as f:
+ youml_settings = f.read().strip()
+ return youml_settings if youml_settings else None
+ except:
+ return None
+
+
+def set_youml_settings(settings):
+ with open(os.path.join(core.comfyui_manager_path, ".youml"), "w") as f:
+ f.write(settings)
+
+
+@PromptServer.instance.routes.get("/manager/get_openart_auth")
+async def api_get_openart_auth(request):
+ # print("Getting stored Matrix credentials...")
+ openart_key = get_openart_auth()
+ if not openart_key:
+ return web.Response(status=404)
+ return web.json_response({"openart_key": openart_key})
+
+
+@PromptServer.instance.routes.post("/manager/set_openart_auth")
+async def api_set_openart_auth(request):
+ json_data = await request.json()
+ openart_key = json_data['openart_key']
+ with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "w") as f:
+ f.write(openart_key)
+ return web.Response(status=200)
+
+
+@PromptServer.instance.routes.get("/manager/get_matrix_auth")
+async def api_get_matrix_auth(request):
+ # print("Getting stored Matrix credentials...")
+ matrix_auth = get_matrix_auth()
+ if not matrix_auth:
+ return web.Response(status=404)
+ return web.json_response(matrix_auth)
+
+
+@PromptServer.instance.routes.get("/manager/youml/settings")
+async def api_get_youml_settings(request):
+ youml_settings = get_youml_settings()
+ if not youml_settings:
+ return web.Response(status=404)
+ return web.json_response(json.loads(youml_settings))
+
+
+@PromptServer.instance.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")
+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
+ # print("Getting stored Comfyworkflows.com auth...")
+ comfyworkflows_auth = get_comfyworkflows_auth()
+ if not comfyworkflows_auth:
+ return web.Response(status=404)
+ return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
+
+
+args.enable_cors_header = "*"
+if hasattr(PromptServer.instance, "app"):
+ app = PromptServer.instance.app
+ cors_middleware = server.create_cors_middleware(args.enable_cors_header)
+ app.middlewares.append(cors_middleware)
+
+
+@PromptServer.instance.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']
+ images = json_data['images']
+ with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
+ json.dump(json_data, file, indent=4)
+ return web.Response(status=200)
+
+
+@PromptServer.instance.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)
+ return web.Response(status=200, text=json.dumps(data))
+
+
+def set_matrix_auth(json_data):
+ homeserver = json_data['homeserver']
+ username = json_data['username']
+ password = json_data['password']
+ with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "w") as f:
+ f.write("\n".join([homeserver, username, password]))
+
+
+def set_comfyworkflows_auth(comfyworkflows_sharekey):
+ with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "w") as f:
+ f.write(comfyworkflows_sharekey)
+
+
+def has_provided_matrix_auth(matrix_auth):
+ return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
+
+
+def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
+ return comfyworkflows_sharekey.strip()
+
+
+def extract_model_file_names(json_data):
+ """Extract unique file names from the input JSON data."""
+ file_names = set()
+ model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
+
+ # Recursively search for file names in the JSON data
+ def recursive_search(data):
+ if isinstance(data, dict):
+ for value in data.values():
+ recursive_search(value)
+ elif isinstance(data, list):
+ for item in data:
+ recursive_search(item)
+ elif isinstance(data, str) and '.' in data:
+ file_names.add(os.path.basename(data)) # file_names.add(data)
+
+ recursive_search(json_data)
+ return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
+
+
+def find_file_paths(base_dir, file_names):
+ """Find the paths of the files in the base directory."""
+ file_paths = {}
+
+ for root, dirs, files in os.walk(base_dir):
+ # Exclude certain directories
+ dirs[:] = [d for d in dirs if d not in ['.git']]
+
+ for file in files:
+ if file in file_names:
+ file_paths[file] = os.path.join(root, file)
+ return file_paths
+
+
+def compute_sha256_checksum(filepath):
+ """Compute the SHA256 checksum of a file, in chunks"""
+ sha256 = hashlib.sha256()
+ with open(filepath, 'rb') as f:
+ for chunk in iter(lambda: f.read(4096), b''):
+ sha256.update(chunk)
+ return sha256.hexdigest()
+
+
+@PromptServer.instance.routes.post("/manager/share")
+async def share_art(request):
+ # get json data
+ json_data = await request.json()
+
+ matrix_auth = json_data['matrix_auth']
+ comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
+
+ set_matrix_auth(matrix_auth)
+ set_comfyworkflows_auth(comfyworkflows_sharekey)
+
+ share_destinations = json_data['share_destinations']
+ credits = json_data['credits']
+ title = json_data['title']
+ description = json_data['description']
+ is_nsfw = json_data['is_nsfw']
+ prompt = json_data['prompt']
+ potential_outputs = json_data['potential_outputs']
+ selected_output_index = json_data['selected_output_index']
+
+ try:
+ output_to_share = potential_outputs[int(selected_output_index)]
+ except:
+ # for now, pick the first output
+ output_to_share = potential_outputs[0]
+
+ assert output_to_share['type'] in ('image', 'output')
+ output_dir = folder_paths.get_output_directory()
+
+ if output_to_share['type'] == 'image':
+ asset_filename = output_to_share['image']['filename']
+ asset_subfolder = output_to_share['image']['subfolder']
+
+ if output_to_share['image']['type'] == 'temp':
+ output_dir = folder_paths.get_temp_directory()
+ else:
+ asset_filename = output_to_share['output']['filename']
+ asset_subfolder = output_to_share['output']['subfolder']
+
+ if asset_subfolder:
+ asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename)
+ else:
+ asset_filepath = os.path.join(output_dir, asset_filename)
+
+ # get the mime type of the asset
+ assetFileType = mimetypes.guess_type(asset_filepath)[0]
+
+ share_website_host = "UNKNOWN"
+ if "comfyworkflows" in share_destinations:
+ share_website_host = "https://comfyworkflows.com"
+ share_endpoint = f"{share_website_host}/api"
+
+ # get presigned urls
+ async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ async with session.post(
+ f"{share_endpoint}/get_presigned_urls",
+ json={
+ "assetFileName": asset_filename,
+ "assetFileType": assetFileType,
+ "workflowJsonFileName": 'workflow.json',
+ "workflowJsonFileType": 'application/json',
+ },
+ ) as resp:
+ assert resp.status == 200
+ presigned_urls_json = await resp.json()
+ assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
+ assetFileKey = presigned_urls_json["assetFileKey"]
+ workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
+ workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
+
+ # upload asset
+ async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
+ assert resp.status == 200
+
+ # upload workflow json
+ async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
+ assert resp.status == 200
+
+ model_filenames = extract_model_file_names(prompt['workflow'])
+ model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
+
+ models_info = {}
+ for filename, filepath in model_file_paths.items():
+ models_info[filename] = {
+ "filename": filename,
+ "sha256_checksum": compute_sha256_checksum(filepath),
+ "relative_path": os.path.relpath(filepath, folder_paths.base_path),
+ }
+
+ # make a POST request to /api/upload_workflow with form data key values
+ async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ form = aiohttp.FormData()
+ if comfyworkflows_sharekey:
+ form.add_field("shareKey", comfyworkflows_sharekey)
+ form.add_field("source", "comfyui_manager")
+ form.add_field("assetFileKey", assetFileKey)
+ form.add_field("assetFileType", assetFileType)
+ form.add_field("workflowJsonFileKey", workflowJsonFileKey)
+ form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
+ form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
+ form.add_field("shareWorkflowCredits", credits)
+ form.add_field("shareWorkflowTitle", title)
+ form.add_field("shareWorkflowDescription", description)
+ form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
+ form.add_field("currentSnapshot", json.dumps(core.get_current_snapshot()))
+ form.add_field("modelsInfo", json.dumps(models_info))
+
+ async with session.post(
+ f"{share_endpoint}/upload_workflow",
+ data=form,
+ ) as resp:
+ assert resp.status == 200
+ upload_workflow_json = await resp.json()
+ workflowId = upload_workflow_json["workflowId"]
+
+ # check if the user has provided Matrix credentials
+ if "matrix" in share_destinations:
+ comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
+ filename = os.path.basename(asset_filepath)
+ content_type = assetFileType
+
+ try:
+ from matrix_client.api import MatrixHttpApi
+ from matrix_client.client import MatrixClient
+
+ homeserver = 'matrix.org'
+ if matrix_auth:
+ homeserver = matrix_auth.get('homeserver', 'matrix.org')
+ homeserver = homeserver.replace("http://", "https://")
+ if not homeserver.startswith("https://"):
+ homeserver = "https://" + homeserver
+
+ client = MatrixClient(homeserver)
+ try:
+ token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
+ if not token:
+ return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
+ except:
+ return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
+
+ matrix = MatrixHttpApi(homeserver, token=token)
+ with open(asset_filepath, 'rb') as f:
+ mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
+
+ workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri']
+
+ text_content = ""
+ if title:
+ text_content += f"{title}\n"
+ if description:
+ text_content += f"{description}\n"
+ if credits:
+ text_content += f"\ncredits: {credits}\n"
+ response = matrix.send_message(comfyui_share_room_id, text_content)
+ response = matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
+ response = matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
+ except:
+ import traceback
+ traceback.print_exc()
+ return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
+
+ return web.json_response({
+ "comfyworkflows": {
+ "url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
+ },
+ "matrix": {
+ "success": None if "matrix" not in share_destinations else True
+ }
+ }, content_type='application/json', status=200)
+
+
+def sanitize(data):
+ return data.replace("<", "<").replace(">", ">")
+
+
+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)
+ target = core.lookup_customnode_by_url(json_obj, custom_node_url)
+
+ if target is not None:
+ PromptServer.instance.send_sync("cm-api-try-install-customnode",
+ {"sender": sender, "target": target, "msg": msg})
+ else:
+ print(f"[ComfyUI Manager API] Failed to try install - Unknown custom node url '{custom_node_url}'")
+
+
+def confirm_try_install(sender, custom_node_url, msg):
+ asyncio.run(_confirm_try_install(sender, custom_node_url, msg))
+
+
+cm_global.register_api('cm.try-install-custom-node', confirm_try_install)
+
+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 = os.path.join(core.cache_dir, cache_uri)
+
+ json_obj = await core.get_data(uri, True)
+
+ with core.cache_lock:
+ with open(cache_uri, "w", encoding='utf-8') as file:
+ json.dump(json_obj, file, indent=4, sort_keys=True)
+ print(f"[ComfyUI-Manager] default cache updated: {uri}")
+
+ a = get_cache("custom-node-list.json")
+ b = get_cache("extension-node-map.json")
+ c = get_cache("model-list.json")
+ d = get_cache("alter-list.json")
+
+ await asyncio.gather(a, b, c, d)
+
+
+threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()
+
+if not os.path.exists(core.config_path):
+ core.get_config()
+ core.write_config()
+
+
+cm_global.register_extension('ComfyUI-Manager',
+ {'version': core.version,
+ 'name': 'ComfyUI Manager',
+ 'nodes': {'Terminal Log //CM'},
+ 'description': 'It provides the ability to manage custom nodes in ComfyUI.', })
+
diff --git a/prestartup_script.py b/prestartup_script.py
index e419f30..c3476c9 100644
--- a/prestartup_script.py
+++ b/prestartup_script.py
@@ -459,40 +459,39 @@ if os.path.exists(restore_snapshot_path):
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture)
- with open(restore_snapshot_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
- info = json.load(json_file)
- 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):
- 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}")
- this_exit_code += process_wrap(install_cmd, repo_path)
-
- if this_exit_code != 0:
- print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
-
- except Exception as e:
- print(e)
+ 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):
+ 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}")
+ this_exit_code += process_wrap(install_cmd, repo_path)
+
+ 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: