feat: native disable/enable fallback and package whitelist; bump to 1.8.0
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
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>
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user