6d433ba371
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
Disable/enable no longer require ComfyUI Manager: - New pack_fs.py moves packs in/out of custom_nodes/.disabled/ (no import, delete, or re-clone). Fallback for hand-cloned packs, loose single-file nodes, or when Manager is absent. enable strips the @version suffix so packs restore as clean, importable dir names. - Routes: native-disable, native-enable, disabled-packs. - Frontend routes each disable per-pack (Manager queue vs native move), and shows an Enable button on recoverable packs in the Uninstalled tier. The restart banner degrades to a manual-restart notice when no Manager exists. Whitelist (packages-only): a star toggle protects a pack — pulled into its own pinned group, no Disable button, skipped by the 7-day trial auto-disable. - New whitelist_packages table; whitelisted flag on package stats. - Routes: whitelist, whitelist/add, whitelist/remove. Tests: test_pack_fs.py, test_whitelist.py (60 passing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""Native enable/disable of custom-node packages, independent of ComfyUI Manager.
|
|
|
|
Disabling a pack is just a filesystem convention that ComfyUI core honors: a
|
|
directory (or ``*.py`` file) moved into ``custom_nodes/.disabled/`` is skipped at
|
|
boot, because core ignores any entry named ``.disabled`` / starting with a dot.
|
|
Manager uses the same convention. We only *move* files here — never import,
|
|
delete, or re-clone — so the operation is reversible and safe.
|
|
|
|
This is the fallback the extension uses when ComfyUI Manager is absent or does
|
|
not recognize a pack (a hand-cloned repo, an oddly-installed pack, or a loose
|
|
single-file node). A restart is required for ComfyUI to pick up the change,
|
|
exactly as with a Manager disable.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# find_disabled_pack_path already knows how to locate a pack under .disabled/,
|
|
# including Manager's @version suffix. Import it in a way that works both as a
|
|
# package (runtime) and as a top-level module (tests add the root to sys.path).
|
|
try:
|
|
from .node_introspect import find_disabled_pack_path
|
|
except ImportError: # pragma: no cover - exercised via top-level import in tests
|
|
from node_introspect import find_disabled_pack_path
|
|
|
|
|
|
def _custom_node_roots():
|
|
import folder_paths
|
|
|
|
return [os.path.normpath(r) for r in folder_paths.get_folder_paths("custom_nodes")]
|
|
|
|
|
|
def _valid_name(pack_name):
|
|
"""Reject path-y input; pack names are plain directory/file names."""
|
|
return bool(pack_name) and not any(c in pack_name for c in ("/", "\\")) and ".." not in pack_name
|
|
|
|
|
|
def list_disabled_packs():
|
|
"""Return clean names of packs sitting in any custom_nodes/.disabled/ dir.
|
|
|
|
Strips a Manager ``@version`` suffix and a ``.py`` extension so the names
|
|
match package names as reported in the stats (and accepted by
|
|
``enable_pack_native``).
|
|
"""
|
|
names = set()
|
|
for root in _custom_node_roots():
|
|
ddir = os.path.join(root, ".disabled")
|
|
try:
|
|
entries = os.listdir(ddir)
|
|
except Exception:
|
|
continue
|
|
for e in entries:
|
|
if e.startswith("."):
|
|
continue
|
|
base = e.split("@", 1)[0]
|
|
stem = base[:-3] if base.endswith(".py") else base
|
|
if stem:
|
|
names.add(stem)
|
|
return names
|
|
|
|
|
|
def find_active_pack_path(pack_name):
|
|
"""Locate an installed (active) pack directly under a custom_nodes root.
|
|
|
|
Returns an absolute path to the pack directory or single ``.py`` file, or
|
|
None. Matches case-insensitively and tolerates a Manager ``@version`` suffix.
|
|
Only real packs (a directory, or a ``.py`` file) qualify.
|
|
"""
|
|
if not _valid_name(pack_name):
|
|
return None
|
|
target = pack_name.lower()
|
|
for root in _custom_node_roots():
|
|
try:
|
|
entries = os.listdir(root)
|
|
except Exception:
|
|
continue
|
|
for e in entries:
|
|
if e.startswith("."):
|
|
continue
|
|
base = e.split("@", 1)[0]
|
|
stem = base[:-3] if base.endswith(".py") else base
|
|
if target not in (stem.lower(), base.lower(), e.lower()):
|
|
continue
|
|
full = os.path.join(root, e)
|
|
if os.path.isdir(full) or (os.path.isfile(full) and full.endswith(".py")):
|
|
return os.path.normpath(full)
|
|
return None
|
|
|
|
|
|
def disable_pack_native(pack_name):
|
|
"""Move an active pack into ``custom_nodes/.disabled/``.
|
|
|
|
Returns ``(ok, message)``. Never raises for expected conditions (pack not
|
|
found, collision, permission error); those come back as ``(False, reason)``.
|
|
"""
|
|
src = find_active_pack_path(pack_name)
|
|
if not src:
|
|
return False, "pack not found on disk"
|
|
root = os.path.dirname(src)
|
|
ddir = os.path.join(root, ".disabled")
|
|
dest = os.path.join(ddir, os.path.basename(src))
|
|
if os.path.exists(dest):
|
|
return False, "a disabled copy already exists at " + dest
|
|
try:
|
|
os.makedirs(ddir, exist_ok=True)
|
|
shutil.move(src, dest)
|
|
except Exception as e:
|
|
logger.warning("nodes-stats: native disable failed for %s", pack_name, exc_info=True)
|
|
return False, str(e)
|
|
return True, "disabled"
|
|
|
|
|
|
def enable_pack_native(pack_name):
|
|
"""Move a pack from ``custom_nodes/.disabled/`` back to its root.
|
|
|
|
Returns ``(ok, message)``. Drops any Manager ``@version`` suffix from the
|
|
destination so the pack lands as a clean, importable directory name — the
|
|
same shape Manager itself restores on enable (``ComfyMath@nightly`` ->
|
|
``ComfyMath``). A single ``.py`` file keeps its extension.
|
|
"""
|
|
src = find_disabled_pack_path(pack_name)
|
|
if not src:
|
|
return False, "disabled pack not found"
|
|
ddir = os.path.dirname(src) # .../custom_nodes/.disabled
|
|
root = os.path.dirname(ddir) # .../custom_nodes
|
|
dest_name = os.path.basename(src).split("@", 1)[0]
|
|
dest = os.path.join(root, dest_name)
|
|
if os.path.exists(dest):
|
|
return False, "an active copy already exists at " + dest
|
|
try:
|
|
shutil.move(src, dest)
|
|
except Exception as e:
|
|
logger.warning("nodes-stats: native enable failed for %s", pack_name, exc_info=True)
|
|
return False, str(e)
|
|
return True, "enabled"
|