Merge branch 'Comfy-Org:main' into main

This commit is contained in:
hkun 2025-12-25 21:03:21 +08:00 committed by GitHub
commit a546a5c20b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 43894 additions and 8670 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import subprocess
import sys
import os
import traceback
import time
import git
import json
@ -219,7 +220,14 @@ def gitpull(path):
repo.close()
return
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha

22814
github-stats-cache.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 38, 1]
version_code = [3, 39]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@ -2253,9 +2253,17 @@ def git_pull(path):
current_branch = repo.active_branch
remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name)
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
branch_name = current_branch.name
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
repo.close()
@ -2523,22 +2531,22 @@ def update_to_stable_comfyui(repo_path):
logging.error('\t'+branch.name)
return "fail", None
versions, current_tag, _ = get_comfyui_versions(repo)
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
versions, current_tag, latest_tag = get_comfyui_versions(repo)
if latest_tag is None:
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
return "fail", None
if versions[0] == 'nightly':
latest_tag = versions[1]
else:
latest_tag = versions[0]
if current_tag == latest_tag:
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
if tag_ref is None:
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
return "fail", None
if repo.head.commit == tag_ref.commit:
return "skip", None
else:
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
repo.git.checkout(latest_tag)
repo.git.checkout(tag_ref.name)
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
return 'updated', latest_tag
except:
@ -3362,36 +3370,80 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
def get_comfyui_versions(repo=None):
if repo is None:
repo = git.Repo(comfy_path)
repo = repo or git.Repo(comfy_path)
remote_name = None
try:
remote = get_remote_name(repo)
repo.remotes[remote].fetch()
remote_name = get_remote_name(repo)
repo.remotes[remote_name].fetch()
except:
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
versions = [x.name for x in repo.tags if x.name.startswith('v')]
def parse_semver(tag_name):
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
return tuple(int(x) for x in match.groups()) if match else None
# nearest tag
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
def normalize_describe(tag_name):
if not tag_name:
return None
base = tag_name.split('-', 1)[0]
return base if parse_semver(base) else None
current_tag = repo.git.describe('--tags')
# Collect semver tags and sort descending (highest first)
semver_tags = []
for tag in repo.tags:
semver = parse_semver(tag.name)
if semver:
semver_tags.append((semver, tag.name))
semver_tags.sort(key=lambda x: x[0], reverse=True)
semver_tags = [name for _, name in semver_tags]
if current_tag not in versions:
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
latest_tag = semver_tags[0] if semver_tags else None
main_branch = repo.heads.master
latest_commit = main_branch.commit
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
try:
described = repo.git.describe('--tags')
except Exception:
described = ''
if latest_tag != versions[0]:
versions.insert(0, 'nightly')
else:
versions[0] = 'nightly'
try:
exact_tag = repo.git.describe('--tags', '--exact-match')
except Exception:
exact_tag = ''
head_is_default = False
if remote_name:
try:
default_head_ref = repo.refs[f'{remote_name}/HEAD']
default_commit = default_head_ref.reference.commit
head_is_default = repo.head.commit == default_commit
except Exception:
head_is_default = False
nearest_semver = normalize_describe(described)
exact_semver = exact_tag if parse_semver(exact_tag) else None
if head_is_default and not exact_tag:
current_tag = 'nightly'
else:
current_tag = exact_tag or described or 'nightly'
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
display_semver_tags = semver_tags[:4]
if exact_semver and exact_semver not in display_semver_tags:
display_semver_tags.append(exact_semver)
elif nearest_semver and nearest_semver not in display_semver_tags:
display_semver_tags.append(nearest_semver)
versions = ['nightly']
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
versions.append(current_tag)
for tag in display_semver_tags:
if tag not in versions:
versions.append(tag)
versions = versions[:6]
return versions, current_tag, latest_tag

View File

@ -38,6 +38,25 @@ SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in
routes = PromptServer.instance.routes
def has_per_queue_preview():
"""
Check if ComfyUI PR #11261 (per-queue live preview override) is merged
Returns:
bool: True if ComfyUI has per-queue preview feature
"""
try:
import latent_preview
return hasattr(latent_preview, 'set_preview_method')
except ImportError:
return False
# Detect ComfyUI per-queue preview override feature (PR #11261)
COMFYUI_HAS_PER_QUEUE_PREVIEW = has_per_queue_preview()
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
@ -182,10 +201,19 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
logging.info(
"[ComfyUI-Manager] ComfyUI per-queue preview override detected (PR #11261). "
"Manager's preview method feature is disabled. "
"Use ComfyUI's --preview-method CLI option or 'Settings > Execution > Live preview method'."
)
elif args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
set_preview_method(core.get_config()['preview_method'])
else:
logging.warning("[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored.")
logging.warning(
"[ComfyUI-Manager] Since --preview-method is set, "
"ComfyUI-Manager's preview method feature will be ignored."
)
def set_component_policy(mode):
@ -1482,13 +1510,25 @@ async def install_model(request):
@routes.get("/manager/preview_method")
async def preview_method(request):
# Setting change request
if "value" in request.rel_url.query:
# Reject setting change if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
# Process normally if not available
set_preview_method(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
return web.Response(status=200)
return web.Response(status=200)
# Status query request
else:
# Return DISABLED if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
# Return current value if not available
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
@routes.get("/manager/db_mode")

227
js/comfyui-gui-builder.js Normal file
View File

@ -0,0 +1,227 @@
import { $el } from "../../scripts/ui.js";
function normalizeContent(content) {
const tmp = document.createElement('div');
if (typeof content === 'string') {
tmp.innerHTML = content;
return Array.from(tmp.childNodes);
}
if (content instanceof Node) {
return content;
}
return content;
}
export function createSettingsCombo(label, content) {
const settingItem = $el("div.setting-item", {}, [
$el("div.flex.flex-row.items-center.gap-2",[
$el("div.form-label.flex.grow.items-center", [
$el("span.text-muted", { textContent: label },)
]),
$el("div.form-input.flex.justify-end",
[content]
)
]
)
]);
return settingItem;
}
export function buildGuiFrame(dialogId, title, iconClass, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
[
$el("h2.px-4", [
$el(iconClass, {
style: {
"font-size": "1.25rem",
"margin-right": ".5rem"
}
}),
$el("span", { textContent: title })
])
]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}
export function buildGuiFrameCustomHeader(dialogId, customHeader, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const _customHeader = normalizeContent(customHeader);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
Array.isArray(_customHeader) ? _customHeader : [_customHeader]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}

View File

@ -20,6 +20,7 @@ import { ComponentBuilderDialog, getPureName, load_components, set_component_pol
import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js";
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
let manager_version = await getVersion();
@ -44,12 +45,16 @@ docStyle.innerHTML = `
#cm-manager-dialog {
width: 1000px;
height: 455px;
height: auto;
box-sizing: content-box;
z-index: 1000;
overflow-y: auto;
}
#cm-manager-dialog br {
margin-bottom: 1em;
}
.cb-widget {
width: 400px;
height: 25px;
@ -80,6 +85,7 @@ docStyle.innerHTML = `
}
.cm-menu-container {
padding : calc(var(--spacing)*2);
column-gap: 20px;
display: flex;
flex-wrap: wrap;
@ -140,8 +146,8 @@ docStyle.innerHTML = `
}
.cm-notice-board {
width: 290px;
height: 230px;
width: auto;
height: 280px;
overflow: auto;
color: var(--input-text);
border: 1px solid var(--descrip-text);
@ -238,68 +244,50 @@ var is_updating = false;
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
const style = `
#workflowgallery-button {
width: 310px;
height: 27px;
height: 50px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-nodeinfo-button {
width: 310px;
height: 27px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-manual-button {
width: 310px;
height: 27px;
position: relative;
overflow: hidden;
}
.cm-button {
width: 310px;
height: 30px;
width: auto;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: var(--comfy-menu-secondary-bg);
border-color: var(--border-color);
color: var(--input-text);
}
.cm-button:hover {
filter: brightness(125%);
}
.cm-button-red {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cm-button-red:hover {
background-color: #88181b !important;
}
.cm-button-orange {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
font-weight: bold;
background-color: orange !important;
color: black !important;
}
.cm-experimental-button {
width: 290px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
width: 100%;
}
.cm-experimental {
width: 310px;
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
@ -326,8 +314,14 @@ const style = `
.cm-menu-combo {
cursor: pointer;
width: 310px;
box-sizing: border-box;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-menu-secondary-bg);
}
.cm-menu-combo:hover {
filter: brightness(125%);
}
.cm-small-button {
@ -831,7 +825,7 @@ class ManagerMenuDialog extends ComfyDialog {
const isElectron = 'electronAPI' in window;
update_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update ComfyUI",
style: {
@ -842,7 +836,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
switch_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Switch ComfyUI",
style: {
@ -853,7 +847,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
restart_stop_button =
$el("button.cm-button-red", {
$el("button.p-button.p-component.cm-button-red", {
type: "button",
textContent: "Restart",
onclick: () => restartOrStop()
@ -861,7 +855,7 @@ class ManagerMenuDialog extends ComfyDialog {
if(isElectron) {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All Custom Nodes",
onclick:
@ -870,7 +864,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
else {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All",
onclick:
@ -880,7 +874,7 @@ class ManagerMenuDialog extends ComfyDialog {
const res =
[
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes Manager",
onclick:
@ -892,7 +886,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install Missing Custom Nodes",
onclick:
@ -904,7 +898,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes In Workflow",
onclick:
@ -916,8 +910,8 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("button.cm-button", {
$el("div", {}, []),
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Model Manager",
onclick:
@ -929,7 +923,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install via Git URL",
onclick: async () => {
@ -941,13 +935,13 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("div", {}, []),
update_all_button,
update_comfyui_button,
switch_comfyui_button,
// fetch_updates_button,
$el("br", {}, []),
$el("div", {}, []),
restart_stop_button,
];
@ -960,12 +954,13 @@ class ManagerMenuDialog extends ComfyDialog {
let self = this;
// db mode
this.datasrc_combo = document.createElement("select");
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
this.datasrc_combo.className = "cm-menu-combo";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
this.datasrc_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled ";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
api.fetchApi('/manager/db_mode')
.then(response => response.text())
@ -975,27 +970,110 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
// preview method
let preview_combo = document.createElement("select");
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
preview_combo.className = "cm-menu-combo";
preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, []));
preview_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
// Loading state to prevent flash of enabled state
preview_combo.appendChild($el('option', { value: '', text: 'Loading...', disabled: true }, []));
preview_combo.appendChild($el('option', { value: 'auto', text: 'Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'None (very fast)' }, []));
// Start disabled to prevent flash
preview_combo.disabled = true;
preview_combo.value = '';
// Fetch current state
api.fetchApi('/manager/preview_method')
.then(response => response.text())
.then(data => { preview_combo.value = data; });
.then(data => {
// Remove loading option
preview_combo.querySelector('option[value=""]')?.remove();
if (data === "DISABLED") {
// ComfyUI per-queue preview feature is active
preview_combo.disabled = true;
preview_combo.value = 'auto';
// Accessibility attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
// Tooltip for mouse users
preview_combo.setAttribute("title",
"This feature is now provided natively by ComfyUI. " +
"Please use 'Settings > Execution > Live preview method' instead."
);
// Visual feedback
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
} else {
// Manager feature is active
preview_combo.disabled = false;
preview_combo.value = data;
// Accessibility for enabled state
preview_combo.setAttribute("aria-label",
"Preview method setting. Select how latent variables are decoded during preview."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Failed to fetch preview method status:', error);
// Error recovery: fallback to enabled
preview_combo.querySelector('option[value=""]')?.remove();
preview_combo.disabled = false;
preview_combo.value = 'auto';
});
preview_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
// Ignore if disabled
if (preview_combo.disabled) {
event.preventDefault();
return;
}
// Normal operation
api.fetchApi(`/manager/preview_method?value=${event.target.value}`)
.then(response => {
if (response.status === 403) {
// Feature transitioned to native
alert(
'This feature is now provided natively by ComfyUI.\n' +
'Please use \'Settings > Execution > Live preview method\' instead.'
);
preview_combo.disabled = true;
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
// Update aria attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Preview method update failed:', error);
});
});
const previewSetttingItem = createSettingsCombo("Preview method", preview_combo);
// channel
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.");
channel_combo.className = "cm-menu-combo";
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
api.fetchApi('/manager/channel_url_list')
.then(response => response.json())
.then(async data => {
@ -1004,7 +1082,7 @@ class ManagerMenuDialog extends ComfyDialog {
for (let i in urls) {
if (urls[i] != '') {
let name_url = urls[i].split('::');
channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, []));
channel_combo.appendChild($el('option', { value: name_url[0], text: `${name_url[0]}` }, []));
}
}
@ -1019,11 +1097,13 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const channelSetttingItem = createSettingsCombo("Channel", channel_combo);
// share
let share_combo = document.createElement("select");
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
share_combo.className = "cm-menu-combo";
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
const share_options = [
['none', 'None'],
['openart', 'OpenArt AI'],
@ -1034,7 +1114,7 @@ class ManagerMenuDialog extends ComfyDialog {
['all', 'All'],
];
for (const option of share_options) {
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
}
api.fetchApi('/manager/share_option')
@ -1056,12 +1136,14 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const shareSetttingItem = createSettingsCombo("Share", share_combo);
let component_policy_combo = document.createElement("select");
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
component_policy_combo.className = "cm-menu-combo";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
component_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Use my version' }, []));
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => {
@ -1074,15 +1156,14 @@ class ManagerMenuDialog extends ComfyDialog {
set_component_policy(event.target.value);
});
update_policy_combo = document.createElement("select");
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
if(isElectron)
update_policy_combo.style.display = 'none';
update_policy_combo = document.createElement("select");
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
update_policy_combo.className = "cm-menu-combo";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
api.fetchApi('/manager/policy/update')
.then(response => response.text())
.then(data => {
@ -1093,20 +1174,22 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
});
return [
$el("br", {}, []),
this.datasrc_combo,
channel_combo,
preview_combo,
share_combo,
component_policy_combo,
update_policy_combo,
$el("br", {}, []),
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
if(isElectron)
updateSetttingItem.style.display = 'none';
$el("br", {}, []),
$el("filedset.cm-experimental", {}, [
return [
dbRetrievalSetttingItem,
channelSetttingItem,
previewSetttingItem,
shareSetttingItem,
componentSetttingItem,
updateSetttingItem,
//[TODO] replace mt-2 with wrapper div with flex column gap
$el("filedset.cm-experimental.mt-auto", {}, [
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button", {
type: "button",
textContent: "Snapshot Manager",
onclick:
@ -1116,7 +1199,7 @@ class ManagerMenuDialog extends ComfyDialog {
SnapshotManager.instance.show();
}
}),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button.mt-2", {
type: "button",
textContent: "Install PIP packages",
onclick:
@ -1134,7 +1217,7 @@ class ManagerMenuDialog extends ComfyDialog {
createControlsRight() {
const elts = [
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-manual-button',
type: "button",
textContent: "Community Manual",
@ -1185,11 +1268,11 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button", {
$el("button.p-button.p-component.cm-button", {
id: 'workflowgallery-button',
type: "button",
style: {
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
},
onclick: (e) => {
const last_visited_site = localStorage.getItem("wg_last_visited")
@ -1212,7 +1295,7 @@ class ManagerMenuDialog extends ComfyDialog {
}, [
$el("p", {
id: 'workflowgallery-button-last-visited-label',
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
style: {
'text-align': 'center',
'color': 'var(--input-text)',
@ -1228,13 +1311,12 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-nodeinfo-button',
type: "button",
textContent: "Nodes Info",
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
}),
$el("br", {}, []),
];
var textarea = document.createElement("div");
@ -1249,31 +1331,23 @@ class ManagerMenuDialog extends ComfyDialog {
constructor() {
super();
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
const content = $el("div.cm-menu-container",
[
$el("div.cm-menu-column.gap-2", [...this.createControlsLeft()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsMid()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsRight()])
]
);
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
$el("div.cm-menu-column", [...this.createControlsLeft()]),
$el("div.cm-menu-column", [...this.createControlsMid()]),
$el("div.cm-menu-column", [...this.createControlsRight()])
]),
const frame = buildGuiFrame(
'cm-manager-dialog', // dialog id
`ComfyUI Manager ${manager_version}`, // dialog title
"i.mdi.mdi-puzzle", // dialog icon class to show before title
content, // dialog content element
this
); // send this so we can attach close functions
$el("br", {}, []),
close_button,
]
);
content.style.width = '100%';
content.style.height = '100%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
this.element = frame;
}
get isVisible() {
@ -1281,7 +1355,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
show() {
this.element.style.display = "block";
this.element.style.display = "flex";
}
toggleVisibility() {

View File

@ -1,8 +1,9 @@
.cn-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
@ -10,6 +11,7 @@
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.cn-manager .cn-flex-auto {
@ -17,17 +19,21 @@
}
.cn-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cn-manager button:hover {
filter: brightness(125%);
}
.cn-manager button:disabled,
.cn-manager input:disabled,
.cn-manager select:disabled {
@ -40,8 +46,13 @@
.cn-manager .cn-manager-restart {
display: none;
background-color: #500000;
color: white;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cn-manager .cn-manager-restart:hover {
background-color: #88181b !important;
}
.cn-manager .cn-manager-stop {
@ -79,7 +90,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cn-manager-header label {
@ -91,16 +101,32 @@
.cn-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cn-manager-filter:hover {
filter: brightness(125%);
}
.cn-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cn-manager-status {
@ -588,6 +614,10 @@
height: 100%;
}
.cn-install-buttons button {
padding: 4px 8px;
}
.cn-selected-buttons {
display: flex;
gap: 5px;

View File

@ -1,6 +1,7 @@
import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
import {
manager_instance, rebootAPI, install_via_git_url,
@ -18,32 +19,19 @@ loadCss("./custom-nodes-manager.css");
const gridId = "node";
const pageHtml = `
<div class="cn-manager-header">
<label>Filter
<select class="cn-manager-filter"></select>
</label>
<input class="cn-manager-keywords" type="search" placeholder="Search" />
<div class="cn-manager-status"></div>
<div class="cn-flex-auto"></div>
<div class="cn-manager-channel"></div>
</div>
<div class="cn-manager-grid"></div>
<div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div>
<div class="cn-manager-footer">
<button class="cn-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cn-manager-restart">Restart</button>
<button class="cn-manager-stop">Stop</button>
<div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
<button class="cn-manager-check-update">Check Update</button>
<button class="cn-manager-check-missing">Check Missing</button>
<button class="cn-manager-install-url">Install via Git URL</button>
<div class="cn-manager cn-manager-dark">
<div class="cn-manager-grid"></div>
<div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div>
<div class="cn-manager-footer">
<button class="cn-manager-restart p-button p-component">Restart</button>
<button class="cn-manager-stop p-button p-component">Stop</button>
<div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow p-button p-component">Used In Workflow</button>
<button class="cn-manager-check-update p-button p-component">Check Update</button>
<button class="cn-manager-check-missing p-button p-component">Check Missing</button>
<button class="cn-manager-install-url p-button p-component">Install via Git URL</button>
</div>
</div>
`;
@ -89,11 +77,26 @@ export class CustomNodesManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cn-manager"
});
this.element.innerHTML = pageHtml;
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
// $el("select.cn-manager-filter")
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
]);
const frame = buildGuiFrameCustomHeader(
'cn-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
this.element.setAttribute("tabindex", 0);
this.element.focus();
@ -372,7 +375,7 @@ export class CustomNodesManager {
return list.map(id => {
const bt = buttons[id];
return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
return `<button class="cn-btn-${id} p-button p-component" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
}).join("");
}
@ -655,7 +658,6 @@ export class CustomNodesManager {
}
renderGrid() {
// update theme
const globalStyle = window.getComputedStyle(document.body);
this.colorVars = {

View File

@ -1,13 +1,15 @@
.cmm-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
margin: calc(var(--spacing)*2);
}
.cmm-manager .cmm-flex-auto {
@ -18,14 +20,15 @@
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cmm-manager button:hover {
filter: brightness(125%);
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
@ -38,7 +41,7 @@
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080;
background-color: #000080 !important;
color: white;
}
@ -53,7 +56,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cmm-manager-header label {
@ -67,16 +69,34 @@
.cmm-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cmm-manager-type:hover,
.cmm-manager-base:hover,
.cmm-manager-filter:hover {
filter: brightness(125%);
}
.cmm-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cmm-manager-status {
@ -148,6 +168,10 @@
color: white;
}
.cmm-btn-install {
padding: 4px 8px;
}
.cmm-btn-download {
width: 18px;
height: 18px;

View File

@ -9,39 +9,22 @@ import { api } from "../../scripts/api.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
loadCss("./model-manager.css");
const gridId = "model";
const pageHtml = `
<div class="cmm-manager-header">
<label>Filter
<select class="cmm-manager-filter"></select>
</label>
<label>Type
<select class="cmm-manager-type"></select>
</label>
<label>Base
<select class="cmm-manager-base"></select>
</label>
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
<div class="cmm-manager-status"></div>
<div class="cmm-flex-auto"></div>
</div>
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cmm-manager-refresh">Refresh</button>
<button class="cmm-manager-stop">Stop</button>
<div class="cmm-flex-auto"></div>
<div class="cmm-manager cmm-manager-dark">
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-refresh p-button p-component">Refresh</button>
<button class="cmm-manager-stop p-button p-component">Stop</button>
<div class="cmm-flex-auto"></div>
</div>
</div>
`;
@ -64,11 +47,23 @@ export class ModelManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cmm-manager"
});
this.element.innerHTML = pageHtml;
const header = $el("div.cmm-manager-header", {}, [
createSettingsCombo("Filter", $el("select.cmm-manager-filter")),
createSettingsCombo("Type", $el("select.cmm-manager-type")),
createSettingsCombo("Base", $el("select.cmm-manager-base")),
$el("input.cmm-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cmm-manager-status"),
$el("div.cmm-flex-auto")
]);
const frame = buildGuiFrameCustomHeader(
'cmm-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
this.initFilter();
this.bindEvents();
this.initGrid();
@ -347,7 +342,7 @@ export class ModelManager {
if (installed === "True") {
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
}
return `<button class="cmm-btn-install" mode="install">Install</button>`;
return `<button class="cmm-btn-install p-button p-component" mode="install">Install</button>`;
}
}, {
id: 'url',
@ -420,7 +415,7 @@ export class ModelManager {
}
this.selectedModels = selectedList;
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install" mode="install">Install</button>`);
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install p-button p-component" mode="install">Install</button>`);
}
focusInstall(item) {

65
js/snapshot.css Normal file
View File

@ -0,0 +1,65 @@
.snapshot-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.snapshot-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: var(--border-color);
margin: 0;
min-width: 100px;
padding: 4px 8px;
}
.snapshot-manager .snapshot-restore-btn {
background-color: #00158f !important;
border-color: #2025b9 !important;
color: white !important;
}
.snapshot-manager .snapshot-remove-btn {
background-color: #970000 !important;
border-color: #be2127 !important;
color: white !important;
}
.snapshot-manager button:hover {
filter: brightness(125%);
}
.snapshot-manager .data-btns {
display: flex;
flex-direction: column;
gap: calc(var(--spacing)*2);
padding: calc(var(--spacing)*2);
align-items: center;
justify-content: center;
height: 100%;
}
.snapshot-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.snapshot-manager .cn-flex-auto {
flex: auto;
}

View File

@ -1,8 +1,10 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { manager_instance, rebootAPI, show_message, handle403Response } from "./common.js";
import { manager_instance, rebootAPI, show_message, handle403Response, loadCss } from "./common.js";
import { buildGuiFrame } from "./comfyui-gui-builder.js";
loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
@ -27,7 +29,7 @@ async function restore_snapshot(target) {
}
finally {
await SnapshotManager.instance.invalidateControl();
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='cm-small-button'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='p-button p-component'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
}
}
}
@ -88,6 +90,8 @@ export class SnapshotManager extends ComfyDialog {
message_box = null;
data = null;
content = $el("div.snapshot-manager");
clear() {
this.restore_buttons = [];
this.message_box = null;
@ -96,9 +100,18 @@ export class SnapshotManager extends ComfyDialog {
constructor(app, manager_dialog) {
super();
this.manager_dialog = manager_dialog;
// this.manager_dialog = manager_dialog;
this.search_keyword = '';
this.element = $el("div.comfy-modal", { parent: document.body }, []);
const frame = buildGuiFrame(
'snapshot-manager-dialog', // dialog id
'Snapshot Manager', // title
'i.mdi.mdi-puzzle', // icon class
this.content, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
}
async remove_item() {
@ -109,7 +122,7 @@ export class SnapshotManager extends ComfyDialog {
createControls() {
return [
$el("button.cm-small-button", {
$el("button.p-button.p-component", {
type: "button",
textContent: "Close",
onclick: () => { this.close(); }
@ -132,8 +145,8 @@ export class SnapshotManager extends ComfyDialog {
this.clear();
this.data = (await getSnapshotList()).items;
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
while (this.content.children.length) {
this.content.removeChild(this.content.children[0]);
}
await this.createGrid();
@ -204,20 +217,21 @@ export class SnapshotManager extends ComfyDialog {
data2.innerHTML = `&nbsp;${data}`;
var data_button = document.createElement('td');
data_button.style.textAlign = "center";
data_button.className = "data-btns";
var restoreBtn = document.createElement('button');
restoreBtn.className = "snapshot-restore-btn p-button p-component";
restoreBtn.innerHTML = 'Restore';
restoreBtn.style.width = "100px";
restoreBtn.style.backgroundColor = 'blue';
restoreBtn.addEventListener('click', function() {
restore_snapshot(data);
});
var removeBtn = document.createElement('button');
removeBtn.className = "snapshot-remove-btn p-button p-component";
removeBtn.innerHTML = 'Remove';
removeBtn.style.width = "100px";
removeBtn.style.backgroundColor = 'red';
removeBtn.addEventListener('click', function() {
remove_snapshot(data);
@ -241,13 +255,14 @@ export class SnapshotManager extends ComfyDialog {
let self = this;
const panel = document.createElement('div');
panel.style.width = "100%";
panel.style.height = "100%";
panel.appendChild(grid);
function handleResize() {
const parentHeight = self.element.clientHeight;
const gridHeight = parentHeight - 200;
grid.style.height = gridHeight + "px";
// grid.style.height = gridHeight + "px";
}
window.addEventListener("resize", handleResize);
@ -256,25 +271,17 @@ export class SnapshotManager extends ComfyDialog {
grid.style.width = "100%";
grid.style.height = "100%";
grid.style.overflowY = "scroll";
this.element.style.height = "85%";
this.element.style.width = "80%";
this.element.appendChild(panel);
this.content.appendChild(panel);
handleResize();
}
async createBottomControls() {
var close_button = document.createElement("button");
close_button.className = "cm-small-button";
close_button.innerHTML = "Close";
close_button.onclick = () => { this.close(); }
close_button.style.display = "inline-block";
var save_button = document.createElement("button");
save_button.className = "cm-small-button";
save_button.className = "p-button p-component";
save_button.innerHTML = "Save snapshot";
save_button.onclick = () => { save_current_snapshot(); }
save_button.style.display = "inline-block";
save_button.style.horizontalAlign = "right";
save_button.style.width = "170px";
@ -282,15 +289,19 @@ export class SnapshotManager extends ComfyDialog {
this.message_box.style.height = '60px';
this.message_box.style.verticalAlign = 'middle';
this.element.appendChild(this.message_box);
this.element.appendChild(close_button);
this.element.appendChild(save_button);
const footer = $el("div.snapshot-footer");
const spacer = $el("div.cn-flex-auto");
footer.appendChild(spacer);
footer.appendChild(save_button);
this.content.appendChild(this.message_box);
this.content.appendChild(footer);
}
async show() {
try {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.display = "flex";
this.element.style.zIndex = 1099;
}
catch(exception) {

View File

@ -1,25 +1,264 @@
import json
import argparse
#!/usr/bin/env python3
"""JSON Entry Validator
def check_json_syntax(file_path):
Validates JSON entries based on content structure.
Validation rules based on JSON content:
- {"custom_nodes": [...]}: Validates required fields (author, title, reference, files, install_type, description)
- {"models": [...]}: Validates JSON syntax only (no required fields)
- Other JSON structures: Validates JSON syntax only
Git repository URL validation (for custom_nodes):
1. URLs must NOT end with .git
2. URLs must follow format: https://github.com/{author}/{reponame}
3. .py and .js files are exempt from this check
Supported formats:
- Array format: [{...}, {...}]
- Object format: {"custom_nodes": [...]} or {"models": [...]}
"""
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Tuple
# Required fields for each entry type
REQUIRED_FIELDS_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description']
REQUIRED_FIELDS_MODEL = [] # model-list.json doesn't require field validation
# Pattern for valid GitHub repository URL (without .git suffix)
GITHUB_REPO_PATTERN = re.compile(r'^https://github\.com/[^/]+/[^/]+$')
def get_entry_context(entry: Dict) -> str:
"""Get identifying information from entry for error messages
Args:
entry: JSON entry
Returns:
String with author and reference info
"""
parts = []
if 'author' in entry:
parts.append(f"author={entry['author']}")
if 'reference' in entry:
parts.append(f"ref={entry['reference']}")
if 'title' in entry:
parts.append(f"title={entry['title']}")
if parts:
return " | ".join(parts)
else:
# No identifying info - show actual entry content (truncated)
import json
entry_str = json.dumps(entry, ensure_ascii=False)
if len(entry_str) > 100:
entry_str = entry_str[:100] + "..."
return f"content={entry_str}"
def validate_required_fields(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
"""Validate that all required fields are present
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
required_fields: List of required field names
Returns:
List of error descriptions (without entry prefix/context)
"""
errors = []
for field in required_fields:
if field not in entry:
errors.append(f"Missing required field '{field}'")
elif entry[field] is None:
errors.append(f"Field '{field}' is null")
elif isinstance(entry[field], str) and not entry[field].strip():
errors.append(f"Field '{field}' is empty")
elif field == 'files' and not entry[field]: # Empty array
errors.append("Field 'files' is empty array")
return errors
def validate_git_repo_urls(entry: Dict, entry_index: int) -> List[str]:
"""Validate git repository URLs in 'files' array
Requirements:
- Git repo URLs must NOT end with .git
- Must follow format: https://github.com/{author}/{reponame}
- .py and .js files are exempt
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
Returns:
List of error descriptions (without entry prefix/context)
"""
errors = []
if 'files' not in entry or not isinstance(entry['files'], list):
return errors
for file_url in entry['files']:
if not isinstance(file_url, str):
continue
# Skip .py and .js files - they're exempt from git repo validation
if file_url.endswith('.py') or file_url.endswith('.js'):
continue
# Check if it's a GitHub URL (likely a git repo)
if 'github.com' in file_url:
# Error if URL ends with .git
if file_url.endswith('.git'):
errors.append(f"Git repo URL must NOT end with .git: {file_url}")
continue
# Validate format: https://github.com/{author}/{reponame}
if not GITHUB_REPO_PATTERN.match(file_url):
errors.append(f"Invalid git repo URL format (expected https://github.com/author/reponame): {file_url}")
return errors
def validate_entry(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
"""Validate a single JSON entry
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
required_fields: List of required field names
Returns:
List of error messages (empty if valid)
"""
errors = []
# Check required fields
errors.extend(validate_required_fields(entry, entry_index, required_fields))
# Check git repository URLs
errors.extend(validate_git_repo_urls(entry, entry_index))
return errors
def validate_json_file(file_path: str) -> Tuple[bool, List[str]]:
"""Validate JSON file containing entries
Args:
file_path: Path to JSON file
Returns:
Tuple of (is_valid, error_messages)
"""
errors = []
# Check file exists
path = Path(file_path)
if not path.exists():
return False, [f"File not found: {file_path}"]
# Load JSON
try:
with open(file_path, 'r', encoding='utf-8') as file:
json_str = file.read()
json.loads(json_str)
print(f"[ OK ] {file_path}")
except UnicodeDecodeError as e:
print(f"Unicode decode error: {e}")
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"[FAIL] {file_path}\n\n {e}\n")
except FileNotFoundError:
print(f"[FAIL] {file_path}\n\n File not found\n")
return False, [f"Invalid JSON: {e}"]
except Exception as e:
return False, [f"Error reading file: {e}"]
# Determine required fields based on JSON content
required_fields = []
# Validate structure - support both array and object formats
entries_to_validate = []
if isinstance(data, list):
# Direct array format: [{...}, {...}]
entries_to_validate = data
elif isinstance(data, dict):
# Object format: {"custom_nodes": [...]} or {"models": [...]}
# Determine validation based on keys
if 'custom_nodes' in data and isinstance(data['custom_nodes'], list):
required_fields = REQUIRED_FIELDS_CUSTOM_NODE
entries_to_validate = data['custom_nodes']
elif 'models' in data and isinstance(data['models'], list):
required_fields = REQUIRED_FIELDS_MODEL
entries_to_validate = data['models']
else:
# Other JSON structures (extension-node-map.json, etc.) - just validate JSON syntax
return True, []
else:
return False, ["JSON root must be either an array or an object containing arrays"]
# Validate each entry
for idx, entry in enumerate(entries_to_validate, start=1):
if not isinstance(entry, dict):
# Show actual value for type errors
entry_str = json.dumps(entry, ensure_ascii=False) if not isinstance(entry, str) else repr(entry)
if len(entry_str) > 150:
entry_str = entry_str[:150] + "..."
errors.append(f"\n❌ Entry #{idx}: Must be an object, got {type(entry).__name__}")
errors.append(f" Actual value: {entry_str}")
continue
entry_errors = validate_entry(entry, idx, required_fields)
if entry_errors:
# Group errors by entry with context
context = get_entry_context(entry)
errors.append(f"\n❌ Entry #{idx} ({context}):")
for error in entry_errors:
errors.append(f" - {error}")
is_valid = len(errors) == 0
return is_valid, errors
def main():
parser = argparse.ArgumentParser(description="JSON File Syntax Checker")
parser.add_argument("file_path", type=str, help="Path to the JSON file for syntax checking")
"""Main entry point"""
if len(sys.argv) < 2:
print("Usage: python json-checker.py <json-file>")
print("\nValidates JSON entries based on content:")
print(" - {\"custom_nodes\": [...]}: Validates required fields (author, title, reference, files, install_type, description)")
print(" - {\"models\": [...]}: Validates JSON syntax only (no required fields)")
print(" - Other JSON structures: Validates JSON syntax only")
print("\nGit repo URL validation (for custom_nodes):")
print(" - URLs must NOT end with .git")
print(" - URLs must follow: https://github.com/{author}/{reponame}")
sys.exit(1)
args = parser.parse_args()
check_json_syntax(args.file_path)
file_path = sys.argv[1]
if __name__ == "__main__":
is_valid, errors = validate_json_file(file_path)
if is_valid:
print(f"{file_path}: Validation passed")
sys.exit(0)
else:
print(f"Validating: {file_path}")
print("=" * 60)
print("❌ Validation failed!\n")
print("Errors:")
# Count actual errors (lines starting with " -")
error_count = sum(1 for e in errors if e.strip().startswith('-'))
for error in errors:
# Don't add ❌ prefix to grouped entries (they already have it)
if error.strip().startswith(''):
print(error)
else:
print(error)
print(f"\nTotal errors: {error_count}")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,5 +1,725 @@
{
"custom_nodes": [
{
"author": "starsFriday",
"title": "ComfyUI-KLingAI-OmniVideo [WIP]",
"reference": "https://github.com/starsFriday/ComfyUI-KLingAI-OmniVideo",
"files": [
"https://github.com/starsFriday/ComfyUI-KLingAI-OmniVideo"
],
"install_type": "git-clone",
"description": "Five API nodes for KLingAI's OmniVideo (O1) usage\nNOTE: The files in the repo are not organized."
},
{
"author": "Hifunyo",
"title": "comfyui_google_ai",
"reference": "https://github.com/Hifunyo/comfyui_google_ai",
"files": [
"https://github.com/Hifunyo/comfyui_google_ai"
],
"install_type": "git-clone",
"description": "ComfyUI integration node for Google AI image generation capabilities. (Description by CC)"
},
{
"author": "nschpy",
"title": "ComfyUI_MovisAdapter [UNSAFE]",
"reference": "https://github.com/nschpy/ComfyUI_MovisAdapter",
"files": [
"https://github.com/nschpy/ComfyUI_MovisAdapter"
],
"install_type": "git-clone",
"description": "A collection of custom nodes for ComfyUI[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "devzeroLL",
"title": "comfyui-lxj-Node",
"reference": "https://github.com/devzeroLL/comfyui-lxj-Node",
"files": [
"https://github.com/devzeroLL/comfyui-lxj-Node"
],
"install_type": "git-clone",
"description": "NODES: lxj_ImageBatch14, lxj_TextBatch14"
},
{
"author": "hgh086",
"title": "Comfyui-HghImage",
"reference": "https://github.com/hgh086/Comfyui-HghImage",
"files": [
"https://github.com/hgh086/Comfyui-HghImage"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image comparison functionality. (Description by CC)"
},
{
"author": "Ginolazy",
"title": "ComfyUI-FluxKontextImageCompensate [WIP]",
"reference": "https://github.com/Ginolazy/ComfyUI-FluxKontextImageCompensate",
"files": [
"https://github.com/Ginolazy/ComfyUI-FluxKontextImageCompensate"
],
"install_type": "git-clone",
"description": "A focused ComfyUI plugin to handle the vertical stretching issue introduced by the Flux Kontext model.\nNOTE: The files in the repo are not organized."
},
{
"author": "IO-AtelierTech",
"title": "comfyui-genai-connectors [WIP]",
"reference": "https://github.com/IO-AtelierTech/comfyui-genai-connectors",
"files": [
"https://github.com/IO-AtelierTech/comfyui-genai-connectors"
],
"install_type": "git-clone",
"description": "The ComfyUI-fal-Connector is a tool designed to provide an integration between ComfyUI and fal. This extension allows users to execute their ComfyUI workflows directly on [a/fal.ai](https://fal.ai/). This enables users to leverage the computational power and resources provided by fal.ai for running their ComfyUI workflows.\nNOTE: The files in the repo are not organized."
},
{
"author": "PozzettiAndrea",
"title": "ComfyUI-MVDUST3R [UNSAFE]",
"reference": "https://github.com/PozzettiAndrea/ComfyUI-MVDUST3R",
"files": [
"https://github.com/PozzettiAndrea/ComfyUI-MVDUST3R"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for MVDUST3R multi-view 3D reconstruction[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "ProjectAtlantis-dev",
"title": "comfyui-atlantis-json [UNSAFE]",
"reference": "https://github.com/ProjectAtlantis-dev/comfyui-atlantis-json",
"files": [
"https://github.com/ProjectAtlantis-dev/comfyui-atlantis-json"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for JSON processing and transcription workflows, including text-to-JSON conversion, SRT subtitle parsing, and file saving. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "ShammiG",
"title": "ComfyUI_Text_Tools_SG [UNSAFE]",
"reference": "https://github.com/ShammiG/ComfyUI_Text_Tools_SG",
"files": [
"https://github.com/ShammiG/ComfyUI_Text_Tools_SG"
],
"install_type": "git-clone",
"description": "Text Editor node with Markdown editing plus quick shortcuts, Text Viewer node, with extra features plus Text Merge, Text Save and Load Text from anywhere nodes.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "Smyshnikof",
"title": "ComfyUI-PresetDownloadManager [UNSAFE]",
"reference": "https://github.com/Smyshnikof/ComfyUI-PresetDownloadManager",
"files": [
"https://github.com/Smyshnikof/ComfyUI-PresetDownloadManager"
],
"install_type": "git-clone",
"description": "A custom ComfyUI node for managing and downloading models from HuggingFace with preset support[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "gulajawalegit",
"title": "ComfyUI-Telegram-Sender [UNSAFE]",
"reference": "https://github.com/gulajawalegit/ComfyUI-Telegram-Sender",
"files": [
"https://github.com/gulajawalegit/ComfyUI-Telegram-Sender"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for sending generated videos to Telegram, enabling direct output sharing to messaging platforms. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "Laolilzp",
"title": "Laoli3D [UNSAFE]",
"reference": "https://github.com/Laolilzp/Laoli3D",
"files": [
"https://github.com/Laolilzp/Laoli3D"
],
"install_type": "git-clone",
"description": "ComfyUI 3D pose editor enabling visual character manipulation and ControlNet image generation for precise AI figure control without prompt description. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "Goldlionren",
"title": "ComfyUI_Bridge_fabric [UNSAFE]",
"reference": "https://github.com/Goldlionren/ComfyUI_Bridge_fabric",
"files": [
"https://github.com/Goldlionren/ComfyUI_Bridge_fabric"
],
"install_type": "git-clone",
"description": "AI Compute Fabric bridge for ComfyUI enabling distributed CLIP, VAE, and ControlNet inference across local and remote machines. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "CypherNaught-0x",
"title": "ComfyUI-StarVector [WIP]",
"reference": "https://github.com/CypherNaught-0x/ComfyUI-StarVector",
"files": [
"https://github.com/CypherNaught-0x/ComfyUI-StarVector"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for SVG generation using StarVector models\nNOTE: The files in the repo are not organized."
},
{
"author": "Clivey1234",
"title": "ComfyUI_FBX_Import [UNSAFE]",
"reference": "https://github.com/Clivey1234/ComfyUI_FBX_Import",
"files": [
"https://github.com/Clivey1234/ComfyUI_FBX_Import"
],
"install_type": "git-clone",
"description": "Convert FBX animations into ControlNet OpenPose images for driving AI video generation with motion from any animation source. (Description by CC)[w/This nodepack has a vulnerability that allows arbitrary code execution remotely.]"
},
{
"author": "TobiasGlaubach",
"title": "ComfyUI-TG_PyCode [UNSAFE]",
"reference": "https://github.com/TobiasGlaubach/ComfyUI-TG_PyCode",
"files": [
"https://github.com/TobiasGlaubach/ComfyUI-TG_PyCode"
],
"install_type": "git-clone",
"description": "ComfyUI node library with an editor and nodes to run Python code within ComfyUI workflows.[w/This nodepack has a vulnerability that allows arbitrary code execution remotely.]"
},
{
"author": "jchiotaka",
"title": "ComfyUI-ClarityAI-Upscaler",
"reference": "https://github.com/jchiotaka/ComfyUI-ClarityAI-Upscaler",
"files": [
"https://github.com/jchiotaka/ComfyUI-ClarityAI-Upscaler"
],
"install_type": "git-clone",
"description": "ComfyUI upscaler nodes including ClarityCreativeUpscaler, ClarityCrystalUpscaler, and ClarityFluxUpscaler. (Description by CC)"
},
{
"author": "tpc2233",
"title": "ComfyUI-TP-IMtalker [WIP]",
"reference": "https://github.com/tpc2233/ComfyUI-TP-IMtalker",
"files": [
"https://github.com/tpc2233/ComfyUI-TP-IMtalker"
],
"install_type": "git-clone",
"description": "Comfy UI nodes for IMtalker to run native weights.)\nNOTE: The files in the repo are not organized."
},
{
"author": "yutrodimitri-ship-it",
"title": "ComfyUI-YUTRO-CastingStudio-v2 [WIP]",
"reference": "https://github.com/yutrodimitri-ship-it/ComfyUI-YUTRO-CastingStudio-v2",
"files": [
"https://github.com/yutrodimitri-ship-it/ComfyUI-YUTRO-CastingStudio-v2"
],
"install_type": "git-clone",
"description": "A professional modular suite of nodes for ComfyUI designed for virtual casting agencies, professional photographers, and content creators to generate high-quality model portfolios efficiently. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "yuyu0218yu",
"title": "comfyui-NXCM-tool [UNSAFE]",
"reference": "https://github.com/yuyu0218yu/comfyui-NXCM-tool",
"files": [
"https://github.com/yuyu0218yu/comfyui-NXCM-tool"
],
"install_type": "git-clone",
"description": "Next-generation creative media encryption toolkit for ComfyUI providing image and video encryption with AES-256-CTR + HMAC-SHA256, metadata preservation, and batch processing support. (Description by CC) [w/hardcoded encryption key]"
},
{
"author": "SergeyKarleev",
"title": "[WIP] comfyui-textutils",
"reference": "https://github.com/SergeyKarleev/comfyui-textutils",
"files": [
"https://github.com/SergeyKarleev/comfyui-textutils"
],
"install_type": "git-clone",
"description": "Small utility nodes for ComfyUI text workflows.\nNOTE: The files in the repo are not organized."
},
{
"author": "love530love",
"title": "[WIP] ComfyUI-TorchMonitor",
"reference": "https://github.com/love530love/ComfyUI-TorchMonitor",
"files": [
"https://github.com/love530love/ComfyUI-TorchMonitor"
],
"install_type": "git-clone",
"description": "Fixed-position real-time monitor for ComfyUI displaying CPU, RAM, VRAM, and GPU temperature metrics with zero configuration and single-file installation.\nNOTE: The files in the repo are not organized."
},
{
"author": "Enferlain",
"title": "ComfyUI-SamplerCustom-3Decimals",
"reference": "https://github.com/Enferlain/ComfyUI-SamplerCustom-3Decimals",
"files": [
"https://github.com/Enferlain/ComfyUI-SamplerCustom-3Decimals"
],
"install_type": "git-clone",
"description": "ComfyUI sampler node with custom 3 decimals precision for sampling parameters. (Description by CC)"
},
{
"author": "lfelipegg",
"title": "[WIP] lfgg_custom_nodes_comfyui",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes_comfyui",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes_comfyui"
],
"install_type": "git-clone",
"description": "LFGG custom nodes for ComfyUI providing resolution-first utilities, latent sizing by aspect ratio, and divisibility-aware image processing.\nNOTE: The files in the repo are not organized."
},
{
"author": "saltchicken",
"title": "ComfyUI-Selector",
"reference": "https://github.com/saltchicken/ComfyUI-Selector",
"files": [
"https://github.com/saltchicken/ComfyUI-Selector"
],
"install_type": "git-clone",
"description": "ComfyUI node providing simple selector functionality. (Description by CC)"
},
{
"author": "saltchicken",
"title": "ComfyUI-Video-Utils",
"reference": "https://github.com/saltchicken/ComfyUI-Video-Utils",
"files": [
"https://github.com/saltchicken/ComfyUI-Video-Utils"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for video processing including FinalFrameSelector and VideoMerge functionality. (Description by CC)"
},
{
"author": "EricRorich",
"title": "[WIP] ComfyUI-MegaTran-cutom-node",
"reference": "https://github.com/EricRorich/ComfyUI-MegaTran-cutom-node",
"files": [
"https://github.com/EricRorich/ComfyUI-MegaTran-cutom-node"
],
"install_type": "git-clone",
"description": "ComfyUI custom node applying image transformations (scaling, rotation, translation) around a pivot point with optional canvas expansion and pivot visualization. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "OhSeongHyeon",
"title": "comfyui-random-image-size",
"reference": "https://github.com/OhSeongHyeon/comfyui-random-image-size",
"files": [
"https://github.com/OhSeongHyeon/comfyui-random-image-size"
],
"install_type": "git-clone",
"description": "ComfyUI Random Image Size"
},
{
"author": "Enferlain",
"title": "ComfyUI-Model-Comparison [WIP]",
"reference": "https://github.com/Enferlain/ComfyUI-Model-Comparison",
"files": [
"https://github.com/Enferlain/ComfyUI-Model-Comparison"
],
"install_type": "git-clone",
"description": "ComfyUI node for comparing multiple models side-by-side. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "AMTPorn",
"title": "comfyui_amt",
"reference": "https://github.com/AMTPorn/comfyui_amt",
"files": [
"https://github.com/AMTPorn/comfyui_amt"
],
"install_type": "git-clone",
"description": "NODES: AMTStringDeduplication"
},
{
"author": "gajjar4",
"title": "ComfyUI-Qwen-Image-i2L [UNSAFE]",
"reference": "https://github.com/gajjar4/ComfyUI-Qwen-Image-i2L",
"files": [
"https://github.com/gajjar4/ComfyUI-Qwen-Image-i2L"
],
"install_type": "git-clone",
"description": "A fully optimized ComfyUI custom node for Qwen-Image-i2L (Image-to-LoRA) that extracts style, composition, or details from images and saves them as lightweight LoRA files with intelligent VRAM optimization. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "edvardtoth",
"title": "ComfyUI-ETNodes",
"reference": "https://github.com/edvardtoth/ComfyUI-ETNodes",
"files": [
"https://github.com/edvardtoth/ComfyUI-ETNodes"
],
"install_type": "git-clone",
"description": "NODES: ETNodes-Color-Selector, ETNodes-Gemini-API-Image, ETNodes-Gemini-API-Text, ETNodes-List-Items, ETNodes-List-Selector, ETNodes-Text-Previe"
},
{
"author": "jtydhr88",
"title": "ComfyUI-PolotnoCanvasEditor [UNSAFE]",
"reference": "https://github.com/jtydhr88/ComfyUI-PolotnoCanvasEditor",
"files": [
"https://github.com/jtydhr88/ComfyUI-PolotnoCanvasEditor"
],
"install_type": "git-clone",
"description": "Integrates Polotno Canvas Editor into ComfyUI for advanced image editing and design.[w/This nodepack contains a path traversal vulnerability.]"
},
{
"author": "Taremin",
"title": "comfyui-remove-print",
"reference": "https://github.com/Taremin/comfyui-remove-print",
"files": [
"https://github.com/Taremin/comfyui-remove-print"
],
"install_type": "git-clone",
"description": "ComfyUI extension for removing or suppressing print statements in node output. (Description by CC)"
},
{
"author": "JiangAogo",
"title": "ComfyUI-Gemini-API [WIP]",
"reference": "https://github.com/JiangAogo/ComfyUI-Gemini-API",
"files": [
"https://github.com/JiangAogo/ComfyUI-Gemini-API"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for Google Gemini API integration, supporting both text generation (LLM) and image generation.\nNOTE: The files in the repo are not organized."
},
{
"author": "AprEcho",
"title": "ComfyUI-RandomSeed",
"reference": "https://github.com/AprEcho/ComfyUI-RandomSeed",
"files": [
"https://github.com/AprEcho/ComfyUI-RandomSeed"
],
"install_type": "git-clone",
"description": "Generates random seed values for ComfyUI workflows. (Description by CC)"
},
{
"author": "Suzu008",
"title": "ComfyUI-ImageCritic",
"reference": "https://github.com/Suzu008/ComfyUI-ImageCritic",
"files": [
"https://github.com/Suzu008/ComfyUI-ImageCritic"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image analysis and evaluation. (Description by CC)"
},
{
"author": "Spicely",
"title": "[WIP] ComfyUI-Luma",
"reference": "https://github.com/Spicely/ComfyUI-Luma",
"files": [
"https://github.com/Spicely/ComfyUI-Luma"
],
"install_type": "git-clone",
"description": "ComfyUI utility providing video and audio processing capabilities including text watermarking, audio-video separation, and audio-to-subtitle conversion. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "SSYCloud",
"title": "comfyui-ssy-syncapi [WIP]",
"reference": "https://github.com/SSYCloud/comfyui-ssy-syncapi",
"files": [
"https://github.com/SSYCloud/comfyui-ssy-syncapi"
],
"install_type": "git-clone",
"description": "Powerful ComfyUI custom node collection providing 4 dedicated nodes to access SSY Cloud synchronous image generation and processing models including Google Gemini, ByteDance Doubao, OpenAI, and image enhancement APIs. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "LMietkiewicz",
"title": "HiggsfieldAPI_Node",
"reference": "https://github.com/LMietkiewicz/HiggsfieldAPI_Node",
"files": [
"https://github.com/LMietkiewicz/HiggsfieldAPI_Node"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for Higgsfield API integration. (Description by CC)"
},
{
"author": "starsFriday",
"title": "ComfyUI-LongCat-Image [WIP]",
"reference": "https://github.com/starsFriday/ComfyUI-LongCat-Image",
"files": [
"https://github.com/starsFriday/ComfyUI-LongCat-Image"
],
"install_type": "git-clone",
"description": "ComfyUI wrapper nodes for the LongCat-Image text-to-image and image-editing pipelines with Python 3.12 support.\nNOTE: The files in the repo are not organized."
},
{
"author": "logicalor",
"title": "comfyui_mv_adapter [WIP]",
"reference": "https://github.com/logicalor/comfyui_mv_adapter",
"files": [
"https://github.com/logicalor/comfyui_mv_adapter"
],
"install_type": "git-clone",
"description": "MV-Adapter nodes for ComfyUI - Multi-view image generation\nNOTE: The files in the repo are not organized."
},
{
"author": "rafstahelin",
"title": "ComfyUI_KieNanoBananaPro",
"reference": "https://github.com/rafstahelin/ComfyUI_KieNanoBananaPro",
"files": [
"https://github.com/rafstahelin/ComfyUI_KieNanoBananaPro"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for processing and filtering using KieNanoBananaPro algorithm. (Description by CC)"
},
{
"author": "jinchanz",
"title": "ComfyUI-Midjourney",
"reference": "https://github.com/jinchanz/ComfyUI-Midjourney",
"files": [
"https://github.com/jinchanz/ComfyUI-Midjourney"
],
"install_type": "git-clone",
"description": "ComfyUI integration for Midjourney API with nodes for submitting requests, polling results, and extracting JSON data. (Description by CC)"
},
{
"author": "saltchicken",
"title": "ComfyUI-Local-Loader",
"reference": "https://github.com/saltchicken/ComfyUI-Local-Loader",
"files": [
"https://github.com/saltchicken/ComfyUI-Local-Loader"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for loading images from specified directories and file paths. (Description by CC)"
},
{
"author": "satyasairazole",
"title": "ComfyUI-Turbandetection [WIP]",
"reference": "https://github.com/satyasairazole/ComfyUI-Turbandetection",
"files": [
"https://github.com/satyasairazole/ComfyUI-Turbandetection"
],
"install_type": "git-clone",
"description": "ComfyUI node for detecting turbans in images using computer vision. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "dougbtv",
"title": "comfyui-vllm-omni",
"reference": "https://github.com/dougbtv/comfyui-vllm-omni",
"files": [
"https://github.com/dougbtv/comfyui-vllm-omni"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for vLLM-Omni text-to-image generation"
},
{
"author": "u5dev",
"title": "ComfyUI_u5_EasyScripter [UNSAFE]",
"reference": "https://github.com/u5dev/ComfyUI_u5_EasyScripter",
"files": [
"https://github.com/u5dev/ComfyUI_u5_EasyScripter"
],
"install_type": "git-clone",
"description": "EASY VBA-style script for ComfyUI. Anything you want be in 1 node. Conditional branching, iteration, prompt generation, parameter adjustment, memory release, file input/output, Using HTTP RestAPI ... with 100+ built-in functions![w/This nodepack has RCE, SSRF, and arbitrary file read vulnerabilities.]"
},
{
"author": "stalkervr",
"title": "ComfyUI-StalkerVr",
"reference": "https://github.com/stalkervr/ComfyUI-StalkerVr",
"files": [
"https://github.com/stalkervr/ComfyUI-StalkerVr"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for image processing, aspect ratio fixing, batch cropping, grid manipulation, JSON handling, and value extraction. (Description by CC)"
},
{
"author": "baoanhng",
"title": "ComfyUI-utils",
"reference": "https://github.com/baoanhng/ComfyUI-utils",
"files": [
"https://github.com/baoanhng/ComfyUI-utils"
],
"install_type": "git-clone",
"description": "Provides text utility nodes (TextJoiner, TextSplitter) for ComfyUI workflows. (Description by CC)"
},
{
"author": "xuchenxu168",
"title": "[WIP] comfyui_meituan_image",
"reference": "https://github.com/xuchenxu168/comfyui_meituan_image",
"files": [
"https://github.com/xuchenxu168/comfyui_meituan_image"
],
"install_type": "git-clone",
"description": "Generate high-quality images from text prompts with excellent Chinese text renderingEdit images using natural language instructions..\nNOTE: The files in the repo are not organized."
},
{
"author": "saltchicken",
"title": "ComfyUI-Identity-Mixer",
"reference": "https://github.com/saltchicken/ComfyUI-Identity-Mixer",
"files": [
"https://github.com/saltchicken/ComfyUI-Identity-Mixer"
],
"install_type": "git-clone",
"description": "Mixes multiple identity LoRAs with normalized strength."
},
{
"author": "saltchicken",
"title": "ComfyUI-Prompter",
"reference": "https://github.com/saltchicken/ComfyUI-Prompter",
"files": [
"https://github.com/saltchicken/ComfyUI-Prompter"
],
"install_type": "git-clone",
"description": "ComfyUI custom node providing customizable prompt generation capabilities. (Description by CC)"
},
{
"author": "JBKing514",
"title": "[WIP] map_comfyui",
"reference": "https://github.com/JBKing514/map_comfyui",
"files": [
"https://github.com/JBKing514/map_comfyui"
],
"install_type": "git-clone",
"description": "A custom node implementing the Manifold Alignment Protocol (MAP) within ComfyUI, transforming diffusion sampling into a measurable and visualizable geometric process. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "Nynxz",
"title": "ComfyUI_DiffsynthPause",
"reference": "https://github.com/Nynxz/ComfyUI_DiffsynthPause",
"files": [
"https://github.com/Nynxz/ComfyUI_DiffsynthPause"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for controlling Diffsynth checkpoint pausing behavior during image generation workflows. (Description by CC)"
},
{
"author": "binarystatic",
"title": "ComfyUI-BinarystaticMasterSeed",
"reference": "https://github.com/binarystatic/ComfyUI-BinarystaticMasterSeed",
"files": [
"https://github.com/binarystatic/ComfyUI-BinarystaticMasterSeed"
],
"install_type": "git-clone",
"description": "BinarystaticMasterSeed node for ComfyUI. (Description by CC)"
},
{
"author": "Aruntd008",
"title": "[WIP] ComfyUI_SeamlessPattern",
"reference": "https://github.com/Aruntd008/ComfyUI_SeamlessPattern",
"files": [
"https://github.com/Aruntd008/ComfyUI_SeamlessPattern"
],
"install_type": "git-clone",
"description": "SeamlessPatternNode for ComfyUI. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "SilentLuxRay",
"title": "[WIP] ComfyUI-Furrey-Super-Prompt",
"reference": "https://github.com/SilentLuxRay/ComfyUI-Furrey-Super-Prompt",
"files": [
"https://github.com/SilentLuxRay/ComfyUI-Furrey-Super-Prompt"
],
"install_type": "git-clone",
"description": "A personalized all-in-one node for ComfyUI that simplifies prompt management and LoRA handling with automatic translation to English. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "Rayen21",
"title": "[WIP] ComfyUI-PromptLinePlus",
"reference": "https://github.com/Rayen21/ComfyUI-PromptLinePlus",
"files": [
"https://github.com/Rayen21/ComfyUI-PromptLinePlus"
],
"install_type": "git-clone",
"description": "ComfyUI custom node that splits multi-line prompts by line, enabling batch image generation with each line triggering one execution and supporting custom prompt boxes. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "rookiestar28",
"title": "ComfyUI_Security_Audit",
"reference": "https://github.com/rookiestar28/ComfyUI_Security_Audit",
"files": [
"https://github.com/rookiestar28/ComfyUI_Security_Audit"
],
"install_type": "git-clone",
"description": "A lightweight, dual-layer security extension for ComfyUI using AST-based static analysis and runtime monitoring to detect malicious code in custom nodes."
},
{
"author": "c1660181647-hash",
"title": "ComfyUI-MM-Visual-Encryption",
"reference": "https://github.com/c1660181647-hash/ComfyUI-MM-Visual-Encryption",
"files": [
"https://github.com/c1660181647-hash/ComfyUI-MM-Visual-Encryption"
],
"install_type": "git-clone",
"description": "A visual noise encryption custom node for ComfyUI, supporting Image and Video privacy protection."
},
{
"author": "charlierz",
"title": "comfyui-charlierz",
"reference": "https://github.com/charlierz/comfyui-charlierz",
"files": [
"https://github.com/charlierz/comfyui-charlierz"
],
"install_type": "git-clone",
"description": "NODES: BackgroundColor, ScaleDimensions"
},
{
"author": "lrzjason",
"title": "Comfyui-DiffusersUtils [WIP]",
"reference": "https://github.com/lrzjason/Comfyui-DiffusersUtils",
"files": [
"https://github.com/lrzjason/Comfyui-DiffusersUtils"
],
"install_type": "git-clone",
"description": "A set of nodes which provide flexible inference using diffusers in comfyui env. (Description by CC)"
},
{
"author": "anilstream",
"title": "ComfyUI-NanoBananaPro",
"reference": "https://github.com/anilstream/ComfyUI-NanoBananaPro",
"files": [
"https://github.com/anilstream/ComfyUI-NanoBananaPro"
],
"install_type": "git-clone",
"description": "ComfyUI node implementing basic functionality with NanoBananaBasicNode. (Description by CC)"
},
{
"author": "Toxic1228",
"title": "Eleven-labs-comfyui-sts",
"reference": "https://github.com/Toxic1228/Eleven-labs-comfyui-sts",
"files": [
"https://github.com/Toxic1228/Eleven-labs-comfyui-sts"
],
"install_type": "git-clone",
"description": "ComfyUI integration node for Eleven Labs text-to-speech service (requires API key). (Description by CC)"
},
{
"author": "NeoTech",
"title": "comfyui-laserprep",
"reference": "https://github.com/NeoTech/comfyui-laserprep",
"files": [
"https://github.com/NeoTech/comfyui-laserprep"
],
"install_type": "git-clone",
"description": "ComfyUI node implementing laser preparation functionality with LaserPrep node. (Description by CC)"
},
{
"author": "Enferlain",
"title": "ComfyUI-extra-schedulers [WIP]",
"reference": "https://github.com/Enferlain/ComfyUI-extra-schedulers",
"files": [
"https://github.com/Enferlain/ComfyUI-extra-schedulers"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes providing additional scheduler implementations for advanced sampling control. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "tiange-tree",
"title": "BLUEAI_ComfyUI_OpenAI",
"reference": "https://github.com/tiange-tree/BLUEAI_ComfyUI_OpenAI",
"files": [
"https://github.com/tiange-tree/BLUEAI_ComfyUI_OpenAI"
],
"install_type": "git-clone",
"description": "NODES: BLUEAI_OpenAI_Node"
},
{
"author": "nestflow",
"title": "ComfyUI-WanPlus",
"reference": "https://github.com/nestflow/ComfyUI-WanPlus",
"files": [
"https://github.com/nestflow/ComfyUI-WanPlus"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for video frame manipulation and image-to-video conversion. (Description by CC)"
},
{
"author": "twdockery",
"title": "ComfyUI_Prompt_Batch_Generator",
"reference": "https://github.com/twdockery/ComfyUI_Prompt_Batch_Generator",
"files": [
"https://github.com/twdockery/ComfyUI_Prompt_Batch_Generator"
],
"install_type": "git-clone",
"description": "Custom nodes for batch image generation with Stable Diffusion 1.5, optimized for low VRAM systems. (Description by CC)"
},
{
"author": "tuxiansheng-ld",
"title": "Comfyui-tuxiansheng-nodes",
"reference": "https://github.com/tuxiansheng-ld/Comfyui-tuxiansheng-nodes",
"files": [
"https://github.com/tuxiansheng-ld/Comfyui-tuxiansheng-nodes"
],
"install_type": "git-clone",
"description": "NODES: StringToListNode"
},
{
"author": "krakenunbound",
"title": "Kraken Discord Bot",
@ -71,16 +791,6 @@
"install_type": "git-clone",
"description": "ComfyUI node for tracking and displaying LoRA parameters. (Description by CC)"
},
{
"author": "anilsathyan7",
"title": "ComfyUI-Crystal-Upscaler",
"reference": "https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler",
"files": [
"https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image upscaling using crystal upscaling technology. (Description by CC)"
},
{
"author": "SleazySleaze",
"title": "aesthetic-persona-comfyui-node",
@ -441,16 +1151,6 @@
"install_type": "git-clone",
"description": "Integrated Qwen-Image node for ComfyUI with all-in-one model loading, 4 LoRA slots, memory optimization via BlockSwap reducing VRAM usage by 30-60%, and multiple quantization options.\nNOTE: The files in the repo are not organized."
},
{
"author": "nohikomiso",
"title": "ComfyUI-ImageFolderPicker [UNSAFE]",
"reference": "https://github.com/nohikomiso/ComfyUI-ImageFolderPicker",
"files": [
"https://github.com/nohikomiso/ComfyUI-ImageFolderPicker"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for browsing local server folders and selecting images via thumbnail display in a grid interface. (Description by CC)[w/This nodepack has a vulnerability that allows it to retrieve a list of files from arbitrary paths.]"
},
{
"author": "tori29umai0123",
"title": "ComfyUI-SDXLGenerateFromTextFile [UNSAFE]",
@ -1184,16 +1884,6 @@
"install_type": "git-clone",
"description": "ComfyUI-CC-ImageLoader is an enhanced image loading node designed for ComfyUI. It is developed based on two excellent projects: ComfyUI-Thumbnails and ComfyUI_Local_Media_Manager.[w/This nodepack includes an endpoint that access files from arbitrary paths.]"
},
{
"author": "rzasharp79",
"title": "ComfyUI--SolarFlare",
"reference": "https://github.com/rzasharp79/ComfyUI--SolarFlare",
"files": [
"https://github.com/rzasharp79/ComfyUI--SolarFlare"
],
"install_type": "git-clone",
"description": "NODES: Qwen Image, ..."
},
{
"author": "A1rCHAN",
"title": "Eric's Prompt Enhancers for ComfyUI# Eric's Prompt Enhancers for ComfyUI",
@ -1284,16 +1974,6 @@
"install_type": "git-clone",
"description": "A powerful ComfyUI extension that helps you focus on selected nodes by highlighting their connections while dimming or hiding unrelated links\nNOTE: The files in the repo are not organized."
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [WIP]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "pizurny",
"title": "ComfyUI Feedback Sampler [WIP]",
@ -1714,16 +2394,6 @@
"install_type": "git-clone",
"description": "NODES: 'Pluto : Auto Crop Faces', 'Pluto : Composite Image', 'Pluto : Float Math', 'Pluto : GetSizeFromImage', 'Pluto : Text Append', ..."
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [WIP]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "lu64k",
"title": "ks_nodes",
@ -4729,7 +5399,8 @@
"description": "NODES: Face Detector Selector, YC Human Parts Ultra(Advance), Color Match (YC)"
},
{
"author": "virallover",
"author": "maizerrr",
"title": "comfyui-code-nodes",
"reference": "https://github.com/maizerrr/comfyui-code-nodes",
"files": [
"https://github.com/maizerrr/comfyui-code-nodes"
@ -4927,16 +5598,6 @@
"description": "NODES: Properties, Apply SVG to Image",
"install_type": "git-clone"
},
{
"author": "AhBumm",
"title": "ComfyUI_MangaLineExtraction",
"reference": "https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf",
"files": [
"https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf"
],
"description": "p1atdev/MangaLineExtraction-hf as a node in comfyui",
"install_type": "git-clone"
},
{
"author": "Kur0butiMegane",
"title": "Comfyui-StringUtils",
@ -4987,16 +5648,6 @@
"install_type": "git-clone",
"description": "ComfyUI implementation of the partfield nvidea segmentation models\nNOTE: The files in the repo are not organized."
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "silveroxides",
"title": "ComfyUI_ReduxEmbedToolkit",
@ -8289,16 +8940,6 @@
"install_type": "git-clone",
"description": "Gets a random file from a directory. Returns the filpath as a STRING. [w/This node allows access to arbitrary files through the workflow, which could pose a security threat.]"
},
{
"author": "neeltheninja",
"title": "ComfyUI-ControlNeXt [WIP]",
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
"files": [
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
],
"install_type": "git-clone",
"description": "In progress🚧"
},
{
"author": "neeltheninja",
"title": "ComfyUI-TextOverlay",
@ -8350,16 +8991,6 @@
"install_type": "git-clone",
"description": "A powerful debugging tool designed to provide in-depth analysis of your environment and dependencies by exposing API endpoints. This tool allows you to inspect environment variables, pip packages, python info and dependency trees, making it easier to diagnose and resolve issues in your ComfyUI setup.[w/This tool may expose sensitive system information if used on a public server]"
},
{
"author": "Futureversecom",
"title": "ComfyUI-JEN",
"reference": "https://github.com/futureversecom/ComfyUI-JEN",
"files": [
"https://github.com/futureversecom/ComfyUI-JEN"
],
"install_type": "git-clone",
"description": "Comfy UI custom nodes for JEN music generation powered by Futureverse"
},
{
"author": "denislov",
"title": "Comfyui_AutoSurvey",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,15 @@
{
"custom_nodes": [
{
"author": "Fossiel",
"title": "ComfyUI-MultiGPU-Patched",
"reference": "https://github.com/Fossiel/ComfyUI-MultiGPU-Patched",
"files": [
"https://github.com/Fossiel/ComfyUI-MultiGPU-Patched"
],
"install_type": "git-clone",
"description": "Patched fork of ComfyUI-MultiGPU providing universal .safetensors and GGUF multi-GPU distribution with DisTorch 2.0 engine, model-driven allocation options (bytes/ratio modes), WanVideoWrapper integration, and up to 10% faster GGUF inference. (Description by CC)"
},
{
"author": "synchronicity-labs",
"title": "ComfyUI Sync Lipsync Node",

View File

@ -1,5 +1,298 @@
{
"custom_nodes": [
{
"author": "AhBumm",
"title": "ComfyUI_MangaLineExtraction [REMOVED]",
"reference": "https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf",
"files": [
"https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf"
],
"description": "p1atdev/MangaLineExtraction-hf as a node in comfyui",
"install_type": "git-clone"
},
{
"author": "danieljanata",
"title": "ComfyUI-QwenVL-Override [REMOVED]",
"reference": "https://github.com/danieljanata/ComfyUI-QwenVL-Override",
"files": [
"https://github.com/danieljanata/ComfyUI-QwenVL-Override"
],
"install_type": "git-clone",
"description": "Adds two nodes that reuse upstream ComfyUI-QwenVL presets but add a runtime override that can be wired/unwired without getting stuck."
},
{
"author": "Futureversecom",
"title": "ComfyUI-JEN [REMOVED]",
"reference": "https://github.com/futureversecom/ComfyUI-JEN",
"files": [
"https://github.com/futureversecom/ComfyUI-JEN"
],
"install_type": "git-clone",
"description": "Comfy UI custom nodes for JEN music generation powered by Futureverse"
},
{
"author": "TheBill2001",
"title": "comfyui-upscale-by-model [REMOVED]",
"reference": "https://github.com/TheBill2001/comfyui-upscale-by-model",
"files": [
"https://github.com/TheBill2001/comfyui-upscale-by-model"
],
"install_type": "git-clone",
"description": "This custom node allow upscaling an image by a factor using a model."
},
{
"author": "XYMikky12138",
"title": "ComfyUI-NanoBanana-inpaint [REMOVED]",
"reference": "https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint",
"files": [
"https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for API-based inpainting (Gemini, Imagen) with aspect ratio constraints, smart cropping, resize fitting, intelligent paste-back with transparency support. (Description by CC)"
},
{
"author": "Blonicx",
"title": "ComfyUI-Rework-X [REMOVED]",
"id": "rework-x",
"reference": "https://github.com/Blonicx/ComfyUI-X-Rework",
"files": [
"https://github.com/Blonicx/ComfyUI-X-Rework"
],
"install_type": "git-clone",
"description": "This is a plugin for ComfyUI that adds new Util Nodes and Nodes for easier image creation and sharing."
},
{
"author": "scott-createplay",
"title": "ComfyUI_video_essentials [REMOVED]",
"reference": "https://github.com/scott-createplay/ComfyUI_video_essentials",
"files": [
"https://github.com/scott-createplay/ComfyUI_video_essentials"
],
"install_type": "git-clone",
"description": "Essential video processing nodes for ComfyUI"
},
{
"author": "thnikk",
"title": "comfyui-thnikk-utils [REMOVED]",
"reference": "https://github.com/thnikk/comfyui-thnikk-utils",
"files": [
"https://github.com/thnikk/comfyui-thnikk-utils"
],
"install_type": "git-clone",
"description": "Nodes to clean up your workflow."
},
{
"author": "Tr1dae",
"title": "LoRA Matcher Nodes for ComfyUI [REMOVED]",
"reference": "https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher",
"files": [
"https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher"
],
"install_type": "git-clone",
"description": "This custom node provides two different approaches to automatically match text prompts with LoRA models using their descriptions."
},
{
"author": "jkhayiying",
"title": "ImageLoadFromLocalOrUrl Node for ComfyUI [REMOVED]",
"id": "JkhaImageLoaderPathOrUrl",
"reference": "https://gitee.com/yyh915/jkha-load-img",
"files": [
"https://gitee.com/yyh915/jkha-load-img"
],
"install_type": "git-clone",
"description": "This is a node to load an image from local path or url."
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [REMOVED]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [REMOVED]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "AndSni",
"title": "Comfy-FL-Nodes [REMOVED]",
"reference": "https://github.com/AndSni/Comfy-FL-Nodes",
"files": [
"https://github.com/AndSni/Comfy-FL-Nodes"
],
"install_type": "git-clone",
"description": "Generates human characters for commerce applications in ComfyUI. (Description by CC)"
},
{
"author": "neeltheninja",
"title": "ComfyUI-ControlNeXt [REMOVED]",
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
"files": [
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
],
"install_type": "git-clone",
"description": "In progress🚧"
},
{
"author": "TheArtOfficial",
"title": "ComfyUI-Nitra [REMOVED]",
"reference": "https://github.com/TheArtOfficial/ComfyUI-Nitra",
"files": [
"https://github.com/TheArtOfficial/ComfyUI-Nitra"
],
"install_type": "git-clone",
"description": "Nitra custom node for ComfyUI"
},
{
"author": "AngelCookies",
"title": "ComfyUI-Seed-Tracker [REMOVED]",
"reference": "https://github.com/AngelCookies/ComfyUI-Seed-Tracker",
"files": [
"https://github.com/AngelCookies/ComfyUI-Seed-Tracker"
],
"install_type": "git-clone",
"description": "A ComfyUI extension that tracks random seeds throughout your image generation workflows"
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened [REMOVED]",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "ashtar1984",
"title": "comfyui-switch-bypass-mute-by-group [REMOVED]",
"reference": "https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group",
"files": [
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for group-based node switching, bypassing, and muting control. (Description by CC)"
},
{
"author": "wallen0322",
"title": "ComfyUI-TTM-WAN22 [REMOVED]",
"reference": "https://github.com/wallen0322/ComfyUI-TTM-WAN22",
"files": [
"https://github.com/wallen0322/ComfyUI-TTM-WAN22"
],
"install_type": "git-clone",
"description": "TTM (Time-to-Move) node for ComfyUI enabling motion-controlled video generation with Wan2.2 models using dual-clock denoising for independent background and object animation control."
},
{
"author": "cdanielp",
"title": "COMFYUI_PROMPTMODELS [REMOVED]",
"reference": "https://github.com/cdanielp/COMFYUI_PROMPTMODELS",
"files": [
"https://github.com/cdanielp/COMFYUI_PROMPTMODELS"
],
"install_type": "git-clone",
"description": "Custom nodes for ComfyUI by PROMPTMODELS."
},
{
"author": "mcrataobrabo",
"title": "comfyui-smart-lora-downloader - Automatically Fetch Missing LoRAs [REMOVED]",
"reference": "https://github.com/mcrataobrabo/comfyui-smart-lora-downloader",
"files": [
"https://github.com/mcrataobrabo/comfyui-smart-lora-downloader"
],
"install_type": "git-clone",
"description": "Automatically detect and download missing LoRAs for ComfyUI workflows"
},
{
"author": "KANAsho34636",
"title": "ComfyUI-NaturalSort-ImageLoader [REMOVED]",
"reference": "https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader",
"files": [
"https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader"
],
"install_type": "git-clone",
"description": "Custom image loader node supporting natural number sorting with multiple sort modes (natural, lexicographic, modification time, creation time, reverse natural). (Description by CC)"
},
{
"author": "johninthewinter",
"title": "comfyui-fal-flux-2-John [REMOVED]",
"reference": "https://github.com/johninthewinter/comfyui-fal-flux-2-John",
"files": [
"https://github.com/johninthewinter/comfyui-fal-flux-2-John"
],
"install_type": "git-clone",
"description": "Custom nodes for ComfyUI that integrate with fal.ai's FLUX 2 and FLUX 1 LoRA APIs for text-to-image generation."
},
{
"author": "LargeModGames",
"title": "ComfyUI LoRA Auto Downloader [REMOVED]",
"reference": "https://github.com/LargeModGames/comfyui-smart-lora-downloader",
"files": [
"https://github.com/LargeModGames/comfyui-smart-lora-downloader"
],
"install_type": "git-clone",
"description": "Automatically download missing LoRAs from CivitAI and detect missing LoRAs in workflows. Features smart directory detection and easy installation."
},
{
"author": "DiffusionWave",
"title": "PickResolution_DiffusionWave [DEPRECATED]",
"reference": "https://github.com/DiffusionWave/PickResolution_DiffusionWave",
"files": [
"https://github.com/DiffusionWave/PickResolution_DiffusionWave"
],
"install_type": "git-clone",
"description": "A custom node for ComfyUI that allows selecting a base resolution, applying a custom scaling value based on FLOAT (up to 10 decimal places), and adding an extra integer value. Outputs include both INT and FLOAT resolutions, making it perfect for you to play around with."
},
{
"author": "geltz",
"title": "ComfyUI-geltz [REMOVED]",
"reference": "https://github.com/geltz/ComfyUI-geltz",
"files": [
"https://github.com/geltz/ComfyUI-geltz"
],
"install_type": "git-clone",
"description": "Various custom nodes; guidance, latents, sampling, tokenization, etc."
},
{
"author": "anilsathyan7",
"title": "ComfyUI-Crystal-Upscaler [REMOVED]",
"reference": "https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler",
"files": [
"https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image upscaling using crystal upscaling technology. (Description by CC)"
},
{
"author": "nohikomiso",
"title": "ComfyUI-ImageFolderPicker [REMOVED/UNSAFE]",
"reference": "https://github.com/nohikomiso/ComfyUI-ImageFolderPicker",
"files": [
"https://github.com/nohikomiso/ComfyUI-ImageFolderPicker"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for browsing local server folders and selecting images via thumbnail display in a grid interface. (Description by CC)[w/This nodepack has a vulnerability that allows it to retrieve a list of files from arbitrary paths.]"
},
{
"author": "rzasharp79",
"title": "ComfyUI--SolarFlare [REMOVED]",
"reference": "https://github.com/rzasharp79/ComfyUI--SolarFlare",
"files": [
"https://github.com/rzasharp79/ComfyUI--SolarFlare"
],
"install_type": "git-clone",
"description": "NODES: Qwen Image, ..."
},
{
"author": "shinich39",
"title": "comfyui-no-one-above-me [REMOVED]",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[project]
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."
version = "3.38.1"
version = "3.39"
license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]

View File

@ -16,6 +16,108 @@ import sys
from urllib.parse import urlparse
from github import Github, Auth
from pathlib import Path
from typing import Set, Dict, Optional
# Scanner version for cache invalidation
SCANNER_VERSION = "2.0.12" # Add dict comprehension + export list detection
# Cache for extract_nodes and extract_nodes_enhanced results
_extract_nodes_cache: Dict[str, Set[str]] = {}
_extract_nodes_enhanced_cache: Dict[str, Set[str]] = {}
_file_mtime_cache: Dict[Path, float] = {}
def _get_repo_root(file_path: Path) -> Optional[Path]:
"""Find the repository root directory containing .git"""
current = file_path if file_path.is_dir() else file_path.parent
while current != current.parent:
if (current / ".git").exists():
return current
current = current.parent
return None
def _get_repo_hash(repo_path: Path) -> str:
"""Get git commit hash or fallback identifier"""
git_dir = repo_path / ".git"
if not git_dir.exists():
return ""
try:
# Read HEAD to get current commit
head_file = git_dir / "HEAD"
if head_file.exists():
head_content = head_file.read_text().strip()
if head_content.startswith("ref:"):
# HEAD points to a ref
ref_path = git_dir / head_content[5:].strip()
if ref_path.exists():
commit_hash = ref_path.read_text().strip()
return commit_hash[:16] # First 16 chars
else:
# Detached HEAD
return head_content[:16]
except:
pass
return ""
def _load_per_repo_cache(repo_path: Path) -> Optional[tuple]:
"""Load nodes and metadata from per-repo cache
Returns:
tuple: (nodes_set, metadata_dict) or None if cache invalid
"""
cache_file = repo_path / ".git" / "nodecache.json"
if not cache_file.exists():
return None
try:
with open(cache_file, 'r') as f:
cache_data = json.load(f)
# Verify scanner version
if cache_data.get('scanner_version') != SCANNER_VERSION:
return None
# Verify git hash
current_hash = _get_repo_hash(repo_path)
if cache_data.get('git_hash') != current_hash:
return None
# Return nodes and metadata
nodes = cache_data.get('nodes', [])
metadata = cache_data.get('metadata', {})
return (set(nodes) if nodes else set(), metadata)
except:
return None
def _save_per_repo_cache(repo_path: Path, all_nodes: Set[str], metadata: dict = None):
"""Save nodes and metadata to per-repo cache"""
cache_file = repo_path / ".git" / "nodecache.json"
if not cache_file.parent.exists():
return
git_hash = _get_repo_hash(repo_path)
cache_data = {
"scanner_version": SCANNER_VERSION,
"git_hash": git_hash,
"scanned_at": datetime.datetime.now().isoformat(),
"nodes": sorted(list(all_nodes)),
"metadata": metadata if metadata else {}
}
try:
with open(cache_file, 'w') as f:
json.dump(cache_data, f, indent=2)
except:
pass # Silently fail - cache is optional
def download_url(url, dest_folder, filename=None):
@ -51,11 +153,12 @@ Examples:
# Standard mode
python3 scanner.py
python3 scanner.py --skip-update
python3 scanner.py --skip-all --force-rescan
# Scan-only mode
python3 scanner.py --scan-only temp-urls-clean.list
python3 scanner.py --scan-only urls.list --temp-dir /custom/temp
python3 scanner.py --scan-only urls.list --skip-update
python3 scanner.py --scan-only urls.list --skip-update --force-rescan
'''
)
@ -69,6 +172,8 @@ Examples:
help='Skip GitHub stats collection')
parser.add_argument('--skip-all', action='store_true',
help='Skip all update operations')
parser.add_argument('--force-rescan', action='store_true',
help='Force rescan all nodes (ignore cache)')
# Backward compatibility: positional argument for temp_dir
parser.add_argument('temp_dir_positional', nargs='?', metavar='TEMP_DIR',
@ -94,6 +199,11 @@ parse_cnt = 0
def extract_nodes(code_text):
global parse_cnt
# Check cache first
cache_key = hash(code_text)
if cache_key in _extract_nodes_cache:
return _extract_nodes_cache[cache_key].copy()
try:
if parse_cnt % 100 == 0:
print(".", end="", flush=True)
@ -128,12 +238,614 @@ def extract_nodes(code_text):
if key is not None and isinstance(key.value, str):
s.add(key.value.strip())
# Cache the result
_extract_nodes_cache[cache_key] = s
return s
else:
# Cache empty result
_extract_nodes_cache[cache_key] = set()
return set()
except:
# Cache empty result on error
_extract_nodes_cache[cache_key] = set()
return set()
def extract_nodes_from_repo(repo_path: Path, verbose: bool = False, force_rescan: bool = False) -> tuple:
"""
Extract all nodes and metadata from a repository with per-repo caching.
Automatically caches results in .git/nodecache.json.
Cache is invalidated when:
- Git commit hash changes
- Scanner version changes
- force_rescan flag is True
Args:
repo_path: Path to repository root
verbose: If True, print UI-only extension detection messages
force_rescan: If True, ignore cache and force fresh scan
Returns:
tuple: (nodes_set, metadata_dict)
"""
# Ensure path is absolute
repo_path = repo_path.resolve()
# Check per-repo cache first (unless force_rescan is True)
if not force_rescan:
cached_result = _load_per_repo_cache(repo_path)
if cached_result is not None:
return cached_result
# Cache miss - scan all .py files
all_nodes = set()
all_metadata = {}
py_files = list(repo_path.rglob("*.py"))
# Filter out __pycache__, .git, and other hidden directories
filtered_files = []
for f in py_files:
try:
rel_path = f.relative_to(repo_path)
# Skip __pycache__, .git, and any directory starting with .
if '__pycache__' not in str(rel_path) and not any(part.startswith('.') for part in rel_path.parts):
filtered_files.append(f)
except:
continue
py_files = filtered_files
for py_file in py_files:
try:
# Read file with proper encoding
with open(py_file, 'r', encoding='utf-8', errors='ignore') as f:
code = f.read()
if code:
# Extract nodes using SAME logic as scan_in_file
# V1 nodes (enhanced with fallback patterns)
nodes = extract_nodes_enhanced(code, py_file, visited=set(), verbose=verbose)
all_nodes.update(nodes)
# V3 nodes detection
v3_nodes = extract_v3_nodes(code)
all_nodes.update(v3_nodes)
# Dict parsing - exclude commented NODE_CLASS_MAPPINGS lines
pattern = r"_CLASS_MAPPINGS\s*(?::\s*\w+\s*)?=\s*(?:\\\s*)?{([^}]*)}"
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
for match_obj in regex.finditer(code):
# Get the line where NODE_CLASS_MAPPINGS is defined
match_start = match_obj.start()
line_start = code.rfind('\n', 0, match_start) + 1
line_end = code.find('\n', match_start)
if line_end == -1:
line_end = len(code)
line = code[line_start:line_end]
# Skip if line starts with # (commented)
if re.match(r'^\s*#', line):
continue
match = match_obj.group(1)
# Filter out commented lines from dict content
match_lines = match.split('\n')
match_filtered = '\n'.join(
line for line in match_lines
if not re.match(r'^\s*#', line)
)
# Extract key-value pairs with double quotes
key_value_pairs = re.findall(r"\"([^\"]*)\"\s*:\s*([^,\n]*)", match_filtered)
for key, value in key_value_pairs:
all_nodes.add(key.strip())
# Extract key-value pairs with single quotes
key_value_pairs = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", match_filtered)
for key, value in key_value_pairs:
all_nodes.add(key.strip())
# Handle .update() pattern (AFTER comment removal)
code_cleaned = re.sub(r'^#.*?$', '', code, flags=re.MULTILINE)
update_pattern = r"_CLASS_MAPPINGS\.update\s*\(\s*{([^}]*)}\s*\)"
update_match = re.search(update_pattern, code_cleaned, re.DOTALL)
if update_match:
update_dict_text = update_match.group(1)
# Extract key-value pairs (double quotes)
update_pairs = re.findall(r'"([^"]*)"\s*:\s*([^,\n]*)', update_dict_text)
for key, value in update_pairs:
all_nodes.add(key.strip())
# Extract key-value pairs (single quotes)
update_pairs_single = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", update_dict_text)
for key, value in update_pairs_single:
all_nodes.add(key.strip())
# Additional regex patterns (AFTER comment removal)
patterns = [
r'^[^=]*_CLASS_MAPPINGS\["(.*?)"\]',
r'^[^=]*_CLASS_MAPPINGS\[\'(.*?)\'\]',
r'@register_node\("(.+)",\s*\".+"\)',
r'"(\w+)"\s*:\s*{"class":\s*\w+\s*'
]
for pattern in patterns:
keys = re.findall(pattern, code_cleaned)
all_nodes.update(key.strip() for key in keys)
# Extract metadata from this file
metadata = extract_metadata_only(str(py_file))
all_metadata.update(metadata)
except Exception:
# Silently skip files that can't be read
continue
# Save to per-repo cache
_save_per_repo_cache(repo_path, all_nodes, all_metadata)
return (all_nodes, all_metadata)
def _verify_class_exists(node_name: str, code_text: str, file_path: Optional[Path] = None) -> tuple[bool, Optional[str], Optional[int]]:
"""
Verify that a node class exists and has ComfyUI node structure.
Returns: (exists: bool, file_path: str, line_number: int)
A valid ComfyUI node must have:
- Class definition (not commented)
- At least one of: INPUT_TYPES, RETURN_TYPES, FUNCTION method/attribute
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
tree = ast.parse(code_text)
except:
return (False, None, None)
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if node.name == node_name or node.name.replace('_', '') == node_name.replace('_', ''):
# Found class definition - check if it has ComfyUI interface
has_input_types = False
has_return_types = False
has_function = False
for item in node.body:
# Check for INPUT_TYPES method
if isinstance(item, ast.FunctionDef) and item.name == 'INPUT_TYPES':
has_input_types = True
# Check for RETURN_TYPES attribute
elif isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name):
if target.id == 'RETURN_TYPES':
has_return_types = True
elif target.id == 'FUNCTION':
has_function = True
# Check for FUNCTION method
elif isinstance(item, ast.FunctionDef):
has_function = True
# Valid if has any ComfyUI signature
if has_input_types or has_return_types or has_function:
file_str = str(file_path) if file_path else None
return (True, file_str, node.lineno)
return (False, None, None)
def _extract_display_name_mappings(code_text: str) -> Set[str]:
"""
Extract node names from NODE_DISPLAY_NAME_MAPPINGS.
Pattern:
NODE_DISPLAY_NAME_MAPPINGS = {
"node_key": "Display Name",
...
}
Returns:
Set of node keys from NODE_DISPLAY_NAME_MAPPINGS
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
tree = ast.parse(code_text)
except:
return set()
nodes = set()
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_DISPLAY_NAME_MAPPINGS':
if isinstance(node.value, ast.Dict):
for key in node.value.keys:
if isinstance(key, ast.Constant) and isinstance(key.value, str):
nodes.add(key.value.strip())
return nodes
def extract_nodes_enhanced(
code_text: str,
file_path: Optional[Path] = None,
visited: Optional[Set[Path]] = None,
verbose: bool = False
) -> Set[str]:
"""
Enhanced node extraction with multi-layer detection system.
Scanner 2.0.11 - Comprehensive detection strategy:
- Phase 1: NODE_CLASS_MAPPINGS dict literal
- Phase 2: Class.NAME attribute access (e.g., FreeChat.NAME)
- Phase 3: Item assignment (NODE_CLASS_MAPPINGS["key"] = value)
- Phase 4: Class existence verification (detects active classes even if registration commented)
- Phase 5: NODE_DISPLAY_NAME_MAPPINGS cross-reference
- Phase 6: Empty dict detection (UI-only extensions, logging only)
Fixed Bugs:
- Scanner 2.0.9: Fallback cascade prevented Phase 3 execution
- Scanner 2.0.10: Missed active classes with commented registrations (15 false negatives)
Args:
code_text: Python source code
file_path: Path to file (for logging and caching)
visited: Visited paths (for circular import prevention)
verbose: If True, print UI-only extension detection messages
Returns:
Set of node names (union of all detected patterns)
"""
# Check file-based cache if file_path provided
if file_path is not None:
try:
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
if file_path_obj.exists():
current_mtime = file_path_obj.stat().st_mtime
# Check if we have cached result with matching mtime and scanner version
if file_path_obj in _file_mtime_cache:
cached_mtime = _file_mtime_cache[file_path_obj]
cache_key = (str(file_path_obj), cached_mtime, SCANNER_VERSION)
if current_mtime == cached_mtime and cache_key in _extract_nodes_enhanced_cache:
return _extract_nodes_enhanced_cache[cache_key].copy()
except:
pass # Ignore cache errors, proceed with normal execution
# Suppress warnings from AST parsing
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning)
# Phase 1: Original extract_nodes() - dict literal
phase1_nodes = extract_nodes(code_text)
# Phase 2: Class.NAME pattern
if visited is None:
visited = set()
phase2_nodes = _fallback_classname_resolver(code_text, file_path)
# Phase 3: Item assignment pattern
phase3_nodes = _fallback_item_assignment(code_text)
# Phase 4: NODE_DISPLAY_NAME_MAPPINGS cross-reference (NEW in 2.0.11)
# This catches nodes that are in display names but not in NODE_CLASS_MAPPINGS
phase4_nodes = _extract_display_name_mappings(code_text)
# Phase 5: Class existence verification ONLY for display name candidates (NEW in 2.0.11)
# This phase is CONSERVATIVE - only verify classes that appear in display names
# This catches the specific Scanner 2.0.10 bug pattern:
# - NODE_CLASS_MAPPINGS registration is commented
# - NODE_DISPLAY_NAME_MAPPINGS still has the entry
# - Class implementation exists
# Example: Bjornulf_ollamaLoader in Bjornulf_custom_nodes
phase5_nodes = set()
for node_name in phase4_nodes:
# Only check classes that appear in display names but not in registrations
if node_name not in (phase1_nodes | phase2_nodes | phase3_nodes):
exists, _, _ = _verify_class_exists(node_name, code_text, file_path)
if exists:
phase5_nodes.add(node_name)
# Phase 6: Dict comprehension pattern (NEW in 2.0.12)
# Detects: NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
# Example: TobiasGlaubach/ComfyUI-TG_PyCode
phase6_nodes = _fallback_dict_comprehension(code_text, file_path)
# Phase 7: Import-based class names for dict comprehension (NEW in 2.0.12)
# Detects imported classes that are added to export lists
phase7_nodes = _fallback_import_class_names(code_text, file_path)
# Union all results (FIX: Scanner 2.0.9 bug + Scanner 2.0.10 bug + Scanner 2.0.12 dict comp)
# 2.0.9: Used early return which missed Phase 3 nodes
# 2.0.10: Only checked registrations, missed classes referenced in display names
# 2.0.12: Added dict comprehension and import-based class detection
all_nodes = phase1_nodes | phase2_nodes | phase3_nodes | phase4_nodes | phase5_nodes | phase6_nodes | phase7_nodes
# Phase 8: Empty dict detector (logging only, doesn't add nodes)
if not all_nodes:
_fallback_empty_dict_detector(code_text, file_path, verbose)
# Cache the result
if file_path is not None:
try:
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
if file_path_obj.exists():
current_mtime = file_path_obj.stat().st_mtime
cache_key = (str(file_path_obj), current_mtime, SCANNER_VERSION)
_extract_nodes_enhanced_cache[cache_key] = all_nodes
_file_mtime_cache[file_path_obj] = current_mtime
except:
pass
return all_nodes
def _fallback_classname_resolver(code_text: str, file_path: Optional[Path]) -> Set[str]:
"""
Detect Class.NAME pattern in NODE_CLASS_MAPPINGS.
Pattern:
NODE_CLASS_MAPPINGS = {
FreeChat.NAME: FreeChat,
PaidChat.NAME: PaidChat
}
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
for node in parsed.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
if isinstance(node.value, ast.Dict):
for key in node.value.keys:
# Detect Class.NAME pattern
if isinstance(key, ast.Attribute):
if isinstance(key.value, ast.Name):
# Use class name as node name
nodes.add(key.value.id)
# Also handle literal strings
elif isinstance(key, ast.Constant) and isinstance(key.value, str):
nodes.add(key.value.strip())
return nodes
def _fallback_item_assignment(code_text: str) -> Set[str]:
"""
Detect item assignment pattern.
Pattern:
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS["MyNode"] = MyNode
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Subscript):
if (isinstance(target.value, ast.Name) and
target.value.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']):
# Extract key
if isinstance(target.slice, ast.Constant):
if isinstance(target.slice.value, str):
nodes.add(target.slice.value)
return nodes
def _fallback_dict_comprehension(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
"""
Detect dict comprehension pattern with __name__ attribute access.
Pattern:
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
NODE_CLASS_MAPPINGS = {c.__name__: c for c in [ClassA, ClassB]}
This function detects dict comprehension assignments to NODE_CLASS_MAPPINGS
and extracts class names from the iterable (list literal or variable reference).
Returns:
Set of class names extracted from the dict comprehension
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
export_lists = {} # Track list variables and their contents
# First pass: collect list assignments (to_export = [...], exports = [...])
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
var_name = target.id
# Check for list literal
if isinstance(node.value, ast.List):
class_names = set()
for elt in node.value.elts:
if isinstance(elt, ast.Name):
class_names.add(elt.id)
export_lists[var_name] = class_names
# Handle augmented assignment: to_export += [...]
elif isinstance(node, ast.AugAssign):
if isinstance(node.target, ast.Name) and isinstance(node.op, ast.Add):
var_name = node.target.id
if isinstance(node.value, ast.List):
class_names = set()
for elt in node.value.elts:
if isinstance(elt, ast.Name):
class_names.add(elt.id)
if var_name in export_lists:
export_lists[var_name].update(class_names)
else:
export_lists[var_name] = class_names
# Second pass: find NODE_CLASS_MAPPINGS dict comprehension
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']:
# Check for dict comprehension
if isinstance(node.value, ast.DictComp):
dictcomp = node.value
# Check if key is cls.__name__ pattern
key = dictcomp.key
if isinstance(key, ast.Attribute) and key.attr == '__name__':
# Get the iterable from the first generator
for generator in dictcomp.generators:
iter_node = generator.iter
# Case 1: Inline list [ClassA, ClassB, ...]
if isinstance(iter_node, ast.List):
for elt in iter_node.elts:
if isinstance(elt, ast.Name):
nodes.add(elt.id)
# Case 2: Variable reference (to_export, exports, etc.)
elif isinstance(iter_node, ast.Name):
var_name = iter_node.id
if var_name in export_lists:
nodes.update(export_lists[var_name])
return nodes
def _fallback_import_class_names(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
"""
Extract class names from imports that are added to export lists.
Pattern:
from .module import ClassA, ClassB
to_export = [ClassA, ClassB]
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
This is a complementary fallback that works with _fallback_dict_comprehension
to resolve import-based node registrations.
Returns:
Set of imported class names that appear in export-like contexts
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
# Collect imported names
imported_names = set()
for node in ast.walk(parsed):
if isinstance(node, ast.ImportFrom):
for alias in node.names:
name = alias.asname if alias.asname else alias.name
imported_names.add(name)
# Check if these names appear in list assignments that feed into NODE_CLASS_MAPPINGS
export_candidates = set()
has_dict_comp_mapping = False
for node in ast.walk(parsed):
# Check for dict comprehension NODE_CLASS_MAPPINGS
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
if isinstance(node.value, ast.DictComp):
has_dict_comp_mapping = True
# Collect list contents
if isinstance(node, ast.Assign):
if isinstance(node.value, ast.List):
for elt in node.value.elts:
if isinstance(elt, ast.Name) and elt.id in imported_names:
export_candidates.add(elt.id)
# Handle augmented assignment
elif isinstance(node, ast.AugAssign):
if isinstance(node.value, ast.List):
for elt in node.value.elts:
if isinstance(elt, ast.Name) and elt.id in imported_names:
export_candidates.add(elt.id)
# Only return if there's a dict comprehension mapping
if has_dict_comp_mapping:
return export_candidates
return set()
def _extract_repo_name(file_path: Path) -> str:
"""
Extract repository name from file path.
Path structure: /home/rho/.tmp/analysis/temp/{author}_{reponame}/{path/to/file.py}
Returns: {author}_{reponame} or filename if extraction fails
"""
try:
parts = file_path.parts
# Find 'temp' directory in path
if 'temp' in parts:
temp_idx = parts.index('temp')
if temp_idx + 1 < len(parts):
# Next part after 'temp' is the repo directory
return parts[temp_idx + 1]
except (ValueError, IndexError):
pass
# Fallback to filename if extraction fails
return file_path.name if hasattr(file_path, 'name') else str(file_path)
def _fallback_empty_dict_detector(code_text: str, file_path: Optional[Path], verbose: bool = False) -> None:
"""
Detect empty NODE_CLASS_MAPPINGS (UI-only extensions).
Logs for documentation purposes only (when verbose=True).
Args:
code_text: Python source code to analyze
file_path: Path to the file being analyzed
verbose: If True, print detection messages
"""
empty_patterns = [
'NODE_CLASS_MAPPINGS = {}',
'NODE_CLASS_MAPPINGS={}',
]
code_normalized = code_text.replace(' ', '').replace('\n', '')
for pattern in empty_patterns:
pattern_normalized = pattern.replace(' ', '')
if pattern_normalized in code_normalized:
if file_path and verbose:
repo_name = _extract_repo_name(file_path)
print(f"Info: UI-only extension (empty NODE_CLASS_MAPPINGS): {repo_name}")
return
def has_comfy_node_base(class_node):
"""Check if class inherits from io.ComfyNode or ComfyNode"""
@ -229,6 +941,25 @@ def extract_v3_nodes(code_text):
# scan
def extract_metadata_only(filename):
"""Extract only metadata (@author, @title, etc) without node scanning"""
try:
with open(filename, encoding='utf-8', errors='ignore') as file:
code = file.read()
metadata = {}
lines = code.strip().split('\n')
for line in lines:
if line.startswith('@'):
if line.startswith("@author:") or line.startswith("@title:") or line.startswith("@nickname:") or line.startswith("@description:"):
key, value = line[1:].strip().split(':', 1)
metadata[key.strip()] = value.strip()
return metadata
except:
return {}
def scan_in_file(filename, is_builtin=False):
global builtin_nodes
@ -242,8 +973,8 @@ def scan_in_file(filename, is_builtin=False):
nodes = set()
class_dict = {}
# V1 nodes detection
nodes |= extract_nodes(code)
# V1 nodes detection (enhanced with fallback patterns)
nodes |= extract_nodes_enhanced(code, file_path=Path(filename), visited=set())
# V3 nodes detection
nodes |= extract_v3_nodes(code)
@ -620,13 +1351,14 @@ def update_custom_nodes(scan_only_mode=False, url_list_file=None):
return node_info
def gen_json(node_info, scan_only_mode=False):
def gen_json(node_info, scan_only_mode=False, force_rescan=False):
"""
Generate extension-node-map.json from scanned node information
Args:
node_info (dict): Repository metadata mapping
scan_only_mode (bool): If True, exclude metadata from output
force_rescan (bool): If True, ignore cache and force rescan all nodes
"""
# scan from .py file
node_files, node_dirs = get_nodes(temp_dir)
@ -642,13 +1374,17 @@ def gen_json(node_info, scan_only_mode=False):
py_files = get_py_file_paths(dirname)
metadata = {}
nodes = set()
for py in py_files:
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
nodes.update(nodes_in_file)
# Include metadata from .py files in both modes
metadata.update(metadata_in_file)
# Use per-repo cache for node AND metadata extraction
try:
nodes, metadata = extract_nodes_from_repo(Path(dirname), verbose=False, force_rescan=force_rescan)
except:
# Fallback to file-by-file scanning if extract_nodes_from_repo fails
nodes = set()
for py in py_files:
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
nodes.update(nodes_in_file)
metadata.update(metadata_in_file)
dirname = os.path.basename(dirname)
if 'Jovimetrix' in dirname:
@ -810,11 +1546,14 @@ if __name__ == "__main__":
print("\n# Generating 'extension-node-map.json'...\n")
# Generate extension-node-map.json
gen_json(updated_node_info, scan_only_mode)
force_rescan = args.force_rescan if hasattr(args, 'force_rescan') else False
if force_rescan:
print("⚠️ Force rescan enabled - ignoring all cached results\n")
gen_json(updated_node_info, scan_only_mode, force_rescan)
print("\n✅ DONE.\n")
if scan_only_mode:
print("Output: extension-node-map.json (node mappings only)")
else:
print("Output: extension-node-map.json (full metadata)")
print("Output: extension-node-map.json (full metadata)")