Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7d16f382 |
@@ -458,51 +458,6 @@ Changes take effect after a **restart** (no hot reload).
|
||||
> outdated ComfyUI deployments.
|
||||
|
||||
|
||||
### Flagged CNR versions: `allow_flagged_nodepack_install`
|
||||
|
||||
Manager checks the version status in the existing CNR install response; no
|
||||
additional Registry request is needed. Active versions continue to install
|
||||
normally, subject to the existing security policy. Registry refusals (including
|
||||
HTTP 404) still prevent installation.
|
||||
|
||||
Flagged versions must also satisfy the existing security policy and Registry
|
||||
checks. In addition, they require either **all** `--listen` addresses to be
|
||||
loopback, such as `127.0.0.1` or `::1`, or the explicit override below.
|
||||
A mixed loopback/non-loopback listener is non-local.
|
||||
|
||||
For a **trusted private network**, administrators can explicitly allow flagged
|
||||
versions on non-loopback listeners:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
allow_flagged_nodepack_install = true
|
||||
```
|
||||
|
||||
The default is `false`; only the case-insensitive value `true` enables it.
|
||||
Restart ComfyUI after editing the setting. This option does not detect private
|
||||
networks and is independent of `network_mode`. It does not bypass
|
||||
`security_level`, `allow_git_url_install`, or `allow_pip_install`.
|
||||
|
||||
When the flagged-version check denies an action, the UI reports that the current
|
||||
security configuration does not allow it and directs users to the terminal.
|
||||
Only the terminal provides the loopback-only `--listen` examples, the private
|
||||
network override setting, and the instruction to restart ComfyUI after changes.
|
||||
|
||||
Enabling an already installed version does not query the Registry or reinstall it.
|
||||
Older deferred switches without a stored status require loopback-only listeners
|
||||
or the override. Otherwise they are rejected without changing the installed pack;
|
||||
request the installation again so Manager can check its current Registry status.
|
||||
|
||||
The check applies to server-initiated installation, reinstallation, version switches, updates,
|
||||
and snapshot restores. A policy refusal preserves the existing nodepack.
|
||||
Scheduled version switches save the returned status and check the current
|
||||
listener and setting again at startup, before downloading or running scripts.
|
||||
Server-scheduled snapshot restores inherit the permission computed from the
|
||||
server's current listener and setting. Standalone `cm-cli` commands remain local
|
||||
administrator operations and do not use the server listener policy.
|
||||
Saved statuses are not refreshed from the Registry.
|
||||
|
||||
|
||||
# Disclaimer
|
||||
|
||||
* This extension simply provides the convenience of installing custom nodes and does not guarantee their proper functioning.
|
||||
|
||||
+453
-1367
File diff suppressed because it is too large
Load Diff
+780
-2336
File diff suppressed because it is too large
Load Diff
+8586
-8761
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -129,7 +129,6 @@ class NodeVersion:
|
||||
id: str
|
||||
version: str
|
||||
download_url: str
|
||||
status: str = ''
|
||||
|
||||
|
||||
def map_node_version(api_node_version):
|
||||
@@ -161,7 +160,6 @@ def map_node_version(api_node_version):
|
||||
download_url=api_node_version.get(
|
||||
"downloadUrl", ""
|
||||
), # Provide a default value if 'downloadUrl' is missing
|
||||
status=api_node_version.get('status', ''),
|
||||
)
|
||||
|
||||
|
||||
@@ -183,6 +181,7 @@ def install_node(node_id, version=None):
|
||||
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
# Convert the API response to a NodeVersion object
|
||||
return map_node_version(response.json())
|
||||
else:
|
||||
return None
|
||||
|
||||
+70
-140
@@ -44,7 +44,7 @@ import manager_migration
|
||||
from node_package import InstalledNodePackage
|
||||
|
||||
|
||||
version_code = [3, 42]
|
||||
version_code = [3, 41]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
|
||||
|
||||
@@ -953,10 +953,10 @@ class UnifiedManager:
|
||||
|
||||
return res
|
||||
|
||||
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps, status=''):
|
||||
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps):
|
||||
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
|
||||
with open(script_path, "a") as file:
|
||||
obj = [target, "#LAZY-CNR-SWITCH-SCRIPT", zip_url, from_path, to_path, no_deps, get_default_custom_nodes_path(), sys.executable, status]
|
||||
obj = [target, "#LAZY-CNR-SWITCH-SCRIPT", zip_url, from_path, to_path, no_deps, get_default_custom_nodes_path(), sys.executable]
|
||||
file.write(f"{obj}\n")
|
||||
|
||||
print(f"Installation reserved: {target}")
|
||||
@@ -982,45 +982,35 @@ class UnifiedManager:
|
||||
|
||||
return result
|
||||
|
||||
def _get_cnr_install_info(self, node_id, version_spec):
|
||||
node_info = cnr_utils.install_node(node_id, version_spec)
|
||||
if node_info is not None and node_info.status == 'NodeVersionStatusFlagged' and not manager_funcs.is_flagged_install_allowed():
|
||||
logging.error(manager_util.FLAGGED_NODEPACK_INSTALL_GUIDANCE)
|
||||
raise PermissionError(manager_util.FLAGGED_NODEPACK_INSTALL_ERROR)
|
||||
return node_info
|
||||
|
||||
def cnr_switch_version(self, node_id, version_spec=None, instant_execution=False, no_deps=False, return_postinstall=False):
|
||||
if instant_execution:
|
||||
return self.cnr_switch_version_instant(node_id, version_spec, instant_execution, no_deps, return_postinstall)
|
||||
else:
|
||||
return self.cnr_switch_version_lazy(node_id, version_spec, no_deps, return_postinstall)
|
||||
|
||||
def cnr_switch_version_lazy(self, node_id, version_spec=None, no_deps=False, return_postinstall=False):
|
||||
"""
|
||||
switch between cnr version (lazy mode)
|
||||
"""
|
||||
|
||||
result = ManagedResult('switch-cnr')
|
||||
|
||||
try:
|
||||
node_info = self._get_cnr_install_info(node_id, version_spec)
|
||||
except PermissionError as exc:
|
||||
return result.fail(str(exc))
|
||||
node_info = cnr_utils.install_node(node_id, version_spec)
|
||||
if node_info is None or not node_info.download_url:
|
||||
return result.fail(f'not available node: {node_id}@{version_spec}')
|
||||
|
||||
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
|
||||
version_spec = node_info.version
|
||||
|
||||
def _cnr_switch_version(self, node_id, node_info, instant_execution=False, no_deps=False, return_postinstall=False):
|
||||
"""Execute a switch using the checked CNR response."""
|
||||
if self.active_nodes[node_id][0] == node_info.version:
|
||||
if self.active_nodes[node_id][0] == version_spec:
|
||||
return ManagedResult('skip').with_msg("Up to date")
|
||||
|
||||
if instant_execution:
|
||||
return self._cnr_switch_version_instant(node_id, node_info, no_deps, return_postinstall)
|
||||
return self._cnr_switch_version_lazy(node_id, node_info, no_deps, return_postinstall)
|
||||
|
||||
def _cnr_switch_version_lazy(self, node_id, node_info, no_deps=False, return_postinstall=False):
|
||||
"""Reserve a switch using the validated install response."""
|
||||
result = ManagedResult('switch-cnr')
|
||||
version_spec = node_info.version
|
||||
zip_url = node_info.download_url
|
||||
from_path = self.active_nodes[node_id][1]
|
||||
target = node_id
|
||||
to_path = os.path.join(get_default_custom_nodes_path(), target)
|
||||
|
||||
def postinstall():
|
||||
return self.reserve_cnr_switch(target, zip_url, from_path, to_path, no_deps, node_info.status)
|
||||
return self.reserve_cnr_switch(target, zip_url, from_path, to_path, no_deps)
|
||||
|
||||
if return_postinstall:
|
||||
return result.with_postinstall(postinstall)
|
||||
@@ -1030,12 +1020,23 @@ class UnifiedManager:
|
||||
|
||||
return result
|
||||
|
||||
def _cnr_switch_version_instant(self, node_id, node_info, no_deps=False, return_postinstall=False):
|
||||
"""Apply a switch using the validated install response."""
|
||||
result = ManagedResult('switch-cnr')
|
||||
version_spec = node_info.version
|
||||
def cnr_switch_version_instant(self, node_id, version_spec=None, instant_execution=True, no_deps=False, return_postinstall=False):
|
||||
"""
|
||||
switch between cnr version
|
||||
"""
|
||||
|
||||
# 1. download
|
||||
result = ManagedResult('switch-cnr')
|
||||
|
||||
node_info = cnr_utils.install_node(node_id, version_spec)
|
||||
if node_info is None or not node_info.download_url:
|
||||
return result.fail(f'not available node: {node_id}@{version_spec}')
|
||||
|
||||
version_spec = node_info.version
|
||||
|
||||
if self.active_nodes[node_id][0] == version_spec:
|
||||
return ManagedResult('skip').with_msg("Up to date")
|
||||
|
||||
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
|
||||
download_path = os.path.join(get_default_custom_nodes_path(), archive_name)
|
||||
manager_downloader.basic_download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name)
|
||||
@@ -1080,7 +1081,7 @@ class UnifiedManager:
|
||||
result.target = version_spec
|
||||
|
||||
def postinstall():
|
||||
res = self.execute_install_script(f"{node_id}@{version_spec}", install_path, instant_execution=True, no_deps=no_deps)
|
||||
res = self.execute_install_script(f"{node_id}@{version_spec}", install_path, instant_execution=instant_execution, no_deps=no_deps)
|
||||
return res
|
||||
|
||||
if return_postinstall:
|
||||
@@ -1297,24 +1298,10 @@ class UnifiedManager:
|
||||
if 'comfyui-manager' in node_id.lower():
|
||||
return result.fail(f"ignored: installing '{node_id}'")
|
||||
|
||||
try:
|
||||
node_info = self._get_cnr_install_info(node_id, version_spec)
|
||||
except PermissionError as exc:
|
||||
return result.fail(str(exc))
|
||||
node_info = cnr_utils.install_node(node_id, version_spec)
|
||||
if node_info is None or not node_info.download_url:
|
||||
return result.fail(f'not available node: {node_id}@{version_spec}')
|
||||
|
||||
result = self._cnr_install(node_id, node_info, instant_execution, no_deps, return_postinstall)
|
||||
# Keep the requested version in the public result.
|
||||
if result.target is not None:
|
||||
result.target = version_spec
|
||||
return result
|
||||
|
||||
def _cnr_install(self, node_id, node_info, instant_execution=False, no_deps=False, return_postinstall=False):
|
||||
"""Install using the checked CNR response."""
|
||||
result = ManagedResult('install-cnr')
|
||||
version_spec = node_info.version
|
||||
|
||||
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
|
||||
download_path = os.path.join(get_default_custom_nodes_path(), archive_name)
|
||||
|
||||
@@ -1503,13 +1490,21 @@ class UnifiedManager:
|
||||
else:
|
||||
version_spec = self.resolve_unspecified_version(node_id)
|
||||
|
||||
if version_spec in ('unknown', 'nightly'):
|
||||
if version_spec == 'unknown' or version_spec == 'nightly':
|
||||
try:
|
||||
repo_url = await self._get_git_install_url(node_id, version_spec, channel, mode)
|
||||
except InvalidChannel as exc:
|
||||
return ManagedResult('fail').fail(f'Invalid channel is used: {exc.channel}')
|
||||
except LookupError as exc:
|
||||
return ManagedResult('install').fail(str(exc))
|
||||
custom_nodes = await self.get_custom_nodes(channel, mode)
|
||||
except InvalidChannel as e:
|
||||
return ManagedResult('fail').fail(f'Invalid channel is used: {e.channel}')
|
||||
|
||||
the_node = custom_nodes.get(node_id)
|
||||
if the_node is not None:
|
||||
if version_spec == 'unknown':
|
||||
repo_url = the_node['files'][0]
|
||||
else: # nightly
|
||||
repo_url = the_node['repository']
|
||||
else:
|
||||
result = ManagedResult('install')
|
||||
return result.fail(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
|
||||
|
||||
if self.is_enabled(node_id, version_spec):
|
||||
return ManagedResult('skip').with_target(f"{node_id}@{version_spec}")
|
||||
@@ -1517,17 +1512,24 @@ class UnifiedManager:
|
||||
elif self.is_disabled(node_id, version_spec):
|
||||
return self.unified_enable(node_id, version_spec)
|
||||
|
||||
elif version_spec in ('unknown', 'nightly'):
|
||||
return self._install_git(node_id, version_spec, repo_url, instant_execution, no_deps, return_postinstall)
|
||||
elif version_spec == 'unknown' or version_spec == 'nightly':
|
||||
if version_spec == 'nightly':
|
||||
# disable cnr nodes
|
||||
if self.is_enabled(node_id, 'cnr'):
|
||||
self.unified_disable(node_id, False)
|
||||
|
||||
# Check before changing the existing pack, then reuse this response.
|
||||
try:
|
||||
node_info = self._get_cnr_install_info(node_id, version_spec)
|
||||
except PermissionError as exc:
|
||||
return ManagedResult('install-cnr').fail(str(exc))
|
||||
if node_info is None or not node_info.download_url:
|
||||
return ManagedResult('install-cnr').fail(f'not available node: {node_id}@{version_spec}')
|
||||
version_spec = node_info.version
|
||||
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
|
||||
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
if res.result:
|
||||
if version_spec == 'unknown':
|
||||
self.unknown_active_nodes[node_id] = repo_url, to_path
|
||||
elif version_spec == 'nightly':
|
||||
cnr_utils.generate_cnr_id(to_path, node_id)
|
||||
self.active_nodes[node_id] = 'nightly', to_path
|
||||
else:
|
||||
return res
|
||||
|
||||
return res.with_target(version_spec)
|
||||
|
||||
if self.is_enabled(node_id, 'nightly'):
|
||||
# disable nightly nodes
|
||||
@@ -1540,77 +1542,17 @@ class UnifiedManager:
|
||||
if self.is_disabled(node_id, "cnr"):
|
||||
# enable and switch version if cnr is disabled (not specified version)
|
||||
self.unified_enable(node_id, "cnr")
|
||||
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
|
||||
return self.cnr_switch_version(node_id, version_spec, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
|
||||
if self.is_enabled(node_id, "cnr"):
|
||||
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
|
||||
return self.cnr_switch_version(node_id, version_spec, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
|
||||
res = self._cnr_install(node_id, node_info, instant_execution, no_deps, return_postinstall)
|
||||
res = self.cnr_install(node_id, version_spec, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
if res.result:
|
||||
self.active_nodes[node_id] = version_spec, res.to_path
|
||||
|
||||
return res
|
||||
|
||||
async def _get_git_install_url(self, node_id, version_spec, channel, mode):
|
||||
custom_nodes = await self.get_custom_nodes(channel, mode)
|
||||
node = custom_nodes.get(node_id)
|
||||
if node is None:
|
||||
raise LookupError(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
|
||||
return node['files'][0] if version_spec == 'unknown' else node['repository']
|
||||
|
||||
def _install_git(self, node_id, version_spec, repo_url, instant_execution=False, no_deps=False, return_postinstall=False):
|
||||
if version_spec == 'nightly':
|
||||
# disable cnr nodes
|
||||
if self.is_enabled(node_id, 'cnr'):
|
||||
self.unified_disable(node_id, False)
|
||||
|
||||
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
|
||||
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
if res.result:
|
||||
if version_spec == 'unknown':
|
||||
self.unknown_active_nodes[node_id] = repo_url, to_path
|
||||
elif version_spec == 'nightly':
|
||||
cnr_utils.generate_cnr_id(to_path, node_id)
|
||||
self.active_nodes[node_id] = 'nightly', to_path
|
||||
else:
|
||||
return res
|
||||
|
||||
return res.with_target(version_spec)
|
||||
|
||||
async def reinstall_by_id(self, node_id, version_spec, channel=None, mode=None):
|
||||
if 'comfyui-manager' in node_id.lower():
|
||||
return ManagedResult('skip').fail(f"ignored: installing '{node_id}'")
|
||||
|
||||
if version_spec in ('unknown', 'nightly'):
|
||||
try:
|
||||
repo_url = await self._get_git_install_url(node_id, version_spec, channel, mode)
|
||||
except InvalidChannel as exc:
|
||||
return ManagedResult('fail').fail(f'Invalid channel is used: {exc.channel}')
|
||||
except LookupError as exc:
|
||||
return ManagedResult('install').fail(str(exc))
|
||||
else:
|
||||
try:
|
||||
node_info = self._get_cnr_install_info(node_id, version_spec)
|
||||
except PermissionError as exc:
|
||||
return ManagedResult('install-cnr').fail(str(exc))
|
||||
if node_info is None or not node_info.download_url:
|
||||
return ManagedResult('install-cnr').fail(f'not available node: {node_id}@{version_spec}')
|
||||
|
||||
removed = self.unified_uninstall(node_id, version_spec == 'unknown')
|
||||
if not removed.result:
|
||||
return removed
|
||||
|
||||
for item in removed.items:
|
||||
path = item if version_spec == 'unknown' else item[1]
|
||||
self.processed_install.discard(os.path.join(path, 'install.py'))
|
||||
if version_spec in ('unknown', 'nightly'):
|
||||
return self._install_git(node_id, version_spec, repo_url)
|
||||
|
||||
result = self._cnr_install(node_id, node_info)
|
||||
if result.result:
|
||||
self.active_nodes[node_id] = node_info.version, result.to_path
|
||||
return result
|
||||
|
||||
|
||||
unified_manager = UnifiedManager()
|
||||
|
||||
@@ -1721,10 +1663,6 @@ class ManagerFuncs:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def is_flagged_install_allowed(self):
|
||||
# Server-scheduled restore children inherit only the resolved permission.
|
||||
return os.environ.get('_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED', 'true') == 'true'
|
||||
|
||||
def get_current_preview_method(self):
|
||||
return "none"
|
||||
|
||||
@@ -1765,7 +1703,6 @@ WRITTEN_CONFIG_KEYS = (
|
||||
'db_mode',
|
||||
'allow_git_url_install',
|
||||
'allow_pip_install',
|
||||
'allow_flagged_nodepack_install',
|
||||
)
|
||||
|
||||
|
||||
@@ -1940,7 +1877,6 @@ def read_config():
|
||||
'db_mode': default_conf.get('db_mode', 'cache').lower(),
|
||||
'allow_git_url_install': get_bool('allow_git_url_install', False),
|
||||
'allow_pip_install': get_bool('allow_pip_install', False),
|
||||
'allow_flagged_nodepack_install': get_bool('allow_flagged_nodepack_install', False),
|
||||
}
|
||||
manager_migration.force_security_level_if_needed(result)
|
||||
return result
|
||||
@@ -1972,7 +1908,6 @@ def read_config():
|
||||
'db_mode': 'cache', # local | cache | remote
|
||||
'allow_git_url_install': False,
|
||||
'allow_pip_install': False,
|
||||
'allow_flagged_nodepack_install': False,
|
||||
}
|
||||
manager_migration.force_security_level_if_needed(result)
|
||||
return result
|
||||
@@ -3380,20 +3315,15 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
skip_node_packs.append(f"{x[0]}@{x[1]}")
|
||||
elif not ps.result:
|
||||
failed.append(f"{x[0]}@{x[1]}")
|
||||
logging.error(ps.msg)
|
||||
|
||||
# Do not retry processed switches as fresh installs.
|
||||
checked_cnr = {node_id for node_id, _ in todo_checkout}
|
||||
# install listed cnr nodes
|
||||
for k, v in cnr_info.items():
|
||||
if 'comfyui-manager' in k or k in checked_cnr:
|
||||
if 'comfyui-manager' in k:
|
||||
continue
|
||||
|
||||
ps = await unified_manager.install_by_id(k, version_spec=v, instant_execution=True, return_postinstall=True)
|
||||
if ps.action == 'install-cnr' and ps.result:
|
||||
installed_node_packs.append(f"{k}@{v}")
|
||||
elif not ps.result:
|
||||
failed.append(f"{k}@{v}")
|
||||
logging.error(ps.msg)
|
||||
|
||||
if ps is not None and ps.result:
|
||||
if hasattr(ps, 'postinstall'):
|
||||
|
||||
+10
-19
@@ -162,9 +162,6 @@ async def get_risky_level(files, pip_packages):
|
||||
|
||||
|
||||
class ManagerFuncsInComfyUI(core.ManagerFuncs):
|
||||
def is_flagged_install_allowed(self):
|
||||
return core.get_config()['allow_flagged_nodepack_install'] or manager_util.is_loopback_listener(args.listen)
|
||||
|
||||
def get_current_preview_method(self):
|
||||
if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
|
||||
return "auto"
|
||||
@@ -520,7 +517,7 @@ async def task_worker():
|
||||
global model_result
|
||||
global tasks_in_progress
|
||||
|
||||
async def do_install(item, operation) -> str:
|
||||
async def do_install(item) -> str:
|
||||
ui_id, node_spec_str, channel, mode, skip_post_install = item
|
||||
|
||||
try:
|
||||
@@ -530,10 +527,7 @@ async def task_worker():
|
||||
return f"Cannot resolve install target: '{node_spec_str}'"
|
||||
|
||||
node_name, version_spec, is_specified = node_spec
|
||||
if operation == 'reinstall':
|
||||
res = await core.unified_manager.reinstall_by_id(node_name, version_spec, channel, mode)
|
||||
else:
|
||||
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
|
||||
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
|
||||
# discard post install if skip_post_install mode
|
||||
|
||||
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
|
||||
@@ -580,7 +574,7 @@ async def task_worker():
|
||||
base_res['msg'] = 'success'
|
||||
return base_res
|
||||
|
||||
base_res['msg'] = res.msg or f"An error occurred while updating '{node_name}'."
|
||||
base_res['msg'] = f"An error occurred while updating '{node_name}'."
|
||||
logging.error(f"\nERROR: An error occurred while updating '{node_name}'. (res.result={res.result}, res.action={res.action})")
|
||||
return base_res
|
||||
except Exception:
|
||||
@@ -733,8 +727,8 @@ async def task_worker():
|
||||
tasks_in_progress.add((kind, item[0]))
|
||||
|
||||
try:
|
||||
if kind in ('install', 'reinstall'):
|
||||
msg = await do_install(item, kind)
|
||||
if kind == 'install':
|
||||
msg = await do_install(item)
|
||||
elif kind == 'install-model':
|
||||
msg = await do_install_model(item)
|
||||
elif kind == 'update':
|
||||
@@ -1419,7 +1413,8 @@ async def import_fail_info(request):
|
||||
|
||||
@routes.post("/manager/queue/reinstall")
|
||||
async def reinstall_custom_node(request):
|
||||
return await _queue_node_install(request, "reinstall")
|
||||
await uninstall_custom_node(request)
|
||||
await install_custom_node(request)
|
||||
|
||||
|
||||
@routes.post("/manager/queue/reset")
|
||||
@@ -1449,20 +1444,16 @@ async def queue_count(request):
|
||||
|
||||
@routes.post("/manager/queue/install")
|
||||
async def install_custom_node(request):
|
||||
return await _queue_node_install(request, "install")
|
||||
|
||||
|
||||
async def _queue_node_install(request, operation):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
json_data = await request.json()
|
||||
|
||||
# CNR version status is checked by the worker before installation.
|
||||
# non-nightly cnr is safe
|
||||
risky_level = None
|
||||
cnr_id = json_data.get('id')
|
||||
skip_post_install = False if operation == 'reinstall' else json_data.get('skip_post_install')
|
||||
skip_post_install = json_data.get('skip_post_install')
|
||||
|
||||
git_url = None
|
||||
|
||||
@@ -1535,7 +1526,7 @@ async def _queue_node_install(request, operation):
|
||||
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install
|
||||
task_queue.put((operation, install_item))
|
||||
task_queue.put(("install", install_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
+1
-47
@@ -3,8 +3,6 @@ description:
|
||||
`manager_util` is the lightest module shared across the prestartup_script, main code, and cm-cli of ComfyUI-Manager.
|
||||
"""
|
||||
import traceback
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
import aiohttp
|
||||
import json
|
||||
@@ -30,50 +28,6 @@ cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also up
|
||||
use_uv = False
|
||||
bypass_ssl = False
|
||||
|
||||
FLAGGED_NODEPACK_INSTALL_ERROR = (
|
||||
'This action is not allowed by the current security configuration. '
|
||||
'See the terminal for details.'
|
||||
)
|
||||
FLAGGED_NODEPACK_INSTALL_GUIDANCE = (
|
||||
'Installation of this flagged CNR version is blocked. To satisfy the additional '
|
||||
'flagged-version requirement, choose one of the following:\n'
|
||||
'1. Run ComfyUI with loopback-only listeners, for example --listen 127.0.0.1 '
|
||||
'or --listen ::1. All configured listen addresses must be loopback.\n'
|
||||
' Do not use bare --listen or any non-loopback address, such as 0.0.0.0 or ::.\n'
|
||||
'2. For a trusted private network, set allow_flagged_nodepack_install = true '
|
||||
'in the [default] section of ComfyUI-Manager\'s config.ini.\n'
|
||||
'Restart ComfyUI after changing the listener or configuration. '
|
||||
'All other installation security checks still apply.'
|
||||
)
|
||||
|
||||
|
||||
def is_loopback_listener(listen_address: str) -> bool:
|
||||
"""All addresses bound by --listen must resolve exclusively to loopback."""
|
||||
if not isinstance(listen_address, str) or not listen_address:
|
||||
return False
|
||||
for address in listen_address.split(','):
|
||||
address = address.strip()
|
||||
if not address:
|
||||
return False
|
||||
try:
|
||||
if ipaddress.ip_address(address).is_loopback:
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
resolved = socket.getaddrinfo(address, None, type=socket.SOCK_STREAM)
|
||||
except OSError:
|
||||
return False
|
||||
if not resolved or any(not ipaddress.ip_address(item[4][0]).is_loopback for item in resolved):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_cnr_install_allowed(status: str, allow_flagged: bool, listen_address: str) -> bool:
|
||||
"""Only flagged versions require loopback or the private-network opt-in."""
|
||||
return status != 'NodeVersionStatusFlagged' or bool(allow_flagged) or is_loopback_listener(listen_address)
|
||||
|
||||
|
||||
def add_python_path_to_env():
|
||||
if platform.system() != "Windows":
|
||||
sep = ':'
|
||||
@@ -756,4 +710,4 @@ def restore_pip_snapshot(pips, options):
|
||||
if res != 0:
|
||||
failed.append(x)
|
||||
|
||||
print(f"Installation failed for pip packages: {failed}")
|
||||
print(f"Installation failed for pip packages: {failed}")
|
||||
+1
-23
@@ -6249,28 +6249,6 @@
|
||||
"filename": "Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
|
||||
"size": "2.54GB"
|
||||
},
|
||||
{
|
||||
"name": "Anime Eye Detector (YOLOv8n)",
|
||||
"type": "Ultralytics",
|
||||
"base": "yolov8",
|
||||
"save_path": "ultralytics/bbox",
|
||||
"description": "Anime eye detector (YOLOv8n). Detects eyes in anime-style character art for use with BboxDetectorSEGS/Detailer workflows.",
|
||||
"reference": "https://huggingface.co/killjoyelite/anime-eye-yolov8",
|
||||
"filename": "eye_yolov8n.pt",
|
||||
"url": "https://huggingface.co/killjoyelite/anime-eye-yolov8/resolve/6bceece59aa38f7fe9d3867f3e2931b11c7ff9a1/eye_yolov8n.pt",
|
||||
"size": "6.24MB"
|
||||
},
|
||||
{
|
||||
"name": "Anime Eye Detector (YOLOv8s)",
|
||||
"type": "Ultralytics",
|
||||
"base": "yolov8",
|
||||
"save_path": "ultralytics/bbox",
|
||||
"description": "Anime eye detector (YOLOv8s). Detects eyes in anime-style character art for use with BboxDetectorSEGS/Detailer workflows.",
|
||||
"reference": "https://huggingface.co/killjoyelite/anime-eye-yolov8",
|
||||
"filename": "eye_yolov8s.pt",
|
||||
"url": "https://huggingface.co/killjoyelite/anime-eye-yolov8/resolve/6bceece59aa38f7fe9d3867f3e2931b11c7ff9a1/eye_yolov8s.pt",
|
||||
"size": "22.5MB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,5 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "magicwang1111",
|
||||
"title": "ComfyUI-GPT-image (OpenAI/Relay) [NAME CONFLICT]",
|
||||
"id": "comfyui-gpt-image-openai-relay",
|
||||
"reference": "https://github.com/magicwang1111/ComfyUI-GPT-image",
|
||||
"files": [
|
||||
"https://github.com/magicwang1111/ComfyUI-GPT-image"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Generate and edit images in ComfyUI with OpenAI GPT Image models. Includes dedicated Generate and Edit nodes, multi-image reference input, size presets, and support for both the official OpenAI API and OpenAI-compatible relay endpoints."
|
||||
},
|
||||
{
|
||||
"author": "Yuan-ManX",
|
||||
"title": "ComfyUI-Bagel [NAME CONFLICT]",
|
||||
"reference": "https://github.com/Yuan-ManX/ComfyUI-Bagel",
|
||||
"files": [
|
||||
"https://github.com/Yuan-ManX/ComfyUI-Bagel"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI-Bagel is now available in ComfyUI, BAGEL is an open‑source multimodal foundation model with 7B active parameters (14B total) trained on large‑scale interleaved multimodal data. [w/Don't install together with neverbiasu/ComfyUI-BAGEL simultaneously.]"
|
||||
},
|
||||
{
|
||||
"author": "liming123332",
|
||||
"title": "ComfyUI-Ranbooru [NAME CONFLICT]",
|
||||
"reference": "https://github.com/liming123332/ComfyUI-Ranbooru",
|
||||
"files": [
|
||||
"https://github.com/liming123332/ComfyUI-Ranbooru"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom node that migrates WebUI Forge's Ranbooru plugin to ComfyUI, enabling searches across Booru sites to generate positive/negative prompts with tag cleaning, rating sorting, and caching support. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "katalist-ai",
|
||||
"title": "comfyUI-nsfw-detection [NAME CONFLICT]",
|
||||
"id": "nsfw-detection",
|
||||
"reference": "https://github.com/katalist-ai/comfyUI-nsfw-detection",
|
||||
"files": [
|
||||
"https://github.com/katalist-ai/comfyUI-nsfw-detection"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: NudenetDetector"
|
||||
},
|
||||
{
|
||||
"author": "AIFSH",
|
||||
"title": "DiffSynth-ComfyUI [NAME CONFLICT]",
|
||||
"reference": "https://github.com/AIFSH/DiffSynth-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/AIFSH/DiffSynth-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "a custom node for [a/DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio)"
|
||||
},
|
||||
{
|
||||
"author": "flyghtxmz",
|
||||
"title": "ComfyUI-CFG-Ctrl [NAME CONFLICT]",
|
||||
"reference": "https://github.com/flyghtxmz/ComfyUI-CFG-Ctrl",
|
||||
"files": [
|
||||
"https://github.com/flyghtxmz/ComfyUI-CFG-Ctrl"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node implementing SMC-CFG (Sliding Mode Control Classifier-Free Guidance), a nonlinear control-theory approach to CFG that prevents instability and semantic overshooting at high guidance scales. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "trusapien",
|
||||
"title": "ComfyUI-Trusapien",
|
||||
@@ -183,6 +121,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A small pack of utility nodes including CLIP/VAE fallbacks, Anima model detection, and multi image preview.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "TechnoWarrior2",
|
||||
"title": "comfyui-live-preview [WIP]",
|
||||
"reference": "https://github.com/TechnoWarrior2/comfyui-live-preview",
|
||||
"files": [
|
||||
"https://github.com/TechnoWarrior2/comfyui-live-preview"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node displaying large floating overlay window with live denoising step previews as image generates, similar to A1111 and Forge.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "HuajunPeng1982",
|
||||
"title": "comfyui-grsai-storyboard",
|
||||
@@ -613,6 +561,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI utility nodes including SamplerPipeIn, SamplerPipeOut, and VAENames for workflow management. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "pollockjj",
|
||||
"title": "JAXTools",
|
||||
"reference": "https://github.com/pollockjj/ComfyUI-JAXTools",
|
||||
"files": [
|
||||
"https://github.com/pollockjj/ComfyUI-JAXTools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "JAX-accelerated image processing nodes for ComfyUI via dm_pix sealed worker isolation"
|
||||
},
|
||||
{
|
||||
"author": "ptmff",
|
||||
"title": "ComfyUI-Nodes8734",
|
||||
@@ -1013,6 +971,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A lightweight utility node for ComfyUI that detects aspect ratio of input images and selects preset resolutions with multiplier support.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "MengYe-Z",
|
||||
"title": "ComfyUI-MultiPointWhiteBalance [WIP]",
|
||||
"reference": "https://github.com/MengYe-Z/ComfyUI-MultiPointWhiteBalance",
|
||||
"files": [
|
||||
"https://github.com/MengYe-Z/ComfyUI-MultiPointWhiteBalance"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node pack for e-commerce product photography with precise white balance and exposure correction using multi-point sampling.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "Mervent",
|
||||
"title": "comfyui-fast-lut [WIP]",
|
||||
@@ -1213,6 +1181,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Image selector node for ComfyUI.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Kitten-TTS [NAME CONFLICT]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-KittenTTS",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-KittenTTS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node for KittenTTS - Ultra-lightweight text-to-speech (CUDA / CPU)"
|
||||
},
|
||||
{
|
||||
"author": "opj161",
|
||||
"title": "comfyui-comfyui-lmstudio [NAME CONFLICT]",
|
||||
@@ -1875,6 +1853,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI Frontend Vue Basic is custom node that demonstrate how to use vue as frontend framework along with primevue and vue-i18n, cooperating with ComfyUI API. Needs ComfyUI Frontend 1.25.0 or later."
|
||||
},
|
||||
{
|
||||
"author": "vjumpkung",
|
||||
"title": "comfyui-mosaic-blur [NAME CONFLICT]",
|
||||
"reference": "https://github.com/vjumpkung/comfyui-mosaic-blur",
|
||||
"files": [
|
||||
"https://github.com/vjumpkung/comfyui-mosaic-blur"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node for applying mosaic blur effects. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "richardctw",
|
||||
"title": "ComfyUI-ImageFillBackgroundColor",
|
||||
@@ -1925,6 +1913,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: CLIPModelSelectionNode, CLIPVisionModelSelectionNode, CheckpointSelectionNode, DiffusionModelSelectionNode, FilteredCLIPModelsNode, ... (13 total)"
|
||||
},
|
||||
{
|
||||
"author": "pollockjj",
|
||||
"title": "ComfyUI-StabilityTest",
|
||||
"reference": "https://github.com/pollockjj/ComfyUI-StabilityTest",
|
||||
"files": [
|
||||
"https://github.com/pollockjj/ComfyUI-StabilityTest"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI stability testing node. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "Tr1dae",
|
||||
"title": "ComfyUI-MobileSAM [WIP]",
|
||||
@@ -1995,6 +1993,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes for Google Gemini AI with text generation and image generation capabilities using the Python SDK."
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "ComfyUI-NovaSR",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-NovaSR",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-NovaSR"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for NovaSR - Ultra-fast audio super resolution (3600x realtime, 50KB model)"
|
||||
},
|
||||
{
|
||||
"author": "GokuHiki",
|
||||
"title": "ComfyUI-Goku-Tools",
|
||||
@@ -2235,6 +2243,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: GetSimilarity, LPIPSSimilarity"
|
||||
},
|
||||
{
|
||||
"author": "StevenBaby",
|
||||
"title": "comfyui-tools",
|
||||
"reference": "https://github.com/StevenBaby/comfyui-tools",
|
||||
"files": [
|
||||
"https://github.com/StevenBaby/comfyui-tools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "IntParameterNode: A Node contain 4 int parameters to quick switch input to other nodes."
|
||||
},
|
||||
{
|
||||
"author": "zhu798542746",
|
||||
"title": "comfyui_model [UNSAFE]",
|
||||
@@ -2349,7 +2367,6 @@
|
||||
"author": "huhu-tiger",
|
||||
"title": "ComfyUI-RemoteResource",
|
||||
"reference": "https://github.com/huhu-tiger/ComfyUI-RemoteResource",
|
||||
"reference2": "https://github.com/huhu-tiger/ComfyUI-RemoteDownload",
|
||||
"files": [
|
||||
"https://github.com/huhu-tiger/ComfyUI-RemoteResource"
|
||||
],
|
||||
@@ -2676,6 +2693,16 @@
|
||||
"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]",
|
||||
@@ -2906,6 +2933,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node providing customizable prompt generation capabilities. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "JBKing514",
|
||||
"title": "map_comfyui [WIP]",
|
||||
"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": "binarystatic",
|
||||
"title": "ComfyUI-BinarystaticMasterSeed",
|
||||
@@ -3216,6 +3253,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A simple ComfyUI node that lets you choose one text from up to 10 inputs.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "Isi-dev",
|
||||
"title": "ComfyUI_DeleteModelPassthrough_ExecutionFlowControl [WIP]",
|
||||
"reference": "https://github.com/Isi-dev/ComfyUI_DeleteModelPassthrough_ExecutionFlowControl",
|
||||
"files": [
|
||||
"https://github.com/Isi-dev/ComfyUI_DeleteModelPassthrough_ExecutionFlowControl"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Memory management custom node that deletes specific models from VRAM and RAM while passing through other input types unchanged, useful for low-memory environments. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "K1maran",
|
||||
"title": "ComfyUI-Kimaran",
|
||||
@@ -3386,6 +3433,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Quality-of-life utility nodes for ComfyUI providing smart switches for dynamic data routing, path helpers for consistent file naming, and image utilities including Qwen vision model preparation.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "huhu-tiger",
|
||||
"title": "ComfyUI-RemoteDownload",
|
||||
"reference": "https://github.com/huhu-tiger/ComfyUI-RemoteDownload",
|
||||
"files": [
|
||||
"https://github.com/huhu-tiger/ComfyUI-RemoteDownload"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: LoadImageFromRemote"
|
||||
},
|
||||
{
|
||||
"author": "basenc",
|
||||
"title": "Comfyui-Nodes-basenc [UNSAFE]",
|
||||
@@ -3466,6 +3523,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This repo has different functions to help dealing with huggingface data"
|
||||
},
|
||||
{
|
||||
"author": "ilslli",
|
||||
"title": "Comfyui-IV2Z",
|
||||
"reference": "https://github.com/ilslli/Comfyui-IV2Z",
|
||||
"files": [
|
||||
"https://github.com/ilslli/Comfyui-IV2Z"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Modify the VideoCombine node of [a/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) so that, after encoding the video, it outputs the result as a compressed archive."
|
||||
},
|
||||
{
|
||||
"author": "PladsElsker",
|
||||
"title": "comfyui-krita [WIP]",
|
||||
@@ -3557,6 +3624,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: 'URL: Load Media', 'Image: Save To Disk (No UI)', 'S3: Upload Presigned', ..."
|
||||
},
|
||||
{
|
||||
"author": "akashch1512",
|
||||
"title": "ComfyUI_UguuUploader",
|
||||
"reference": "https://github.com/akashch1512/ComfyUI_UguuUploader",
|
||||
"files": [
|
||||
"https://github.com/akashch1512/ComfyUI_UguuUploader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Uguu.se Video Uploader\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "vadimcro",
|
||||
"title": "string_by_index",
|
||||
@@ -4349,6 +4426,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: S3 Upload File, File Explorer, Save Image Export Path, Save Video Export Path, Save Audio Export Path, S3 Combine Images to Video, Multi-Line Text Input, ...\nNOTE: The files in the repo are not organized.[w/This extension has a vulnerability that allows arbitrary access to local files from remote.]"
|
||||
},
|
||||
{
|
||||
"author": "kesslerdev",
|
||||
"title": "Comfyui : kesslerdev_comfyui_nodes [UNSAFE]",
|
||||
"reference": "https://github.com/kesslerdev/kesslerdev_comfyui_nodes",
|
||||
"files": [
|
||||
"https://github.com/kesslerdev/kesslerdev_comfyui_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Multiline Text Choice (Kesslerdev), Environment Variable Getter (Kesslerdev)[w/This nodepack includes a node that can remotely read arbitrary environment variables.]"
|
||||
},
|
||||
{
|
||||
"author": "panchovial-max",
|
||||
"title": "ComfyUI-ListHelper [NAME CONFLICT]",
|
||||
@@ -4900,6 +4987,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A ComfyUI custom node implementation of ByteDance's USO (Unified Style and Subject-Driven Generation) model, which enables advanced style transfer and subject preservation using FLUX.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "numq",
|
||||
"title": "comfyui-camera-capture-node",
|
||||
"reference": "https://github.com/numq/comfyui-camera-capture-node",
|
||||
"files": [
|
||||
"https://github.com/numq/comfyui-camera-capture-node"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Camera Capture"
|
||||
},
|
||||
{
|
||||
"author": "pickles",
|
||||
"title": "PyPromptGenerator [UNSAFE]",
|
||||
@@ -5000,6 +5097,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: INT Constant, Simple Int Math Handle, Simple Json Array Handle, Simple Json Object Handle, Select Image Size, Video Time & FPS, Video Frame Size, Wan22 Step Handle, Prompt Selector String, Prompt Example Node, Prompt Join or List, Prompt List, CRD Audio Length Node, ..."
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "ComfyUI YTDL Nodes [WIP]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-ytdl_nodes",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-ytdl_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes for downloading, converting, and previewing audio/video from YouTube and 1,000+ other platforms"
|
||||
},
|
||||
{
|
||||
"author": "comfyscript",
|
||||
"title": "ComfyUI-CloudClient",
|
||||
@@ -5020,6 +5127,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node for ComfyUI that automatically uploads generated images to SharePoint or OneDrive document libraries.[w/This nodepack contains a node that can write files to an arbitrary path.]"
|
||||
},
|
||||
{
|
||||
"author": "KoinnAI",
|
||||
"title": "ComfyUI Dynamic Prompting Simplified [WIP]",
|
||||
"reference": "https://github.com/KoinnAI/ComfyUI-DynPromptSimplified",
|
||||
"files": [
|
||||
"https://github.com/KoinnAI/ComfyUI-DynPromptSimplified"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A minimal dynamic prompting + mirrored wildcards node for ComfyUI.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "jtrue",
|
||||
"title": "MaskTools",
|
||||
@@ -5411,6 +5528,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node Qwen3 designed to integrate with a GPT-based system under the category GPT_QWEN/Qwen. It serves as an interface to interact with the Qwen language model, specifically the 'Qwen/Qwen3-4B-Instruct-2507' variant."
|
||||
},
|
||||
{
|
||||
"author": "MatthewClayHarrison",
|
||||
"title": "MetaMan - Universal AI Image Metadata Manager [UNSAFE]",
|
||||
"reference": "https://github.com/MatthewClayHarrison/ComfyUI-MetaMan",
|
||||
"files": [
|
||||
"https://github.com/MatthewClayHarrison/ComfyUI-MetaMan"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "First universal metadata system for AI image generation, with template-driven architecture allowing easy extension to new services; comprehensive dependency tracking with automatic download resolution; lossless conversion between platform formats where possible; future-proof design with extensible schema and validation system.[w/This nodepack has a vulnerability that allows remote access to arbitrary file paths.]"
|
||||
},
|
||||
{
|
||||
"author": "idoru",
|
||||
"title": "Filestash Upload Node [UNSAFE]",
|
||||
@@ -5781,6 +5908,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a text translation node using a local SLM (Microsoft Phi-4) for ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "DenRakEiw",
|
||||
"title": "Denrakeiw Nodes [WIP]",
|
||||
"reference": "https://github.com/DenRakEiw/DenRakEiw_Nodes",
|
||||
"files": [
|
||||
"https://github.com/DenRakEiw/DenRakEiw_Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom nodepack for ComfyUI that provides utility nodes for image generation and manipulation.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "ahmedbana",
|
||||
"title": "json-creator [WIP]",
|
||||
@@ -6261,6 +6398,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Whisper + AudioTranslate, Google Translate Node"
|
||||
},
|
||||
{
|
||||
"author": "edgerunner",
|
||||
"title": "ComfyUI Queue Manager [WIP]",
|
||||
"reference": "https://github.com/QuietNoise/ComfyUI-Queue-Manager",
|
||||
"files": [
|
||||
"https://github.com/QuietNoise/ComfyUI-Queue-Manager"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "An extension supporting more streamlined prompt queue management."
|
||||
},
|
||||
{
|
||||
"author": "fylrid2",
|
||||
"title": "lockValue",
|
||||
@@ -7033,6 +7180,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Random Line from File (Seeded), Keyword Prompt Composer, Simple Prompt Combiner (5x), Boolean Switch (Any), Select Line from TxT (Any), Multiline Text Input, Flux 2M Aspect Ratios, SDXL Aspect Ratios, Room Mapper, ..."
|
||||
},
|
||||
{
|
||||
"author": "pacchikAI",
|
||||
"title": "ImagePromptBatch [UNSAFE]",
|
||||
"reference": "https://github.com/pacchikAI/ImagePromptBatch",
|
||||
"files": [
|
||||
"https://github.com/pacchikAI/ImagePromptBatch"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Load Image and Prompt[w/This includes a node that can read the contents of a `.csv` file from an arbitrary path.]"
|
||||
},
|
||||
{
|
||||
"author": "papcorns",
|
||||
"title": "ComfyUI-Papcorns-Node-UploadToGCS",
|
||||
@@ -7743,6 +7900,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI extensions for sending prompt progress to webhook"
|
||||
},
|
||||
{
|
||||
"author": "ashllay",
|
||||
"title": "ComfyUI_MoreComfy",
|
||||
"reference": "https://github.com/ashllay/ComfyUI_MoreComfy",
|
||||
"files": [
|
||||
"https://github.com/ashllay/ComfyUI_MoreComfy"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: MC Switch Seed/Image/Latent/Model/String, MC Alter Seed, MC Set Tile Size, MC Get Image Size, MC Multi Concat"
|
||||
},
|
||||
{
|
||||
"author": "gordon1chuge2623",
|
||||
"title": "ComfyUI_seal_migration [WIP]",
|
||||
@@ -7913,6 +8080,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Add Text To Image, Adjust Hex Brightness, Adjust Transparency By Mask, Align Images with Mask, Align Restore Json, Binarize Mask, Blend ICLight, ..."
|
||||
},
|
||||
{
|
||||
"author": "alexgenovese",
|
||||
"title": "ComfyUI-Reica",
|
||||
"reference": "https://github.com/alexgenovese/ComfyUI-Reica",
|
||||
"files": [
|
||||
"https://github.com/alexgenovese/ComfyUI-Reica"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Reica Text Image Display, Flux Image Generator, Reica GCP: Read Image, Reica GCP: Write Image & Get URL, Reica API: Send HTTP Notification"
|
||||
},
|
||||
{
|
||||
"author": "Stable-X",
|
||||
"title": "ComfyUI-Hi3DGen",
|
||||
@@ -8024,6 +8201,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Provides nodes to execute arbitrary Python code snippets and Resize images directly within ComfyUI workflows. [w/This node allows you to execute arbitrary code via the workflow.]"
|
||||
},
|
||||
{
|
||||
"author": "ashllay",
|
||||
"title": "ComfyUI_MoreComfy",
|
||||
"reference": "https://github.com/ashllay/ComfyUI_MoreComfy",
|
||||
"files": [
|
||||
"https://github.com/ashllay/ComfyUI_MoreComfy"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: MC Switch Seed, MC Alter Seed, MC Set Tile Size, MC Multi Concat"
|
||||
},
|
||||
{
|
||||
"author": "ayaoayaoayaoaya",
|
||||
"title": "ComfyUI-KLUT-DeepSeek-API [WIP]",
|
||||
@@ -8124,6 +8311,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A lightweight nodepack for ComfyUI that adds a few handy nodes that I use in my workflows"
|
||||
},
|
||||
{
|
||||
"author": "CeeVeeR",
|
||||
"title": "ComfyUi-Text-Tiler",
|
||||
"reference": "https://github.com/CeeVeeR/ComfyUi-Text-Tiler",
|
||||
"files": [
|
||||
"https://github.com/CeeVeeR/ComfyUi-Text-Tiler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Text Tiler"
|
||||
},
|
||||
{
|
||||
"author": "Dreamshot-io",
|
||||
"title": "ComfyUI-Extend-Resolution",
|
||||
@@ -8635,6 +8832,26 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Add Grid Boundaries, Pack Frames, Unpack Frames, Resize Frame"
|
||||
},
|
||||
{
|
||||
"author": "Northerner1",
|
||||
"title": "ComfyUI_North_Noise [WIP]",
|
||||
"reference": "https://github.com/Northerner1/ComfyUI_North_Noise",
|
||||
"files": [
|
||||
"https://github.com/Northerner1/ComfyUI_North_Noise"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Unsampler"
|
||||
},
|
||||
{
|
||||
"author": "kimara-ai",
|
||||
"title": "ComfyUI-Kimara-AI-Image-From-URL [WIP]",
|
||||
"reference": "https://github.com/kimara-ai/ComfyUI-Kimara-AI-Image-From-URL",
|
||||
"files": [
|
||||
"https://github.com/kimara-ai/ComfyUI-Kimara-AI-Image-From-URL"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Load image from URL and downscale to desired megapixels. Set megapixels to 0 for no downscaling."
|
||||
},
|
||||
{
|
||||
"author": "tc8M4lF3s88",
|
||||
"title": "comfy-tif-support",
|
||||
@@ -9467,6 +9684,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A set of nodes to edit videos using the Hunyuan Video model"
|
||||
},
|
||||
{
|
||||
"author": "watarika",
|
||||
"title": "ComfyUI-exit [UNSAFE]",
|
||||
"reference": "https://github.com/watarika/ComfyUI-exit",
|
||||
"files": [
|
||||
"https://github.com/watarika/ComfyUI-exit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node that terminates ComfyUI after a specified number of seconds. Use this node if you want Google Colab to automatically terminate after mass generation. It is necessary to disconnect and delete the Google Colab runtime on the Notebook side."
|
||||
},
|
||||
{
|
||||
"author": "backearth1",
|
||||
"title": "Comfyui-MiniMax-Video [WIP]",
|
||||
@@ -9618,6 +9845,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: SaveImageOSS."
|
||||
},
|
||||
{
|
||||
"author": "Matrix-King-Studio",
|
||||
"title": "ComfyUI-MoviePy",
|
||||
"reference": "https://github.com/Matrix-King-Studio/ComfyUI-MoviePy",
|
||||
"files": [
|
||||
"https://github.com/Matrix-King-Studio/ComfyUI-MoviePy"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Image Clip Node, Audio Duration Node, Save Video Node,..."
|
||||
},
|
||||
{
|
||||
"author": "Big Idea Technology",
|
||||
"title": "ComfyUI-Movie-Tools [WIP]",
|
||||
@@ -9952,6 +10189,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES:LETM Save Image, ETM Load Image From Local"
|
||||
},
|
||||
{
|
||||
"author": "m-ai-studio",
|
||||
"title": "mai-prompt-progress",
|
||||
"reference": "https://github.com/m-ai-studio/mai-prompt-progress",
|
||||
"files": [
|
||||
"https://github.com/m-ai-studio/mai-prompt-progress"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI extensions for sending prompt progress to webhook"
|
||||
},
|
||||
{
|
||||
"author": "neeltheninja",
|
||||
"title": "ComfyUI-TempFileDeleter [UNSAFE]",
|
||||
@@ -10166,6 +10413,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "comfyui-photobooth-customnode"
|
||||
},
|
||||
{
|
||||
"author": "netanelben",
|
||||
"title": "comfyui-text2image-customnode",
|
||||
"reference": "https://github.com/netanelben/comfyui-text2image-customnode",
|
||||
"files": [
|
||||
"https://github.com/netanelben/comfyui-text2image-customnode"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "comfyui-text2image-customnode"
|
||||
},
|
||||
{
|
||||
"author": "netanelben",
|
||||
"title": "comfyui-camera2image-customnode",
|
||||
@@ -10579,6 +10836,16 @@
|
||||
"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": "denislov",
|
||||
"title": "Comfyui_AutoSurvey",
|
||||
"reference": "https://github.com/denislov/Comfyui_AutoSurvey",
|
||||
"files": [
|
||||
"https://github.com/denislov/Comfyui_AutoSurvey"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:AutoSurvey, WriteOutline, WriteSection, ChatModel, QueryKnowledge, ManageDatabase, AddDoc2Knowledge"
|
||||
},
|
||||
{
|
||||
"author": "leoleelxh",
|
||||
"title": "ComfyUI-MidjourneyNode-leoleexh",
|
||||
@@ -10938,6 +11205,17 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Lora Block Weight Regex Loader"
|
||||
},
|
||||
{
|
||||
"author": "jtydhr88",
|
||||
"title": "ComfyUI-Unique3D [WIP]",
|
||||
"id": "unique3d",
|
||||
"reference": "https://github.com/jtydhr88/ComfyUI-Unique3D",
|
||||
"files": [
|
||||
"https://github.com/jtydhr88/ComfyUI-Unique3D"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI Unique3D is custom nodes that running [a/AiuniAI/Unique3D](https://github.com/AiuniAI/Unique3D) into ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "kycg",
|
||||
"title": "comfyui-Kwtoolset",
|
||||
@@ -10960,6 +11238,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This Python script is an optional add-on to the Comfy UI stable diffusion client."
|
||||
},
|
||||
{
|
||||
"author": "immersiveexperience",
|
||||
"title": "ie-comfyui-color-nodes",
|
||||
"reference": "https://github.com/immersiveexperience/ie-comfyui-color-nodes",
|
||||
"files": [
|
||||
"https://github.com/immersiveexperience/ie-comfyui-color-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes for simple color correction."
|
||||
},
|
||||
{
|
||||
"author": "LZpenguin",
|
||||
"title": "ComfyUI-Text",
|
||||
@@ -11264,6 +11552,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI extensions for better [a/Captain](https://github.com/blib-la/captain) integration."
|
||||
},
|
||||
{
|
||||
"author": "ejektaflex",
|
||||
"title": "ComfyUI-Ty",
|
||||
"reference": "https://github.com/ejektaflex/ComfyUI-Ty",
|
||||
"files": [
|
||||
"https://github.com/ejektaflex/ComfyUI-Ty"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Lora Block Weight Regex Loader"
|
||||
},
|
||||
{
|
||||
"author": "christian-byrne",
|
||||
"title": "Python Interpreter ComfyUI Node [UNSAFE]",
|
||||
@@ -11385,6 +11683,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "[a/https://huggingface.co/deepseek-ai](https://huggingface.co/deepseek-ai)"
|
||||
},
|
||||
{
|
||||
"author": "GentlemanHu",
|
||||
"title": "ComfyUI-Notifier",
|
||||
"reference": "https://github.com/GentlemanHu/ComfyUI-Notifier",
|
||||
"files": [
|
||||
"https://github.com/GentlemanHu/ComfyUI-Notifier"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:GentlemanHu_Notifier"
|
||||
},
|
||||
{
|
||||
"author": "nat-chan",
|
||||
"title": "Transceiver📡",
|
||||
@@ -11585,6 +11893,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:GH Tools Image Sizer, GH Tools Simple Scale. Simple quality of life Tools for ComfyUI. Basically, If it makes my life easier, it will be here. The list will grow over time."
|
||||
},
|
||||
{
|
||||
"author": "Beinsezii",
|
||||
"title": "comfyui-amd-go-fast",
|
||||
"reference": "https://github.com/Beinsezii/comfyui-amd-go-fast",
|
||||
"files": [
|
||||
"https://github.com/Beinsezii/comfyui-amd-go-fast"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "See details: [a/link](https://github.com/Beinsezii/comfyui-amd-go-fast?tab=readme-ov-file)"
|
||||
},
|
||||
{
|
||||
"author": "SeedV",
|
||||
"title": "ComfyUI-SeedV-Nodes [UNSAFE]",
|
||||
|
||||
+278
-353
File diff suppressed because it is too large
Load Diff
+1604
-1394
File diff suppressed because it is too large
Load Diff
@@ -1,316 +1,5 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "Jokimbe",
|
||||
"title": "ComfyUI-DrawThings-gRPC [DEPRECATED]",
|
||||
"reference": "https://github.com/Jokimbe/ComfyUI-DrawThings-gRPC",
|
||||
"files": [
|
||||
"https://github.com/Jokimbe/ComfyUI-DrawThings-gRPC"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Connect to any Draw Things gRPC server"
|
||||
},
|
||||
{
|
||||
"author": "akashch1512",
|
||||
"title": "ComfyUI_UguuUploader [REMOVED]",
|
||||
"reference": "https://github.com/akashch1512/ComfyUI_UguuUploader",
|
||||
"files": [
|
||||
"https://github.com/akashch1512/ComfyUI_UguuUploader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Uguu.se Video Uploader\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "CeeVeeR",
|
||||
"title": "ComfyUi-Text-Tiler [REMOVED]",
|
||||
"reference": "https://github.com/CeeVeeR/ComfyUi-Text-Tiler",
|
||||
"files": [
|
||||
"https://github.com/CeeVeeR/ComfyUi-Text-Tiler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Text Tiler"
|
||||
},
|
||||
{
|
||||
"author": "ilslli",
|
||||
"title": "Comfyui-IV2Z [REMOVED]",
|
||||
"reference": "https://github.com/ilslli/Comfyui-IV2Z",
|
||||
"files": [
|
||||
"https://github.com/ilslli/Comfyui-IV2Z"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Modify the VideoCombine node of [a/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) so that, after encoding the video, it outputs the result as a compressed archive."
|
||||
},
|
||||
{
|
||||
"author": "TK22-26",
|
||||
"title": "Inspect Image Overlay [REMOVED]",
|
||||
"reference": "https://github.com/TK22-26/comfyui-inspect-image-overlay",
|
||||
"files": [
|
||||
"https://github.com/TK22-26/comfyui-inspect-image-overlay"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node that opens generated images in a fullscreen inspector overlay."
|
||||
},
|
||||
{
|
||||
"author": "space-nuko",
|
||||
"title": "OpenPose Editor [NOT MAINTAINED]",
|
||||
"id": "openpose-editor",
|
||||
"reference": "https://github.com/space-nuko/ComfyUI-OpenPose-Editor",
|
||||
"files": [
|
||||
"https://github.com/space-nuko/ComfyUI-OpenPose-Editor"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A port of the openpose-editor extension for stable-diffusion-webui. NOTE: Requires [a/this ComfyUI patch](https://github.com/comfyanonymous/ComfyUI/pull/711) to work correctly"
|
||||
},
|
||||
{
|
||||
"author": "Zuellni",
|
||||
"title": "Zuellni/ComfyUI-Custom-Nodes [DEPRECATED]",
|
||||
"reference": "https://github.com/Zuellni/ComfyUI-Custom-Nodes",
|
||||
"files": [
|
||||
"https://github.com/Zuellni/ComfyUI-Custom-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: DeepFloyd, Filter, Select, Save, Decode, Encode, Repeat, Noise, Noise"
|
||||
},
|
||||
{
|
||||
"author": "StevenBaby",
|
||||
"title": "comfyui-tools [REMOVED]",
|
||||
"reference": "https://github.com/StevenBaby/comfyui-tools",
|
||||
"files": [
|
||||
"https://github.com/StevenBaby/comfyui-tools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "IntParameterNode: A Node contain 4 int parameters to quick switch input to other nodes."
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Kitten-TTS [NAME CONFLICT] [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-KittenTTS",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-KittenTTS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node for KittenTTS - Ultra-lightweight text-to-speech (CUDA / CPU)"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "ComfyUI-NovaSR [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-NovaSR",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-NovaSR"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for NovaSR - Ultra-fast audio super resolution (3600x realtime, 50KB model)"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "ComfyUI YTDL Nodes [WIP] [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-ytdl_nodes",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-ytdl_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes for downloading, converting, and previewing audio/video from YouTube and 1,000+ other platforms"
|
||||
},
|
||||
{
|
||||
"author": "pollockjj",
|
||||
"title": "JAXTools [REMOVED]",
|
||||
"reference": "https://github.com/pollockjj/ComfyUI-JAXTools",
|
||||
"files": [
|
||||
"https://github.com/pollockjj/ComfyUI-JAXTools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "JAX-accelerated image processing nodes for ComfyUI via dm_pix sealed worker isolation"
|
||||
},
|
||||
{
|
||||
"author": "pollockjj",
|
||||
"title": "ComfyUI-StabilityTest [REMOVED]",
|
||||
"reference": "https://github.com/pollockjj/ComfyUI-StabilityTest",
|
||||
"files": [
|
||||
"https://github.com/pollockjj/ComfyUI-StabilityTest"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI stability testing node. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"author": "denislov",
|
||||
"title": "Comfyui_AutoSurvey [REMOVED]",
|
||||
"reference": "https://github.com/denislov/Comfyui_AutoSurvey",
|
||||
"files": [
|
||||
"https://github.com/denislov/Comfyui_AutoSurvey"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:AutoSurvey, WriteOutline, WriteSection, ChatModel, QueryKnowledge, ManageDatabase, AddDoc2Knowledge"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "ZONOS2 TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/Zonos2_TTS-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/Zonos2_TTS-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Native ComfyUI nodes for Zyphra ZONOS2 text-to-speech and voice cloning with AIMDO memory tracking"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "WavTTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/WavTTS-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/WavTTS-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for WavTTS zero-shot text-to-speech with real ComfyUI memory tracking"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Step Audio EditX TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-Step_Audio_EditX_TTS",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-Step_Audio_EditX_TTS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Professional voice cloning and audio editing node for ComfyUI using Step Audio EditX"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "SoulX-Singer [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-SoulX-Singer",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-SoulX-Singer"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for SoulX-Singer - Zero-Shot Singing Voice Synthesis"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "Seedream4 Replicate [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-Seedream4_Replicate",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-Seedream4_Replicate"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for Seedream 4 image generation via Replicate API with support for text-to-image and image-to-image generation"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "MOSS-TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/Moss_TTS-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/Moss_TTS-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for OpenMOSS MOSS-TTS Local Transformer v1.5 with model catalog downloads, Whisper prefix transcription, continuation, voice cloning, and AIMDO memory tracking"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "Maya1 TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-Maya1_TTS",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-Maya1_TTS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for Maya1 TTS - Expressive voice generation with 20+ emotions, voice design, and SNAC neural codec"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "LongCat AudioDiT TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-LongCat-AudioDIT-TTS",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-LongCat-AudioDIT-TTS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for LongCat-AudioDiT TTS - zero-shot and voice cloning diffusion TTS"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Ideogram 4 Prompter [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ideogram4_prompter-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ideogram4_prompter-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for Ideogram 4 Magic Prompt and Ideogram 4 resolution selection"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Higgs v3 TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/Higgs_v3-TTS-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/Higgs_v3-TTS-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for Higgs Audio v3 TTS with native inference, voice cloning, multi-speaker dialogue, longform chunking, and AIMDO DynamicVRAM support"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "Foundation-1 [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-Foundation-1",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-Foundation-1"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for Foundation-1 | structured text-to-sample generation for music production"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "dotWaveform [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-dotWaveform",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-dotWaveform"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for generating animated dotted waveform visualizations with multiple animation styles including teardrop bars"
|
||||
},
|
||||
{
|
||||
"author": "saganaki22",
|
||||
"title": "Dots TTS [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/Dots-TTS-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/Dots-TTS-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for rednote-hilab dots.tts with model loading, generation, voice cloning, Whisper transcription, progress tracking, and AIMDO memory visualization"
|
||||
},
|
||||
{
|
||||
"author": "Saganaki22",
|
||||
"title": "AudioSR [REMOVED]",
|
||||
"reference": "https://github.com/Saganaki22/ComfyUI-AudioSR",
|
||||
"files": [
|
||||
"https://github.com/Saganaki22/ComfyUI-AudioSR"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI node for AudioSR - Versatile Audio Super Resolution upscales audio to 48kHz using latent diffusion"
|
||||
},
|
||||
{
|
||||
"author": "pollockjj",
|
||||
"title": "comfyui-sealedcuda [REMOVED]",
|
||||
"reference": "https://github.com/pollockjj/ComfyUI-SealedCuda",
|
||||
"files": [
|
||||
"https://github.com/pollockjj/ComfyUI-SealedCuda"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Sealed-worker CUDA image nodes (NATTEN, fused-ssim) sourced from prebuilt cuda-wheels"
|
||||
},
|
||||
{
|
||||
"author": "lvquan",
|
||||
"title": "CS Hybrid 4K Image-to-Video [REMOVED]",
|
||||
"reference": "https://github.com/lvqunx/comfyui-cs-hybrid",
|
||||
"files": [
|
||||
"https://github.com/lvqunx/comfyui-cs-hybrid"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Compressed-sensing hybrid 4K image-to-video: RAFT flow transfer, dual-channel Laplacian recompose, CS tile scheduler, keyframe refresh helpers."
|
||||
},
|
||||
{
|
||||
"author": "leylahviolet",
|
||||
"title": "iolet Tools 💅 [REMOVED]",
|
||||
"reference": "https://github.com/leylahviolet/ComfyUI-Violet-Tools",
|
||||
"files": [
|
||||
"https://github.com/leylahviolet/ComfyUI-Violet-Tools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Aesthetic-focused custom nodes for ComfyUI with sophisticated style and prompt management"
|
||||
},
|
||||
{
|
||||
"author": "mahshid1378",
|
||||
"title": "ThinkSound_Wrapper [REMOVED]",
|
||||
@@ -2040,6 +1729,206 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "🤗🤗🤗Comfyui Universal Translation Plugin (no longer requires adding various nodes, directly add translation function on the existing nodes), allowing Comfyui to support Chinese input and automatic translation for any long text input box, while adding error translation function (calling Baidu Translate), achieving translation freedom![w/This nodepack turns your ComfyUI server into a resource in the cloud.]"
|
||||
},
|
||||
{
|
||||
"author": "Slix-M-Lestragg",
|
||||
"title": "comfyui-enhanced [WIP]",
|
||||
"reference": "https://github.com/Slix-M-Lestragg/comfyui-enhanced",
|
||||
"files": [
|
||||
"https://github.com/Slix-M-Lestragg/comfyui-enhanced"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of enhanced nodes for ComfyUI that provide powerful additional functionality to your workflows.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "Linsoo",
|
||||
"title": "ComfyUI-Linsoo-Custom-Nodes",
|
||||
"reference": "https://github.com/Linsoo/ComfyUI-Linsoo-Custom-Nodes",
|
||||
"files": [
|
||||
"https://github.com/Linsoo/ComfyUI-Linsoo-Custom-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Linsoo Save Image, Linsoo Load Image (In development.. not working), Linsoo Empty Latent Image, Linsoo Multi Inputs, Linsoo Multi Outputs"
|
||||
},
|
||||
{
|
||||
"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": "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": "fuzr0dah",
|
||||
"title": "comfyui-sceneassembly",
|
||||
"reference": "https://github.com/fuzr0dah/comfyui-sceneassembly",
|
||||
"files": [
|
||||
"https://github.com/fuzr0dah/comfyui-sceneassembly"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A bunch of nodes I created that I also find useful."
|
||||
},
|
||||
{
|
||||
"author": "amamisonlyuser",
|
||||
"title": "MixvtonComfyui [WIP]",
|
||||
"reference": "https://github.com/amamisonlyuser/MixvtonComfyui",
|
||||
"files": [
|
||||
"https://github.com/amamisonlyuser/MixvtonComfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: CXH_Leffa_Viton_Load, CXH_Leffa_Viton_Run\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "A4P7J1N7M05OT",
|
||||
"title": "ComfyUI-VAELoaderSDXLmod",
|
||||
"reference": "https://github.com/A4P7J1N7M05OT/ComfyUI-VAELoaderSDXLmod",
|
||||
"files": [
|
||||
"https://github.com/A4P7J1N7M05OT/ComfyUI-VAELoaderSDXLmod"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Modified SDXL VAE Loader, Empty Latent Image Variable"
|
||||
},
|
||||
{
|
||||
"author": "pixixai",
|
||||
"title": "ComfyUI_Pixix-Tools [UNSAFE/WIP]",
|
||||
"reference": "https://github.com/pixixai/ComfyUI_pixixTools",
|
||||
"files": [
|
||||
"https://github.com/pixixai/ComfyUI_pixixTools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Load Text (from folder)\nNOTE: The files in the repo are not organized.[w/The contents of files from arbitrary paths can be read remotely through this node.]"
|
||||
},
|
||||
{
|
||||
"author": "brandonkish",
|
||||
"title": "comfyUI-extractable-text [WIP]",
|
||||
"reference": "https://github.com/brandonkish/comfyUI-extractable-text",
|
||||
"files": [
|
||||
"https://github.com/brandonkish/comfyUI-extractable-text"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Save Image With Description, Save Image To Folder, Load Image With Description, LoRA Testing Node, Get Smaller Of Two Numbers, Get Larger Of Two Numbers\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "Randomwalkforest",
|
||||
"title": "Comfyui-Koi-Toolkit",
|
||||
"reference": "https://github.com/Randomwalkforest/Comfyui-Koi-Toolkit",
|
||||
"files": [
|
||||
"https://github.com/Randomwalkforest/Comfyui-Koi-Toolkit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Koi Toolkit provides advanced nodes"
|
||||
},
|
||||
{
|
||||
"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": "Dehypnotic",
|
||||
"title": "Save MP3 node for ComfyUI [UNSAFE]",
|
||||
"reference": "https://github.com/Dehypnotic/comfyui-save-mp3",
|
||||
"files": [
|
||||
"https://github.com/Dehypnotic/comfyui-save-mp3"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Simple, flexible MP3 saver with bitrate options and handy path/filename templates.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
|
||||
},
|
||||
{
|
||||
"author": "octapus8085",
|
||||
"title": "OpenAI-comfyui-O",
|
||||
"reference": "https://github.com/Spicely/Comfyui-File-Utils",
|
||||
"files": [
|
||||
"https://github.com/Spicely/Comfyui-File-Utils"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This plugin provides multiple file-handling and utility nodes for ComfyUI, including: image saving, audio saving, video saving, video composition, audio-to-subtitle conversion, and random number generation nodes. These nodes not only process files but also return their absolute file paths.\nNOTE: The files in the repo are not organized.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
|
||||
},
|
||||
{
|
||||
"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": "aistudynow",
|
||||
"title": "comfyui-wilddragon-nodes [WIP]",
|
||||
"reference": "https://github.com/aistudynow/comfyui-wilddragon-nodes",
|
||||
"files": [
|
||||
"https://github.com/aistudynow/comfyui-wilddragon-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Image Face Crop (2025), Person Selector, Pose Face Filter, Person BBox Tracker, Face Restore & Blend, ...\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"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": "90cube",
|
||||
"title": "CBCanvas Node for ComfyUI [WIP]",
|
||||
"reference": "https://github.com/90cube/Comfyui-CBcanvas",
|
||||
"files": [
|
||||
"https://github.com/90cube/Comfyui-CBcanvas"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Canvas node with intelligent aspect ratio control - 13 preset aspect ratios from ultra-wide (21:9) to ultra-tall (9:21).\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": "PozzettiAndrea",
|
||||
"title": "ComfyUI-CameraAnalysis",
|
||||
"reference": "https://github.com/PozzettiAndrea/ComfyUI-CameraAnalysis",
|
||||
"files": [
|
||||
"https://github.com/PozzettiAndrea/ComfyUI-CameraAnalysis"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Extracts camera intrinsic parameters from image EXIF data."
|
||||
},
|
||||
{
|
||||
"author": "LSDJesus",
|
||||
"title": "ComfyUI-Luna-Collection [WIP]",
|
||||
"reference": "https://github.com/LSDJesus/ComfyUI-Luna-Collection",
|
||||
"files": [
|
||||
"https://github.com/LSDJesus/ComfyUI-Luna-Collection"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Collection of finetuned and custom nodes for Pyrite and I\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "dorpxam",
|
||||
"title": "ComfyUI-LTXVideoLoRA [REMOVED]",
|
||||
@@ -2684,7 +2573,7 @@
|
||||
},
|
||||
{
|
||||
"author": "BlackVortexAI",
|
||||
"title": "BV Nodes [REMOVED]",
|
||||
"title": "BV Nodes [DEPRECATED]",
|
||||
"reference": "https://github.com/BlackVortexAI/ComfyUI-BVortexNodes",
|
||||
"files": [
|
||||
"https://github.com/BlackVortexAI/ComfyUI-BVortexNodes"
|
||||
@@ -3287,7 +3176,7 @@
|
||||
},
|
||||
{
|
||||
"author": "vsaan212",
|
||||
"title": "ComfyUI Subject Selector [REMOVED]",
|
||||
"title": "ComfyUI Subject Selector [DEPRECATED]",
|
||||
"reference": "https://github.com/vsaan212/ComfyUI_subjectselector",
|
||||
"files": [
|
||||
"https://github.com/vsaan212/ComfyUI_subjectselector"
|
||||
@@ -3619,6 +3508,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A simple custom node for ComfyUI that allows you to compare two images (or batches of images) side-by-side within the UI."
|
||||
},
|
||||
{
|
||||
"author": "APZmedia",
|
||||
"title": "NormalMapLightEstimator [REMOVED]",
|
||||
"reference": "https://github.com/APZmedia/Comfyui-LightDirection-estimation",
|
||||
"files": [
|
||||
"https://github.com/APZmedia/Comfyui-LightDirection-estimation"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A ComfyUI custom node for estimating light direction and quality from normal maps using luma masking. The system analyzes surface normals to infer lighting information for downstream tasks like adaptive relighting, directional masking, or stylized effects."
|
||||
},
|
||||
{
|
||||
"author": "APZmedia",
|
||||
"title": "ComfyUI APZmedia PSD Tools [REMOVED]",
|
||||
@@ -3802,7 +3701,7 @@
|
||||
},
|
||||
{
|
||||
"author": "SlackinJack",
|
||||
"title": "distrifuser_comfyui [REMOVED]",
|
||||
"title": "distrifuser_comfyui [DEPRECATED]",
|
||||
"reference": "https://github.com/SlackinJack/distrifuser_comfyui",
|
||||
"files": [
|
||||
"https://github.com/SlackinJack/distrifuser_comfyui"
|
||||
@@ -3812,7 +3711,7 @@
|
||||
},
|
||||
{
|
||||
"author": "SlackinJack",
|
||||
"title": "asyncdiff_comfyui [REMOVED]",
|
||||
"title": "asyncdiff_comfyui [DEPRECATED]",
|
||||
"reference": "https://github.com/SlackinJack/asyncdiff_comfyui",
|
||||
"files": [
|
||||
"https://github.com/SlackinJack/asyncdiff_comfyui"
|
||||
@@ -3900,6 +3799,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This node generates a professional prompt from an input text for modern video AI models (e.g., Alibaba Wan 2.2) via the OpenRouter API."
|
||||
},
|
||||
{
|
||||
"author": "perilli",
|
||||
"title": "apw_nodes [REMOVED]",
|
||||
"reference": "https://github.com/alessandroperilli/APW_Nodes",
|
||||
"files": [
|
||||
"https://github.com/alessandroperilli/APW_Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node suite to augment the capabilities of the [a/AP Workflows for ComfyUI](https://perilli.com/ai/comfyui/)\nNOTE: See [a/Open Creative Studio Nodes](https://github.com/alessandroperilli/OCS_Nodes)"
|
||||
},
|
||||
{
|
||||
"author": "greengerong",
|
||||
"title": "ComfyUI-Lumina-Video [REMOVED]",
|
||||
@@ -3910,6 +3819,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a video generation plugin implementation for ComfyUI based on the Lumina Video model."
|
||||
},
|
||||
{
|
||||
"author": "SatadalAI",
|
||||
"title": "Combined Upscale Node for ComfyUI [REMOVED]",
|
||||
"reference": "https://github.com/SatadalAI/SATA_UtilityNode",
|
||||
"files": [
|
||||
"https://github.com/SatadalAI/SATA_UtilityNode"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Combined_Upscale is a custom ComfyUI node designed for high-quality image enhancement workflows. It intelligently combines model-based upscaling with efficient CPU-based resizing, offering granular control over output dimensions and quality. Ideal for asset pipelines, UI prototyping, and generative workflows.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "netroxin",
|
||||
"title": "Netro_wildcards [REMOVED]",
|
||||
@@ -4174,6 +4093,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of custom nodes for ComfyUI that provide various input and output capabilities."
|
||||
},
|
||||
{
|
||||
"author": "NicholasKao1029",
|
||||
"title": "comfyui-pixxio [REMOVED]",
|
||||
"reference": "https://github.com/NicholasKao1029/comfyui-pixxio",
|
||||
"files": [
|
||||
"https://github.com/NicholasKao1029/comfyui-pixxio"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Auto-Upload Image to Pixxio Collection, Load Image from Pixx.io"
|
||||
},
|
||||
{
|
||||
"author": "ComfyUI-Workflow",
|
||||
"title": "ComfyUI OpenAI Nodes [REMOVED]",
|
||||
@@ -4195,6 +4124,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a node to convert workflows to cyuai api available nodes."
|
||||
},
|
||||
{
|
||||
"author": "1H-hobit",
|
||||
"title": "ComfyUI_InternVL3 [REMOVED]",
|
||||
"reference": "https://github.com/1H-hobit/ComfyUI_InternVL3",
|
||||
"files": [
|
||||
"https://github.com/1H-hobit/ComfyUI_InternVL3"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI for [a/InternVL](https://github.com/OpenGVLab/InternVL)"
|
||||
},
|
||||
{
|
||||
"author": "spacepxl",
|
||||
"title": "ComfyUI-Florence-2 [DEPRECATED]",
|
||||
@@ -4401,6 +4340,17 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI extension for parsing Civitai PNG workflows and automatically downloading missing models"
|
||||
},
|
||||
{
|
||||
"author": "DriftJohnson",
|
||||
"title": "DJZ-Nodes [REMOVED]",
|
||||
"id": "DJZ-Nodes",
|
||||
"reference": "https://github.com/MushroomFleet/DJZ-Nodes",
|
||||
"files": [
|
||||
"https://github.com/MushroomFleet/DJZ-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "AspectSize and other nodes"
|
||||
},
|
||||
{
|
||||
"author": "DriftJohnson",
|
||||
"title": "KokoroTTS Node [REMOVED]",
|
||||
@@ -4564,6 +4514,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom nodes for loading and previewing media from URLs in ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "pureexe",
|
||||
"title": "DiffusionLight-ComfyUI [REMOVED]",
|
||||
"reference": "https://github.com/pureexe/DiffusionLight-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/pureexe/DiffusionLight-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "DiffusionLight (Turbo) implemented in ComfyUI"
|
||||
},
|
||||
{
|
||||
"author": "gondar-software",
|
||||
"title": "comfyui-custom-padding [REMOVED]",
|
||||
@@ -4716,7 +4676,7 @@
|
||||
},
|
||||
{
|
||||
"author": "Kur0butiMegane",
|
||||
"title": "Comfyui-StringUtils [REMOVED]",
|
||||
"title": "Comfyui-StringUtils [DEPRECATED]",
|
||||
"reference": "https://github.com/Kur0butiMegane/Comfyui-StringUtils",
|
||||
"files": [
|
||||
"https://github.com/Kur0butiMegane/Comfyui-StringUtils"
|
||||
@@ -5041,6 +5001,26 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: MPN Switch, MPN Reroute, PN Switch. This is a set of custom nodes for ComfyUI. Mainly focus on control switches."
|
||||
},
|
||||
{
|
||||
"author": "IfnotFr",
|
||||
"title": "ComfyUI-Connect [REMOVED]",
|
||||
"reference": "https://github.com/IfnotFr/ComfyUI-Connect",
|
||||
"files": [
|
||||
"https://github.com/IfnotFr/ComfyUI-Connect"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Transform your ComfyUI into a powerful API, exposing all your saved workflows as ready-to-use HTTP endpoints."
|
||||
},
|
||||
{
|
||||
"author": "KurtHokke",
|
||||
"title": "ComfyUI_KurtHokke-Nodes [REMOVED]",
|
||||
"reference": "https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes",
|
||||
"files": [
|
||||
"https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI_KurtHokke-Nodes"
|
||||
},
|
||||
{
|
||||
"author": "SpatialDeploy",
|
||||
"title": "ComfyUI-Voxels [REMOVED]",
|
||||
@@ -5185,7 +5165,7 @@
|
||||
},
|
||||
{
|
||||
"author": "Makki_Shizu",
|
||||
"title": "ComfyUI-SaveAnimatedGIF [REMOVED]",
|
||||
"title": "ComfyUI-SaveAnimatedGIF [DEPRECATED]",
|
||||
"id": "SaveAnimatedGIF",
|
||||
"reference": "https://github.com/MakkiShizu/ComfyUI-SaveAnimatedGIF",
|
||||
"files": [
|
||||
@@ -5501,7 +5481,7 @@
|
||||
},
|
||||
{
|
||||
"author": "42lux",
|
||||
"title": "ComfyUI-safety-checker [REMOVED]",
|
||||
"title": "ComfyUI-safety-checker [DEPRECATED]",
|
||||
"reference": "https://github.com/42lux/ComfyUI-safety-checker",
|
||||
"files": [
|
||||
"https://github.com/42lux/ComfyUI-safety-checker"
|
||||
@@ -5689,7 +5669,7 @@
|
||||
},
|
||||
{
|
||||
"author": "zmwv823",
|
||||
"title": "ComfyUI-Sana [REMOVED]",
|
||||
"title": "ComfyUI-Sana [DEPRECATED]",
|
||||
"reference": "https://github.com/zmwv823/ComfyUI-Sana",
|
||||
"files": [
|
||||
"https://github.com/zmwv823/ComfyUI-Sana"
|
||||
@@ -5730,7 +5710,7 @@
|
||||
},
|
||||
{
|
||||
"author": "huangyangke",
|
||||
"title": "ComfyUI-Kolors-IpadapterFaceId [REMOVED]",
|
||||
"title": "ComfyUI-Kolors-IpadapterFaceId [DEPRECATED]",
|
||||
"reference": "https://github.com/huangyangke/ComfyUI-Kolors-IpadapterFaceId",
|
||||
"files": [
|
||||
"https://github.com/huangyangke/ComfyUI-Kolors-IpadapterFaceId"
|
||||
@@ -5740,7 +5720,7 @@
|
||||
},
|
||||
{
|
||||
"author": "zmwv823",
|
||||
"title": "ComfyUI_Ctrlora [REMOVED]",
|
||||
"title": "ComfyUI_Ctrlora [DEPRECATED]",
|
||||
"reference": "https://github.com/zmwv823/ComfyUI_Ctrlora",
|
||||
"files": [
|
||||
"https://github.com/zmwv823/ComfyUI_Ctrlora"
|
||||
@@ -5761,7 +5741,7 @@
|
||||
},
|
||||
{
|
||||
"author": "DonBaronFactory",
|
||||
"title": "ComfyUI-Cre8it-Nodes [REMOVED]",
|
||||
"title": "ComfyUI-Cre8it-Nodes [DEPRECATED]",
|
||||
"reference": "https://github.com/DonBaronFactory/ComfyUI-Cre8it-Nodes",
|
||||
"files": [
|
||||
"https://github.com/DonBaronFactory/ComfyUI-Cre8it-Nodes"
|
||||
@@ -5791,7 +5771,7 @@
|
||||
},
|
||||
{
|
||||
"author": "palant",
|
||||
"title": "Extended Save Image for ComfyUI [REMOVED]",
|
||||
"title": "Extended Save Image for ComfyUI [DEPRECATED]",
|
||||
"reference": "https://github.com/palant/extended-saveimage-comfyui",
|
||||
"files": [
|
||||
"https://github.com/palant/extended-saveimage-comfyui"
|
||||
@@ -5810,6 +5790,17 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI Node - Hugging Face repositories GTP2 Prompt"
|
||||
},
|
||||
{
|
||||
"author": "yushan777",
|
||||
"title": "Y7 Nodes for ComfyUI [REMOVED]",
|
||||
"id": "y7nodes",
|
||||
"reference": "https://github.com/yushan777/ComfyUI-Y7Nodes",
|
||||
"files": [
|
||||
"https://github.com/yushan777/ComfyUI-Y7Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Count_Tokens_(Y7)"
|
||||
},
|
||||
{
|
||||
"author": "city96",
|
||||
"title": "SD-Advanced-Noise [DEPRECATED]",
|
||||
@@ -6061,7 +6052,7 @@
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-pkg39 [REMOVED]",
|
||||
"title": "comfyui-pkg39 [DEPRECATED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-pkg39",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-pkg39"
|
||||
@@ -6071,7 +6062,7 @@
|
||||
},
|
||||
{
|
||||
"author": "dnl13",
|
||||
"title": "ComfyUI-dnl13-seg [REMOVED]",
|
||||
"title": "ComfyUI-dnl13-seg [DEPRECATED]",
|
||||
"reference": "https://github.com/dnl13/ComfyUI-dnl13-seg",
|
||||
"files": [
|
||||
"https://github.com/dnl13/ComfyUI-dnl13-seg"
|
||||
@@ -6397,7 +6388,7 @@
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-text-pipe-39 [REMOVED]",
|
||||
"title": "comfyui-text-pipe-39 [DEPRECATED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-text-pipe-39",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-text-pipe-39"
|
||||
@@ -6507,7 +6498,7 @@
|
||||
},
|
||||
{
|
||||
"author": "ccvv804",
|
||||
"title": "ComfyUI StableCascade using diffusers for Low VRAM [REMOVED]",
|
||||
"title": "ComfyUI StableCascade using diffusers for Low VRAM [DEPRECATED]",
|
||||
"reference": "https://github.com/ccvv804/ComfyUI-DiffusersStableCascade-LowVRAM",
|
||||
"files": [
|
||||
"https://github.com/ccvv804/ComfyUI-DiffusersStableCascade-LowVRAM"
|
||||
|
||||
+34
-1317
File diff suppressed because it is too large
Load Diff
+780
-2336
File diff suppressed because it is too large
Load Diff
+3
-21
@@ -629,12 +629,6 @@ if os.path.exists(restore_snapshot_path):
|
||||
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||
|
||||
from comfy.cli_args import args
|
||||
|
||||
allow_flagged = default_conf.get('allow_flagged_nodepack_install', '').lower() == 'true'
|
||||
new_env['_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED'] = str(
|
||||
allow_flagged or manager_util.is_loopback_listener(args.listen)
|
||||
).lower()
|
||||
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
|
||||
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
|
||||
|
||||
@@ -683,18 +677,9 @@ def execute_lazy_install_script(repo_path, executable):
|
||||
process_wrap(install_cmd, repo_path, env=new_env)
|
||||
|
||||
|
||||
def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom_nodes_path, status=''):
|
||||
def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom_nodes_path):
|
||||
import uuid
|
||||
import shutil
|
||||
from comfy.cli_args import args
|
||||
|
||||
allow_flagged = default_conf.get('allow_flagged_nodepack_install', '').lower() == 'true'
|
||||
if not manager_util.is_cnr_install_allowed(status or 'NodeVersionStatusFlagged', allow_flagged, args.listen):
|
||||
if not status:
|
||||
logging.error("Cannot execute reserved CNR switch for '%s': stored Registry status is missing. Request the installation again so its current status can be checked.", target)
|
||||
else:
|
||||
logging.error(manager_util.FLAGGED_NODEPACK_INSTALL_GUIDANCE)
|
||||
return False
|
||||
|
||||
# 1. download
|
||||
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
|
||||
@@ -742,8 +727,6 @@ def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom
|
||||
with open(tracking_info_file, "w", encoding='utf-8') as file:
|
||||
file.write('\n'.join(list(extracted)))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
script_executed = False
|
||||
|
||||
@@ -799,9 +782,8 @@ def execute_startup_script():
|
||||
execute_lazy_install_script(script[0], script[2])
|
||||
|
||||
elif script[1] == "#LAZY-CNR-SWITCH-SCRIPT":
|
||||
status = script[8] if len(script) > 8 else ''
|
||||
if execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6], status):
|
||||
execute_lazy_install_script(script[3], script[7])
|
||||
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
|
||||
execute_lazy_install_script(script[3], script[7])
|
||||
|
||||
elif script[1] == "#LAZY-DELETE-NODEPACK":
|
||||
execute_lazy_delete(script[2])
|
||||
|
||||
+1
-1
@@ -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.42"
|
||||
version = "3.41"
|
||||
license = { file = "LICENSE.txt" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet", "packaging", "nh3>=0.3.7"]
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
"""Real ComfyUI HTTP/queue/install tests with a local CNR service and harmless ZIPs.
|
||||
|
||||
Run with E2E_ROOT pointing to the existing ComfyUI + venv fixture. Each server
|
||||
uses an isolated base directory; no installed nodes or user config are changed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import ast
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import zipfile
|
||||
from contextlib import contextmanager
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
E2E_ROOT = Path(os.environ.get('E2E_ROOT', '/nonexistent'))
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (E2E_ROOT / 'venv/bin/python').is_file(), reason='E2E_ROOT is required',
|
||||
)
|
||||
PACKS = ['fixture-active', 'fixture-flagged', 'fixture-pending', 'fixture-banned', 'fixture-latest', 'fixture-switch', 'fixture-reinstall', 'fixture-update', 'fixture-snapshot']
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def registry():
|
||||
calls = []
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
calls.append(parsed.path + ('?' + parsed.query if parsed.query else ''))
|
||||
parts = parsed.path.strip('/').split('/')
|
||||
version = parse_qs(parsed.query).get('version', ['2.0.0'])[0]
|
||||
node = parts[1] if len(parts) > 1 else ''
|
||||
base = f'http://127.0.0.1:{self.server.server_port}'
|
||||
|
||||
def metadata(node, version):
|
||||
status = {'1.0.0': 'Active', '1.1.0': 'Active', '2.0.0': 'Flagged', '3.0.0': 'Pending'}[version]
|
||||
return {'id': f'{node}-{version}', 'node_id': node, 'version': version,
|
||||
'status': 'NodeVersionStatus' + status, 'dependencies': [],
|
||||
'downloadUrl': f'{base}/zip/{node}/{version}'}
|
||||
|
||||
status = 200
|
||||
if parts[0] == 'zip':
|
||||
node, version = parts[1:]
|
||||
archive = io.BytesIO()
|
||||
with zipfile.ZipFile(archive, 'w') as z:
|
||||
z.writestr('__init__.py', 'NODE_CLASS_MAPPINGS = {}\n')
|
||||
z.writestr('pyproject.toml', f'[project]\nname = "{node}"\nversion = "{version}"\n')
|
||||
z.writestr('install.py', 'from pathlib import Path\n'
|
||||
f'Path("installed.txt").write_text("{version}")\n')
|
||||
body = archive.getvalue()
|
||||
elif parsed.path == '/nodes':
|
||||
body = json.dumps({'nodes': [
|
||||
{'id': name, 'name': name, 'description': '', 'status': 'NodeStatusActive',
|
||||
'repository': f'https://example.invalid/{name}',
|
||||
'publisher': {'id': 'fixture', 'name': 'Fixture'},
|
||||
'latest_version': metadata(name, '2.0.0')}
|
||||
for name in PACKS], 'totalPages': 1}).encode()
|
||||
elif len(parts) == 3 and parts[0] == 'nodes' and parts[2] == 'install':
|
||||
if node == 'fixture-banned':
|
||||
status, body = 404, b'{"message":"Not found"}'
|
||||
else:
|
||||
body = json.dumps(metadata(node, version)).encode()
|
||||
else:
|
||||
status, body = 404, b'{}'
|
||||
self.send_response(status)
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Content-Type', 'application/zip' if parts[0] == 'zip' else 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
yield SimpleNamespace(url=f'http://127.0.0.1:{server.server_port}', calls=calls)
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join()
|
||||
|
||||
|
||||
# Redirect only the external Registry in both ComfyUI and its restore subprocess.
|
||||
REGISTRY_BOOTSTRAP = '''
|
||||
import asyncio, json, os, sys, urllib.request
|
||||
sys.path.insert(0, os.environ['CM_E2E_MANAGER_GLOB'])
|
||||
import manager_core, cnr_utils, manager_util
|
||||
cnr_utils.base_url = os.environ['CM_E2E_REGISTRY']
|
||||
with urllib.request.urlopen(cnr_utils.base_url + '/nodes') as response:
|
||||
manager_util.save_to_cache(cnr_utils.base_url + '/nodes', json.load(response))
|
||||
asyncio.run(manager_core.unified_manager.reload('cache', dont_wait=False))
|
||||
manager_core.unified_manager.custom_node_map_cache[(manager_core.normalize_channel('default'), 'cache')] = manager_core.NormalizedKeyDict()
|
||||
'''
|
||||
BOOTSTRAP = '''
|
||||
import os, runpy, sys
|
||||
sys.argv = [os.environ['CM_E2E_MAIN'], *sys.argv[1:]]
|
||||
import comfy.options
|
||||
comfy.options.enable_args_parsing()
|
||||
''' + REGISTRY_BOOTSTRAP + "\nrunpy.run_path(sys.argv[0], run_name='__main__')\n"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def running_server(root, listen, override, registry, security='normal'):
|
||||
custom_nodes = root / 'custom_nodes'
|
||||
custom_nodes.mkdir(exist_ok=True)
|
||||
mount = custom_nodes / 'comfyui-manager'
|
||||
if not mount.exists():
|
||||
mount.symlink_to(REPO_ROOT, target_is_directory=True)
|
||||
config_dir = root / 'user/__manager'
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
(config_dir / 'config.ini').write_text(
|
||||
'[default]\nnetwork_mode = offline\nuse_uv = false\n'
|
||||
f'security_level = {security}\nallow_flagged_nodepack_install = {override}\n'
|
||||
)
|
||||
# cm-cli.py inherits this test-only IO redirection from its real parent.
|
||||
hook = root / 'sitecustomize.py'
|
||||
hook.write_text("import os, sys\nif os.path.basename(sys.argv[0]) == 'cm-cli.py':\n"
|
||||
" from comfy.cli_args import args\n"
|
||||
" args.base_directory = os.environ['CM_E2E_BASE']\n"
|
||||
+ '\n'.join(' ' + line for line in REGISTRY_BOOTSTRAP.splitlines()))
|
||||
with socket.socket() as sock:
|
||||
sock.bind(('127.0.0.1', 0))
|
||||
port = sock.getsockname()[1]
|
||||
base = f'http://127.0.0.1:{port}'
|
||||
env = dict(os.environ, PYTHONUNBUFFERED='1',
|
||||
PYTHONPATH=os.pathsep.join([str(root), str(E2E_ROOT / 'comfyui')]),
|
||||
COMFYUI_PATH=str(E2E_ROOT / 'comfyui'), COMFYUI_FOLDERS_BASE_PATH=str(root),
|
||||
CM_E2E_BASE=str(root), CM_E2E_MAIN=str(E2E_ROOT / 'comfyui/main.py'),
|
||||
CM_E2E_MANAGER_GLOB=str(REPO_ROOT / 'glob'), CM_E2E_REGISTRY=registry.url)
|
||||
command = [str(E2E_ROOT / 'venv/bin/python'), '-c', BOOTSTRAP, '--cpu',
|
||||
'--base-directory', str(root), '--listen', listen, '--port', str(port),
|
||||
'--database-url', f'sqlite:///{root / "comfyui.db"}']
|
||||
log_path = root / ('server-' + uuid.uuid4().hex + '.log')
|
||||
with log_path.open('w') as log:
|
||||
process = subprocess.Popen(command, env=env, cwd=E2E_ROOT / 'comfyui', stdout=log, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
deadline = time.monotonic() + 90
|
||||
while time.monotonic() < deadline:
|
||||
assert process.poll() is None, log_path.read_text()[-6000:]
|
||||
try:
|
||||
if requests.get(base + '/system_stats', timeout=1).status_code == 200:
|
||||
break
|
||||
except requests.RequestException:
|
||||
pass
|
||||
time.sleep(.2)
|
||||
else:
|
||||
pytest.fail(log_path.read_text()[-6000:])
|
||||
assert requests.get(base + '/manager/version', timeout=5).status_code == 200, log_path.read_text()[-6000:]
|
||||
assert 'PRESTARTUP FAILED' not in log_path.read_text(), log_path.read_text()
|
||||
yield SimpleNamespace(root=root, base=base, listen=listen, override=override,
|
||||
security=security, registry=registry, log=log_path, env=env)
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('operation', ['install', 'restore-snapshot'])
|
||||
def test_direct_cli_installs_flagged_without_server_options(tmp_path, registry, operation):
|
||||
with running_server(tmp_path, '0.0.0.0', False, registry) as server:
|
||||
env = server.env
|
||||
command = [str(E2E_ROOT / 'venv/bin/python'), str(REPO_ROOT / 'cm-cli.py'), operation]
|
||||
if operation == 'install':
|
||||
command += ['fixture-flagged@2.0.0', '--mode', 'cache']
|
||||
else:
|
||||
snapshot = tmp_path / 'direct-snapshot.json'
|
||||
snapshot.write_text(json.dumps({'cnr_custom_nodes': {'fixture-flagged': '2.0.0'},
|
||||
'git_custom_nodes': {}, 'file_custom_nodes': []}))
|
||||
command.append(str(snapshot))
|
||||
command += ['--user-directory', str(tmp_path / 'user')]
|
||||
before = len(registry.calls)
|
||||
|
||||
result = subprocess.run(command, env=env, cwd=E2E_ROOT / 'comfyui',
|
||||
capture_output=True, text=True, timeout=60)
|
||||
|
||||
output = result.stdout + result.stderr
|
||||
assert result.returncode == 0, output
|
||||
installed = tmp_path / 'custom_nodes/fixture-flagged'
|
||||
assert 'version = "2.0.0"' in (installed / 'pyproject.toml').read_text(), output
|
||||
if operation == 'install':
|
||||
assert (installed / 'installed.txt').read_text() == '2.0.0', output
|
||||
assert sum('/install' in call for call in registry.calls[before:]) == 1
|
||||
assert 'Installation of this flagged CNR version is blocked' not in output
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', params=[
|
||||
('127.0.0.1', False, 'normal'), ('0.0.0.0', False, 'normal'),
|
||||
('0.0.0.0', True, 'normal'), ('0.0.0.0', True, 'strong'),
|
||||
('127.0.0.1', False, 'strong'),
|
||||
], ids=lambda row: '-'.join(map(str, row)))
|
||||
def comfy_server(request, tmp_path_factory, registry):
|
||||
listen, override, security = request.param
|
||||
root = tmp_path_factory.mktemp('flagged-v3')
|
||||
with running_server(root, listen, override, registry, security) as server:
|
||||
yield server
|
||||
|
||||
|
||||
def install(server, node, version, operation='install'):
|
||||
async def request_and_wait():
|
||||
ui_id = uuid.uuid4().hex
|
||||
params = {'id': node, 'version': '1.0.0', 'selected_version': version,
|
||||
'mode': 'cache', 'channel': 'default', 'ui_id': ui_id,
|
||||
'repository': 'https://example.invalid/fixture'}
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=40)) as session:
|
||||
# Subscribe first: a fast worker can emit completion before POST returns.
|
||||
async with session.ws_connect(server.base + '/ws?clientId=' + ui_id) as ws:
|
||||
async with session.post(server.base + '/manager/queue/' + operation, json=params) as response:
|
||||
if response.status != 200:
|
||||
return {'http': response.status, 'message': await response.text()}
|
||||
async with session.post(server.base + '/manager/queue/start') as response:
|
||||
response.raise_for_status()
|
||||
while True:
|
||||
event = await ws.receive_json(timeout=30)
|
||||
data = event.get('data', {})
|
||||
if event.get('type') == 'cm-queue-status' and data.get('status') == 'done':
|
||||
if ui_id in data.get('nodepack_result', {}):
|
||||
return {'http': 200, 'message': data['nodepack_result'][ui_id]}
|
||||
return asyncio.run(request_and_wait())
|
||||
|
||||
|
||||
def assert_terminal_guidance(log):
|
||||
for detail in ('--listen 127.0.0.1', '--listen ::1', '0.0.0.0', '[default]',
|
||||
'allow_flagged_nodepack_install = true', 'config.ini',
|
||||
'trusted private network', 'Restart ComfyUI'):
|
||||
assert detail in log
|
||||
|
||||
|
||||
def assert_flagged_denial(result, log):
|
||||
assert result['message'] == ('This action is not allowed by the current security configuration. '
|
||||
'See the terminal for details.'), result
|
||||
assert_terminal_guidance(log)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('node,version,flagged', [
|
||||
('fixture-active', '1.0.0', False), ('fixture-flagged', '2.0.0', True),
|
||||
('fixture-pending', '3.0.0', False), ('fixture-banned', '1.0.0', False),
|
||||
('fixture-latest', 'latest', True),
|
||||
])
|
||||
def test_install_policy(comfy_server, node, version, flagged):
|
||||
server = comfy_server
|
||||
log_before = len(server.log.read_text())
|
||||
before = len(server.registry.calls)
|
||||
result = install(server, node, version)
|
||||
calls = server.registry.calls[before:]
|
||||
strong = server.security == 'strong'
|
||||
allowed = not strong and node != 'fixture-banned' and (
|
||||
not flagged or server.override or server.listen == '127.0.0.1')
|
||||
marker = server.root / 'custom_nodes' / node / 'installed.txt'
|
||||
assert marker.exists() is allowed, (result, server.log.read_text()[-6000:])
|
||||
assert sum('/install' in call for call in calls) == (0 if strong else 1), calls
|
||||
assert any(call.startswith('/zip/') for call in calls) is allowed
|
||||
if allowed:
|
||||
assert result['message'] == 'success', result
|
||||
assert marker.read_text() == ('2.0.0' if version == 'latest' else version)
|
||||
elif strong:
|
||||
assert result['http'] == 403, result
|
||||
elif flagged:
|
||||
assert_flagged_denial(result, server.log.read_text()[log_before:])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('operation', ['install', 'reinstall', 'update'])
|
||||
def test_existing_pack_policy(comfy_server, operation):
|
||||
server = comfy_server
|
||||
if server.security == 'strong':
|
||||
pytest.skip('Existing-pack transitions require an installed pack')
|
||||
node = {'install': 'fixture-switch', 'reinstall': 'fixture-reinstall', 'update': 'fixture-update'}[operation]
|
||||
assert install(server, node, '1.0.0')['message'] == 'success'
|
||||
marker = server.root / 'custom_nodes' / node / 'installed.txt'
|
||||
log_before = len(server.log.read_text())
|
||||
before = len(server.registry.calls)
|
||||
result = install(server, node, '2.0.0', operation)
|
||||
calls = server.registry.calls[before:]
|
||||
allowed = server.override or server.listen == '127.0.0.1'
|
||||
assert sum('/install' in call for call in calls) == 1, calls
|
||||
if allowed:
|
||||
assert result['message'] == 'success', result
|
||||
if operation == 'reinstall':
|
||||
assert marker.read_text() == '2.0.0', server.log.read_text()[-6000:]
|
||||
else:
|
||||
assert marker.read_text() == '1.0.0'
|
||||
assert 'NodeVersionStatusFlagged' in (server.root / 'user/__manager/startup-scripts/install-scripts.txt').read_text()
|
||||
else:
|
||||
assert_flagged_denial(result, server.log.read_text()[log_before:])
|
||||
assert marker.read_text() == '1.0.0'
|
||||
assert any(call.startswith('/zip/') for call in calls) is (allowed and operation == 'reinstall')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('restart_listen,restart_override,allowed', [
|
||||
('0.0.0.0', False, False), ('0.0.0.0', True, True), ('127.0.0.1', False, True),
|
||||
])
|
||||
@pytest.mark.parametrize('stored_status', [True, False])
|
||||
def test_restart_uses_current_policy(tmp_path, registry, restart_listen, restart_override, allowed, stored_status):
|
||||
with running_server(tmp_path, '127.0.0.1', False, registry) as server:
|
||||
assert install(server, 'fixture-switch', '1.0.0')['message'] == 'success'
|
||||
assert install(server, 'fixture-switch', '2.0.0')['message'] == 'success'
|
||||
if not stored_status:
|
||||
reservation = tmp_path / 'user/__manager/startup-scripts/install-scripts.txt'
|
||||
record = ast.literal_eval(reservation.read_text().strip())
|
||||
assert record[1] == '#LAZY-CNR-SWITCH-SCRIPT'
|
||||
assert len(record) == 9
|
||||
reservation.write_text(repr(record[:8]) + '\n')
|
||||
marker = tmp_path / 'custom_nodes/fixture-switch/installed.txt'
|
||||
original = (marker.read_bytes(), marker.stat().st_mtime_ns)
|
||||
before = len(registry.calls)
|
||||
with running_server(tmp_path, restart_listen, restart_override, registry) as server:
|
||||
calls = registry.calls[before:]
|
||||
assert not any('/install' in call for call in calls), calls
|
||||
assert any(call.startswith('/zip/') for call in calls) is allowed
|
||||
if allowed:
|
||||
assert marker.read_text() == '2.0.0', server.log.read_text()[-6000:]
|
||||
else:
|
||||
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
|
||||
if stored_status:
|
||||
assert_terminal_guidance(server.log.read_text())
|
||||
else:
|
||||
assert 'stored Registry status is missing' in server.log.read_text()
|
||||
assert 'Request the installation again' in server.log.read_text()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('surface,payload,flag', [
|
||||
('git_url', {'url': 'https://example.invalid/fixture'}, 'allow_git_url_install'),
|
||||
('pip', {'packages': 'fixture-do-not-install'}, 'allow_pip_install'),
|
||||
])
|
||||
def test_other_install_flags_remain_required(comfy_server, surface, payload, flag):
|
||||
server = comfy_server
|
||||
before = len(server.registry.calls)
|
||||
response = requests.post(server.base + '/customnode/install/' + surface, json=payload, timeout=10)
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {'error': flag}
|
||||
assert server.registry.calls[before:] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize('existing', [False, True])
|
||||
@pytest.mark.parametrize('version,listen,override,allowed', [
|
||||
('1.1.0', '0.0.0.0', False, True),
|
||||
('2.0.0', '0.0.0.0', False, False),
|
||||
('2.0.0', '0.0.0.0', True, True),
|
||||
('2.0.0', '127.0.0.1', False, True),
|
||||
])
|
||||
def test_scheduled_snapshot_inherits_parent_policy(tmp_path, registry, existing, version, listen, override, allowed):
|
||||
installed = tmp_path / 'custom_nodes/fixture-snapshot'
|
||||
marker = installed / 'installed.txt'
|
||||
reservation = tmp_path / 'user/__manager/startup-scripts/restore-snapshot.json'
|
||||
with running_server(tmp_path, '127.0.0.1', False, registry) as server:
|
||||
if existing:
|
||||
assert install(server, 'fixture-snapshot', '1.0.0')['message'] == 'success'
|
||||
assert marker.read_text() == '1.0.0'
|
||||
original = (marker.read_bytes(), marker.stat().st_mtime_ns)
|
||||
snapshot = tmp_path / 'user/__manager/snapshots/flagged-policy.json'
|
||||
snapshot.write_text(json.dumps({'cnr_custom_nodes': {'fixture-snapshot': version},
|
||||
'git_custom_nodes': {}, 'file_custom_nodes': []}))
|
||||
before = len(registry.calls)
|
||||
response = requests.post(server.base + '/snapshot/restore',
|
||||
json={'target': snapshot.stem}, timeout=10)
|
||||
assert response.status_code == 200, response.text
|
||||
assert reservation.read_bytes() == snapshot.read_bytes()
|
||||
assert registry.calls[before:] == []
|
||||
if existing:
|
||||
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
|
||||
else:
|
||||
assert not installed.exists()
|
||||
|
||||
before = len(registry.calls)
|
||||
with running_server(tmp_path, listen, override, registry) as server:
|
||||
log = server.log.read_text()
|
||||
calls = registry.calls[before:]
|
||||
assert 'Restore snapshot done.' in log, log[-6000:]
|
||||
assert 'Restore snapshot failed.' not in log, log[-6000:]
|
||||
assert 'Error in sitecustomize' not in log, log[-6000:]
|
||||
assert not reservation.exists()
|
||||
assert sum('/install' in call for call in calls) == 1, (calls, log[-6000:])
|
||||
assert any(call.startswith('/zip/') for call in calls) is allowed
|
||||
if allowed:
|
||||
assert f'version = "{version}"' in (installed / 'pyproject.toml').read_text(), log[-6000:]
|
||||
assert 'allow_flagged_nodepack_install' not in log
|
||||
else:
|
||||
assert_terminal_guidance(log)
|
||||
if existing:
|
||||
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
|
||||
assert '1.0.0' in (installed / 'pyproject.toml').read_text()
|
||||
else:
|
||||
assert not installed.exists()
|
||||
@@ -1,325 +0,0 @@
|
||||
"""Behavioral checks against real v3 modules in an isolated ComfyUI runtime."""
|
||||
|
||||
import pytest
|
||||
|
||||
from test_install_flags_config import _run_child
|
||||
|
||||
|
||||
PRELUDE = '''
|
||||
import asyncio, io, logging, pathlib, zipfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
import cnr_utils, manager_util
|
||||
from comfy.cli_args import args
|
||||
args.listen = '0.0.0.0'
|
||||
core = manager_core
|
||||
core.manager_funcs.is_flagged_install_allowed = lambda: False
|
||||
config = core.get_config()
|
||||
config['allow_flagged_nodepack_install'] = False
|
||||
nodes = pathlib.Path(tmp) / 'custom_nodes'
|
||||
nodes.mkdir()
|
||||
core.get_default_custom_nodes_path = lambda: str(nodes)
|
||||
manager = core.UnifiedManager()
|
||||
core.unified_manager = manager
|
||||
terminal = io.StringIO()
|
||||
logging.getLogger().addHandler(logging.StreamHandler(terminal))
|
||||
status = 'NodeVersionStatusFlagged'
|
||||
http = 200
|
||||
get = Mock(side_effect=lambda *a, **k: SimpleNamespace(status_code=http, json=lambda: {
|
||||
'id': 'version-id', 'node_id': 'fixture', 'version': '2.0.0', 'status': status,
|
||||
'downloadUrl': 'https://example.invalid/fixture.zip',
|
||||
}))
|
||||
cnr_utils.requests.get = get
|
||||
def download(url, directory, name):
|
||||
with zipfile.ZipFile(pathlib.Path(directory) / name, 'w') as archive:
|
||||
archive.writestr('__init__.py', 'VERSION = "2.0.0"')
|
||||
archive.writestr('pyproject.toml', '[project]\\nname = "fixture"\\nversion = "2.0.0"\\n')
|
||||
core.manager_downloader.download_url = Mock(side_effect=download)
|
||||
core.manager_downloader.basic_download_url = core.manager_downloader.download_url
|
||||
manager.execute_install_script = Mock(return_value=True)
|
||||
'''
|
||||
|
||||
|
||||
def run(body):
|
||||
stub = '''
|
||||
comfy = types.ModuleType('comfy')
|
||||
cli_args = types.ModuleType('comfy.cli_args')
|
||||
cli_args.args = types.SimpleNamespace(listen='127.0.0.1')
|
||||
sys.modules['comfy'] = comfy
|
||||
sys.modules['comfy.cli_args'] = cli_args
|
||||
'''
|
||||
return _run_child(stub + PRELUDE + body)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('permission,allowed', [(None, True), ('true', True), ('false', False)])
|
||||
def test_cli_install_uses_only_parent_permission(permission, allowed):
|
||||
data = run(f'''
|
||||
permission = {permission!r}
|
||||
os.environ.pop('_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED', None)
|
||||
if permission is not None:
|
||||
os.environ['_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED'] = permission
|
||||
core.manager_funcs = core.ManagerFuncs()
|
||||
args.listen = '127.0.0.1' if permission == 'false' else '0.0.0.0'
|
||||
config['allow_flagged_nodepack_install'] = permission == 'false'
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
print(json.dumps({{'ok': result.result, 'queries': get.call_count,
|
||||
'downloads': core.manager_downloader.download_url.call_count}}))
|
||||
''')
|
||||
assert data == {'ok': allowed, 'queries': 1, 'downloads': int(allowed)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('status,permission,allowed', [
|
||||
('Active', False, True), ('Pending', False, True),
|
||||
('Flagged', False, False), ('Flagged', True, True),
|
||||
])
|
||||
def test_install_policy_before_download(status, permission, allowed):
|
||||
data = run(f'''
|
||||
status = {'NodeVersionStatus' + status!r}
|
||||
core.manager_funcs.is_flagged_install_allowed = lambda: {permission!r}
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
print(json.dumps({{'ok': result.result, 'message': result.msg,
|
||||
'terminal': terminal.getvalue(),
|
||||
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count,
|
||||
'installed': (nodes / 'fixture/__init__.py').exists()}}))
|
||||
''')
|
||||
assert data['ok'] is allowed, data
|
||||
assert data['installed'] is allowed, data
|
||||
assert data['downloads'] == int(allowed), data
|
||||
assert data['queries'] == 1, data
|
||||
if not allowed:
|
||||
assert data['message'] == ('This action is not allowed by the current security configuration. '
|
||||
'See the terminal for details.')
|
||||
for detail in ('--listen 127.0.0.1', '--listen ::1', '0.0.0.0', '[default]',
|
||||
'allow_flagged_nodepack_install = true', 'config.ini',
|
||||
'trusted private network', 'Restart ComfyUI'):
|
||||
assert detail in data['terminal'], data
|
||||
else:
|
||||
assert 'allow_flagged_nodepack_install' not in data['terminal']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('http', [403, 404, 500])
|
||||
def test_override_does_not_bypass_registry_refusal(http):
|
||||
data = run(f'''
|
||||
http = {http}
|
||||
core.manager_funcs.is_flagged_install_allowed = lambda: True
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
print(json.dumps({{'ok': result.result, 'queries': get.call_count,
|
||||
'downloads': core.manager_downloader.download_url.call_count}}))
|
||||
''')
|
||||
assert data == {'ok': False, 'queries': 1, 'downloads': 0}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('operation', ['install', 'lazy', 'instant'])
|
||||
@pytest.mark.parametrize('return_postinstall', [False, True])
|
||||
def test_direct_cnr_execution_and_postinstall(operation, return_postinstall):
|
||||
data = run(f'''
|
||||
status = 'NodeVersionStatusActive'
|
||||
operation = {operation!r}
|
||||
return_postinstall = {return_postinstall!r}
|
||||
installed = nodes / 'fixture'
|
||||
if operation != 'install':
|
||||
installed.mkdir()
|
||||
(installed / '__init__.py').write_text('original')
|
||||
(installed / '.tracking').write_text('__init__.py')
|
||||
manager.active_nodes['fixture'] = ('1.0.0', str(installed))
|
||||
manager.reserve_cnr_switch = Mock(return_value=True)
|
||||
if operation == 'install':
|
||||
result = manager.cnr_install('fixture', instant_execution=True,
|
||||
no_deps=True, return_postinstall=return_postinstall)
|
||||
else:
|
||||
result = manager.cnr_switch_version('fixture', instant_execution=operation == 'instant',
|
||||
no_deps=True, return_postinstall=return_postinstall)
|
||||
assert result.result, result.msg
|
||||
assert result.target == ('2.0.0' if operation == 'instant' else None)
|
||||
effect = manager.reserve_cnr_switch if operation == 'lazy' else manager.execute_install_script
|
||||
before = effect.call_count
|
||||
if return_postinstall:
|
||||
assert result.postinstall()
|
||||
if operation == 'lazy':
|
||||
assert effect.call_args.args[1] == 'https://example.invalid/fixture.zip'
|
||||
assert effect.call_args.args[-2:] == (True, 'NodeVersionStatusActive')
|
||||
else:
|
||||
assert effect.call_args.kwargs == {{'instant_execution': True, 'no_deps': True}}
|
||||
print(json.dumps({{'queries': get.call_count,
|
||||
'downloads': core.manager_downloader.download_url.call_count,
|
||||
'before': before, 'after': effect.call_count,
|
||||
'content': (installed / '__init__.py').read_text()}}))
|
||||
''')
|
||||
assert data == {'queries': 1, 'downloads': int(operation != 'lazy'),
|
||||
'before': int(not return_postinstall), 'after': 1,
|
||||
'content': 'original' if operation == 'lazy' else 'VERSION = "2.0.0"'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('operation', ['install', 'reinstall', 'lazy', 'instant', 'snapshot'])
|
||||
def test_denial_preserves_installed_version(operation):
|
||||
data = run(f'''
|
||||
installed = nodes / 'fixture'
|
||||
installed.mkdir()
|
||||
(installed / '__init__.py').write_text('VERSION = "1.0.0"')
|
||||
(installed / '.tracking').write_text('__init__.py')
|
||||
manager.active_nodes['fixture'] = ('1.0.0', str(installed))
|
||||
manager.reserve_cnr_switch = Mock(return_value=True)
|
||||
operation = {operation!r}
|
||||
if operation == 'snapshot':
|
||||
manager.reload = AsyncMock()
|
||||
manager.get_custom_nodes = AsyncMock(return_value={{}})
|
||||
core.manager_util.restore_pip_snapshot = Mock()
|
||||
snapshot = pathlib.Path(tmp) / 'snapshot.json'
|
||||
snapshot.write_text(json.dumps({{'cnr_custom_nodes': {{'fixture': '2.0.0'}},
|
||||
'git_custom_nodes': {{}}, 'file_custom_nodes': []}}))
|
||||
asyncio.run(core.restore_snapshot(str(snapshot)))
|
||||
elif operation in ('lazy', 'instant'):
|
||||
result = manager.cnr_switch_version('fixture', '2.0.0', instant_execution=operation == 'instant')
|
||||
assert not result.result, result.msg
|
||||
elif operation == 'reinstall':
|
||||
result = asyncio.run(manager.reinstall_by_id('fixture', '2.0.0'))
|
||||
assert not result.result, result.msg
|
||||
else:
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
assert not result.result, result.msg
|
||||
print(json.dumps({{'files': (installed / '__init__.py').read_text(),
|
||||
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count,
|
||||
'reservations': manager.reserve_cnr_switch.call_count}}))
|
||||
''')
|
||||
assert data == {'files': 'VERSION = "1.0.0"', 'queries': 1, 'downloads': 0, 'reservations': 0}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('listen,allowed', [
|
||||
('127.0.0.1', True), ('::1', True), ('127.0.0.1,::1', True),
|
||||
('0.0.0.0', False), ('::', False), ('0.0.0.0,::', False),
|
||||
('192.168.1.2', False), ('127.0.0.1,0.0.0.0', False), ('', False),
|
||||
])
|
||||
def test_all_listener_addresses_must_be_loopback(listen, allowed):
|
||||
data = run(f'''
|
||||
print(json.dumps(manager_util.is_cnr_install_allowed('NodeVersionStatusFlagged', False, {listen!r})))
|
||||
''')
|
||||
assert data is allowed
|
||||
|
||||
|
||||
@pytest.mark.parametrize('addresses,allowed', [
|
||||
(['127.0.0.1', '::1'], True), (['127.0.0.1', '192.168.1.2'], False),
|
||||
([], False), (None, False),
|
||||
])
|
||||
def test_hostname_must_resolve_exclusively_to_loopback(addresses, allowed):
|
||||
data = run(f'''
|
||||
addresses = {addresses!r}
|
||||
if addresses is None:
|
||||
manager_util.socket.getaddrinfo = Mock(side_effect=OSError('unresolvable'))
|
||||
else:
|
||||
manager_util.socket.getaddrinfo = Mock(return_value=[
|
||||
(0, 0, 0, '', (ip, 0)) for ip in addresses])
|
||||
print(json.dumps(manager_util.is_cnr_install_allowed('NodeVersionStatusFlagged', False, 'host.test')))
|
||||
''')
|
||||
assert data is allowed
|
||||
|
||||
|
||||
@pytest.mark.parametrize('state', ['nightly', 'disabled'])
|
||||
def test_denial_preserves_enabled_state(state):
|
||||
data = run(f'''
|
||||
state = {state!r}
|
||||
installed = nodes / ('fixture' if state == 'nightly' else '.disabled/fixture@1_0_0')
|
||||
installed.mkdir(parents=True)
|
||||
(installed / '__init__.py').write_text('original')
|
||||
if state == 'nightly':
|
||||
manager.active_nodes['fixture'] = ('nightly', str(installed))
|
||||
else:
|
||||
manager.cnr_inactive_nodes['fixture'] = {{'1.0.0': str(installed)}}
|
||||
before = repr((manager.active_nodes, manager.cnr_inactive_nodes))
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
print(json.dumps({{'ok': result.result, 'original': (installed / '__init__.py').read_text(),
|
||||
'unchanged': before == repr((manager.active_nodes, manager.cnr_inactive_nodes)),
|
||||
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count}}))
|
||||
''')
|
||||
assert data == {'ok': False, 'original': 'original', 'unchanged': True, 'queries': 1, 'downloads': 0}
|
||||
|
||||
|
||||
def test_exact_installed_version_enables_without_registry_lookup():
|
||||
data = run('''
|
||||
installed = nodes / '.disabled/fixture@2_0_0'
|
||||
installed.mkdir(parents=True)
|
||||
(installed / '__init__.py').write_text('already installed')
|
||||
manager.cnr_inactive_nodes['fixture'] = {'2.0.0': str(installed)}
|
||||
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
|
||||
assert manager.active_nodes['fixture'] == ('2.0.0', str(nodes / 'fixture'))
|
||||
assert asyncio.run(manager.install_by_id('fixture', '2.0.0')).action == 'skip'
|
||||
print(json.dumps({'ok': result.result, 'old_path': installed.exists(),
|
||||
'content': (nodes / 'fixture/__init__.py').read_text(), 'queries': get.call_count,
|
||||
'downloads': core.manager_downloader.download_url.call_count,
|
||||
'scripts': manager.execute_install_script.call_count}))
|
||||
''')
|
||||
assert data == {'ok': True, 'old_path': False, 'content': 'already installed',
|
||||
'queries': 0, 'downloads': 0, 'scripts': 0}
|
||||
|
||||
|
||||
def test_failed_uninstall_stops_reinstall():
|
||||
data = run('''
|
||||
status = 'NodeVersionStatusActive'
|
||||
manager.unified_uninstall = Mock(return_value=core.ManagedResult('uninstall').fail('cannot remove'))
|
||||
result = asyncio.run(manager.reinstall_by_id('fixture', '2.0.0'))
|
||||
print(json.dumps({'ok': result.result, 'message': result.msg, 'queries': get.call_count,
|
||||
'downloads': core.manager_downloader.download_url.call_count}))
|
||||
''')
|
||||
assert data == {'ok': False, 'message': 'cannot remove', 'queries': 1, 'downloads': 0}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('version', ['nightly', 'unknown'])
|
||||
def test_git_reinstall_replaces_pack_and_reruns_script(version):
|
||||
data = run(f'''
|
||||
version = {version!r}
|
||||
installed = nodes / 'fixture'
|
||||
installed.mkdir()
|
||||
(installed / 'original.txt').write_text('original')
|
||||
repo_url = 'https://example.invalid/fixture'
|
||||
if version == 'nightly':
|
||||
manager.active_nodes['fixture'] = ('nightly', str(installed))
|
||||
else:
|
||||
manager.unknown_active_nodes['fixture'] = (repo_url, str(installed))
|
||||
manager.get_custom_nodes = AsyncMock(return_value={{
|
||||
'fixture': {{'repository': repo_url, 'files': [repo_url]}},
|
||||
}})
|
||||
get.side_effect = AssertionError('Git needs no CNR install query')
|
||||
manager.processed_install.add(str(installed / 'install.py'))
|
||||
manager.execute_install_script = core.UnifiedManager.execute_install_script.__get__(manager)
|
||||
core.try_install_script = Mock(return_value=True)
|
||||
def clone(url, path, **kwargs):
|
||||
assert url == repo_url
|
||||
path = pathlib.Path(path)
|
||||
assert not path.exists()
|
||||
path.mkdir()
|
||||
(path / 'replacement.txt').write_text('installed')
|
||||
(path / 'install.py').write_text('')
|
||||
assert manager.execute_install_script(url, str(path))
|
||||
return core.ManagedResult('install-git')
|
||||
manager.repo_install = Mock(side_effect=clone)
|
||||
result = asyncio.run(manager.reinstall_by_id('fixture', version))
|
||||
print(json.dumps({{'ok': result.result, 'original': (installed / 'original.txt').exists(),
|
||||
'replacement': (installed / 'replacement.txt').read_text(),
|
||||
'clones': manager.repo_install.call_count, 'scripts': core.try_install_script.call_count}}))
|
||||
''')
|
||||
assert data == {'ok': True, 'original': False, 'replacement': 'installed', 'clones': 1, 'scripts': 1}
|
||||
|
||||
|
||||
def test_config_defaults_and_manual_edits_survive_settings_save():
|
||||
data = _run_child('''
|
||||
write_ini('[default]\\nsecurity_level = normal\\n')
|
||||
first = fresh_read()['allow_flagged_nodepack_install']
|
||||
write_ini('[default]\\nsecurity_level = normal\\nallow_flagged_nodepack_install = true\\n')
|
||||
manager_core.get_config()['db_mode'] = 'local'
|
||||
manager_core.write_config()
|
||||
cached = manager_core.get_config()['allow_flagged_nodepack_install']
|
||||
fresh = fresh_read()['allow_flagged_nodepack_install']
|
||||
print(json.dumps([first, cached, fresh]))
|
||||
''')
|
||||
assert data == [False, False, True]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw,expected', [('TRUE', True), ('false', False), ('garbage', False)])
|
||||
def test_config_value_roundtrip(raw, expected):
|
||||
data = _run_child(f'''
|
||||
write_ini('[default]\\nallow_flagged_nodepack_install = {raw}\\n')
|
||||
loaded = fresh_read()['allow_flagged_nodepack_install']
|
||||
manager_core.get_config()['allow_flagged_nodepack_install'] = not loaded
|
||||
manager_core.write_config()
|
||||
print(json.dumps([loaded, fresh_read()['allow_flagged_nodepack_install']]))
|
||||
''')
|
||||
assert data == [expected, not expected]
|
||||
@@ -56,7 +56,7 @@ _WANTED_CONSTS = {
|
||||
_HANDLER_NAMES = {
|
||||
"install_custom_node_git_url", # S-A
|
||||
"install_custom_node_pip", # S-B
|
||||
"_queue_node_install", # S-C
|
||||
"install_custom_node", # S-C
|
||||
}
|
||||
|
||||
|
||||
@@ -435,14 +435,14 @@ class BindingProofTest(unittest.TestCase):
|
||||
for name, flag in (
|
||||
("install_custom_node_git_url", "allow_git_url_install"),
|
||||
("install_custom_node_pip", "allow_pip_install"),
|
||||
("_queue_node_install", "allow_git_url_install"),
|
||||
("install_custom_node", "allow_git_url_install"),
|
||||
):
|
||||
with self.subTest(handler=name):
|
||||
src = ast.unparse(HANDLERS[name])
|
||||
self.assertIn("is_dedicated_install_allowed(", src)
|
||||
self.assertIn(flag, src)
|
||||
self.assertIn("args.listen", src)
|
||||
sc_literals = self._ias_literal_calls(HANDLERS["_queue_node_install"])
|
||||
sc_literals = self._ias_literal_calls(HANDLERS["install_custom_node"])
|
||||
self.assertIn("middle", sc_literals, "entry gate must stay UNCHANGED")
|
||||
self.assertIn(None, sc_literals, "a variable-arg retained path must remain")
|
||||
|
||||
|
||||
@@ -299,8 +299,7 @@ class WriteConfigPersistenceTest(unittest.TestCase):
|
||||
"security_level": cp["default"].get("security_level", "<ABSENT>"),
|
||||
"allow_git_url_install": cp["default"].get(
|
||||
"allow_git_url_install", "<ABSENT>"),
|
||||
"allow_flagged_nodepack_install": cp["default"].get(
|
||||
"allow_flagged_nodepack_install", "<ABSENT>"),
|
||||
"key_count": len(cp["default"]),
|
||||
}))
|
||||
"""
|
||||
)
|
||||
@@ -311,8 +310,10 @@ class WriteConfigPersistenceTest(unittest.TestCase):
|
||||
"T5: the install flag must bootstrap secure-by-default",
|
||||
)
|
||||
self.assertEqual(
|
||||
"False", payload["allow_flagged_nodepack_install"],
|
||||
"T5: flagged installs must bootstrap with the override disabled",
|
||||
18, payload["key_count"],
|
||||
"T5: bootstrap wrote %d keys; write_config persists 18 "
|
||||
"(manager_core.py:1685-1704). A change to that count is a scope "
|
||||
"signal, not a nit." % payload["key_count"],
|
||||
)
|
||||
|
||||
def test_t6_persisted_key_is_not_reclobbered(self):
|
||||
|
||||
Reference in New Issue
Block a user