Compare commits

...

82 Commits

Author SHA1 Message Date
Dr.Lt.Data
285d0c067f fix: message fix 2024-11-14 18:34:50 +09:00
Dr.Lt.Data
a6956ddafb Merge branch 'main' into manager-ext 2024-11-14 02:27:04 +09:00
Dr.Lt.Data
f2bc05f5f1 Merge branch 'feat/cnr' into manager-ext 2024-11-11 16:52:03 +09:00
Dr.Lt.Data
1bd64e97cc Merge branch 'main' into feat/cnr 2024-11-11 16:51:56 +09:00
Dr.Lt.Data
a534296d97 Merge branch 'feat/cnr' into manager-ext 2024-11-11 13:43:45 +09:00
Dr.Lt.Data
b703384f6b Merge branch 'main' into feat/cnr 2024-11-11 13:42:13 +09:00
Dr.Lt.Data
722b40df80 add comfyui-manager to favorites 2024-11-05 20:02:38 +09:00
Dr.Lt.Data
8c58353e25 Merge branch 'feat/cnr' into manager-ext 2024-11-05 19:52:28 +09:00
Dr.Lt.Data
d968c55e48 Merge branch 'main' into feat/cnr 2024-11-05 19:52:17 +09:00
Dr.Lt.Data
e3aea8c3e2 move model manager backend code to manager-core 2024-11-05 19:51:09 +09:00
Dr.Lt.Data
3b8b91d648 Merge branch 'feat/cnr' into manager-ext 2024-10-30 03:53:41 +09:00
Dr.Lt.Data
bc4126f526 Merge branch 'main' into feat/cnr 2024-10-30 03:49:32 +09:00
Dr.Lt.Data
a735f60790 provide addtional channels to the manager-core 2024-10-27 23:46:38 +09:00
Dr.Lt.Data
7bc1a944af Merge branch 'main' into manager-ext 2024-10-20 14:07:49 +09:00
Dr.Lt.Data
41d4ba9721 Merge branch 'main' into feat/cnr 2024-10-20 14:07:08 +09:00
Dr.Lt.Data
164f25fb43 fix: robust config writer 2024-10-16 12:38:44 +09:00
Dr.Lt.Data
57861e9cbd Merge branch 'feat/cnr' into manager-ext 2024-10-16 08:37:16 +09:00
Dr.Lt.Data
5e5e567181 Merge branch 'main' into feat/cnr 2024-10-16 08:36:21 +09:00
Dr.Lt.Data
fd62ada1a6 Merge branch 'feat/cnr' into manager-ext 2024-10-15 23:36:51 +09:00
Dr.Lt.Data
a6eaba7e18 Merge branch 'main' into feat/cnr 2024-10-15 23:36:42 +09:00
Dr.Lt.Data
929453d105 MODIFIED: remove config.ini to <user_path>/default/manager-ext.ini
FIXED: confusing of config handler with manager-core
2024-10-14 00:31:16 +09:00
Dr.Lt.Data
559d4c1185 Merge branch 'feat/cnr' into manager-ext 2024-10-13 17:07:01 +09:00
Dr.Lt.Data
f8221b9b5d Merge branch 'main' into feat/cnr 2024-10-13 17:06:19 +09:00
Dr.Lt.Data
c1fc8aabdc Merge branch 'feat/cnr' into manager-ext 2024-10-08 19:48:34 +09:00
Dr.Lt.Data
f4442972bc Merge branch 'main' into feat/cnr 2024-10-08 19:47:38 +09:00
Dr.Lt.Data
851742e5e7 add migration code 2024-10-05 16:50:26 +09:00
Dr.Lt.Data
adf3f8d094 Merge branch 'feat/cnr' into manager-ext 2024-10-05 15:36:35 +09:00
Dr.Lt.Data
aa4b3d81ba Merge branch 'main' into feat/cnr 2024-10-05 15:35:48 +09:00
Dr.Lt.Data
cbb6432803 Merge branch 'main' into feat/cnr 2024-09-29 17:18:47 +09:00
Dr.Lt.Data
95df2ad212 fix: invalid components path 2024-09-26 10:17:28 +09:00
Dr.Lt.Data
b333342d5f Merge branch 'feat/cnr' into manager-ext 2024-09-26 09:52:53 +09:00
Dr.Lt.Data
14afc8d998 Merge branch 'main' into feat/cnr 2024-09-26 09:51:01 +09:00
Dr.Lt.Data
e83e15b9fc Merge branch 'main' into feat/cnr 2024-09-26 08:57:29 +09:00
Dr.Lt.Data
e34b59f16d Merge branch 'feat/cnr' into manager-ext 2024-09-24 09:04:26 +09:00
Dr.Lt.Data
76db17c7f8 Merge branch 'main' into feat/cnr 2024-09-24 09:03:32 +09:00
Dr.Lt.Data
f8306c0896 remove legacy code 2024-09-24 02:50:00 +09:00
Dr.Lt.Data
f6572e3f5a Merge branch 'feat/cnr' into manager-ext 2024-09-24 02:21:11 +09:00
Dr.Lt.Data
d48c936770 Merge branch 'main' into feat/cnr 2024-09-24 02:07:19 +09:00
Dr.Lt.Data
c694764c7a update README 2024-09-23 02:31:22 +09:00
Dr.Lt.Data
da42eca04a wip: remove manager core code 2024-09-23 02:22:00 +09:00
Dr.Lt.Data
527c994d43 Merge branch 'main' into feat/cnr 2024-09-23 01:18:25 +09:00
Dr.Lt.Data
800faf96d4 Merge branch 'main' into feat/cnr 2024-09-21 01:38:51 +09:00
Dr.Lt.Data
2c3a11012f Merge branch 'main' into feat/cnr 2024-09-19 02:45:39 +09:00
Dr.Lt.Data
95311cb225 Merge branch 'main' into feat/cnr 2024-09-19 01:57:15 +09:00
Dr.Lt.Data
70471b54f6 hotfix: updating cnr node - invalid garbage handling 2024-09-18 01:21:32 +09:00
Dr.Lt.Data
f0205c8eba Merge branch 'main' into feat/cnr 2024-09-17 23:30:36 +09:00
Dr.Lt.Data
1a5fa290a3 Merge branch 'main' into feat/cnr 2024-09-13 01:46:04 +09:00
Dr.Lt.Data
4fc50d5019 Merge branch 'main' into feat/cnr 2024-09-11 00:08:36 +09:00
Dr.Lt.Data
a1c90ceb52 modify: close button -> back button 2024-09-08 16:51:41 +09:00
Dr.Lt.Data
ecda9bd34e add favorites button 2024-09-08 16:42:36 +09:00
Dr.Lt.Data
a952009d4a modified: remove uninstall/switch/disable button for ComfyUI-Manager in the list
feat: support favorites list
2024-09-08 15:53:54 +09:00
Dr.Lt.Data
6f2e1345b2 Merge branch 'main' into feat/cnr 2024-09-08 13:48:35 +09:00
Dr.Lt.Data
7b93c831de Merge branch 'main' into feat/cnr 2024-09-05 22:18:56 +09:00
Dr.Lt.Data
80e1fcd672 Merge branch 'main' into feat/cnr 2024-09-01 15:49:03 +09:00
Dr.Lt.Data
bff8dbee30 Merge branch 'main' into feat/cnr 2024-08-29 21:53:37 +09:00
Dr.Lt.Data
32c828670a fix: update_all - nightly version issue 2024-08-29 21:27:10 +09:00
Dr.Lt.Data
ad1faee2ef fix: snapshot 2024-08-28 02:09:00 +09:00
Dr.Lt.Data
005fa14254 fix: execute_install_script - missing pip install except first pip item in requirements.txt 2024-08-24 18:40:25 +09:00
Dr.Lt.Data
7b60b69968 Merge branch 'main' into feat/cnr 2024-08-24 17:20:59 +09:00
Dr.Lt.Data
ed123750d9 postponed processing for cnr switch & migration 2024-08-22 03:38:22 +09:00
Dr.Lt.Data
bede95cd05 improve: better comfyui switch
show 'nightly' if current commit is latest commit.
2024-08-22 02:10:04 +09:00
Dr.Lt.Data
693a226a41 improve: comfyui version switch
top 4 + nightly
2024-08-22 02:08:23 +09:00
Dr.Lt.Data
7ec2793c9a Merge branch 'main' into feat/cnr 2024-08-22 01:47:55 +09:00
Dr.Lt.Data
a1f7f7069f comfyui version switch 2024-08-21 01:33:55 +09:00
Dr.Lt.Data
f74d8cb470 print stash message.
https://github.com/ltdrdata/ComfyUI-Manager/issues/976#issuecomment-2295670323
2024-08-21 00:45:04 +09:00
Dr.Lt.Data
b02cb2b833 Merge branch 'main' into feat/cnr 2024-08-18 13:02:39 +09:00
Dr.Lt.Data
243b65961f unknown fix 2024-08-17 16:55:23 +09:00
Dr.Lt.Data
a6d20b0865 Merge branch 'main' into feat/cnr 2024-08-17 16:35:22 +09:00
Dr.Lt.Data
06b79287e2 Merge branch 'main' into feat/cnr 2024-08-16 00:10:02 +09:00
Dr.Lt.Data
e906d27606 fix: nightly url mismatch if ssh github url is used
fix: don't show pure cnr node unless default channel
2024-08-15 23:44:12 +09:00
Dr.Lt.Data
0968dd85aa fix: undefined show_message
fix: invalid disable for nightly
2024-08-15 22:45:37 +09:00
Dr.Lt.Data
75240a028a Merge branch 'main' into feat/cnr 2024-08-14 02:02:33 +09:00
Dr.Lt.Data
3335c82350 Merge branch 'main' into feat/cnr 2024-08-08 22:52:07 +09:00
Dr.Lt.Data
e16e72cbbd feat: config.ini - skip_migration_check is supported. 2024-08-04 22:39:13 +09:00
Dr.Lt.Data
0b6f7962a4 fix: should not be displayed switch button if unknown node 2024-08-04 16:34:40 +09:00
Dr.Lt.Data
ea3413be9b conservative migration system
keep original repo name as possible if unknown node
2024-08-04 16:26:23 +09:00
Dr.Lt.Data
10055f578b Merge branch 'main' into feat/cnr 2024-08-04 16:03:25 +09:00
Dr.Lt.Data
cddd000848 Merge branch 'main' into feat/cnr 2024-08-02 03:25:30 +09:00
Dr.Lt.Data
cdb400d32b implement: invalid installation handling
- print invalid installation nodes on terminal
(installed by `comfy registryinstall`)

- show only 'reinstall' menu if invalid installation node in gui
(and show INVALID marker)
2024-07-31 02:08:30 +09:00
Dr.Lt.Data
8e1f792cd1 fix: crash by version handling 2024-07-27 22:08:19 +09:00
Max Klein
f0299e07f9 added support for --no-deps option to node install and reinstall (#886)
* added support for `--no-deps` option to node install and reinstall

* post rebase fixup

* fixup help msg for --no-deps option
2024-07-27 01:52:07 +09:00
Dr.Lt.Data
b3be556837 support CNR 2024-07-25 00:24:58 +09:00
33 changed files with 1934 additions and 5585 deletions

View File

@@ -3,7 +3,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- main-blocked - main
paths: paths:
- "pyproject.toml" - "pyproject.toml"

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
__pycache__/ __pycache__/
.idea/ .idea/
.vscode/ .vscode/
.history/
*.code-workspace
.tmp .tmp
.cache .cache
config.ini config.ini

130
README.md
View File

@@ -1,21 +1,22 @@
# ComfyUI Manager # ComfyUI Manager (Extension)
**ComfyUI-Manager** is an extension designed to enhance the usability of [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It offers management functions to **install, remove, disable, and enable** various custom nodes of ComfyUI. Furthermore, this extension provides a hub feature and convenience functions to access a wide range of information within ComfyUI. **ComfyUI-Manager (Extension)** expands the functionality of [manager-core](https://github.com/Comfy-Org/manager-core), allowing users to access the existing ComfyUI-Manager features.
![menu](misc/menu.jpg) ![menu](misc/menu.jpg)
## NOTICE ## NOTICE
* V2.48.1: Security policy has been changed. Downloads of models in the list are allowed under the 'normal' security level. * V3.0: ComfyUI-Manager (Extension)
* V2.47: Security policy has been changed. The former 'normal' is now 'normal-', and 'normal' no longer allows high-risk features, even if your ComfyUI is local.
* V2.37 Show a ✅ mark to accounts that have been active on GitHub for more than six months.
* V2.33 Security policy is applied.
* V2.21 [cm-cli](docs/en/cm-cli.md) tool is added.
* V2.18 to V2.18.3 is not functioning due to a severe bug. Users on these versions are advised to promptly update to V2.18.4. Please navigate to the `ComfyUI/custom_nodes/ComfyUI-Manager` directory and execute `git pull` to update.
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
## Installation ## Installation
### Installation[method1] (General installation method: ComfyUI-Manager only) ### Installation[method1] installation via manager-core (Recommended)
Search for ComfyUI-Manager in the custom node installation feature of manager-core and install it.
### Installation[method1] manual (Not Recommended)
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps: To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
@@ -24,49 +25,6 @@ To install ComfyUI-Manager in addition to an existing installation of ComfyUI, y
3. Restart ComfyUI 3. Restart ComfyUI
### Installation[method2] (Installation for portable ComfyUI version: ComfyUI-Manager only)
1. install git
- https://git-scm.com/download/win
- standalone version
- select option: use windows default console window
2. Download [scripts/install-manager-for-portable-version.bat](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-manager-for-portable-version.bat) into installed `"ComfyUI_windows_portable"` directory
3. double click `install-manager-for-portable-version.bat` batch file
![portable-install](misc/portable-install.png)
### Installation[method3] (Installation through comfy-cli: install ComfyUI and ComfyUI-Manager at once.)
> RECOMMENDED: comfy-cli provides various features to manage ComfyUI from the CLI.
* **prerequisite: python 3, git**
Windows:
```commandline
python -m venv venv
venv\Scripts\activate
pip install comfy-cli
comfy install
```
Linux/OSX:
```commandline
python -m venv venv
. venv/bin/activate
pip install comfy-cli
comfy install
```
### Installation[method4] (Installation for linux+venv: ComfyUI + ComfyUI-Manager)
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
* **prerequisite: python-is-python3, python3-venv, git**
1. Download [scripts/install-comfyui-venv-linux.sh](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-comfyui-venv-linux.sh) into empty install directory
- ComfyUI will be installed in the subdirectory of the specified directory, and the directory will contain the generated executable script.
2. `chmod +x install-comfyui-venv-linux.sh`
3. `./install-comfyui-venv-linux.sh`
### Installation Precautions ### Installation Precautions
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/ComfyUI-Manager` * **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/ComfyUI-Manager`
* Installing in a compressed file format is not recommended. * Installing in a compressed file format is not recommended.
@@ -79,7 +37,6 @@ To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you c
* You have to rename `ComfyUI/custom_nodes/ComfyUI-Manager-main` to `ComfyUI/custom_nodes/ComfyUI-Manager` * You have to rename `ComfyUI/custom_nodes/ComfyUI-Manager-main` to `ComfyUI/custom_nodes/ComfyUI-Manager`
You can execute ComfyUI by running either `./run_gpu.sh` or `./run_cpu.sh` depending on your system configuration.
## Colab Notebook ## Colab Notebook
This repository provides Colab notebooks that allow you to install and use ComfyUI, including ComfyUI-Manager. To use ComfyUI, [click on this link](https://colab.research.google.com/github/ltdrdata/ComfyUI-Manager/blob/main/notebooks/comfyui_colab_with_manager.ipynb). This repository provides Colab notebooks that allow you to install and use ComfyUI, including ComfyUI-Manager. To use ComfyUI, [click on this link](https://colab.research.google.com/github/ltdrdata/ComfyUI-Manager/blob/main/notebooks/comfyui_colab_with_manager.ipynb).
@@ -87,31 +44,6 @@ This repository provides Colab notebooks that allow you to install and use Comfy
* Support for basic installation of ComfyUI-Manager * Support for basic installation of ComfyUI-Manager
* Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks. * Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks.
## Changes
* **2.38** `Install Custom Nodes` menu is changed to `Custom Nodes Manager`.
* **2.21** [cm-cli](docs/en/cm-cli.md) tool is added.
* **2.4** Copy the connections of the nearest node by double-clicking.
* **2.2.3** Support Components System
* **0.29** Add `Update all` feature
* **0.25** support db channel
* You can directly modify the db channel settings in the `config.ini` file.
* If you want to maintain a new DB channel, please modify the `channels.list` and submit a PR.
* **0.23** support multiple selection
* **0.18.1** `skip update check` feature added.
* A feature that allows quickly opening windows in environments where update checks take a long time.
* **0.17.1** Bug fix for the issue where enable/disable of the web extension was not working. Compatibility patch for StableSwarmUI.
* Requires latest version of ComfyUI (Revision: 1240)
* **0.17** Support preview method setting feature.
* **0.14** Support robust update.
* **0.13** Support additional 'pip' section for install spec.
* **0.12** Better installation support for Windows.
* **0.9** Support keyword search in installer menu.
* **V0.7.1** Bug fix for the issue where updates were not being applied on Windows.
* **For those who have been using versions 0.6, please perform a manual git pull in the custom_nodes/ComfyUI-Manager directory.**
* **V0.7** To address the issue of a slow list refresh, separate the fetch update and update check processes.
* **V0.6** Support extension installation for missing nodes.
* **V0.5** Removed external git program dependencies.
## How To Use ## How To Use
@@ -320,6 +252,10 @@ NODE_CLASS_MAPPINGS.update({
* Use `aria2` as downloader * Use `aria2` as downloader
* [howto](docs/en/use_aria2.md) * [howto](docs/en/use_aria2.md)
* If you add the item `skip_migration_check = True` to `config.ini`, it will not check whether there are nodes that can be migrated at startup.
* This option can be used if performance issues occur in a Colab+GDrive environment.
## Scanner ## Scanner
When you run the `scan.sh` script: When you run the `scan.sh` script:
@@ -375,42 +311,6 @@ When you run the `scan.sh` script:
* Update ComfyUI * Update ComfyUI
## TODO: Unconventional form of custom node list
* https://github.com/diontimmer/Sample-Diffusion-ComfyUI-Extension
* https://github.com/senshilabs/NINJA-plugin
* https://github.com/MockbaTheBorg/Nodes
* https://github.com/StartHua/Comfyui_GPT_Story
* https://github.com/NielsGercama/comfyui_customsampling
* https://github.com/wrightdaniel2017/ComfyUI-VideoLipSync
* https://github.com/bxdsjs/ComfyUI-Image-preprocessing
* https://github.com/SMUELDigital/ComfyUI-ONSET
* https://github.com/SimithWang/comfyui-renameImages
* https://github.com/icefairy64/comfyui-model-tilt
* https://github.com/andrewharp/ComfyUI-EasyNodes
* https://github.com/SimithWang/comfyui-renameImages
* https://github.com/Tcheko243/ComfyUI-Photographer-Alpha7-Nodes
* https://github.com/Limbicnation/ComfyUINodeToolbox
* https://github.com/chenpipi0807/pip_longsize
* https://github.com/APZmedia/ComfyUI-APZmedia-srtTools
## Roadmap
- [x] System displaying information about failed custom nodes import.
- [x] Guide for missing nodes in ComfyUI vanilla nodes.
- [x] Collision checking system for nodes with the same ID across extensions.
- [x] Template sharing system. (-> Component system based on Group Nodes)
- [x] 3rd party API system.
- [ ] Auto migration for custom nodes with changed structures.
- [ ] Version control feature for nodes.
- [ ] List of currently used custom nodes.
- [x] Download support multiple model download.
- [x] Model download via url.
- [x] List sorting (custom nodes).
- [x] List sorting (model).
- [ ] Provides description of node.
# Disclaimer # Disclaimer
* This extension simply provides the convenience of installing custom nodes and does not guarantee their proper functioning. * This extension simply provides the convenience of installing custom nodes and does not guarantee their proper functioning.

View File

@@ -1,16 +1,37 @@
import os legacy_manager_core_path = None
manager_core_path = None
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
if not os.path.exists(cli_mode_flag): def is_manager_core_exists():
from .glob import manager_server global legacy_manager_core_path
from .glob import share_3rdparty global manager_core_path
WEB_DIRECTORY = "js" import os
import folder_paths
comfy_path = os.path.dirname(folder_paths.__file__)
legacy_manager_core_path = os.path.join(comfy_path, 'custom_nodes', 'manager-core')
manager_core_path = legacy_manager_core_path
manager_core_path_file = os.path.join(comfy_path, 'manager_core_path.txt')
if os.path.exists(manager_core_path_file):
with open(manager_core_path_file, 'r') as f:
manager_core_path = os.path.join(f.read().strip(), 'manager-core')
return os.path.exists(manager_core_path) or os.path.exists(legacy_manager_core_path)
if not is_manager_core_exists():
from .modules import migration_server
migration_server.manager_core_path = manager_core_path
WEB_DIRECTORY = "migration_js"
NODE_CLASS_MAPPINGS = {}
else: else:
print(f"\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n") # Main code
from .modules import manager_ext_server
NODE_CLASS_MAPPINGS = {} from .modules import share_3rdparty
__all__ = ['NODE_CLASS_MAPPINGS']
WEB_DIRECTORY = "js"
NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS']

View File

@@ -1,5 +1,3 @@
default::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
recent::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/new
legacy::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/legacy legacy::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/legacy
forked::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/forked forked::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/forked
dev::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/dev dev::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/dev

View File

@@ -9,6 +9,7 @@ files=(
"alter-list.json" "alter-list.json"
"extension-node-map.json" "extension-node-map.json"
"github-stats.json" "github-stats.json"
"extras.json"
"node_db/new/custom-node-list.json" "node_db/new/custom-node-list.json"
"node_db/new/model-list.json" "node_db/new/model-list.json"
"node_db/new/extension-node-map.json" "node_db/new/extension-node-map.json"

1031
cm-cli.py
View File

File diff suppressed because it is too large Load Diff

26
extras.json Normal file
View File

@@ -0,0 +1,26 @@
{
"favorites": [
"comfyui-manager",
"comfyui_ipadapter_plus",
"comfyui-animatediff-evolved",
"comfyui_controlnet_aux",
"comfyui-impact-pack",
"comfyui-custom-scripts",
"comfyui-layerdiffuse",
"comfyui-liveportraitkj",
"aigodlike-comfyui-translation",
"comfyui-reactor-node",
"comfyui_instantid",
"sd-dynamic-thresholding",
"pr-was-node-suite-comfyui-47064894",
"comfyui-advancedliveportrait",
"comfyui_layerstyle",
"efficiency-nodes-comfyui",
"comfyui-crystools",
"comfyui-advanced-controlnet",
"comfyui-videohelpersuite",
"comfyui-kjnodes",
"comfy-mtb",
"comfyui_essentials"
]
}

View File

@@ -1,450 +0,0 @@
import subprocess
import sys
import os
import traceback
import git
import configparser
import re
import json
import yaml
import requests
from tqdm.auto import tqdm
from git.remote import RemoteProgress
def download_url(url, dest_folder, filename=None):
# Ensure the destination folder exists
if not os.path.exists(dest_folder):
os.makedirs(dest_folder)
# Extract filename from URL if not provided
if filename is None:
filename = os.path.basename(url)
# Full path to save the file
dest_path = os.path.join(dest_folder, filename)
# Download the file
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(dest_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
else:
print(f"Failed to download file from {url}")
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
working_directory = os.getcwd()
if os.path.basename(working_directory) != 'custom_nodes':
print(f"WARN: This script should be executed in custom_nodes dir")
print(f"DBG: INFO {working_directory}")
print(f"DBG: INFO {sys.argv}")
# exit(-1)
class GitProgress(RemoteProgress):
def __init__(self):
super().__init__()
self.pbar = tqdm(ascii=True)
def update(self, op_code, cur_count, max_count=None, message=''):
self.pbar.total = max_count
self.pbar.n = cur_count
self.pbar.pos = 0
self.pbar.refresh()
def gitclone(custom_nodes_path, url, target_hash=None):
repo_name = os.path.splitext(os.path.basename(url))[0]
repo_path = os.path.join(custom_nodes_path, repo_name)
# Clone the repository from the remote URL
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
if target_hash is not None:
print(f"CHECKOUT: {repo_name} [{target_hash}]")
repo.git.checkout(target_hash)
repo.git.clear_cache()
repo.close()
def gitcheck(path, do_fetch=False):
try:
# Fetch the latest commits from the remote repository
repo = git.Repo(path)
if repo.head.is_detached:
print("CUSTOM NODE CHECK: True")
return
current_branch = repo.active_branch
branch_name = current_branch.name
remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name)
if do_fetch:
remote.fetch()
# Get the current commit hash and the commit hash of the remote branch
commit_hash = repo.head.commit.hexsha
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
# Compare the commit hashes to determine if the local repository is behind the remote repository
if commit_hash != remote_commit_hash:
# Get the commit dates
commit_date = repo.head.commit.committed_datetime
remote_commit_date = repo.refs[f'{remote_name}/{branch_name}'].object.committed_datetime
# Compare the commit dates to determine if the local repository is behind the remote repository
if commit_date < remote_commit_date:
print("CUSTOM NODE CHECK: True")
else:
print("CUSTOM NODE CHECK: False")
except Exception as e:
print(e)
print("CUSTOM NODE CHECK: Error")
def switch_to_default_branch(repo):
show_result = repo.git.remote("show", "origin")
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
if matches:
default_branch = matches.group(1)
repo.git.checkout(default_branch)
def gitpull(path):
# Check if the path is a git repository
if not os.path.exists(os.path.join(path, '.git')):
raise ValueError('Not a git repository')
# Pull the latest changes from the remote repository
repo = git.Repo(path)
if repo.is_dirty():
repo.git.stash()
commit_hash = repo.head.commit.hexsha
try:
if repo.head.is_detached:
switch_to_default_branch(repo)
current_branch = repo.active_branch
branch_name = current_branch.name
remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name)
remote.fetch()
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
if commit_hash == remote_commit_hash:
print("CUSTOM NODE PULL: None") # there is no update
repo.close()
return
remote.pull()
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
if commit_hash != new_commit_hash:
print("CUSTOM NODE PULL: Success") # update success
else:
print("CUSTOM NODE PULL: Fail") # update fail
except Exception as e:
print(e)
print("CUSTOM NODE PULL: Fail") # unknown git error
repo.close()
def checkout_comfyui_hash(target_hash):
repo_path = os.path.abspath(os.path.join(working_directory, '..')) # ComfyUI dir
repo = git.Repo(repo_path)
commit_hash = repo.head.commit.hexsha
if commit_hash != target_hash:
try:
print(f"CHECKOUT: ComfyUI [{target_hash}]")
repo.git.checkout(target_hash)
except git.GitCommandError as e:
print(f"Error checking out the ComfyUI: {str(e)}")
def checkout_custom_node_hash(git_custom_node_infos):
repo_name_to_url = {}
for url in git_custom_node_infos.keys():
repo_name = url.split('/')[-1]
if repo_name.endswith('.git'):
repo_name = repo_name[:-4]
repo_name_to_url[repo_name] = url
for path in os.listdir(working_directory):
if path.endswith("ComfyUI-Manager"):
continue
fullpath = os.path.join(working_directory, 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
need_checkout = False
repo_name = os.path.basename(fullpath)
if repo_name.endswith('.disabled'):
repo_name = repo_name[:-9]
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:
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)
commit_hash = repo.head.commit.hexsha
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}'")
# clone missing
for k, v in git_custom_node_infos.items():
if not v['disabled']:
repo_name = k.split('/')[-1]
if repo_name.endswith('.git'):
repo_name = repo_name[:-4]
path = os.path.join(working_directory, repo_name)
if not os.path.exists(path):
print(f"CLONE: {path}")
gitclone(working_directory, k, v['hash'])
def invalidate_custom_node_file(file_custom_node_infos):
global nodelist_path
enabled_set = set()
for item in file_custom_node_infos:
if not item['disabled']:
enabled_set.add(item['filename'])
for path in os.listdir(working_directory):
fullpath = os.path.join(working_directory, path)
if not os.path.isdir(fullpath) and fullpath.endswith('.py'):
if path not in enabled_set:
print(f"DISABLE: {path}")
new_path = fullpath+'.disabled'
os.rename(fullpath, new_path)
elif not os.path.isdir(fullpath) and fullpath.endswith('.py.disabled'):
path = path[:-9]
if path in enabled_set:
print(f"ENABLE: {path}")
new_path = fullpath[:-9]
os.rename(fullpath, new_path)
# download missing: just support for 'copy' style
py_to_url = {}
with open(nodelist_path, 'r', encoding="UTF-8") as json_file:
info = json.load(json_file)
for item in info['custom_nodes']:
if item['install_type'] == 'copy':
for url in item['files']:
if url.endswith('.py'):
py = url.split('/')[-1]
py_to_url[py] = url
for item in file_custom_node_infos:
filename = item['filename']
if not item['disabled']:
target_path = os.path.join(working_directory, filename)
if not os.path.exists(target_path) and filename in py_to_url:
url = py_to_url[filename]
print(f"DOWNLOAD: {filename}")
download_url(url, working_directory)
def apply_snapshot(target):
try:
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
if os.path.exists(path):
if not target.endswith('.json') and not target.endswith('.yaml'):
print(f"Snapshot file not found: `{path}`")
print("APPLY SNAPSHOT: False")
return None
with open(path, 'r', encoding="UTF-8") as snapshot_file:
if target.endswith('.json'):
info = json.load(snapshot_file)
elif target.endswith('.yaml'):
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
info = info['custom_nodes']
else:
# impossible case
print("APPLY SNAPSHOT: False")
return None
comfyui_hash = info['comfyui']
git_custom_node_infos = info['git_custom_nodes']
file_custom_node_infos = info['file_custom_nodes']
checkout_comfyui_hash(comfyui_hash)
checkout_custom_node_hash(git_custom_node_infos)
invalidate_custom_node_file(file_custom_node_infos)
print("APPLY SNAPSHOT: True")
if 'pips' in info:
return info['pips']
else:
return None
print(f"Snapshot file not found: `{path}`")
print("APPLY SNAPSHOT: False")
return None
except Exception as e:
print(e)
traceback.print_exc()
print("APPLY SNAPSHOT: False")
return None
def restore_pip_snapshot(pips, options):
non_url = []
local_url = []
non_local_url = []
for k, v in pips.items():
if v == "":
non_url.append(k)
else:
if v.startswith('file:'):
local_url.append(v)
else:
non_local_url.append(v)
failed = []
if '--pip-non-url' in options:
# try all at once
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + non_url)
except:
pass
# fallback
if res != 0:
for x in non_url:
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
pass
if res != 0:
failed.append(x)
if '--pip-non-local-url' in options:
for x in non_local_url:
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
pass
if res != 0:
failed.append(x)
if '--pip-local-url' in options:
for x in local_url:
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
pass
if res != 0:
failed.append(x)
print(f"Installation failed for pip packages: {failed}")
def setup_environment():
config = configparser.ConfigParser()
config.read(config_path)
if 'default' in config and 'git_exe' in config['default'] and config['default']['git_exe'] != '':
git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=config['default']['git_exe'])
setup_environment()
try:
if sys.argv[1] == "--clone":
gitclone(sys.argv[2], sys.argv[3])
elif sys.argv[1] == "--check":
gitcheck(sys.argv[2], False)
elif sys.argv[1] == "--fetch":
gitcheck(sys.argv[2], True)
elif sys.argv[1] == "--pull":
gitpull(sys.argv[2])
elif sys.argv[1] == "--apply-snapshot":
options = set()
for x in sys.argv:
if x in ['--pip-non-url', '--pip-local-url', '--pip-non-local-url']:
options.add(x)
pips = apply_snapshot(sys.argv[2])
if pips and len(options) > 0:
restore_pip_snapshot(pips, options)
sys.exit(0)
except Exception as e:
print(e)
sys.exit(-1)

View File

@@ -1,112 +0,0 @@
import traceback
#
# Global Var
#
# Usage:
# import cm_global
# cm_global.variables['comfyui.revision'] = 1832
# print(f"log mode: {cm_global.variables['logger.enabled']}")
#
variables = {}
#
# Global API
#
# Usage:
# [register API]
# import cm_global
#
# def api_hello(msg):
# print(f"hello: {msg}")
# return msg
#
# cm_global.register_api('hello', api_hello)
#
# [use API]
# import cm_global
#
# test = cm_global.try_call(api='hello', msg='an example')
# print(f"'{test}' is returned")
#
APIs = {}
def register_api(k, f):
global APIs
APIs[k] = f
def try_call(**kwargs):
if 'api' in kwargs:
api_name = kwargs['api']
try:
api = APIs.get(api_name)
if api is not None:
del kwargs['api']
return api(**kwargs)
else:
print(f"WARN: The '{kwargs['api']}' API has not been registered.")
except Exception as e:
print(f"ERROR: An exception occurred while calling the '{api_name}' API.")
raise e
else:
return None
#
# Extension Info
#
# Usage:
# import cm_global
#
# cm_global.extension_infos['my_extension'] = {'version': [0, 1], 'name': 'me', 'description': 'example extension', }
#
extension_infos = {}
on_extension_registered_handlers = {}
def register_extension(extension_name, v):
global extension_infos
global on_extension_registered_handlers
extension_infos[extension_name] = v
if extension_name in on_extension_registered_handlers:
for k, f in on_extension_registered_handlers[extension_name]:
try:
f(extension_name, v)
except Exception:
print(f"[ERROR] '{k}' on_extension_registered_handlers")
traceback.print_exc()
del on_extension_registered_handlers[extension_name]
def add_on_extension_registered(k, extension_name, f):
global on_extension_registered_handlers
if extension_name in extension_infos:
try:
v = extension_infos[extension_name]
f(extension_name, v)
except Exception:
print(f"[ERROR] '{k}' on_extension_registered_handler")
traceback.print_exc()
else:
if extension_name not in on_extension_registered_handlers:
on_extension_registered_handlers[extension_name] = []
on_extension_registered_handlers[extension_name].append((k, f))
def add_on_revision_detected(k, f):
if 'comfyui.revision' in variables:
try:
f(variables['comfyui.revision'])
except Exception:
print(f"[ERROR] '{k}' on_revision_detected_handler")
traceback.print_exc()
else:
variables['cm.on_revision_detected_handler'].append((k, f))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +0,0 @@
import os
from urllib.parse import urlparse
aria2 = os.getenv('COMFYUI_MANAGER_ARIA2_SERVER')
HF_ENDPOINT = os.getenv('HF_ENDPOINT')
if aria2 is not None:
secret = os.getenv('COMFYUI_MANAGER_ARIA2_SECRET')
url = urlparse(aria2)
port = url.port
host = url.scheme + '://' + url.hostname
import aria2p
aria2 = aria2p.API(aria2p.Client(host=host, port=port, secret=secret))
def download_url(model_url: str, model_dir: str, filename: str):
if aria2:
return aria2_download_url(model_url, model_dir, filename)
else:
from torchvision.datasets.utils import download_url as torchvision_download_url
return torchvision_download_url(model_url, model_dir, filename)
def aria2_find_task(dir: str, filename: str):
target = os.path.join(dir, filename)
downloads = aria2.get_downloads()
for download in downloads:
for file in download.files:
if file.is_metadata:
continue
if str(file.path) == target:
return download
def aria2_download_url(model_url: str, model_dir: str, filename: str):
import manager_core as core
import tqdm
import time
if model_dir.startswith(core.comfy_path):
model_dir = model_dir[len(core.comfy_path) :]
if HF_ENDPOINT:
model_url = model_url.replace('https://huggingface.co', HF_ENDPOINT)
download_dir = model_dir if model_dir.startswith('/') else os.path.join('/models', model_dir)
download = aria2_find_task(download_dir, filename)
if download is None:
options = {'dir': download_dir, 'out': filename}
download = aria2.add(model_url, options)[0]
if download.is_active:
with tqdm.tqdm(
total=download.total_length,
bar_format='{l_bar}{bar}{r_bar}',
desc=filename,
unit='B',
unit_scale=True,
) as progress_bar:
while download.is_active:
if progress_bar.total == 0 and download.total_length != 0:
progress_bar.reset(download.total_length)
progress_bar.update(download.completed_length - progress_bar.n)
time.sleep(1)
download.update()

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +0,0 @@
# DON'T USE StrictVersion - cannot handle pre_release version
# try:
# from distutils.version import StrictVersion
# except:
# print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
class StrictVersion:
def __init__(self, version_string):
self.version_string = version_string
self.major = 0
self.minor = 0
self.patch = 0
self.pre_release = None
self.parse_version_string()
def parse_version_string(self):
parts = self.version_string.split('.')
if not parts:
raise ValueError("Version string must not be empty")
self.major = int(parts[0])
self.minor = int(parts[1]) if len(parts) > 1 else 0
self.patch = int(parts[2]) if len(parts) > 2 else 0
# Handling pre-release versions if present
if len(parts) > 3:
self.pre_release = parts[3]
def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}"
if self.pre_release:
version += f"-{self.pre_release}"
return version
def __eq__(self, other):
return (self.major, self.minor, self.patch, self.pre_release) == \
(other.major, other.minor, other.patch, other.pre_release)
def __lt__(self, other):
if (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch):
return self.pre_release_compare(self.pre_release, other.pre_release) < 0
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
@staticmethod
def pre_release_compare(pre1, pre2):
if pre1 == pre2:
return 0
if pre1 is None:
return 1
if pre2 is None:
return -1
return -1 if pre1 < pre2 else 1
def __le__(self, other):
return self == other or self < other
def __gt__(self, other):
return not self <= other
def __ge__(self, other):
return not self < other
def __ne__(self, other):
return not self == other

View File

@@ -1,92 +0,0 @@
import sys
import subprocess
import os
def security_check():
print("[START] Security scan")
custom_nodes_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
comfyui_path = os.path.abspath(os.path.join(custom_nodes_path, '..'))
guide = {
"ComfyUI_LLMVISION": """
0.Remove ComfyUI\\custom_nodes\\ComfyUI_LLMVISION.
1.Remove pip packages: openai-1.16.3.dist-info, anthropic-0.21.4.dist-info, openai-1.30.2.dist-info, anthropic-0.21.5.dist-info, anthropic-0.26.1.dist-info, %LocalAppData%\\rundll64.exe
(For portable versions, it is recommended to reinstall. If you are using a venv, it is advised to recreate the venv.)
2.Remove these files in your system: lib/browser/admin.py, Cadmino.py, Fadmino.py, VISION-D.exe, BeamNG.UI.exe
3.Check your Windows registry for the key listed above and remove it.
(HKEY_CURRENT_USER\\Software\\OpenAICLI)
4.Run a malware scanner.
5.Change all of your passwords, everywhere.
(Reinstall OS is recommended.)
\n
Detailed information: https://old.reddit.com/r/comfyui/comments/1dbls5n/psa_if_youve_used_the_comfyui_llmvision_node_from/
""",
"lolMiner": """
1. Remove pip packages: lolMiner*
2. Remove files: lolMiner*, 4G_Ethash_Linux_Readme.txt, mine* in ComfyUI dir.
(Reinstall ComfyUI is recommended.)
"""
}
node_blacklist = {"ComfyUI_LLMVISION": "ComfyUI_LLMVISION"}
pip_blacklist = {"AppleBotzz": "ComfyUI_LLMVISION"}
file_blacklist = {
"ComfyUI_LLMVISION": ["%LocalAppData%\\rundll64.exe"],
"lolMiner": [os.path.join(comfyui_path, 'lolMiner')]
}
installed_pips = subprocess.check_output([sys.executable, '-m', "pip", "freeze"], text=True)
detected = set()
try:
anthropic_info = subprocess.check_output([sys.executable, '-m', "pip", "show", "anthropic"], text=True, stderr=subprocess.DEVNULL)
anthropic_reqs = [x for x in anthropic_info.split('\n') if x.startswith("Requires")][0].split(': ')[1]
if "pycrypto" in anthropic_reqs:
location = [x for x in anthropic_info.split('\n') if x.startswith("Location")][0].split(': ')[1]
for fi in os.listdir(location):
if fi.startswith("anthropic"):
guide["ComfyUI_LLMVISION"] = f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"]
detected.add("ComfyUI_LLMVISION")
except subprocess.CalledProcessError:
pass
for k, v in node_blacklist.items():
if os.path.exists(os.path.join(custom_nodes_path, k)):
print(f"[SECURITY ALERT] custom node '{k}' is dangerous.")
detected.add(v)
for k, v in pip_blacklist.items():
if k in installed_pips:
detected.add(v)
break
for k, v in file_blacklist.items():
for x in v:
if os.path.exists(os.path.expandvars(x)):
detected.add(k)
break
if len(detected) > 0:
for line in installed_pips.split('\n'):
for k, v in pip_blacklist.items():
if k in line:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
print("\n########################################################################")
print(" Malware has been detected, forcibly terminating ComfyUI execution.")
print("########################################################################\n")
for x in detected:
print(f"\n======== TARGET: {x} =========")
print(f"\nTODO:")
print(guide.get(x))
exit(-1)
print("[DONE] Security scan")

View File

@@ -11,7 +11,7 @@ import {
showYouMLShareDialog showYouMLShareDialog
} from "./comfyui-share-common.js"; } from "./comfyui-share-common.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js"; import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message } from "./common.js"; import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, migrateAPI, setManagerInstance, show_message } from "./common.js";
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js"; import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
import { CustomNodesManager } from "./custom-nodes-manager.js"; import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js"; import { ModelManager } from "./model-manager.js";
@@ -101,24 +101,6 @@ docStyle.innerHTML = `
vertical-align: middle; vertical-align: middle;
} }
#cm-channel-badge {
color: white;
background-color: #AA0000;
width: 220px;
height: 23px;
font-size: 13px;
border-radius: 5px;
left: 5px;
top: 5px;
align-content: center;
justify-content: center;
text-align: center;
font-weight: bold;
float: left;
vertical-align: middle;
position: relative;
}
#custom-nodes-grid a { #custom-nodes-grid a {
color: #5555FF; color: #5555FF;
font-weight: bold; font-weight: bold;
@@ -239,9 +221,9 @@ function is_legacy_front() {
document.head.appendChild(docStyle); document.head.appendChild(docStyle);
var update_comfyui_button = null; var update_comfyui_button = null;
var switch_comfyui_button = null;
var fetch_updates_button = null; var fetch_updates_button = null;
var update_all_button = null; var update_all_button = null;
var badge_mode = "none";
let share_option = 'all'; let share_option = 'all';
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts // copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
@@ -287,6 +269,18 @@ const style = `
color: white !important; color: white !important;
} }
.cm-button-orange {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
font-weight: bold;
background-color: orange !important;
color: black !important;
}
.cm-experimental-button { .cm-experimental-button {
width: 290px; width: 290px;
height: 30px; height: 30px;
@@ -396,10 +390,10 @@ const style = `
.pysssss-workflow-arrow-2:after { .pysssss-workflow-arrow-2:after {
content: "▼"; content: "▼";
} }
.pysssss-workflow-arrow-2:hover { .pysssss-workflow-arrow-2:hover {
filter: brightness(1.6); filter: brightness(1.6);
background-color: var(--comfy-menu-bg); background-color: var(--comfy-menu-bg);
} }
.pysssss-workflow-popup-2 ~ .litecontextmenu { .pysssss-workflow-popup-2 ~ .litecontextmenu {
transform: scale(1.3); transform: scale(1.3);
} }
@@ -412,13 +406,6 @@ const style = `
`; `;
async function init_badge_mode() {
api.fetchApi('/manager/badge_mode')
.then(response => response.text())
.then(data => { badge_mode = data; })
}
async function init_share_option() { async function init_share_option() {
api.fetchApi('/manager/share_option') api.fetchApi('/manager/share_option')
.then(response => response.text()) .then(response => response.text())
@@ -435,7 +422,6 @@ async function init_notice(notice) {
}) })
} }
await init_badge_mode();
await init_share_option(); await init_share_option();
async function fetchNicknames() { async function fetchNicknames() {
@@ -502,65 +488,6 @@ function getNickname(node, nodename) {
} }
} }
function drawBadge(node, orig, restArgs) {
let ctx = restArgs[0];
const r = orig?.apply?.(node, restArgs);
if (!node.flags.collapsed && badge_mode != 'none' && node.constructor.title_mode != LiteGraph.NO_TITLE) {
let text = "";
if (badge_mode.startsWith('id_nick'))
text = `#${node.id} `;
let nick = node.getNickname();
if (nick) {
if (nick == 'ComfyUI') {
if(badge_mode.endsWith('hide')) {
nick = "";
}
else {
nick = "🦊"
}
}
if (nick.length > 25) {
text += nick.substring(0, 23) + "..";
}
else {
text += nick;
}
}
if (text != "") {
let fgColor = "white";
let bgColor = "#0F1F0F";
let visible = true;
ctx.save();
ctx.font = "12px sans-serif";
const sz = ctx.measureText(text);
ctx.fillStyle = bgColor;
ctx.beginPath();
ctx.roundRect(node.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
ctx.fill();
ctx.fillStyle = fgColor;
ctx.fillText(text, node.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
ctx.restore();
if (node.has_errors) {
ctx.save();
ctx.font = "bold 14px sans-serif";
const sz2 = ctx.measureText(node.type);
ctx.fillStyle = 'white';
ctx.fillText(node.type, node.size[0] / 2 - sz2.width / 2, node.size[1] / 2);
ctx.restore();
}
}
}
return r;
}
async function updateComfyUI() { async function updateComfyUI() {
let prev_text = update_comfyui_button.innerText; let prev_text = update_comfyui_button.innerText;
update_comfyui_button.innerText = "Updating ComfyUI..."; update_comfyui_button.innerText = "Updating ComfyUI...";
@@ -595,6 +522,154 @@ async function updateComfyUI() {
} }
} }
function showVersionSelectorDialog(versions, current, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 100003;
dialog.element.style.width = "300px";
dialog.element.style.padding = "0";
dialog.element.style.backgroundColor = "#2a2a2a";
dialog.element.style.border = "1px solid #3a3a3a";
dialog.element.style.borderRadius = "8px";
dialog.element.style.boxSizing = "border-box";
dialog.element.style.overflow = "hidden";
const contentStyle = {
width: "300px",
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "20px",
boxSizing: "border-box",
gap: "15px"
};
let selectedVersion = versions[0];
const versionList = $el("select", {
multiple: true,
size: Math.min(10, versions.length),
style: {
width: "260px",
height: "auto",
backgroundColor: "#383838",
color: "#ffffff",
border: "1px solid #4a4a4a",
borderRadius: "4px",
padding: "5px",
boxSizing: "border-box"
}
},
versions.map((v, index) => $el("option", {
value: v,
textContent: v,
selected: v === current
}))
);
versionList.addEventListener('change', (e) => {
selectedVersion = e.target.value;
Array.from(e.target.options).forEach(opt => {
opt.selected = opt.value === selectedVersion;
});
});
const content = $el("div", {
style: contentStyle
}, [
$el("h3", {
textContent: "Select Version",
style: {
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: "10px 15px",
margin: "0 0 10px 0",
width: "260px",
textAlign: "center",
borderRadius: "4px",
boxSizing: "border-box",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
versionList,
$el("div", {
style: {
display: "flex",
justifyContent: "space-between",
width: "260px",
gap: "10px"
}
}, [
$el("button", {
textContent: "Cancel",
onclick: () => dialog.close(),
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4a4a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
$el("button", {
textContent: "Select",
onclick: () => {
if (selectedVersion) {
onSelect(selectedVersion);
dialog.close();
} else {
alert("Please select a version.");
}
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4CAF50",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
])
]);
dialog.show(content);
}
async function switchComfyUI() {
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
if(res.status == 200) {
let obj = await res.json();
let versions = [];
let default_version;
for(let v of obj.versions) {
default_version = v;
versions.push(v);
}
showVersionSelectorDialog(versions, obj.current, (selected_version) => {
api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
});
}
else {
show_message('Failed to fetch ComfyUI versions.');
}
}
async function fetchUpdates(update_check_checkbox) { async function fetchUpdates(update_check_checkbox) {
let prev_text = fetch_updates_button.innerText; let prev_text = fetch_updates_button.innerText;
fetch_updates_button.innerText = "Fetching updates..."; fetch_updates_button.innerText = "Fetching updates...";
@@ -745,6 +820,14 @@ class ManagerMenuDialog extends ComfyDialog {
() => updateComfyUI() () => updateComfyUI()
}); });
switch_comfyui_button =
$el("button.cm-button", {
type: "button",
textContent: "Switch ComfyUI",
onclick:
() => switchComfyUI()
});
fetch_updates_button = fetch_updates_button =
$el("button.cm-button", { $el("button.cm-button", {
type: "button", type: "button",
@@ -815,6 +898,7 @@ class ManagerMenuDialog extends ComfyDialog {
$el("br", {}, []), $el("br", {}, []),
update_all_button, update_all_button,
update_comfyui_button, update_comfyui_button,
switch_comfyui_button,
fetch_updates_button, fetch_updates_button,
$el("br", {}, []), $el("br", {}, []),
@@ -838,6 +922,28 @@ class ManagerMenuDialog extends ComfyDialog {
}), }),
]; ];
let migration_btn =
$el("button.cm-button-orange", {
type: "button",
textContent: "Migrate to New Node System",
onclick: () => migrateAPI()
});
migration_btn.style.display = 'none';
res.push(migration_btn);
api.fetchApi('/manager/need_to_migrate')
.then(response => response.text())
.then(text => {
if (text === 'True') {
migration_btn.style.display = 'block';
}
})
.catch(error => {
console.error('Error checking migration status:', error);
});
return res; return res;
} }
@@ -875,32 +981,9 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`); api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
}); });
// nickname
let badge_combo = "";
if(is_legacy_front()) {
badge_combo = document.createElement("select");
badge_combo.setAttribute("title", "Configure the content to be displayed on the badge at the top right corner of the node. The ID is the identifier of the node. If 'hide built-in' is selected, both unknown nodes and built-in nodes will be omitted, making them indistinguishable");
badge_combo.className = "cm-menu-combo";
badge_combo.appendChild($el('option', { value: 'none', text: 'Badge: None' }, []));
badge_combo.appendChild($el('option', { value: 'nick', text: 'Badge: Nickname' }, []));
badge_combo.appendChild($el('option', { value: 'nick_hide', text: 'Badge: Nickname (hide built-in)' }, []));
badge_combo.appendChild($el('option', { value: 'id_nick', text: 'Badge: #ID Nickname' }, []));
badge_combo.appendChild($el('option', { value: 'id_nick_hide', text: 'Badge: #ID Nickname (hide built-in)' }, []));
api.fetchApi('/manager/badge_mode')
.then(response => response.text())
.then(data => { badge_combo.value = data; badge_mode = data; });
badge_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/badge_mode?value=${event.target.value}`);
badge_mode = event.target.value;
app.graph.setDirtyCanvas(true);
});
}
// channel // channel
let channel_combo = document.createElement("select"); let channel_combo = document.createElement("select");
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list. Note that the badge utilizes local information."); channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
channel_combo.className = "cm-menu-combo"; channel_combo.className = "cm-menu-combo";
api.fetchApi('/manager/channel_url_list') api.fetchApi('/manager/channel_url_list')
.then(response => response.json()) .then(response => response.json())
@@ -1024,7 +1107,6 @@ class ManagerMenuDialog extends ComfyDialog {
this.datasrc_combo, this.datasrc_combo,
channel_combo, channel_combo,
preview_combo, preview_combo,
badge_combo,
default_ui_combo, default_ui_combo,
share_combo, share_combo,
component_policy_combo, component_policy_combo,
@@ -1324,7 +1406,7 @@ class ManagerMenuDialog extends ComfyDialog {
app.registerExtension({ app.registerExtension({
name: "Comfy.ManagerMenu", name: "Comfy.ManagerExtMenu",
init() { init() {
$el("style", { $el("style", {
textContent: style, textContent: style,
@@ -1349,20 +1431,32 @@ app.registerExtension({
try { try {
// new style Manager buttons // new style Manager buttons
// unload models button into new style Manager button // unload models button into new style Manager button
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ new(await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "puzzle", icon: "star",
action: () => { action: () => {
if(!manager_instance) if(!manager_instance)
setManagerInstance(new ManagerMenuDialog()); setManagerInstance(new ManagerMenuDialog());
manager_instance.show();
}, if(!CustomNodesManager.instance) {
tooltip: "ComfyUI Manager", CustomNodesManager.instance = new CustomNodesManager(app, self);
content: "Manager", }
classList: "comfyui-button comfyui-menu-mobile-collapse primary" CustomNodesManager.instance.show(CustomNodesManager.ShowMode.FAVORITES);
}).element, },
tooltip: "Show favorite custom node list",
content: "Manager",
classList: "comfyui-button comfyui-menu-mobile-collapse primary"
}).element,
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "puzzle",
action: () => {
if(!manager_instance)
setManagerInstance(new ManagerMenuDialog());
manager_instance.show();
},
tooltip: "ComfyUI Manager",
}).element,
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ new(await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "vacuum-outline", icon: "vacuum-outline",
action: () => { action: () => {
@@ -1452,32 +1546,6 @@ app.registerExtension({
this._addExtraNodeContextMenu(nodeType, app); this._addExtraNodeContextMenu(nodeType, app);
}, },
async nodeCreated(node, app) {
if(is_legacy_front()) {
if(!node.badge_enabled) {
node.getNickname = function () { return getNickname(node, node.comfyClass.trim()) };
let orig = node.onDrawForeground;
if(!orig)
orig = node.__proto__.onDrawForeground;
node.onDrawForeground = function (ctx) {
drawBadge(node, orig, arguments)
};
node.badge_enabled = true;
}
}
},
async loadedGraphNode(node, app) {
if(is_legacy_front()) {
if(!node.badge_enabled) {
const orig = node.onDrawForeground;
node.getNickname = function () { return getNickname(node, node.type.trim()) };
node.onDrawForeground = function (ctx) { drawBadge(node, orig, arguments) };
}
}
},
_addExtraNodeContextMenu(node, app) { _addExtraNodeContextMenu(node, app) {
const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions; const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.cm_menu_added = true; node.prototype.cm_menu_added = true;

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -235,7 +235,7 @@ const pageHtml = `
<div class="cmm-manager-selection"></div> <div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div> <div class="cmm-manager-message"></div>
<div class="cmm-manager-footer"> <div class="cmm-manager-footer">
<button class="cmm-manager-close">Close</button> <button class="cmm-manager-back">Back</button>
<div class="cmm-flex-auto"></div> <div class="cmm-flex-auto"></div>
</div> </div>
`; `;
@@ -365,10 +365,12 @@ export class ModelManager {
} }
}, },
".cmm-manager-close": { ".cmm-manager-back": {
click: (e) => this.close() click: (e) => {
this.close()
manager_instance.show();
}
}, },
}; };
Object.keys(eventsMap).forEach(selector => { Object.keys(eventsMap).forEach(selector => {
const target = this.element.querySelector(selector); const target = this.element.querySelector(selector);

34
migration_js/migration.js Normal file
View File

@@ -0,0 +1,34 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "Comfy.ManagerExtMenu",
init() {
$el("style", {
textContent: style,
parent: document.head,
});
},
async setup() {
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "puzzle",
action: async () => {
if(confirm('As some features of ComfyUI Manager have been integrated into ComfyUI, they have been separated into manager-core.\n\nComfyUI Manager only includes additional extension features that are not provided by manager-core.\n\nWill you install manager-core?')) {
app.ui.dialog.show('Installing manager-core...');
app.ui.dialog.element.style.zIndex = 10010;
await api.fetchApi("/manager/install_manager_core");
app.ui.dialog.show('The installation of manager-core will be completed after restarting.');
}
},
tooltip: "Need to install manager-core",
content: "Manager (Need To Install)",
classList: "comfyui-button comfyui-menu-mobile-collapse primary"
}).element
);
app.menu?.settingsGroup.element.before(cmGroup.element);
}
});

113
modules/manager_ext_core.py Normal file
View File

@@ -0,0 +1,113 @@
import os
import sys
import configparser
import manager_core as core
import cm_global
from manager_util import *
import shutil
import folder_paths
from comfy.cli_args import args
import latent_preview
version_code = [3, 0, 1]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
manager_ext_config_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'manager-ext.ini'))
cached_config = None
manager_ext_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
channel_list_path = os.path.join(manager_ext_path, 'channels.list')
def update_channel_dict():
if not os.path.exists(channel_list_path):
shutil.copy(channel_list_path+'.template', channel_list_path)
core.get_channel_dict() # for the loading
with open(os.path.join(manager_ext_path, 'channels.list'), 'r') as file:
channels = file.read()
for x in channels.split('\n'):
channel_info = x.split("::")
if len(channel_info) == 2:
core.channel_dict[channel_info[0]] = channel_info[1]
update_channel_dict()
def get_current_preview_method():
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 write_config():
config = configparser.ConfigParser()
config['default'] = {
'preview_method': get_current_preview_method(),
'share_option': get_config()['share_option'],
'default_ui': get_config()['default_ui'],
'component_policy': get_config()['component_policy'],
'double_click_policy': get_config()['double_click_policy'],
'security_level': get_config()['security_level'],
}
directory = os.path.dirname(manager_ext_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
with open(manager_ext_config_path, 'w') as configfile:
config.write(configfile)
def read_config():
try:
config = configparser.ConfigParser()
config.read(manager_ext_config_path)
default_conf = config['default']
# policy migration: disable_unsecure_features -> security_level
security_level = default_conf['security_level'] if 'security_level' in default_conf else 'normal'
return {
'preview_method': default_conf['preview_method'] if 'preview_method' in default_conf else get_current_preview_method(),
'share_option': default_conf['share_option'] if 'share_option' in default_conf else 'all',
'default_ui': default_conf['default_ui'] if 'default_ui' in default_conf else 'none',
'component_policy': default_conf['component_policy'] if 'component_policy' in default_conf else 'workflow',
'double_click_policy': default_conf['double_click_policy'] if 'double_click_policy' in default_conf else 'copy-all',
'security_level': security_level
}
except Exception:
return {
'preview_method': get_current_preview_method(),
'share_option': 'all',
'default_ui': 'none',
'component_policy': 'workflow',
'double_click_policy': 'copy-all',
'security_level': 'normal',
}
def get_config():
global cached_config
if cached_config is None:
cached_config = read_config()
return cached_config
def pip_install(packages):
install_cmd = ['#FORCE', sys.executable, "-m", "pip", "install", '-U'] + packages
core.try_install_script('pip install via manager', '..', install_cmd)

View File

@@ -0,0 +1,745 @@
import traceback
import folder_paths
import locale
import concurrent
import nodes
import os
import sys
import re
import git
from server import PromptServer
import manager_core as core
import cm_global
from . import manager_ext_core as ext_core
from . import manager_ext_util
print(f"### Loading: ComfyUI-Manager (ext) ({ext_core.version_str})")
comfy_ui_hash = "-"
SECURITY_MESSAGE_MIDDLE_OR_BELOW = f"ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS = f"ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_GENERAL = f"ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
routes = PromptServer.instance.routes
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
is_local_mode = args.listen.startswith('127.') or args.listen.startswith('local.')
def is_allowed_security_level(level):
if level == 'block':
return False
elif level == 'high':
if is_local_mode:
return core.get_config()['security_level'].lower() in ['weak', 'normal-']
else:
return core.get_config()['security_level'].lower() == 'weak'
elif level == 'middle':
return core.get_config()['security_level'].lower() in ['weak', 'normal', 'normal-']
else:
return True
async def get_risky_level(files, pip_packages):
json_data1 = await core.get_data_by_mode('local', 'custom-node-list.json')
json_data2 = await core.get_data_by_mode('cache', 'custom-node-list.json', channel_url='https://github.com/ltdrdata/ComfyUI-Manager/raw/main')
all_urls = set()
for x in json_data1['custom_nodes'] + json_data2['custom_nodes']:
all_urls.update(x['files'])
for x in files:
if x not in all_urls:
return "high"
all_pip_packages = set()
for x in json_data1['custom_nodes'] + json_data2['custom_nodes']:
if "pip" in x:
all_pip_packages.update(x['pip'])
for p in pip_packages:
if p not in all_pip_packages:
return "block"
return "middle"
from manager_downloader import download_url
components_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '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
ext_core.get_config()['preview_method'] = args.preview_method
set_preview_method(ext_core.get_config()['preview_method'])
def set_default_ui_mode(mode):
ext_core.get_config()['default_ui'] = mode
def set_component_policy(mode):
ext_core.get_config()['component_policy'] = mode
def set_double_click_policy(mode):
ext_core.get_config()['double_click_policy'] = mode
# Expand Server api
import server
from aiohttp import web
import aiohttp
import json
import zipfile
import urllib.request
def check_state_of_git_node_pack(node_packs, do_fetch=False, do_update_check=True, do_update=False):
if do_fetch:
print("Start fetching...", end="")
elif do_update:
print("Start updating...", end="")
elif do_update_check:
print("Start update check...", end="")
def process_custom_node(item):
core.check_state_of_git_node_pack_single(item, do_fetch, do_update_check, do_update)
with concurrent.futures.ThreadPoolExecutor(4) as executor:
for k, v in node_packs.items():
if v.get('active_version') in ['unknown', 'nightly']:
executor.submit(process_custom_node, v)
if do_fetch:
print(f"\x1b[2K\rFetching done.")
elif do_update:
update_exists = any(item.get('updatable', False) for item in node_packs.values())
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
@routes.get("/customnode/getmappings")
async def fetch_customnode_mappings(request):
"""
provide unified (node -> node pack) mapping list
"""
mode = request.rel_url.query["mode"]
nickname_mode = False
if mode == "nickname":
mode = "local"
nickname_mode = True
json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json')
json_obj = core.map_to_unified_keys(json_obj)
if nickname_mode:
json_obj = nickname_filter(json_obj)
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')
@routes.get("/customnode/fetch_updates")
async def fetch_updates(request):
try:
if request.rel_url.query["mode"] == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
for x in res['failed']:
print(f"FETCH FAILED: {x}")
print("\nDone.")
if len(res['updated']) > 0:
return web.Response(status=201)
return web.Response(status=200)
except:
traceback.print_exc()
return web.Response(status=400)
@routes.get("/customnode/update_all")
async def update_all(request):
if not is_allowed_security_level('middle'):
print(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
try:
core.save_snapshot_with_postfix('autosave')
if request.rel_url.query["mode"] == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
updated_cnr = []
for k, v in core.unified_manager.active_nodes.items():
if v[0] != 'nightly':
res = core.unified_manager.unified_update(k, v[0])
if res.action == 'switch-cnr' and res:
updated_cnr.append(k)
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=True)
res['updated'] += updated_cnr
for x in res['failed']:
print(f"PULL FAILED: {x}")
if len(res['updated']) == 0 and len(res['failed']) == 0:
status = 200
else:
status = 201
print(f"\nDone.")
return web.json_response(res, status=status, content_type='application/json')
except:
traceback.print_exc()
return web.Response(status=400)
finally:
core.clear_pip_cache()
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"<a href='{match.group(2)}' target='blank'>{match.group(1)}</a>"
def replace_w(match):
return f"<p class='cm-warn-note'>{match.group(1)}</p>"
def replace_i(match):
return f"<p class='cm-info-note'>{match.group(1)}</p>"
def replace_bold(match):
return f"<B>{match.group(1)}</B>"
def replace_white(match):
return f"<font color='white'>{match.group(1)}</font>"
input_text = input_text.replace('\\[', '&#91;').replace('\\]', '&#93;').replace('<', '&lt;').replace('>', '&gt;')
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", "<BR>")
def populate_markdown(x):
if 'description' in x:
x['description'] = convert_markdown_to_html(manager_ext_util.sanitize_tag(x['description']))
if 'name' in x:
x['name'] = manager_ext_util.sanitize_tag(x['name'])
if 'title' in x:
x['title'] = manager_ext_util.sanitize_tag(x['title'])
@routes.get("/customnode/getlist")
async def fetch_customnode_list(request):
"""
provide unified custom node list
"""
if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true":
skip_update = True
else:
skip_update = False
if request.rel_url.query["mode"] == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
node_packs = await core.get_unified_total_nodes(channel, request.rel_url.query["mode"])
json_obj_github = core.get_data_by_mode(request.rel_url.query["mode"], 'github-stats.json', 'default')
json_obj_extras = core.get_data_by_mode(request.rel_url.query["mode"], 'extras.json', 'default')
core.populate_github_stats(node_packs, await json_obj_github)
core.populate_favorites(node_packs, await json_obj_extras)
check_state_of_git_node_pack(node_packs, False, do_update_check=not skip_update)
for v in node_packs.values():
populate_markdown(v)
if channel != 'local':
found = 'custom'
for name, url in core.get_channel_dict().items():
if url == channel:
found = name
break
channel = found
result = dict(channel=channel, node_packs=node_packs)
return web.json_response(result, content_type='application/json')
@routes.get("/customnode/alternatives")
async def fetch_customnode_alternatives(request):
alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json')
res = {}
for item in alter_json['items']:
populate_markdown(item)
res[item['id']] = item
res = core.map_to_unified_keys(res)
return web.json_response(res, 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')
@routes.get("/snapshot/remove")
async def remove_snapshot(request):
if not is_allowed_security_level('middle'):
print(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
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)
@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)
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
@routes.post("/customnode/install/git_url")
async def install_custom_node_git_url(request):
if not is_allowed_security_level('high'):
print(SECURITY_MESSAGE_NORMAL_MINUS)
return web.Response(status=403)
url = await request.text()
res = await core.gitclone_install(url)
if res.action == 'skip':
print(f"Already installed: '{res.target}'")
return web.Response(status=200)
elif res.result:
print(f"After restarting ComfyUI, please refresh the browser.")
return web.Response(status=200)
print(res.msg)
return web.Response(status=400)
@routes.post("/customnode/install/pip")
async def install_pip(request):
if not is_allowed_security_level('high'):
print(SECURITY_MESSAGE_NORMAL_MINUS)
return web.Response(status=403)
packages = await request.text()
core.pip_install(packages.split(' '))
return web.Response(status=200)
@routes.get("/comfyui_manager/update_comfyui")
async def update_comfyui(request):
print(f"Update ComfyUI")
try:
repo_path = os.path.dirname(folder_paths.__file__)
res = core.update_path(repo_path)
if res == "fail":
print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
return web.Response(status=400)
elif res == "updated":
return web.Response(status=201)
else: # skipped
return web.Response(status=200)
except Exception as e:
print(f"ComfyUI update fail: {e}", file=sys.stderr)
return web.Response(status=400)
@routes.get("/comfyui_manager/comfyui_switch_version")
async def comfyui_switch_version(request):
try:
if "ver" in request.rel_url.query:
core.switch_comfyui(request.rel_url.query['ver'])
return web.Response(status=200)
except Exception as e:
print(f"ComfyUI update fail: {e}", file=sys.stderr)
return web.Response(status=400)
@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'])
ext_core.write_config()
else:
return web.Response(text=ext_core.get_current_preview_method(), status=200)
return web.Response(status=200)
@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'])
ext_core.write_config()
else:
return web.Response(text=ext_core.get_config()['default_ui'], status=200)
return web.Response(status=200)
@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'])
ext_core.write_config()
else:
return web.Response(text=ext_core.get_config()['component_policy'], status=200)
return web.Response(status=200)
@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'])
ext_core.write_config()
else:
return web.Response(text=ext_core.get_config()['double_click_policy'], status=200)
return web.Response(status=200)
def add_target_blank(html_text):
pattern = r'(<a\s+href="[^"]*"\s*[^>]*)(>)'
def add_target(match):
if 'target=' not in match.group(1):
return match.group(1) + ' target="_blank"' + match.group(2)
return match.group(0)
modified_html = re.sub(pattern, add_target, html_text)
return modified_html
@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'<div class="markdown-body">([\s\S]*?)</div>')
match = pattern.search(html_content)
if match:
markdown_content = match.group(1)
if core.comfyui_tag:
markdown_content += f"<HR>ComfyUI: <b>{core.comfyui_tag}</b><BR>Commit Date: {core.comfy_ui_commit_datetime.date()}"
else:
markdown_content += f"<HR>ComfyUI: <b>{core.comfy_ui_revision}</b>[{comfy_ui_hash[:6]}]({core.comfy_ui_commit_datetime.date()})"
# markdown_content += f"<BR>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;()"
markdown_content += f"<BR>manager-core: {core.version_str}"
markdown_content += f"<BR>manager-ext: {ext_core.version_str}"
markdown_content = add_target_blank(markdown_content)
try:
if core.comfy_ui_required_commit_datetime.date() > core.comfy_ui_commit_datetime.date():
markdown_content = f'<P style="text-align: center; color:red; background-color:white; font-weight:bold">Your ComfyUI is too OUTDATED!!!</P>' + 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)
@routes.get("/manager/reboot")
def restart(self):
if not is_allowed_security_level('middle'):
print(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
try:
sys.stdout.close_log()
except Exception as e:
pass
if '__COMFY_CLI_SESSION__' in os.environ:
with open(os.path.join(os.environ['__COMFY_CLI_SESSION__'] + '.reboot'), 'w') as file:
pass
print(f"\nRestarting...\n\n")
exit(0)
print(f"\nRestarting... [Legacy Mode]\n\n")
if sys.platform.startswith('win32'):
return os.execv(sys.executable, ['"' + sys.executable + '"', '"' + sys.argv[0] + '"'] + sys.argv[1:])
else:
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
@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)
@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)
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)
def sanitize(data):
return data.replace("<", "&lt;").replace(">", "&gt;")
if not os.path.exists(ext_core.manager_ext_config_path):
ext_core.get_config()
ext_core.write_config()
cm_global.register_extension('ComfyUI-Manager',
{'version': core.version,
'name': 'ComfyUI Manager (Extension)',
'nodes': {},
'description': 'ComfyUI-Manager (Extension)', })
cm_global.variables['manager-core.show_menu'] = False

174
modules/manager_ext_util.py Normal file
View File

@@ -0,0 +1,174 @@
import traceback
import aiohttp
import json
import threading
import os
from datetime import datetime
cache_lock = threading.Lock()
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
cache_dir = os.path.join(comfyui_manager_path, '.cache')
# DON'T USE StrictVersion - cannot handle pre_release version
# try:
# from distutils.version import StrictVersion
# except:
# print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
class StrictVersion:
def __init__(self, version_string):
self.version_string = version_string
self.major = 0
self.minor = 0
self.patch = 0
self.pre_release = None
self.parse_version_string()
def parse_version_string(self):
parts = self.version_string.split('.')
if not parts:
raise ValueError("Version string must not be empty")
self.major = int(parts[0])
self.minor = int(parts[1]) if len(parts) > 1 else 0
self.patch = int(parts[2]) if len(parts) > 2 else 0
# Handling pre-release versions if present
if len(parts) > 3:
self.pre_release = parts[3]
def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}"
if self.pre_release:
version += f"-{self.pre_release}"
return version
def __eq__(self, other):
return (self.major, self.minor, self.patch, self.pre_release) == \
(other.major, other.minor, other.patch, other.pre_release)
def __lt__(self, other):
if (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch):
return self.pre_release_compare(self.pre_release, other.pre_release) < 0
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
@staticmethod
def pre_release_compare(pre1, pre2):
if pre1 == pre2:
return 0
if pre1 is None:
return 1
if pre2 is None:
return -1
return -1 if pre1 < pre2 else 1
def __le__(self, other):
return self == other or self < other
def __gt__(self, other):
return not self <= other
def __ge__(self, other):
return not self < other
def __ne__(self, other):
return not self == other
def simple_hash(input_string):
hash_value = 0
for char in input_string:
hash_value = (hash_value * 31 + ord(char)) % (2**32)
return hash_value
def is_file_created_within_one_day(file_path):
if not os.path.exists(file_path):
return False
file_creation_time = os.path.getctime(file_path)
current_time = datetime.now().timestamp()
time_difference = current_time - file_creation_time
return time_difference <= 86400
async def get_data(uri, silent=False):
if not silent:
print(f"FETCH DATA from: {uri}", end="")
if uri.startswith("http"):
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.get(uri) as resp:
json_text = await resp.text()
else:
with cache_lock:
with open(uri, "r", encoding="utf-8") as f:
json_text = f.read()
json_obj = json.loads(json_text)
if not silent:
print(f" [DONE]")
return json_obj
async def get_data_with_cache(uri, silent=False, cache_mode=True):
cache_uri = str(simple_hash(uri)) + '_' + os.path.basename(uri).replace('&', "_").replace('?', "_").replace('=', "_")
cache_uri = os.path.join(cache_dir, cache_uri+'.json')
if cache_mode and is_file_created_within_one_day(cache_uri):
json_obj = await get_data(cache_uri, silent=silent)
else:
json_obj = await get_data(uri, silent=silent)
with cache_lock:
with open(cache_uri, "w", encoding='utf-8') as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
if not silent:
print(f"[ComfyUI-Manager] default cache updated: {uri}")
return json_obj
def sanitize_tag(x):
return x.replace('<', '&lt;').replace('>', '&gt;')
def download_url(url, dest_folder, filename):
import requests
# Ensure the destination folder exists
if not os.path.exists(dest_folder):
os.makedirs(dest_folder)
# Full path to save the file
dest_path = os.path.join(dest_folder, filename)
# Download the file
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(dest_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
else:
raise Exception(f"Failed to download file from {url}")
def extract_package_as_zip(file_path, extract_path):
import zipfile
try:
with zipfile.ZipFile(file_path, "r") as zip_ref:
zip_ref.extractall(extract_path)
extracted_files = zip_ref.namelist()
print(f"Extracted zip file to {extract_path}")
return extracted_files
except zipfile.BadZipFile:
print(f"File '{file_path}' is not a zip or is corrupted.")
return None

View File

@@ -0,0 +1,17 @@
import git
from aiohttp import web
from server import PromptServer
manager_core_path = None
manager_core_url = "https://github.com/Comfy-Org/manager-core"
@PromptServer.instance.routes.get("/manager/install_manager_core")
def install_manager_core(request):
if manager_core_path is not None:
repo = git.Repo.clone_from(manager_core_url, manager_core_path)
repo.git.clear_cache()
repo.close()
else:
print(f"[ComfyUI Manager] Failed to install `manager-core`")
return web.Response(status=200)

View File

@@ -1,648 +0,0 @@
import datetime
import os
import subprocess
import sys
import atexit
import threading
import re
import locale
import platform
import json
import ast
import logging
glob_path = os.path.join(os.path.dirname(__file__), "glob")
sys.path.append(glob_path)
import security_check
from manager_util import *
import cm_global
security_check.security_check()
cm_global.pip_blacklist = ['torch', 'torchsde', 'torchvision']
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
def skip_pip_spam(x):
return ('Requirement already satisfied:' in x) or ("DEPRECATION: Loading egg at" in x)
message_collapses = [skip_pip_spam]
import_failed_extensions = set()
cm_global.variables['cm.on_revision_detected_handler'] = []
enable_file_logging = True
def register_message_collapse(f):
global message_collapses
message_collapses.append(f)
def is_import_failed_extension(name):
global import_failed_extensions
return name in import_failed_extensions
def check_file_logging():
global enable_file_logging
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 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
enable_file_logging = False
except Exception:
pass
check_file_logging()
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
sys.__comfyui_manager_register_message_collapse = register_message_collapse
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extension)
comfyui_manager_path = os.path.dirname(__file__)
custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, ".."))
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json")
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)
cm_global.pip_overrides['numpy'] = 'numpy<2'
def remap_pip_package(pkg):
if pkg in cm_global.pip_overrides:
res = cm_global.pip_overrides[pkg]
print(f"[ComfyUI-Manager] '{pkg}' is remapped to '{res}'")
return res
else:
return pkg
std_log_lock = threading.Lock()
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="")
def process_wrap(cmd_str, cwd_path, handler=None, env=None):
process = subprocess.Popen(cmd_str, cwd=cwd_path, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
if handler is None:
handler = handle_stream
stdout_thread = threading.Thread(target=handler, args=(process.stdout, ""))
stderr_thread = threading.Thread(target=handler, args=(process.stderr, "[!]"))
stdout_thread.start()
stderr_thread.start()
stdout_thread.join()
stderr_thread.join()
return process.wait()
try:
if '--port' in sys.argv:
port_index = sys.argv.index('--port')
if port_index + 1 < len(sys.argv):
port = int(sys.argv[port_index + 1])
postfix = f"_{port}"
else:
postfix = ""
else:
postfix = ""
# Logger setup
if enable_file_logging:
if os.path.exists(f"comfyui{postfix}.log"):
if os.path.exists(f"comfyui{postfix}.prev.log"):
if os.path.exists(f"comfyui{postfix}.prev2.log"):
os.remove(f"comfyui{postfix}.prev2.log")
os.rename(f"comfyui{postfix}.prev.log", f"comfyui{postfix}.prev2.log")
os.rename(f"comfyui{postfix}.log", f"comfyui{postfix}.prev.log")
log_file = open(f"comfyui{postfix}.log", "w", encoding="utf-8", errors="ignore")
log_lock = threading.Lock()
original_stdout = sys.stdout
original_stderr = sys.stderr
if original_stdout.encoding.lower() == 'utf-8':
write_stdout = original_stdout.write
write_stderr = original_stderr.write
else:
def wrapper_stdout(msg):
original_stdout.write(msg.encode('utf-8').decode(original_stdout.encoding, errors="ignore"))
def wrapper_stderr(msg):
original_stderr.write(msg.encode('utf-8').decode(original_stderr.encoding, errors="ignore"))
write_stdout = wrapper_stdout
write_stderr = wrapper_stderr
pat_tqdm = r'\d+%.*\[(.*?)\]'
pat_import_fail = r'seconds \(IMPORT FAILED\):.*[/\\]custom_nodes[/\\](.*)$'
is_start_mode = True
class ComfyUIManagerLogger:
def __init__(self, is_stdout):
self.is_stdout = is_stdout
self.encoding = "utf-8"
self.last_char = ''
def fileno(self):
try:
if self.is_stdout:
return original_stdout.fileno()
else:
return original_stderr.fileno()
except AttributeError:
# Handle error
raise ValueError("The object does not have a fileno method")
def isatty(self):
return False
def write(self, message):
global is_start_mode
if any(f(message) for f in message_collapses):
return
if is_start_mode:
match = re.search(pat_import_fail, message)
if match:
import_failed_extensions.add(match.group(1))
if 'Starting server' in message:
is_start_mode = False
if not self.is_stdout:
match = re.search(pat_tqdm, message)
if match:
message = re.sub(r'([#|])\d', r'\1▌', message)
message = re.sub('#', '', message)
if '100%' in message:
self.sync_write(message)
else:
write_stderr(message)
original_stderr.flush()
else:
self.sync_write(message)
else:
self.sync_write(message)
def sync_write(self, message, file_only=False):
with log_lock:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
if self.last_char != '\n':
log_file.write(message)
else:
log_file.write(f"[{timestamp}] {message}")
log_file.flush()
self.last_char = message if message == '' else message[-1]
if not file_only:
with std_log_lock:
if self.is_stdout:
write_stdout(message)
original_stdout.flush()
else:
write_stderr(message)
original_stderr.flush()
def flush(self):
log_file.flush()
with std_log_lock:
if self.is_stdout:
original_stdout.flush()
else:
original_stderr.flush()
def close(self):
self.flush()
def reconfigure(self, *args, **kwargs):
pass
# You can close through sys.stderr.close_log()
def close_log(self):
sys.stderr = original_stderr
sys.stdout = original_stdout
log_file.close()
def close_log():
sys.stderr = original_stderr
sys.stdout = original_stdout
log_file.close()
if enable_file_logging:
sys.stdout = ComfyUIManagerLogger(True)
stderr_wrapper = ComfyUIManagerLogger(False)
sys.stderr = stderr_wrapper
atexit.register(close_log)
else:
sys.stdout.close_log = lambda: None
stderr_wrapper = None
class LoggingHandler(logging.Handler):
def emit(self, record):
global is_start_mode
message = record.getMessage()
if is_start_mode:
match = re.search(pat_import_fail, message)
if match:
import_failed_extensions.add(match.group(1))
if 'Starting server' in message:
is_start_mode = False
if stderr_wrapper:
stderr_wrapper.sync_write(message+'\n', file_only=True)
logging.getLogger().addHandler(LoggingHandler())
except Exception as e:
print(f"[ComfyUI-Manager] Logging failed: {e}")
try:
import git
except ModuleNotFoundError:
my_path = os.path.dirname(__file__)
requirements_path = os.path.join(my_path, "requirements.txt")
print(f"## ComfyUI-Manager: installing dependencies. (GitPython)")
try:
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '-r', requirements_path])
except subprocess.CalledProcessError as e:
print(f"## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
try:
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '--user', '-r', requirements_path])
except subprocess.CalledProcessError as e:
print(f"## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
try:
import git
print(f"## ComfyUI-Manager: installing dependencies done.")
except:
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
print(f"## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
print("** ComfyUI startup time:", datetime.datetime.now())
print("** Platform:", platform.system())
print("** Python version:", sys.version)
print("** Python executable:", sys.executable)
print("** ComfyUI Path:", comfy_path)
if enable_file_logging:
print("** Log path:", os.path.abspath('comfyui.log'))
else:
print("** Log path: file logging is disabled")
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()
def check_bypass_ssl():
try:
import configparser
import ssl
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
config = configparser.ConfigParser()
config.read(config_path)
default_conf = config['default']
if 'bypass_ssl' in default_conf and default_conf['bypass_ssl'].lower() == 'true':
print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see ComfyUI-Manager/config.ini)")
ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
except Exception:
pass
check_bypass_ssl()
# Perform install
processed_install = set()
script_list_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "startup-scripts", "install-scripts.txt")
pip_map = None
def get_installed_packages():
global pip_map
if pip_map is None:
try:
result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True)
pip_map = {}
for line in result.split('\n'):
x = line.strip()
if x:
y = line.split()
if y[0] == 'Package' or y[0].startswith('-'):
continue
pip_map[y[0]] = y[1]
except subprocess.CalledProcessError as e:
print(f"[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
return set()
return pip_map
def is_installed(name):
name = name.strip()
if name.startswith('#'):
return True
pattern = r'([^<>!=]+)([<>!=]=?)([0-9.a-zA-Z]*)'
match = re.search(pattern, name)
if match:
name = match.group(1)
if name in cm_global.pip_blacklist:
return True
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
pkg = get_installed_packages().get(name.lower())
if pkg is None:
return False # update if not installed
if match is None:
return True # don't update if version is not specified
if match.group(2) in ['>', '>=']:
if StrictVersion(pkg) < StrictVersion(match.group(3)):
return False
elif StrictVersion(pkg) > StrictVersion(match.group(3)):
print(f"[SKIP] Downgrading pip package isn't allowed: {name.lower()} (cur={pkg})")
return True # prevent downgrade
if os.path.exists(restore_snapshot_path):
try:
cloned_repos = []
def msg_capture(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
if msg.startswith("CLONE: "):
cloned_repos.append(msg[7:])
if prefix == '[!]':
print(prefix, msg, end="", file=sys.stderr)
else:
print(prefix, msg, end="")
elif 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="")
print(f"[ComfyUI-Manager] Restore snapshot.")
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path
exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture, env=new_env)
repository_name = ''
for url in cloned_repos:
try:
repository_name = url.split("/")[-1].strip()
repo_path = os.path.join(custom_nodes_path, repository_name)
repo_path = os.path.abspath(repo_path)
requirements_path = os.path.join(repo_path, 'requirements.txt')
install_script_path = os.path.join(repo_path, 'install.py')
this_exit_code = 0
if os.path.exists(requirements_path):
with open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file:
for line in file:
package_name = remap_pip_package(line.strip())
if package_name and not is_installed(package_name):
if not package_name.startswith('#'):
if '--index-url' in package_name:
s = package_name.split('--index-url')
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()]
else:
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
this_exit_code += process_wrap(install_cmd, repo_path)
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
processed_install.add(f'{repo_path}/install.py')
install_cmd = [sys.executable, install_script_path]
print(f">>> {install_cmd} / {repo_path}")
new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path
this_exit_code += process_wrap(install_cmd, repo_path, env=new_env)
if this_exit_code != 0:
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
except Exception as e:
print(e)
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
if exit_code != 0:
print(f"[ComfyUI-Manager] Restore snapshot failed.")
else:
print(f"[ComfyUI-Manager] Restore snapshot done.")
except Exception as e:
print(e)
print(f"[ComfyUI-Manager] Restore snapshot failed.")
os.remove(restore_snapshot_path)
def execute_lazy_install_script(repo_path, executable):
global processed_install
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
if os.path.exists(requirements_path):
print(f"Install: pip packages for '{repo_path}'")
with open(requirements_path, "r") as requirements_file:
for line in requirements_file:
package_name = remap_pip_package(line.strip())
if package_name and not is_installed(package_name):
if '--index-url' in package_name:
s = package_name.split('--index-url')
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()]
else:
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
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')
print(f"Install: install script for '{repo_path}'")
install_cmd = [executable, "install.py"]
new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path
process_wrap(install_cmd, repo_path, env=new_env)
# Check if script_list_path exists
if os.path.exists(script_list_path):
print("\n#######################################################################")
print("[ComfyUI-Manager] Starting dependency installation/(de)activation for the extension\n")
executed = set()
# Read each line from the file and convert it to a list using eval
with open(script_list_path, 'r', encoding="UTF-8", errors="ignore") as file:
for line in file:
if line in executed:
continue
executed.add(line)
try:
script = ast.literal_eval(line)
if script[1].startswith('#') and script[1] != '#FORCE':
if script[1] == "#LAZY-INSTALL-SCRIPT":
execute_lazy_install_script(script[0], script[2])
elif os.path.exists(script[0]):
if script[1] == "#FORCE":
del script[1]
else:
if 'pip' in script[1:] and 'install' in script[1:] and is_installed(script[-1]):
continue
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
print(f"\n## Execute install/(de)activation script for '{script[0]}'")
new_env = os.environ.copy()
new_env["COMFYUI_PATH"] = comfy_path
exit_code = process_wrap(script[1:], script[0], env=new_env)
if exit_code != 0:
print(f"install/(de)activation script failed: {script[0]}")
else:
print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
except Exception as e:
print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")
# Remove the script_list_path file
if os.path.exists(script_list_path):
os.remove(script_list_path)
print("\n[ComfyUI-Manager] Startup script completed.")
print("#######################################################################\n")
del processed_install
del pip_map
def check_windows_event_loop_policy():
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 'windows_selector_event_loop_policy' in default_conf and default_conf['windows_selector_event_loop_policy'].lower() == 'true':
try:
import asyncio
import asyncio.windows_events
asyncio.set_event_loop_policy(asyncio.windows_events.WindowsSelectorEventLoopPolicy())
print(f"[ComfyUI-Manager] Windows event loop policy mode enabled")
except Exception as e:
print(f"[ComfyUI-Manager] WARN: Windows initialization fail: {e}")
except Exception:
pass
if platform.system() == 'Windows':
check_windows_event_loop_policy()

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-manager" name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "2.51.9" version = "3.0.1"
license = { file = "LICENSE.txt" } license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"] dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]

View File

@@ -1,39 +0,0 @@
import os
import subprocess
def get_enabled_subdirectories_with_files(base_directory):
subdirs_with_files = []
for subdir in os.listdir(base_directory):
try:
full_path = os.path.join(base_directory, subdir)
if os.path.isdir(full_path) and not subdir.endswith(".disabled") and not subdir.startswith('.') and subdir != '__pycache__':
print(f"## Install dependencies for '{subdir}'")
requirements_file = os.path.join(full_path, "requirements.txt")
install_script = os.path.join(full_path, "install.py")
if os.path.exists(requirements_file) or os.path.exists(install_script):
subdirs_with_files.append((full_path, requirements_file, install_script))
except Exception as e:
print(f"EXCEPTION During Dependencies INSTALL on '{subdir}':\n{e}")
return subdirs_with_files
def install_requirements(requirements_file_path):
if os.path.exists(requirements_file_path):
subprocess.run(["pip", "install", "-r", requirements_file_path])
def run_install_script(install_script_path):
if os.path.exists(install_script_path):
subprocess.run(["python", install_script_path])
custom_nodes_directory = "custom_nodes"
subdirs_with_files = get_enabled_subdirectories_with_files(custom_nodes_directory)
for subdir, requirements_file, install_script in subdirs_with_files:
install_requirements(requirements_file)
run_install_script(install_script)

View File

@@ -1,21 +0,0 @@
git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI/custom_nodes
git clone https://github.com/ltdrdata/ComfyUI-Manager
cd ..
python -m venv venv
source venv/bin/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
cd ..
echo "#!/bin/bash" > run_gpu.sh
echo "cd ComfyUI" >> run_gpu.sh
echo "source venv/bin/activate" >> run_gpu.sh
echo "python main.py --preview-method auto" >> run_gpu.sh
chmod +x run_gpu.sh
echo "#!/bin/bash" > run_cpu.sh
echo "cd ComfyUI" >> run_cpu.sh
echo "source venv/bin/activate" >> run_cpu.sh
echo "python main.py --preview-method auto --cpu" >> run_cpu.sh
chmod +x run_cpu.sh

View File

@@ -1,17 +0,0 @@
git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI/custom_nodes
git clone https://github.com/ltdrdata/ComfyUI-Manager
cd ..
python -m venv venv
call venv/Scripts/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
cd ..
echo "cd ComfyUI" >> run_gpu.bat
echo "call venv/Scripts/activate" >> run_gpu.bat
echo "python main.py" >> run_gpu.bat
echo "cd ComfyUI" >> run_cpu.bat
echo "call venv/Scripts/activate" >> run_cpu.bat
echo "python main.py --cpu" >> run_cpu.bat

View File

@@ -1,2 +0,0 @@
.\python_embeded\python.exe -s -m pip install gitpython
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/ComfyUI-Manager')"

View File

@@ -1,12 +0,0 @@
import git
commit_hash = "a361cc1"
repo = git.Repo('.')
if repo.is_dirty():
repo.git.stash()
repo.git.update_ref("refs/remotes/origin/main", commit_hash)
repo.remotes.origin.fetch()
repo.git.pull("origin", "main")